Repository navigation
fix(vrowzer): restore the project after the Service Worker restarts - #72
Merged
Merged
Conversation
writeFile() and unlink() accept an operation id, which a Worker can acknowledge with V_FS_ACK, but V_FS_INIT had no id. FSInitMessage now has an optional id, and initFiles() takes the same options as writeFile() and unlink() and sets the id on the message for every target. The vrowzer runtime will send V_FS_INIT with an id to restore a restarted Service Worker, and wait for its acknowledgement. The README and the API references describe the option.
…t its Web Worker channel When the browser restarts the Service Worker process, the Service Worker loses the project files and the Web Worker channel, while the registration and the controller stay the same. Its owner has to restore both, and requests within the base path must not be answered before that. createServer() now takes two hooks: - beforeRequest() is called before a request within the base path is handled, and the request waits for its promise. When it resolves to a Response, that response is returned instead, rebuilt through the Hono context so that the cross-origin isolation headers are kept. The middleware runs before the configureServer hooks, so the middlewares of plugins wait as well. - onWorkerChannelReady() is called each time the channel handshake with a Web Worker completes, before the client is told that the connection is established. The message types add V_SW_INSTANCE_STARTED, which a Service Worker instance sends to its window clients when listen() completes, an instanceId on V_SW_LISTEN_READY, and an optional runtimeId on V_WW_CONNECT_PORT that marks the owner of the channel for when several runtimes share one Service Worker.
…ew one connects The Web Worker accepted a new port from V_SW_CONNECT_PORT at any time, but kept the previous port and its birpc open. After the browser restarts the Service Worker process, the runtime connects a new port, and calls made over the previous one would wait for replies that never come. When a new port arrives, the Web Worker now closes the previous port and its birpc, which rejects the calls still waiting on it. A port that a newer one replaces during its handshake, or while it sends the safe module paths, is closed without an acknowledgement, because V_SW_CONNECT_PORT_ACK does not name the port it acknowledges.
… project When the browser restarts the Service Worker process, the new instance has no project files and no Web Worker channel, so the preview HTML got a 404, modules a 500 and public files a 404 until the host page was reloaded. The runtime could not tell that the Service Worker had restarted, because the registration and the controller stay the same. Each evaluation of the Service Worker script now creates an instance ID. It is sent with V_SW_LISTEN_READY, and when listen() completes, V_SW_INSTANCE_STARTED with the ID is sent to every window client the instance controls. A runtime that knows another ID can then restore the project. A request within the base path now waits until this instance has received both the project files (V_FS_INIT) and the Web Worker channel. ready() sends both before the preview opens, so normal startup does not wait. After 10 seconds, the same as the default fileSyncTimeout, the request gets a 503 with a short explanation, which the preview reports as a load error. V_FS_INIT with an id is acknowledged like V_FS_WRITE and V_FS_UNLINK, and project files that fail to apply do not count.
The browser stops an idle Service Worker and starts it again for the next request or message. The restarted Service Worker had lost the project files and the Web Worker channel, so the preview broke until the host page was reloaded, and the runtime did not notice. The runtime now remembers the Service Worker instance ID from V_SW_LISTEN_READY, and listens for V_SW_INSTANCE_STARTED. When an instance with another ID starts, it restores the project there: - It keeps the latest files given to the Workers: the files of ready(), including the default index.html and the client files, updated whenever addFile(), updateFile() or deleteFile() is sent. It sends them to the Service Worker only, with V_FS_INIT and an id. - When the Service Worker acknowledges the files, operations that were sent before and still wait for the Service Worker are settled, because the files include them. The previous instance can no longer acknowledge them. - Then it connects the Web Worker channel again with establishChannel(), and emits serviceWorkerRecovered. File operations called during a recovery are held and sent in order after it, and their fileSyncTimeout counts from then. An ArrayBuffer is now copied when the operation is called, so a held write sends the content of that time. A recovery that does not finish within fileSyncTimeout, or whose files the Service Worker fails to apply, emits serviceWorkerRecoveryError and rejects the operations that wait for the Service Worker. Later operations are sent as usual, and the next restart is tried again. A newer instance that starts during a recovery replaces it, an instance that starts during ready() is restored once ready() completes, and dispose() stops a recovery without sending anything more. V_WW_CONNECT_PORT carries a runtime ID, created when ready() starts, to mark the owner of the channel for when several runtimes share one Service Worker.
The new restart fixture gives ready() a module, files to change and delete, public files and a binary file, without /index.html. The tests stop the Service Worker process with ServiceWorker.stopAllWorkers of the DevTools Protocol, which keeps the registration, and start it again with a request. They use their own browser and a new browser context for each test, because stopping affects every page that uses the Service Worker. - After file changes and a restart, the preview HTML, modules, public files and binary files have the latest contents, a deleted file gets a 404, and no request on the way gets a 404 or a 500. - After a restart, file changes reach the open preview by HMR, the preview loads assets, reloadPreview() loads the latest contents, and a preview mounted afterwards gets HMR updates as well, without reloading the host page. - A request pending when the stop is requested ends, and the module is served after the recovery. The browser stops the Service Worker only after the request ends, so a test plugin holds it in the Web Worker until a file is written. - Three restarts in a row are each recovered. - Without the Web Worker, serviceWorkerRecoveryError is emitted within fileSyncTimeout, a file change made meanwhile rejects, a preview request gets a 503 after 10 seconds, and a reloaded preview reports the 503. - An instance disposed during a recovery sends neither V_FS_INIT nor V_WW_CONNECT_PORT afterwards, also after another restart. Without the recovery in the runtime, the restarted Service Worker answers with a 503 after 10 seconds and the first test fails. Without the requests held in the Service Worker, the first request after the restart gets a 500.
A new README section, "Service Worker restarts", describes how Vrowzer restores the project when the browser restarts an idle Service Worker: the restarted Service Worker notifies the pages it controls, Vrowzer sends the latest files from its copy and connects the Web Worker channel again, and emits serviceWorkerRecovered. Preview requests wait for up to 10 seconds, file operations are held until the project is restored, and mounted previews keep their HMR connection. When the restoration fails, serviceWorkerRecoveryError is emitted, and the section shows how to start over with a new instance. The README events section describes serviceWorkerRecovered and serviceWorkerRecoveryError. The descriptions of fileSyncTimeout, the file operations and dispose(), in the README and in the JSDoc, say how they relate to the restoration. The API references are regenerated.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #46
When the browser restarts the Service Worker process, Vrowzer now restores the project in the restarted Service Worker, without a reload of the host page: the latest files and the Web Worker channel.
Cause
The browser stops an idle Service Worker and starts it again for the next request or message. The registration and the controller stay the same, and no
activateorcontrollerchangeevent fires. The restarted Service Worker had lost the project files, the Web Worker channel, the public file list and the safe module paths. It had only the files that its own script writes. The preview HTML got a 404, modules a 500 and public files a 404, until the host page was reloaded. The runtime did not notice, because it stops checking the Service Worker onceready()completes.A browser test reproduced this with
ServiceWorker.stopAllWorkersof the DevTools Protocol.Fix
V_SW_LISTEN_READYcarries the ID.listen()completes, the Service Worker sendsV_SW_INSTANCE_STARTEDwith the ID to every window client that it controls.ready(), and restores the project when an instance with another ID starts.activatebroadcast of a new version does not start a recovery.ready(), including the defaultindex.htmland the client files.addFile(),updateFile()ordeleteFile()is sent.V_FS_INITand an id, which the Service Worker acknowledges.@vrowzer/fs,V_FS_INITcan carry an id, andinitFiles()takes the same options aswriteFile()andunlink().establishChannel()again. When a new port connects, the Web Worker now closes the previous port and its birpc.fileSyncTimeout. After that, it gets a 503 with a short explanation, which the preview reports as a load error.@vrowzer/vite-dev-servergets two hooks for this:beforeRequest()andonWorkerChannelReady().ready()sends both the files and the channel before the preview opens, so a normal startup does not wait.fileSyncTimeoutcounts from then.ArrayBufferis now copied when the operation is called.serviceWorkerRecoveredis emitted after a recovery.fileSyncTimeout, or when the Service Worker fails to apply the files. A failure emitsserviceWorkerRecoveryError, and rejects the operations that wait for the Service Worker.ready()is restored onceready()completes.dispose()stops a recovery, and nothing more is sent.Behavior changes
serviceWorkerRecoveredandserviceWorkerRecoveryError.V_SW_LISTEN_READYcarries an instance ID, andV_WW_CONNECT_PORTcarries an optional runtime ID.There are no breaking changes.
Scope
ready().V_WW_CONNECT_PORTnow carries a runtime ID, which Isolate projects sharing a Service Worker registration #45 can use to tell the owners apart.listen()completes: a request that starts the Service Worker before its server listens goes to the network.@vrowzer/vite-pluginanswers it with its guard page.reloadSuggestedand so on).Tests
Unit tests:
service-worker-core.test.ts:V_SW_LISTEN_READYand the start notice.V_FS_INITwith an id is acknowledged.index-recovery.test.ts(new): detection, the restored files, the order of the recovery, held and settled operations, the timeout of held operations, failures, overlapping recoveries anddispose().controller-ready-timeout.test.ts: the controller keeps the instance ID fromV_SW_LISTEN_READY.beforeRequest.test.ts(new): the hook holds requests, and a response from the hook keeps the cross-origin isolation headers.web-worker.test.ts: a new port closes the previous one, including a port that a new one replaced during its handshake or while it sent the safe paths.publisher.test.ts:initFiles()sets the operation id.The new tests failed before the change.
Integration tests: the new file
restart.integration-test.tsuses its own fixture (integration/restart/) and browser, and a new browser context for each test. Each test stops the Service Worker with the DevTools Protocol and starts it again with a request.index.html), modules, public and binary files have the latest contents. A deleted file gets a 404, and no request on the way gets a 404 or a 500.reloadPreview()serviceWorkerRecoveryErroris emitted withinfileSyncTimeout.V_FS_INITnorV_WW_CONNECT_PORTafterwards, even after another restart.I checked that the tests fail without parts of the fix:
I ran these checks locally:
vp run test:unit: 121 files, 1,953 tests (3 skipped)vp run test:integration:vrowzer: 76 passed and 1 conditional skip, in six runsvp run test:e2e: 46 passed, 6 existingtest.skip, in 87 s, about as long as before