refactor(web): split into GPL standalone editor + thin reference backend
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>
This commit is contained in:
parent
b672c54189
commit
b6a77ee93c
80 changed files with 671 additions and 2992 deletions
65
web/standalone/src/wasm/collab/broadcast-transport.ts
Normal file
65
web/standalone/src/wasm/collab/broadcast-transport.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue