@lumencast/server 0.1.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 (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +69 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/adapters/http-poll.d.ts +16 -0
  5. package/dist/adapters/http-poll.d.ts.map +1 -0
  6. package/dist/adapters/http-poll.js +46 -0
  7. package/dist/adapters/http-poll.js.map +1 -0
  8. package/dist/auth.d.ts +33 -0
  9. package/dist/auth.d.ts.map +1 -0
  10. package/dist/auth.js +65 -0
  11. package/dist/auth.js.map +1 -0
  12. package/dist/cli.d.ts +3 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +154 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +8 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/replay-buffer.d.ts +32 -0
  21. package/dist/replay-buffer.d.ts.map +1 -0
  22. package/dist/replay-buffer.js +60 -0
  23. package/dist/replay-buffer.js.map +1 -0
  24. package/dist/scene.d.ts +67 -0
  25. package/dist/scene.d.ts.map +1 -0
  26. package/dist/scene.js +109 -0
  27. package/dist/scene.js.map +1 -0
  28. package/dist/server.d.ts +38 -0
  29. package/dist/server.d.ts.map +1 -0
  30. package/dist/server.js +279 -0
  31. package/dist/server.js.map +1 -0
  32. package/dist/store.d.ts +17 -0
  33. package/dist/store.d.ts.map +1 -0
  34. package/dist/store.js +34 -0
  35. package/dist/store.js.map +1 -0
  36. package/dist/test-control.d.ts +22 -0
  37. package/dist/test-control.d.ts.map +1 -0
  38. package/dist/test-control.js +225 -0
  39. package/dist/test-control.js.map +1 -0
  40. package/package.json +50 -0
  41. package/src/adapters/http-poll.ts +59 -0
  42. package/src/auth.ts +81 -0
  43. package/src/cli.ts +166 -0
  44. package/src/index.ts +20 -0
  45. package/src/replay-buffer.ts +72 -0
  46. package/src/scene.ts +168 -0
  47. package/src/server.ts +386 -0
  48. package/src/store.ts +40 -0
  49. package/src/test-control.ts +279 -0
package/dist/cli.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // lumencast-js server CLI.
3
+ //
4
+ // Subcommands:
5
+ // serve-scenario --ws-port N --test-control-port M [--host H]
6
+ // Boots the LSDP/1 server with the interop test control plane on a
7
+ // separate port. Prints exactly one JSON discovery line on stdout :
8
+ // {"control_url":"http://...","ws_url":"ws://.../lsdp/v1"}
9
+ // before any other output, then stays silent on stdout (logs go to stderr).
10
+ import { parseArgs } from "node:util";
11
+ import { createScene } from "./scene.js";
12
+ import { startServer } from "./server.js";
13
+ import { StaticTokens } from "./auth.js";
14
+ import { startTestControl } from "./test-control.js";
15
+ async function main(argv) {
16
+ const sub = argv[0];
17
+ if (!sub || sub === "--help" || sub === "-h") {
18
+ printUsage();
19
+ return sub ? 0 : 2;
20
+ }
21
+ if (sub === "serve-scenario")
22
+ return cmdServeScenario(argv.slice(1));
23
+ process.stderr.write(`lumencast-js: unknown subcommand "${sub}"\n`);
24
+ printUsage();
25
+ return 2;
26
+ }
27
+ function printUsage() {
28
+ process.stderr.write([
29
+ "usage: lumencast-js <subcommand> [flags]",
30
+ "",
31
+ "subcommands:",
32
+ " serve-scenario --ws-port N --test-control-port M [--host H]",
33
+ " Boot LSDP/1 server + interop test control plane (separate ports).",
34
+ "",
35
+ ].join("\n"));
36
+ }
37
+ async function cmdServeScenario(args) {
38
+ let parsed;
39
+ try {
40
+ parsed = parseArgs({
41
+ args,
42
+ options: {
43
+ "ws-port": { type: "string" },
44
+ "test-control-port": { type: "string" },
45
+ host: { type: "string", default: "127.0.0.1" },
46
+ help: { type: "boolean", short: "h" },
47
+ },
48
+ strict: true,
49
+ allowPositionals: false,
50
+ });
51
+ }
52
+ catch (err) {
53
+ process.stderr.write(`serve-scenario: ${err.message}\n`);
54
+ return 2;
55
+ }
56
+ if (parsed.values["help"]) {
57
+ process.stderr.write("serve-scenario --ws-port N --test-control-port M [--host H]\n");
58
+ return 0;
59
+ }
60
+ const wsPort = parseIntFlag(parsed.values["ws-port"], "--ws-port");
61
+ const controlPort = parseIntFlag(parsed.values["test-control-port"], "--test-control-port");
62
+ if (wsPort === null || controlPort === null)
63
+ return 2;
64
+ const host = parsed.values["host"] ?? "127.0.0.1";
65
+ // Bootstrap a synthetic scene + empty bundle provider. Both are immediately
66
+ // overridden by the first POST /test/setup.
67
+ const initialScene = createScene({
68
+ sceneId: "__interop_initial__",
69
+ sceneVersion: "sha256:" + "0".repeat(64),
70
+ initialState: {},
71
+ });
72
+ const auth = new StaticTokens();
73
+ let server;
74
+ let control;
75
+ try {
76
+ server = await startServer({
77
+ port: wsPort,
78
+ host,
79
+ scene: initialScene,
80
+ bundleProvider: () => undefined,
81
+ authenticate: auth.authenticate,
82
+ });
83
+ }
84
+ catch (err) {
85
+ process.stderr.write(`serve-scenario: bind ws: ${err.message}\n`);
86
+ return 1;
87
+ }
88
+ try {
89
+ control = await startTestControl({
90
+ port: controlPort,
91
+ host,
92
+ server,
93
+ auth,
94
+ });
95
+ }
96
+ catch (err) {
97
+ await server.close();
98
+ process.stderr.write(`serve-scenario: bind control: ${err.message}\n`);
99
+ return 1;
100
+ }
101
+ // Discovery line — written before any other stdout output. The matrix
102
+ // driver greps for "control_url" to know we're ready.
103
+ process.stdout.write(JSON.stringify({
104
+ control_url: control.url,
105
+ ws_url: server.wsUrl,
106
+ }) + "\n");
107
+ let shutdown = async () => {
108
+ shutdown = null;
109
+ process.stderr.write("[lumencast-js] shutting down\n");
110
+ try {
111
+ await control.close();
112
+ }
113
+ catch {
114
+ // ignore
115
+ }
116
+ try {
117
+ await server.close();
118
+ }
119
+ catch {
120
+ // ignore
121
+ }
122
+ };
123
+ process.on("SIGINT", () => void shutdown?.());
124
+ process.on("SIGTERM", () => void shutdown?.());
125
+ await new Promise((resolve) => {
126
+ const tick = () => {
127
+ if (shutdown === null)
128
+ resolve();
129
+ else
130
+ setTimeout(tick, 100);
131
+ };
132
+ tick();
133
+ });
134
+ return 0;
135
+ }
136
+ function parseIntFlag(raw, name) {
137
+ if (typeof raw !== "string") {
138
+ process.stderr.write(`${name} required\n`);
139
+ return null;
140
+ }
141
+ const n = Number.parseInt(raw, 10);
142
+ if (!Number.isFinite(n) || n < 0 || n > 65535) {
143
+ process.stderr.write(`${name}: invalid port\n`);
144
+ return null;
145
+ }
146
+ return n;
147
+ }
148
+ main(process.argv.slice(2)).then((code) => {
149
+ process.exitCode = code;
150
+ }, (err) => {
151
+ process.stderr.write(`lumencast-js: ${err.stack ?? String(err)}\n`);
152
+ process.exitCode = 1;
153
+ });
154
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { startServer, type ServerConfig, type ServerHandle } from "./server.js";
2
+ export { createScene, type Scene, type SceneInit } from "./scene.js";
3
+ export { LeafStore, type LeafStoreListener } from "./store.js";
4
+ export { defaultAuthenticate, canWritePath, StaticTokens, type Authenticate, type AuthDecision, type Role, } from "./auth.js";
5
+ export { startHttpPoll, type HttpPollOptions } from "./adapters/http-poll.js";
6
+ export { startTestControl, installTokens, type TestControlOptions, type TestControlHandle, } from "./test-control.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,IAAI,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Public surface of @lumencast/server.
2
+ export { startServer } from "./server.js";
3
+ export { createScene } from "./scene.js";
4
+ export { LeafStore } from "./store.js";
5
+ export { defaultAuthenticate, canWritePath, StaticTokens, } from "./auth.js";
6
+ export { startHttpPoll } from "./adapters/http-poll.js";
7
+ export { startTestControl, installTokens, } from "./test-control.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AAEvC,OAAO,EAAE,WAAW,EAAwC,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,WAAW,EAA8B,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,SAAS,EAA0B,MAAM,YAAY,CAAC;AAC/D,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,YAAY,GAIb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAwB,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EACL,gBAAgB,EAChB,aAAa,GAGd,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { Cause, Patch } from "@lumencast/protocol";
2
+ export interface ReplayRecord {
3
+ readonly seq: number;
4
+ readonly patches: Patch[];
5
+ readonly cause?: Cause;
6
+ }
7
+ /** Default capacity (LSDP/1.1 §18.1 SHOULD ≥ 256). */
8
+ export declare const DEFAULT_REPLAY_BUFFER_SIZE = 256;
9
+ export declare class ReplayBuffer {
10
+ private readonly cap;
11
+ private readonly records;
12
+ private head;
13
+ private size;
14
+ constructor(capacity?: number);
15
+ /** Record one emission. Caller is responsible for monotonic seq. */
16
+ push(record: ReplayRecord): void;
17
+ /**
18
+ * Return every record with seq > sinceSeq, in monotonic order.
19
+ *
20
+ * The boolean is `false` when sinceSeq is older than the buffer's
21
+ * earliest retained entry — the caller MUST then fall back to a
22
+ * fresh snapshot per LSDP/1.1 §18.1.
23
+ */
24
+ since(sinceSeq: number): {
25
+ records: ReplayRecord[];
26
+ covered: boolean;
27
+ };
28
+ /** Clear the buffer. Used on scene_changed. */
29
+ reset(): void;
30
+ get length(): number;
31
+ }
32
+ //# sourceMappingURL=replay-buffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-buffer.d.ts","sourceRoot":"","sources":["../src/replay-buffer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED,sDAAsD;AACtD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,IAAI,CAAK;gBAEL,QAAQ,SAA6B;IAKjD,oEAAoE;IACpE,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAMhC;;;;;;OAMG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,YAAY,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE;IAoBtE,+CAA+C;IAC/C,KAAK,IAAI,IAAI;IAMb,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF"}
@@ -0,0 +1,60 @@
1
+ // Per-scene replay buffer (LSDP/1.1 §18.1).
2
+ // Bounded ring of recent (seq, patches, cause) emissions so a 1.1
3
+ // client reconnecting with `since_sequence` can resume without a fresh
4
+ // snapshot.
5
+ /** Default capacity (LSDP/1.1 §18.1 SHOULD ≥ 256). */
6
+ export const DEFAULT_REPLAY_BUFFER_SIZE = 256;
7
+ export class ReplayBuffer {
8
+ cap;
9
+ records;
10
+ head = 0;
11
+ size = 0;
12
+ constructor(capacity = DEFAULT_REPLAY_BUFFER_SIZE) {
13
+ this.cap = capacity > 0 ? capacity : DEFAULT_REPLAY_BUFFER_SIZE;
14
+ this.records = new Array(this.cap);
15
+ }
16
+ /** Record one emission. Caller is responsible for monotonic seq. */
17
+ push(record) {
18
+ this.records[this.head] = record;
19
+ this.head = (this.head + 1) % this.cap;
20
+ if (this.size < this.cap)
21
+ this.size++;
22
+ }
23
+ /**
24
+ * Return every record with seq > sinceSeq, in monotonic order.
25
+ *
26
+ * The boolean is `false` when sinceSeq is older than the buffer's
27
+ * earliest retained entry — the caller MUST then fall back to a
28
+ * fresh snapshot per LSDP/1.1 §18.1.
29
+ */
30
+ since(sinceSeq) {
31
+ if (this.size === 0) {
32
+ // Empty buffer — caller decides whether sinceSeq matches the
33
+ // current scene seq (caught up) or warrants a snapshot.
34
+ return { records: [], covered: true };
35
+ }
36
+ const tail = (this.head - this.size + this.cap) % this.cap;
37
+ const earliest = this.records[tail].seq;
38
+ if (sinceSeq + 1 < earliest) {
39
+ return { records: [], covered: false };
40
+ }
41
+ const out = [];
42
+ for (let i = 0; i < this.size; i++) {
43
+ const idx = (tail + i) % this.cap;
44
+ const r = this.records[idx];
45
+ if (r.seq > sinceSeq)
46
+ out.push(r);
47
+ }
48
+ return { records: out, covered: true };
49
+ }
50
+ /** Clear the buffer. Used on scene_changed. */
51
+ reset() {
52
+ this.head = 0;
53
+ this.size = 0;
54
+ this.records.fill(undefined);
55
+ }
56
+ get length() {
57
+ return this.size;
58
+ }
59
+ }
60
+ //# sourceMappingURL=replay-buffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-buffer.js","sourceRoot":"","sources":["../src/replay-buffer.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,kEAAkE;AAClE,uEAAuE;AACvE,YAAY;AAUZ,sDAAsD;AACtD,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAE9C,MAAM,OAAO,YAAY;IACN,GAAG,CAAS;IACZ,OAAO,CAA+B;IAC/C,IAAI,GAAG,CAAC,CAAC;IACT,IAAI,GAAG,CAAC,CAAC;IAEjB,YAAY,QAAQ,GAAG,0BAA0B;QAC/C,IAAI,CAAC,GAAG,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,0BAA0B,CAAC;QAChE,IAAI,CAAC,OAAO,GAAG,IAAI,KAAK,CAA2B,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/D,CAAC;IAED,oEAAoE;IACpE,IAAI,CAAC,MAAoB;QACvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG;YAAE,IAAI,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,QAAgB;QACpB,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACpB,6DAA6D;YAC7D,wDAAwD;YACxD,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACxC,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;QAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAE,CAAC,GAAG,CAAC;QACzC,IAAI,QAAQ,GAAG,CAAC,GAAG,QAAQ,EAAE,CAAC;YAC5B,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,CAAC;QACD,MAAM,GAAG,GAAmB,EAAE,CAAC;QAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;YAClC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAE,CAAC;YAC7B,IAAI,CAAC,CAAC,GAAG,GAAG,QAAQ;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACzC,CAAC;IAED,+CAA+C;IAC/C,KAAK;QACH,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,67 @@
1
+ import type { Cause, LeafPath, LeafValue, Patch, SceneId, SceneVersion } from "@lumencast/protocol";
2
+ import { LeafStore } from "./store.js";
3
+ import { type ReplayRecord } from "./replay-buffer.js";
4
+ export interface OperatorInputDecl {
5
+ path: LeafPath;
6
+ /** "string" | "number" | "boolean" | "enum" | ... — see LSML 1.0 §8. */
7
+ type: string;
8
+ /** Type-specific constraints — currently maxLength, minLength, min, max, pattern. */
9
+ constraints?: {
10
+ maxLength?: number;
11
+ minLength?: number;
12
+ min?: number;
13
+ max?: number;
14
+ pattern?: string;
15
+ };
16
+ values?: unknown[];
17
+ writable_by?: string[];
18
+ [extra: string]: unknown;
19
+ }
20
+ export interface SceneInit {
21
+ sceneId: SceneId;
22
+ sceneVersion: SceneVersion;
23
+ initialState?: Record<LeafPath, LeafValue>;
24
+ /** Declared operator inputs. When set, `validateInput` enforces the schema. */
25
+ operatorInputs?: OperatorInputDecl[];
26
+ }
27
+ export type ValidationError = {
28
+ code: "UNKNOWN_PATH";
29
+ path: LeafPath;
30
+ message: string;
31
+ } | {
32
+ code: "INVALID_VALUE";
33
+ path: LeafPath;
34
+ message: string;
35
+ };
36
+ export interface Scene {
37
+ readonly sceneId: SceneId;
38
+ readonly sceneVersion: SceneVersion;
39
+ readonly store: LeafStore;
40
+ readonly operatorInputs: OperatorInputDecl[];
41
+ /** Update one or more leaves. Atomic per call. Optional `cause` propagates
42
+ * to subscribers as the resulting Delta.cause (LSDP/1.1 §3.2.3). */
43
+ update(patches: Patch[] | Record<LeafPath, LeafValue>, cause?: Cause): void;
44
+ /** Subscribe to all patches emitted by this scene. The listener receives
45
+ * the per-scene `seq` (LSDP/1.1 §18.1.1) — the same value across all
46
+ * concurrent listeners on a given delta. */
47
+ onPatches(listener: (seq: number, patches: Patch[], cause?: Cause) => void): () => void;
48
+ /** Current per-scene seq counter (LSDP/1.1 §18.1.1). 1 on a fresh
49
+ * scene ; increments on every emit. Late-joining subscribers ship
50
+ * snapshot at this value. */
51
+ currentSeq(): number;
52
+ /** Replay records strictly after `sinceSeq` for §18.1 resume.
53
+ * Returns `covered: false` when sinceSeq is older than the buffer's
54
+ * earliest entry — the caller MUST fall back to a fresh snapshot. */
55
+ replaySince(sinceSeq: number): {
56
+ records: ReplayRecord[];
57
+ covered: boolean;
58
+ };
59
+ /**
60
+ * Validate input patches against the declared operator_inputs schema.
61
+ * Returns the first error or null. The reserved `__test.*` namespace is
62
+ * always permitted (test sessions own that namespace per LSDP/1 §10).
63
+ */
64
+ validateInput(patches: Patch[]): ValidationError | null;
65
+ }
66
+ export declare function createScene(init: SceneInit): Scene;
67
+ //# sourceMappingURL=scene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAA4C,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEjG,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,QAAQ,CAAC;IACf,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,WAAW,CAAC,EAAE;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC3C,+EAA+E;IAC/E,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACtC;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/D,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,iBAAiB,EAAE,CAAC;IAC7C;wEACoE;IACpE,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IAC5E;;gDAE4C;IAC5C,SAAS,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxF;;iCAE6B;IAC7B,UAAU,IAAI,MAAM,CAAC;IACrB;;yEAEqE;IACrE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,YAAY,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7E;;;;OAIG;IACH,aAAa,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,eAAe,GAAG,IAAI,CAAC;CACzD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAiElD"}
package/dist/scene.js ADDED
@@ -0,0 +1,109 @@
1
+ // Scene — the server's authoritative view of one Lumencast scene.
2
+ // Wraps a LeafStore + identity (sceneId, sceneVersion) and an optional
3
+ // operator_inputs schema used by the server to validate `input` frames.
4
+ import { LeafStore } from "./store.js";
5
+ import { DEFAULT_REPLAY_BUFFER_SIZE, ReplayBuffer } from "./replay-buffer.js";
6
+ export function createScene(init) {
7
+ const store = new LeafStore(init.initialState ?? {});
8
+ const operatorInputs = init.operatorInputs ?? [];
9
+ const inputsByPath = new Map();
10
+ for (const oi of operatorInputs)
11
+ inputsByPath.set(oi.path, oi);
12
+ // LSDP/1.1 §18.1.1 — per-scene monotonic seq. Pre-seeded to 1 so the
13
+ // very first subscriber's snapshot ships at seq=1 (matches every
14
+ // existing 1.0 conformance scenario). Subsequent emits increment.
15
+ let seq = 1;
16
+ const replay = new ReplayBuffer(DEFAULT_REPLAY_BUFFER_SIZE);
17
+ const sceneListeners = new Set();
18
+ // Bridge LeafStore.onPatches → scene listeners. Single store listener
19
+ // increments `seq`, records to the buffer, fans out to scene listeners.
20
+ store.onPatches((patches, cause) => {
21
+ seq += 1;
22
+ replay.push({ seq, patches: patches.slice(), cause });
23
+ for (const l of sceneListeners)
24
+ l(seq, patches, cause);
25
+ });
26
+ function update(input, cause) {
27
+ const patches = Array.isArray(input)
28
+ ? input
29
+ : Object.entries(input).map(([path, value]) => ({ path, value }));
30
+ store.apply(patches, cause);
31
+ }
32
+ function validateInput(patches) {
33
+ // No declared schema → server accepts any path (legacy / dev-mode behavior).
34
+ if (operatorInputs.length === 0)
35
+ return null;
36
+ for (const p of patches) {
37
+ if (p.path.startsWith("__test."))
38
+ continue; // test namespace bypass
39
+ const decl = inputsByPath.get(p.path);
40
+ if (!decl) {
41
+ return {
42
+ code: "UNKNOWN_PATH",
43
+ path: p.path,
44
+ message: `path ${p.path} not declared in operator_inputs`,
45
+ };
46
+ }
47
+ const err = checkConstraint(decl, p.value);
48
+ if (err)
49
+ return { code: "INVALID_VALUE", path: p.path, message: err };
50
+ }
51
+ return null;
52
+ }
53
+ return {
54
+ sceneId: init.sceneId,
55
+ sceneVersion: init.sceneVersion,
56
+ store,
57
+ operatorInputs,
58
+ update,
59
+ onPatches: (listener) => {
60
+ sceneListeners.add(listener);
61
+ return () => {
62
+ sceneListeners.delete(listener);
63
+ };
64
+ },
65
+ currentSeq: () => seq,
66
+ replaySince: (sinceSeq) => replay.since(sinceSeq),
67
+ validateInput,
68
+ };
69
+ }
70
+ function checkConstraint(decl, value) {
71
+ switch (decl.type) {
72
+ case "string":
73
+ case "text":
74
+ if (typeof value !== "string")
75
+ return `expected string, got ${typeof value}`;
76
+ if (decl.constraints?.maxLength !== undefined && value.length > decl.constraints.maxLength) {
77
+ return `exceeds maxLength ${decl.constraints.maxLength}`;
78
+ }
79
+ if (decl.constraints?.minLength !== undefined && value.length < decl.constraints.minLength) {
80
+ return `below minLength ${decl.constraints.minLength}`;
81
+ }
82
+ if (decl.constraints?.pattern && !new RegExp(decl.constraints.pattern).test(value)) {
83
+ return `does not match pattern`;
84
+ }
85
+ return null;
86
+ case "number":
87
+ if (typeof value !== "number")
88
+ return `expected number, got ${typeof value}`;
89
+ if (decl.constraints?.min !== undefined && value < decl.constraints.min) {
90
+ return `below min ${decl.constraints.min}`;
91
+ }
92
+ if (decl.constraints?.max !== undefined && value > decl.constraints.max) {
93
+ return `above max ${decl.constraints.max}`;
94
+ }
95
+ return null;
96
+ case "boolean":
97
+ if (typeof value !== "boolean")
98
+ return `expected boolean, got ${typeof value}`;
99
+ return null;
100
+ case "enum":
101
+ if (decl.values && !decl.values.includes(value)) {
102
+ return `not in enum`;
103
+ }
104
+ return null;
105
+ default:
106
+ return null;
107
+ }
108
+ }
109
+ //# sourceMappingURL=scene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.js","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { type SceneVersion } from "@lumencast/protocol";
2
+ import { type Authenticate } from "./auth.js";
3
+ import type { Scene } from "./scene.js";
4
+ export interface ServerConfig {
5
+ /** TCP port to listen on. 0 = random. */
6
+ port?: number;
7
+ /** Hostname to bind. Defaults to 127.0.0.1. */
8
+ host?: string;
9
+ /** The initial scene this server serves. May be swapped via setActiveScene(). */
10
+ scene: Scene;
11
+ /** Resolves a SceneVersion to its LSML bundle JSON. May be swapped. */
12
+ bundleProvider: (version: SceneVersion) => Promise<unknown> | unknown;
13
+ /** Authenticate every subscribe; default accepts everything as `viewer`. */
14
+ authenticate?: Authenticate;
15
+ /** Optional ws path. Defaults to `/lsdp/v1`. */
16
+ wsPath?: string;
17
+ /** Optional bundle path prefix. Defaults to `/lsdp/v1/scenes`. */
18
+ bundlePathPrefix?: string;
19
+ }
20
+ export interface ServerHandle {
21
+ readonly wsUrl: string;
22
+ readonly httpUrl: string;
23
+ /**
24
+ * Replace the active scene. Detaches every subscriber (they receive a 1012
25
+ * "service restart" close so well-behaved clients reconnect cleanly).
26
+ * Test/interop only — production code should never call this.
27
+ */
28
+ setActiveScene(scene: Scene): void;
29
+ /** Replace the bundle provider. Test/interop only. */
30
+ setBundleProvider(fn: (version: SceneVersion) => Promise<unknown> | unknown): void;
31
+ /** Read the current active scene. */
32
+ activeScene(): Scene;
33
+ /** Drop all subscribers without replacing the scene. Test/interop only. */
34
+ reset(): void;
35
+ close(): Promise<void>;
36
+ }
37
+ export declare function startServer(config: ServerConfig): Promise<ServerHandle>;
38
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,OAAO,EAcL,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAqC,KAAK,YAAY,EAAqB,MAAM,WAAW,CAAC;AACpG,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,EAAE,KAAK,CAAC;IACb,uEAAuE;IACvE,cAAc,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACtE,4EAA4E;IAC5E,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACnC,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;IACnF,qCAAqC;IACrC,WAAW,IAAI,KAAK,CAAC;IACrB,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAUD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA8S7E"}