Skip to content

Commit 5f92556

Browse files
authored
fix: for more stable broadcast channels on CF workers (#2007)
1 parent 983b302 commit 5f92556

File tree

2 files changed

+70
-38
lines changed

2 files changed

+70
-38
lines changed

app/lib/stores/previews.ts

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,43 +20,47 @@ const PREVIEW_CHANNEL = 'preview-updates';
2020
export class PreviewsStore {
2121
#availablePreviews = new Map<number, PreviewInfo>();
2222
#webcontainer: Promise<WebContainer>;
23-
#broadcastChannel: BroadcastChannel;
23+
#broadcastChannel?: BroadcastChannel;
2424
#lastUpdate = new Map<string, number>();
2525
#watchedFiles = new Set<string>();
2626
#refreshTimeouts = new Map<string, NodeJS.Timeout>();
2727
#REFRESH_DELAY = 300;
28-
#storageChannel: BroadcastChannel;
28+
#storageChannel?: BroadcastChannel;
2929

3030
previews = atom<PreviewInfo[]>([]);
3131

3232
constructor(webcontainerPromise: Promise<WebContainer>) {
3333
this.#webcontainer = webcontainerPromise;
34-
this.#broadcastChannel = new BroadcastChannel(PREVIEW_CHANNEL);
35-
this.#storageChannel = new BroadcastChannel('storage-sync-channel');
36-
37-
// Listen for preview updates from other tabs
38-
this.#broadcastChannel.onmessage = (event) => {
39-
const { type, previewId } = event.data;
40-
41-
if (type === 'file-change') {
42-
const timestamp = event.data.timestamp;
43-
const lastUpdate = this.#lastUpdate.get(previewId) || 0;
44-
45-
if (timestamp > lastUpdate) {
46-
this.#lastUpdate.set(previewId, timestamp);
47-
this.refreshPreview(previewId);
34+
this.#broadcastChannel = this.#maybeCreateChannel(PREVIEW_CHANNEL);
35+
this.#storageChannel = this.#maybeCreateChannel('storage-sync-channel');
36+
37+
if (this.#broadcastChannel) {
38+
// Listen for preview updates from other tabs
39+
this.#broadcastChannel.onmessage = (event) => {
40+
const { type, previewId } = event.data;
41+
42+
if (type === 'file-change') {
43+
const timestamp = event.data.timestamp;
44+
const lastUpdate = this.#lastUpdate.get(previewId) || 0;
45+
46+
if (timestamp > lastUpdate) {
47+
this.#lastUpdate.set(previewId, timestamp);
48+
this.refreshPreview(previewId);
49+
}
4850
}
49-
}
50-
};
51+
};
52+
}
5153

52-
// Listen for storage sync messages
53-
this.#storageChannel.onmessage = (event) => {
54-
const { storage, source } = event.data;
54+
if (this.#storageChannel) {
55+
// Listen for storage sync messages
56+
this.#storageChannel.onmessage = (event) => {
57+
const { storage, source } = event.data;
5558

56-
if (storage && source !== this._getTabId()) {
57-
this._syncStorage(storage);
58-
}
59-
};
59+
if (storage && source !== this._getTabId()) {
60+
this._syncStorage(storage);
61+
}
62+
};
63+
}
6064

6165
// Override localStorage setItem to catch all changes
6266
if (typeof window !== 'undefined') {
@@ -71,6 +75,29 @@ export class PreviewsStore {
7175
this.#init();
7276
}
7377

78+
#maybeCreateChannel(name: string): BroadcastChannel | undefined {
79+
if (typeof globalThis === 'undefined') {
80+
return undefined;
81+
}
82+
83+
const globalBroadcastChannel = (
84+
globalThis as typeof globalThis & {
85+
BroadcastChannel?: typeof BroadcastChannel;
86+
}
87+
).BroadcastChannel;
88+
89+
if (typeof globalBroadcastChannel !== 'function') {
90+
return undefined;
91+
}
92+
93+
try {
94+
return new globalBroadcastChannel(name);
95+
} catch (error) {
96+
console.warn('[Preview] BroadcastChannel unavailable:', error);
97+
return undefined;
98+
}
99+
}
100+
74101
// Generate a unique ID for this tab
75102
private _getTabId(): string {
76103
if (typeof window !== 'undefined') {
@@ -130,7 +157,7 @@ export class PreviewsStore {
130157
}
131158
}
132159

133-
this.#storageChannel.postMessage({
160+
this.#storageChannel?.postMessage({
134161
type: 'storage-sync',
135162
storage,
136163
source: this._getTabId(),
@@ -192,7 +219,7 @@ export class PreviewsStore {
192219
const timestamp = Date.now();
193220
this.#lastUpdate.set(previewId, timestamp);
194221

195-
this.#broadcastChannel.postMessage({
222+
this.#broadcastChannel?.postMessage({
196223
type: 'state-change',
197224
previewId,
198225
timestamp,
@@ -204,7 +231,7 @@ export class PreviewsStore {
204231
const timestamp = Date.now();
205232
this.#lastUpdate.set(previewId, timestamp);
206233

207-
this.#broadcastChannel.postMessage({
234+
this.#broadcastChannel?.postMessage({
208235
type: 'file-change',
209236
previewId,
210237
timestamp,
@@ -219,7 +246,7 @@ export class PreviewsStore {
219246
const timestamp = Date.now();
220247
this.#lastUpdate.set(previewId, timestamp);
221248

222-
this.#broadcastChannel.postMessage({
249+
this.#broadcastChannel?.postMessage({
223250
type: 'file-change',
224251
previewId,
225252
timestamp,

app/routes/webcontainer.preview.$id.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,22 @@ export default function WebContainerPreview() {
4646
}, [previewId, previewUrl]);
4747

4848
useEffect(() => {
49-
// Initialize broadcast channel
50-
broadcastChannelRef.current = new BroadcastChannel(PREVIEW_CHANNEL);
49+
const supportsBroadcastChannel = typeof window !== 'undefined' && typeof window.BroadcastChannel === 'function';
5150

52-
// Listen for preview updates
53-
broadcastChannelRef.current.onmessage = (event) => {
54-
if (event.data.previewId === previewId) {
55-
if (event.data.type === 'refresh-preview' || event.data.type === 'file-change') {
56-
handleRefresh();
51+
if (supportsBroadcastChannel) {
52+
broadcastChannelRef.current = new window.BroadcastChannel(PREVIEW_CHANNEL);
53+
54+
// Listen for preview updates
55+
broadcastChannelRef.current.onmessage = (event) => {
56+
if (event.data.previewId === previewId) {
57+
if (event.data.type === 'refresh-preview' || event.data.type === 'file-change') {
58+
handleRefresh();
59+
}
5760
}
58-
}
59-
};
61+
};
62+
} else {
63+
broadcastChannelRef.current = undefined;
64+
}
6065

6166
// Construct the WebContainer preview URL
6267
const url = `https://${previewId}.local-credentialless.webcontainer-api.io`;

0 commit comments

Comments
 (0)