@norskvideo/moq-json 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 (50) hide show
  1. package/README.md +60 -0
  2. package/diff.d.ts +20 -0
  3. package/diff.d.ts.map +1 -0
  4. package/diff.js +83 -0
  5. package/diff.js.map +1 -0
  6. package/diff.test.d.ts +2 -0
  7. package/diff.test.d.ts.map +1 -0
  8. package/diff.test.js +60 -0
  9. package/diff.test.js.map +1 -0
  10. package/index.d.ts +17 -0
  11. package/index.d.ts.map +1 -0
  12. package/index.js +20 -0
  13. package/index.js.map +1 -0
  14. package/package.json +23 -0
  15. package/snapshot/compression.test.d.ts +2 -0
  16. package/snapshot/compression.test.d.ts.map +1 -0
  17. package/snapshot/compression.test.js +132 -0
  18. package/snapshot/compression.test.js.map +1 -0
  19. package/snapshot/consumer.d.ts +25 -0
  20. package/snapshot/consumer.d.ts.map +1 -0
  21. package/snapshot/consumer.js +93 -0
  22. package/snapshot/consumer.js.map +1 -0
  23. package/snapshot/index.d.ts +19 -0
  24. package/snapshot/index.d.ts.map +1 -0
  25. package/snapshot/index.js +20 -0
  26. package/snapshot/index.js.map +1 -0
  27. package/snapshot/producer.d.ts +64 -0
  28. package/snapshot/producer.d.ts.map +1 -0
  29. package/snapshot/producer.js +230 -0
  30. package/snapshot/producer.js.map +1 -0
  31. package/snapshot/producer.test.d.ts +2 -0
  32. package/snapshot/producer.test.d.ts.map +1 -0
  33. package/snapshot/producer.test.js +75 -0
  34. package/snapshot/producer.test.js.map +1 -0
  35. package/snapshot/snapshot.test.d.ts +2 -0
  36. package/snapshot/snapshot.test.d.ts.map +1 -0
  37. package/snapshot/snapshot.test.js +185 -0
  38. package/snapshot/snapshot.test.js.map +1 -0
  39. package/stream.d.ts +54 -0
  40. package/stream.d.ts.map +1 -0
  41. package/stream.js +92 -0
  42. package/stream.js.map +1 -0
  43. package/stream.test.d.ts +2 -0
  44. package/stream.test.d.ts.map +1 -0
  45. package/stream.test.js +64 -0
  46. package/stream.test.js.map +1 -0
  47. package/vectors.test.d.ts +2 -0
  48. package/vectors.test.d.ts.map +1 -0
  49. package/vectors.test.js +18 -0
  50. package/vectors.test.js.map +1 -0
