fix(standalone): never let the boot-time default-lib create blank the lib tables
Anonymous open of a public project (read-only viewer): boot saw no writable
lib, POSTed createLib("My Symbols"), the session gate 401'd, and the throw
escaped the listLibs try/catch — both sym/fp lib tables were seeded EMPTY for
the session (staging: /tg44/projects/arduino/... Arduino Leonardo.kicad_sch).
- ensureWritableLib(): skipped for readOnly sessions; a failed create is
logged and swallowed, never propagated (the listed libs stay seeded).
- WasmTool passes readOnly through to boot.
- unit tests for the skip / create / already-writable / rejected-create paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtN6ASBvMGNbjPqY5boycg
This commit is contained in:
parent
aa26125f61
commit
fab6108cbb
3 changed files with 95 additions and 9 deletions
|
|
@ -997,6 +997,10 @@ export function WasmTool({
|
|||
// frame token tells its single_top launcher which editor frame to open.
|
||||
frame: TOOL_FRAME[tool],
|
||||
mobile: mobileUi,
|
||||
// Viewers can't write: skip the boot-time default-lib create (a
|
||||
// session-gated POST that 401s anonymously and used to blank the
|
||||
// lib tables — anonymous public-schematic open on staging).
|
||||
readOnly,
|
||||
});
|
||||
// Identity must be settled before the doc session / presence binds
|
||||
// below — effectively instant, it raced the multi-second wasm boot.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ensureWritableLib,
|
||||
hasWritableLib,
|
||||
installPthreadWorkerRedirect,
|
||||
pthreadWorkerScript,
|
||||
|
|
@ -28,6 +29,57 @@ describe("hasWritableLib", () => {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Boot-time default-lib create. Regression: an anonymous viewer of a public
|
||||
* project has no writable lib, so boot POSTed createLib, the session gate
|
||||
* 401'd, and the throw escaped the caller's try — seeding EMPTY lib tables.
|
||||
*/
|
||||
describe("ensureWritableLib", () => {
|
||||
const log = () => {};
|
||||
|
||||
it("skips the create entirely for read-only sessions", async () => {
|
||||
const createLib = vi.fn(async () => lib("org"));
|
||||
const lists = [[lib("origin")]];
|
||||
expect(await ensureWritableLib({ createLib }, lists, { readOnly: true, log })).toBeNull();
|
||||
expect(createLib).not.toHaveBeenCalled();
|
||||
expect(lists[0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("creates once and joins every per-kind list when nothing writable is listed", async () => {
|
||||
const created = lib("org");
|
||||
const createLib = vi.fn(async () => created);
|
||||
const sym = [lib("origin")];
|
||||
const fp = [lib("mirror")];
|
||||
expect(await ensureWritableLib({ createLib }, [sym, fp], { log })).toBe(created);
|
||||
expect(createLib).toHaveBeenCalledWith("My Symbols");
|
||||
expect(sym).toContain(created);
|
||||
expect(fp).toContain(created);
|
||||
});
|
||||
|
||||
it("does nothing when a writable lib already exists", async () => {
|
||||
const createLib = vi.fn(async () => lib("org"));
|
||||
await ensureWritableLib({ createLib }, [[lib("user")]], { log });
|
||||
expect(createLib).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("swallows a rejected create (401) and leaves the listed libs intact", async () => {
|
||||
const createLib = vi.fn(async () => {
|
||||
throw new Error("401");
|
||||
});
|
||||
const logs: string[] = [];
|
||||
const sym = [lib("origin"), lib("mirror")];
|
||||
await expect(
|
||||
ensureWritableLib({ createLib }, [sym], { log: (m) => logs.push(m) }),
|
||||
).resolves.toBeNull();
|
||||
expect(sym).toHaveLength(2);
|
||||
expect(logs.some((m) => /non-fatal/.test(m))).toBe(true);
|
||||
});
|
||||
|
||||
it("is a no-op for sources without createLib", async () => {
|
||||
expect(await ensureWritableLib({}, [[lib("origin")]], { log })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Cross-origin (CDN) pthread spawn fix — doc-23 §7 KNOWN GAP. Emscripten
|
||||
* spawns pthread workers from the glue's absolute URL; when that URL is the
|
||||
|
|
|
|||
|
|
@ -41,6 +41,38 @@ export function hasWritableLib(lists: Iterable<LibInfo[]>): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the owner has at least one writable lib to save items into, creating
|
||||
* the default one when none is listed. A writable lib holds either kind, so a
|
||||
* created lib joins every per-kind list (mutated in place).
|
||||
*
|
||||
* Skipped outright for read-only sessions (anonymous viewer of a public
|
||||
* project, read-only-viewer): they cannot write, so there is nothing to save
|
||||
* into — and the create is a session-gated POST that 401s for them. Best-effort
|
||||
* either way: a failed create (401/403, backend without the route, a network
|
||||
* blip) is logged and swallowed, NEVER propagated — an exception here used to
|
||||
* escape the caller's listLibs try/catch and seed EMPTY lib tables for the
|
||||
* whole session (observed on staging: anonymous open of a public schematic).
|
||||
*/
|
||||
export async function ensureWritableLib(
|
||||
source: Pick<LibsSource, "createLib">,
|
||||
listsByKind: Iterable<LibInfo[]>,
|
||||
opts: { readOnly?: boolean; log: (msg: string) => void; name?: string },
|
||||
): Promise<LibInfo | null> {
|
||||
const lists = [...listsByKind];
|
||||
if (opts.readOnly || !source.createLib || hasWritableLib(lists)) return null;
|
||||
try {
|
||||
const created = await source.createLib(opts.name ?? DEFAULT_USER_LIB_NAME);
|
||||
if (!created) return null;
|
||||
for (const libs of lists) libs.push(created);
|
||||
opts.log(`[libs] created default user lib "${created.name}"`);
|
||||
return created;
|
||||
} catch (e) {
|
||||
opts.log(`[libs] default user lib create failed (non-fatal): ${String(e)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot a KiCad tool directly in the main React document — no iframe.
|
||||
*
|
||||
|
|
@ -116,6 +148,9 @@ export interface BootOptions {
|
|||
* only — chrome visibility is owned by the shell's chrome-visibility store
|
||||
* (WasmTool applies it via kicadSetChrome). */
|
||||
mobile?: boolean;
|
||||
/** Read-only session (anonymous/public viewer): never attempt writes at
|
||||
* boot — in particular no default-user-lib create (see ensureWritableLib). */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
let booted: { tool: Tool; promise: Promise<void> } | null = null;
|
||||
|
|
@ -363,6 +398,7 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
|||
libsSource,
|
||||
modelsSource,
|
||||
enumerateGate,
|
||||
readOnly,
|
||||
} = opts;
|
||||
// Truthful fetch label: a warm start (download-completion marker present)
|
||||
// reads from the HTTP cache, so "Downloading" would be a lie — and vice versa.
|
||||
|
|
@ -436,15 +472,9 @@ async function doBoot(opts: BootOptions): Promise<void> {
|
|||
// kind-agnostic containers and appear in every list).
|
||||
const listsByKind = new Map<"symbol" | "footprint", LibInfo[]>();
|
||||
for (const k of libKinds) listsByKind.set(k, await libsSource.listLibs(k));
|
||||
// Ensure the owner has at least one writable lib to save items into.
|
||||
// A writable lib holds either kind, so the created lib joins every table.
|
||||
if (libsSource.createLib && !hasWritableLib(listsByKind.values())) {
|
||||
const created = await libsSource.createLib(DEFAULT_USER_LIB_NAME);
|
||||
if (created) {
|
||||
for (const libs of listsByKind.values()) libs.push(created);
|
||||
log(`[libs] created default user lib "${created.name}"`);
|
||||
}
|
||||
}
|
||||
// Ensure the owner has at least one writable lib to save items into
|
||||
// (skipped read-only; never throws — a failure must not empty the tables).
|
||||
await ensureWritableLib(libsSource, listsByKind.values(), { readOnly, log });
|
||||
const symList = listsByKind.get("symbol");
|
||||
const fpList = listsByKind.get("footprint");
|
||||
if (symList) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue