UNPKG

@readium/navigator-html-injectables

Version:

An embeddable solution for connecting frames of HTML publications with a Readium Navigator

168 lines (151 loc) 6.4 kB
import { ReadiumWindow } from "../helpers/dom"; import { CommsEventKey, CommsCommandKey } from "./keys"; import { mid } from "./mid"; export const COMMS_VERSION = 1; export interface CommsMessage { _readium: number; // Sanity/version-checking field _channel: string; // Channel ID id?: string; // Optional (but recommended!) unique identifier strict?: boolean; // Whether or not the event *must* be handled by the receiver key: CommsEventKey | CommsCommandKey; // The "key" for identification to the listener data: unknown; // The data to be sent to the module } export interface Registrant { module: string; cb: CommsCallback; } export type CommsAck = (ok: boolean) => void; export type CommsCallback = (data: unknown, ack: CommsAck) => void; // TODO: maybe more than void? /** * Comms is basically a wrapper around window.postMessage that * adds structure to the messages and lets modules register callbacks. */ export class Comms { private readonly wnd: ReadiumWindow; private destination: ReadiumWindow | null = null; private registrar = new Map<CommsCommandKey, Registrant[]>(); private origin: string = ""; private channelId: string = ""; constructor(wnd: ReadiumWindow) { this.wnd = wnd; wnd.addEventListener("message", this.receiver); } private receive(event: MessageEvent) { if(event.source === null) throw Error("Event source is null"); if(typeof event.data !== "object") return; const data = event.data as CommsMessage; // Cast it as a CommsMessage if(!("_readium" in data) || !data._readium || data._readium <= 0) return; // Not for us if(data.key === "_ping") { // The "ping" gives us a destination we bind to for posting events if(!this.destination) { this.destination = event.source as Window as ReadiumWindow; this.origin = event.origin; this.channelId = data._channel; // Make sure we're communicating with a host on the same comms version if(data._readium !== COMMS_VERSION) { if(data._readium > COMMS_VERSION) this.send("error", `received comms version ${data._readium} higher than ${COMMS_VERSION}`); else this.send("error", `received comms version ${data._readium} lower than ${COMMS_VERSION}`); this.destination = null; this.origin = ""; this.channelId = ""; return; } this.send("_pong", undefined); this.preLog.forEach(d => this.send("log", d)); this.preLog = []; } return; } else if(this.channelId) { // Enforce matching channel ID and origin if( data._channel !== this.channelId || event.origin !== this.origin ) return; } else { // Ignore any messages beside _ping if not initialized return; } this.handle(data); } private receiver = this.receive.bind(this); private handle(data: CommsMessage) { const listeners = this.registrar.get(data.key as CommsCommandKey); if(!listeners || listeners.length === 0) { if(data.strict) this.send("_unhandled", data); // Let the sender know the data was not handled by any listener return; } listeners.forEach(l => l.cb(data.data, (ok: boolean) => { this.send("_ack", ok, data.id); // Acknowledge handling of the event })); } public register(key: CommsCommandKey | CommsCommandKey[], module: string, callback: CommsCallback) { if(!Array.isArray(key)) key = [key]; key.forEach(k => { const listeners = this.registrar.get(k); if(listeners && listeners.length >= 0) { const existing = listeners.find(l => l.module === module); if(existing) throw new Error(`Trying to register another callback for combination of event ${k} and module ${module}`); listeners.push({ cb: callback, module }) this.registrar.set(k, listeners); } else this.registrar.set(k, [{ cb: callback, module }]); }) } public unregister(key: CommsCommandKey | CommsCommandKey[], module: string) { if(!Array.isArray(key)) key = [key]; key.forEach(k => { const listeners = this.registrar.get(k); if(!listeners || listeners.length === 0) return; listeners.splice(listeners.findIndex(l => l.module === module), 1); }); } public unregisterAll(module: string) { this.registrar.forEach((v, k) => this.registrar.set(k, v.filter(r => r.module !== module))); } // Convenience function for logging data private preLog: any[] = []; public log(...data: any[]) { if(!this.destination) this.preLog.push(data); else this.send("log", data); } public get ready() { return !!this.destination; } public destroy() { this.destination = null; this.channelId = ""; this.preLog = []; this.registrar.clear(); this.wnd.removeEventListener("message", this.receiver); } public send(key: CommsEventKey, data: unknown, id: unknown = undefined, transfer: Transferable[] = []) { if(!this.destination) throw Error("Attempted to send comms message before destination has been initialized"); const msg = { _readium: COMMS_VERSION, _channel: this.channelId, id: id ?? mid(), // scrict, key, data } as CommsMessage; try { this.destination.postMessage(msg, { targetOrigin: this.origin, transfer }); } catch (error) { // Fallback for when browser doesn't support WindowPostMessageOptions // For example, older Safari versions if(transfer.length > 0) throw error; this.destination.postMessage(msg, this.origin, transfer); } } }