Repository navigation
fix(vrowzer): isolate projects that share a Service Worker registration - #73
Merged
Merged
Conversation
The Web Worker has the project files, the module graph and the plugins, so it can answer a whole request with the Vite middlewares. This is the first step of #45, where the Service Worker only forwards requests to the Web Worker that owns the project. - createRequestPipeline() builds the Vite middlewares in the Web Worker, in the order of Vite's dev server: base, public files, transform, raw fs, static files, the HTML fallback, the functions that the configureServer hooks return, index.html, 404 and the error handler. - The configureServer hooks get these middlewares as server.middlewares. - setupHMR() returns the public file list that its watcher handlers keep up to date, and the public file middleware uses it. - The new handleRequest RPC answers a forwarded request. Requests and responses cross the port as plain data, and their bodies are transferred instead of copied. - The test plugins of the integration fixtures find the Web Worker's dev server by its environments, since it has middlewares now. The Service Worker still answers requests itself. It forwards them in the next commit.
…o the Web Worker The Service Worker no longer runs the Vite middlewares. It forwards each request within the base path to the Web Worker with handleRequest, and answers with the Web Worker's response and the cross-origin isolation headers. It still forwards the HMR ports of the previews. createServer() of @vrowzer/vite-dev-server/service-worker takes only the version, the base path and ownerWaitTimeout now. The Service Worker does not resolve the Vite config, and has neither plugins nor the public file list: - A request that comes before a Web Worker connects, e.g. right after the browser restarted the Service Worker, waits for ownerWaitTimeout (10 s by default), then gets a 503. This replaces the beforeRequest and onWorkerChannelReady hooks of #46, and vrowzer's wait for the project files and the channel. - An error on the way to the Web Worker gets a plain 500. The Web Worker answers the errors of the project with Vite's error page. Only the Web Worker, which has environments, uses the error middleware now, so logError() is back to Vite's form. - The Web Worker no longer sends the safe module paths to the Service Worker, which serves no files. - initServiceWorker() of vrowzer/service-worker-core no longer takes plugins. Plugins run in the Web Worker. - The old browser test of the Service Worker server, which no test project ran, is removed. The Service Worker still receives the project files. The next commit stops sending them.
The Service Worker forwards the preview requests to the Web Worker, so it needs no copy of the project. The project files now live only in the Web Worker: - ready() gives the files to the Web Worker only. The Service Worker gets no V_FS_INIT, no later V_FS_* messages, and no virtual file system. - A file operation resolves once the Web Worker acknowledges it, and times out waiting for the Web Worker only. - The recovery after a Service Worker restart (#46) only connects the Web Worker channel again. The runtime no longer keeps a copy of the files for it, and no longer holds the file operations called in the meantime, since the Web Worker can apply them right away. - The error page test checks that the Web Worker names the failed plugin. serviceWorkerRecovered and serviceWorkerRecoveryError are emitted as before, when the channel is connected again or cannot be within fileSyncTimeout.
The Web Worker now runs the middlewares that plugins add in configureServer. A test plugin of the playground adds one in the hook and one in the function that the hook returns. The test checks that the first runs before the internal middlewares, and the second after them and before the SPA fallback to index.html.
- README: the Architecture section describes how the Service Worker forwards the requests within basePath to the Web Worker, which has the project files and runs the Vite middlewares. A NOTE describes the change from vrowzer 0.4.x: the Service Worker no longer takes plugins with initServiceWorker(), and the middlewares that plugins add in configureServer run in the Web Worker. - README and JSDoc: file operations wait for the Web Worker only, and a restarted Service Worker only gets the Web Worker channel again, while file operations go on. - The architecture diagram drops the Service Worker's file subscriber, virtual file system and Vite dev server, and shows the V_FS_* messages going to the Web Worker only. - The recovery error message says that the restarted Service Worker could not be connected to the Web Worker. - The API references are regenerated.
Each Vrowzer instance now has an ID, 12 hexadecimal digits from 48 random bits, created when Vrowzer() is called. Its previews load from its own base path, basePath followed by the ID, e.g. /__preview__/0123456789ab/. The path is also the Vite base of its Web Worker, so import.meta.env.BASE_URL in the preview returns it. - The new read-only previewBasePath returns the path. It is set right after Vrowzer() and stays the same after dispose(). - The ID replaces the runtimeId that V_WW_CONNECT_PORT has carried since #46. It no longer uses crypto.randomUUID(), so it does not change the order of the preview load tokens. - The integration and e2e tests read preview files under the previewBasePath of their instance. The Service Worker still forwards every request within basePath to the Web Worker that connected last. The next commit routes the requests by the ID in the path.
…Worker The Service Worker kept one Web Worker channel, the last one connected, so two runtimes that share it, e.g. in two tabs, got each other's previews, transforms and HMR updates (#45). The Service Worker now keeps the Web Worker channels by runtime ID, from V_WW_CONNECT_PORT, and routes by the ID in the preview URL: - A request goes to the Web Worker of the runtime named by the first path segment within the base path, e.g. /__preview__/0123456789ab/. - A request for a runtime that has not connected waits up to ownerWaitTimeout, e.g. right after a restart, then gets a 503. A request without a runtime ID in its URL gets a 404 right away. - A new channel of the same runtime replaces and closes its previous one. - The HMR client sends the Vite base of the preview with vite:mc:init, and the Service Worker forwards the port to the Web Worker of that runtime. A port without a runtime in its base is closed. - V_WW_CONNECT_PORT_ACK names the runtime, and a runtime takes only its own acknowledgement, since runtimes in the same page receive each other's. A connection without a valid runtime ID is closed.
A disposed runtime left its Web Worker channel in the Service Worker, which forwarded the requests of its previews to a terminated Web Worker until they timed out. - dispose() sends the new V_WW_DISCONNECT_PORT with the runtime ID, once the instance has sent its Web Worker channel. The Service Worker closes the channel, and answers the requests and HMR ports of the runtime's previews with 404 and a closed port from then on. Requests that wait for the runtime to connect get the 404 as well. - A closed page cannot send the message, so when a runtime connects, the Service Worker releases the runtimes whose pages clients.matchAll() no longer lists. - A released runtime that connects again is accepted.
The new isolation tests open two pages in one browser context, which share the Service Worker registration as tabs do. Each test checks a criterion of #45, and those carried over from #43 and #46: - Both tabs have /index.html and /src/main.ts with different contents, initialized in both orders, and each preview shows its own. - Files that one tab updates, adds or deletes stay in its project, also after both previews reload. - While the Web Worker of one tab holds a transform, the other tab edits, and the held response and the HMR updates reach their own tabs. - After one tab is disposed or closed, the other one keeps loading assets, editing and reloading. The previews of the disposed tab get 404, and so do those of the closed tab once another runtime connects. - The previews of one tab share its updates. - After the Service Worker restarts, each tab restores its own project. Without the routing by runtime ID, the first test fails.
- README: basePath is the start of the preview URLs. Each instance loads its previews from its own path under it, which the new previewBasePath returns, so several instances, e.g. in two tabs, can share a Service Worker. A NOTE describes the change from vrowzer 0.4.x: the preview URLs and import.meta.env.BASE_URL now have the instance ID, and hosts that build preview URLs from basePath should use previewBasePath. - README: dispose() releases the previews of the instance in the Service Worker, and a closed page's previews are released when another instance connects. - JSDoc of basePath and dispose(), and the architecture diagram. - The API references are regenerated, with previewBasePath.
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 #45
Two tabs of the same application can now use separate Vrowzer projects while they share one Service Worker registration. Each tab sees only its own files, transforms and HMR updates, and keeps working when the other tab is disposed or closed.
Cause
The Service Worker held the state of one project only:
V_FS_*messages of every tab wrote to;In a browser, two tabs with the same file paths overwrote each other's files, all transforms and HMR ports went to the Web Worker that connected last, and disposing that Web Worker made the other tab's preview HTML fail with a 500 after the 30-second RPC timeout.
Fix
The change has two stages, and each commit can be read on its own.
Stage A: the Web Worker answers the preview requests (commits 1–5)
The Web Worker already has the project files, the module graph and the plugins, so it now answers whole requests:
createRequestPipeline()), and answers each request through a newhandleRequestRPC.server.middlewaresinconfigureServer. The functions that the hooks return add their middlewares after the internal ones, as in Vite.ArrayBuffers instead of copies.plugin,pluginCode), and the HMR error payload is sent as in Vite.Stage B: route by owner (commits 6–10)
Vrowzer()instance gets an ID of 12 hexadecimal digits. Its previews load frombasePathfollowed by the ID, e.g./__preview__/0123456789ab/, which the new read-onlypreviewBasePathreturns. The path is also the Vitebaseof its Web Worker.V_WW_CONNECT_PORT.runtimeId), and routes each request by the ID in its URL.ownerWaitTimeout), then gets a 503.vite:mc:initcarries the Vitebaseof the preview, and the Service Worker forwards the HMR port to the Web Worker of that runtime.V_WW_CONNECT_PORT_ACKnames the runtime, and each runtime takes only its own acknowledgement. Runtimes in the same page receive each other's.dispose()sends the newV_WW_DISCONNECT_PORT. A closed tab cannot, so when a runtime connects, the Service Worker releases the runtimes whose pagesclients.matchAll()no longer lists.Performance
Measured locally with a temporary benchmark test, two runs each. Each value is the mean of 200 reads from the preview iframe.
main(ms)main.main, where the Service Worker served static files, it waited for up to 34–39 ms. Usual modules transform in well under a millisecond.Acceptance criteria
The new
isolation.integration-test.tsopens two pages in one browser context, which share the Service Worker registration as tabs do./index.htmland/src/main.tsat the same paths, initialized in both ordersBreaking change
basePathfollowed by an instance ID (previewBasePath), not frombasePathitself, andimport.meta.env.BASE_URLin the preview ispreviewBasePath. Hosts that build preview URLs frombasePathshould usepreviewBasePath.initServiceWorker()ofvrowzer/service-worker-coreno longer takesplugins. The Service Worker runs no plugins; give them to the Web Worker, as@vrowzer/vite-plugindoes with the plugins ofvite.config.ts.configureServerrun in the Web Worker. Up to 0.4.x, the Web Worker's dev server had nomiddlewares, so they ran only in the Service Worker, for the plugins given to it.createServer()of@vrowzer/vite-dev-server/service-workertakes(scope, { version, basePath, ownerWaitTimeout })and forwards requests to the Web Workers. It no longer takes a Vite config,plugins,watcherFactory,beforeRequestoronWorkerChannelReady, and the module no longer exports the config, HMR, middleware and module graph APIs.The README has a NOTE for the first three. A footer for the squash commit:
Other changes
V_FS_*messages, andV_WW_CONNECT_PORTrequires a validruntimeId.public-files-sync.ts(fix(vite-dev-server): serve public files given to the runtime from the Service Worker #64). The Web Worker serves public files with the list that its watcher handlers keep, as Vite does.beforeRequestmiddleware and theonWorkerChannelReadyhook (Recover project files and channels after Service Worker process restart #46, not released)logError()for a server withoutenvironments(fix(vite-dev-server): use Vite's error handler for Service Worker requests #58)Tests
Unit tests:
requestTransport.test.ts(new): serializing requests and responses, and the transfer list of birpc messages, including through aMessageChannel.requestPipeline.test.ts(new): the order of the middlewares, the post hooks, errors, and requests outside the base path.service-worker.test.ts(new): forwarding by runtime ID, two owners kept apart, the waits, 404 and 503, HMR ports, replaced channels,V_WW_DISCONNECT_PORT, and closed pages.web-worker.test.ts: the pipeline around theconfigureServerhooks, andhandleRequest.index.test.ts,index-file-sync.test.ts,index-recovery.test.ts,index-dispose.test.ts:previewBasePath, the files sent to the Web Worker only, the channel-only recovery, the own acknowledgement, and the release ondispose().The new and changed tests failed before the changes.
Integration tests:
isolation.integration-test.ts(new), as in the table above.vrowzer.integration-test.ts: plugin middlewares in the Web Worker, before and after the internal middlewares, and the plugin name on the error page. The tests read the preview files underpreviewBasePath.I checked that tests fail without parts of the fix:
I ran these checks locally:
vp run test:unit: 122 files, 1,974 tests (3 skipped)vp run test:integration:vrowzer: 85 passed and 1 conditional skip, in five runsvp run test:e2e: 46 passed and 6 existingtest.skip, in 81.9 s