diff --git a/webxdc.d.ts b/webxdc.d.ts index e5d4ef8..fa7c2a2 100644 --- a/webxdc.d.ts +++ b/webxdc.d.ts @@ -65,6 +65,25 @@ type SendOptions = text: string; }; +/** + * A listener for realtime data. + */ +export class RealtimeListener { + private listener: (data: Uint8Array) => void + private trashed: boolean + + /* Whether the realtime channel was left */ + is_trashed(): boolean + /* Receive data from the realtime channel */ + receive(data: Uint8Array): void + /* Set a listener for the realtime channel */ + setListener(listener: (data: Uint8Array) => void): void + /* Send data over the realtime channel */ + send(data: Uint8Array): void + /* Leave the realtime channel */ + leave(): void +} + interface Webxdc { /** Returns the peer's own address. * This is esp. useful if you want to differ between different peers - just send the address along with the payload, @@ -82,6 +101,12 @@ interface Webxdc { cb: (statusUpdate: ReceivedStatusUpdate) => void, serial?: number ): Promise; + + /** + * Join a realtime channel. + */ + joinRealtimeChannel(): RealtimeListener; + /** * @deprecated See {@link setUpdateListener|`setUpdateListener()`}. */ diff --git a/webxdc.js b/webxdc.js index 8f68150..8fb5ee7 100644 --- a/webxdc.js +++ b/webxdc.js @@ -4,12 +4,59 @@ // browsers. In an actual webxdc environment (e.g. Delta Chat messenger) this // file is not used and will automatically be replaced with a real one. // See https://docs.webxdc.org/spec.html#webxdc-api +let ephemeralUpdateKey = "__xdcEphemeralUpdateKey__"; + +/** + * @typedef {import('./webxdc.d.ts').RealtimeListener} RT + * @type {RT} + */ +class RealtimeListener { + constructor() { + this.listener = null; + this.trashed = false; + } + + is_trashed() { + return this.trashed; + } + + receive(data) { + if (this.trashed) { + throw new Error("realtime listener is trashed and can no longer be used"); + } + if (this.listener) { + this.listener(data); + } + } + + setListener(listener) { + this.listener = listener; + } + + send(data) { + if (!data instanceof Uint8Array) { + throw new Error("realtime listener data must be a Uint8Array"); + } + window.localStorage.setItem( + ephemeralUpdateKey, + JSON.stringify([window.webxdc.selfAddr, Array.from(data), Date.now()]) // Date.now() is needed to trigger the event + ); + } + + leave() { + this.trashed = true; + } +} // debug friend: document.writeln(JSON.stringify(value)); //@ts-check /** @type {import('./webxdc').Webxdc} */ window.webxdc = (() => { var updateListener = (_) => {}; + /** + * @type {RT | null} + */ + var realtimeListener = null; var updatesKey = "__xdcUpdatesKey__"; window.addEventListener("storage", (event) => { if (event.key == null) { @@ -20,6 +67,11 @@ window.webxdc = (() => { update.max_serial = updates.length; console.log("[Webxdc] " + JSON.stringify(update)); updateListener(update); + } else if (event.key === ephemeralUpdateKey) { + var [sender, update] = JSON.parse(event.newValue); + if (window.webxdc.selfAddr !== sender && realtimeListener && !realtimeListener.is_trashed()) { + realtimeListener.receive( Uint8Array.from(update)); + } } }); @@ -44,6 +96,15 @@ window.webxdc = (() => { updateListener = cb; return Promise.resolve(); }, + joinRealtimeChannel: (cb) => { + if (realtimeListener && realtimeListener.is_trashed()) { + return; + } + rt = new RealtimeListener(); + // mimic connection establishment time + setTimeout(() => realtimeListener = rt, 500); + return rt; + }, getAllUpdates: () => { console.log("[Webxdc] WARNING: getAllUpdates() is deprecated."); return Promise.resolve([]); @@ -245,8 +306,8 @@ window.alterXdcApp = () => { root.innerHTML = ''; controlPanel.insertBefore(root.firstChild, controlPanel.childNodes[1]); - - var pageIcon = document.createElement('link'); + + var pageIcon = document.createElement("link"); pageIcon.rel = "icon"; pageIcon.href = name; document.head.append(pageIcon);