From 7209aa3d8f83c63d2f535710c2d49b1fdf322406 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:19:18 +0000 Subject: [PATCH 1/3] Initial plan From aca12ad0b0ed247e2260e0ff778bcb09b7b50eea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:34:30 +0000 Subject: [PATCH 2/3] Delay Subduction connection until storage loads Co-authored-by: chee <178266+chee@users.noreply.github.com> --- .changeset/defer-subduction-connection.md | 6 +++ core/bootloader/src/automerge-worker.ts | 45 ++++++++++++++++--- .../src/deferred-websocket-endpoint.ts | 30 +++++++++++++ core/patchwork/src/repo.ts | 1 + core/patchwork/src/site-kit/options.ts | 2 + core/patchwork/src/site-kit/sync-servers.ts | 11 ++++- 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 .changeset/defer-subduction-connection.md create mode 100644 core/bootloader/src/deferred-websocket-endpoint.ts diff --git a/.changeset/defer-subduction-connection.md b/.changeset/defer-subduction-connection.md new file mode 100644 index 00000000..d750326c --- /dev/null +++ b/.changeset/defer-subduction-connection.md @@ -0,0 +1,6 @@ +--- +"@inkandswitch/patchwork": patch +"@inkandswitch/patchwork-bootloader": patch +--- + +Patchwork loads persisted Subduction data before opening its sync connection, with an option to opt out. diff --git a/core/bootloader/src/automerge-worker.ts b/core/bootloader/src/automerge-worker.ts index 64e2d5d7..f5cc8d4b 100644 --- a/core/bootloader/src/automerge-worker.ts +++ b/core/bootloader/src/automerge-worker.ts @@ -39,6 +39,7 @@ import { import { DEFAULT_CLASSIC_SYNC_SERVER } from "./sync-config.js"; import { keyhiveStorageName, storagePrefix } from "./storage.js"; +import { DeferredWebSocketEndpoint } from "./deferred-websocket-endpoint.js"; import { HANDOFF_CHANNEL, SYNCSTATE_CHANNEL, @@ -56,6 +57,7 @@ import { declare const __SYNC_SERVER__: { url: string; keyhive?: SyncServerSelection; + connectSubductionAfterStorageLoad?: boolean; }; const syncServer = @@ -194,15 +196,34 @@ function pushSyncState(message: SyncStateDocMessage): void { } const subductionPortProvider = makePortProvider(); +let resolveSubductionStorageLoad: (() => void) | undefined; +const subductionStorageLoaded = syncServer.connectSubductionAfterStorageLoad + ? new Promise((resolve) => { + resolveSubductionStorageLoad = resolve; + }) + : undefined; // Memoized so a construction retry reuses the endpoint instead of leaking one // per attempt. -let subductionEndpoints: WorkerWebSocketEndpoint[] | null = null; -function getSubductionEndpoints(): WorkerWebSocketEndpoint[] { +type SubductionEndpoint = { + readonly url: string; + connect(): ReturnType; + shutdown?(): void; +}; + +let subductionEndpoints: SubductionEndpoint[] | null = null; +function getSubductionEndpoints(): SubductionEndpoint[] { return (subductionEndpoints ??= [ - new WorkerWebSocketEndpoint(syncServer.url, { - worker: subductionPortProvider.source, - }), + (subductionStorageLoaded + ? new DeferredWebSocketEndpoint( + new WorkerWebSocketEndpoint(syncServer.url, { + worker: subductionPortProvider.source, + }), + subductionStorageLoaded + ) + : new WorkerWebSocketEndpoint(syncServer.url, { + worker: subductionPortProvider.source, + })), ]); } @@ -237,6 +258,20 @@ async function setUpRepoHive(): Promise { ? await buildKeyhiveRepo(syncServer.keyhive) : await buildPlainRepo(); + if (resolveSubductionStorageLoad) { + const release = resolveSubductionStorageLoad; + resolveSubductionStorageLoad = undefined; + void (async () => { + const subduction = await built.repo.subduction; + await Promise.all( + (await subduction.sedimentreeIds()).map((id) => subduction.getBlobs(id)) + ); + log("subduction storage loaded"); + })() + .catch((error) => log("subduction storage load failed", error)) + .finally(release); + } + (self as any).repo = built.repo; if (built.hive) (self as any).hive = built.hive; if (built.identity) (self as any).syncIdentity = built.identity; diff --git a/core/bootloader/src/deferred-websocket-endpoint.ts b/core/bootloader/src/deferred-websocket-endpoint.ts new file mode 100644 index 00000000..cc71941b --- /dev/null +++ b/core/bootloader/src/deferred-websocket-endpoint.ts @@ -0,0 +1,30 @@ +type WebSocketEndpoint = { + readonly url: string; + connect(): Promise; + shutdown?(): void; +}; + +/** + * Defers opening a WebSocket until the supplied startup work has completed. + */ +export class DeferredWebSocketEndpoint + implements WebSocketEndpoint +{ + constructor( + private endpoint: WebSocketEndpoint, + private ready: Promise + ) {} + + get url(): string { + return this.endpoint.url; + } + + async connect(): Promise { + await this.ready; + return this.endpoint.connect(); + } + + shutdown(): void { + this.endpoint.shutdown?.(); + } +} diff --git a/core/patchwork/src/repo.ts b/core/patchwork/src/repo.ts index bc1231fb..e743935d 100644 --- a/core/patchwork/src/repo.ts +++ b/core/patchwork/src/repo.ts @@ -29,6 +29,7 @@ const log = debug("patchwork:setup:repo"); declare const __SYNC_SERVER__: { url: string; keyhive?: SyncServerSelection; + connectSubductionAfterStorageLoad?: boolean; }; const syncServer = typeof __SYNC_SERVER__ !== "undefined" diff --git a/core/patchwork/src/site-kit/options.ts b/core/patchwork/src/site-kit/options.ts index f8cfbe03..a6b92661 100644 --- a/core/patchwork/src/site-kit/options.ts +++ b/core/patchwork/src/site-kit/options.ts @@ -48,6 +48,8 @@ export type PatchworkKeyhiveSyncServer = export type PatchworkSyncServersOptions = { /** wss:// URL for the legacy automerge-repo sync-server channel (connected on demand via connectClassicSync). Default: wss://sync3.automerge.org. Pass false to skip its preconnect hint. */ classic?: string | false; + /** Wait for persisted Subduction data before opening the primary sync connection. Defaults to true. */ + connectSubductionAfterStorageLoad?: boolean; } & PatchworkPrimarySyncServerOptions; export const DEFAULT_TITLE = "Patchwork"; diff --git a/core/patchwork/src/site-kit/sync-servers.ts b/core/patchwork/src/site-kit/sync-servers.ts index 98fb5d93..29373487 100644 --- a/core/patchwork/src/site-kit/sync-servers.ts +++ b/core/patchwork/src/site-kit/sync-servers.ts @@ -14,6 +14,7 @@ export const DEFAULT_SYNC_SERVERS = { export function resolvePrimarySyncServer(options: PatchworkSiteOptions): { url: string; keyhive?: SyncServerSelection; + connectSubductionAfterStorageLoad?: boolean; } { const servers = options.syncServers || undefined; if (servers?.keyhive) { @@ -21,16 +22,22 @@ export function resolvePrimarySyncServer(options: PatchworkSiteOptions): { return { keyhive: servers.keyhive, url: DEFAULT_SYNC_SERVERS[servers.keyhive], + connectSubductionAfterStorageLoad: + servers.connectSubductionAfterStorageLoad ?? true, }; } const { url, ...identity } = servers.keyhive; return { keyhive: identity, url, + connectSubductionAfterStorageLoad: + servers.connectSubductionAfterStorageLoad ?? true, }; } return { url: servers?.subduction ?? DEFAULT_SYNC_SERVERS.subduction, + connectSubductionAfterStorageLoad: + servers?.connectSubductionAfterStorageLoad ?? true, }; } @@ -47,7 +54,9 @@ export function resolveSyncServers(options: PatchworkSiteOptions): string[] { if (options.syncServers === false) return []; const primary = resolvePrimarySyncServer(options); const classic = options.syncServers?.classic ?? DEFAULT_SYNC_SERVERS.classic; - const origins = [primary.url]; + const origins = primary.connectSubductionAfterStorageLoad + ? [] + : [primary.url]; if (classic) origins.push(classic); return origins.map(wsToHttpOrigin); } From 6034eb8cee4ab322140f458fd4eefb5964544b5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:36:16 +0000 Subject: [PATCH 3/3] Rename deferred sync option Co-authored-by: chee <178266+chee@users.noreply.github.com> --- .changeset/defer-subduction-connection.md | 2 +- core/bootloader/src/automerge-worker.ts | 4 ++-- core/patchwork/src/repo.ts | 2 +- core/patchwork/src/site-kit/options.ts | 2 +- core/patchwork/src/site-kit/sync-servers.ts | 15 +++++---------- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.changeset/defer-subduction-connection.md b/.changeset/defer-subduction-connection.md index d750326c..07e49dfb 100644 --- a/.changeset/defer-subduction-connection.md +++ b/.changeset/defer-subduction-connection.md @@ -3,4 +3,4 @@ "@inkandswitch/patchwork-bootloader": patch --- -Patchwork loads persisted Subduction data before opening its sync connection, with an option to opt out. +Patchwork loads persisted Subduction data before opening its sync connection, with a `syncServers.defer` option to opt out. diff --git a/core/bootloader/src/automerge-worker.ts b/core/bootloader/src/automerge-worker.ts index f5cc8d4b..027750cd 100644 --- a/core/bootloader/src/automerge-worker.ts +++ b/core/bootloader/src/automerge-worker.ts @@ -57,7 +57,7 @@ import { declare const __SYNC_SERVER__: { url: string; keyhive?: SyncServerSelection; - connectSubductionAfterStorageLoad?: boolean; + defer?: boolean; }; const syncServer = @@ -197,7 +197,7 @@ function pushSyncState(message: SyncStateDocMessage): void { const subductionPortProvider = makePortProvider(); let resolveSubductionStorageLoad: (() => void) | undefined; -const subductionStorageLoaded = syncServer.connectSubductionAfterStorageLoad +const subductionStorageLoaded = syncServer.defer ? new Promise((resolve) => { resolveSubductionStorageLoad = resolve; }) diff --git a/core/patchwork/src/repo.ts b/core/patchwork/src/repo.ts index e743935d..fe02022e 100644 --- a/core/patchwork/src/repo.ts +++ b/core/patchwork/src/repo.ts @@ -29,7 +29,7 @@ const log = debug("patchwork:setup:repo"); declare const __SYNC_SERVER__: { url: string; keyhive?: SyncServerSelection; - connectSubductionAfterStorageLoad?: boolean; + defer?: boolean; }; const syncServer = typeof __SYNC_SERVER__ !== "undefined" diff --git a/core/patchwork/src/site-kit/options.ts b/core/patchwork/src/site-kit/options.ts index a6b92661..f4c81ff3 100644 --- a/core/patchwork/src/site-kit/options.ts +++ b/core/patchwork/src/site-kit/options.ts @@ -49,7 +49,7 @@ export type PatchworkSyncServersOptions = { /** wss:// URL for the legacy automerge-repo sync-server channel (connected on demand via connectClassicSync). Default: wss://sync3.automerge.org. Pass false to skip its preconnect hint. */ classic?: string | false; /** Wait for persisted Subduction data before opening the primary sync connection. Defaults to true. */ - connectSubductionAfterStorageLoad?: boolean; + defer?: boolean; } & PatchworkPrimarySyncServerOptions; export const DEFAULT_TITLE = "Patchwork"; diff --git a/core/patchwork/src/site-kit/sync-servers.ts b/core/patchwork/src/site-kit/sync-servers.ts index 29373487..02de8f81 100644 --- a/core/patchwork/src/site-kit/sync-servers.ts +++ b/core/patchwork/src/site-kit/sync-servers.ts @@ -14,7 +14,7 @@ export const DEFAULT_SYNC_SERVERS = { export function resolvePrimarySyncServer(options: PatchworkSiteOptions): { url: string; keyhive?: SyncServerSelection; - connectSubductionAfterStorageLoad?: boolean; + defer?: boolean; } { const servers = options.syncServers || undefined; if (servers?.keyhive) { @@ -22,22 +22,19 @@ export function resolvePrimarySyncServer(options: PatchworkSiteOptions): { return { keyhive: servers.keyhive, url: DEFAULT_SYNC_SERVERS[servers.keyhive], - connectSubductionAfterStorageLoad: - servers.connectSubductionAfterStorageLoad ?? true, + defer: servers.defer ?? true, }; } const { url, ...identity } = servers.keyhive; return { keyhive: identity, url, - connectSubductionAfterStorageLoad: - servers.connectSubductionAfterStorageLoad ?? true, + defer: servers.defer ?? true, }; } return { url: servers?.subduction ?? DEFAULT_SYNC_SERVERS.subduction, - connectSubductionAfterStorageLoad: - servers?.connectSubductionAfterStorageLoad ?? true, + defer: servers?.defer ?? true, }; } @@ -54,9 +51,7 @@ export function resolveSyncServers(options: PatchworkSiteOptions): string[] { if (options.syncServers === false) return []; const primary = resolvePrimarySyncServer(options); const classic = options.syncServers?.classic ?? DEFAULT_SYNC_SERVERS.classic; - const origins = primary.connectSubductionAfterStorageLoad - ? [] - : [primary.url]; + const origins = primary.defer ? [] : [primary.url]; if (classic) origins.push(classic); return origins.map(wsToHttpOrigin); }