fix: for more stable broadcast channels on CF workers (#2007)

This commit is contained in:
Richard McSharry | Code Monkey
2025-10-23 17:02:19 +02:00
committed by GitHub
parent 983b3025a5
commit 5f925566c4
2 changed files with 67 additions and 35 deletions

View File

@@ -20,20 +20,21 @@ const PREVIEW_CHANNEL = 'preview-updates';
export class PreviewsStore { export class PreviewsStore {
#availablePreviews = new Map<number, PreviewInfo>(); #availablePreviews = new Map<number, PreviewInfo>();
#webcontainer: Promise<WebContainer>; #webcontainer: Promise<WebContainer>;
#broadcastChannel: BroadcastChannel; #broadcastChannel?: BroadcastChannel;
#lastUpdate = new Map<string, number>(); #lastUpdate = new Map<string, number>();
#watchedFiles = new Set<string>(); #watchedFiles = new Set<string>();
#refreshTimeouts = new Map<string, NodeJS.Timeout>(); #refreshTimeouts = new Map<string, NodeJS.Timeout>();
#REFRESH_DELAY = 300; #REFRESH_DELAY = 300;
#storageChannel: BroadcastChannel; #storageChannel?: BroadcastChannel;
previews = atom<PreviewInfo[]>([]); previews = atom<PreviewInfo[]>([]);
constructor(webcontainerPromise: Promise<WebContainer>) { constructor(webcontainerPromise: Promise<WebContainer>) {
this.#webcontainer = webcontainerPromise; this.#webcontainer = webcontainerPromise;
this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL); this.#broadcastChannel = this.#maybeCreateChannel(PREVIEW_CHANNEL);
this.#storageChannel = new BroadcastChannel('storage-sync-channel'); this.#storageChannel = this.#maybeCreateChannel('storage-sync-channel');
if (this.#broadcastChannel) {
// Listen for preview updates from other tabs // Listen for preview updates from other tabs
this.#broadcastChannel.onmessage = (event) => { this.#broadcastChannel.onmessage = (event) => {
const { type, previewId } = event.data; const { type, previewId } = event.data;
@@ -48,7 +49,9 @@ export class PreviewsStore {
} }
} }
}; };
}
if (this.#storageChannel) {
// Listen for storage sync messages // Listen for storage sync messages
this.#storageChannel.onmessage = (event) => { this.#storageChannel.onmessage = (event) => {
const { storage, source } = event.data; const { storage, source } = event.data;
@@ -57,6 +60,7 @@ export class PreviewsStore {
this._syncStorage(storage); this._syncStorage(storage);
} }
}; };
}
// Override localStorage setItem to catch all changes // Override localStorage setItem to catch all changes
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -71,6 +75,29 @@ export class PreviewsStore {
this.#init(); 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 // Generate a unique ID for this tab
private _getTabId(): string { private _getTabId(): string {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -130,7 +157,7 @@ export class PreviewsStore {
} }
} }
this.#storageChannel.postMessage({ this.#storageChannel?.postMessage({
type: 'storage-sync', type: 'storage-sync',
storage, storage,
source: this._getTabId(), source: this._getTabId(),
@@ -192,7 +219,7 @@ export class PreviewsStore {
const timestamp = Date.now(); const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp); this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({ this.#broadcastChannel?.postMessage({
type: 'state-change', type: 'state-change',
previewId, previewId,
timestamp, timestamp,
@@ -204,7 +231,7 @@ export class PreviewsStore {
const timestamp = Date.now(); const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp); this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({ this.#broadcastChannel?.postMessage({
type: 'file-change', type: 'file-change',
previewId, previewId,
timestamp, timestamp,
@@ -219,7 +246,7 @@ export class PreviewsStore {
const timestamp = Date.now(); const timestamp = Date.now();
this.#lastUpdate.set(previewId, timestamp); this.#lastUpdate.set(previewId, timestamp);
this.#broadcastChannel.postMessage({ this.#broadcastChannel?.postMessage({
type: 'file-change', type: 'file-change',
previewId, previewId,
timestamp, timestamp,

View File

@@ -46,8 +46,10 @@ export default function WebContainerPreview() {
}, [previewId, previewUrl]); }, [previewId, previewUrl]);
useEffect(() => { useEffect(() => {
// Initialize broadcast channel const supportsBroadcastChannel = typeof window !== 'undefined' && typeof window.BroadcastChannel === 'function';
broadcastChannelRef.current = new BroadcastChannel(PREVIEW_CHANNEL);
if (supportsBroadcastChannel) {
broadcastChannelRef.current = new window.BroadcastChannel(PREVIEW_CHANNEL);
// Listen for preview updates // Listen for preview updates
broadcastChannelRef.current.onmessage = (event) => { broadcastChannelRef.current.onmessage = (event) => {
@@ -57,6 +59,9 @@ export default function WebContainerPreview() {
} }
} }
}; };
} else {
broadcastChannelRef.current = undefined;
}
// Construct the WebContainer preview URL // Construct the WebContainer preview URL
const url = `https://${previewId}.local-credentialless.webcontainer-api.io`; const url = `https://${previewId}.local-credentialless.webcontainer-api.io`;