v157 · javascript · service workers
Expose ProgressEvent in service workers
ProgressEvent has always been available on the window and in dedicated and shared workers, but not inside a ServiceWorkerGlobalScope — so a service worker tracking a long fetch() had to invent its own progress message shape. Chrome 157 exposes the interface there too, aligning service workers with every other worker context and with the XMLHttpRequest Standard.
concepts
-
Global-scope probe
Register a real service worker, ask it whether
ProgressEventexists in its global, and have it construct one withlengthComputable,loadedandtotal. The answer comes back overpostMessagefrom inside the worker, not from a guess made on the page. -
Which scopes have it
Probe
ProgressEventin four realms at once — window, dedicated worker, shared worker, service worker — and see exactly where the gap was. Each cell is a live construction attempt in that realm, not a support table. -
Streaming download relay
The use case that motivated it: a service worker intercepts a download, counts bytes off the
ReadableStream, and reports progress to the page as a realProgressEventrather than an ad-hoc object — with the pre-157 hand-rolled shape beside it for comparison.
why it was missing
ProgressEvent is defined by the XMLHttpRequest Standard, and its [Exposed] extended attribute historically listed (Window,DedicatedWorker,SharedWorker) — the realms where XHR itself lives. Service workers never had XHR, so the event came along for the ride and was left out.
But the interface is not really about XHR any more: it is the standard shape for “this operation is n of m bytes through”, and service workers are exactly where long transfers get intercepted. Without it, every service worker that wanted to report progress invented its own { loaded, total } message and every page had to know that worker's private convention.
the API
// Inside the service worker (Chrome 157+)
self.addEventListener("fetch", (event) => {
event.respondWith((async () => {
const response = await fetch(event.request);
const total = Number(response.headers.get("content-length")) || 0;
let loaded = 0;
const stream = new ReadableStream({
async start(controller) {
const reader = response.body.getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
loaded += value.byteLength;
// The standard shape, constructible here for the first time:
const progress = new ProgressEvent("progress", {
lengthComputable: total > 0,
loaded,
total,
});
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage({
type: progress.type,
lengthComputable: progress.lengthComputable,
loaded: progress.loaded,
total: progress.total,
});
}
controller.enqueue(value);
}
controller.close();
},
});
return new Response(stream, { headers: response.headers });
})());
});
The event still has to be serialised across postMessage — an Event is not structured-cloneable — but constructing it in the worker means the field names, types and lengthComputable semantics come from the platform rather than from a convention you documented in a README.