@nebutra/collab 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @nebutra/collab
2
2
 
3
+ ## 0.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @nebutra/capability-kit@2.0.0
9
+ - @nebutra/tenant-store@2.0.0
10
+
3
11
  ## 0.2.2
4
12
 
5
13
  ### Patch Changes
package/README.md CHANGED
Binary file
@@ -29,4 +29,4 @@ var LoopbackTransport = class {
29
29
  export {
30
30
  LoopbackTransport
31
31
  };
32
- //# sourceMappingURL=chunk-S2HTNEZN.js.map
32
+ //# sourceMappingURL=chunk-PMIKORQM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/loopback.ts"],"sourcesContent":["/**\n * Zero-config default `CollabTransport`. An in-process loopback bus keyed by\n * `tenantId\\0roomId`, so a subscriber for tenant A's room can never be\n * reached by a broadcast for tenant B's room of the same id (the tenant is\n * part of the channel key, not trusted from payload).\n *\n * A real network transport (Pusher channels, a WebSocket relay) implements\n * the same `CollabTransport` interface and is injected via\n * `createCollab({ transport })`. This file intentionally ships NO network\n * code — only the loopback so zero-config single-process usage works.\n */\n\nimport type { CollabTransport } from \"../types\";\n\ntype Listener = (update: Uint8Array) => void;\n\nfunction channel(tenantId: string, roomId: string): string {\n // NUL separator: cannot occur in normal ids, so \"a\",\"bc\" and \"ab\",\"c\"\n // can never collide into the same channel.\n return `${tenantId}\\0${roomId}`;\n}\n\nexport class LoopbackTransport implements CollabTransport {\n private readonly channels = new Map<string, Set<Listener>>();\n\n broadcast(tenantId: string, roomId: string, update: Uint8Array): void {\n const set = this.channels.get(channel(tenantId, roomId));\n if (!set) return;\n // Snapshot listeners so unsubscribing during dispatch is safe.\n for (const cb of [...set]) cb(update);\n }\n\n subscribe(tenantId: string, roomId: string, cb: Listener): () => void {\n const key = channel(tenantId, roomId);\n let set = this.channels.get(key);\n if (!set) {\n set = new Set();\n this.channels.set(key, set);\n }\n set.add(cb);\n return () => {\n const s = this.channels.get(key);\n if (!s) return;\n s.delete(cb);\n if (s.size === 0) this.channels.delete(key);\n };\n }\n}\n"],"mappings":";AAgBA,SAAS,QAAQ,UAAkB,QAAwB;AAGzD,SAAO,GAAG,QAAQ,KAAK,MAAM;AAC/B;AAEO,IAAM,oBAAN,MAAmD;AAAA,EACvC,WAAW,oBAAI,IAA2B;AAAA,EAE3D,UAAU,UAAkB,QAAgB,QAA0B;AACpE,UAAM,MAAM,KAAK,SAAS,IAAI,QAAQ,UAAU,MAAM,CAAC;AACvD,QAAI,CAAC,IAAK;AAEV,eAAW,MAAM,CAAC,GAAG,GAAG,EAAG,IAAG,MAAM;AAAA,EACtC;AAAA,EAEA,UAAU,UAAkB,QAAgB,IAA0B;AACpE,UAAM,MAAM,QAAQ,UAAU,MAAM;AACpC,QAAI,MAAM,KAAK,SAAS,IAAI,GAAG;AAC/B,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,SAAS,IAAI,KAAK,GAAG;AAAA,IAC5B;AACA,QAAI,IAAI,EAAE;AACV,WAAO,MAAM;AACX,YAAM,IAAI,KAAK,SAAS,IAAI,GAAG;AAC/B,UAAI,CAAC,EAAG;AACR,QAAE,OAAO,EAAE;AACX,UAAI,EAAE,SAAS,EAAG,MAAK,SAAS,OAAO,GAAG;AAAA,IAC5C;AAAA,EACF;AACF;","names":[]}
package/dist/index.d.ts CHANGED
Binary file
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-6XHNO2RP.js";
4
4
  import {
5
5
  LoopbackTransport
6
- } from "./chunk-S2HTNEZN.js";
6
+ } from "./chunk-PMIKORQM.js";
7
7
 
8
8
  // src/errors.ts
