perf(libs): realtime only for libs the open document references
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4EzyhjhDzLdNZsFAYjz7X
This commit is contained in:
parent
826941a1a6
commit
9ea8b263cc
5 changed files with 95 additions and 2 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 29f6898c2ffdc0f6ab94bf7dbab790265c0b4a6c
|
||||
Subproject commit 46f9a8e58d22c22fd9aa1f58a471db7cbe035b6e
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
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. */
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -220,6 +220,12 @@ export function syncedLibsSource(
|
|||
opened?.then((r) => r.stack.close()).catch(() => {});
|
||||
opened = null;
|
||||
},
|
||||
async enableRealtime(): Promise<void> {
|
||||
// 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<void> {
|
||||
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)}`),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue