@lumencast/runtime 0.10.0 → 0.12.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/README.md +39 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/{broadcast-L5wm2I6J.js → broadcast-ydSpPUje.js} +3 -3
- package/dist/{broadcast-L5wm2I6J.js.map → broadcast-ydSpPUje.js.map} +1 -1
- package/dist/{control-eEUG7unp.js → control-zTsF-bHP.js} +4 -4
- package/dist/{control-eEUG7unp.js.map → control-zTsF-bHP.js.map} +1 -1
- package/dist/{index-Clrya_9l.js → index-ClWi5UzJ.js} +422 -341
- package/dist/index-ClWi5UzJ.js.map +1 -0
- package/dist/index.d.ts +2 -1
- 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 +63 -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-DkHIOL5V.js} +2 -2
- package/dist/{status-pill-elORkMrh.js.map → status-pill-DkHIOL5V.js.map} +1 -1
- package/dist/{test-7q_KJkdX.js → test-COpMkyms.js} +4 -4
- package/dist/{test-7q_KJkdX.js.map → test-COpMkyms.js.map} +1 -1
- package/dist/transport/ws.d.ts +4 -1
- package/dist/transport/ws.d.ts.map +1 -1
- package/dist/transport/ws.js +7 -0
- package/dist/transport/ws.js.map +1 -1
- package/dist/{tree-BMxx5170.js → tree-Cubmxeqo.js} +213 -197
- package/dist/tree-Cubmxeqo.js.map +1 -0
- package/dist/types.d.ts +30 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/webrtc/index.d.ts +1 -1
- package/dist/webrtc/index.d.ts.map +1 -1
- package/dist/webrtc/index.js.map +1 -1
- package/dist/webrtc/peer-stream-registry.d.ts +17 -0
- package/dist/webrtc/peer-stream-registry.d.ts.map +1 -1
- package/dist/webrtc/peer-stream-registry.js +22 -0
- package/dist/webrtc/peer-stream-registry.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +13 -0
- package/src/mount.ts +66 -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/transport/ws.ts +11 -0
- package/src/types.ts +30 -2
- package/src/webrtc/index.ts +1 -0
- package/src/webrtc/peer-stream-registry.ts +38 -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
|
-
import type { ErrorCode } from "@lumencast/protocol";
|
|
1
|
+
import type { ErrorCode, SceneRosterEntry } 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 {
|
|
@@ -13,7 +14,12 @@ export interface LumencastError {
|
|
|
13
14
|
recoverable: boolean;
|
|
14
15
|
}
|
|
15
16
|
export interface LumencastMetric {
|
|
16
|
-
name: "delta_received" | "delta_applied" | "frame_dropped" | "reconnect" | "snapshot_received" | "scene_changed"
|
|
17
|
+
name: "delta_received" | "delta_applied" | "frame_dropped" | "reconnect" | "snapshot_received" | "scene_changed"
|
|
18
|
+
/** A render bundle was warmed ahead of time from a roster entry (either a
|
|
19
|
+
* `scene_roster` frame or the `preloadRoster` mount option). Emitted once
|
|
20
|
+
* the warm fetch resolves (or from cache). Carries `scene_id` +
|
|
21
|
+
* `scene_version` + `source` ("frame" | "option"). */
|
|
22
|
+
| "roster_preloaded";
|
|
17
23
|
[key: string]: unknown;
|
|
18
24
|
}
|
|
19
25
|
/** Anti-silent-drop render diagnostic (ADR 001 §3.4, issue #34).
|
|
@@ -76,6 +82,28 @@ export interface MountOptions {
|
|
|
76
82
|
* re-renders when its peer joins mid-show. `createPeerViewer()` returns one.
|
|
77
83
|
* Preferred over `resolvePeerStream` when both are supplied. */
|
|
78
84
|
subscribePeerStream?: SubscribePeerStream;
|
|
85
|
+
/** ADR Blue 009 §3.2–3.3 — host sink for the reserved `__cam.*` LSDP leaves
|
|
86
|
+
* (never rendered, never bound to a node). Called with the full current
|
|
87
|
+
* projection whenever it changes : `viewer` carries the receive-only viewer
|
|
88
|
+
* creds (`__cam.viewer`, Orion #268) and `slots` the `slotRef → peer_label`
|
|
89
|
+
* snapshot (`__cam.slots.*`, Orion #267). The host (Solar) feeds `viewer` into
|
|
90
|
+
* its peer-viewer injection and drives its slot-binding registry's
|
|
91
|
+
* `assign(slotRef, peer_label | null)` so `x-zab.meet-peer` nodes re-key by
|
|
92
|
+
* `slotRef`. Receive-only : the runtime forwards the token verbatim, never
|
|
93
|
+
* reads it. Omit it and the reserved leaves are simply not surfaced (the
|
|
94
|
+
* preview/headless paths are unaffected). */
|
|
95
|
+
onReservedLeaves?: (leaves: ReservedCamLeaves) => void;
|
|
96
|
+
/** Preload the render bundles of a known scene roster so the FIRST switch to
|
|
97
|
+
* each scene is instant (a warm cache hit instead of a blocking fetch).
|
|
98
|
+
* Each entry is `{ scene_id, scene_version }`. Warmed in the background right
|
|
99
|
+
* after mount — best-effort: a failed warm is swallowed (the scene still
|
|
100
|
+
* fetches on demand at switch time) and never blocks or errors the mount.
|
|
101
|
+
*
|
|
102
|
+
* This is the PUBLIC preload surface (lumencast-js #87b) for hosts that
|
|
103
|
+
* already know the roster at mount time. When the server also emits
|
|
104
|
+
* `scene_roster` frames the runtime warms from those too — both paths feed
|
|
105
|
+
* the same cache and are idempotent (a version is warmed at most once). */
|
|
106
|
+
preloadRoster?: readonly SceneRosterEntry[];
|
|
79
107
|
}
|
|
80
108
|
export interface LumencastHandle {
|
|
81
109
|
/** 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;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvE,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;IACjB;;;2DAGuD;OACrD,kBAAkB,CAAC;IACvB,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;IACvD;;;;;;;;;gFAS4E;IAC5E,aAAa,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC7C;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/dist/webrtc/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MeetViewer, type MeetViewerOptions } from "./meet-viewer.js";
|
|
2
2
|
import { type PeerStreamListener, type PeerStreamRegistry } from "./peer-stream-registry.js";
|
|
3
3
|
export { MeetViewer, type MeetViewerOptions, type MeetViewerDeps, type PeerInfo, type RemoteTrackEvent, } from "./meet-viewer.js";
|
|
4
|
-
export { createPeerStreamRegistry, type PeerStreamRegistry, type PeerStreamListener, } from "./peer-stream-registry.js";
|
|
4
|
+
export { createPeerStreamRegistry, type PeerStreamRegistry, type PeerStreamListener, type RosterListener, } from "./peer-stream-registry.js";
|
|
5
5
|
export interface PeerViewer {
|
|
6
6
|
/** Join the room(s) (viewer role, no capture). */
|
|
7
7
|
join: () => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAyB,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAyB,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,yEAAyE;IACzE,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAC;IAC7D,8EAA8E;IAC9E,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,KAAK,MAAM,IAAI,CAAC;IACrF,mDAAmD;IACnD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;gDAC4C;IAC5C,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED;;;;mFAImF;AACnF;;;;;;;8BAO8B;AAC9B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1C;AA+BD;8EAC8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAevE;AAED,8DAA8D;AAC9D,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG;IAC1D,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB;oFACgF;IAChF,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD;;oEAEgE;IAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED;;;;;;;;;;2EAU2E;AAC3E,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,mBAAmB,CA8ErB;AAED;;oEAEoE;AACpE,MAAM,MAAM,mBAAmB,GAC3B,0BAA0B,GAC1B,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE1D;;oCAEoC;AACpC,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,mBAAmB,GAAG,mBAAmB,CAYjG"}
|
package/dist/webrtc/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,mEAAmE;AACnE,mFAAmF;AACnF,8EAA8E;AAC9E,kEAAkE;AAClE,+EAA+E;AAC/E,EAAE;AACF,iFAAiF;AACjF,iFAAiF;AACjF,6EAA6E;AAE7E,OAAO,EAAE,UAAU,EAAiD,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EACL,wBAAwB,GAGzB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,UAAU,GAKX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
export type PeerStreamListener = (stream: MediaStream | null) => void;
|
|
2
|
+
/** Notified whenever the live roster changes ORDER (a peer connects for the first
|
|
3
|
+
* time, or a connected peer drops). A pure re-`set` of an already-present label's
|
|
4
|
+
* stream is NOT a roster change (same arrival order) — it reaches per-label
|
|
5
|
+
* `subscribe` listeners only. Carries no payload : the listener re-reads
|
|
6
|
+
* `orderedLabels()`. */
|
|
7
|
+
export type RosterListener = () => void;
|
|
2
8
|
export interface PeerStreamRegistry {
|
|
3
9
|
/** #4 contract — the current stream for a label, or `null` if the peer is not
|
|
4
10
|
* connected (yet / any more). Synchronous, side-effect free. */
|
|
5
11
|
resolve(peerLabel: string): MediaStream | null;
|
|
12
|
+
/** The live `peer_label`s in ARRIVAL ORDER (insertion order of the backing Map,
|
|
13
|
+
* restricted to labels that currently hold a stream). Drives positional slot
|
|
14
|
+
* resolution (`@<n>` → `orderedLabels()[n]`, ADR Blue 009 axe 1 positional
|
|
15
|
+
* variant). Returns a fresh array — safe for the caller to keep / index. */
|
|
16
|
+
orderedLabels(): string[];
|
|
6
17
|
/** Push channel for the LIVE `media` primitive : invoked immediately with the
|
|
7
18
|
* current value, then on every change for `peerLabel`. Returns an
|
|
8
19
|
* unsubscribe. */
|
|
9
20
|
subscribe(peerLabel: string, listener: PeerStreamListener): () => void;
|
|
21
|
+
/** Roster-change channel : invoked whenever a peer connects (new label) or
|
|
22
|
+
* leaves (label dropped) — i.e. whenever `orderedLabels()` could shift. Lets a
|
|
23
|
+
* positional consumer re-resolve `@<n>` when arrivals/departures shuffle the
|
|
24
|
+
* order. NOT invoked on a pure stream replacement of an existing label. Returns
|
|
25
|
+
* an unsubscribe. */
|
|
26
|
+
subscribeRoster(listener: RosterListener): () => void;
|
|
10
27
|
/** Viewer-side : publish / replace a peer's stream (peer connected). */
|
|
11
28
|
set(peerLabel: string, stream: MediaStream): void;
|
|
12
29
|
/** Viewer-side : drop a peer's stream (peer left / connection failed). The
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peer-stream-registry.d.ts","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;AAEtE,MAAM,WAAW,kBAAkB;IACjC;qEACiE;IACjE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAC/C;;uBAEmB;IACnB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAC;IACvE,wEAAwE;IACxE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD;;gDAE4C;IAC5C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;iBACa;IACb,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,wBAAwB,IAAI,kBAAkB,
|
|
1
|
+
{"version":3,"file":"peer-stream-registry.d.ts","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;AAEtE;;;;yBAIyB;AACzB,MAAM,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC;AAExC,MAAM,WAAW,kBAAkB;IACjC;qEACiE;IACjE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAC/C;;;iFAG6E;IAC7E,aAAa,IAAI,MAAM,EAAE,CAAC;IAC1B;;uBAEmB;IACnB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAC;IACvE;;;;0BAIsB;IACtB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IACtD,wEAAwE;IACxE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD;;gDAE4C;IAC5C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;iBACa;IACb,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,wBAAwB,IAAI,kBAAkB,CAoE7D"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
export function createPeerStreamRegistry() {
|
|
24
24
|
const streams = new Map();
|
|
25
25
|
const listeners = new Map();
|
|
26
|
+
const rosterListeners = new Set();
|
|
26
27
|
function notify(peerLabel) {
|
|
27
28
|
const set = listeners.get(peerLabel);
|
|
28
29
|
if (set === undefined)
|
|
@@ -31,10 +32,25 @@ export function createPeerStreamRegistry() {
|
|
|
31
32
|
for (const listener of set)
|
|
32
33
|
listener(value);
|
|
33
34
|
}
|
|
35
|
+
function notifyRoster() {
|
|
36
|
+
for (const listener of [...rosterListeners])
|
|
37
|
+
listener();
|
|
38
|
+
}
|
|
34
39
|
return {
|
|
35
40
|
resolve(peerLabel) {
|
|
36
41
|
return streams.get(peerLabel) ?? null;
|
|
37
42
|
},
|
|
43
|
+
orderedLabels() {
|
|
44
|
+
// Map preserves insertion order = arrival order ; every key holds a stream
|
|
45
|
+
// (a dropped peer is `delete`d in remove()).
|
|
46
|
+
return [...streams.keys()];
|
|
47
|
+
},
|
|
48
|
+
subscribeRoster(listener) {
|
|
49
|
+
rosterListeners.add(listener);
|
|
50
|
+
return () => {
|
|
51
|
+
rosterListeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
},
|
|
38
54
|
subscribe(peerLabel, listener) {
|
|
39
55
|
let set = listeners.get(peerLabel);
|
|
40
56
|
if (set === undefined) {
|
|
@@ -57,20 +73,26 @@ export function createPeerStreamRegistry() {
|
|
|
57
73
|
set(peerLabel, stream) {
|
|
58
74
|
if (streams.get(peerLabel) === stream)
|
|
59
75
|
return; // idempotent re-emit guard
|
|
76
|
+
const isNew = !streams.has(peerLabel); // a brand-new arrival shifts the roster
|
|
60
77
|
streams.set(peerLabel, stream);
|
|
61
78
|
notify(peerLabel);
|
|
79
|
+
if (isNew)
|
|
80
|
+
notifyRoster();
|
|
62
81
|
},
|
|
63
82
|
remove(peerLabel) {
|
|
64
83
|
if (!streams.has(peerLabel))
|
|
65
84
|
return;
|
|
66
85
|
streams.delete(peerLabel);
|
|
67
86
|
notify(peerLabel);
|
|
87
|
+
notifyRoster(); // a departure shifts every later position
|
|
68
88
|
},
|
|
69
89
|
clear() {
|
|
70
90
|
const labels = [...streams.keys()];
|
|
71
91
|
streams.clear();
|
|
72
92
|
for (const label of labels)
|
|
73
93
|
notify(label);
|
|
94
|
+
if (labels.length > 0)
|
|
95
|
+
notifyRoster();
|
|
74
96
|
},
|
|
75
97
|
};
|
|
76
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peer-stream-registry.js","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,sCAAsC;AACtC,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,8EAA8E;AAC9E,uEAAuE;AACvE,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,wEAAwE;AACxE,oEAAoE;AACpE,2EAA2E;AAC3E,2EAA2E;AAC3E,+EAA+E;AAC/E,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,4DAA4D;
|
|
1
|
+
{"version":3,"file":"peer-stream-registry.js","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumencast/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.12.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.
|
|
54
|
-
"@lumencast/server": "0.
|
|
53
|
+
"@lumencast/dev-server": "0.12.0",
|
|
54
|
+
"@lumencast/server": "0.12.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
|
|
@@ -64,6 +76,7 @@ export {
|
|
|
64
76
|
type RemoteTrackEvent,
|
|
65
77
|
type PeerStreamRegistry,
|
|
66
78
|
type PeerStreamListener,
|
|
79
|
+
type RosterListener,
|
|
67
80
|
} from "./webrtc/index.js";
|
|
68
81
|
|
|
69
82
|
// Bundle types are useful for hosts that want to typecheck pre-compiled scenes.
|
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,20 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
45
46
|
|
|
46
47
|
let active = true;
|
|
47
48
|
|
|
49
|
+
// Render-bundle versions already warmed (or warming) by the preload path.
|
|
50
|
+
// Keeps both roster sources (the `scene_roster` frame and the `preloadRoster`
|
|
51
|
+
// mount option) idempotent — a version is fetched by the warmer at most once.
|
|
52
|
+
const warmedVersions = new Set<string>();
|
|
53
|
+
|
|
54
|
+
// ADR Blue 009 §3.2–3.3 — surface the reserved `__cam.*` LSDP leaves (the
|
|
55
|
+
// slot→peer assignments + the receive-only viewer creds) to the host so its
|
|
56
|
+
// WebRTC viewer (Solar) can drive room joins + `x-zab.meet-peer` slot re-keying.
|
|
57
|
+
// The runtime never joins, holds creds, or re-keys ; it only forwards. Created
|
|
58
|
+
// only when the host opts in — zero cost on the preview/headless paths.
|
|
59
|
+
const reservedLeaves = options.onReservedLeaves
|
|
60
|
+
? createReservedLeafObserver(options.onReservedLeaves)
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
48
63
|
// ADR 001 §3.4 (issue #34) — anti-silent-drop diagnostics are events
|
|
49
64
|
// surfaced to the host, never console logs in `broadcast` mode.
|
|
50
65
|
const removeDiagnosticsHandler = options.onDiagnostic
|
|
@@ -59,6 +74,7 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
59
74
|
onStatus: setStatus,
|
|
60
75
|
onSnapshot: (frame) => {
|
|
61
76
|
if (!active) return;
|
|
77
|
+
reservedLeaves?.onSnapshot(frame.state);
|
|
62
78
|
void onSnapshot(
|
|
63
79
|
bundleFetcher,
|
|
64
80
|
bundleSignal,
|
|
@@ -78,6 +94,7 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
78
94
|
if (!active) return;
|
|
79
95
|
const start = performance.now();
|
|
80
96
|
applyDelta(store, frame);
|
|
97
|
+
reservedLeaves?.onDelta(frame.patches);
|
|
81
98
|
options.onMetric?.({
|
|
82
99
|
name: "delta_applied",
|
|
83
100
|
duration_ms: performance.now() - start,
|
|
@@ -95,6 +112,13 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
95
112
|
to: frame.scene_version,
|
|
96
113
|
});
|
|
97
114
|
},
|
|
115
|
+
onSceneRoster: (frame) => {
|
|
116
|
+
if (!active) return;
|
|
117
|
+
// The server advertised the show roster (LSDP/1.1 `scene_roster`).
|
|
118
|
+
// Warm every scene's render bundle in the background so the first switch
|
|
119
|
+
// to each is a cache hit, not a blocking fetch.
|
|
120
|
+
warmRoster(frame.entries, "frame");
|
|
121
|
+
},
|
|
98
122
|
onServerError: (frame) => {
|
|
99
123
|
reportError({
|
|
100
124
|
code: frame.code,
|
|
@@ -109,6 +133,12 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
109
133
|
|
|
110
134
|
ws.start();
|
|
111
135
|
|
|
136
|
+
// Public preload surface (#87b) — warm a host-supplied roster right after
|
|
137
|
+
// mount, before any switch. Same warmer + cache as the `scene_roster` frame.
|
|
138
|
+
if (options.preloadRoster !== undefined && options.preloadRoster.length > 0) {
|
|
139
|
+
warmRoster(options.preloadRoster, "option");
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
const root: Root = createRoot(options.target);
|
|
113
143
|
root.render(
|
|
114
144
|
createElement(LumencastApp, {
|
|
@@ -153,6 +183,42 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
153
183
|
|
|
154
184
|
// --- helpers ----------------------------------------------------------
|
|
155
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Warm the render bundles for a set of roster entries in the background.
|
|
188
|
+
* Best-effort and non-blocking: each `get()` populates the fetcher's cache
|
|
189
|
+
* keyed by `scene_version`, so the eventual `onSnapshot` fetch for that scene
|
|
190
|
+
* is an instant cache hit. Idempotent via `warmedVersions`; the already-active
|
|
191
|
+
* scene is skipped (its bundle is already loaded or in flight). A failed warm
|
|
192
|
+
* is swallowed and its version released so a later roster can retry — the
|
|
193
|
+
* scene still fetches on demand at switch time.
|
|
194
|
+
*/
|
|
195
|
+
function warmRoster(
|
|
196
|
+
entries: readonly { scene_id: string; scene_version: string }[],
|
|
197
|
+
source: "frame" | "option",
|
|
198
|
+
): void {
|
|
199
|
+
const activeVersion = bundleSignal.value?.scene_version;
|
|
200
|
+
for (const { scene_id, scene_version } of entries) {
|
|
201
|
+
if (scene_version === activeVersion) continue;
|
|
202
|
+
if (warmedVersions.has(scene_version)) continue;
|
|
203
|
+
warmedVersions.add(scene_version);
|
|
204
|
+
void bundleFetcher
|
|
205
|
+
.get(scene_id, scene_version)
|
|
206
|
+
.then(() => {
|
|
207
|
+
if (!active) return;
|
|
208
|
+
options.onMetric?.({
|
|
209
|
+
name: "roster_preloaded",
|
|
210
|
+
scene_id,
|
|
211
|
+
scene_version,
|
|
212
|
+
source,
|
|
213
|
+
});
|
|
214
|
+
})
|
|
215
|
+
.catch(() => {
|
|
216
|
+
// Release so a subsequent roster advertisement can retry the warm.
|
|
217
|
+
warmedVersions.delete(scene_version);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
156
222
|
async function onSnapshot(
|
|
157
223
|
fetcher: BundleFetcher,
|
|
158
224
|
bSignal: typeof bundleSignal,
|
|
@@ -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/transport/ws.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type ErrorFrame,
|
|
26
26
|
type Patch,
|
|
27
27
|
type SceneChangedFrame,
|
|
28
|
+
type SceneRosterFrame,
|
|
28
29
|
type SnapshotFrame,
|
|
29
30
|
} from "@lumencast/protocol";
|
|
30
31
|
import type { LumencastToken } from "../types.js";
|
|
@@ -57,6 +58,9 @@ export interface WsClientOptions {
|
|
|
57
58
|
onSnapshot?: (frame: SnapshotFrame) => void;
|
|
58
59
|
onDelta?: (frame: DeltaFrame) => void;
|
|
59
60
|
onSceneChanged?: (frame: SceneChangedFrame) => void;
|
|
61
|
+
/** Out-of-band roster advertisement (LSDP/1.1 `scene_roster`, additive).
|
|
62
|
+
* Carries no sequence — dispatched without touching the sequence tracker. */
|
|
63
|
+
onSceneRoster?: (frame: SceneRosterFrame) => void;
|
|
60
64
|
onServerError?: (frame: ErrorFrame) => void;
|
|
61
65
|
/** Wire-level / codec / unrecoverable errors. */
|
|
62
66
|
onTransportError?: (err: TransportError) => void;
|
|
@@ -298,6 +302,13 @@ export class WsClient {
|
|
|
298
302
|
if (!frame.recoverable) this.close();
|
|
299
303
|
return;
|
|
300
304
|
}
|
|
305
|
+
case "scene_roster": {
|
|
306
|
+
// Out-of-band roster metadata: NOT part of the snapshot/delta stream.
|
|
307
|
+
// It carries no seq and MUST NOT touch the sequence tracker — forward
|
|
308
|
+
// it verbatim so the host can warm its render-bundle cache.
|
|
309
|
+
this.opts.onSceneRoster?.(frame);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
301
312
|
case "pong":
|
|
302
313
|
return;
|
|
303
314
|
}
|