9
9
  import { CapabilityError } from "@nebutra/capability-kit";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/room.ts","../src/hub.ts"],"sourcesContent":["/**\n * Every failure surfaced by this package is a `CollabError`. The contract is\n * deliberately strict: a machine-stable `code` and a human-actionable\n * `suggestion` are MANDATORY, so no code path can throw a bare `Error` that\n * leaves a caller without a remediation hint.\n *\n * Mechanics (code/suggestion/toJSON/empty-suggestion fallback) are inherited\n * from the shared `@nebutra/capability-kit` `CapabilityError`; this subclass\n * only pins collab's error name + its package-specific fallback wording, so\n * the observable contract is unchanged.\n */\n\nimport { CapabilityError } from \"@nebutra/capability-kit\";\n\nexport type CollabErrorCode =\n | \"COLLAB_INVALID_TENANT\"\n | \"COLLAB_INVALID_ROOM\"\n | \"COLLAB_SNAPSHOT_FAILED\"\n | \"COLLAB_RESTORE_FAILED\"\n | \"COLLAB_DESTROYED\"\n | \"COLLAB_TEST\"\n | (string & {});\n\nexport interface CollabErrorInit {\n readonly code: CollabErrorCode;\n /** A non-empty, actionable remediation hint. */\n readonly suggestion: string;\n readonly cause?: unknown;\n}\n\nexport class CollabError extends CapabilityError {\n declare readonly code: CollabErrorCode;\n\n constructor(message: string, init: CollabErrorInit) {\n super(message, init, {\n name: \"CollabError\",\n emptySuggestionFallback:\n \"No suggestion was provided. This is a bug in @nebutra/collab — \" +\n \"report it with the failing operation.\",\n });\n }\n}\n","/**\n * A single tenant-scoped CRDT room. One `Y.Doc` per (tenant, room). The hub\n * owns the partitioning; this class assumes its `tenantId`/`roomId` are\n * already the partition it belongs to and never reaches outside them.\n *\n * Snapshot persistence is serialized through `withTenantLock(tenantId,\n * roomId, ...)` borrowed from `@nebutra/tenant-store` rather than a\n * hand-rolled mutex — same primitive used by canvas/reel, so a future swap\n * to a distributed lock changes one place.\n */\n\nimport { withTenantLock } from \"@nebutra/tenant-store\";\nimport * as Y from \"yjs\";\nimport { CollabError } from \"./errors\";\nimport type { CollabRoom, CollabTransport, SnapshotStore, UpdateListener } from \"./types\";\n\n/** Origin tag used when applying remote updates so we don't echo them back. */\nconst REMOTE_ORIGIN = Symbol(\"collab.remote\");\n\nexport class Room implements CollabRoom {\n readonly doc: Y.Doc;\n private readonly listeners = new Set<UpdateListener>();\n private readonly unsubTransport: () => void;\n private destroyed = false;\n\n constructor(\n readonly tenantId: string,\n readonly roomId: string,\n private readonly store: SnapshotStore,\n private readonly transport: CollabTransport,\n ) {\n this.doc = new Y.Doc();\n\n // Fan local updates out to: registered listeners + the transport. The\n // transport echo is guarded by origin so a remote-applied update is not\n // re-broadcast into a loop.\n this.doc.on(\"update\", (update: Uint8Array, origin: unknown) => {\n for (const cb of [...this.listeners]) cb(update, origin);\n if (origin !== REMOTE_ORIGIN) {\n void Promise.resolve(this.transport.broadcast(this.tenantId, this.roomId, update)).catch(\n () => {\n // Transport delivery is best-effort; CRDT state stays correct and\n // converges on the next exchanged update. Swallowing here avoids\n // an unhandled rejection from a flaky network adapter.\n },\n );\n }\n });\n\n // Remote updates for THIS tenant-scoped channel only.\n this.unsubTransport = this.transport.subscribe(this.tenantId, this.roomId, (update) => {\n if (this.destroyed) return;\n Y.applyUpdate(this.doc, update, REMOTE_ORIGIN);\n });\n }\n\n applyUpdate(update: Uint8Array, origin?: unknown): void {\n this.assertLive();\n Y.applyUpdate(this.doc, update, origin);\n }\n\n encodeState(): Uint8Array {\n this.assertLive();\n return Y.encodeStateAsUpdate(this.doc);\n }\n\n onUpdate(cb: UpdateListener): () => void {\n this.assertLive();\n this.listeners.add(cb);\n return () => {\n this.listeners.delete(cb);\n };\n }\n\n async snapshot(): Promise<void> {\n this.assertLive();\n const state = this.encodeState();\n try {\n // Serialize concurrent snapshots of the SAME room; different rooms (or\n // the same room under another tenant) persist in parallel.\n await withTenantLock(this.tenantId, this.roomId, () =>\n this.store.save(this.tenantId, this.roomId, state),\n );\n } catch (cause) {\n throw new CollabError(`Failed to persist snapshot for room \"${this.roomId}\".`, {\n code: \"COLLAB_SNAPSHOT_FAILED\",\n suggestion:\n \"Verify the configured SnapshotStore is reachable (DB/Redis up, \" +\n \"credentials valid). The in-memory default never fails; a custom \" +\n \"adapter likely threw.\",\n cause,\n });\n }\n }\n\n /** Hydrate this doc from persisted state, if any. Internal to the hub. */\n async _restore(): Promise<void> {\n try {\n const persisted = await this.store.load(this.tenantId, this.roomId);\n if (persisted) Y.applyUpdate(this.doc, persisted, REMOTE_ORIGIN);\n } catch (cause) {\n throw new CollabError(`Failed to restore room \"${this.roomId}\" from snapshot store.`, {\n code: \"COLLAB_RESTORE_FAILED\",\n suggestion:\n \"Check the SnapshotStore adapter's load() — it should resolve \" +\n \"null (not throw) when no snapshot exists for the tenant+room.\",\n cause,\n });\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.unsubTransport();\n this.listeners.clear();\n this.doc.destroy();\n }\n\n private assertLive(): void {\n if (this.destroyed) {\n throw new CollabError(`Room \"${this.roomId}\" was destroyed and can no longer be used.`, {\n code: \"COLLAB_DESTROYED\",\n suggestion:\n \"Acquire a fresh room via hub.room(tenantId, roomId) instead of \" +\n \"reusing a destroyed instance.\",\n });\n }\n }\n}\n","/**\n * `CollabHub` — the tenant-partition boundary.\n *\n * TENANT ISOLATION IS STRUCTURAL: rooms are stored in a `Map` keyed by a\n * composite `tenantId\u0000roomId` string with a NUL separator that cannot\n * appear in a normal id. There is no API that takes only a roomId, so a key\n * minted for tenant A is unreachable from tenant B — a room handle is only\n * ever produced by passing an explicit `tenantId`, and the snapshot store +\n * transport are likewise addressed by (tenantId, roomId). The same property\n * a Prisma adapter gets from RLS is enforced here by the composite key, with\n * no trust placed in caller-supplied payload.\n */\n\nimport { CollabError } from \"./errors\";\nimport { Room } from \"./room\";\nimport { InMemorySnapshotStore } from \"./store/memory\";\nimport { LoopbackTransport } from \"./transport/loopback\";\nimport type {\n CollabConfig,\n CollabHub,\n CollabRoom,\n CollabTransport,\n DoctorReport,\n SnapshotStore,\n} from \"./types\";\n\n// NUL separator: it cannot appear in a normal tenant/room id, so the pairs\n// (\"a\",\"bc\") and (\"ab\",\"c\") can never collide into the same room key — the\n// tenant partition is exact, not merely conventional.\nconst KEY_SEP = \"\u0000\";\n\nfunction roomKey(tenantId: string, roomId: string): string {\n return `${tenantId}${KEY_SEP}${roomId}`;\n}\n\nfunction assertId(value: string, kind: \"tenant\" | \"room\"): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new CollabError(`A non-empty ${kind}Id is required.`, {\n code: kind === \"tenant\" ? \"COLLAB_INVALID_TENANT\" : \"COLLAB_INVALID_ROOM\",\n suggestion:\n kind === \"tenant\"\n ? \"Pass the current tenant id, e.g. from getCurrentTenant().tenantId — \" +\n \"never call room() without an explicit tenant.\"\n : \"Pass a stable, non-empty roomId (document/canvas id).\",\n });\n }\n}\n\nclass Hub implements CollabHub {\n private readonly rooms = new Map<string, Room>();\n private readonly store: SnapshotStore;\n private readonly transport: CollabTransport;\n\n constructor(config: CollabConfig = {}) {\n this.store = config.store ?? new InMemorySnapshotStore();\n this.transport = config.transport ?? new LoopbackTransport();\n }\n\n room(tenantId: string, roomId: string): CollabRoom {\n assertId(tenantId, \"tenant\");\n assertId(roomId, \"room\");\n const key = roomKey(tenantId, roomId);\n let room = this.rooms.get(key);\n if (!room) {\n room = new Room(tenantId, roomId, this.store, this.transport);\n this.rooms.set(key, room);\n }\n return room;\n }\n\n async roomRestored(tenantId: string, roomId: string): Promise<CollabRoom> {\n const key = roomKey(tenantId, roomId);\n const existed = this.rooms.has(key);\n const room = this.room(tenantId, roomId) as Room;\n // Only hydrate a freshly created room; an already-live room is the\n // authoritative in-memory state.\n if (!existed) await room._restore();\n return room;\n }\n\n async doctor(): Promise<DoctorReport> {\n const start = Date.now();\n\n // 1. Yjs presence + a real round-trip (not just \"is it imported\").\n let yjsOk = false;\n let yjsDetail = \"\";\n try {\n const Y = await import(\"yjs\");\n const probe = new Y.Doc();\n probe.getMap(\"p\").set(\"k\", 1);\n const restored = new Y.Doc();\n Y.applyUpdate(restored, Y.encodeStateAsUpdate(probe));\n const ok = restored.getMap(\"p\").get(\"k\") === 1;\n probe.destroy();\n restored.destroy();\n yjsOk = ok;\n yjsDetail = ok\n ? \"Yjs encode/apply round-trip succeeded.\"\n : \"Yjs round-trip produced unexpected state.\";\n } catch (e) {\n yjsDetail = `Yjs unavailable: ${String(e)}`;\n }\n const yjs = { ok: yjsOk, detail: yjsDetail };\n\n // 2. Store health — write+read under a reserved diagnostic tenant.\n let storeOk = false;\n let storeDetail = \"\";\n try {\n const probe = new Uint8Array([7, 7, 7]);\n await this.store.save(\"__collab_doctor__\", \"__probe__\", probe);\n const back = await this.store.load(\"__collab_doctor__\", \"__probe__\");\n const ok = !!back && back.length === probe.length && back[0] === 7;\n storeOk = ok;\n storeDetail = ok\n ? \"SnapshotStore save/load round-trip succeeded.\"\n : \"SnapshotStore returned unexpected bytes.\";\n } catch (e) {\n storeDetail = `SnapshotStore error: ${String(e)}`;\n }\n const store = { ok: storeOk, detail: storeDetail };\n\n // 3. Transport health — subscribe, broadcast, observe, unsubscribe.\n let transportOk = false;\n let transportDetail = \"\";\n try {\n let received = false;\n const off = this.transport.subscribe(\"__collab_doctor__\", \"__probe__\", () => {\n received = true;\n });\n await this.transport.broadcast(\"__collab_doctor__\", \"__probe__\", new Uint8Array([1]));\n off();\n transportOk = received;\n transportDetail = received\n ? \"Transport broadcast/subscribe round-trip succeeded.\"\n : \"Transport did not deliver the probe (network adapter may be async).\";\n } catch (e) {\n transportDetail = `Transport error: ${String(e)}`;\n }\n const transport = { ok: transportOk, detail: transportDetail };\n\n return {\n ok: yjs.ok && store.ok && transport.ok,\n durationMs: Date.now() - start,\n checks: { yjs, store, transport },\n };\n }\n\n destroy(): void {\n for (const room of this.rooms.values()) room.destroy();\n this.rooms.clear();\n }\n}\n\n/** Synchronous factory. */\nexport function createCollab(config?: CollabConfig): CollabHub {\n return new Hub(config);\n}\n\n/**\n * Async factory mirroring other Sailor integration packages. There is no\n * env/credential negotiation for the zero-config defaults, so this resolves\n * immediately — the async shape is reserved for future credentialed\n * store/transport providers without a breaking signature change.\n */\nexport async function getCollab(config?: CollabConfig): Promise<CollabHub> {\n return createCollab(config);\n}\n"],"mappings":"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","names":["Y"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/room.ts","../src/hub.ts"],"sourcesContent":["/**\n * Every failure surfaced by this package is a `CollabError`. The contract is\n * deliberately strict: a machine-stable `code` and a human-actionable\n * `suggestion` are MANDATORY, so no code path can throw a bare `Error` that\n * leaves a caller without a remediation hint.\n *\n * Mechanics (code/suggestion/toJSON/empty-suggestion fallback) are inherited\n * from the shared `@nebutra/capability-kit` `CapabilityError`; this subclass\n * only pins collab's error name + its package-specific fallback wording, so\n * the observable contract is unchanged.\n */\n\nimport { CapabilityError } from \"@nebutra/capability-kit\";\n\nexport type CollabErrorCode =\n | \"COLLAB_INVALID_TENANT\"\n | \"COLLAB_INVALID_ROOM\"\n | \"COLLAB_SNAPSHOT_FAILED\"\n | \"COLLAB_RESTORE_FAILED\"\n | \"COLLAB_DESTROYED\"\n | \"COLLAB_TEST\"\n | (string & {});\n\nexport interface CollabErrorInit {\n readonly code: CollabErrorCode;\n /** A non-empty, actionable remediation hint. */\n readonly suggestion: string;\n readonly cause?: unknown;\n}\n\nexport class CollabError extends CapabilityError {\n declare readonly code: CollabErrorCode;\n\n constructor(message: string, init: CollabErrorInit) {\n super(message, init, {\n name: \"CollabError\",\n emptySuggestionFallback:\n \"No suggestion was provided. This is a bug in @nebutra/collab — \" +\n \"report it with the failing operation.\",\n });\n }\n}\n","/**\n * A single tenant-scoped CRDT room. One `Y.Doc` per (tenant, room). The hub\n * owns the partitioning; this class assumes its `tenantId`/`roomId` are\n * already the partition it belongs to and never reaches outside them.\n *\n * Snapshot persistence is serialized through `withTenantLock(tenantId,\n * roomId, ...)` borrowed from `@nebutra/tenant-store` rather than a\n * hand-rolled mutex — same primitive used by canvas/reel, so a future swap\n * to a distributed lock changes one place.\n */\n\nimport { withTenantLock } from \"@nebutra/tenant-store\";\nimport * as Y from \"yjs\";\nimport { CollabError } from \"./errors\";\nimport type { CollabRoom, CollabTransport, SnapshotStore, UpdateListener } from \"./types\";\n\n/** Origin tag used when applying remote updates so we don't echo them back. */\nconst REMOTE_ORIGIN = Symbol(\"collab.remote\");\n\nexport class Room implements CollabRoom {\n readonly doc: Y.Doc;\n private readonly listeners = new Set<UpdateListener>();\n private readonly unsubTransport: () => void;\n private destroyed = false;\n\n constructor(\n readonly tenantId: string,\n readonly roomId: string,\n private readonly store: SnapshotStore,\n private readonly transport: CollabTransport,\n ) {\n this.doc = new Y.Doc();\n\n // Fan local updates out to: registered listeners + the transport. The\n // transport echo is guarded by origin so a remote-applied update is not\n // re-broadcast into a loop.\n this.doc.on(\"update\", (update: Uint8Array, origin: unknown) => {\n for (const cb of [...this.listeners]) cb(update, origin);\n if (origin !== REMOTE_ORIGIN) {\n void Promise.resolve(this.transport.broadcast(this.tenantId, this.roomId, update)).catch(\n () => {\n // Transport delivery is best-effort; CRDT state stays correct and\n // converges on the next exchanged update. Swallowing here avoids\n // an unhandled rejection from a flaky network adapter.\n },\n );\n }\n });\n\n // Remote updates for THIS tenant-scoped channel only.\n this.unsubTransport = this.transport.subscribe(this.tenantId, this.roomId, (update) => {\n if (this.destroyed) return;\n Y.applyUpdate(this.doc, update, REMOTE_ORIGIN);\n });\n }\n\n applyUpdate(update: Uint8Array, origin?: unknown): void {\n this.assertLive();\n Y.applyUpdate(this.doc, update, origin);\n }\n\n encodeState(): Uint8Array {\n this.assertLive();\n return Y.encodeStateAsUpdate(this.doc);\n }\n\n onUpdate(cb: UpdateListener): () => void {\n this.assertLive();\n this.listeners.add(cb);\n return () => {\n this.listeners.delete(cb);\n };\n }\n\n async snapshot(): Promise<void> {\n this.assertLive();\n const state = this.encodeState();\n try {\n // Serialize concurrent snapshots of the SAME room; different rooms (or\n // the same room under another tenant) persist in parallel.\n await withTenantLock(this.tenantId, this.roomId, () =>\n this.store.save(this.tenantId, this.roomId, state),\n );\n } catch (cause) {\n throw new CollabError(`Failed to persist snapshot for room \"${this.roomId}\".`, {\n code: \"COLLAB_SNAPSHOT_FAILED\",\n suggestion:\n \"Verify the configured SnapshotStore is reachable (DB/Redis up, \" +\n \"credentials valid). The in-memory default never fails; a custom \" +\n \"adapter likely threw.\",\n cause,\n });\n }\n }\n\n /** Hydrate this doc from persisted state, if any. Internal to the hub. */\n async _restore(): Promise<void> {\n try {\n const persisted = await this.store.load(this.tenantId, this.roomId);\n if (persisted) Y.applyUpdate(this.doc, persisted, REMOTE_ORIGIN);\n } catch (cause) {\n throw new CollabError(`Failed to restore room \"${this.roomId}\" from snapshot store.`, {\n code: \"COLLAB_RESTORE_FAILED\",\n suggestion:\n \"Check the SnapshotStore adapter's load() — it should resolve \" +\n \"null (not throw) when no snapshot exists for the tenant+room.\",\n cause,\n });\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.unsubTransport();\n this.listeners.clear();\n this.doc.destroy();\n }\n\n private assertLive(): void {\n if (this.destroyed) {\n throw new CollabError(`Room \"${this.roomId}\" was destroyed and can no longer be used.`, {\n code: \"COLLAB_DESTROYED\",\n suggestion:\n \"Acquire a fresh room via hub.room(tenantId, roomId) instead of \" +\n \"reusing a destroyed instance.\",\n });\n }\n }\n}\n","/**\n * `CollabHub` — the tenant-partition boundary.\n *\n * TENANT ISOLATION IS STRUCTURAL: rooms are stored in a `Map` keyed by a\n * composite `tenantId\\0roomId` string with a NUL separator that cannot\n * appear in a normal id. There is no API that takes only a roomId, so a key\n * minted for tenant A is unreachable from tenant B — a room handle is only\n * ever produced by passing an explicit `tenantId`, and the snapshot store +\n * transport are likewise addressed by (tenantId, roomId). The same property\n * a Prisma adapter gets from RLS is enforced here by the composite key, with\n * no trust placed in caller-supplied payload.\n */\n\nimport { CollabError } from \"./errors\";\nimport { Room } from \"./room\";\nimport { InMemorySnapshotStore } from \"./store/memory\";\nimport { LoopbackTransport } from \"./transport/loopback\";\nimport type {\n CollabConfig,\n CollabHub,\n CollabRoom,\n CollabTransport,\n DoctorReport,\n SnapshotStore,\n} from \"./types\";\n\n// NUL separator: it cannot appear in a normal tenant/room id, so the pairs\n// (\"a\",\"bc\") and (\"ab\",\"c\") can never collide into the same room key — the\n// tenant partition is exact, not merely conventional.\nconst KEY_SEP = \"\\0\";\n\nfunction roomKey(tenantId: string, roomId: string): string {\n return `${tenantId}${KEY_SEP}${roomId}`;\n}\n\nfunction assertId(value: string, kind: \"tenant\" | \"room\"): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new CollabError(`A non-empty ${kind}Id is required.`, {\n code: kind === \"tenant\" ? \"COLLAB_INVALID_TENANT\" : \"COLLAB_INVALID_ROOM\",\n suggestion:\n kind === \"tenant\"\n ? \"Pass the current tenant id, e.g. from getCurrentTenant().tenantId — \" +\n \"never call room() without an explicit tenant.\"\n : \"Pass a stable, non-empty roomId (document/canvas id).\",\n });\n }\n}\n\nclass Hub implements CollabHub {\n private readonly rooms = new Map<string, Room>();\n private readonly store: SnapshotStore;\n private readonly transport: CollabTransport;\n\n constructor(config: CollabConfig = {}) {\n this.store = config.store ?? new InMemorySnapshotStore();\n this.transport = config.transport ?? new LoopbackTransport();\n }\n\n room(tenantId: string, roomId: string): CollabRoom {\n assertId(tenantId, \"tenant\");\n assertId(roomId, \"room\");\n const key = roomKey(tenantId, roomId);\n let room = this.rooms.get(key);\n if (!room) {\n room = new Room(tenantId, roomId, this.store, this.transport);\n this.rooms.set(key, room);\n }\n return room;\n }\n\n async roomRestored(tenantId: string, roomId: string): Promise<CollabRoom> {\n const key = roomKey(tenantId, roomId);\n const existed = this.rooms.has(key);\n const room = this.room(tenantId, roomId) as Room;\n // Only hydrate a freshly created room; an already-live room is the\n // authoritative in-memory state.\n if (!existed) await room._restore();\n return room;\n }\n\n async doctor(): Promise<DoctorReport> {\n const start = Date.now();\n\n // 1. Yjs presence + a real round-trip (not just \"is it imported\").\n let yjsOk = false;\n let yjsDetail = \"\";\n try {\n const Y = await import(\"yjs\");\n const probe = new Y.Doc();\n probe.getMap(\"p\").set(\"k\", 1);\n const restored = new Y.Doc();\n Y.applyUpdate(restored, Y.encodeStateAsUpdate(probe));\n const ok = restored.getMap(\"p\").get(\"k\") === 1;\n probe.destroy();\n restored.destroy();\n yjsOk = ok;\n yjsDetail = ok\n ? \"Yjs encode/apply round-trip succeeded.\"\n : \"Yjs round-trip produced unexpected state.\";\n } catch (e) {\n yjsDetail = `Yjs unavailable: ${String(e)}`;\n }\n const yjs = { ok: yjsOk, detail: yjsDetail };\n\n // 2. Store health — write+read under a reserved diagnostic tenant.\n let storeOk = false;\n let storeDetail = \"\";\n try {\n const probe = new Uint8Array([7, 7, 7]);\n await this.store.save(\"__collab_doctor__\", \"__probe__\", probe);\n const back = await this.store.load(\"__collab_doctor__\", \"__probe__\");\n const ok = !!back && back.length === probe.length && back[0] === 7;\n storeOk = ok;\n storeDetail = ok\n ? \"SnapshotStore save/load round-trip succeeded.\"\n : \"SnapshotStore returned unexpected bytes.\";\n } catch (e) {\n storeDetail = `SnapshotStore error: ${String(e)}`;\n }\n const store = { ok: storeOk, detail: storeDetail };\n\n // 3. Transport health — subscribe, broadcast, observe, unsubscribe.\n let transportOk = false;\n let transportDetail = \"\";\n try {\n let received = false;\n const off = this.transport.subscribe(\"__collab_doctor__\", \"__probe__\", () => {\n received = true;\n });\n await this.transport.broadcast(\"__collab_doctor__\", \"__probe__\", new Uint8Array([1]));\n off();\n transportOk = received;\n transportDetail = received\n ? \"Transport broadcast/subscribe round-trip succeeded.\"\n : \"Transport did not deliver the probe (network adapter may be async).\";\n } catch (e) {\n transportDetail = `Transport error: ${String(e)}`;\n }\n const transport = { ok: transportOk, detail: transportDetail };\n\n return {\n ok: yjs.ok && store.ok && transport.ok,\n durationMs: Date.now() - start,\n checks: { yjs, store, transport },\n };\n }\n\n destroy(): void {\n for (const room of this.rooms.values()) room.destroy();\n this.rooms.clear();\n }\n}\n\n/** Synchronous factory. */\nexport function createCollab(config?: CollabConfig): CollabHub {\n return new Hub(config);\n}\n\n/**\n * Async factory mirroring other Sailor integration packages. There is no\n * env/credential negotiation for the zero-config defaults, so this resolves\n * immediately — the async shape is reserved for future credentialed\n * store/transport providers without a breaking signature change.\n */\nexport async function getCollab(config?: CollabConfig): Promise<CollabHub> {\n return createCollab(config);\n}\n"],"mappings":"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","names":["Y"]}
