Reduce web/ to a single generic editor app plus a minimal example backend, moving all project-specific/app code out to the closed root repo: - apps/frontend -> standalone (@pcbjam/standalone): the GPL editor. Strips the project-management UI (ProjectsPage/ProjectDetailPage/UploadDropzone and the write half of lib/api.ts). WasmTool now takes fetchBytes + assetBaseUrl as injected props (decoupled from the API client) so it can be driven by either a backend or a local folder. New HomePage (local-folder loader + backend project list) and read-only ProjectView. BroadcastChannel collab unchanged. - backend (@pcbjam/backend-example): thin Fastify+ts-rest reference impl of the @pcbjam/shared contract — serves a single project off the local filesystem (PROJECT_DIR), no DB/auth/uploads. - packages/contract -> web/pcbjam-shared (git submodule, MIT @pcbjam/shared). - Remove apps/server, packages/storage, packages/contract, docker-compose (server + storage move to the closed root repo; preserved via subtree branches). Rewrite pnpm-workspace/turbo/.env for the frontend-only layout. - Add tests/fixtures/demo so the editor + example backend run out of the box. Standalone typechecks + builds; backend serves the contract end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2 KiB
TypeScript
65 lines
2 KiB
TypeScript
import * as Y from "yjs";
|
|
import { clog } from "./debug";
|
|
|
|
/**
|
|
* Minimal BroadcastChannel sync provider for a Y.Doc (features/yjs-bridge/0001 §6,
|
|
* PoC transport — two same-origin tabs share one doc, zero backend). Encodes Yjs
|
|
* updates and ferries them between tabs; on join it requests the current state so a
|
|
* late tab catches up.
|
|
*
|
|
* Remote updates are applied with origin "remote" so (a) the reconciler treats them
|
|
* as peer changes (its own writes use a different origin) and (b) we don't re-broadcast
|
|
* them into a loop.
|
|
*/
|
|
export interface Transport {
|
|
destroy(): void;
|
|
}
|
|
|
|
type Msg =
|
|
| { t: "update"; u: Uint8Array }
|
|
| { t: "query" }
|
|
| { t: "state"; u: Uint8Array };
|
|
|
|
export const REMOTE_ORIGIN = "remote";
|
|
|
|
export function connectBroadcastChannel(
|
|
doc: Y.Doc,
|
|
channelName: string,
|
|
): Transport {
|
|
const bc = new BroadcastChannel(channelName);
|
|
clog("BroadcastChannel open:", channelName);
|
|
|
|
const onUpdate = (update: Uint8Array, origin: unknown) => {
|
|
if (origin === REMOTE_ORIGIN) return; // don't echo applied remote updates
|
|
clog("→ BC send update", update.byteLength, "bytes (origin:", String(origin) + ")");
|
|
bc.postMessage({ t: "update", u: update } satisfies Msg);
|
|
};
|
|
doc.on("update", onUpdate);
|
|
|
|
bc.onmessage = (e: MessageEvent<Msg>) => {
|
|
const msg = e.data;
|
|
if (!msg) return;
|
|
switch (msg.t) {
|
|
case "update":
|
|
case "state":
|
|
clog("← BC recv", msg.t, msg.u?.byteLength, "bytes → applyUpdate");
|
|
Y.applyUpdate(doc, new Uint8Array(msg.u), REMOTE_ORIGIN);
|
|
break;
|
|
case "query":
|
|
clog("← BC recv query → replying with state");
|
|
bc.postMessage({ t: "state", u: Y.encodeStateAsUpdate(doc) } satisfies Msg);
|
|
break;
|
|
}
|
|
};
|
|
|
|
// Ask any existing tab for the current state.
|
|
clog("→ BC send query (asking peers for state)");
|
|
bc.postMessage({ t: "query" } satisfies Msg);
|
|
|
|
return {
|
|
destroy: () => {
|
|
doc.off("update", onUpdate);
|
|
bc.close();
|
|
},
|
|
};
|
|
}
|