pcbjam/web/standalone/src/wasm/libs/remote-source.ts
Gergő Törcsvári 8ee8db69e6
feat: standalone download-consent gate + truthful loading states (standalone-load-ux 0001/0002)
Cold loads on versioned CDN deploys now show a consent card (editor MB +
symbol/footprint lib figures, downloaded-now vs on-demand) and wait for OK
before any big fetch; warm loads skip it and show truthful stages (loading
from cache / Compiling / Starting KiCad) instead of the first-download line.

- wasm-assets: resolveWasmMeta (bundle/ver/sizes), download-completion marker
  keyed by content-addressed bundle/ver, update wording, auto-download opt-out,
  HEAD size fallback
- boot: manifest raw size as the progress total (fixes the br/gzip
  Content-Length mismatch), marker written after download+instantiate succeed
- cdn-source: syncState() — IDB warmth peek + sizes.json cold sums
- synced-source: syncState() from the backend envelope's sync refs (private
  platform); remote-source passes libSchema.sync through
- publish-wasm: manifest schema 2 with per-bundle sizes (registry-persisted,
  reuse + snapshot modes); publish-libs: sizes.json sibling key + top-up mode
- fixed 4 stale unit tests (bundle mapping, session-identity email)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011CC8aAnUUHcnHy3QCJtUwb
2026-07-29 07:48:25 +02:00

103 lines
3.2 KiB
TypeScript

import {
contract,
PROJECT_HEADER,
SCOPE_HEADER,
USER_HEADER,
} from "@pcbjam/shared";
import { initClient } from "@ts-rest/core";
import type { LibInfo, LibItemInfo, LibsSource } from "./source";
/**
* A `LibsSource` backed by a contract-conforming backend (the closed registry
* server, or the GPL example backend). Read list ops go through the ts-rest
* client; item bodies stream from the raw text route
* `GET /api/scopes/:scope/libs/:lib/items/:kind/:name`, and writes hit the
* symmetric `PUT` route. The `scope` is a path param; `user` (thin per-user) and
* `project` (project-scoped resolution) ride USER_HEADER/PROJECT_HEADER.
*/
export function remoteLibsSource(
apiBase: string,
scope: string,
user?: string,
project?: string,
): LibsSource {
const reqHeaders: Record<string, string> = {
[SCOPE_HEADER]: scope,
...(user ? { [USER_HEADER]: user } : {}),
...(project ? { [PROJECT_HEADER]: project } : {}),
};
const client = initClient(contract, {
baseUrl: apiBase,
baseHeaders: reqHeaders,
// Session-cookie auth (backends with real auth ignore the thin headers).
credentials: "include",
});
const enc = encodeURIComponent;
const itemUrl = (libId: string, kind: string, name: string) =>
`${apiBase}/api/scopes/${enc(scope)}/libs/${enc(libId)}/items/` +
`${enc(kind)}/${enc(name)}`;
return {
async listLibs(kind?: string): Promise<LibInfo[]> {
const res = await client.listLibs({ params: { scope }, query: { kind } });
if (res.status !== 200) return [];
return res.body.map((l) => ({
id: l.id,
name: l.name,
description: l.description ?? null,
type: l.type,
itemCount: l.itemCount,
// Cache identity + cold-download bytes (standalone-load-ux 0002) —
// absent on backends predating the field.
sync: l.sync ?? null,
}));
},
async listItems(libId: string): Promise<LibItemInfo[]> {
const res = await client.listLibItems({ params: { scope, lib: libId } });
if (res.status !== 200) return [];
return res.body.map((i) => ({ kind: i.kind, name: i.name }));
},
async getItemBody(
libId: string,
kind: string,
name: string,
): Promise<string | null> {
const res = await fetch(itemUrl(libId, kind, name), {
headers: reqHeaders,
credentials: "include",
});
if (!res.ok) return null;
return await res.text();
},
async saveItemBody(
libId: string,
kind: string,
name: string,
body: string,
): Promise<boolean> {
const res = await fetch(itemUrl(libId, kind, name), {
method: "PUT",
headers: { ...reqHeaders, "Content-Type": "text/plain; charset=utf-8" },
body,
credentials: "include",
});
return res.ok;
},
async createLib(name: string): Promise<LibInfo | null> {
const res = await client.createLib({ params: { scope }, body: { name } });
if (res.status !== 201) return null;
return {
id: res.body.id,
name: res.body.name,
description: res.body.description ?? null,
type: res.body.type,
sync: res.body.sync ?? null,
};
},
};
}