diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 7e906c8..2318f17 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,6 +1,7 @@
import { Component, HostListener, OnInit } from '@angular/core';
import { MultiWindowService, Message, KnownAppWindow } from 'ngx-multi-window';
import { NameGeneratorService } from './providers/name-generator.service';
+import {delay} from "rxjs";
@Component({
selector: 'app-root',
@@ -24,6 +25,11 @@ export class AppComponent implements OnInit {
constructor(private multiWindowService: MultiWindowService, private nameGenerator: NameGeneratorService) {
}
+ public pause(milliseconds) {
+ var dt = new Date();
+ while ((new Date().getTime()) - dt.getTime() <= milliseconds) { /* Do nothing */ }
+ }
+
ngOnInit(): void {
this.ownId = this.multiWindowService.id;
this.ownName = this.multiWindowService.name;
@@ -34,14 +40,23 @@ export class AppComponent implements OnInit {
}
this.newName = this.ownName;
this.windows = this.multiWindowService.getKnownWindows();
-
- this.multiWindowService.onMessage().subscribe((value: Message) => {
- this.logs.unshift('Received a message from ' + value.senderId + ': ' + value.data);
- });
+ if (this.multiWindowService.getKnownWindows().length > 0) {
+ this.multiWindowService.onMessage().subscribe((value: Message) => {
+ if (value.senderId != this.ownId) {
+ this.logs.unshift('Received a message from ' + value.senderId + ': ' + value.data);
+ }
+ });
+ }
this.multiWindowService.onWindows().subscribe(knownWindows => this.windows = knownWindows);
}
+ public sendTonsOfMessages(recipientId: string, message: string) {
+ for (let i = 0; i < 5000; i++) {
+ this.sendMessage(recipientId, message);
+ }
+ }
+
public sendMessage(recipientId: string, message: string) {
if (recipientId === this.ownId) {
// Catch sending messages to itself. Trying to do so throws an error from multiWindowService.sendMessage()
From 2476b0f81ee439b9a28653885a134d5885b081f7 Mon Sep 17 00:00:00 2001
From: Jared
Date: Thu, 1 Jun 2023 21:53:25 -0400
Subject: [PATCH 2/9] Start new project off of this old code and make use of
BroadcastChannel API rather than using storage for passing messages...
---
README.md | 5 +-
.../src/lib/providers/multi-window.service.ts | 501 +++---------------
.../src/lib/providers/storage.service.ts | 168 ------
.../lib/types/window-save-strategy.enum.ts | 23 -
.../src/lib/types/window.type.ts | 23 +-
src/app/app.component.html | 3 -
src/app/app.component.ts | 70 +--
7 files changed, 95 insertions(+), 698 deletions(-)
delete mode 100644 projects/ngx-multi-window/src/lib/providers/storage.service.ts
delete mode 100644 projects/ngx-multi-window/src/lib/types/window-save-strategy.enum.ts
diff --git a/README.md b/README.md
index 6e627a5..83a397e 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
-# ngx-multi-window [](https://www.npmjs.com/package/ngx-multi-window) [](http://opensource.org/licenses/MIT)
+# ngx-multi-window-communication [](https://www.npmjs.com/package/ngx-multi-window) [](http://opensource.org/licenses/MIT)
-Pull-based cross-window communication for multi-window angular applications
+Cross-window communication for multi-window angular applications
[](http://makeapullrequest.com)
-[](https://www.codacy.com/app/sebastian-fuss/ngx-multi-window?utm_source=github.com&utm_medium=referral&utm_content=Nolanus/ngx-multi-window&utm_campaign=Badge_Grade)
## Features
diff --git a/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts b/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
index 149ca2b..e408138 100644
--- a/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
+++ b/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
@@ -1,119 +1,57 @@
-import { Inject, Injectable, Optional } from '@angular/core';
+import {Inject, Injectable, OnDestroy, Optional} from '@angular/core';
import { Location } from '@angular/common';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
-import { ignoreElements } from 'rxjs/operators';
-import { StorageService } from './storage.service';
import { defaultMultiWindowConfig, NGXMW_CONFIG } from './config.provider';
import { MultiWindowConfig } from '../types/multi-window.config';
-import { AppWindow, KnownAppWindow, WindowData } from '../types/window.type';
-import { Message, MessageTemplate, MessageType } from '../types/message.type';
+import { KnownAppWindow } from '../types/window.type';
+import { Message } from '../types/message.type';
import { WindowRef } from '../providers/window.provider';
-import { WindowSaveStrategy } from '../types/window-save-strategy.enum';
@Injectable({
providedIn: 'root',
})
-export class MultiWindowService {
+export class MultiWindowService implements OnDestroy {
private config: MultiWindowConfig;
- private myWindow: WindowData;
+ private myWindow: KnownAppWindow;
- private heartbeatId = null;
- private windowScanId = null;
+ private knownWindows: {[broadcastId: string]: {[windowId: string]: {}}} = {};
- private knownWindows: KnownAppWindow[] = [];
+ private knownBroadcasters: {[broadcastId: string]: BroadcastChannel} = {};
- /**
- * A hash that keeps track of subjects for all send messages
- */
- private messageTracker: { [key: string]: Subject } = {};
-
- /**
- * A copy of the outbox that is regularly written to the local storage
- */
- private outboxCache: { [key: string]: Message } = {};
+ private knownListeners: string[] = [];
/**
- * A subject to subscribe to in order to get notified about messages send to this window
+ * A subject to subscribe to in order to get notified about messages sent to this window
*/
- private messageSubject: Subject = new Subject();
+ private messageSubjects: Subject[] = [];
/**
* A subject to subscribe to in order to get notified about all known windows
*/
- private windowSubject: Subject = new BehaviorSubject(this.knownWindows);
+ //private windowSubject: Subject = new BehaviorSubject(this.knownWindows);
- private static generateId(): string {
+ private generateId(): string {
return new Date().getTime().toString(36).substr(-4) + Math.random().toString(36).substr(2, 9);
}
- private tryMatchWindowKey(source: string): string {
- const nameRegex = new RegExp(
- // Escape any potential regex-specific chars in the keyPrefix which may be changed by the dev
- this.config.keyPrefix.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + 'w_([a-z0-9]+)'
- );
- const match = source.match(nameRegex);
- if (match !== null) {
- return match[1];
- }
-
- return null;
- }
-
- private generatePayloadKey({messageId}: Message): string {
- return this.config.keyPrefix + 'payload_' + messageId;
- }
-
- private generateWindowKey(windowId: string): string {
- return this.config.keyPrefix + 'w_' + windowId;
- }
-
- private isWindowKey(key: string): boolean {
- return key.indexOf(this.config.keyPrefix + 'w_') === 0;
- }
-
- constructor(@Inject(NGXMW_CONFIG) customConfig: MultiWindowConfig, @Optional() private location: Location,
- private storageService: StorageService, private windowRef: WindowRef) {
+ constructor(@Inject(NGXMW_CONFIG) customConfig: MultiWindowConfig, @Optional() private location: Location, private windowRef: WindowRef) {
this.config = {...defaultMultiWindowConfig, ...customConfig};
-
- let windowId: string;
-
- if (this.location) {
- // Try to extract the new window name from the location path
- windowId = this.tryMatchWindowKey(this.location.path(true));
- }
- // Only check the name save strategy if no id has been extracted from the path already
- if (!windowId && this.config.windowSaveStrategy !== WindowSaveStrategy.NONE) {
- // There might be window data stored in the window.name property, try restoring it
- const storedWindowData = this.windowRef.nativeWindow.name;
-
- if (this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_BACKUP) {
- // There should be a JSON string in the window.name, try parsing it and set the values
- try {
- const storedJsonData = JSON.parse(storedWindowData);
- windowId = storedJsonData.ngxmw_id;
- this.windowRef.nativeWindow.name = storedJsonData.backup;
- } catch (ex) { // Swallow JSON parsing exceptions, as we can't handle them anyway
- }
- } else {
- windowId = this.tryMatchWindowKey(storedWindowData);
- if (this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_WHEN_EMPTY) {
- this.windowRef.nativeWindow.name = '';
- }
- }
- }
- this.init(windowId);
- this.start();
}
- get name(): string {
- return this.myWindow.name;
+ public ngOnDestroy() {
+ this.handleServiceDestroyed();
+ this.closeAllListeners();
}
- set name(value: string) {
- this.myWindow.name = value;
+ public getKnownBroadcasters(): BroadcastChannel[] {
+ let knownBroadcasters: BroadcastChannel[] = [];
+ for (const bcId of Object.keys(this.knownBroadcasters)) {
+ knownBroadcasters.push(this.knownBroadcasters[bcId]);
+ }
+ return knownBroadcasters;
}
get id(): string {
@@ -125,8 +63,64 @@ export class MultiWindowService {
* After a message has been emitted by this observable it is marked as read, so the sending window
* gets informed about successful delivery.
*/
- public onMessage(): Observable {
- return this.messageSubject.asObservable();
+ public getBroadcastChannel(broadcastChannelId: string): BroadcastChannel {
+ for (const bcId of Object.keys(this.knownBroadcasters)) {
+ const bc = this.knownBroadcasters[bcId];
+ if (bcId == broadcastChannelId)
+ return bc;
+ }
+ const bc = new BroadcastChannel(broadcastChannelId);
+ this.knownBroadcasters[bc.name] = bc;
+ this.messageSubjects[bc.name] = new Subject();
+ return bc;
+ }
+
+ public listen(broadcastChannelId: string): MultiWindowService {
+ const bc = this.getBroadcastChannel(broadcastChannelId);
+ if (this.knownListeners[bc.name] == undefined) {
+ bc.onmessage = (message: any) => {
+ if (message.type == 'WINDOW_KILLED') {
+ // We want to handle the window killed here and make sure we remove it as a known window
+ delete this.knownWindows[bc.name][message.senderId];
+ return;
+ } else
+ if (message.type == 'WINDOW_CREATED') {
+ this.knownWindows[bc.name][message.senderId] = {};
+ return;
+ }
+ this.messageSubjects[bc.name].next(message as Message);
+ }
+ }
+ // When we register a new listener, we want to tell all the windows listening that we have a new known window to add to their list...
+ this.sendMessageToAll({
+ type: 'WINDOW_CREATED',
+ senderId: this.id
+ })
+ return this;
+ }
+
+ public closeListener(broadcastChannelId: string) {
+ if (this.knownListeners[broadcastChannelId] != undefined) {
+ this.getBroadcastChannel(broadcastChannelId).close();
+ delete this.knownListeners[broadcastChannelId];
+ }
+ }
+ public closeAllListeners() {
+ for (const bcId of this.knownListeners) {
+ this.getBroadcastChannel(bcId).close();
+ }
+ this.knownListeners = [];
+ }
+ private handleServiceDestroyed() {
+ this.sendMessageToAll({
+ type: "WINDOW_KILLED",
+ senderId: this.id
+ });
+ this.closeAllListeners();
+ }
+
+ public onMessage(broadcastChannelId: string): Observable {
+ return this.messageSubjects[broadcastChannelId].asObservable();
}
/**
@@ -142,9 +136,11 @@ export class MultiWindowService {
* @see {@link MultiWindowConfig#newWindowScan}
* @returns
*/
+ /** /
public onWindows(): Observable {
return this.windowSubject.asObservable();
}
+ /* */
/**
* Get the latest list of known windows.
@@ -156,335 +152,6 @@ export class MultiWindowService {
return this.knownWindows;
}
- /**
- * Start so that the current instance of the angular app/service/tab participates in the cross window communication.
- * It starts the interval-based checking for messages and updating the heartbeat.
- *
- * Note: There should be no need to call this method in production apps. It will automatically be called internally
- * during service construction (see {@link MultiWindowService} constructor)
- */
- public start(): void {
- if (!this.heartbeatId) {
- this.heartbeatId = setInterval(this.heartbeat, this.config.heartbeat);
- }
- if (!this.windowScanId) {
- this.windowScanId = setInterval(this.scanForWindows, this.config.newWindowScan);
- }
- }
-
- /**
- * Stop the current instance of the angular app/service/tab from participating in the cross window communication.
- * It stops the interval-based checking for messages and updating the heartbeat.
- *
- * Note: There should be no need to call this method in production apps.
- */
- public stop(): void {
- if (this.heartbeatId) {
- clearInterval(this.heartbeatId);
- this.heartbeatId = null;
- }
- if (this.windowScanId) {
- clearInterval(this.windowScanId);
- this.windowScanId = null;
- }
- }
-
- /**
- * Remove the current window representation from the localstorage.
- *
- * Note: Unless you {@link stop}ped the service the window representation will be
- * recreated after {@link MultiWindowConfig#heartbeat} milliseconds
- */
- public clear(): void {
- this.storageService.removeLocalItem(this.generateWindowKey(this.myWindow.id));
- }
-
- /**
- * Send a message to another window.
- *
- * @param recipientId The ID of the recipient window.
- * @param event Custom identifier to distinguish certain events
- * @param data Custom data to contain the data for the event
- * @param payload Further data to be passed to the recipient window via a separate entry in the localstorage
- * @returns An observable that emits the messageId once the message has been put into
- * the current windows outbox. It completes on successful delivery and fails if the delivery times out.
- */
- public sendMessage(recipientId: string, event: string, data: any, payload?: any): Observable {
- const messageId = this.pushToOutbox({
- recipientId,
- type: MessageType.MESSAGE,
- event,
- data,
- payload,
- });
- this.messageTracker[messageId] = new Subject();
-
- return this.messageTracker[messageId].asObservable();
- }
-
- /**
- * Create a new window and get informed once it has been created.
- *
- * Note: The actual window "creation" of redirection needs to be performed by
- * the library user. This method only returns helpful information on how to
- * do that
- *
- * The returned object contains three properties:
- *
- * windowId: An id generated to be assigned to the newly created window
- *
- * urlString: This string must be included in the url the new window loads.
- * It does not matter whether it is in the path, query or hash part
- *
- * created: An observable that will complete after new window was opened and was able
- * to receive a message. If the window does not start consuming messages within
- * {@link MultiWindowConfig#messageTimeout}, this observable will fail, although
- * the window might become present after that. The Observable will never emit any elements.
- */
- public newWindow(): { windowId: string, urlString: string, created: Observable } {
- if (this.location === null) {
- // Reading information from the URL is only possible with the Location provider. If
- // this window does not have one, another one will have none as well and thus would
- // not be able to read its new windowId from the url path
- throw new Error('No Location Provider present');
- }
- const newWindowId = MultiWindowService.generateId();
-
- const messageId = this.pushToOutbox({
- recipientId: newWindowId,
- type: MessageType.PING,
- event: undefined,
- data: undefined,
- payload: undefined,
- });
-
- this.messageTracker[messageId] = new Subject();
-
- return {
- windowId: newWindowId,
- urlString: this.generateWindowKey(newWindowId),
- created: this.messageTracker[messageId].pipe(ignoreElements()),
- };
- }
-
- public saveWindow(): void {
- if (this.config.windowSaveStrategy !== WindowSaveStrategy.NONE) {
- const windowId = this.generateWindowKey(this.id);
- if ((this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_WHEN_EMPTY && !this.windowRef.nativeWindow.name)
- || this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_FORCE) {
- this.windowRef.nativeWindow.name = windowId;
- } else if (this.config.windowSaveStrategy === WindowSaveStrategy.SAVE_BACKUP) {
- this.windowRef.nativeWindow.name = JSON.stringify({ngxmw_id: windowId, backup: this.windowRef.nativeWindow.name});
- }
- }
- }
-
- private init(windowId?: string): void {
- const windowKey = windowId
- ? this.generateWindowKey(windowId)
- : this.storageService.getWindowName();
- let windowData: WindowData | null = null;
- if (windowKey && this.isWindowKey(windowKey)) {
- windowData = this.storageService.getLocalObject(windowKey);
- }
-
- if (windowData !== null) {
- // Restore window information from storage
- this.myWindow = {
- id: windowData.id,
- name: windowData.name,
- heartbeat: windowData.heartbeat,
- };
- } else {
- const myWindowId = windowId || MultiWindowService.generateId();
- this.myWindow = {
- id: myWindowId,
- name: 'AppWindow ' + myWindowId,
- heartbeat: -1,
- };
- }
-
- this.storageService.setWindowName(windowKey);
-
- // Scan for already existing windows
- this.scanForWindows();
-
- // Trigger heartbeat for the first time
- this.heartbeat();
- }
-
- private pushToOutbox({recipientId, type, event, data, payload}: MessageTemplate,
- messageId: string = MultiWindowService.generateId()): string {
- if (recipientId === this.id) {
- throw new Error('Cannot send messages to self');
- }
- this.outboxCache[messageId] = {
- messageId,
- recipientId,
- senderId: this.myWindow.id,
- sendTime: new Date().getTime(),
- type,
- event,
- data,
- payload,
- send: false,
- };
-
- return messageId;
- }
-
- private heartbeat = () => {
- const now = new Date().getTime();
- // Check whether there are new messages for the current window in the other window's outboxes
-
- // Store the ids of all messages we receive in this iteration
- const receivedMessages: string[] = [];
-
- this.knownWindows.forEach(windowData => {
- // Load the window from the localstorage
- const appWindow = this.storageService.getLocalObject(this.generateWindowKey(windowData.id));
-
- if (appWindow === null) {
- // No window found, it possibly got closed/removed since our last scanForWindow
- return;
- }
-
- if (appWindow.id === this.myWindow.id) {
- // Ignore messages from myself (not done using Array.filter to reduce array iterations)
- // but check for proper last heartbeat time
- if (this.myWindow.heartbeat !== -1 && this.myWindow.heartbeat !== appWindow.heartbeat) {
- // The heartbeat value in the localstorage is a different one than the one we wrote into localstorage
- // during our last heartbeat. There are probably two app windows
- // using the same window id => change the current windows id
- this.myWindow.id = MultiWindowService.generateId();
- // eslint-disable-next-line no-console
- console.warn('Window ' + appWindow.id + ' detected that there is probably another instance with' +
- ' this id, changed id to ' + this.myWindow.id);
- this.storageService.setWindowName(this.generateWindowKey(this.myWindow.id));
- }
-
- return;
- }
-
- if (now - appWindow.heartbeat > this.config.windowTimeout) {
- // The window seems to be dead, remove the entry from the localstorage
- this.storageService.removeLocalItem(this.generateWindowKey(appWindow.id));
- }
-
- // Update the windows name and heartbeat value in the list of known windows (that's what we iterate over)
- windowData.name = appWindow.name;
- windowData.heartbeat = appWindow.heartbeat;
-
- if (appWindow.messages && appWindow.messages.length > 0) {
- // This other window has messages, so iterate over the messages the other window has
- appWindow.messages.forEach(message => {
- if (message.recipientId !== this.myWindow.id) {
- // The message is not targeted to the current window
- return;
- }
-
- if (message.type === MessageType.MESSAGE_RECEIVED) {
- // It's a message to inform the current window that a previously sent message from the
- // current window has been processed by the recipient window
-
- // Trigger the observable to complete and then remove it
- if (this.messageTracker[message.messageId]) {
- this.messageTracker[message.messageId].complete();
- }
- delete this.messageTracker[message.messageId];
-
- // Remove the message from the outbox, as the transfer is complete
- delete this.outboxCache[message.messageId];
- } else {
- receivedMessages.push(message.messageId);
- // Check whether we already processed that message. If that's the case we've got a 'message_received'
- // confirmation in our own outbox.
-
- if (!(this.outboxCache[message.messageId] &&
- this.outboxCache[message.messageId].type === MessageType.MESSAGE_RECEIVED)) {
- // We did not process that message
-
- // Create a new message for the message sender in the current window's
- // outbox that the message has been processed (reuse the message id for that)
- this.pushToOutbox({
- recipientId: message.senderId,
- type: MessageType.MESSAGE_RECEIVED,
- event: undefined,
- }, message.messageId);
-
- // Process it locally, unless it's just a PING message
- if (message.type !== MessageType.PING) {
- if (message.payload === true) {
- // The message has a separate payload
- const payloadKey = this.generatePayloadKey(message);
- message.payload = this.storageService.getLocalObject(payloadKey);
- this.storageService.removeLocalItem(payloadKey);
- }
- this.messageSubject.next(message);
- }
- }
- }
- });
- }
- });
-
- // Iterate over the outbox to clean it up, process timeouts and payloads
- Object.keys(this.outboxCache).forEach(messageId => {
- const message = this.outboxCache[messageId];
- if (message.type === MessageType.MESSAGE_RECEIVED && !receivedMessages.some(msgId => msgId === messageId)) {
- // It's a message confirmation and we did not receive the 'original' method for that confirmation
- // => the sender has received our confirmation and removed the message from it's outbox, thus we can
- // safely remove the message confirmation as well
- delete this.outboxCache[messageId];
- } else if (message.type !== MessageType.MESSAGE_RECEIVED && now - message.sendTime > this.config.messageTimeout) {
- // Delivering the message has failed, as the target window did not pick it up in time
- // The type of message doesn't matter for that
- delete this.outboxCache[messageId];
- if (this.messageTracker[messageId]) {
- this.messageTracker[messageId].error('Timeout');
- }
- delete this.messageTracker[messageId];
- } else if (message.type === MessageType.MESSAGE && message.send === false) {
- if (message.payload !== undefined && message.payload !== true) {
- // Message has a payload that has not been moved yet, so move that in a separate localstorage key
- this.storageService.setLocalObject(this.generatePayloadKey(message), message.payload);
- message.payload = true;
- }
- this.messageTracker[message.messageId].next(message.messageId);
- // Set property to undefined, as we do not need to encode "send:true" in the localstorage json multiple times
- message.send = undefined;
- }
- });
-
- this.storageService.setLocalObject(this.generateWindowKey(this.myWindow.id), {
- heartbeat: now,
- id: this.myWindow.id,
- name: this.myWindow.name,
- messages: Object.keys(this.outboxCache).map(key => this.outboxCache[key]),
- });
-
- if (this.myWindow.heartbeat === -1) {
- // This was the first heartbeat run for the local window, so rescan for known windows to get
- // the current (new) window in the list
- this.scanForWindows();
- }
-
- // Store the new heartbeat value in the local windowData copy
- this.myWindow.heartbeat = now;
- }
-
- private scanForWindows = () => {
- this.knownWindows = this.storageService.getLocalObjects(
- this.storageService.getLocalItemKeys().filter((key) => this.isWindowKey(key)))
- .map(({id, name, heartbeat}: WindowData) => {
- return {
- id,
- name,
- heartbeat,
- stalled: new Date().getTime() - heartbeat > this.config.heartbeat * 2,
- self: this.myWindow.id === id,
- };
- });
- this.windowSubject.next(this.knownWindows);
- }
+ public sendMessage(broadcastChannelId: string, message: any) {}
+ public sendMessageToAll(message: any) {}
}
diff --git a/projects/ngx-multi-window/src/lib/providers/storage.service.ts b/projects/ngx-multi-window/src/lib/providers/storage.service.ts
deleted file mode 100644
index 46b502d..0000000
--- a/projects/ngx-multi-window/src/lib/providers/storage.service.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { Injectable, Optional, SkipSelf } from '@angular/core';
-
-@Injectable({
- providedIn: 'root',
-})
-export class StorageService {
-
- private window: Window;
- private localStorage: Storage;
- private sessionStorage: Storage;
-
- constructor() {
- this.window = window;
- this.localStorage = window.localStorage;
- this.sessionStorage = window.sessionStorage;
- }
-
- /*
- Write methods
- */
- public setLocalObject(key: string, obj: any): boolean {
- return this.setObject(this.localStorage, key, obj);
- }
-
- public setLocalItem(key: string, obj: string): void {
- this.setItem(this.localStorage, key, obj);
- }
-
- public setSessionObject(key: string, obj: any): boolean {
- return this.setObject(this.sessionStorage, key, obj);
- }
-
- public setSessionItem(key: string, obj: string): void {
- this.setItem(this.sessionStorage, key, obj);
- }
-
- private setObject(storage: Storage, key: string, obj: any): boolean {
- let jsonString: string;
- try {
- jsonString = JSON.stringify(obj);
- } catch (ex) {
- return false;
- }
- this.setItem(storage, key, jsonString);
-
- return true;
- }
-
- private setItem(storage: Storage, key: string, obj: string): void {
- storage.setItem(key, obj);
- }
-
- public setWindowName(value: string): void {
- this.window.name = value;
- }
-
- /*
- Read methods
- */
-
- public getLocalObject(key: string): T | null {
- return this.getObject(this.localStorage, key);
- }
-
- public getLocalObjects(keys: string[]): (T | null)[] {
- return this.getObjects(this.localStorage, keys);
- }
-
- public getLocalItem(key: string): string | null {
- return this.getItem(this.localStorage, key);
- }
-
- public getSessionObject(key: string): T | null {
- return this.getObject(this.sessionStorage, key);
- }
-
- public getSessionObjects(keys: string[]): (T | null)[] {
- return this.getObjects(this.sessionStorage, keys);
- }
-
- public getSessionItem(key: string): string | null {
- return this.getItem(this.sessionStorage, key);
- }
-
- public getObjects(storage: Storage, keys: string[]): (T | null)[] {
- return keys.map(key => this.getObject(storage, key));
- }
-
- private getObject(storage: Storage, key: string): T | null {
- const jsonString = this.getItem(storage, key);
- if (jsonString === null) {
- return null;
- }
- try {
- return JSON.parse(jsonString) as T;
- } catch (ex) {
- return null;
- }
- }
-
- private getItem(storage: Storage, key: string): string | null {
- return storage.getItem(key) || null;
- }
-
- public getWindowName(): string {
- return this.window.name;
- }
-
- /*
- Remove methods
- */
-
- public removeLocalItem(key: string): void {
- this.removeItem(this.localStorage, key);
- }
-
- public removeSessionItem(key: string): void {
- this.removeItem(this.sessionStorage, key);
- }
-
- private removeItem(storage: Storage, key: string): void {
- storage.removeItem(key);
- }
-
- public clearLocalStorage(): void {
- this.clearStorage(this.localStorage);
- }
-
- public clearSessionStorage(): void {
- this.clearStorage(this.sessionStorage);
- }
-
- private clearStorage(storage: Storage): void {
- storage.clear();
- }
-
- /*
- Inspection methods
- */
-
- public getLocalItemKeys(): string[] {
- return this.getStorageItemKeys(this.localStorage);
- }
-
- public getSessionItemKeys(): string[] {
- return this.getStorageItemKeys(this.sessionStorage);
- }
-
- private getStorageItemKeys(storage: Storage): string[] {
- const keys = [];
- for (let i = 0; i < storage.length; i++) {
- keys.push(storage.key(i));
- }
-
- return keys;
- }
-}
-
-/* singleton pattern taken from https://github.com/angular/angular/issues/13854 */
-export const StorageServiceProviderFactory = (parentDispatcher: StorageService) => {
- return parentDispatcher || new StorageService();
-};
-
-export const StorageServiceProvider = {
- provide: StorageService,
- deps: [[new Optional(), new SkipSelf(), StorageService]],
- useFactory: StorageServiceProviderFactory,
-};
diff --git a/projects/ngx-multi-window/src/lib/types/window-save-strategy.enum.ts b/projects/ngx-multi-window/src/lib/types/window-save-strategy.enum.ts
deleted file mode 100644
index 0d44545..0000000
--- a/projects/ngx-multi-window/src/lib/types/window-save-strategy.enum.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * A representation of a strategy on how to save the window id in the native window.name property.
- */
-export enum WindowSaveStrategy {
- /**
- * Default behaviour. Window data will not be saved.
- */
- NONE,
- /**
- * Only save the window data when the storage (native window.name property) is empty/undefined, meaning nothing else is
- * probably utilizing it
- */
- SAVE_WHEN_EMPTY,
- /**
- * Save the window data without checking whether the storage might be used by another script
- */
- SAVE_FORCE,
- /**
- * Save window data, but backup probable existing data in the storage and restore the original data after reading the window data.
- * Restoring the original data might not happen "on time" for the script using it, which would require delaying it's execution.
- */
- SAVE_BACKUP,
-}
diff --git a/projects/ngx-multi-window/src/lib/types/window.type.ts b/projects/ngx-multi-window/src/lib/types/window.type.ts
index f13a772..91179f6 100644
--- a/projects/ngx-multi-window/src/lib/types/window.type.ts
+++ b/projects/ngx-multi-window/src/lib/types/window.type.ts
@@ -1,24 +1,3 @@
-import { Message } from './message.type';
-
-export interface WindowData {
- heartbeat: number;
+export interface KnownAppWindow {
id: string;
- name: string;
-}
-
-export interface KnownAppWindow extends WindowData {
- stalled: boolean;
- self: boolean;
-}
-
-/**
- * Represents a window with the current application how it is stored
- * in the localstorage. It has an id, name, heartbeat represented in a timestamp
- * and an array of messages that that window sent out.
- *
- * It's named "AppWindow" to avoid confusion with the type Window
- * of the global variable "window".
- */
-export interface AppWindow extends WindowData {
- messages: Message[];
}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 34151c7..134135f 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -72,9 +72,6 @@
{{ window.name }}
Current windowOther window,
- Status:
- Stalled
- Active
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 2318f17..ea68fd8 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -12,16 +12,10 @@ export class AppComponent implements OnInit {
ownName: string;
ownId: string;
- windows: KnownAppWindow[] = [];
logs: string[] = [];
newName: string;
- @HostListener('window:unload')
- unloadHandler() {
- this.multiWindowService.saveWindow();
- }
-
constructor(private multiWindowService: MultiWindowService, private nameGenerator: NameGeneratorService) {
}
@@ -32,72 +26,24 @@ export class AppComponent implements OnInit {
ngOnInit(): void {
this.ownId = this.multiWindowService.id;
- this.ownName = this.multiWindowService.name;
- if (this.ownName.indexOf(this.ownId) >= 0) {
- // This window still has the automatic given name, so generate a fake one for demo reasons
- // Generate a random name for the current window, just for fun
- this.multiWindowService.name = this.ownName = this.nameGenerator.getRandomFakeName();
- }
this.newName = this.ownName;
- this.windows = this.multiWindowService.getKnownWindows();
- if (this.multiWindowService.getKnownWindows().length > 0) {
- this.multiWindowService.onMessage().subscribe((value: Message) => {
- if (value.senderId != this.ownId) {
- this.logs.unshift('Received a message from ' + value.senderId + ': ' + value.data);
- }
- });
- }
-
- this.multiWindowService.onWindows().subscribe(knownWindows => this.windows = knownWindows);
- }
-
- public sendTonsOfMessages(recipientId: string, message: string) {
- for (let i = 0; i < 5000; i++) {
- this.sendMessage(recipientId, message);
- }
+ this.multiWindowService.onMessage('data').subscribe((value: Message) => {
+ if (value.senderId != this.ownId) {
+ this.logs.unshift('Received a message from ' + value.senderId + ': ' + value.data);
+ }
+ });
}
- public sendMessage(recipientId: string, message: string) {
- if (recipientId === this.ownId) {
- // Catch sending messages to itself. Trying to do so throws an error from multiWindowService.sendMessage()
- this.logs.unshift('Can\'t send messages to itself. Select another window.');
-
- return;
- }
- this.multiWindowService.sendMessage(recipientId, 'customEvent', message).subscribe(
- (messageId: string) => {
- this.logs.unshift('Message send, ID is ' + messageId);
- },
- (error) => {
- this.logs.unshift('Message sending failed, error: ' + error);
- },
- () => {
- this.logs.unshift('Message successfully delivered');
- });
+ public sendMessage(message: string) {
+ this.multiWindowService.sendMessage("data", message);
}
public removeLogMessage(index: number) {
this.logs.splice(index, 1);
}
- public changeName() {
- this.multiWindowService.name = this.ownName = this.newName;
- }
-
public newWindow() {
- const newWindowData = this.multiWindowService.newWindow();
- newWindowData.created.subscribe({
- next: () => {
- },
- error: (err) => {
- this.logs.unshift('An error occured while waiting for the new window to start consuming messages');
- },
- complete: () => {
- this.logs.unshift('The new window with id ' + newWindowData.windowId + ' got created and starts consuming messages');
- }
- }
- );
- window.open('?' + newWindowData.urlString);
+ window.open('?');
}
public windowTrackerFunc(item, index) {
From 63e05f8489dadb6204385721e9b88c9d191f0ee0 Mon Sep 17 00:00:00 2001
From: Jared
Date: Fri, 2 Jun 2023 22:52:19 -0400
Subject: [PATCH 3/9] Constructing this to be the way I want it to be --
Improving library... (making my own basically)
---
.../src/lib/providers/config.provider.ts | 14 ----
.../src/lib/providers/multi-window.service.ts | 76 +++++++++++--------
.../src/lib/types/message.type.ts | 22 +++---
.../src/lib/types/multi-window.config.ts | 42 ----------
.../src/lib/types/window.type.ts | 1 +
src/app/app.component.ts | 20 +++--
6 files changed, 69 insertions(+), 106 deletions(-)
delete mode 100644 projects/ngx-multi-window/src/lib/providers/config.provider.ts
delete mode 100644 projects/ngx-multi-window/src/lib/types/multi-window.config.ts
diff --git a/projects/ngx-multi-window/src/lib/providers/config.provider.ts b/projects/ngx-multi-window/src/lib/providers/config.provider.ts
deleted file mode 100644
index 5297ec6..0000000
--- a/projects/ngx-multi-window/src/lib/providers/config.provider.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { InjectionToken } from '@angular/core';
-import { MultiWindowConfig } from '../types/multi-window.config';
-import { WindowSaveStrategy } from '../types/window-save-strategy.enum';
-
-export const NGXMW_CONFIG = new InjectionToken('ngxmw_config');
-
-export const defaultMultiWindowConfig: MultiWindowConfig = {
- keyPrefix: 'ngxmw_',
- heartbeat: 1000,
- newWindowScan: 1000,
- messageTimeout: 10000,
- windowTimeout: 2000,
- windowSaveStrategy: WindowSaveStrategy.SAVE_WHEN_EMPTY,
-};
diff --git a/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts b/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
index e408138..ea23bdf 100644
--- a/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
+++ b/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
@@ -1,23 +1,23 @@
import {Inject, Injectable, OnDestroy, Optional} from '@angular/core';
-import { Location } from '@angular/common';
-import { BehaviorSubject, Observable, Subject } from 'rxjs';
+import {Location} from '@angular/common';
+import {Observable, Subject} from 'rxjs';
-import { defaultMultiWindowConfig, NGXMW_CONFIG } from './config.provider';
-import { MultiWindowConfig } from '../types/multi-window.config';
-import { KnownAppWindow } from '../types/window.type';
-import { Message } from '../types/message.type';
-import { WindowRef } from '../providers/window.provider';
+import {KnownAppWindow} from '../types/window.type';
+import {EventType, Message, MessageTemplate, MessageType} from '../types/message.type';
+import {WindowRef} from '../providers/window.provider';
@Injectable({
providedIn: 'root',
})
export class MultiWindowService implements OnDestroy {
- private config: MultiWindowConfig;
-
private myWindow: KnownAppWindow;
- private knownWindows: {[broadcastId: string]: {[windowId: string]: {}}} = {};
+ public getMyWindow(): KnownAppWindow {
+ return this.myWindow;
+ }
+
+ private knownWindows: {[broadcastId: string]: KnownAppWindow[]} = {};
private knownBroadcasters: {[broadcastId: string]: BroadcastChannel} = {};
@@ -37,8 +37,9 @@ export class MultiWindowService implements OnDestroy {
return new Date().getTime().toString(36).substr(-4) + Math.random().toString(36).substr(2, 9);
}
- constructor(@Inject(NGXMW_CONFIG) customConfig: MultiWindowConfig, @Optional() private location: Location, private windowRef: WindowRef) {
- this.config = {...defaultMultiWindowConfig, ...customConfig};
+ constructor() {
+ this.myWindow.id = this.generateId();
+ this.myWindow.name = window.name;
}
public ngOnDestroy() {
@@ -54,10 +55,6 @@ export class MultiWindowService implements OnDestroy {
return knownBroadcasters;
}
- get id(): string {
- return this.myWindow.id;
- }
-
/**
* An observable to subscribe to. It emits all messages the current window receives.
* After a message has been emitted by this observable it is marked as read, so the sending window
@@ -79,23 +76,26 @@ export class MultiWindowService implements OnDestroy {
const bc = this.getBroadcastChannel(broadcastChannelId);
if (this.knownListeners[bc.name] == undefined) {
bc.onmessage = (message: any) => {
- if (message.type == 'WINDOW_KILLED') {
+ const msg: Message = message as Message;
+ if (msg.type == MessageType.WINDOW_KILLED) {
// We want to handle the window killed here and make sure we remove it as a known window
- delete this.knownWindows[bc.name][message.senderId];
+ this.knownWindows[bc.name] = this.knownWindows[bc.name].filter((win) => {return win.id != message.senderId});
return;
} else
- if (message.type == 'WINDOW_CREATED') {
- this.knownWindows[bc.name][message.senderId] = {};
+ if (msg.type == MessageType.WINDOW_CREATED) {
+ this.knownWindows[bc.name].push({name: message.senderName, id: message.senderId} as KnownAppWindow);
return;
}
this.messageSubjects[bc.name].next(message as Message);
}
}
// When we register a new listener, we want to tell all the windows listening that we have a new known window to add to their list...
- this.sendMessageToAll({
- type: 'WINDOW_CREATED',
- senderId: this.id
- })
+ this.sendMessage({
+ type: MessageType.WINDOW_KILLED,
+ event: EventType.INTERNAL,
+ senderId: this.myWindow.id,
+ senderName: window.name
+ } as Message)
return this;
}
@@ -112,10 +112,12 @@ export class MultiWindowService implements OnDestroy {
this.knownListeners = [];
}
private handleServiceDestroyed() {
- this.sendMessageToAll({
- type: "WINDOW_KILLED",
- senderId: this.id
- });
+ this.sendMessage({
+ type: MessageType.WINDOW_KILLED,
+ event: EventType.INTERNAL,
+ senderId: this.myWindow.id,
+ senderName: window.name
+ } as Message);
this.closeAllListeners();
}
@@ -152,6 +154,20 @@ export class MultiWindowService implements OnDestroy {
return this.knownWindows;
}
- public sendMessage(broadcastChannelId: string, message: any) {}
- public sendMessageToAll(message: any) {}
+ public sendMessage(message: MessageTemplate, broadcastId?: string) {
+ let msg: Message = message as Message;
+ msg.senderName = this.myWindow.name;
+ msg.senderId = this.myWindow.id;
+ switch (msg.type) {
+ case MessageType.ALL_LISTENERS:
+ case MessageType.SPECIFIC_WINDOW:
+ for (const bc of this.getKnownBroadcasters()) {
+ bc.postMessage(msg);
+ }
+ break;
+ case MessageType.SPECIFIC_LISTENER:
+ this.getBroadcastChannel(broadcastId).postMessage(msg);
+ break;
+ }
+ }
}
diff --git a/projects/ngx-multi-window/src/lib/types/message.type.ts b/projects/ngx-multi-window/src/lib/types/message.type.ts
index 6f44f25..e26129c 100644
--- a/projects/ngx-multi-window/src/lib/types/message.type.ts
+++ b/projects/ngx-multi-window/src/lib/types/message.type.ts
@@ -1,20 +1,24 @@
export enum MessageType {
- MESSAGE,
- MESSAGE_RECEIVED,
- PING,
+ WINDOW_CREATED,
+ WINDOW_KILLED,
+ SPECIFIC_WINDOW,
+ SPECIFIC_LISTENER,
+ ALL_LISTENERS
+}
+
+export enum EventType {
+ CUSTOM_EVENT,
+ INTERNAL
}
export interface MessageTemplate {
- recipientId: string;
+ recipientId?: string;
type: MessageType;
- event: string;
+ event: EventType;
data?: any;
- payload?: any;
}
export interface Message extends MessageTemplate {
- send?: boolean;
- messageId: string;
- sendTime: number;
senderId: string;
+ senderName: string;
}
diff --git a/projects/ngx-multi-window/src/lib/types/multi-window.config.ts b/projects/ngx-multi-window/src/lib/types/multi-window.config.ts
deleted file mode 100644
index ebf2e92..0000000
--- a/projects/ngx-multi-window/src/lib/types/multi-window.config.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { WindowSaveStrategy } from './window-save-strategy.enum';
-/**
- * Object representing the configuration options for the MultiWindow Module
- */
-export interface MultiWindowConfig {
- /**
- * String to be used as prefix when storing data in the localstorage
- */
- keyPrefix?: string;
-
- /**
- * Time in milliseconds how often a heartbeat should be performed. During a heartbeat a window
- * looks for new messages from other windows and processes these messages.
- */
- heartbeat?: number;
-
- /**
- * Time in milliseconds how often a scan for new windows should be performed.
- */
- newWindowScan?: number;
-
- /**
- * Time in milliseconds after which a message delivery is considered to have failed.
- * If no "message_received" confirmation has arrived during the specified duration,
- * the message will be removed from the outbox.
- */
- messageTimeout?: number;
-
- /**
- * Time in milliseconds after which a window is considered dead.
- *
- * Should be a multiple of {@link MultiWindowConfig#newWindowScan}
- */
- windowTimeout?: number;
-
- /**
- * A strategy to be used when it comes to saving the window name.
- *
- * Refer to the different enum values for detailed description about the possible values.
- */
- windowSaveStrategy?: WindowSaveStrategy;
-}
diff --git a/projects/ngx-multi-window/src/lib/types/window.type.ts b/projects/ngx-multi-window/src/lib/types/window.type.ts
index 91179f6..b87ff90 100644
--- a/projects/ngx-multi-window/src/lib/types/window.type.ts
+++ b/projects/ngx-multi-window/src/lib/types/window.type.ts
@@ -1,3 +1,4 @@
export interface KnownAppWindow {
id: string;
+ name: string;
}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index ea68fd8..54c7a2f 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,7 +1,6 @@
-import { Component, HostListener, OnInit } from '@angular/core';
-import { MultiWindowService, Message, KnownAppWindow } from 'ngx-multi-window';
-import { NameGeneratorService } from './providers/name-generator.service';
-import {delay} from "rxjs";
+import {Component, OnInit} from '@angular/core';
+import {Message, MessageType, MultiWindowService} from 'ngx-multi-window';
+import {NameGeneratorService} from './providers/name-generator.service';
@Component({
selector: 'app-root',
@@ -25,7 +24,7 @@ export class AppComponent implements OnInit {
}
ngOnInit(): void {
- this.ownId = this.multiWindowService.id;
+ this.ownId = this.multiWindowService.getMyWindow().id;
this.newName = this.ownName;
this.multiWindowService.onMessage('data').subscribe((value: Message) => {
if (value.senderId != this.ownId) {
@@ -34,8 +33,11 @@ export class AppComponent implements OnInit {
});
}
- public sendMessage(message: string) {
- this.multiWindowService.sendMessage("data", message);
+ public sendMessageToAll(message: string) {
+ this.multiWindowService.sendMessage({
+ data: message,
+ type: MessageType.ALL_LISTENERS
+ } as Message);
}
public removeLogMessage(index: number) {
@@ -45,8 +47,4 @@ export class AppComponent implements OnInit {
public newWindow() {
window.open('?');
}
-
- public windowTrackerFunc(item, index) {
- return item.id;
- }
}
From 170b03b59a0d9ccd6ad77d8ac6cf42a875a4617a Mon Sep 17 00:00:00 2001
From: Jared
Date: Sat, 3 Jun 2023 21:23:54 -0400
Subject: [PATCH 4/9] Most of the library is done... Just has some kinks here
and there
---
projects/ngx-multi-window/src/index.ts | 4 -
.../src/lib/multi-window.module.ts | 16 +-
.../src/lib/providers/multi-window.service.ts | 140 ++++++++++++++----
.../src/lib/types/message.type.ts | 3 +
.../src/lib/types/window.type.ts | 6 +
src/app/app.component.html | 16 +-
src/app/app.component.ts | 44 ++++--
src/app/app.module.ts | 6 +-
8 files changed, 163 insertions(+), 72 deletions(-)
diff --git a/projects/ngx-multi-window/src/index.ts b/projects/ngx-multi-window/src/index.ts
index 6cfd0c3..15a0e23 100644
--- a/projects/ngx-multi-window/src/index.ts
+++ b/projects/ngx-multi-window/src/index.ts
@@ -4,12 +4,8 @@
export * from './lib/multi-window.module';
-export * from './lib/providers/config.provider';
export * from './lib/providers/multi-window.service';
-export * from './lib/providers/storage.service';
export * from './lib/providers/window.provider';
export * from './lib/types/message.type';
-export * from './lib/types/multi-window.config';
-export * from './lib/types/window-save-strategy.enum';
export * from './lib/types/window.type';
diff --git a/projects/ngx-multi-window/src/lib/multi-window.module.ts b/projects/ngx-multi-window/src/lib/multi-window.module.ts
index 9d411c4..f8f81a9 100644
--- a/projects/ngx-multi-window/src/lib/multi-window.module.ts
+++ b/projects/ngx-multi-window/src/lib/multi-window.module.ts
@@ -2,25 +2,13 @@ import { CommonModule } from '@angular/common';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { MultiWindowService } from './providers/multi-window.service';
-import { StorageService } from './providers/storage.service';
-import { NGXMW_CONFIG } from './providers/config.provider';
-import { MultiWindowConfig } from './types/multi-window.config';
import { WindowRef } from './providers/window.provider';
@NgModule({
imports: [CommonModule],
providers: [
- StorageService,
MultiWindowService,
- WindowRef,
- {provide: NGXMW_CONFIG, useValue: {}},
+ WindowRef
],
})
-export class MultiWindowModule {
- static forRoot(config?: MultiWindowConfig): ModuleWithProviders {
- return {
- ngModule: MultiWindowModule,
- providers: [MultiWindowService, {provide: NGXMW_CONFIG, useValue: config}],
- };
- }
-}
+export class MultiWindowModule {}
diff --git a/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts b/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
index ea23bdf..97380ac 100644
--- a/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
+++ b/projects/ngx-multi-window/src/lib/providers/multi-window.service.ts
@@ -1,23 +1,35 @@
-import {Inject, Injectable, OnDestroy, Optional} from '@angular/core';
-import {Location} from '@angular/common';
-import {Observable, Subject} from 'rxjs';
+import {Injectable, OnDestroy} from '@angular/core';
+import {BehaviorSubject, fromEvent, Observable, Subject, Subscription, tap} from 'rxjs';
-import {KnownAppWindow} from '../types/window.type';
+import {KnownAppWindow, KnownWindows} from '../types/window.type';
import {EventType, Message, MessageTemplate, MessageType} from '../types/message.type';
-import {WindowRef} from '../providers/window.provider';
@Injectable({
providedIn: 'root',
})
export class MultiWindowService implements OnDestroy {
- private myWindow: KnownAppWindow;
+ private myWindow: KnownAppWindow = {} as KnownAppWindow;
public getMyWindow(): KnownAppWindow {
return this.myWindow;
}
- private knownWindows: {[broadcastId: string]: KnownAppWindow[]} = {};
+ public setName(name: string): void {
+ this.myWindow.name = name;
+ this.sendMessage({
+ event: EventType.INTERNAL,
+ type: MessageType.CHANGE_NAME
+ } as MessageTemplate);
+ for (const bcId of Object.keys(this.knownBroadcasters)) {
+ for (let win of this.knownWindows[bcId]) {
+ if (win.id == this.myWindow.id)
+ win.name = name;
+ }
+ }
+ }
+
+ private knownWindows: KnownWindows = {};
private knownBroadcasters: {[broadcastId: string]: BroadcastChannel} = {};
@@ -31,7 +43,10 @@ export class MultiWindowService implements OnDestroy {
/**
* A subject to subscribe to in order to get notified about all known windows
*/
- //private windowSubject: Subject = new BehaviorSubject(this.knownWindows);
+ private windowSubject: Subject = new BehaviorSubject(this.knownWindows);
+
+ private windowUnload$ = fromEvent(window, 'unload').pipe(tap(() => this.destroy()));
+ private unloadSub: Subscription = new Subscription();
private generateId(): string {
return new Date().getTime().toString(36).substr(-4) + Math.random().toString(36).substr(2, 9);
@@ -40,13 +55,19 @@ export class MultiWindowService implements OnDestroy {
constructor() {
this.myWindow.id = this.generateId();
this.myWindow.name = window.name;
+ this.myWindow.self = true;
+ this.unloadSub.add(this.windowUnload$.subscribe());
}
- public ngOnDestroy() {
+ private destroy() {
this.handleServiceDestroyed();
this.closeAllListeners();
}
+ ngOnDestroy() {
+ this.unloadSub.unsubscribe();
+ }
+
public getKnownBroadcasters(): BroadcastChannel[] {
let knownBroadcasters: BroadcastChannel[] = [];
for (const bcId of Object.keys(this.knownBroadcasters)) {
@@ -74,28 +95,67 @@ export class MultiWindowService implements OnDestroy {
public listen(broadcastChannelId: string): MultiWindowService {
const bc = this.getBroadcastChannel(broadcastChannelId);
+ if (this.knownWindows[bc.name] == undefined)
+ this.knownWindows[bc.name] = [];
+ this.knownWindows[bc.name].push({
+ id: this.myWindow.id,
+ name: this.myWindow.name,
+ self: true
+ } as KnownAppWindow);
if (this.knownListeners[bc.name] == undefined) {
bc.onmessage = (message: any) => {
- const msg: Message = message as Message;
- if (msg.type == MessageType.WINDOW_KILLED) {
- // We want to handle the window killed here and make sure we remove it as a known window
- this.knownWindows[bc.name] = this.knownWindows[bc.name].filter((win) => {return win.id != message.senderId});
- return;
- } else
- if (msg.type == MessageType.WINDOW_CREATED) {
- this.knownWindows[bc.name].push({name: message.senderName, id: message.senderId} as KnownAppWindow);
- return;
- }
- this.messageSubjects[bc.name].next(message as Message);
+ const msg: Message = message.data as Message;
+ switch (msg.type) {
+ case MessageType.WINDOW_KILLED:
+ // We want to handle the window killed here and make sure we remove it as a known window
+ this.knownWindows[bc.name] = this.knownWindows[bc.name].filter((win) => {
+ return win.id != msg.senderId
+ });
+ this.windowSubject.next(this.knownWindows);
+ break;
+ case MessageType.WINDOW_CREATED:
+ this.knownWindows[bc.name].push({name: msg.senderName, id: msg.senderId} as KnownAppWindow);
+ this.windowSubject.next(this.knownWindows);
+ break;
+ case MessageType.REQUEST_ALL_WINDOWS:
+ this.sendMessage({
+ type: MessageType.REPORT_WINDOW,
+ event: EventType.INTERNAL,
+ recipientId: msg.senderId
+ } as Message, bc.name);
+ break;
+ case MessageType.REPORT_WINDOW:
+ if (msg.recipientId == this.myWindow.id) {
+ // They requested all the windows to report, give them the windows
+ this.knownWindows[bc.name].push({name: msg.senderName, id: msg.senderId} as KnownAppWindow);
+ this.windowSubject.next(this.knownWindows);
+ }
+ break;
+ case MessageType.CHANGE_NAME:
+ for (let win of this.knownWindows[bc.name]) {
+ if (msg.senderId == win.id)
+ win.name = msg.senderName;
+ }
+ break;
+ case MessageType.SPECIFIC_WINDOW:
+ if (msg.recipientId == this.myWindow.id)
+ this.messageSubjects[bc.name].next(msg);
+ break;
+ case MessageType.ALL_LISTENERS:
+ this.messageSubjects[bc.name].next(msg);
+ break;
+ }
}
}
// When we register a new listener, we want to tell all the windows listening that we have a new known window to add to their list...
this.sendMessage({
- type: MessageType.WINDOW_KILLED,
- event: EventType.INTERNAL,
- senderId: this.myWindow.id,
- senderName: window.name
- } as Message)
+ type: MessageType.WINDOW_CREATED,
+ event: EventType.INTERNAL
+ } as MessageTemplate);
+ this.sendMessage({
+ type: MessageType.REQUEST_ALL_WINDOWS,
+ event: EventType.INTERNAL
+ } as MessageTemplate);
return this;
}
@@ -116,7 +176,7 @@ export class MultiWindowService implements OnDestroy {
type: MessageType.WINDOW_KILLED,
event: EventType.INTERNAL,
senderId: this.myWindow.id,
- senderName: window.name
+ senderName: this.myWindow.name
} as Message);
this.closeAllListeners();
}
@@ -125,6 +185,24 @@ export class MultiWindowService implements OnDestroy {
return this.messageSubjects[broadcastChannelId].asObservable();
}
+ /**
+ * An observable to subscribe to. It emits the array of known windows
+ * whenever it is read again from the localstorage.
+ *
+ * This Observable emits the last list of known windows on subscription
+ * (refer to rxjs BehaviorSubject).
+ *
+ * Use {@link getKnownWindows} to get the current list of
+ * known windows or if you only need a snapshot of that list.
+ *
+ * @see {@link MultiWindowConfig#newWindowScan}
+ * @returns
+ */
+ public onWindows(): Observable {
+ return this.windowSubject.asObservable();
+ }
+
+
/**
* An observable to subscribe to. It emits the array of known windows
* whenever it is read again from the localstorage.
@@ -159,15 +237,13 @@ export class MultiWindowService implements OnDestroy {
msg.senderName = this.myWindow.name;
msg.senderId = this.myWindow.id;
switch (msg.type) {
- case MessageType.ALL_LISTENERS:
- case MessageType.SPECIFIC_WINDOW:
- for (const bc of this.getKnownBroadcasters()) {
- bc.postMessage(msg);
- }
- break;
case MessageType.SPECIFIC_LISTENER:
this.getBroadcastChannel(broadcastId).postMessage(msg);
break;
+ default:
+ for (const bc of this.getKnownBroadcasters()) {
+ bc.postMessage(msg);
+ }
}
}
}
diff --git a/projects/ngx-multi-window/src/lib/types/message.type.ts b/projects/ngx-multi-window/src/lib/types/message.type.ts
index e26129c..f2e739e 100644
--- a/projects/ngx-multi-window/src/lib/types/message.type.ts
+++ b/projects/ngx-multi-window/src/lib/types/message.type.ts
@@ -1,6 +1,9 @@
export enum MessageType {
WINDOW_CREATED,
WINDOW_KILLED,
+ REPORT_WINDOW,
+ CHANGE_NAME,
+ REQUEST_ALL_WINDOWS,
SPECIFIC_WINDOW,
SPECIFIC_LISTENER,
ALL_LISTENERS
diff --git a/projects/ngx-multi-window/src/lib/types/window.type.ts b/projects/ngx-multi-window/src/lib/types/window.type.ts
index b87ff90..5fa5141 100644
--- a/projects/ngx-multi-window/src/lib/types/window.type.ts
+++ b/projects/ngx-multi-window/src/lib/types/window.type.ts
@@ -1,4 +1,10 @@
export interface KnownAppWindow {
id: string;
name: string;
+ self: boolean;
+}
+
+
+export interface KnownWindows {
+ [broadcastId: string]: KnownAppWindow[];
}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 134135f..490fcdf 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -61,17 +61,17 @@