From 5f925566c4f28c87fa23af9a016edc30e80cf43b Mon Sep 17 00:00:00 2001 From: Richard McSharry | Code Monkey Date: Thu, 23 Oct 2025 17:02:19 +0200 Subject: [PATCH] fix: for more stable broadcast channels on CF workers (#2007) --- app/lib/stores/previews.ts | 79 +++++++++++++++++-------- app/routes/webcontainer.preview.$id.tsx | 23 ++++--- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/app/lib/stores/previews.ts b/app/lib/stores/previews.ts index 710d912..74c5442 100644 --- a/app/lib/stores/previews.ts +++ b/app/lib/stores/previews.ts @@ -20,43 +20,47 @@ const PREVIEW_CHANNEL = 'preview-updates'; export class PreviewsStore { #availablePreviews = new Map(); #webcontainer: Promise; - #broadcastChannel: BroadcastChannel; + #broadcastChannel?: BroadcastChannel; #lastUpdate = new Map(); #watchedFiles = new Set(); #refreshTimeouts = new Map(); #REFRESH_DELAY = 300; - #storageChannel: BroadcastChannel; + #storageChannel?: BroadcastChannel; previews = atom([]); constructor(webcontainerPromise: Promise) { this.#webcontainer = webcontainerPromise; - this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL); - this.#storageChannel = new BroadcastChannel('storage-sync-channel'); + this.#broadcastChannel = this.#maybeCreateChannel(PREVIEW_CHANNEL); + this.#storageChannel = this.#maybeCreateChannel('storage-sync-channel'); - // Listen for preview updates from other tabs - this.#broadcastChannel.onmessage = (event) => { - const { type, previewId } = event.data; + if (this.#broadcastChannel) { + // Listen for preview updates from other tabs + this.#broadcastChannel.onmessage = (event) => { + const { type, previewId } = event.data; - if (type === 'file-change') { - const timestamp = event.data.timestamp; - const lastUpdate = this.#lastUpdate.get(previewId) || 0; + if (type === 'file-change') { + const timestamp = event.data.timestamp; + const lastUpdate = this.#lastUpdate.get(previewId) || 0; - if (timestamp > lastUpdate) { - this.#lastUpdate.set(previewId, timestamp); - this.refreshPreview(previewId); + if (timestamp > lastUpdate) { + this.#lastUpdate.set(previewId, timestamp); + this.refreshPreview(previewId); + } } - } - }; + }; + } - // Listen for storage sync messages - this.#storageChannel.onmessage = (event) => { - const { storage, source } = event.data; + if (this.#storageChannel) { + // Listen for storage sync messages + this.#storageChannel.onmessage = (event) => { + const { storage, source } = event.data; - if (storage && source !== this._getTabId()) { - this._syncStorage(storage); - } - }; + if (storage && source !== this._getTabId()) { + this._syncStorage(storage); + } + }; + } // Override localStorage setItem to catch all changes if (typeof window !== 'undefined') { @@ -71,6 +75,29 @@ export class PreviewsStore { this.#init(); } + #maybeCreateChannel(name: string): BroadcastChannel | undefined { + if (typeof globalThis === 'undefined') { + return undefined; + } + + const globalBroadcastChannel = ( + globalThis as typeof globalThis & { + BroadcastChannel?: typeof BroadcastChannel; + } + ).BroadcastChannel; + + if (typeof globalBroadcastChannel !== 'function') { + return undefined; + } + + try { + return new globalBroadcastChannel(name); + } catch (error) { + console.warn('[Preview] BroadcastChannel unavailable:', error); + return undefined; + } + } + // Generate a unique ID for this tab private _getTabId(): string { if (typeof window !== 'undefined') { @@ -130,7 +157,7 @@ export class PreviewsStore { } } - this.#storageChannel.postMessage({ + this.#storageChannel?.postMessage({ type: 'storage-sync', storage, source: this._getTabId(), @@ -192,7 +219,7 @@ export class PreviewsStore { const timestamp = Date.now(); this.#lastUpdate.set(previewId, timestamp); - this.#broadcastChannel.postMessage({ + this.#broadcastChannel?.postMessage({ type: 'state-change', previewId, timestamp, @@ -204,7 +231,7 @@ export class PreviewsStore { const timestamp = Date.now(); this.#lastUpdate.set(previewId, timestamp); - this.#broadcastChannel.postMessage({ + this.#broadcastChannel?.postMessage({ type: 'file-change', previewId, timestamp, @@ -219,7 +246,7 @@ export class PreviewsStore { const timestamp = Date.now(); this.#lastUpdate.set(previewId, timestamp); - this.#broadcastChannel.postMessage({ + this.#broadcastChannel?.postMessage({ type: 'file-change', previewId, timestamp, diff --git a/app/routes/webcontainer.preview.$id.tsx b/app/routes/webcontainer.preview.$id.tsx index a52ccf9..702f654 100644 --- a/app/routes/webcontainer.preview.$id.tsx +++ b/app/routes/webcontainer.preview.$id.tsx @@ -46,17 +46,22 @@ export default function WebContainerPreview() { }, [previewId, previewUrl]); useEffect(() => { - // Initialize broadcast channel - broadcastChannelRef.current = new BroadcastChannel(PREVIEW_CHANNEL); + const supportsBroadcastChannel = typeof window !== 'undefined' && typeof window.BroadcastChannel === 'function'; - // Listen for preview updates - broadcastChannelRef.current.onmessage = (event) => { - if (event.data.previewId === previewId) { - if (event.data.type === 'refresh-preview' || event.data.type === 'file-change') { - handleRefresh(); + if (supportsBroadcastChannel) { + broadcastChannelRef.current = new window.BroadcastChannel(PREVIEW_CHANNEL); + + // Listen for preview updates + broadcastChannelRef.current.onmessage = (event) => { + if (event.data.previewId === previewId) { + if (event.data.type === 'refresh-preview' || event.data.type === 'file-change') { + handleRefresh(); + } } - } - }; + }; + } else { + broadcastChannelRef.current = undefined; + } // Construct the WebContainer preview URL const url = `https://${previewId}.local-credentialless.webcontainer-api.io`;