diff --git a/.changeset/defer-subduction-connection.md b/.changeset/defer-subduction-connection.md new file mode 100644 index 00000000..07e49dfb --- /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 a `syncServers.defer` option to opt out. diff --git a/core/bootloader/src/automerge-worker.ts b/core/bootloader/src/automerge-worker.ts index 64e2d5d7..027750cd 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; + defer?: boolean; }; const syncServer = @@ -194,15 +196,34 @@ function pushSyncState(message: SyncStateDocMessage): void { } const subductionPortProvider = makePortProvider(); +let resolveSubductionStorageLoad: (() => void) | undefined; +const subductionStorageLoaded = syncServer.defer + ? 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..fe02022e 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; + 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 f8cfbe03..f4c81ff3 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. */ + 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 98fb5d93..02de8f81 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; + defer?: boolean; } { const servers = options.syncServers || undefined; if (servers?.keyhive) { @@ -21,16 +22,19 @@ export function resolvePrimarySyncServer(options: PatchworkSiteOptions): { return { keyhive: servers.keyhive, url: DEFAULT_SYNC_SERVERS[servers.keyhive], + defer: servers.defer ?? true, }; } const { url, ...identity } = servers.keyhive; return { keyhive: identity, url, + defer: servers.defer ?? true, }; } return { url: servers?.subduction ?? DEFAULT_SYNC_SERVERS.subduction, + defer: servers?.defer ?? true, }; } @@ -47,7 +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.url]; + const origins = primary.defer ? [] : [primary.url]; if (classic) origins.push(classic); return origins.map(wsToHttpOrigin); }