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: {
|
opts: {
|
||||||
docSource?: DocSource;
|
docSource?: DocSource;
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
|
scopeId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
targetPath?: string;
|
targetPath?: string;
|
||||||
log: (m: string) => void;
|
log: (m: string) => void;
|
||||||
|
|
@ -414,7 +415,7 @@ async function maybeConnectDocSession(
|
||||||
if (!opts.targetPath || !COLLAB_TOOLS.has(opts.tool)) return {};
|
if (!opts.targetPath || !COLLAB_TOOLS.has(opts.tool)) return {};
|
||||||
|
|
||||||
const { connectKicadDoc } = await import("@/wasm/collab");
|
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 });
|
const session = await connectKicadDoc({ provider: yjsProviderConfig(), room });
|
||||||
|
|
||||||
// Use the full doc state (meta + layout + items), NOT just item count: a
|
// Use the full doc state (meta + layout + items), NOT just item count: a
|
||||||
|
|
@ -449,6 +450,7 @@ async function maybeStartCollab(
|
||||||
opts: {
|
opts: {
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
scopeId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
targetPath?: string;
|
targetPath?: string;
|
||||||
collabSession?: KicadDocSession;
|
collabSession?: KicadDocSession;
|
||||||
|
|
@ -514,7 +516,7 @@ async function maybeStartCollab(
|
||||||
// One room per (project, document). Two tabs of the same build compute the
|
// 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
|
// same id, so cross-tab BroadcastChannel still works; network providers use it
|
||||||
// verbatim to namespace + persist (see @pcbjam/shared collabRoomId).
|
// 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);
|
clog("starting collab", provider.kind, "room", room, "seedDoc:", !!seedDoc);
|
||||||
const handle = await startKicadCollab(mod, win as unknown as KicadItemsWindow, {
|
const handle = await startKicadCollab(mod, win as unknown as KicadItemsWindow, {
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -543,6 +545,7 @@ async function startSheetCollab(
|
||||||
win: ToolWindow,
|
win: ToolWindow,
|
||||||
opts: {
|
opts: {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
scopeId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
targetPath?: string;
|
targetPath?: string;
|
||||||
files: ToolFile[];
|
files: ToolFile[];
|
||||||
|
|
@ -578,6 +581,7 @@ async function startSheetCollab(
|
||||||
const manager = createSheetCollabManager({
|
const manager = createSheetCollabManager({
|
||||||
mod,
|
mod,
|
||||||
win: win as unknown as KicadItemsWindow,
|
win: win as unknown as KicadItemsWindow,
|
||||||
|
scopeId: opts.scopeId,
|
||||||
projectId: opts.projectId,
|
projectId: opts.projectId,
|
||||||
provider: yjsProviderConfig(),
|
provider: yjsProviderConfig(),
|
||||||
seedDocForPath: (sheet) => seedDocFromMemfs(win, opts.slug, sheet),
|
seedDocForPath: (sheet) => seedDocFromMemfs(win, opts.slug, sheet),
|
||||||
|
|
@ -679,6 +683,7 @@ async function waitForWxUi(win: ToolWindow, timeoutMs = 25_000): Promise<void> {
|
||||||
export function WasmTool({
|
export function WasmTool({
|
||||||
tool,
|
tool,
|
||||||
slug,
|
slug,
|
||||||
|
scopeId,
|
||||||
projectId,
|
projectId,
|
||||||
files,
|
files,
|
||||||
targetPath,
|
targetPath,
|
||||||
|
|
@ -692,6 +697,8 @@ export function WasmTool({
|
||||||
}: {
|
}: {
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
slug: string;
|
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). */
|
/** Stable project id — used to key the collab room (see @pcbjam/shared). */
|
||||||
projectId: string;
|
projectId: string;
|
||||||
files: ToolFile[];
|
files: ToolFile[];
|
||||||
|
|
@ -1182,6 +1189,7 @@ export function WasmTool({
|
||||||
const { session, targetBytes } = await maybeConnectDocSession(win, {
|
const { session, targetBytes } = await maybeConnectDocSession(win, {
|
||||||
docSource,
|
docSource,
|
||||||
tool,
|
tool,
|
||||||
|
scopeId,
|
||||||
projectId,
|
projectId,
|
||||||
targetPath,
|
targetPath,
|
||||||
log: append,
|
log: append,
|
||||||
|
|
@ -1242,6 +1250,7 @@ export function WasmTool({
|
||||||
if ((tool === "pcbnew" || tool === "eeschema") && !collabOptOut && !readOnly) {
|
if ((tool === "pcbnew" || tool === "eeschema") && !collabOptOut && !readOnly) {
|
||||||
crossAppRef.current =
|
crossAppRef.current =
|
||||||
(await startCrossAppPresence({
|
(await startCrossAppPresence({
|
||||||
|
scopeId,
|
||||||
projectId,
|
projectId,
|
||||||
provider: yjsProviderConfig(),
|
provider: yjsProviderConfig(),
|
||||||
user: presenceUser(),
|
user: presenceUser(),
|
||||||
|
|
@ -1259,6 +1268,7 @@ export function WasmTool({
|
||||||
sheetManagerRef.current =
|
sheetManagerRef.current =
|
||||||
(await startSheetCollab(win, {
|
(await startSheetCollab(win, {
|
||||||
slug,
|
slug,
|
||||||
|
scopeId,
|
||||||
projectId,
|
projectId,
|
||||||
targetPath,
|
targetPath,
|
||||||
files,
|
files,
|
||||||
|
|
@ -1291,6 +1301,7 @@ export function WasmTool({
|
||||||
const collabHandle = await maybeStartCollab(win, {
|
const collabHandle = await maybeStartCollab(win, {
|
||||||
tool,
|
tool,
|
||||||
slug,
|
slug,
|
||||||
|
scopeId,
|
||||||
projectId,
|
projectId,
|
||||||
targetPath,
|
targetPath,
|
||||||
collabSession: session,
|
collabSession: session,
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ export function HomePage() {
|
||||||
<WasmTool
|
<WasmTool
|
||||||
tool={launchedTool.tool}
|
tool={launchedTool.tool}
|
||||||
slug="local"
|
slug="local"
|
||||||
|
scopeId="local"
|
||||||
projectId="local"
|
projectId="local"
|
||||||
files={[]}
|
files={[]}
|
||||||
libsSource={launchedTool.libsSource}
|
libsSource={launchedTool.libsSource}
|
||||||
|
|
@ -185,6 +186,7 @@ export function HomePage() {
|
||||||
<WasmTool
|
<WasmTool
|
||||||
tool={launched.tool}
|
tool={launched.tool}
|
||||||
slug="local"
|
slug="local"
|
||||||
|
scopeId="local"
|
||||||
projectId="local"
|
projectId="local"
|
||||||
files={local.files}
|
files={local.files}
|
||||||
targetPath={launched.target}
|
targetPath={launched.target}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export function LibToolPage() {
|
||||||
<WasmTool
|
<WasmTool
|
||||||
tool={tool}
|
tool={tool}
|
||||||
slug="local"
|
slug="local"
|
||||||
|
scopeId="local"
|
||||||
projectId="local"
|
projectId="local"
|
||||||
files={[]}
|
files={[]}
|
||||||
libsSource={libsSource}
|
libsSource={libsSource}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export function ToolPage() {
|
||||||
<WasmTool
|
<WasmTool
|
||||||
tool={tool}
|
tool={tool}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
|
scopeId={data.project.scopeId ?? "local"}
|
||||||
projectId={data.project.id}
|
projectId={data.project.id}
|
||||||
files={data.files}
|
files={data.files}
|
||||||
targetPath={targetPath}
|
targetPath={targetPath}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ let projectSeq = 0;
|
||||||
|
|
||||||
async function join(projectId: string, userId: string, tool: string): Promise<CrossAppHandle> {
|
async function join(projectId: string, userId: string, tool: string): Promise<CrossAppHandle> {
|
||||||
const h = await startCrossAppPresence({
|
const h = await startCrossAppPresence({
|
||||||
|
scopeId: "S",
|
||||||
projectId,
|
projectId,
|
||||||
provider: { kind: "broadcastchannel", settleMs: 10 },
|
provider: { kind: "broadcastchannel", settleMs: 10 },
|
||||||
user: user(userId),
|
user: user(userId),
|
||||||
|
|
@ -43,6 +44,7 @@ afterEach(async () => {
|
||||||
describe("startCrossAppPresence", () => {
|
describe("startCrossAppPresence", () => {
|
||||||
it("returns undefined for the none provider", async () => {
|
it("returns undefined for the none provider", async () => {
|
||||||
const h = await startCrossAppPresence({
|
const h = await startCrossAppPresence({
|
||||||
|
scopeId: "S",
|
||||||
projectId: "P",
|
projectId: "P",
|
||||||
provider: { kind: "none" },
|
provider: { kind: "none" },
|
||||||
user: user("alice"),
|
user: user("alice"),
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export interface CrossAppHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startCrossAppPresence(opts: {
|
export async function startCrossAppPresence(opts: {
|
||||||
|
scopeId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
provider: ProviderConfig;
|
provider: ProviderConfig;
|
||||||
user: PresenceUser;
|
user: PresenceUser;
|
||||||
|
|
@ -51,7 +52,7 @@ export async function startCrossAppPresence(opts: {
|
||||||
}): Promise<CrossAppHandle | undefined> {
|
}): Promise<CrossAppHandle | undefined> {
|
||||||
if (opts.provider.kind === "none") return 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();
|
const doc = new Y.Doc();
|
||||||
let provider: YjsProvider;
|
let provider: YjsProvider;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ const { connectKicadDoc, bindKicadCollab, moduleItemsBridge } = vi.hoisted(() =>
|
||||||
|
|
||||||
vi.mock("./index", () => ({ connectKicadDoc }));
|
vi.mock("./index", () => ({ connectKicadDoc }));
|
||||||
vi.mock("./kicad-binding", () => ({ bindKicadCollab, moduleItemsBridge }));
|
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";
|
import { createSheetCollabManager } from "./sheet-manager";
|
||||||
|
|
||||||
|
|
@ -53,6 +55,7 @@ function makeManager() {
|
||||||
return createSheetCollabManager({
|
return createSheetCollabManager({
|
||||||
mod: {} as never,
|
mod: {} as never,
|
||||||
win: {} as never,
|
win: {} as never,
|
||||||
|
scopeId: "S",
|
||||||
projectId: "P",
|
projectId: "P",
|
||||||
provider: { kind: "none" } as never,
|
provider: { kind: "none" } as never,
|
||||||
seedDocForPath: () => undefined,
|
seedDocForPath: () => undefined,
|
||||||
|
|
@ -167,13 +170,14 @@ describe("sheet-manager warm pool", () => {
|
||||||
it("uses the pre-connected entry session (ydoc mode) instead of reconnecting", async () => {
|
it("uses the pre-connected entry session (ydoc mode) instead of reconnecting", async () => {
|
||||||
const entryDoc = makeDoc();
|
const entryDoc = makeDoc();
|
||||||
const entrySession: FakeSession = {
|
const entrySession: FakeSession = {
|
||||||
room: "P:root.kicad_sch",
|
room: "S:P:root.kicad_sch",
|
||||||
doc: entryDoc,
|
doc: entryDoc,
|
||||||
provider: { destroy: vi.fn() },
|
provider: { destroy: vi.fn() },
|
||||||
};
|
};
|
||||||
const m = createSheetCollabManager({
|
const m = createSheetCollabManager({
|
||||||
mod: {} as never,
|
mod: {} as never,
|
||||||
win: {} as never,
|
win: {} as never,
|
||||||
|
scopeId: "S",
|
||||||
projectId: "P",
|
projectId: "P",
|
||||||
provider: { kind: "none" } as never,
|
provider: { kind: "none" } as never,
|
||||||
seedDocForPath: () => undefined,
|
seedDocForPath: () => undefined,
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,9 @@ export interface SheetManagerOptions {
|
||||||
mod: KicadItemsModule;
|
mod: KicadItemsModule;
|
||||||
/** The global the C++ emit side calls into (`window.kicadCollab.onItems`). */
|
/** The global the C++ emit side calls into (`window.kicadCollab.onItems`). */
|
||||||
win: KicadItemsWindow;
|
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;
|
projectId: string;
|
||||||
/** The env-selected Yjs provider config (same one the single-room path uses). */
|
/** The env-selected Yjs provider config (same one the single-room path uses). */
|
||||||
provider: ProviderConfig;
|
provider: ProviderConfig;
|
||||||
|
|
@ -127,7 +129,7 @@ interface Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSheetCollabManager(opts: SheetManagerOptions): SheetCollabManager {
|
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 bridge = moduleItemsBridge(mod, win);
|
||||||
const rooms = new Map<string, Room>();
|
const rooms = new Map<string, Room>();
|
||||||
// In-flight connects, so connectAll() and switchTo() racing on the same sheet (api
|
// 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 pending = (async () => {
|
||||||
const session = await connectKicadDoc({
|
const session = await connectKicadDoc({
|
||||||
provider,
|
provider,
|
||||||
room: collabRoomId(projectId, sheetPath),
|
room: collabRoomId(scopeId, projectId, sheetPath),
|
||||||
});
|
});
|
||||||
// Invisible observer (read-only-viewer): drop the provider's initial
|
// Invisible observer (read-only-viewer): drop the provider's initial
|
||||||
// empty awareness state before anyone can see it.
|
// 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({
|
const m = createSheetCollabManager({
|
||||||
mod,
|
mod,
|
||||||
win,
|
win,
|
||||||
|
scopeId: "S",
|
||||||
projectId: "P",
|
projectId: "P",
|
||||||
provider: { kind: "none" } satisfies ProviderConfig,
|
provider: { kind: "none" } satisfies ProviderConfig,
|
||||||
seedDocForPath: () => undefined,
|
seedDocForPath: () => undefined,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue