/** * Tiny client-side SDK for canvas renderers. Import this directly (it's bundled into * your canvas by esbuild, not shared at runtime). It's the only thing a canvas needs * to talk to the host. No Node, no fs, no DOM access outside your own iframe. */ import { BRIDGE_CHANNEL, type BridgeEventMessage, type BridgeEventName, type BridgeEventPayloadMap, type BridgeMethod, type BridgeMethodMap, type BridgeRequestMessage, type BridgeResponseMessage } from './types' let counter = 0 function nextId(): string { counter += 1 return `${Date.now()}-${counter}` } const pending = new Map void; reject: (e: Error) => void }>() const eventListeners = new Map void>>() window.addEventListener('message', (e: MessageEvent) => { const data = e.data as Partial if (!data || data.channel !== BRIDGE_CHANNEL) return if (data.type === 'response') { const msg = data as BridgeResponseMessage const waiter = pending.get(msg.id) if (!waiter) return pending.delete(msg.id) if (msg.ok) waiter.resolve(msg.result) else waiter.reject(new Error(msg.error ?? 'Bridge call failed')) } else if (data.type === 'event') { const msg = data as BridgeEventMessage for (const cb of eventListeners.get(msg.event) ?? []) cb(msg.payload) } }) /** Call a bridge method. Rejects if the host denies it (missing capability, ownership, etc). */ export function callBridge( method: M, params: BridgeMethodMap[M]['params'] ): Promise { const id = nextId() const message: BridgeRequestMessage = { channel: BRIDGE_CHANNEL, type: 'request', id, method, params } return new Promise((resolve, reject) => { pending.set(id, { resolve: resolve as (v: unknown) => void, reject }) window.parent.postMessage(message, '*') }) } /** Subscribe to a host-pushed event (e.g. `note.changed`). Returns an unsubscribe fn. */ export function onBridgeEvent( event: E, cb: (payload: BridgeEventPayloadMap[E]) => void ): () => void { if (!eventListeners.has(event)) eventListeners.set(event, new Set()) const set = eventListeners.get(event)! set.add(cb as (payload: unknown) => void) return () => set.delete(cb as (payload: unknown) => void) }