feat(storage): thread the owning team's scopeId into collab rooms
The closed backend's storage rework scopes collab rooms/keys by team: room ids are now <scopeId>:<projectId>:<docPath> (@pcbjam/shared bump). The editor threads a scopeId prop parallel to projectId — ToolPage takes it from the project DTO (falling back to "local" for scope-less backends; Home/LibTool pages pass "local" explicitly) — through WasmTool into the doc session, sheet-manager warm rooms, and cross-app presence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f9091a28c5
commit
bea14c772f
10 changed files with 34 additions and 9 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 77eea4933734fbb410c9dabbede970e84944d4a0
|
||||
Subproject commit 9f8bd91104e6cd9a31921f25bf2f351a2a5b8c08
|
||||
|
|
@ -405,6 +405,7 @@ async function maybeConnectDocSession(
|
|||
opts: {
|
||||
docSource?: DocSource;
|
||||
tool: Tool;
|
||||
scopeId: string;
|
||||
projectId: string;
|
||||
targetPath?: string;
|
||||
log: (m: string) => void;
|
||||
|
|
@ -414,7 +415,7 @@ async function maybeConnectDocSession(
|
|||
if (!opts.targetPath || !COLLAB_TOOLS.has(opts.tool)) return {};
|
||||
|
||||
const { connectKicadDoc } = await import("@/wasm/collab");
|
||||
const room = collabRoomId(opts.projectId, opts.targetPath);
|
||||
const room = collabRoomId(opts.scopeId, opts.projectId, opts.targetPath);
|
||||
const session = await connectKicadDoc({ provider: yjsProviderConfig(), room });
|
||||
|
||||
// Use the full doc state (meta + layout + items), NOT just item count: a
|
||||
|
|
@ -449,6 +450,7 @@ async function maybeStartCollab(
|
|||
opts: {
|
||||
tool: Tool;
|
||||
slug: string;
|
||||
scopeId: string;
|
||||
projectId: string;
|
||||
targetPath?: string;
|
||||
collabSession?: KicadDocSession;
|
||||
|
|
@ -514,7 +516,7 @@ async function maybeStartCollab(
|
|||
// One room per (project, document). Two tabs of the same build compute the
|
||||
// same id, so cross-tab BroadcastChannel still works; network providers use it
|
||||
// verbatim to namespace + persist (see @pcbjam/shared collabRoomId).
|
||||
const room = collabRoomId(opts.projectId, opts.targetPath ?? opts.tool);
|
||||
const room = collabRoomId(opts.scopeId, opts.projectId, opts.targetPath ?? opts.tool);
|
||||
clog("starting collab", provider.kind, "room", room, "seedDoc:", !!seedDoc);
|
||||
const handle = await startKicadCollab(mod, win as unknown as KicadItemsWindow, {
|
||||
provider,
|
||||
|
|
@ -543,6 +545,7 @@ async function startSheetCollab(
|
|||
win: ToolWindow,
|
||||
opts: {
|
||||
slug: string;
|
||||
scopeId: string;
|
||||
projectId: string;
|
||||
targetPath?: string;
|
||||
files: ToolFile[];
|
||||
|
|
@ -578,6 +581,7 @@ async function startSheetCollab(
|
|||
const manager = createSheetCollabManager({
|
||||
mod,
|
||||
win: win as unknown as KicadItemsWindow,
|
||||
scopeId: opts.scopeId,
|
||||
projectId: opts.projectId,
|
||||
provider: yjsProviderConfig(),
|
||||
seedDocForPath: (sheet) => seedDocFromMemfs(win, opts.slug, sheet),
|
||||
|
|
@ -679,6 +683,7 @@ async function waitForWxUi(win: ToolWindow, timeoutMs = 25_000): Promise<void> {
|
|||
export function WasmTool({
|
||||
tool,
|
||||
slug,
|
||||
scopeId,
|
||||
projectId,
|
||||
files,
|
||||
targetPath,
|
||||
|
|
@ -692,6 +697,8 @@ export function WasmTool({
|
|||
}: {
|
||||
tool: Tool;
|
||||
slug: string;
|
||||
/** Owning team's stable id (`"local"` when scope-less) — first room-id segment. */
|
||||
scopeId: string;
|
||||
/** Stable project id — used to key the collab room (see @pcbjam/shared). */
|
||||
projectId: string;
|
||||
files: ToolFile[];
|
||||
|
|
@ -1182,6 +1189,7 @@ export function WasmTool({
|
|||
const { session, targetBytes } = await maybeConnectDocSession(win, {
|
||||
docSource,
|
||||
tool,
|
||||
scopeId,
|
||||
projectId,
|
||||
targetPath,
|
||||
log: append,
|
||||
|
|
@ -1242,6 +1250,7 @@ export function WasmTool({
|
|||
if ((tool === "pcbnew" || tool === "eeschema") && !collabOptOut && !readOnly) {
|
||||
crossAppRef.current =
|
||||
(await startCrossAppPresence({
|
||||
scopeId,
|
||||
projectId,
|
||||
provider: yjsProviderConfig(),
|
||||
user: presenceUser(),
|
||||
|
|
@ -1259,6 +1268,7 @@ export function WasmTool({
|
|||
sheetManagerRef.current =
|
||||
(await startSheetCollab(win, {
|
||||
slug,
|
||||
scopeId,
|
||||
projectId,
|
||||
targetPath,
|
||||
files,
|
||||
|
|
@ -1291,6 +1301,7 @@ export function WasmTool({
|
|||
const collabHandle = await maybeStartCollab(win, {
|
||||
tool,
|
||||
slug,
|
||||
scopeId,
|
||||
projectId,
|
||||
targetPath,
|
||||
collabSession: session,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export function HomePage() {
|
|||
<WasmTool
|
||||
tool={launchedTool.tool}
|
||||
slug="local"
|
||||
scopeId="local"
|
||||
projectId="local"
|
||||
files={[]}
|
||||
libsSource={launchedTool.libsSource}
|
||||
|
|
@ -185,6 +186,7 @@ export function HomePage() {
|
|||
<WasmTool
|
||||
tool={launched.tool}
|
||||
slug="local"
|
||||
scopeId="local"
|
||||
projectId="local"
|
||||
files={local.files}
|
||||
targetPath={launched.target}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export function LibToolPage() {
|
|||
<WasmTool
|
||||
tool={tool}
|
||||
slug="local"
|
||||
scopeId="local"
|
||||
projectId="local"
|
||||
files={[]}
|
||||
libsSource={libsSource}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export function ToolPage() {
|
|||
<WasmTool
|
||||
tool={tool}
|
||||
slug={slug}
|
||||
scopeId={data.project.scopeId ?? "local"}
|
||||
projectId={data.project.id}
|
||||
files={data.files}
|
||||
targetPath={targetPath}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ let projectSeq = 0;
|
|||
|
||||
async function join(projectId: string, userId: string, tool: string): Promise<CrossAppHandle> {
|
||||
const h = await startCrossAppPresence({
|
||||
scopeId: "S",
|
||||
projectId,
|
||||
provider: { kind: "broadcastchannel", settleMs: 10 },
|
||||
user: user(userId),
|
||||
|
|
@ -43,6 +44,7 @@ afterEach(async () => {
|
|||
describe("startCrossAppPresence", () => {
|
||||
it("returns undefined for the none provider", async () => {
|
||||
const h = await startCrossAppPresence({
|
||||
scopeId: "S",
|
||||
projectId: "P",
|
||||
provider: { kind: "none" },
|
||||
user: user("alice"),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export interface CrossAppHandle {
|
|||
}
|
||||
|
||||
export async function startCrossAppPresence(opts: {
|
||||
scopeId: string;
|
||||
projectId: string;
|
||||
provider: ProviderConfig;
|
||||
user: PresenceUser;
|
||||
|
|
@ -51,7 +52,7 @@ export async function startCrossAppPresence(opts: {
|
|||
}): Promise<CrossAppHandle | undefined> {
|
||||
if (opts.provider.kind === "none") return undefined;
|
||||
|
||||
const room = presenceRoomId(opts.projectId);
|
||||
const room = presenceRoomId(opts.scopeId, opts.projectId);
|
||||
const doc = new Y.Doc();
|
||||
let provider: YjsProvider;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ const { connectKicadDoc, bindKicadCollab, moduleItemsBridge } = vi.hoisted(() =>
|
|||
|
||||
vi.mock("./index", () => ({ connectKicadDoc }));
|
||||
vi.mock("./kicad-binding", () => ({ bindKicadCollab, moduleItemsBridge }));
|
||||
vi.mock("@pcbjam/shared", () => ({ collabRoomId: (p: string, d: string) => `${p}:${d}` }));
|
||||
vi.mock("@pcbjam/shared", () => ({
|
||||
collabRoomId: (s: string, p: string, d: string) => `${s}:${p}:${d}`,
|
||||
}));
|
||||
|
||||
import { createSheetCollabManager } from "./sheet-manager";
|
||||
|
||||
|
|
@ -53,6 +55,7 @@ function makeManager() {
|
|||
return createSheetCollabManager({
|
||||
mod: {} as never,
|
||||
win: {} as never,
|
||||
scopeId: "S",
|
||||
projectId: "P",
|
||||
provider: { kind: "none" } as never,
|
||||
seedDocForPath: () => undefined,
|
||||
|
|
@ -167,13 +170,14 @@ describe("sheet-manager warm pool", () => {
|
|||
it("uses the pre-connected entry session (ydoc mode) instead of reconnecting", async () => {
|
||||
const entryDoc = makeDoc();
|
||||
const entrySession: FakeSession = {
|
||||
room: "P:root.kicad_sch",
|
||||
room: "S:P:root.kicad_sch",
|
||||
doc: entryDoc,
|
||||
provider: { destroy: vi.fn() },
|
||||
};
|
||||
const m = createSheetCollabManager({
|
||||
mod: {} as never,
|
||||
win: {} as never,
|
||||
scopeId: "S",
|
||||
projectId: "P",
|
||||
provider: { kind: "none" } as never,
|
||||
seedDocForPath: () => undefined,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ export interface SheetManagerOptions {
|
|||
mod: KicadItemsModule;
|
||||
/** The global the C++ emit side calls into (`window.kicadCollab.onItems`). */
|
||||
win: KicadItemsWindow;
|
||||
/** Project uuid — keys each room as `collabRoomId(projectId, sheetPath)`. */
|
||||
/** Owning team's stable id (`"local"` when scope-less) — first room-id segment. */
|
||||
scopeId: string;
|
||||
/** Project uuid — keys each room as `collabRoomId(scopeId, projectId, sheetPath)`. */
|
||||
projectId: string;
|
||||
/** The env-selected Yjs provider config (same one the single-room path uses). */
|
||||
provider: ProviderConfig;
|
||||
|
|
@ -127,7 +129,7 @@ interface Room {
|
|||
}
|
||||
|
||||
export function createSheetCollabManager(opts: SheetManagerOptions): SheetCollabManager {
|
||||
const { mod, win, projectId, provider, seedDocForPath, log } = opts;
|
||||
const { mod, win, scopeId, projectId, provider, seedDocForPath, log } = opts;
|
||||
const bridge = moduleItemsBridge(mod, win);
|
||||
const rooms = new Map<string, Room>();
|
||||
// In-flight connects, so connectAll() and switchTo() racing on the same sheet (api
|
||||
|
|
@ -179,7 +181,7 @@ export function createSheetCollabManager(opts: SheetManagerOptions): SheetCollab
|
|||
const pending = (async () => {
|
||||
const session = await connectKicadDoc({
|
||||
provider,
|
||||
room: collabRoomId(projectId, sheetPath),
|
||||
room: collabRoomId(scopeId, projectId, sheetPath),
|
||||
});
|
||||
// Invisible observer (read-only-viewer): drop the provider's initial
|
||||
// empty awareness state before anyone can see it.
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ describe("bug 07b — sheet-switch gap: stale onItems writes into the old sheet'
|
|||
const m = createSheetCollabManager({
|
||||
mod,
|
||||
win,
|
||||
scopeId: "S",
|
||||
projectId: "P",
|
||||
provider: { kind: "none" } satisfies ProviderConfig,
|
||||
seedDocForPath: () => undefined,
|
||||
|
|
|
|||
Loading…
Reference in a new issue