Skip to content

Version Packages - #464

Merged
chee merged 1 commit into
mainfrom
changeset-release/main
Oct 25, 2026
Merged

chee merged 1 commit into
mainfrom
changeset-release/main

Conversation

@patchcrow

Copy link
Copy Markdown
Collaborator

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@inkandswitch/patchwork-bootloader@0.8.7

Patch Changes

  • bedd49b: Classic automerge-repo sync connects from the tab's own Repo instead of the automerge worker, so a worker restart no longer drops it and tabs no longer ask the replacement to connect again.

    connectClassicSync in @inkandswitch/patchwork-bootloader now takes the Repo: connectClassicSync(repo, server?). setupServiceWorker() no longer returns connectClassicSync. patchwork.sw.connectClassicSync(server?) works as before. The ConnectClassicSyncMessage type is gone.

  • bedd49b: The automerge worker no longer measures package memory. Removed: the @patchwork/package-memory channel and its message types (PACKAGE_MEMORY_CHANNEL, PackageMemoryRequestMessage, PackageMemoryDocument, PackageMemoryAcceptedMessage, PackageMemoryReplyMessage), and the records a restarting worker saved for its replacement (LOADED_CACHE). The service worker clears that cache along with every other cache but its own. packageDocuments is gone from @inkandswitch/patchwork-filesystem; nothing else walked a package.

  • c9d2f39: Every tab is its own Subduction node again, with its own socket to the sync server. The sync node in the automerge worker is gone. When the worker restarts (to recycle its wasm memory, for a new build, or after a trap), it takes only its resolver Repo with it. Before, every restart threw away the node every tab synced through, and each tab had to reconnect and resubscribe. The worker's resolver Repo has its own socket too.

    The automerge worker still holds AUTOMERGE_WORKER_LOCK for as long as it lives. The lock now does one thing: it lets tabs and the service worker tell when the worker has died. A replacement no longer waits for it before it builds its Repo, answers handoffs or announces itself online. An instance that's restarting already stops taking handoffs and package-memory requests, so the two don't answer at once.

    Removed from @inkandswitch/patchwork-bootloader: getSyncRemote, watchDocument, relayKeyhive and the PortSyncRemote type. Pass subductionWebsocketEndpoints: [syncServerUrl] to the Repo instead of subductionRemote: getSyncRemote(). On keyhive sites, use new Repo(config) directly rather than wrapping it in relayKeyhive. loadKeyhiveSigner is gone from @inkandswitch/patchwork-bootloader/signer. startWorkerControl no longer returns holding.

    Tabs still follow package docs without loading them. watchDocument(repo, url, onChange), now exported from @inkandswitch/patchwork, subscribes to the document through the tab's own Subduction connection and listens on the origin's heads channel. Every Repo relays the server's heads there, so the watcher hears both the server's pushes and other tabs' writes. onChange(seen) gets the same summary ModuleWatcher already compares, so a repeat costs no lookup. Subduction can't unsubscribe from a single document, so after unwatch() the server keeps pushing it to that tab until the connection drops.

  • a12981b: A build emits every file the automerge worker and the page load under a content-hashed name in /assets, served immutable, instead of at a fixed path: the /packages/… chunks, automerge.wasm, subduction.wasm, keyhive_wasm.wasm, the worker's second stage (automerge-protocol-handler.js) and module-loader-worker.js. A fixed path could only be revalidated, which fails offline, or cached on a guess, which after a deploy could start the worker on one build's files and the rest of another's. Now a build only ever loads its own copies, and they start offline from the HTTP cache.

    Only service-worker.js and the automerge worker's script, automerge-protocol-handler-worker.js, keep their fixed paths, since the browser starts both by URL from one build to the next. The worker's script is served max-age=0, stale-while-revalidate=31536000, so the browser starts it from its copy when it can't check for a newer one. Everything that copy imports is hashed, so even a copy from an older build brings up one whole build. A worker that restarts because a newer tab connected fetches its script past the cache first, so the replacement starts on the newer build rather than the cached one, which would carry on serving the newer tab.

    The fixed paths still work: the generated _redirects sends each one to its hashed name with a 302, and vite preview does the same, whether or not the site writes Netlify files. But a module imported both by a fixed path and by its hashed name is two modules, so code should import the packages by bare specifier (the import map points at the hashed names) and load the rest by the URLs in @inkandswitch/patchwork-bootloader/emitted: AUTOMERGE_WASM, SUBDUCTION_WASM, KEYHIVE_WASM, AUTOMERGE_PROTOCOL_HANDLER and MODULE_LOADER_WORKER. In dev these are the fixed paths.

    API changes:

    • emitWasmAssets (@inkandswitch/patchwork-bootloader/externals) emits the wasm under hashed names and returns each one's reference id by file name.
    • @inkandswitch/patchwork/vite has a hashed() plugin, included in patchwork(), which swaps in the hashed names. hashedFiles(config) gives a build's table of them, also exported from @inkandswitch/patchwork/vite/hashed.
    • workers entries say whether a worker is hashed. The workers no longer import the packages from /packages/… URLs: they link to the build's chunks like the page does.
    • buildHeaders(options, hashed?) takes that table for the wasm preloads.
    • The new buildRedirects(hashed?) writes the redirects. REVALIDATED_PATHS and REVALIDATED_CACHE_CONTROL name the stale-while-revalidate files and their header.

    The e2e offline test waits for the wasm by the URLs the page preloads.

  • bedd49b: The service worker asks a tab to resolve automerge: requests before falling back to the automerge worker. Every tab is its own node with its own Repo, so the tab that made a request can usually answer it from documents it already holds.

    The service worker asks the tab that made the request first, then any other open tab (focused first, then visible). This covers requests from the module-loader worker and navigations, which have no window of their own. Only when no tab can answer does it broadcast to the automerge worker, which now handles just those requests.

    The tab replies accepted straight away, or unavailable if it has no Repo yet. A tab that says nothing within 250ms is passed over. That number is provisional: the service worker logs each tab's accept latency under patchwork:serviceworker, to measure from. A tab that has accepted holds a Web Lock for that one request until it has replied. If the lock is released with no reply, the tab has gone (closed, crashed, navigated away) and the next candidate is asked.

    New in @inkandswitch/patchwork-bootloader: resolveHandoffsWith(repo) makes this tab answer with repo. @inkandswitch/patchwork calls it with the Repo it creates; a site that passes its own Repo isn't asked and keeps resolving through the automerge worker. The resolving code the worker used is shared by both, in resolver.ts. The new messages are ResolveRequestMessage, ResolveAcceptedMessage and ResolveUnavailableMessage.

  • a12981b: Fixes to how the automerge worker recycles itself to give back wasm memory:

    • The service worker no longer deletes the cache a restarting automerge worker saves what it had loaded into (patchwork-package-memory, now LOADED_CACHE in the bootloader's types). It cleared every cache but its own on each activation, so the replacement never had anything to restore.
    • A worker with classic sync connected recycles like any other. Tabs ask the replacement to connect classic sync again, after a restart and after the worker goes unexpectedly, so connecting it no longer stops the worker from ever recycling.
    • The recycle check counts subduction's wasm memory too, not only automerge's.
  • a12981b: The automerge worker's resolver Repo is on self.repo again from the moment the worker starts, for the console. The Repo is only built once something has to be resolved, so until then reading self.repo starts building it and gives its promise. await repo works either way.

  • Updated dependencies [bedd49b]

  • Updated dependencies [c9d2f39]

    • @inkandswitch/patchwork-filesystem@0.2.16

@inkandswitch/patchwork-filesystem@0.2.16

Patch Changes

  • bedd49b: The automerge worker no longer measures package memory. Removed: the @patchwork/package-memory channel and its message types (PACKAGE_MEMORY_CHANNEL, PackageMemoryRequestMessage, PackageMemoryDocument, PackageMemoryAcceptedMessage, PackageMemoryReplyMessage), and the records a restarting worker saved for its replacement (LOADED_CACHE). The service worker clears that cache along with every other cache but its own. packageDocuments is gone from @inkandswitch/patchwork-filesystem; nothing else walked a package.

  • c9d2f39: Every tab is its own Subduction node again, with its own socket to the sync server. The sync node in the automerge worker is gone. When the worker restarts (to recycle its wasm memory, for a new build, or after a trap), it takes only its resolver Repo with it. Before, every restart threw away the node every tab synced through, and each tab had to reconnect and resubscribe. The worker's resolver Repo has its own socket too.

    The automerge worker still holds AUTOMERGE_WORKER_LOCK for as long as it lives. The lock now does one thing: it lets tabs and the service worker tell when the worker has died. A replacement no longer waits for it before it builds its Repo, answers handoffs or announces itself online. An instance that's restarting already stops taking handoffs and package-memory requests, so the two don't answer at once.

    Removed from @inkandswitch/patchwork-bootloader: getSyncRemote, watchDocument, relayKeyhive and the PortSyncRemote type. Pass subductionWebsocketEndpoints: [syncServerUrl] to the Repo instead of subductionRemote: getSyncRemote(). On keyhive sites, use new Repo(config) directly rather than wrapping it in relayKeyhive. loadKeyhiveSigner is gone from @inkandswitch/patchwork-bootloader/signer. startWorkerControl no longer returns holding.

    Tabs still follow package docs without loading them. watchDocument(repo, url, onChange), now exported from @inkandswitch/patchwork, subscribes to the document through the tab's own Subduction connection and listens on the origin's heads channel. Every Repo relays the server's heads there, so the watcher hears both the server's pushes and other tabs' writes. onChange(seen) gets the same summary ModuleWatcher already compares, so a repeat costs no lookup. Subduction can't unsubscribe from a single document, so after unwatch() the server keeps pushing it to that tab until the connection drops.

@inkandswitch/patchwork@0.8.8

Patch Changes

  • bedd49b: Classic automerge-repo sync connects from the tab's own Repo instead of the automerge worker, so a worker restart no longer drops it and tabs no longer ask the replacement to connect again.

    connectClassicSync in @inkandswitch/patchwork-bootloader now takes the Repo: connectClassicSync(repo, server?). setupServiceWorker() no longer returns connectClassicSync. patchwork.sw.connectClassicSync(server?) works as before. The ConnectClassicSyncMessage type is gone.

  • c9d2f39: Every tab is its own Subduction node again, with its own socket to the sync server. The sync node in the automerge worker is gone. When the worker restarts (to recycle its wasm memory, for a new build, or after a trap), it takes only its resolver Repo with it. Before, every restart threw away the node every tab synced through, and each tab had to reconnect and resubscribe. The worker's resolver Repo has its own socket too.

    The automerge worker still holds AUTOMERGE_WORKER_LOCK for as long as it lives. The lock now does one thing: it lets tabs and the service worker tell when the worker has died. A replacement no longer waits for it before it builds its Repo, answers handoffs or announces itself online. An instance that's restarting already stops taking handoffs and package-memory requests, so the two don't answer at once.

    Removed from @inkandswitch/patchwork-bootloader: getSyncRemote, watchDocument, relayKeyhive and the PortSyncRemote type. Pass subductionWebsocketEndpoints: [syncServerUrl] to the Repo instead of subductionRemote: getSyncRemote(). On keyhive sites, use new Repo(config) directly rather than wrapping it in relayKeyhive. loadKeyhiveSigner is gone from @inkandswitch/patchwork-bootloader/signer. startWorkerControl no longer returns holding.

    Tabs still follow package docs without loading them. watchDocument(repo, url, onChange), now exported from @inkandswitch/patchwork, subscribes to the document through the tab's own Subduction connection and listens on the origin's heads channel. Every Repo relays the server's heads there, so the watcher hears both the server's pushes and other tabs' writes. onChange(seen) gets the same summary ModuleWatcher already compares, so a repeat costs no lookup. Subduction can't unsubscribe from a single document, so after unwatch() the server keeps pushing it to that tab until the connection drops.

  • a12981b: A build emits every file the automerge worker and the page load under a content-hashed name in /assets, served immutable, instead of at a fixed path: the /packages/… chunks, automerge.wasm, subduction.wasm, keyhive_wasm.wasm, the worker's second stage (automerge-protocol-handler.js) and module-loader-worker.js. A fixed path could only be revalidated, which fails offline, or cached on a guess, which after a deploy could start the worker on one build's files and the rest of another's. Now a build only ever loads its own copies, and they start offline from the HTTP cache.

    Only service-worker.js and the automerge worker's script, automerge-protocol-handler-worker.js, keep their fixed paths, since the browser starts both by URL from one build to the next. The worker's script is served max-age=0, stale-while-revalidate=31536000, so the browser starts it from its copy when it can't check for a newer one. Everything that copy imports is hashed, so even a copy from an older build brings up one whole build. A worker that restarts because a newer tab connected fetches its script past the cache first, so the replacement starts on the newer build rather than the cached one, which would carry on serving the newer tab.

    The fixed paths still work: the generated _redirects sends each one to its hashed name with a 302, and vite preview does the same, whether or not the site writes Netlify files. But a module imported both by a fixed path and by its hashed name is two modules, so code should import the packages by bare specifier (the import map points at the hashed names) and load the rest by the URLs in @inkandswitch/patchwork-bootloader/emitted: AUTOMERGE_WASM, SUBDUCTION_WASM, KEYHIVE_WASM, AUTOMERGE_PROTOCOL_HANDLER and MODULE_LOADER_WORKER. In dev these are the fixed paths.

    API changes:

    • emitWasmAssets (@inkandswitch/patchwork-bootloader/externals) emits the wasm under hashed names and returns each one's reference id by file name.
    • @inkandswitch/patchwork/vite has a hashed() plugin, included in patchwork(), which swaps in the hashed names. hashedFiles(config) gives a build's table of them, also exported from @inkandswitch/patchwork/vite/hashed.
    • workers entries say whether a worker is hashed. The workers no longer import the packages from /packages/… URLs: they link to the build's chunks like the page does.
    • buildHeaders(options, hashed?) takes that table for the wasm preloads.
    • The new buildRedirects(hashed?) writes the redirects. REVALIDATED_PATHS and REVALIDATED_CACHE_CONTROL name the stale-while-revalidate files and their header.

    The e2e offline test waits for the wasm by the URLs the page preloads.

  • bedd49b: The shipped automerge-repo patch no longer adds subductionRemote, the option that let a Repo sync through another node; nothing uses it since every tab became its own node again. The SubductionRemote, SubductionRemoteHandlers and SubductionRemoteSyncResult types are gone, as are the SubductionStorageBridge, INTERCEPTOR_PREFIX and SubductionConnections exports. subductionSaveThrottle stays. A site applying the patch from @inkandswitch/patchwork/dist/patches should take the new one.

  • bedd49b: The service worker asks a tab to resolve automerge: requests before falling back to the automerge worker. Every tab is its own node with its own Repo, so the tab that made a request can usually answer it from documents it already holds.

    The service worker asks the tab that made the request first, then any other open tab (focused first, then visible). This covers requests from the module-loader worker and navigations, which have no window of their own. Only when no tab can answer does it broadcast to the automerge worker, which now handles just those requests.

    The tab replies accepted straight away, or unavailable if it has no Repo yet. A tab that says nothing within 250ms is passed over. That number is provisional: the service worker logs each tab's accept latency under patchwork:serviceworker, to measure from. A tab that has accepted holds a Web Lock for that one request until it has replied. If the lock is released with no reply, the tab has gone (closed, crashed, navigated away) and the next candidate is asked.

    New in @inkandswitch/patchwork-bootloader: resolveHandoffsWith(repo) makes this tab answer with repo. @inkandswitch/patchwork calls it with the Repo it creates; a site that passes its own Repo isn't asked and keeps resolving through the automerge worker. The resolving code the worker used is shared by both, in resolver.ts. The new messages are ResolveRequestMessage, ResolveAcceptedMessage and ResolveUnavailableMessage.

  • Updated dependencies [bedd49b]

  • Updated dependencies [bedd49b]

  • Updated dependencies [c9d2f39]

  • Updated dependencies [a12981b]

  • Updated dependencies [bedd49b]

  • Updated dependencies [a12981b]

  • Updated dependencies [a12981b]

    • @inkandswitch/patchwork-bootloader@0.8.7
    • @inkandswitch/patchwork-filesystem@0.2.16

@inkandswitch/patchwork-e2e@0.2.4

Patch Changes

  • a12981b: A build emits every file the automerge worker and the page load under a content-hashed name in /assets, served immutable, instead of at a fixed path: the /packages/… chunks, automerge.wasm, subduction.wasm, keyhive_wasm.wasm, the worker's second stage (automerge-protocol-handler.js) and module-loader-worker.js. A fixed path could only be revalidated, which fails offline, or cached on a guess, which after a deploy could start the worker on one build's files and the rest of another's. Now a build only ever loads its own copies, and they start offline from the HTTP cache.

    Only service-worker.js and the automerge worker's script, automerge-protocol-handler-worker.js, keep their fixed paths, since the browser starts both by URL from one build to the next. The worker's script is served max-age=0, stale-while-revalidate=31536000, so the browser starts it from its copy when it can't check for a newer one. Everything that copy imports is hashed, so even a copy from an older build brings up one whole build. A worker that restarts because a newer tab connected fetches its script past the cache first, so the replacement starts on the newer build rather than the cached one, which would carry on serving the newer tab.

    The fixed paths still work: the generated _redirects sends each one to its hashed name with a 302, and vite preview does the same, whether or not the site writes Netlify files. But a module imported both by a fixed path and by its hashed name is two modules, so code should import the packages by bare specifier (the import map points at the hashed names) and load the rest by the URLs in @inkandswitch/patchwork-bootloader/emitted: AUTOMERGE_WASM, SUBDUCTION_WASM, KEYHIVE_WASM, AUTOMERGE_PROTOCOL_HANDLER and MODULE_LOADER_WORKER. In dev these are the fixed paths.

    API changes:

    • emitWasmAssets (@inkandswitch/patchwork-bootloader/externals) emits the wasm under hashed names and returns each one's reference id by file name.
    • @inkandswitch/patchwork/vite has a hashed() plugin, included in patchwork(), which swaps in the hashed names. hashedFiles(config) gives a build's table of them, also exported from @inkandswitch/patchwork/vite/hashed.
    • workers entries say whether a worker is hashed. The workers no longer import the packages from /packages/… URLs: they link to the build's chunks like the page does.
    • buildHeaders(options, hashed?) takes that table for the wasm preloads.
    • The new buildRedirects(hashed?) writes the redirects. REVALIDATED_PATHS and REVALIDATED_CACHE_CONTROL name the stale-while-revalidate files and their header.

    The e2e offline test waits for the wasm by the URLs the page preloads.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Benchmarks: Patchwork with this PR against main

396bd4b against main at 37b839c, Patchwork as it ships (patchwork mode), in chromium, firefox and webkit, each on its own runner with main and the PR measured one after the other (run). Lower is better throughout. A row is in the table when every run of the PR came out on the same side of every run of main, with the medians more than 20% apart; every benchmark is below it.

This run fails: the PR fails 3 checks main passes.

  • chromium: …the server confirmed every edit within 5s failed in run 1 of the PR's 3; main passed it in all 3 runs. Run 1: 8 of 10 edits unconfirmed; the first, edit 3: server never confirmed automerge:2fJeuxLdm3DxRxdpaP4YJfUMgdEo: it holds heads 2HvSWoxZ…,1LkJsdmY…, the tab 1LkJsdmY…
  • firefox: …server confirmed every edit (40 tabs) failed in run 3 of the PR's 3; main passed it in all 3 runs.
  • firefox: …the server confirmed every edit within 5s failed in run 2 of the PR's 3; main passed it in all 3 runs. Run 2: 6 of 10 edits unconfirmed; the first, edit 5: page.evaluate: server never confirmed automerge:LUdC284jBVwAwC9zKGszZr5g2a6: it holds heads VZ6m25LB…,1HzVdsPj…, the tab 1HzVdsPj…
browser benchmark main PR change
chromium …the server confirmed every edit within 5s ok 2/3 ok fails 1 of 3
firefox …server confirmed every edit (40 tabs) ok 2/3 ok fails 1 of 3
firefox …the server confirmed every edit within 5s ok 2/3 ok fails 1 of 3
every benchmark: the median of 3 runs of main and 3 runs of the PR, their range in brackets
browser benchmark main PR change
chromium boot → repo, first tab (1 tabs) 249 ms (243–261) 255 ms (248–270) +3%
chromium boot → server connected, first tab (1 tabs) 559 ms (545–566) 567 ms (562–573) +1%
chromium renderer memory, all tabs + workers (1 tabs) 364 MB (353–371) 360 MB (352–367) −1%
chromium renderer processes (1 tabs) 3 3
chromium boot → repo, first tab (3 tabs) 256 ms (249–268) 267 ms (251–268) +4%
chromium boot → repo, last tab (3 tabs) 196 ms (187–211) 194 ms (188–197) −1%
chromium boot → server connected, first tab (3 tabs) 576 ms (566–590) 586 ms (574–591) +2%
chromium renderer memory, all tabs + workers (3 tabs) 711 MB (702–718) 703 MB (695–711) −1%
chromium renderer processes (3 tabs) 5 5
chromium boot → repo, first tab (10 tabs) 248 ms (245–440) 261 ms (249–263) +5%
chromium boot → repo, last tab (10 tabs) 202 ms (184–207) 190 ms (187–190) −6%
chromium boot → server connected, first tab (10 tabs) 571 ms (568–770) 570 ms (564–582) −0%
chromium renderer memory, all tabs + workers (10 tabs) 1853 MB (1840–1921) 1867 MB (1835–1881) +1%
chromium renderer processes (10 tabs) 12 12
chromium sync still works after the first tab closes ok ok
chromium edit → seen + confirmed, after first tab closed 194 ms (193–195) 192 ms (191–469) −1%
chromium server link seen down while offline ok ok
chromium …and the server did not see the offline edits ok ok
chromium tabs converged while offline, over a local channel ok ok
chromium offline edits in two tabs both survive reconnect ok ok
chromium reconnect → tabs converged (only where they hadn't already) – –
chromium …and a fresh tab sees both ok ok
chromium boot → doc open, tab 1 718 ms (715–726) 721 ms (721–735) +0%
chromium boot → doc open, tab 2 345 ms (341–354) 336 ms (333–353) −3%
chromium boot → doc open, tab 10 317 ms (312–355) 298 ms (293–304) −6%
chromium open 40 tabs, wall clock 9409 ms (9244–10624) 9459 ms (9365–9739) +1%
chromium boot → repo, last of 40 tabs 75 ms (72–80) 75 ms (61–81) −1%
chromium boot → linked, last of 40 tabs – –
chromium boot → server connected, last of 40 tabs 538 ms (516–724) 674 ms (628–753) +25%
chromium renderer memory, 40 tabs 7245 MB (7145–7342) 7198 MB (7138–7374) −1%
chromium renderer processes, 40 tabs 42 42
chromium 39 tabs find the doc: median 869 ms (846–870) 872 ms (851–880) +0%
chromium 39 tabs find the doc: slowest 1010 ms (1003–1014) 977 ms (954–986) −3%
chromium …all 39 found it ok ok
chromium …finds that needed a retry (of 39) 0 0
chromium edit → seen in 39 tabs: median tab 53 ms (51–53) 53 ms (52–58) +1%
chromium edit → seen in 39 tabs: slowest tab 140 ms (131–148) 132 ms (125–140) −5%
chromium …tabs that saw every edit (of 39) 39 39
chromium edit → server holds our heads (40 tabs) 208 ms (204–213) 208 ms (207–214) −0%
chromium …server confirmed every edit (40 tabs) ok ok
chromium …longest main-thread stall in tab 0 during the edits 29 ms (14–36) 49 ms (13–74) +71%
chromium sync rounds per sibling edit, non-editing tab 0 0
chromium server bytes received per edit per tab – –
chromium storage writes (saveBatch) per edit per tab – –
chromium worker errors (40 tabs) – –
chromium a fresh tab finds a closed tab's doc (no server) ok ok
chromium …time to find it, from storage 41 ms (39–47) 38 ms (35–44) −7%
chromium …and every edit made before the close is in it ok ok
chromium edits from tabs closed right after editing all survive ok ok
chromium …or at least 5s later ok ok
chromium find a doc another tab just created 58 ms (52–59) 53 ms (52–60) −10%
chromium …and every such find() succeeded within 5s ok ok
chromium …finds that settled unavailable first (of 2) 0 0
chromium edit → seen in 2 other tabs 7 ms (7–8) 8 ms (8–9) +3%
chromium …every edit reached every other tab within 5s ok ok
chromium edit → server holds our heads 151 ms (151–155) 151 ms (151–152) −0%
chromium …the server confirmed every edit within 5s ok 2/3 ok fails 1 of 3
chromium …and a stranger gets the whole doc from the server ok ok
chromium renderer memory, 1 tab 364 MB (355–371) 362 MB (356–364) −1%
chromium renderer memory, 10 tabs 1870 MB (1865–1871) 1866 MB (1861–1889) −0%
firefox boot → repo, first tab (1 tabs) 368 ms (365–381) 370 ms (354–398) +1%
firefox boot → server connected, first tab (1 tabs) 758 ms (641–780) 728 ms (653–888) −4%
firefox renderer memory, all tabs + workers (1 tabs) – –
firefox renderer processes (1 tabs) – –
firefox boot → repo, first tab (3 tabs) 369 ms (357–371) 364 ms (362–394) −1%
firefox boot → repo, last tab (3 tabs) 267 ms (253–279) 265 ms (261–266) −1%
firefox boot → server connected, first tab (3 tabs) 707 ms (665–725) 726 ms (687–739) +3%
firefox renderer memory, all tabs + workers (3 tabs) – –
firefox renderer processes (3 tabs) – –
firefox boot → repo, first tab (10 tabs) 358 ms (354–359) 368 ms (363–369) +3%
firefox boot → repo, last tab (10 tabs) 273 ms (263–277) 261 ms (258–297) −4%
firefox boot → server connected, first tab (10 tabs) 752 ms (712–909) 902 ms (778–937) +20%
firefox renderer memory, all tabs + workers (10 tabs) – –
firefox renderer processes (10 tabs) – –
firefox sync still works after the first tab closes ok ok
firefox edit → seen + confirmed, after first tab closed 107 ms (100–109) 100 ms (99–111) −7%
firefox server link seen down while offline ok ok
firefox …and the server did not see the offline edits ok ok
firefox tabs converged while offline, over a local channel ok ok
firefox offline edits in two tabs both survive reconnect ok ok
firefox reconnect → tabs converged (only where they hadn't already) – –
firefox …and a fresh tab sees both ok ok
firefox boot → doc open, tab 1 733 ms (732–851) 884 ms (690–935) +21%
firefox boot → doc open, tab 2 362 ms (343–500) 521 ms (499–522) +44%
firefox boot → doc open, tab 10 358 ms (355–515) 497 ms (496–498) +39%
firefox open 40 tabs, wall clock 17593 ms (17475–24640) 24320 ms (24103–24488) +38%
firefox boot → repo, last of 40 tabs 121 ms (113–132) 142 ms (122–154) +18%
firefox boot → linked, last of 40 tabs – –
firefox boot → server connected, last of 40 tabs 460 ms (413–624) 626 ms (596–637) +36%
firefox renderer memory, 40 tabs – –
firefox renderer processes, 40 tabs – –
firefox 39 tabs find the doc: median 293 ms (291–316) 292 ms (290–329) −0%
firefox 39 tabs find the doc: slowest 307 ms (304–322) 302 ms (296–361) −2%
firefox …all 39 found it ok ok
firefox …finds that needed a retry (of 39) 0 0
firefox edit → seen in 39 tabs: median tab 98 ms (97–100) 95 ms (94–99) −3%
firefox edit → seen in 39 tabs: slowest tab 172 ms (170–185) 190 ms (178–190) +10%
firefox …tabs that saw every edit (of 39) 39 39
firefox edit → server holds our heads (40 tabs) 296 ms (284–310) 279 ms (268–289) −6%
firefox …server confirmed every edit (40 tabs) ok 2/3 ok fails 1 of 3
firefox …longest main-thread stall in tab 0 during the edits 118 ms (103–125) 122 ms (122–136) +4%
firefox sync rounds per sibling edit, non-editing tab 0 0
firefox server bytes received per edit per tab – –
firefox storage writes (saveBatch) per edit per tab – –
firefox worker errors (40 tabs) – –
firefox a fresh tab finds a closed tab's doc (no server) ok ok
firefox …time to find it, from storage 18 ms (16–20) 17 ms (17–18) −6%
firefox …and every edit made before the close is in it ok ok
firefox edits from tabs closed right after editing all survive ok ok
firefox …or at least 5s later ok ok
firefox find a doc another tab just created 25 ms (22–26) 23 ms (23–24) −7%
firefox …and every such find() succeeded within 5s ok ok
firefox …finds that settled unavailable first (of 2) 0 0
firefox edit → seen in 2 other tabs 12 ms (11–13) 13 ms (12–14) +7%
firefox …every edit reached every other tab within 5s ok ok
firefox edit → server holds our heads 78 ms (77–83) 73 ms (72–77) −6%
firefox …the server confirmed every edit within 5s ok 2/3 ok fails 1 of 3
firefox …and a stranger gets the whole doc from the server ok ok
webkit boot → repo, first tab (1 tabs) 455 ms (434–462) 450 ms (449–455) −1%
webkit boot → server connected, first tab (1 tabs) 521 ms (518–545) 524 ms (518–525) +1%
webkit renderer memory, all tabs + workers (1 tabs) – –
webkit renderer processes (1 tabs) – –
webkit boot → repo, first tab (3 tabs) 473 ms (433–481) 460 ms (443–464) −3%
webkit boot → repo, last tab (3 tabs) 379 ms (364–379) 380 ms (361–385) +0%
webkit boot → server connected, first tab (3 tabs) 551 ms (550–561) 527 ms (521–536) −4%
webkit renderer memory, all tabs + workers (3 tabs) – –
webkit renderer processes (3 tabs) – –
webkit boot → repo, first tab (10 tabs) 458 ms (437–460) 455 ms (434–473) −1%
webkit boot → repo, last tab (10 tabs) 396 ms (385–416) 396 ms (392–415)
webkit boot → server connected, first tab (10 tabs) 550 ms (543–568) 547 ms (517–547) −1%
webkit renderer memory, all tabs + workers (10 tabs) – –
webkit renderer processes (10 tabs) – –
webkit sync still works after the first tab closes ok ok
webkit edit → seen + confirmed, after first tab closed 65 ms (60–74) 60 ms (57–62) −8%
webkit server link seen down while offline ok ok
webkit …and the server did not see the offline edits ok ok
webkit tabs converged while offline, over a local channel ok ok
webkit offline edits in two tabs both survive reconnect ok ok
webkit reconnect → tabs converged (only where they hadn't already) – –
webkit …and a fresh tab sees both ok ok
webkit boot → doc open, tab 1 664 ms (650–765) 651 ms (638–656) −2%
webkit boot → doc open, tab 2 489 ms (473–512) 497 ms (453–522) +2%
webkit boot → doc open, tab 10 539 ms (516–576) 506 ms (491–532) −6%
webkit open 40 tabs, wall clock 31082 ms (31038–31413) 30886 ms (30781–30989) −1%
webkit boot → repo, last of 40 tabs 194 ms (193–267) 208 ms (194–229) +7%
webkit boot → linked, last of 40 tabs – –
webkit boot → server connected, last of 40 tabs 495 ms (468–562) 504 ms (497–504) +2%
webkit renderer memory, 40 tabs – –
webkit renderer processes, 40 tabs – –
webkit 39 tabs find the doc: median 603 ms (594–665) 559 ms (538–607) −7%
webkit 39 tabs find the doc: slowest 944 ms (900–945) 730 ms (603–907) −23%
webkit …all 39 found it ok ok
webkit …finds that needed a retry (of 39) 0 0
webkit edit → seen in 39 tabs: median tab 115 ms (88–118) 94 ms (92–97) −18%
webkit edit → seen in 39 tabs: slowest tab 310 ms (256–310) 269 ms (224–300) −13%
webkit …tabs that saw every edit (of 39) 39 39
webkit edit → server holds our heads (40 tabs) 319 ms (300–321) 331 ms (299–353) +4%
webkit …server confirmed every edit (40 tabs) ok ok
webkit …longest main-thread stall in tab 0 during the edits 53 ms (41–80) 66 ms (51–95) +25%
webkit sync rounds per sibling edit, non-editing tab 0 0
webkit server bytes received per edit per tab – –
webkit storage writes (saveBatch) per edit per tab – –
webkit worker errors (40 tabs) – –
webkit a fresh tab finds a closed tab's doc (no server) ok ok
webkit …time to find it, from storage 80 ms (63–92) 83 ms (67–94) +4%
webkit …and every edit made before the close is in it ok ok
webkit edits from tabs closed right after editing all survive ok ok
webkit …or at least 5s later ok ok
webkit find a doc another tab just created 43 ms (40–48) 47 ms (41–50) +9%
webkit …and every such find() succeeded within 5s ok ok
webkit …finds that settled unavailable first (of 2) 0 0
webkit edit → seen in 2 other tabs 14 ms (13–15) 14 ms (13–14) +4%
webkit …every edit reached every other tab within 5s ok ok
webkit edit → server holds our heads 38 ms (36–41) 38 ms (37–45) −1%
webkit …the server confirmed every edit within 5s ok ok
webkit …and a stranger gets the whole doc from the server ok ok

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

🪡 Patchwork

Torn down.

@chee
chee merged commit 912c770 into main Oct 25, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants