feat: live lib edit — cmd+s accelerators, editing-context overlay, lib-set realtime (libs 0015)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166YJapMpPB8Ab7wT8Nx51t
This commit is contained in:
parent
4e089ae455
commit
aeacadf603
11 changed files with 312 additions and 7 deletions
|
|
@ -172,7 +172,12 @@
|
|||
// (wxWasmYieldUntil inside the load) find a tracked record and get the
|
||||
// green-region spill-stack discipline. Embind names live on Module WITHOUT
|
||||
// the underscore prefix, hence the separate installer.
|
||||
PARKER_NAMES: ["kicadOpenFile", "kicadOpenFiles", "kicadLibsReload"],
|
||||
PARKER_NAMES: [
|
||||
"kicadOpenFile",
|
||||
"kicadOpenFiles",
|
||||
"kicadLibsReload",
|
||||
"kicadLibsAddEntry",
|
||||
],
|
||||
_wrapParkers: function () {
|
||||
var wrapped = 0;
|
||||
for (var i = 0; i < this.PARKER_NAMES.length; i++) {
|
||||
|
|
|
|||
|
|
@ -2104,6 +2104,9 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
|||
function("kicadCollabTestClearSelection", &schCollabTestClearSelection);
|
||||
// Library reload after a remote (synced) lib edit — r2-idb-sync realtime.
|
||||
function("kicadLibsReload", &pcbjam_libs::reloadLibrary PCBJAM_PARKER_POLICY);
|
||||
// Runtime lib-table row insert + load (a new team library appeared
|
||||
// mid-session; the lib set is otherwise frozen at boot).
|
||||
function("kicadLibsAddEntry", &pcbjam_libs::addLibraryEntry PCBJAM_PARKER_POLICY);
|
||||
// Placed-instance count for a library symbol (drives the "a symbol you are
|
||||
// using was updated" toast after a remote lib edit).
|
||||
function("kicadLibsSymbolUsage", &schLibsSymbolUsage);
|
||||
|
|
|
|||
|
|
@ -596,6 +596,9 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
|
|||
function("kicadCollabTestClearSelection", &collabTestClearSelection);
|
||||
// Library reload after a remote (synced) lib edit — r2-idb-sync realtime.
|
||||
function("kicadLibsReload", &pcbjam_libs::reloadLibrary PCBJAM_PARKER_POLICY);
|
||||
// Runtime lib-table row insert + load (a new team library appeared
|
||||
// mid-session; the lib set is otherwise frozen at boot).
|
||||
function("kicadLibsAddEntry", &pcbjam_libs::addLibraryEntry PCBJAM_PARKER_POLICY);
|
||||
// Placed-instance count for a library symbol (schematic sessions only —
|
||||
// 0 from any other frame; drives the "symbol you are using was updated"
|
||||
// toast after a remote lib edit).
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
#include <kiway.h>
|
||||
#include <kiway_player.h>
|
||||
#include <libraries/library_manager.h>
|
||||
#include <libraries/library_table.h>
|
||||
#include <mail_type.h>
|
||||
#include <pgm_base.h>
|
||||
|
||||
|
|
@ -76,6 +77,74 @@ inline void reloadLibrary( std::string aKind, std::string aNickname )
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one PCBJAM lib-table row at RUNTIME and load it — the lib SET is
|
||||
* otherwise frozen at boot (sym/fp-lib-table are written once in preRun).
|
||||
* Used by the "a new team library appeared" flow: the JS side has already
|
||||
* created the `/mnt/pcbjam/<id>` MEMFS placeholder; this inserts the row into
|
||||
* the LIVE in-memory global table (the table FILE is only re-read by
|
||||
* LoadGlobalTables, which would drop every adapter cache — deliberately not
|
||||
* used), loads the entry through the provider bridge, and re-syncs any open
|
||||
* editor tree. Idempotent: an existing nickname is a no-op.
|
||||
*
|
||||
* @param aKind "symbol" | "footprint" — which lib table gets the row.
|
||||
* @param aNickname the lib-table row name (LibInfo.name on the JS side).
|
||||
* @param aUri the virtual mount uri ("/mnt/pcbjam/<libId>").
|
||||
*/
|
||||
inline void addLibraryEntry( std::string aKind, std::string aNickname, std::string aUri )
|
||||
{
|
||||
KIWAY_PLAYER* top =
|
||||
wxTheApp ? dynamic_cast<KIWAY_PLAYER*>( wxTheApp->GetTopWindow() ) : nullptr;
|
||||
|
||||
if( !top )
|
||||
return;
|
||||
|
||||
const bool fp = aKind == "footprint";
|
||||
const wxString nick = wxString::FromUTF8( aNickname.c_str() );
|
||||
const wxString uri = wxString::FromUTF8( aUri.c_str() );
|
||||
|
||||
pcbjam_collab::runOnCoroutine( top, [top, fp, nick, uri]()
|
||||
{
|
||||
LIBRARY_MANAGER& mgr = Pgm().GetLibraryManager();
|
||||
const LIBRARY_TABLE_TYPE type =
|
||||
fp ? LIBRARY_TABLE_TYPE::FOOTPRINT : LIBRARY_TABLE_TYPE::SYMBOL;
|
||||
|
||||
if( std::optional<LIBRARY_MANAGER_ADAPTER*> adapter = mgr.Adapter( type ); adapter )
|
||||
{
|
||||
LIBRARY_TABLE* table = ( *adapter )->GlobalTable();
|
||||
|
||||
if( !table )
|
||||
return;
|
||||
|
||||
// Idempotent-cheap: a nickname that already has a row is done —
|
||||
// content refresh is kicadLibsReload's job, and doing it here
|
||||
// would let a re-announce trigger a pointless re-fat-load.
|
||||
if( table->HasRow( nick ) )
|
||||
return;
|
||||
|
||||
LIBRARY_TABLE_ROW& row = table->InsertRow();
|
||||
row.SetNickname( nick );
|
||||
row.SetURI( uri );
|
||||
row.SetType( fp ? wxS( "PCBJAM_FP" ) : wxS( "PCBJAM" ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Bring the new row to LOADED through the JS provider bridge (GetRow
|
||||
// walks the table on a cache miss, so the inserted row is found).
|
||||
mgr.LoadLibraryEntry( type, nick );
|
||||
|
||||
// Open editor frames re-sync their tree; the nickname payload forces
|
||||
// the named node's rebuild past the pinned-modify-hash gate (same
|
||||
// reasoning as reloadLibrary above).
|
||||
std::string payload( nick.utf8_str() );
|
||||
top->Kiway().ExpressMail( fp ? FRAME_FOOTPRINT_EDITOR : FRAME_SCH_SYMBOL_EDITOR,
|
||||
MAIL_RELOAD_LIB, payload );
|
||||
} );
|
||||
}
|
||||
|
||||
} // namespace pcbjam_libs
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
|
|
|||
|
|
@ -2451,6 +2451,9 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
|||
function("kicadCollabTestClearSelection", &pcbCollabTestClearSelection);
|
||||
// Library reload after a remote (synced) lib edit — r2-idb-sync realtime.
|
||||
function("kicadLibsReload", &pcbjam_libs::reloadLibrary PCBJAM_PARKER_POLICY);
|
||||
// Runtime lib-table row insert + load (a new team library appeared
|
||||
// mid-session; the lib set is otherwise frozen at boot).
|
||||
function("kicadLibsAddEntry", &pcbjam_libs::addLibraryEntry PCBJAM_PARKER_POLICY);
|
||||
#endif // !KICAD_MERGED_EMBIND
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0230fde538a4b6f2bf07b2e7856c3f8d440205fa
|
||||
Subproject commit b2af70c0001c52aaa8a408cc3c9ed63d3eeb4806
|
||||
|
|
@ -48,13 +48,22 @@ import {
|
|||
LIB_ERROR_EVENT,
|
||||
LIB_ITEM_UPDATED_EVENT,
|
||||
LIB_LOADING_EVENT,
|
||||
LIB_SET_CHANGED_EVENT,
|
||||
type LibBusyDetail,
|
||||
type LibErrorDetail,
|
||||
type LibItemUpdatedDetail,
|
||||
type LibLoadingDetail,
|
||||
type LibSetChangedDetail,
|
||||
type LibsSource,
|
||||
type LibsSyncState,
|
||||
} from "@/wasm/libs/source";
|
||||
import { addAnnouncedLib } from "@/wasm/libs/runtime-add";
|
||||
|
||||
/** The libset toast's message once live-loading failed and reload is the offer. */
|
||||
function reloadFallbackMsg(notice: { detail: LibSetChangedDetail }): string {
|
||||
const label = notice.detail.name ? `"${notice.detail.name}"` : "the new library";
|
||||
return `Couldn't load ${label} into the running session — click to reload the editor.`;
|
||||
}
|
||||
import {
|
||||
MODELS_LOADING_EVENT,
|
||||
type ModelsLoadingDetail,
|
||||
|
|
@ -69,7 +78,7 @@ import {
|
|||
import { resolveSheetHierarchy } from "@/wasm/collab/sheet-hierarchy";
|
||||
import { dump as dumpTrace, mark } from "@/wasm/load-trace";
|
||||
import { errorMessage, isTerminalError } from "@/wasm/terminal-error";
|
||||
import { registerSaveHook, type SaveBytes } from "@/wasm/save-flow";
|
||||
import { registerSaveHook, type SaveBlock, type SaveBytes } from "@/wasm/save-flow";
|
||||
import type {
|
||||
KicadCollabHandle,
|
||||
KicadDocSession,
|
||||
|
|
@ -1110,6 +1119,24 @@ export function WasmTool({
|
|||
// The backend rolled this document back to its last valid state
|
||||
// (kicad-validity 0001 — DOC_REVERTED_EVENT from the collab binding).
|
||||
const [docReverted, setDocReverted] = React.useState<string | null>(null);
|
||||
// A peer changed the team's lib SET mid-session (LIB_SET_CHANGED_EVENT —
|
||||
// the scope room's `libset` broadcast). The lib table is frozen at boot, so
|
||||
// the toast's click action loads the new lib live (addAnnouncedLib), with a
|
||||
// reload fallback when the runtime bridge is missing.
|
||||
const [libSetNotice, setLibSetNotice] = React.useState<{
|
||||
message: string;
|
||||
detail: LibSetChangedDetail;
|
||||
mode: "load" | "reload";
|
||||
} | null>(null);
|
||||
// The one libs source instance the running editor uses (set by the boot
|
||||
// effect) — the libset toast's action needs it to re-list and load.
|
||||
const activeLibsSourceRef = React.useRef<LibsSource | null>(null);
|
||||
// A save path entered the DURABLE blocked state (409 conflict / unknown
|
||||
// commit state — save-flow's absorbing blockedPaths). Rendered as a
|
||||
// persistent banner, never auto-dismissed: further Ctrl+S on the path is
|
||||
// silently absorbed, so without this surface the user would keep "saving"
|
||||
// into the void.
|
||||
const [saveBlocked, setSaveBlocked] = React.useState<SaveBlock | null>(null);
|
||||
// Eager whole-library idb→wasm load in flight (the ~tens-of-seconds fat-load on
|
||||
// first chooser/editor open). Drives a full-cover overlay so the freeze reads as
|
||||
// "loading, just slow" rather than a hang. Null when idle; `done/total` count the
|
||||
|
|
@ -1262,6 +1289,19 @@ export function WasmTool({
|
|||
};
|
||||
const onItemUpdated = (e: Event) => {
|
||||
const d = (e as CustomEvent<LibItemUpdatedDetail>).detail;
|
||||
// Footprints have no placed-usage bridge (kicadLibsSymbolUsage is
|
||||
// symbol-only), so every applied peer edit is announced — silently
|
||||
// refreshing the lib under the user was the worse failure mode.
|
||||
if (d.kind === "footprint") {
|
||||
if (d.names.length === 0) return;
|
||||
const names = d.names.map((n) => `"${n}"`).join(", ");
|
||||
setLibUpdate(
|
||||
`${d.names.length === 1 ? "Footprint" : "Footprints"} ${names} in "${d.lib}" ` +
|
||||
`${d.names.length === 1 ? "was" : "were"} updated by a collaborator — ` +
|
||||
`placed copies keep the previous version until updated from the library.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Only warn when the update touches something PLACED here — the library
|
||||
// tree already reflects updates to everything else.
|
||||
if (d.usedNames.length === 0) return;
|
||||
|
|
@ -1279,15 +1319,30 @@ export function WasmTool({
|
|||
`was detected${d?.reason ? ` (${d.reason})` : ""}. Recent edits may have been undone.`,
|
||||
);
|
||||
};
|
||||
const onLibSet = (e: Event) => {
|
||||
const d = (e as CustomEvent<LibSetChangedDetail>).detail;
|
||||
// Only additions get a call to action — a removed lib's table row is
|
||||
// inert until the next boot and needs no interruption.
|
||||
if (d.op !== "add") return;
|
||||
setLibSetNotice({
|
||||
message: d.name
|
||||
? `A collaborator added library "${d.name}" — click to load it into this session.`
|
||||
: `A collaborator added a new library — click to load it into this session.`,
|
||||
detail: d,
|
||||
mode: "load",
|
||||
});
|
||||
};
|
||||
window.addEventListener(LIB_BUSY_EVENT, onBusy);
|
||||
window.addEventListener(LIB_ERROR_EVENT, onError);
|
||||
window.addEventListener(LIB_ITEM_UPDATED_EVENT, onItemUpdated);
|
||||
window.addEventListener(LIB_SET_CHANGED_EVENT, onLibSet);
|
||||
window.addEventListener(DOC_REVERTED_EVENT, onDocReverted);
|
||||
return () => {
|
||||
clearTimeout(busyTimer);
|
||||
window.removeEventListener(LIB_BUSY_EVENT, onBusy);
|
||||
window.removeEventListener(LIB_ERROR_EVENT, onError);
|
||||
window.removeEventListener(LIB_ITEM_UPDATED_EVENT, onItemUpdated);
|
||||
window.removeEventListener(LIB_SET_CHANGED_EVENT, onLibSet);
|
||||
window.removeEventListener(DOC_REVERTED_EVENT, onDocReverted);
|
||||
};
|
||||
}, []);
|
||||
|
|
@ -1306,6 +1361,13 @@ export function WasmTool({
|
|||
return () => clearTimeout(t);
|
||||
}, [libUpdate]);
|
||||
|
||||
// Auto-dismiss the lib-set toast (long — it carries a click action).
|
||||
React.useEffect(() => {
|
||||
if (!libSetNotice) return;
|
||||
const t = setTimeout(() => setLibSetNotice(null), 30_000);
|
||||
return () => clearTimeout(t);
|
||||
}, [libSetNotice]);
|
||||
|
||||
// Auto-dismiss the doc-reverted toast (longest — the user should see it).
|
||||
React.useEffect(() => {
|
||||
if (!docReverted) return;
|
||||
|
|
@ -1679,6 +1741,7 @@ export function WasmTool({
|
|||
boot ? { libs: boot.libs, stacks: boot.stacks } : undefined,
|
||||
);
|
||||
if (libsSource === undefined) ownedLibsSource = source;
|
||||
activeLibsSourceRef.current = source;
|
||||
// Download-consent gate (standalone-load-ux 0001): before pulling the
|
||||
// (large) cold wasm + lib bundles, say how many MB and wait for the OK.
|
||||
// Runs only on versioned CDN deploys (`meta.ver` — flat dev roots and
|
||||
|
|
@ -1870,6 +1933,9 @@ export function WasmTool({
|
|||
...(readOnly
|
||||
? {}
|
||||
: {
|
||||
// Durable per-path block (409 conflict / unknown commit
|
||||
// state): surface it as the persistent save-blocked banner.
|
||||
onBlocked: (block: SaveBlock) => setSaveBlocked(block),
|
||||
// A sheet created mid-session ("Add Sheet") saves to a new .kicad_sch path the
|
||||
// page-load file list can't contain — warm its collab room so it stays in sync.
|
||||
onSaved: (relPath: string) => {
|
||||
|
|
@ -2625,10 +2691,26 @@ export function WasmTool({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* A save path is durably BLOCKED (CAS conflict / unknown commit state) —
|
||||
persistent full-width banner, no auto-dismiss: subsequent Ctrl+S on
|
||||
the path is absorbed by the save lane, so this must stay visible. */}
|
||||
{saveBlocked && (
|
||||
<div
|
||||
data-testid="save-blocked-banner"
|
||||
className="absolute inset-x-0 top-0 z-40 bg-red-900/95 px-4 py-2 text-center text-xs font-medium text-red-100 shadow-lg"
|
||||
>
|
||||
{saveBlocked.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top-center toast column: simultaneous notices stack instead of
|
||||
overlapping (they all used to render at the same absolute spot). */}
|
||||
<div className="absolute left-1/2 top-3 z-40 flex -translate-x-1/2 flex-col items-center gap-2">
|
||||
|
||||
{/* Library error (e.g. a backend 404 on open) — auto-dismisses. */}
|
||||
{libError && (
|
||||
<button
|
||||
className="absolute left-1/2 top-3 z-40 max-w-md -translate-x-1/2 rounded bg-red-950/95 px-3 py-2 text-center text-xs text-red-100 shadow-lg ring-1 ring-red-500/40"
|
||||
className="max-w-md rounded bg-red-950/95 px-3 py-2 text-center text-xs text-red-100 shadow-lg ring-1 ring-red-500/40"
|
||||
onClick={() => setLibError(null)}
|
||||
title="Dismiss"
|
||||
>
|
||||
|
|
@ -2640,7 +2722,7 @@ export function WasmTool({
|
|||
{libUpdate && (
|
||||
<button
|
||||
data-testid="lib-update-toast"
|
||||
className="absolute left-1/2 top-3 z-40 max-w-md -translate-x-1/2 rounded bg-amber-950/95 px-3 py-2 text-center text-xs text-amber-100 shadow-lg ring-1 ring-amber-500/40"
|
||||
className="max-w-md rounded bg-amber-950/95 px-3 py-2 text-center text-xs text-amber-100 shadow-lg ring-1 ring-amber-500/40"
|
||||
onClick={() => setLibUpdate(null)}
|
||||
title="Dismiss"
|
||||
>
|
||||
|
|
@ -2648,11 +2730,47 @@ export function WasmTool({
|
|||
</button>
|
||||
)}
|
||||
|
||||
{/* A peer changed the team's lib set — click loads the new lib live
|
||||
(kicadLibsAddEntry bridge), falling back to a reload offer. */}
|
||||
{libSetNotice && (
|
||||
<button
|
||||
data-testid="lib-set-toast"
|
||||
className="max-w-md rounded bg-sky-950/95 px-3 py-2 text-center text-xs text-sky-100 shadow-lg ring-1 ring-sky-500/40"
|
||||
onClick={() => {
|
||||
const notice = libSetNotice;
|
||||
if (notice.mode === "reload") {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
const source = activeLibsSourceRef.current;
|
||||
if (!source) {
|
||||
setLibSetNotice({ ...notice, mode: "reload", message: reloadFallbackMsg(notice) });
|
||||
return;
|
||||
}
|
||||
setLibSetNotice(null);
|
||||
void addAnnouncedLib(source, notice.detail, (m) => console.log(m)).then(
|
||||
(ok) => {
|
||||
if (!ok) {
|
||||
setLibSetNotice({
|
||||
...notice,
|
||||
mode: "reload",
|
||||
message: reloadFallbackMsg(notice),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
title={libSetNotice.mode === "reload" ? "Reload" : "Load the new library"}
|
||||
>
|
||||
{libSetNotice.message}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Backend rolled this doc back to the last valid state (kicad-validity). */}
|
||||
{docReverted && (
|
||||
<button
|
||||
data-testid="doc-reverted-toast"
|
||||
className="absolute left-1/2 top-3 z-40 max-w-md -translate-x-1/2 rounded bg-orange-950/95 px-3 py-2 text-center text-xs text-orange-100 shadow-lg ring-1 ring-orange-500/40"
|
||||
className="max-w-md rounded bg-orange-950/95 px-3 py-2 text-center text-xs text-orange-100 shadow-lg ring-1 ring-orange-500/40"
|
||||
onClick={() => setDocReverted(null)}
|
||||
title="Dismiss"
|
||||
>
|
||||
|
|
@ -2660,6 +2778,8 @@ export function WasmTool({
|
|||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</WasmErrorBoundary>
|
||||
|
||||
{/* Terminal failure — z-35, ABOVE the boot overlay but below the console
|
||||
|
|
|
|||
69
web/standalone/src/wasm/libs/runtime-add.ts
Normal file
69
web/standalone/src/wasm/libs/runtime-add.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import type { LibsSource } from "./source";
|
||||
import type { LibSetChangedDetail } from "./source";
|
||||
import { libUri } from "./uri";
|
||||
|
||||
/**
|
||||
* Load a mid-session-announced library into the RUNNING editor — the action
|
||||
* behind the LIB_SET_CHANGED_EVENT toast. The lib set is otherwise frozen at
|
||||
* boot (sym/fp-lib-table written once in preRun), so this:
|
||||
* 1. re-lists the source per kind to find the announced lib (and its
|
||||
* display name — the announce payload's name is advisory),
|
||||
* 2. creates the `/mnt/pcbjam/<id>` MEMFS placeholder the post-save stat
|
||||
* path needs (boot creates these for boot-time libs only),
|
||||
* 3. calls the `kicadLibsAddEntry` bridge, which inserts the lib-table row
|
||||
* into the live in-memory table, loads the lib through the provider and
|
||||
* re-syncs any open editor tree.
|
||||
* Returns true when at least one kind actually added the lib. False ⇒ the
|
||||
* bridge is missing (pre-addEntry wasm build) or the lib isn't listed for
|
||||
* this session — the caller falls back to suggesting a reload.
|
||||
*/
|
||||
export async function addAnnouncedLib(
|
||||
source: LibsSource,
|
||||
detail: LibSetChangedDetail,
|
||||
log: (msg: string) => void = () => {},
|
||||
): Promise<boolean> {
|
||||
const win = globalThis as {
|
||||
Module?: Record<string, unknown>;
|
||||
FS?: { writeFile?: (path: string, data: string) => void; analyzePath?: (p: string) => { exists: boolean } };
|
||||
};
|
||||
const addEntry = win.Module?.kicadLibsAddEntry as
|
||||
| ((kind: string, nickname: string, uri: string) => void)
|
||||
| undefined;
|
||||
if (typeof addEntry !== "function") {
|
||||
log(`[libs] addAnnouncedLib: kicadLibsAddEntry bridge missing`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let added = false;
|
||||
for (const kind of ["symbol", "footprint"]) {
|
||||
let libs;
|
||||
try {
|
||||
libs = await source.listLibs(kind);
|
||||
} catch (e) {
|
||||
log(`[libs] addAnnouncedLib: listLibs(${kind}) failed: ${String(e)}`);
|
||||
continue;
|
||||
}
|
||||
const lib = libs.find((l) => l.id === detail.libId);
|
||||
if (!lib) continue;
|
||||
|
||||
const uri = libUri(lib.id);
|
||||
try {
|
||||
// The empty placeholder file the save flow's stat()s need; harmless if
|
||||
// another kind's pass (or a same-session retry) already wrote it.
|
||||
if (!win.FS?.analyzePath?.(uri)?.exists) {
|
||||
win.FS?.writeFile?.(uri, "");
|
||||
}
|
||||
} catch (e) {
|
||||
log(`[libs] addAnnouncedLib: placeholder write failed: ${String(e)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
addEntry(kind, lib.name, uri);
|
||||
log(`[libs] added ${kind} lib "${lib.name}" at runtime`);
|
||||
added = true;
|
||||
} catch (e) {
|
||||
log(`[libs] addAnnouncedLib: bridge failed for ${kind}: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
|
@ -203,6 +203,13 @@ export const LIB_LOADING_EVENT = "pcbjam:lib-loading";
|
|||
* updated" (placed copies keep the previous version until updated explicitly).
|
||||
*/
|
||||
export const LIB_ITEM_UPDATED_EVENT = "pcbjam:lib-item-updated";
|
||||
/**
|
||||
* Fired when the TEAM'S LIB SET changed mid-session (a peer created an org
|
||||
* lib, pinned/unpinned one — the scope room's `libset` broadcast). The lib
|
||||
* table is frozen at boot, so the chrome offers a "load the new library"
|
||||
* action (`addAnnouncedLib`) instead of requiring a reload.
|
||||
*/
|
||||
export const LIB_SET_CHANGED_EVENT = "pcbjam:lib-set-changed";
|
||||
|
||||
export interface LibBusyDetail {
|
||||
busy: boolean;
|
||||
|
|
@ -230,6 +237,11 @@ export interface LibItemUpdatedDetail {
|
|||
/** The subset placed in the open document (empty ⇒ informational only). */
|
||||
usedNames: string[];
|
||||
}
|
||||
export interface LibSetChangedDetail {
|
||||
op: "add" | "remove" | "update";
|
||||
libId: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
function emitLibBusy(detail: LibBusyDetail): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
USER_HEADER,
|
||||
} from "@pcbjam/shared";
|
||||
import {
|
||||
onSyncRoomFrame,
|
||||
peekNamespaces,
|
||||
SyncRoomMovedError,
|
||||
SyncStack,
|
||||
|
|
@ -16,9 +17,11 @@ import {
|
|||
} from "@pcbjam/sync-client";
|
||||
import {
|
||||
LIB_ITEM_UPDATED_EVENT,
|
||||
LIB_SET_CHANGED_EVENT,
|
||||
type LibInfo,
|
||||
type LibItemInfo,
|
||||
type LibItemUpdatedDetail,
|
||||
type LibSetChangedDetail,
|
||||
type LibsSource,
|
||||
type LibsSyncState,
|
||||
} from "./source";
|
||||
|
|
@ -372,6 +375,23 @@ export function syncedScopeLibsSource(
|
|||
},
|
||||
): LibsSource {
|
||||
const perLib = new Map<string, LibsSource>();
|
||||
|
||||
// Room-level `libset` frames (a peer created an org lib / changed pins —
|
||||
// the scope room's announce broadcast): surface them to the chrome as
|
||||
// LIB_SET_CHANGED_EVENT so it can offer "load the new library" without a
|
||||
// reload. The session only ever dials its own team's scope room, so no
|
||||
// URL filtering is needed; frames are advisory (the action re-lists).
|
||||
const offRoomFrames = onSyncRoomFrame((_roomUrl, msg) => {
|
||||
if (msg.t !== "libset" || typeof window === "undefined") return;
|
||||
const detail: LibSetChangedDetail = {
|
||||
op: msg.op,
|
||||
libId: msg.libId,
|
||||
name: msg.name,
|
||||
};
|
||||
opts.log?.(`[synced] lib set changed: ${msg.op} ${msg.name ?? msg.libId}`);
|
||||
window.dispatchEvent(new CustomEvent(LIB_SET_CHANGED_EVENT, { detail }));
|
||||
});
|
||||
|
||||
// Stacks resolved in bulk by `prefetchStacks`. A hit means the per-lib source
|
||||
// makes NO resolve request; `null` records "backend says unresolvable" so a
|
||||
// stale pin isn't retried one-by-one. Misses simply fall back per-lib, which
|
||||
|
|
@ -539,6 +559,7 @@ export function syncedScopeLibsSource(
|
|||
);
|
||||
},
|
||||
dispose(): void {
|
||||
offRoomFrames();
|
||||
for (const src of perLib.values()) src.dispose?.();
|
||||
perLib.clear();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit ae28fab038ed54e3fb65fb7d4bc0dc7e123ae99f
|
||||
Subproject commit 304ee266b4437a8b9c8bd49704f1b0a9006c54dc
|
||||
Loading…
Reference in a new issue