@lumencast/runtime 0.10.0 → 0.11.0

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.
Files changed (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/{broadcast-L5wm2I6J.js → broadcast-DtHoU_fS.js} +3 -3
  3. package/dist/{broadcast-L5wm2I6J.js.map → broadcast-DtHoU_fS.js.map} +1 -1
  4. package/dist/{control-eEUG7unp.js → control-B9frEbNG.js} +4 -4
  5. package/dist/{control-eEUG7unp.js.map → control-B9frEbNG.js.map} +1 -1
  6. package/dist/{index-Clrya_9l.js → index-Dz27r92m.js} +378 -332
  7. package/dist/index-Dz27r92m.js.map +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.html +1 -1
  11. package/dist/index.js +5 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lumencast.js +22 -18
  14. package/dist/mount.d.ts.map +1 -1
  15. package/dist/mount.js +11 -0
  16. package/dist/mount.js.map +1 -1
  17. package/dist/render/primitives/index.d.ts.map +1 -1
  18. package/dist/render/primitives/index.js +6 -0
  19. package/dist/render/primitives/index.js.map +1 -1
  20. package/dist/render/primitives/meet-peer-slot.d.ts +29 -0
  21. package/dist/render/primitives/meet-peer-slot.d.ts.map +1 -0
  22. package/dist/render/primitives/meet-peer-slot.js +46 -0
  23. package/dist/render/primitives/meet-peer-slot.js.map +1 -0
  24. package/dist/state/reserved-leaves.d.ts +37 -0
  25. package/dist/state/reserved-leaves.d.ts.map +1 -0
  26. package/dist/state/reserved-leaves.js +96 -0
  27. package/dist/state/reserved-leaves.js.map +1 -0
  28. package/dist/{status-pill-elORkMrh.js → status-pill-B2vBTwRC.js} +2 -2
  29. package/dist/{status-pill-elORkMrh.js.map → status-pill-B2vBTwRC.js.map} +1 -1
  30. package/dist/{test-7q_KJkdX.js → test-DD2SBDku.js} +4 -4
  31. package/dist/{test-7q_KJkdX.js.map → test-DD2SBDku.js.map} +1 -1
  32. package/dist/{tree-BMxx5170.js → tree-CgU_sUwI.js} +213 -197
  33. package/dist/tree-CgU_sUwI.js.map +1 -0
  34. package/dist/types.d.ts +12 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +4 -4
  37. package/src/index.ts +12 -0
  38. package/src/mount.ts +12 -0
  39. package/src/render/primitives/index.ts +6 -0
  40. package/src/render/primitives/meet-peer-slot.tsx +55 -0
  41. package/src/state/reserved-leaves.ts +121 -0
  42. package/src/types.ts +12 -0
  43. package/dist/index-Clrya_9l.js.map +0 -1
  44. package/dist/tree-BMxx5170.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ErrorCode } from "@lumencast/protocol";
2
2
  import type { ResolveCaptureDevice } from "./render/primitives/capture";
3
3
  import type { ResolvePeerStream, SubscribePeerStream } from "./render/primitives/media";
4
+ import type { ReservedCamLeaves } from "./state/reserved-leaves";
4
5
  export type LumencastMode = "broadcast" | "control" | "test";
5
6
  export type LumencastStatus = "disconnected" | "connecting" | "live";
