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,43 +20,47 @@ const PREVIEW_CHANNEL = 'preview-updates';
export class PreviewsStore {
#availablePreviews = new Map<number, PreviewInfo>();
#webcontainer: Promise<WebContainer>;
#broadcastChannel: BroadcastChannel;
#broadcastChannel?: BroadcastChannel;
#lastUpdate = new Map<string, number>();
#watchedFiles = new Set<string>();
#refreshTimeouts = new Map<string, NodeJS.Timeout>();
#REFRESH_DELAY = 300;
#storageChannel: BroadcastChannel;
#storageChannel?: BroadcastChannel;
previews = atom<PreviewInfo[]>([]);
constructor(webcontainerPromise: Promise<WebContainer>) {
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,

View File

@@ -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`;