import { Inject , Injectable , 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 { WindowRef } from '../providers/window.provider' ; import { WindowSaveStrategy } from '../types/window-save-strategy.enum' ; @ Injectable ( { providedIn : 'root' , } ) export class MultiWindowService { private config : MultiWindowConfig ; private myWindow : WindowData ; private heartbeatId = null ; private windowScanId = null ; private knownWindows : KnownAppWindow [ ] = [ ] ; /**
     * A hash that keeps track of subjects for all send messages
     */ private messageTracker : { [ key ] : Subject < string > ; } = { } ; /**
     * A copy of the outbox that is regularly written to the local storage
     */ private outboxCache : { [ key ] : Message ; } = { } ; /**
     * A subject to subscribe to in order to get notified about messages send to this window
     */ private messageSubject : Subject < Message > = new Subject < Message > ( ) ; /**
     * A subject to subscribe to in order to get notified about all known windows
     */ private windowSubject : Subject < KnownAppWindow [ ] > = new BehaviorSubject < KnownAppWindow [ ] > ( this . knownWindows ) ; private static generateId ( ) { return new Date ( ) . getTime ( ) . toString ( 36 ) . substr ( - 4 ) + Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ; } private tryMatchWindowKey ( source ) { const nameRegex = new RegExp ( 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 } ) { return this . config . keyPrefix + 'payload_' + messageId ; } private generateWindowKey ( windowId ) { return this . config . keyPrefix + 'w_' + windowId ; } private isWindowKey ( key ) { return key . indexOf ( this . config . keyPrefix + 'w_' ) === 0 ; } constructor ( @ Inject ( NGXMW_CONFIG ) customConfig , @ Optional ( ) private location , private storageService , private windowRef ) { this . config = { ... defaultMultiWindowConfig , ... customConfig } ; let windowId ; if ( this . location ) { windowId = this . tryMatchWindowKey ( this . location . path ( true ) ) ; } if ( ! windowId && this . config . windowSaveStrategy !== WindowSaveStrategy . NONE ) { const storedWindowData = this . windowRef . nativeWindow . name ; if ( this . config . windowSaveStrategy === WindowSaveStrategy . SAVE_BACKUP ) { try { const storedJsonData = JSON . parse ( storedWindowData ) ; windowId = storedJsonData . ngxmw_id ; this . windowRef . nativeWindow . name = storedJsonData . backup ; } catch ( ex ) { } } 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 ; } set name ( value ) { this . myWindow . name = value ; } 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
     * gets informed about successful delivery.
     */ public onMessage ( ) { return this . messageSubject . 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  getKnownWindows to get the current list of
     * known windows or if you only need a snapshot of that list.
     *
     *  see MultiWindowConfig returns public onWindows ( ) { return this . windowSubject . asObservable ( ) ; } /**
     * Get the latest list of known windows.
     *
     * Use  onWindows to get an observable which emits
     * whenever is updated.
      public getKnownWindows ( ) { 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  MultiWindowService constructor)
      public start ( ) { 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 ( ) { 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  stop ped the service the window representation will be
     * recreated after  MultiWindowConfig milliseconds
      public clear ( ) { this . storageService . removeLocalItem ( this . generateWindowKey ( this . myWindow . id ) ) ; } param recipientId param event param data param payload returns public sendMessage ( recipientId , event , data , payload ? ) { const messageId = this . pushToOutbox ( { recipientId , type : MessageType . MESSAGE , event , data , payload , } ) ; this . messageTracker [ messageId ] = new Subject < string > ( ) ; 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
     *  MultiWindowConfig , this observable will fail, although
     * the window might become present after that. The Observable will never emit any elements.
      public newWindow ( ) { if ( this . location === null ) { 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 < string > ( ) ; return { windowId : newWindowId , urlString : this . generateWindowKey ( newWindowId ) , created : this . messageTracker [ messageId ] . pipe ( ignoreElements ( ) ) , } ; } public saveWindow ( ) { 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 ? ) { const windowKey = windowId ? this . generateWindowKey ( windowId ) : this . storageService . getWindowName ( ) ; let windowData = null ; if ( windowKey && this . isWindowKey ( windowKey ) ) { windowData = this . storageService . getLocalObject < WindowData > ( windowKey ) ; } if ( windowData !== null ) { 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 ) ; this . scanForWindows ( ) ; this . heartbeat ( ) ; } private pushToOutbox ( { recipientId , type , event , data , payload } , messageId = MultiWindowService . generateId ( ) ) { 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 ( ) ; const receivedMessages = [ ] ; this . knownWindows . forEach ( windowData => { const appWindow = this . storageService . getLocalObject < AppWindow > ( this . generateWindowKey ( windowData . id ) ) ; if ( appWindow === null ) { return ; } if ( appWindow . id === this . myWindow . id ) { if ( this . myWindow . heartbeat !== - 1 && this . myWindow . heartbeat !== appWindow . heartbeat ) { this . myWindow . id = MultiWindowService . generateId ( ) ; 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 ) { this . storageService . removeLocalItem ( this . generateWindowKey ( appWindow . id ) ) ; } windowData . name = appWindow . name ; windowData . heartbeat = appWindow . heartbeat ; if ( appWindow . messages && appWindow . messages . length > 0 ) { appWindow . messages . forEach ( message => { if ( message . recipientId !== this . myWindow . id ) { return ; } if ( message . type === MessageType . MESSAGE_RECEIVED ) { if ( this . messageTracker [ message . messageId ] ) { this . messageTracker [ message . messageId ] . complete ( ) ; } delete this . messageTracker [ message . messageId ] ; delete this . outboxCache [ message . messageId ] ; } else { receivedMessages . push ( message . messageId ) ; if ( ! ( this . outboxCache [ message . messageId ] && this . outboxCache [ message . messageId ] . type === MessageType . MESSAGE_RECEIVED ) ) { this . pushToOutbox ( { recipientId : message . senderId , type : MessageType . MESSAGE_RECEIVED , event : undefined , } , message . messageId ) ; if ( message . type !== MessageType . PING ) { if ( message . payload === true ) { const payloadKey = this . generatePayloadKey ( message ) ; message . payload = this . storageService . getLocalObject < any > ( payloadKey ) ; this . storageService . removeLocalItem ( payloadKey ) ; } this . messageSubject . next ( message ) ; } } } } ) ; } } ) ; Object . keys ( this . outboxCache ) . forEach ( messageId => { const message = this . outboxCache [ messageId ] ; if ( message . type === MessageType . MESSAGE_RECEIVED && ! receivedMessages . some ( msgId => msgId === messageId ) ) { delete this . outboxCache [ messageId ] ; } else if ( message . type !== MessageType . MESSAGE_RECEIVED && now - message . sendTime > this . config . messageTimeout ) { 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 ) { this . storageService . setLocalObject ( this . generatePayloadKey ( message ) , message . payload ) ; message . payload = true ; } this . messageTracker [ message . messageId ] . next ( message . messageId ) ; 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 . scanForWindows ( ) ; } this . myWindow . heartbeat = now ; } ; private scanForWindows = ( ) => { this . knownWindows = this . storageService . getLocalObjects < WindowData > ( this . storageService . getLocalItemKeys ( ) . filter ( ( key ) => this . isWindowKey ( key ) ) ) . map ( ( { id , name , heartbeat } ) => { return { id , name , heartbeat , stalled : new Date ( ) . getTime ( ) - heartbeat > this . config . heartbeat * 2 , self : this . myWindow . id === id , } ; } ) ; this . windowSubject . next ( this . knownWindows ) ; } ; }