experimentalLiveSse throws ReferenceError: document is not defined in a Web Worker
Summary
Enabling experimentalLiveSse: true on ShapeStreamOptions crashes every shape stream immediately when the client runs inside a Web Worker (e.g. PGliteWorker). The SSE path depends on @microsoft/fetch-event-source@^2.0.1, which unconditionally touches document and window. Both are undefined in a Worker context.
Repro
@electric-sql/client@^1.0.1
@electric-sql/pglite@0.4.4
@electric-sql/pglite-sync@0.5.4
- Electric server
1.5.1
- Client runs inside
PGliteWorker (spawned via new Worker(new URL('./worker', import.meta.url), { type: 'module' })).
Minimal shape config:
await pglite.sync.syncShapeToTable({
initialInsertMethod: 'insert',
shape: {
url: new URL('/v1/shape', baseUrl).toString(),
params: { table: tableName },
experimentalLiveSse: true,
},
primaryKey: ['id'],
table: tableName,
shapeKey: tableName,
});
Error
Thrown for every shape the moment the SSE path is taken:
[shape] Error syncing event_messages_synced: ReferenceError: document is not defined
at fetch.ts:83:13
at new Promise (<anonymous>)
at fetchEventSource (fetch.ts:67:12)
at ShapeStream.<anonymous> (client.ts:784:13)
at Generator.next (<anonymous>)
at fetch.ts:154:1
at new Promise (<anonymous>)
at __async (fetch.ts:154:1)
at ShapeStream.requestShapeSSE_fn (client.ts:779:21)
at ShapeStream.<anonymous> (client.ts:747:14)
Root cause
@microsoft/fetch-event-source@2.0.1 (lib/esm/fetch.js) unconditionally accesses document and window:
// lib/esm/fetch.js
function onVisibilityChange() {
curRequestController.abort();
if (!document.hidden) { // L26
create();
}
}
if (!openWhenHidden) {
document.addEventListener('visibilitychange', onVisibilityChange); // L31
}
// ...
function dispose() {
document.removeEventListener('visibilitychange', onVisibilityChange); // L36
window.clearTimeout(retryTimer); // L37
// ...
}
const fetch = inputFetch ?? window.fetch; // L44
// ...
retryTimer = window.setTimeout(create, interval); // L74
document doesn't exist in a Web Worker, so the very first property access throws and the stream never opens. This affects any consumer that wants SSE live-updates from a worker — notably the recommended PGliteWorker architecture in the pglite-sync docs.
Proposed fix
Two possible directions:
Option A — guard the DOM accesses in fetch-event-source (minimal)
Wrap the document / window accesses with a typeof check and fall back to globalThis where appropriate. Workers don't have a visibility API, so the listener is simply skipped — which is the correct behavior: the "re-open when tab becomes visible" heuristic only makes sense for a Document-backed browsing context.
let curRequestController;
+const hasDocument = typeof document !== 'undefined';
function onVisibilityChange() {
curRequestController.abort();
- if (!document.hidden) {
+ if (hasDocument && !document.hidden) {
create();
}
}
-if (!openWhenHidden) {
+if (!openWhenHidden && hasDocument) {
document.addEventListener('visibilitychange', onVisibilityChange);
}
// ...
function dispose() {
- document.removeEventListener('visibilitychange', onVisibilityChange);
- window.clearTimeout(retryTimer);
+ if (hasDocument) {
+ document.removeEventListener('visibilitychange', onVisibilityChange);
+ }
+ (typeof window !== 'undefined' ? window : globalThis).clearTimeout(retryTimer);
curRequestController.abort();
}
// ...
-const fetch = inputFetch ?? window.fetch;
+const fetch = inputFetch ?? (typeof window !== 'undefined' ? window : globalThis).fetch;
// ...
-retryTimer = window.setTimeout(create, interval);
+retryTimer = (typeof window !== 'undefined' ? window : globalThis).setTimeout(create, interval);
This is the patch I'm running locally via yarn patch — verified working: SSE engages, shape requests show content-type: text/event-stream, long-poll cycling stops. Happy to submit this to @microsoft/fetch-event-source upstream if preferred.
Option B — swap dependency or vendor a worker-safe SSE parser (in Electric itself)
If coordinating with Microsoft isn't practical, Electric could:
- Vendor the tiny set of SSE framing logic inline (~100 lines).
- Switch to a worker-safe fork such as the one distributed with Vercel's
ai SDK.
- Feature-detect
document in Electric's own code path before invoking fetchEventSource, falling back to the long-poll request shape when running in a worker (so experimentalLiveSse: true degrades gracefully instead of crashing).
Why this matters
pglite-sync's recommended architecture puts the sync client in a Worker. Today that's flatly incompatible with experimentalLiveSse, which is the only option that reclaims the long-poll round-trip floor on live updates. Either fix unblocks SSE for the most common Electric+PGlite deployment shape.
Happy to open a PR against whichever path you prefer.
experimentalLiveSsethrowsReferenceError: document is not definedin a Web WorkerSummary
Enabling
experimentalLiveSse: trueonShapeStreamOptionscrashes every shape stream immediately when the client runs inside a Web Worker (e.g.PGliteWorker). The SSE path depends on@microsoft/fetch-event-source@^2.0.1, which unconditionally touchesdocumentandwindow. Both are undefined in a Worker context.Repro
@electric-sql/client@^1.0.1@electric-sql/pglite@0.4.4@electric-sql/pglite-sync@0.5.41.5.1PGliteWorker(spawned vianew Worker(new URL('./worker', import.meta.url), { type: 'module' })).Minimal shape config:
Error
Thrown for every shape the moment the SSE path is taken:
Root cause
@microsoft/fetch-event-source@2.0.1(lib/esm/fetch.js) unconditionally accessesdocumentandwindow:documentdoesn't exist in a Web Worker, so the very first property access throws and the stream never opens. This affects any consumer that wants SSE live-updates from a worker — notably the recommendedPGliteWorkerarchitecture in thepglite-syncdocs.Proposed fix
Two possible directions:
Option A — guard the DOM accesses in
fetch-event-source(minimal)Wrap the
document/windowaccesses with a typeof check and fall back toglobalThiswhere appropriate. Workers don't have a visibility API, so the listener is simply skipped — which is the correct behavior: the "re-open when tab becomes visible" heuristic only makes sense for a Document-backed browsing context.This is the patch I'm running locally via
yarn patch— verified working: SSE engages, shape requests showcontent-type: text/event-stream, long-poll cycling stops. Happy to submit this to@microsoft/fetch-event-sourceupstream if preferred.Option B — swap dependency or vendor a worker-safe SSE parser (in Electric itself)
If coordinating with Microsoft isn't practical, Electric could:
aiSDK.documentin Electric's own code path before invokingfetchEventSource, falling back to the long-poll request shape when running in a worker (soexperimentalLiveSse: truedegrades gracefully instead of crashing).Why this matters
pglite-sync's recommended architecture puts the sync client in a Worker. Today that's flatly incompatible withexperimentalLiveSse, which is the only option that reclaims the long-poll round-trip floor on live updates. Either fix unblocks SSE for the most common Electric+PGlite deployment shape.Happy to open a PR against whichever path you prefer.