From 9ea8b263ccf536264b51eb4cd8e8b00f517c9017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Fri, 31 Jul 2026 15:43:11 +0200 Subject: [PATCH] perf(libs): realtime only for libs the open document references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under realtime "shared-only" a board/schematic session holds no socket per org lib — which silently broke the lib-update toast (a peer editing a PLACED symbol never reached the open session live). Complete the design with the deferred-realtime upgrade: after open, scan the staged target document for lib-table nicknames (lib_id / footprint / lib_symbols tokens) and promote exactly those libs' stacks to realtime via the new LibsSource.enableRealtime. One socket per lib the document actually uses (typically 0-5) instead of one per lib in scope (60+); every other lib still catches up on the next load via the descriptor digests. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W4EzyhjhDzLdNZsFAYjz7X --- web/pcbjam-shared | 2 +- web/standalone/src/components/WasmTool.tsx | 27 +++++++++++++++- web/standalone/src/wasm/kicad-runner.ts | 32 +++++++++++++++++++ web/standalone/src/wasm/libs/source.ts | 10 ++++++ web/standalone/src/wasm/libs/synced-source.ts | 26 +++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/web/pcbjam-shared b/web/pcbjam-shared index 29f6898..46f9a8e 160000 --- a/web/pcbjam-shared +++ b/web/pcbjam-shared @@ -1 +1 @@ -Subproject commit 29f6898c2ffdc0f6ab94bf7dbab790265c0b4a6c +Subproject commit 46f9a8e58d22c22fd9aa1f58a471db7cbe035b6e diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index 867c9b4..ad62cb8 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -60,7 +60,12 @@ import { type ModelsLoadingDetail, } from "@/wasm/libs/models-bridge"; import { memfsFilePath, memfsProjectDir, TOOL_BUNDLE, TOOL_FRAME } from "@/wasm/constants"; -import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner"; +import { + driveProjectIntoTool, + readStagedFile, + usedLibNicknames, + type ToolFile, +} from "@/wasm/kicad-runner"; import { dump as dumpTrace, mark } from "@/wasm/load-trace"; import { registerSaveHook, type SaveBytes } from "@/wasm/save-flow"; import type { @@ -1797,6 +1802,26 @@ export function WasmTool({ append(`[collab] attach failed — continuing without collab: ${String(err)}`); } } + // Deferred-realtime upgrade: the scope libs source opens its stacks + // channel-less (no socket per org lib), so promote the libs the OPEN + // DOCUMENT references — a peer editing a PLACED symbol must still + // reach this session live (lib-update toast); everything else syncs + // on the next load. Fire-and-forget: boot never waits on sockets. + if (targetPath && source?.enableRealtime) { + const staged = readStagedFile(win, slug, targetPath); + const nicks = staged + ? usedLibNicknames(new TextDecoder().decode(staged)) + : []; + append( + `[libs] doc references ${nicks.length} lib nickname(s)` + + (staged ? "" : " (target not staged?)"), + ); + if (nicks.length) { + void source + .enableRealtime(nicks) + .catch((e) => append(`[libs] realtime upgrade: ${String(e)}`)); + } + } // Lib editors: the enumerate gate holds their whole-set hydrate until // the presync settles — wait for it here too, so the boot overlay (with // its ticking lib line) stays up instead of revealing an empty tree. diff --git a/web/standalone/src/wasm/kicad-runner.ts b/web/standalone/src/wasm/kicad-runner.ts index 2df7296..14187a0 100644 --- a/web/standalone/src/wasm/kicad-runner.ts +++ b/web/standalone/src/wasm/kicad-runner.ts @@ -71,6 +71,38 @@ export function restageFile( log(`[memfs] wrote ${dest} (${bytes.length} bytes)`); } +/** Read one staged project file back from the tool's MEMFS (null if absent). + * Counterpart of {@link restageFile}; used post-open to inspect the target + * document (e.g. which lib nicknames it references). */ +export function readStagedFile( + win: ToolWindow, + slug: string, + relPath: string, +): Uint8Array | null { + try { + const fs = getFS(win) as unknown as { + readFile(path: string): Uint8Array; + }; + return fs.readFile(memfsFilePath(slug, relPath)); + } catch { + return null; + } +} + +/** + * Lib-table nicknames the document references: placed symbols (`lib_id`), + * board footprints (`footprint`), and the embedded `lib_symbols` cache + * (`symbol "NICK:NAME"`). Text scan, not a parse — nicknames land in quoted + * `NICK:NAME` tokens in all three shapes, and a stray match only costs a + * no-op realtime upgrade for a name that resolves to nothing. + */ +export function usedLibNicknames(text: string): string[] { + const out = new Set(); + const re = /\((?:lib_id|footprint|symbol)\s+"([^":]+):[^"]*"/g; + for (let m = re.exec(text); m; m = re.exec(text)) out.add(m[1]!); + return [...out]; +} + /** How many project files are fetched at once by the MEMFS staging below. * Matches the lib presync's default: enough to hide per-request latency on a * many-file project, low enough not to starve the parallel wasm download. */ diff --git a/web/standalone/src/wasm/libs/source.ts b/web/standalone/src/wasm/libs/source.ts index 1236790..5df5ed7 100644 --- a/web/standalone/src/wasm/libs/source.ts +++ b/web/standalone/src/wasm/libs/source.ts @@ -144,6 +144,16 @@ export interface LibsSource { * Optional: stateless sources omit it. */ dispose?(): void; + /** + * Promote the named libs (by display name / lib-table nickname) to realtime + * sync — the deferred-realtime upgrade. Bulk sources open their stacks + * channel-less (a board session must not hold one socket per org lib); the + * editor calls this with the libs the OPEN DOCUMENT references, so a peer's + * edit to a placed symbol still reaches the session live (lib-update toast) + * while the ~150 unreferenced libs stay socket-free. Optional: sources that + * are always-realtime (or never) omit it. + */ + enableRealtime?(libNames: string[]): Promise; } /** diff --git a/web/standalone/src/wasm/libs/synced-source.ts b/web/standalone/src/wasm/libs/synced-source.ts index bb55864..2a96eed 100644 --- a/web/standalone/src/wasm/libs/synced-source.ts +++ b/web/standalone/src/wasm/libs/synced-source.ts @@ -220,6 +220,12 @@ export function syncedLibsSource( opened?.then((r) => r.stack.close()).catch(() => {}); opened = null; }, + async enableRealtime(): Promise { + // Names are the SCOPE source's concern (it fans out per lib); a one-lib + // source just promotes its own stack. + const { stack } = await ensure(); + stack.connectRealtime(); + }, }; } @@ -459,5 +465,25 @@ export function syncedScopeLibsSource( for (const src of perLib.values()) src.dispose?.(); perLib.clear(); }, + async enableRealtime(libNames): Promise { + if (libNames.length === 0) return; + // Nickname → lib id via the backend listing (one request; the wasm boot + // has usually made the same call already). Names that don't resolve — + // project-local table rows, stale nicknames — are simply not ours. + const wanted = new Set(libNames); + const libs = (await remote.listLibs()).filter((l) => wanted.has(l.name)); + opts.log?.( + `[synced] realtime upgrade for ${libs.length}/${libNames.length} referenced lib(s)`, + ); + await Promise.all( + libs.map((l) => + forLib(l.id) + .enableRealtime?.([]) + ?.catch((e) => + opts.log?.(`[synced] realtime upgrade failed for ${l.name}: ${String(e)}`), + ), + ), + ); + }, }; }