@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/{broadcast-L5wm2I6J.js → broadcast-DtHoU_fS.js} +3 -3
- package/dist/{broadcast-L5wm2I6J.js.map → broadcast-DtHoU_fS.js.map} +1 -1
- package/dist/{control-eEUG7unp.js → control-B9frEbNG.js} +4 -4
- package/dist/{control-eEUG7unp.js.map → control-B9frEbNG.js.map} +1 -1
- package/dist/{index-Clrya_9l.js → index-Dz27r92m.js} +378 -332
- package/dist/index-Dz27r92m.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lumencast.js +22 -18
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +11 -0
- package/dist/mount.js.map +1 -1
- package/dist/render/primitives/index.d.ts.map +1 -1
- package/dist/render/primitives/index.js +6 -0
- package/dist/render/primitives/index.js.map +1 -1
- package/dist/render/primitives/meet-peer-slot.d.ts +29 -0
- package/dist/render/primitives/meet-peer-slot.d.ts.map +1 -0
- package/dist/render/primitives/meet-peer-slot.js +46 -0
- package/dist/render/primitives/meet-peer-slot.js.map +1 -0
- package/dist/state/reserved-leaves.d.ts +37 -0
- package/dist/state/reserved-leaves.d.ts.map +1 -0
- package/dist/state/reserved-leaves.js +96 -0
- package/dist/state/reserved-leaves.js.map +1 -0
- package/dist/{status-pill-elORkMrh.js → status-pill-B2vBTwRC.js} +2 -2
- package/dist/{status-pill-elORkMrh.js.map → status-pill-B2vBTwRC.js.map} +1 -1
- package/dist/{test-7q_KJkdX.js → test-DD2SBDku.js} +4 -4
- package/dist/{test-7q_KJkdX.js.map → test-DD2SBDku.js.map} +1 -1
- package/dist/{tree-BMxx5170.js → tree-CgU_sUwI.js} +213 -197
- package/dist/tree-CgU_sUwI.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +12 -0
- package/src/mount.ts +12 -0
- package/src/render/primitives/index.ts +6 -0
- package/src/render/primitives/meet-peer-slot.tsx +55 -0
- package/src/state/reserved-leaves.ts +121 -0
- package/src/types.ts +12 -0
- package/dist/index-Clrya_9l.js.map +0 -1
- 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. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
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/
|
|
54
|
-
"@lumencast/server": "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 {
|