@@ -0,0 +1,185 @@
1
+ /* @ts-self-types="./snapshot.test.d.ts" */
2
+ import { expect, test } from "bun:test";
3
+ import { Track } from "@norskvideo/moq-net";
4
+ import { Consumer } from "./consumer.js";
5
+ import { Producer } from "./producer.js";
6
+ // Reconstruct every value a consumer yields, in order.
7
+ async function drain(track) {
8
+ const out = [];
9
+ for await (const value of new Consumer(track))
10
+ out.push(value);
11
+ return out;
12
+ }
13
+ // Inspect the published layout via the public API: the frame count of each group, in order.
14
+ // The track must be finished first so group/frame reads terminate.
15
+ async function structure(track) {
16
+ const counts = [];
17
+ for (;;) {
18
+ const group = await track.nextGroupOrdered();
19
+ if (!group)
20
+ break;
21
+ let frames = 0;
22
+ while ((await group.readFrame()) !== undefined)
23
+ frames++;
24
+ counts.push(frames);
25
+ }
26
+ return counts;
27
+ }
28
+ test("deltas off: a snapshot group per change", async () => {
29
+ const track = new Track("test");
30
+ const producer = new Producer(track, { deltaRatio: 0 });
31
+ producer.update({ a: 1 });
32
+ producer.update({ a: 2 });
33
+ producer.finish();
34
+ // Two changes => two single-frame snapshot groups, reconstructed in order.
35
+ expect(await drain(track)).toEqual([{ a: 1 }, { a: 2 }]);
36
+ });
37
+ test("deltaRatio 0 disables deltas, like off", async () => {
38
+ const track = new Track("test");
39
+ const producer = new Producer(track, { deltaRatio: 0 });
40
+ producer.update({ a: 1 });
41
+ producer.update({ a: 2 });
42
+ producer.finish();
43
+ // `0` is treated as off, not a degenerate "enabled" value that keeps the group open: each change
44
+ // is its own single-frame snapshot group.
45
+ expect(await structure(track)).toEqual([1, 1]);
46
+ });
47
+ test("live consumer sees each update", async () => {
48
+ const track = new Track("test");
49
+ const producer = new Producer(track);
50
+ const consumer = new Consumer(track);
51
+ for (let n = 1; n <= 3; n++) {
52
+ producer.update({ a: n });
53
+ expect(await consumer.next()).toEqual({ a: n });
54
+ }
55
+ });
56
+ test("unchanged value writes nothing", async () => {
57
+ const track = new Track("test");
58
+ const producer = new Producer(track);
59
+ producer.update({ a: 1 });
60
+ producer.update({ a: 1 });
61
+ producer.finish();
62
+ expect(await structure(track)).toEqual([1]);
63
+ });
64
+ test("deltas share one group", async () => {
65
+ const track = new Track("test");
66
+ const producer = new Producer(track, { deltaRatio: 100 });
67
+ producer.update({ a: 1, b: 1 });
68
+ producer.update({ a: 1, b: 2 });
69
+ producer.update({ a: 1, b: 3 });
70
+ producer.finish();
71
+ // All updates fit in a single group as snapshot + two deltas.
72
+ expect(await structure(track)).toEqual([3]);
73
+ });
74
+ test("deltas reconstruct to the final value", async () => {
75
+ const track = new Track("test");
76
+ const producer = new Producer(track, { deltaRatio: 100 });
77
+ producer.update({ a: 1, b: 1 });
78
+ producer.update({ a: 1, b: 2 });
79
+ producer.update({ a: 5, b: 2 });
80
+ producer.finish();
81
+ expect((await drain(track)).at(-1)).toEqual({ a: 5, b: 2 });
82
+ });
83
+ // `mutate()` edits the shared document: multiple owners edit one producer, each touching its own
84
+ // keys, and each call publishes. This is how the catalog producer is extended (e.g. an scte35
85
+ // section) without a single owner having to rebuild the whole document.
86
+ test("mutate composes independent owners", async () => {
87
+ const track = new Track("test");
88
+ const producer = new Producer(track, { initial: {} });
89
+ const consumer = new Consumer(track);
90
+ producer.mutate((v) => {
91
+ v.video = "v1";
92
+ });
93
+ expect(await consumer.next()).toEqual({ video: "v1" });
94
+ // A second owner starts from the latest value and adds its own key without clobbering the first.
95
+ producer.mutate((v) => {
96
+ v.scte35 = { id: 1 };
97
+ });
98
+ expect(await consumer.next()).toEqual({ video: "v1", scte35: { id: 1 } });
99
+ });
100
+ test("mutate starts from the configured initial value", async () => {
101
+ const track = new Track("test");
102
+ const producer = new Producer(track, { initial: {} });
103
+ const consumer = new Consumer(track);
104
+ producer.mutate((v) => {
105
+ v.a = 1;
106
+ });
107
+ expect(await consumer.next()).toEqual({ a: 1 });
108
+ });
109
+ test("mutate without a prior value or initial throws", () => {
110
+ const producer = new Producer(new Track("test"));
111
+ expect(() => producer.mutate(() => { })).toThrow();
112
+ });
113
+ // Removing a section drops it from the reconstructed value, so a consumer detects the removal.
114
+ // Exercised with deltas on to cover the merge-patch null-deletion path.
115
+ test("mutate removes a section", async () => {
116
+ const track = new Track("test");
117
+ const producer = new Producer(track, { deltaRatio: 100, initial: {} });
118
+ const consumer = new Consumer(track);
119
+ producer.mutate((v) => {
120
+ v.a = 1;
121
+ v.scte35 = { id: 1 };
122
+ });
123
+ expect(await consumer.next()).toEqual({ a: 1, scte35: { id: 1 } });
124
+ producer.mutate((v) => {
125
+ delete v.scte35;
126
+ });
127
+ expect(await consumer.next()).toEqual({ a: 1 });
128
+ });
129
+ test("tight ratio rolls snapshots", async () => {
130
+ const track = new Track("test");
131
+ // A ratio of 1 budgets deltas up to one snapshot (equal 7-byte frames => 7 bytes). The gate checks
132
+ // the deltas already written, so the delta that tips the group over budget still lands (a one-frame
133
+ // overshoot): group 0 takes two deltas (14 bytes) before the fourth update rolls group 1.
134
+ const producer = new Producer(track, { deltaRatio: 1 });
135
+ producer.update({ a: 1 }); // snapshot, group 0
136
+ producer.update({ a: 2 }); // delta, group 0 (deltas = 7)
137
+ producer.update({ a: 3 }); // delta, group 0 (deltas = 14, now over budget)
138
+ producer.update({ a: 4 }); // budget already exceeded, rolls group 1
139
+ producer.finish();
140
+ expect(await structure(track)).toEqual([3, 1]);
141
+ });
142
+ test("deltas stay within ratio times snapshot", async () => {
143
+ const track = new Track("test");
144
+ // The budget covers only the deltas, not the snapshot frame, measured against the group's snapshot
145
+ // size. Single-digit values keep every frame at a constant 7 bytes (`{"n":N}`), so a ratio of 8
146
+ // budgets 56 bytes of deltas. The gate checks the deltas already written, so the group keeps filling
147
+ // until they first exceed 56 (nine deltas = 63 bytes) and the next update rolls (a one-frame
148
+ // overshoot past the 56-byte budget).
149
+ const producer = new Producer(track, { deltaRatio: 8 });
150
+ for (let n = 0; n <= 10; n++)
151
+ producer.update({ n });
152
+ producer.finish();
153
+ expect(await structure(track)).toEqual([10, 1]);
154
+ });
155
+ test("array change is a wholesale delta", async () => {
156
+ const track = new Track("test");
157
+ const producer = new Producer(track, { deltaRatio: 100 });
158
+ producer.update({ list: [1, 2] });
159
+ producer.update({ list: [1, 2, 3] });
160
+ producer.finish();
161
+ // The array is replaced wholesale in a delta, so it stays in the same group.
162
+ expect(await structure(track)).toEqual([2]);
163
+ });
164
+ test("late joiner collapses a buffered backlog to the latest value", async () => {
165
+ const track = new Track("test");
166
+ const producer = new Producer(track, { deltaRatio: 100 });
167
+ for (let n = 0; n <= 20; n++) {
168
+ producer.update({ n });
169
+ }
170
+ producer.finish();
171
+ // A whole group's worth of snapshot + deltas is buffered before the consumer reads, so it applies
172
+ // them all but yields only the latest value once, not every superseded state.
173
+ expect(await drain(track)).toEqual([{ n: 20 }]);
174
+ });
175
+ test("frame cap rolls snapshot", async () => {
176
+ const track = new Track("test");
177
+ const producer = new Producer(track, { deltaRatio: 1_000_000 });
178
+ // First update is the snapshot; deltas fill the group until the frame cap forces a roll.
179
+ for (let i = 0; i <= 256; i++) {
180
+ producer.update({ n: i });
181
+ }
182
+ producer.finish();
183
+ expect(await structure(track)).toEqual([256, 1]);
184
+ });
185
+ //# sourceMappingURL=snapshot.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.test.js","sourceRoot":"","sources":["../../src/snapshot/snapshot.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { Track } from \"@moq/net\";\nimport { Consumer } from \"./consumer.ts\";\nimport { Producer } from \"./producer.ts\";\n\ntype Value = Record<string, unknown>;\n\n// Reconstruct every value a consumer yields, in order.\nasync function drain(track: Track): Promise<Value[]> {\n\tconst out: Value[] = [];\n\tfor await (const value of new Consumer<Value>(track)) out.push(value);\n\treturn out;\n}\n\n// Inspect the published layout via the public API: the frame count of each group, in order.\n// The track must be finished first so group/frame reads terminate.\nasync function structure(track: Track): Promise<number[]> {\n\tconst counts: number[] = [];\n\tfor (;;) {\n\t\tconst group = await track.nextGroupOrdered();\n\t\tif (!group) break;\n\n\t\tlet frames = 0;\n\t\twhile ((await group.readFrame()) !== undefined) frames++;\n\t\tcounts.push(frames);\n\t}\n\treturn counts;\n}\n\ntest(\"deltas off: a snapshot group per change\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 0 });\n\tproducer.update({ a: 1 });\n\tproducer.update({ a: 2 });\n\tproducer.finish();\n\n\t// Two changes => two single-frame snapshot groups, reconstructed in order.\n\texpect(await drain(track)).toEqual([{ a: 1 }, { a: 2 }]);\n});\n\ntest(\"deltaRatio 0 disables deltas, like off\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 0 });\n\tproducer.update({ a: 1 });\n\tproducer.update({ a: 2 });\n\tproducer.finish();\n\n\t// `0` is treated as off, not a degenerate \"enabled\" value that keeps the group open: each change\n\t// is its own single-frame snapshot group.\n\texpect(await structure(track)).toEqual([1, 1]);\n});\n\ntest(\"live consumer sees each update\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track);\n\tconst consumer = new Consumer<Value>(track);\n\n\tfor (let n = 1; n <= 3; n++) {\n\t\tproducer.update({ a: n });\n\t\texpect(await consumer.next()).toEqual({ a: n });\n\t}\n});\n\ntest(\"unchanged value writes nothing\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track);\n\tproducer.update({ a: 1 });\n\tproducer.update({ a: 1 });\n\tproducer.finish();\n\n\texpect(await structure(track)).toEqual([1]);\n});\n\ntest(\"deltas share one group\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100 });\n\tproducer.update({ a: 1, b: 1 });\n\tproducer.update({ a: 1, b: 2 });\n\tproducer.update({ a: 1, b: 3 });\n\tproducer.finish();\n\n\t// All updates fit in a single group as snapshot + two deltas.\n\texpect(await structure(track)).toEqual([3]);\n});\n\ntest(\"deltas reconstruct to the final value\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100 });\n\tproducer.update({ a: 1, b: 1 });\n\tproducer.update({ a: 1, b: 2 });\n\tproducer.update({ a: 5, b: 2 });\n\tproducer.finish();\n\n\texpect((await drain(track)).at(-1)).toEqual({ a: 5, b: 2 });\n});\n\n// `mutate()` edits the shared document: multiple owners edit one producer, each touching its own\n// keys, and each call publishes. This is how the catalog producer is extended (e.g. an scte35\n// section) without a single owner having to rebuild the whole document.\ntest(\"mutate composes independent owners\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { initial: {} });\n\tconst consumer = new Consumer<Value>(track);\n\n\tproducer.mutate((v) => {\n\t\tv.video = \"v1\";\n\t});\n\texpect(await consumer.next()).toEqual({ video: \"v1\" });\n\n\t// A second owner starts from the latest value and adds its own key without clobbering the first.\n\tproducer.mutate((v) => {\n\t\tv.scte35 = { id: 1 };\n\t});\n\texpect(await consumer.next()).toEqual({ video: \"v1\", scte35: { id: 1 } });\n});\n\ntest(\"mutate starts from the configured initial value\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { initial: {} });\n\tconst consumer = new Consumer<Value>(track);\n\n\tproducer.mutate((v) => {\n\t\tv.a = 1;\n\t});\n\texpect(await consumer.next()).toEqual({ a: 1 });\n});\n\ntest(\"mutate without a prior value or initial throws\", () => {\n\tconst producer = new Producer<Value>(new Track(\"test\"));\n\texpect(() => producer.mutate(() => {})).toThrow();\n});\n\n// Removing a section drops it from the reconstructed value, so a consumer detects the removal.\n// Exercised with deltas on to cover the merge-patch null-deletion path.\ntest(\"mutate removes a section\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100, initial: {} });\n\tconst consumer = new Consumer<Value>(track);\n\n\tproducer.mutate((v) => {\n\t\tv.a = 1;\n\t\tv.scte35 = { id: 1 };\n\t});\n\texpect(await consumer.next()).toEqual({ a: 1, scte35: { id: 1 } });\n\n\tproducer.mutate((v) => {\n\t\tdelete v.scte35;\n\t});\n\texpect(await consumer.next()).toEqual({ a: 1 });\n});\n\ntest(\"tight ratio rolls snapshots\", async () => {\n\tconst track = new Track(\"test\");\n\t// A ratio of 1 budgets deltas up to one snapshot (equal 7-byte frames => 7 bytes). The gate checks\n\t// the deltas already written, so the delta that tips the group over budget still lands (a one-frame\n\t// overshoot): group 0 takes two deltas (14 bytes) before the fourth update rolls group 1.\n\tconst producer = new Producer<Value>(track, { deltaRatio: 1 });\n\tproducer.update({ a: 1 }); // snapshot, group 0\n\tproducer.update({ a: 2 }); // delta, group 0 (deltas = 7)\n\tproducer.update({ a: 3 }); // delta, group 0 (deltas = 14, now over budget)\n\tproducer.update({ a: 4 }); // budget already exceeded, rolls group 1\n\tproducer.finish();\n\n\texpect(await structure(track)).toEqual([3, 1]);\n});\n\ntest(\"deltas stay within ratio times snapshot\", async () => {\n\tconst track = new Track(\"test\");\n\t// The budget covers only the deltas, not the snapshot frame, measured against the group's snapshot\n\t// size. Single-digit values keep every frame at a constant 7 bytes (`{\"n\":N}`), so a ratio of 8\n\t// budgets 56 bytes of deltas. The gate checks the deltas already written, so the group keeps filling\n\t// until they first exceed 56 (nine deltas = 63 bytes) and the next update rolls (a one-frame\n\t// overshoot past the 56-byte budget).\n\tconst producer = new Producer<Value>(track, { deltaRatio: 8 });\n\tfor (let n = 0; n <= 10; n++) producer.update({ n });\n\tproducer.finish();\n\n\texpect(await structure(track)).toEqual([10, 1]);\n});\n\ntest(\"array change is a wholesale delta\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100 });\n\tproducer.update({ list: [1, 2] });\n\tproducer.update({ list: [1, 2, 3] });\n\tproducer.finish();\n\n\t// The array is replaced wholesale in a delta, so it stays in the same group.\n\texpect(await structure(track)).toEqual([2]);\n});\n\ntest(\"late joiner collapses a buffered backlog to the latest value\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100 });\n\tfor (let n = 0; n <= 20; n++) {\n\t\tproducer.update({ n });\n\t}\n\tproducer.finish();\n\n\t// A whole group's worth of snapshot + deltas is buffered before the consumer reads, so it applies\n\t// them all but yields only the latest value once, not every superseded state.\n\texpect(await drain(track)).toEqual([{ n: 20 }]);\n});\n\ntest(\"frame cap rolls snapshot\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 1_000_000 });\n\t// First update is the snapshot; deltas fill the group until the frame cap forces a roll.\n\tfor (let i = 0; i <= 256; i++) {\n\t\tproducer.update({ n: i });\n\t}\n\tproducer.finish();\n\n\texpect(await structure(track)).toEqual([256, 1]);\n});\n"]}
package/stream.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Lossless append-log JSON publishing over MoQ tracks: the counterpart to the lossy `Snapshot`
3
+ * module. Instead of one value updated over time, a stream is an ordered log of self-contained
4
+ * records that preserves everything; every {@link Producer.append} writes one JSON object as one
5
+ * frame, nothing is ever superseded, and a {@link Consumer} yields every record in order.
6
+ *
7
+ * The whole log rides a **single group** that is never rolled: with {@link ProducerConfig.compression}
8
+ * on, that one group is one DEFLATE window, so every record compresses against the earlier ones.
9
+ * There is deliberately no group rolling (and so no catch-up machinery); a caller that wants to
10
+ * bound the record rate throttles at the source. Interoperable on the wire with the Rust
11
+ * `moq_json::stream`.
12
+ *
13
+ * @module
14
+ */
15
+ import type * as Moq from "@norskvideo/moq-net";
16
+ /** Options for a stream {@link Producer}. */
17
+ export interface ProducerConfig {
18
+ /**
19
+ * Compress the group as one sync-flushed `deflate-raw` stream, so each record reuses the earlier
20
+ * ones as context and shrinks sharply. A {@link Consumer} reading the track must set the same
21
+ * flag. Defaults to `false`.
22
+ */
23
+ compression?: boolean;
24
+ }
25
+ /** Options for a stream {@link Consumer}. */
26
+ export interface ConsumerConfig {
27
+ /** Whether the track's frames are `deflate-raw` compressed. Must match the producer. Defaults to `false`. */
28
+ compression?: boolean;
29
+ }
30
+ /**
31
+ * Publishes an ordered log of JSON records to a track, one record per frame in a single group.
32
+ */
33
+ export declare class Producer<T> {
34
+ #private;
35
+ /** Wrap a track to publish a record log into it. */
36
+ constructor(track: Moq.Track, config?: ProducerConfig);
37
+ /** Append one record to the log. */
38
+ append(value: T): void;
39
+ /** Finish the track, closing the group. */
40
+ finish(): void;
41
+ }
42
+ /**
43
+ * Consumes an ordered log of JSON records from a track, yielding every record in order.
44
+ *
45
+ * The log rides a single group, so this reads that group's frames in order; one record per frame.
46
+ */
47
+ export declare class Consumer<T> {
48
+ #private;
49
+ constructor(track: Moq.Track, config?: ConsumerConfig);
50
+ /** Get the next record, or `undefined` once the track ends. */
51
+ next(): Promise<T | undefined>;
52
+ [Symbol.asyncIterator](): AsyncIterator<T>;
53
+ }
54
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC9B,6GAA6G;IAC7G,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,qBAAa,QAAQ,CAAC,CAAC;;IAStB,oDAAoD;IACpD,YAAY,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,GAAE,cAAmB,EAGxD;IAED,oCAAoC;IACpC,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CASrB;IAED,2CAA2C;IAC3C,MAAM,IAAI,IAAI,CAIb;CACD;AAED;;;;GAIG;AACH,qBAAa,QAAQ,CAAC,CAAC;;IAQtB,YAAY,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,GAAE,cAAmB,EAGxD;IAED,+DAA+D;IACzD,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAmBnC;IAEM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,CAMhD;CACD"}
package/stream.js ADDED
@@ -0,0 +1,92 @@
1
+ /* @ts-self-types="./stream.d.ts" */
2
+ /**
3
+ * Lossless append-log JSON publishing over MoQ tracks: the counterpart to the lossy `Snapshot`
4
+ * module. Instead of one value updated over time, a stream is an ordered log of self-contained
5
+ * records that preserves everything; every {@link Producer.append} writes one JSON object as one
6
+ * frame, nothing is ever superseded, and a {@link Consumer} yields every record in order.
7
+ *
8
+ * The whole log rides a **single group** that is never rolled: with {@link ProducerConfig.compression}
9
+ * on, that one group is one DEFLATE window, so every record compresses against the earlier ones.
10
+ * There is deliberately no group rolling (and so no catch-up machinery); a caller that wants to
11
+ * bound the record rate throttles at the source. Interoperable on the wire with the Rust
12
+ * `moq_json::stream`.
13
+ *
14
+ * @module
15
+ */
16
+ import { Decoder, Encoder } from "@norskvideo/moq-flate";
17
+ /**
18
+ * Publishes an ordered log of JSON records to a track, one record per frame in a single group.
19
+ */
20
+ export class Producer {
21
+ #track;
22
+ #compress;
23
+ // The single group carrying the whole log, opened on the first append.
24
+ #group;
25
+ // The group's DEFLATE encoder (one window for the whole log), present while compressing.
26
+ #encoder;
27
+ /** Wrap a track to publish a record log into it. */
28
+ constructor(track, config = {}) {
29
+ this.#track = track;
30
+ this.#compress = config.compression ?? false;
31
+ }
32
+ /** Append one record to the log. */
33
+ append(value) {
34
+ const payload = new TextEncoder().encode(JSON.stringify(value));
35
+ if (!this.#group) {
36
+ this.#group = this.#track.appendGroup();
37
+ this.#encoder = this.#compress ? new Encoder() : undefined;
38
+ }
39
+ this.#group.writeFrame(this.#encoder ? this.#encoder.frame(payload) : payload);
40
+ }
41
+ /** Finish the track, closing the group. */
42
+ finish() {
43
+ this.#group?.close();
44
+ this.#group = undefined;
45
+ this.#track.close();
46
+ }
47
+ }
48
+ /**
49
+ * Consumes an ordered log of JSON records from a track, yielding every record in order.
50
+ *
51
+ * The log rides a single group, so this reads that group's frames in order; one record per frame.
52
+ */
53
+ export class Consumer {
54
+ #track;
55
+ #decompress;
56
+ #group;
57
+ // The group's DEFLATE decoder (one window for the whole log), built on the first frame.
58
+ #decoder;
59
+ constructor(track, config = {}) {
60
+ this.#track = track;
61
+ this.#decompress = config.compression ?? false;
62
+ }
63
+ /** Get the next record, or `undefined` once the track ends. */
64
+ async next() {
65
+ for (;;) {
66
+ if (!this.#group) {
67
+ this.#group = await this.#track.nextGroupOrdered();
68
+ if (!this.#group)
69
+ return undefined;
70
+ this.#decoder = this.#decompress ? new Decoder() : undefined;
71
+ }
72
+ const frame = await this.#group.readFrame();
73
+ if (frame === undefined) {
74
+ // The group is finished; the log rides just this one, so the stream ends.
75
+ this.#group = undefined;
76
+ this.#decoder = undefined;
77
+ continue;
78
+ }
79
+ const plain = this.#decoder ? this.#decoder.frame(frame) : frame;
80
+ return JSON.parse(new TextDecoder().decode(plain));
81
+ }
82
+ }
83
+ async *[Symbol.asyncIterator]() {
84
+ for (;;) {
85
+ const value = await this.next();
86
+ if (value === undefined)
87
+ return;
88
+ yield value;
89
+ }
90
+ }
91
+ }
92
+ //# sourceMappingURL=stream.js.map
package/stream.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lossless append-log JSON publishing over MoQ tracks: the counterpart to the lossy `Snapshot`\n * module. Instead of one value updated over time, a stream is an ordered log of self-contained\n * records that preserves everything; every {@link Producer.append} writes one JSON object as one\n * frame, nothing is ever superseded, and a {@link Consumer} yields every record in order.\n *\n * The whole log rides a **single group** that is never rolled: with {@link ProducerConfig.compression}\n * on, that one group is one DEFLATE window, so every record compresses against the earlier ones.\n * There is deliberately no group rolling (and so no catch-up machinery); a caller that wants to\n * bound the record rate throttles at the source. Interoperable on the wire with the Rust\n * `moq_json::stream`.\n *\n * @module\n */\n\nimport { Decoder, Encoder } from \"@moq/flate\";\nimport type * as Moq from \"@moq/net\";\n\n/** Options for a stream {@link Producer}. */\nexport interface ProducerConfig {\n\t/**\n\t * Compress the group as one sync-flushed `deflate-raw` stream, so each record reuses the earlier\n\t * ones as context and shrinks sharply. A {@link Consumer} reading the track must set the same\n\t * flag. Defaults to `false`.\n\t */\n\tcompression?: boolean;\n}\n\n/** Options for a stream {@link Consumer}. */\nexport interface ConsumerConfig {\n\t/** Whether the track's frames are `deflate-raw` compressed. Must match the producer. Defaults to `false`. */\n\tcompression?: boolean;\n}\n\n/**\n * Publishes an ordered log of JSON records to a track, one record per frame in a single group.\n */\nexport class Producer<T> {\n\t#track: Moq.Track;\n\t#compress: boolean;\n\n\t// The single group carrying the whole log, opened on the first append.\n\t#group?: Moq.Group;\n\t// The group's DEFLATE encoder (one window for the whole log), present while compressing.\n\t#encoder?: Encoder;\n\n\t/** Wrap a track to publish a record log into it. */\n\tconstructor(track: Moq.Track, config: ProducerConfig = {}) {\n\t\tthis.#track = track;\n\t\tthis.#compress = config.compression ?? false;\n\t}\n\n\t/** Append one record to the log. */\n\tappend(value: T): void {\n\t\tconst payload = new TextEncoder().encode(JSON.stringify(value));\n\n\t\tif (!this.#group) {\n\t\t\tthis.#group = this.#track.appendGroup();\n\t\t\tthis.#encoder = this.#compress ? new Encoder() : undefined;\n\t\t}\n\n\t\tthis.#group.writeFrame(this.#encoder ? this.#encoder.frame(payload) : payload);\n\t}\n\n\t/** Finish the track, closing the group. */\n\tfinish(): void {\n\t\tthis.#group?.close();\n\t\tthis.#group = undefined;\n\t\tthis.#track.close();\n\t}\n}\n\n/**\n * Consumes an ordered log of JSON records from a track, yielding every record in order.\n *\n * The log rides a single group, so this reads that group's frames in order; one record per frame.\n */\nexport class Consumer<T> {\n\t#track: Moq.Track;\n\t#decompress: boolean;\n\n\t#group?: Moq.Group;\n\t// The group's DEFLATE decoder (one window for the whole log), built on the first frame.\n\t#decoder?: Decoder;\n\n\tconstructor(track: Moq.Track, config: ConsumerConfig = {}) {\n\t\tthis.#track = track;\n\t\tthis.#decompress = config.compression ?? false;\n\t}\n\n\t/** Get the next record, or `undefined` once the track ends. */\n\tasync next(): Promise<T | undefined> {\n\t\tfor (;;) {\n\t\t\tif (!this.#group) {\n\t\t\t\tthis.#group = await this.#track.nextGroupOrdered();\n\t\t\t\tif (!this.#group) return undefined;\n\t\t\t\tthis.#decoder = this.#decompress ? new Decoder() : undefined;\n\t\t\t}\n\n\t\t\tconst frame = await this.#group.readFrame();\n\t\t\tif (frame === undefined) {\n\t\t\t\t// The group is finished; the log rides just this one, so the stream ends.\n\t\t\t\tthis.#group = undefined;\n\t\t\t\tthis.#decoder = undefined;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst plain = this.#decoder ? this.#decoder.frame(frame) : frame;\n\t\t\treturn JSON.parse(new TextDecoder().decode(plain));\n\t\t}\n\t}\n\n\tasync *[Symbol.asyncIterator](): AsyncIterator<T> {\n\t\tfor (;;) {\n\t\t\tconst value = await this.next();\n\t\t\tif (value === undefined) return;\n\t\t\tyield value;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stream.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.test.d.ts","sourceRoot":"","sources":["../src/stream.test.ts"],"names":[],"mappings":""}
package/stream.test.js ADDED
@@ -0,0 +1,64 @@
1
+ /* @ts-self-types="./stream.test.d.ts" */
2
+ import { expect, test } from "bun:test";
3
+ import { Track } from "@norskvideo/moq-net";
4
+ import { Consumer, Producer } from "./stream.js";
5
+ // Drain every record currently available from a fresh consumer over the (finished) track.
6
+ async function drain(track, compression) {
7
+ const consumer = new Consumer(track, { compression });
8
+ const out = [];
9
+ for (;;) {
10
+ const record = await consumer.next();
11
+ if (record === undefined)
12
+ break;
13
+ out.push(record.n);
14
+ }
15
+ return out;
16
+ }
17
+ test("plaintext roundtrip in order", async () => {
18
+ const track = new Track("test");
19
+ const producer = new Producer(track);
20
+ for (let n = 0; n < 5; n++)
21
+ producer.append({ n });
22
+ producer.finish();
23
+ expect(await drain(track, false)).toEqual([0, 1, 2, 3, 4]);
24
+ });
25
+ test("compressed roundtrip in order", async () => {
26
+ const track = new Track("test");
27
+ const producer = new Producer(track, { compression: true });
28
+ for (let n = 0; n < 20; n++)
29
+ producer.append({ n });
30
+ producer.finish();
31
+ expect(await drain(track, true)).toEqual(Array.from({ length: 20 }, (_, n) => n));
32
+ });
33
+ test("the whole log rides one group, never rolled", async () => {
34
+ const track = new Track("test");
35
+ const producer = new Producer(track, { compression: true });
36
+ for (let n = 0; n < 50; n++)
37
+ producer.append({ n });
38
+ producer.finish();
39
+ // A single group holds everything, and the consumer reads it all in order.
40
+ const group0 = await track.nextGroupOrdered();
41
+ expect(group0?.sequence).toBe(0);
42
+ const group1 = await track.nextGroupOrdered();
43
+ expect(group1).toBeUndefined();
44
+ });
45
+ test("records with embedded newlines round-trip (JSON escapes the newline)", async () => {
46
+ // Each record is its own frame (one JSON object), and JSON.stringify escapes control characters,
47
+ // so a string value containing a newline round-trips cleanly.
48
+ const track = new Track("test");
49
+ const producer = new Producer(track, { compression: true });
50
+ const value = { s: "line1\nline2\ttab" };
51
+ for (let i = 0; i < 4; i++)
52
+ producer.append(value);
53
+ producer.finish();
54
+ const consumer = new Consumer(track, { compression: true });
55
+ const out = [];
56
+ for (;;) {
57
+ const record = await consumer.next();
58
+ if (record === undefined)
59
+ break;
60
+ out.push(record);
61
+ }
62
+ expect(out).toEqual([value, value, value, value]);
63
+ });
64
+ //# sourceMappingURL=stream.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.test.js","sourceRoot":"","sources":["../src/stream.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { Track } from \"@moq/net\";\nimport { Consumer, Producer } from \"./stream.ts\";\n\ntype Rec = { n: number };\n\n// Drain every record currently available from a fresh consumer over the (finished) track.\nasync function drain(track: Track, compression: boolean): Promise<number[]> {\n\tconst consumer = new Consumer<Rec>(track, { compression });\n\tconst out: number[] = [];\n\tfor (;;) {\n\t\tconst record = await consumer.next();\n\t\tif (record === undefined) break;\n\t\tout.push(record.n);\n\t}\n\treturn out;\n}\n\ntest(\"plaintext roundtrip in order\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Rec>(track);\n\tfor (let n = 0; n < 5; n++) producer.append({ n });\n\tproducer.finish();\n\n\texpect(await drain(track, false)).toEqual([0, 1, 2, 3, 4]);\n});\n\ntest(\"compressed roundtrip in order\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Rec>(track, { compression: true });\n\tfor (let n = 0; n < 20; n++) producer.append({ n });\n\tproducer.finish();\n\n\texpect(await drain(track, true)).toEqual(Array.from({ length: 20 }, (_, n) => n));\n});\n\ntest(\"the whole log rides one group, never rolled\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Rec>(track, { compression: true });\n\tfor (let n = 0; n < 50; n++) producer.append({ n });\n\tproducer.finish();\n\n\t// A single group holds everything, and the consumer reads it all in order.\n\tconst group0 = await track.nextGroupOrdered();\n\texpect(group0?.sequence).toBe(0);\n\tconst group1 = await track.nextGroupOrdered();\n\texpect(group1).toBeUndefined();\n});\n\ntest(\"records with embedded newlines round-trip (JSON escapes the newline)\", async () => {\n\t// Each record is its own frame (one JSON object), and JSON.stringify escapes control characters,\n\t// so a string value containing a newline round-trips cleanly.\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<{ s: string }>(track, { compression: true });\n\tconst value = { s: \"line1\\nline2\\ttab\" };\n\tfor (let i = 0; i < 4; i++) producer.append(value);\n\tproducer.finish();\n\n\tconst consumer = new Consumer<{ s: string }>(track, { compression: true });\n\tconst out: { s: string }[] = [];\n\tfor (;;) {\n\t\tconst record = await consumer.next();\n\t\tif (record === undefined) break;\n\t\tout.push(record);\n\t}\n\texpect(out).toEqual([value, value, value, value]);\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vectors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vectors.test.d.ts","sourceRoot":"","sources":["../src/vectors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ /* @ts-self-types="./vectors.test.d.ts" */
2
+ import { expect, test } from "bun:test";
3
+ import { diff, merge } from "./diff.js";
4
+ // Shared cross-impl fixture, owned by the reference Rust crate. Both suites assert the same
5
+ // vectors so the two implementations agree on every snapshot/delta decision and patch shape.
6
+ const url = new URL("../../../rs/moq-json/tests/vectors.json", import.meta.url);
7
+ const vectors = (await Bun.file(url).json());
8
+ for (const vector of vectors) {
9
+ test(`vector: ${vector.name}`, () => {
10
+ const result = diff(vector.old, vector.new);
11
+ expect(result.forcedSnapshot).toBe(vector.forced);
12
+ if (!vector.forced) {
13
+ expect(result.patch).toEqual(vector.patch);
14
+ expect(merge(vector.old, result.patch)).toEqual(vector.new);
15
+ }
16
+ });
17
+ }
18
+ //# sourceMappingURL=vectors.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vectors.test.js","sourceRoot":"","sources":["../src/vectors.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAExC,4FAA4F;AAC5F,6FAA6F;AAC7F,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,yCAAyC,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAChF,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAMzC,CAAC;AAEH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;IAC9B,IAAI,CAAC,WAAW,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAElD,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAe,CAAC,CAAC;YACrD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAa,CAAC,CAAC;QACvE,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { diff, merge } from \"./diff.ts\";\n\n// Shared cross-impl fixture, owned by the reference Rust crate. Both suites assert the same\n// vectors so the two implementations agree on every snapshot/delta decision and patch shape.\nconst url = new URL(\"../../../rs/moq-json/tests/vectors.json\", import.meta.url);\nconst vectors = (await Bun.file(url).json()) as Array<{\n\tname: string;\n\told: unknown;\n\tnew: unknown;\n\tforced: boolean;\n\tpatch?: unknown;\n}>;\n\nfor (const vector of vectors) {\n\ttest(`vector: ${vector.name}`, () => {\n\t\tconst result = diff(vector.old, vector.new);\n\t\texpect(result.forcedSnapshot).toBe(vector.forced);\n\n\t\tif (!vector.forced) {\n\t\t\texpect(result.patch).toEqual(vector.patch as object);\n\t\t\texpect(merge(vector.old, result.patch)).toEqual(vector.new as object);\n\t\t}\n\t});\n}\n"]}