6
7
  export interface LumencastTokenProvider {
@@ -76,6 +77,17 @@ export interface MountOptions {
76
77
  * re-renders when its peer joins mid-show. `createPeerViewer()` returns one.
77
78
  * Preferred over `resolvePeerStream` when both are supplied. */
78
79
  subscribePeerStream?: SubscribePeerStream;
80
+ /** ADR Blue 009 §3.2–3.3 — host sink for the reserved `__cam.*` LSDP leaves
81
+ * (never rendered, never bound to a node). Called with the full current
82
+ * projection whenever it changes : `viewer` carries the receive-only viewer
83
+ * creds (`__cam.viewer`, Orion #268) and `slots` the `slotRef → peer_label`
84
+ * snapshot (`__cam.slots.*`, Orion #267). The host (Solar) feeds `viewer` into
85
+ * its peer-viewer injection and drives its slot-binding registry's
86
+ * `assign(slotRef, peer_label | null)` so `x-zab.meet-peer` nodes re-key by
87
+ * `slotRef`. Receive-only : the runtime forwards the token verbatim, never
88
+ * reads it. Omit it and the reserved leaves are simply not surfaced (the
89
+ * preview/headless paths are unaffected). */
90
+ onReservedLeaves?: (leaves: ReservedCamLeaves) => void;
79
91
  }
80
92
  export interface LumencastHandle {
81
93
  /** Tear down the WS, unmount the React tree, release timers. Idempotent. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAExF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,WAAW,GACX,mBAAmB,GACnB,eAAe,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;+BAE+B;AAC/B,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C;;;yDAGqD;IACrD,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACzD;;;;;;;yEAOqE;IACrE,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;;;gFAK4E;IAC5E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;qEAGiE;IACjE,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,WAAW,GACX,mBAAmB,GACnB,eAAe,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;+BAE+B;AAC/B,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C;;;yDAGqD;IACrD,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACzD;;;;;;;yEAOqE;IACrE,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;;;gFAK4E;IAC5E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;qEAGiE;IACjE,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C;;;;;;;;;kDAS8C;IAC9C,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACxD;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumencast/runtime",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Browser runtime for Lumencast — mount(), LSDP/1 transport, leaf-grain store, LSML render, animations, overlays.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "framer-motion": "^12.0.0",
37
37
  "react": "^19.0.0",
38
38
  "react-dom": "^19.0.0",
39
- "@lumencast/protocol": "0.10.0"
39
+ "@lumencast/protocol": "0.11.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@playwright/test": "^1.49.1",
@@ -50,8 +50,8 @@
50
50
  "vite-plugin-dts": "^4.5.0",
51
51
  "vitest": "^4.1.5",
52
52
  "ws": "^8.18.0",
53
- "@lumencast/dev-server": "0.10.0",
54
- "@lumencast/server": "0.10.0"
53
+ "@lumencast/server": "0.11.0",
54
+ "@lumencast/dev-server": "0.11.0"
55
55
  },
56
56
  "scripts": {
57
57
  "dev": "vite",
package/src/index.ts CHANGED
@@ -37,6 +37,18 @@ export type { ResolveCaptureDevice } from "./render/primitives/capture.js";
37
37
  // runtime's contract.
38
38
  export type { ResolvePeerStream, SubscribePeerStream } from "./render/primitives/media.js";
39
39
 
40
+ // ADR Blue 009 §3.2–3.3 — reserved `__cam.*` LSDP leaves (slot→peer assignments
41
+ // + receive-only viewer creds) surfaced to the host via `MountOptions
42
+ // .onReservedLeaves`. Exported so the consuming host (Solar) types its sink and
43
+ // reuses the reserved-path predicate / wire-name constants without re-deriving.
44
+ export {
45
+ CAM_RESERVED_PREFIX,
46
+ CAM_SLOTS_PREFIX,
47
+ CAM_VIEWER_LEAF,
48
+ isReservedCamPath,
49
+ type ReservedCamLeaves,
50
+ } from "./state/reserved-leaves.js";
51
+
40
52
  // ADR 006 #3 — WebRTC viewer (mesh, viewer role) + peer-stream registry. The
41
53
  // #3↔#4 bridge : a viewer joins Meet room(s), receives N peers and returns the
42
54
  // `resolvePeerStream` / `subscribePeerStream` the `media`/`meet.peer` LIVE render
package/src/mount.ts CHANGED
@@ -7,6 +7,7 @@ import { createElement } from "react";
7
7
  import { LumencastApp } from "./app.js";
8
8
  import { applyDelta } from "./state/apply-delta.js";
9
9
  import { applySnapshot } from "./state/apply-snapshot.js";
10
+ import { createReservedLeafObserver } from "./state/reserved-leaves.js";
10
11
  import { createStore } from "./state/store.js";
11
12
  import { createBundleFetcher, type BundleFetcher, type RenderBundle } from "./render/bundle.js";
12
13
  import { WsClient, type ConnectionStatus, type TransportError } from "./transport/ws.js";
@@ -45,6 +46,15 @@ export function mount(options: MountOptions): LumencastHandle {
45
46
 
46
47
  let active = true;
47
48
 
49
+ // ADR Blue 009 §3.2–3.3 — surface the reserved `__cam.*` LSDP leaves (the
50
+ // slot→peer assignments + the receive-only viewer creds) to the host so its
51
+ // WebRTC viewer (Solar) can drive room joins + `x-zab.meet-peer` slot re-keying.
52
+ // The runtime never joins, holds creds, or re-keys ; it only forwards. Created
53
+ // only when the host opts in — zero cost on the preview/headless paths.
54
+ const reservedLeaves = options.onReservedLeaves
55
+ ? createReservedLeafObserver(options.onReservedLeaves)
56
+ : undefined;
57
+
48
58
  // ADR 001 §3.4 (issue #34) — anti-silent-drop diagnostics are events
49
59
  // surfaced to the host, never console logs in `broadcast` mode.
50
60
  const removeDiagnosticsHandler = options.onDiagnostic
@@ -59,6 +69,7 @@ export function mount(options: MountOptions): LumencastHandle {
59
69
  onStatus: setStatus,
60
70
  onSnapshot: (frame) => {
61
71
  if (!active) return;
72
+ reservedLeaves?.onSnapshot(frame.state);
62
73
  void onSnapshot(
63
74
  bundleFetcher,
64
75
  bundleSignal,
@@ -78,6 +89,7 @@ export function mount(options: MountOptions): LumencastHandle {
78
89
  if (!active) return;
79
90
  const start = performance.now();
80
91
  applyDelta(store, frame);
92
+ reservedLeaves?.onDelta(frame.patches);
81
93
  options.onMetric?.({
82
94
  name: "delta_applied",
83
95
  duration_ms: performance.now() - start,
@@ -13,6 +13,7 @@ import { Image } from "./image";
13
13
  import { Shape } from "./shape";
14
14
  import { Media } from "./media";
15
15
  import { MeetPeer } from "./meet-peer";
16
+ import { MeetPeerSlot } from "./meet-peer-slot";
16
17
  import { Instance } from "./instance";
17
18
  import { Capture } from "./capture";
18
19
  // `repeat` is dispatched specially in the tree (it iterates a bound
@@ -55,4 +56,9 @@ export const PRIMITIVES: Partial<Record<RenderKind, ComponentType<PrimitiveProps
55
56
  instance: Instance,
56
57
  // RFC-0001 / ADR 004 — Zab vendor capture placeholder (transparent, inert).
57
58
  "x-zab.capture": Capture,
59
+ // ADR Blue 009 §3.1 (Amendment 2) — Zab vendor meet-peer SLOT placeholder.
60
+ // Carries only a logical `x-zab.slotRef` ; the host's slot-aware peer-stream
61
+ // registry resolves `slotRef → peer_label → MediaStream` (transparent when
62
+ // unbound). Closes the kind→primitive gap that left it an unknown-kind drop.
63
+ "x-zab.meet-peer": MeetPeerSlot,
58
64
  };
@@ -0,0 +1,55 @@
1
+ import type { PrimitiveProps } from "./index";
2
+ import { LivePeerVideo } from "./live-peer-video";
3
+
4
+ /** `x-zab.meet-peer` — the transparent meet-peer SLOT placeholder (Zab vendor
5
+ * primitive, ADR Blue 009 §3.1 Amendment 2 ; type shipped in v0.10.0 / #81).
6
+ *
7
+ * Distinct from `meet.peer` (the cam-identity source) : this node carries NO
8
+ * peer identity, only a hash-stable LOGICAL `x-zab.slotRef` (e.g. `cam-caster-1`)
9
+ * + geometry. WHICH `peer_label` fills a slot is RUNTIME, stream-level ZabCam
10
+ * state — never baked in the scene. The slot→peer binding is ported by Orion on
11
+ * the LSDP as `__cam.slots.<slotRef>` = "<peer_label>" (§3.3) and re-keyed into
12
+ * the host's peer-stream registry (Solar `slot-binding.ts`) so the registry
13
+ * resolves `slotRef → peer_label → MediaStream`.
14
+ *
15
+ * Contract (rendered verbatim) :
16
+ * - `x-zab.slotRef` (string) — the SLOT REFERENCE, used as the resolver KEY.
17
+ * The host's peer-stream resolver (`resolvePeerStream`/`subscribePeerStream`)
18
+ * is keyed by `slotRef` on the antenne (Solar's slot-aware registry maps it
19
+ * to a `peer_label`, then to a stream). Empty / missing → a transparent inert
20
+ * box (the slot is not addressable).
21
+ * - geometry (`width`/`height` + position) — applied by the Tree's
22
+ * UniversalWrapper, exactly like `meet.peer` ; the `<video>` fills the box
23
+ * 100%/100% and is never forced full-screen (RC-Geo).
24
+ *
25
+ * Receive-only : the slot reads its stream through the host viewer (Solar joins
26
+ * the room and owns the peer connections / track lifecycle) ; the primitive
27
+ * carries no creds and never mutates the scene (RC-ReadOnly). An UNBOUND slot
28
+ * (no `__cam.slots.*` assignment) or a not-yet-connected peer → a transparent
29
+ * placeholder, no throw, no diagnostic (R3). */
30
+ export function MeetPeerSlot({ resolved }: PrimitiveProps) {
31
+ const slotRef =
32
+ typeof resolved["x-zab.slotRef"] === "string" &&
33
+ (resolved["x-zab.slotRef"] as string).length > 0
34
+ ? (resolved["x-zab.slotRef"] as string)
35
+ : "";
36
+
37
+ // No slotRef → not addressable : a transparent inert box of the wrapper
38
+ // geometry (no throw, no diagnostic), exactly like an unbound slot.
39
+ if (slotRef === "") {
40
+ return (
41
+ <div
42
+ aria-hidden
43
+ data-lumencast-meet-peer-slot
44
+ style={{ width: "100%", height: "100%", opacity: 0, pointerEvents: "none" }}
45
+ />
46
+ );
47
+ }
48
+
49
+ // Key the peer-viewer resolver by `slotRef` (NOT a peer_label). The shared
50
+ // `LivePeerVideo` is resolver-key agnostic : it passes its `peerLabel` prop
51
+ // straight to `resolvePeerStream`/`subscribePeerStream`, and the host's
52
+ // slot-aware registry translates the slotRef to the bound peer's stream. An
53
+ // unbound slot resolves to `null` → the transparent placeholder.
54
+ return <LivePeerVideo peerLabel={slotRef} objectFit="cover" muted />;
55
+ }
@@ -0,0 +1,121 @@
1
+ // Reserved `__cam.*` LSDP leaves — surfaced to the host, never rendered.
2
+ //
3
+ // ADR Blue 009 §3.2–3.3 (axe 1, antenne). The meet-cam antenne path needs two
4
+ // pieces of RUNTIME, stream-level ZabCam state that travel on the LSDP wire as
5
+ // RESERVED leaves (they bind to no node and are never painted) :
6
+ //
7
+ // - `__cam.slots.<slotRef>` = "<peer_label>" (Orion #267 deltas, §3.3)
8
+ // which peer currently fills an authored `x-zab.meet-peer` slot.
9
+ // - `__cam.viewer` = { rooms: [{ signalingUrl, roomId, token }] } (Orion #268)
10
+ // the receive-only viewer credentials for the active stream.
11
+ //
12
+ // The runtime does NOT join rooms, hold creds, or re-key slots itself — that is
13
+ // the host's WebRTC viewer (Solar `peer-viewer/*`). The runtime's only job is to
14
+ // SURFACE these reserved leaves to the host through `MountOptions.onReservedLeaves`
15
+ // so Solar can feed `__cam.viewer` into its viewer injection and drive its
16
+ // slot-binding registry's `assign(slotRef, peer_label | null)` from `__cam.slots.*`.
17
+ // Receive-only : the token flows host→viewer→join ; the runtime never reads it.
18
+
19
+ /** Every reserved cam leaf lives under this prefix. */
20
+ export const CAM_RESERVED_PREFIX = "__cam.";
21
+
22
+ /** One scalar leaf per bound slot : `__cam.slots.<slotRef>` = "<peer_label>"
23
+ * (ADR Blue 009 §3.3). Mirrors Solar's `CAM_SLOTS_PREFIX` — keep them in sync. */
24
+ export const CAM_SLOTS_PREFIX = "__cam.slots.";
25
+
26
+ /** The single viewer-credentials leaf (ADR Blue 009 §3.2, Orion #268). */
27
+ export const CAM_VIEWER_LEAF = "__cam.viewer";
28
+
29
+ /** The reserved cam state surfaced to the host in one shot on every change. */
30
+ export interface ReservedCamLeaves {
31
+ /** `__cam.viewer` — receive-only viewer creds for the active stream. Opaque to
32
+ * the runtime (shape `{ rooms: [{ signalingUrl, roomId, token }] }` validated
33
+ * host-side) ; `undefined` when the leaf is absent. */
34
+ viewer?: unknown;
35
+ /** `slotRef → peer_label` snapshot from the `__cam.slots.*` subtree. A slot
36
+ * ABSENT from this map is UNBOUND — the host releases it (`assign(slotRef,
37
+ * null)`) so the `x-zab.meet-peer` node falls back to its placeholder. Only
38
+ * non-empty string values are kept. */
39
+ slots: Record<string, string>;
40
+ }
41
+
42
+ /** A reserved cam leaf the runtime forwards rather than renders. */
43
+ export function isReservedCamPath(path: string): boolean {
44
+ return path === CAM_VIEWER_LEAF || path.startsWith(CAM_SLOTS_PREFIX);
45
+ }
46
+
47
+ /** Project a raw reserved-leaf state into the host-facing shape. Defensive : a
48
+ * malformed slot value (non-string / empty) or empty slotRef is dropped. */
49
+ function project(raw: Map<string, unknown>): ReservedCamLeaves {
50
+ const slots: Record<string, string> = {};
51
+ let viewer: unknown;
52
+ for (const [path, value] of raw) {
53
+ if (path === CAM_VIEWER_LEAF) {
54
+ if (value !== undefined && value !== null) viewer = value;
55
+ } else if (path.startsWith(CAM_SLOTS_PREFIX)) {
56
+ const slotRef = path.slice(CAM_SLOTS_PREFIX.length);
57
+ if (slotRef !== "" && typeof value === "string" && value !== "") slots[slotRef] = value;
58
+ }
59
+ }
60
+ return viewer !== undefined ? { viewer, slots } : { slots };
61
+ }
62
+
63
+ /** A stable identity key for change detection — two `ReservedCamLeaves` with the
64
+ * same content always produce the same key regardless of insertion order. */
65
+ function identity(leaves: ReservedCamLeaves): string {
66
+ const slots = Object.keys(leaves.slots)
67
+ .sort()
68
+ .map((k) => `${k}=${leaves.slots[k]}`)
69
+ .join("&");
70
+ const viewer = leaves.viewer === undefined ? "" : JSON.stringify(leaves.viewer);
71
+ return `${slots}|${viewer}`;
72
+ }
73
+
74
+ export interface ReservedLeafObserver {
75
+ /** Reseed from a full snapshot's state (reserved leaves not present are
76
+ * dropped). Emits when the projected state changed. */
77
+ onSnapshot(state: Record<string, unknown>): void;
78
+ /** Apply a delta's patches ; emits only when a reserved leaf actually moved. */
79
+ onDelta(patches: ReadonlyArray<{ path: string; value: unknown }>): void;
80
+ }
81
+
82
+ /** Track the reserved `__cam.*` leaves across snapshots + deltas and `emit` the
83
+ * host-facing projection whenever it changes (de-duplicated by content, so an
84
+ * unrelated scene's deltas never call back). Created only when the host supplies
85
+ * `onReservedLeaves` — zero cost otherwise. */
86
+ export function createReservedLeafObserver(
87
+ emit: (leaves: ReservedCamLeaves) => void,
88
+ ): ReservedLeafObserver {
89
+ const raw = new Map<string, unknown>();
90
+ // Seed with the empty projection's identity so a plain scene (no cam leaves)
91
+ // never fires a spurious empty emit ; a later transition to/from cam state does.
92
+ let last = identity({ slots: {} });
93
+
94
+ const flush = (): void => {
95
+ const leaves = project(raw);
96
+ const key = identity(leaves);
97
+ if (key === last) return;
98
+ last = key;
99
+ emit(leaves);
100
+ };
101
+
102
+ return {
103
+ onSnapshot(state) {
104
+ raw.clear();
105
+ for (const [path, value] of Object.entries(state)) {
106
+ if (isReservedCamPath(path)) raw.set(path, value);
107
+ }
108
+ flush();
109
+ },
110
+ onDelta(patches) {
111
+ let touched = false;
112
+ for (const patch of patches) {
113
+ if (isReservedCamPath(patch.path)) {
114
+ raw.set(patch.path, patch.value);
115
+ touched = true;
116
+ }
117
+ }
118
+ if (touched) flush();
119
+ },
120
+ };
121
+ }
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import type { ErrorCode } from "@lumencast/protocol";
4
4
  import type { ResolveCaptureDevice } from "./render/primitives/capture";
5
5
  import type { ResolvePeerStream, SubscribePeerStream } from "./render/primitives/media";
6
+ import type { ReservedCamLeaves } from "./state/reserved-leaves";
6
7
 
7
8
  export type LumencastMode = "broadcast" | "control" | "test";
8
9
 
@@ -92,6 +93,17 @@ export interface MountOptions {
92
93
  * re-renders when its peer joins mid-show. `createPeerViewer()` returns one.
93
94
  * Preferred over `resolvePeerStream` when both are supplied. */
94
95
  subscribePeerStream?: SubscribePeerStream;
96
+ /** ADR Blue 009 §3.2–3.3 — host sink for the reserved `__cam.*` LSDP leaves
97
+ * (never rendered, never bound to a node). Called with the full current
98
+ * projection whenever it changes : `viewer` carries the receive-only viewer
99
+ * creds (`__cam.viewer`, Orion #268) and `slots` the `slotRef → peer_label`
100
+ * snapshot (`__cam.slots.*`, Orion #267). The host (Solar) feeds `viewer` into
101
+ * its peer-viewer injection and drives its slot-binding registry's
102
+ * `assign(slotRef, peer_label | null)` so `x-zab.meet-peer` nodes re-key by
103
+ * `slotRef`. Receive-only : the runtime forwards the token verbatim, never
104
+ * reads it. Omit it and the reserved leaves are simply not surfaced (the
105
+ * preview/headless paths are unaffected). */
106
+ onReservedLeaves?: (leaves: ReservedCamLeaves) => void;
95
107
  }
96
108
 
97
109
  export interface LumencastHandle {