Binary file
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LoopbackTransport
3
- } from "../chunk-S2HTNEZN.js";
3
+ } from "../chunk-PMIKORQM.js";
4
4
  export {
5
5
  LoopbackTransport
6
6
  };
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@nebutra/collab",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Multi-tenant, transport-agnostic real-time collaborative sync layer: tenant-partitioned CRDT rooms (Yjs) with pluggable snapshot store + transport seams",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "nebutra": {
9
+ "status": "wip",
10
+ "graph": "labs",
9
11
  "featureId": "collab",
10
12
  "category": "integrations",
11
13
  "summary": "Tenant-partitioned CRDT room manager (Yjs) with pluggable store + transport seams"
@@ -21,8 +23,8 @@
21
23
  },
22
24
  "dependencies": {
23
25
  "yjs": "^13.6.30",
24
- "@nebutra/tenant-store": "0.2.2",
25
- "@nebutra/capability-kit": "0.2.4"
26
+ "@nebutra/tenant-store": "2.0.0",
27
+ "@nebutra/capability-kit": "2.0.0"
26
28
  },
27
29
  "devDependencies": {
28
30
  "@types/node": "^25.9.1",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/transport/loopback.ts"],"sourcesContent":["/**\n * Zero-config default `CollabTransport`. An in-process loopback bus keyed by\n * `tenantId\u0000roomId`, so a subscriber for tenant A's room can never be\n * reached by a broadcast for tenant B's room of the same id (the tenant is\n * part of the channel key, not trusted from payload).\n *\n * A real network transport (Pusher channels, a WebSocket relay) implements\n * the same `CollabTransport` interface and is injected via\n * `createCollab({ transport })`. This file intentionally ships NO network\n * code — only the loopback so zero-config single-process usage works.\n */\n\nimport type { CollabTransport } from \"../types\";\n\ntype Listener = (update: Uint8Array) => void;\n\nfunction channel(tenantId: string, roomId: string): string {\n // NUL separator: cannot occur in normal ids, so \"a\",\"bc\" and \"ab\",\"c\"\n // can never collide into the same channel.\n return `${tenantId}\u0000${roomId}`;\n}\n\nexport class LoopbackTransport implements CollabTransport {\n private readonly channels = new Map<string, Set<Listener>>();\n\n broadcast(tenantId: string, roomId: string, update: Uint8Array): void {\n const set = this.channels.get(channel(tenantId, roomId));\n if (!set) return;\n // Snapshot listeners so unsubscribing during dispatch is safe.\n for (const cb of [...set]) cb(update);\n }\n\n subscribe(tenantId: string, roomId: string, cb: Listener): () => void {\n const key = channel(tenantId, roomId);\n let set = this.channels.get(key);\n if (!set) {\n set = new Set();\n this.channels.set(key, set);\n }\n set.add(cb);\n return () => {\n const s = this.channels.get(key);\n if (!s) return;\n s.delete(cb);\n if (s.size === 0) this.channels.delete(key);\n };\n }\n}\n"],"mappings":";AAgBA,SAAS,QAAQ,UAAkB,QAAwB;AAGzD,SAAO,GAAG,QAAQ,KAAI,MAAM;AAC9B;AAEO,IAAM,oBAAN,MAAmD;AAAA,EACvC,WAAW,oBAAI,IAA2B;AAAA,EAE3D,UAAU,UAAkB,QAAgB,QAA0B;AACpE,UAAM,MAAM,KAAK,SAAS,IAAI,QAAQ,UAAU,MAAM,CAAC;AACvD,QAAI,CAAC,IAAK;AAEV,eAAW,MAAM,CAAC,GAAG,GAAG,EAAG,IAAG,MAAM;AAAA,EACtC;AAAA,EAEA,UAAU,UAAkB,QAAgB,IAA0B;AACpE,UAAM,MAAM,QAAQ,UAAU,MAAM;AACpC,QAAI,MAAM,KAAK,SAAS,IAAI,GAAG;AAC/B,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,SAAS,IAAI,KAAK,GAAG;AAAA,IAC5B;AACA,QAAI,IAAI,EAAE;AACV,WAAO,MAAM;AACX,YAAM,IAAI,KAAK,SAAS,IAAI,GAAG;AAC/B,UAAI,CAAC,EAAG;AACR,QAAE,OAAO,EAAE;AACX,UAAI,EAAE,SAAS,EAAG,MAAK,SAAS,OAAO,GAAG;AAAA,IAC5C;AAAA,EACF;AACF;","names":[]}