@norskvideo/moq-json 0.1.2 → 0.1.4

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 (93) hide show
  1. package/index.d.ts +13 -3
  2. package/index.d.ts.map +1 -1
  3. package/index.js +14 -3
  4. package/index.js.map +1 -1
  5. package/package.json +6 -5
  6. package/snapshot/consumer.d.ts +7 -5
  7. package/snapshot/consumer.d.ts.map +1 -1
  8. package/snapshot/consumer.js +31 -35
  9. package/snapshot/consumer.js.map +1 -1
  10. package/snapshot/decoder.d.ts +46 -0
  11. package/snapshot/decoder.d.ts.map +1 -0
  12. package/snapshot/decoder.js +76 -0
  13. package/snapshot/decoder.js.map +1 -0
  14. package/snapshot/encoder.d.ts +96 -0
  15. package/snapshot/encoder.d.ts.map +1 -0
  16. package/snapshot/encoder.js +196 -0
  17. package/snapshot/encoder.js.map +1 -0
  18. package/snapshot/index.d.ts +14 -1
  19. package/snapshot/index.d.ts.map +1 -1
  20. package/snapshot/index.js +13 -0
  21. package/snapshot/index.js.map +1 -1
  22. package/snapshot/producer.d.ts +22 -39
  23. package/snapshot/producer.d.ts.map +1 -1
  24. package/snapshot/producer.js +69 -185
  25. package/snapshot/producer.js.map +1 -1
  26. package/stream/consumer.d.ts +17 -0
  27. package/stream/consumer.d.ts.map +1 -0
  28. package/stream/consumer.js +46 -0
  29. package/stream/consumer.js.map +1 -0
  30. package/stream/decoder.d.ts +21 -0
  31. package/stream/decoder.d.ts.map +1 -0
  32. package/stream/decoder.js +28 -0
  33. package/stream/decoder.js.map +1 -0
  34. package/stream/encoder.d.ts +62 -0
  35. package/stream/encoder.d.ts.map +1 -0
  36. package/stream/encoder.js +78 -0
  37. package/stream/encoder.js.map +1 -0
  38. package/stream/index.d.ts +23 -0
  39. package/stream/index.d.ts.map +1 -0
  40. package/stream/index.js +24 -0
  41. package/stream/index.js.map +1 -0
  42. package/stream/producer.d.ts +18 -0
  43. package/stream/producer.d.ts.map +1 -0
  44. package/stream/producer.js +49 -0
  45. package/stream/producer.js.map +1 -0
  46. package/window/consumer.d.ts +26 -0
  47. package/window/consumer.d.ts.map +1 -0
  48. package/window/consumer.js +74 -0
  49. package/window/consumer.js.map +1 -0
  50. package/window/decoder.d.ts +57 -0
  51. package/window/decoder.d.ts.map +1 -0
  52. package/window/decoder.js +169 -0
  53. package/window/decoder.js.map +1 -0
  54. package/window/encoder.d.ts +72 -0
  55. package/window/encoder.d.ts.map +1 -0
  56. package/window/encoder.js +183 -0
  57. package/window/encoder.js.map +1 -0
  58. package/window/index.d.ts +39 -0
  59. package/window/index.d.ts.map +1 -0
  60. package/window/index.js +40 -0
  61. package/window/index.js.map +1 -0
  62. package/window/producer.d.ts +32 -0
  63. package/window/producer.d.ts.map +1 -0
  64. package/window/producer.js +90 -0
  65. package/window/producer.js.map +1 -0
  66. package/diff.test.d.ts +0 -2
  67. package/diff.test.d.ts.map +0 -1
  68. package/diff.test.js +0 -60
  69. package/diff.test.js.map +0 -1
  70. package/snapshot/compression.test.d.ts +0 -2
  71. package/snapshot/compression.test.d.ts.map +0 -1
  72. package/snapshot/compression.test.js +0 -132
  73. package/snapshot/compression.test.js.map +0 -1
  74. package/snapshot/producer.test.d.ts +0 -2
  75. package/snapshot/producer.test.d.ts.map +0 -1
  76. package/snapshot/producer.test.js +0 -75
  77. package/snapshot/producer.test.js.map +0 -1
  78. package/snapshot/snapshot.test.d.ts +0 -2
  79. package/snapshot/snapshot.test.d.ts.map +0 -1
  80. package/snapshot/snapshot.test.js +0 -185
  81. package/snapshot/snapshot.test.js.map +0 -1
  82. package/stream.d.ts +0 -54
  83. package/stream.d.ts.map +0 -1
  84. package/stream.js +0 -92
  85. package/stream.js.map +0 -1
  86. package/stream.test.d.ts +0 -2
  87. package/stream.test.d.ts.map +0 -1
  88. package/stream.test.js +0 -64
  89. package/stream.test.js.map +0 -1
  90. package/vectors.test.d.ts +0 -2
  91. package/vectors.test.d.ts.map +0 -1
  92. package/vectors.test.js +0 -18
  93. package/vectors.test.js.map +0 -1
@@ -1,132 +0,0 @@
1
- /* @ts-self-types="./compression.test.d.ts" */
2
- import { expect, test } from "bun:test";
3
- import { Decoder } from "@norskvideo/moq-flate";
4
- import { Track } from "@norskvideo/moq-net";
5
- import { Consumer } from "./consumer.js";
6
- import { Producer } from "./producer.js";
7
- const enc = new TextEncoder();
8
- const dec = new TextDecoder();
9
- // Reconstruct every value a compressed consumer yields, in order.
10
- async function drainCompressed(track) {
11
- const out = [];
12
- for await (const value of new Consumer(track, { compression: true }))
13
- out.push(value);
14
- return out;
15
- }
16
- // The raw (stored) bytes of a track's first frame, without reconstructing JSON.
17
- async function firstFrame(track) {
18
- const group = await track.nextGroupOrdered();
19
- if (!group)
20
- throw new Error("expected a group");
21
- const frame = await group.readFrame();
22
- if (!frame)
23
- throw new Error("expected a frame");
24
- return frame;
25
- }
26
- // Count the groups a (finished) track published, draining each so the reads terminate.
27
- async function groupCount(track) {
28
- let groups = 0;
29
- for (;;) {
30
- const group = await track.nextGroupOrdered();
31
- if (!group)
32
- return groups;
33
- groups++;
34
- while ((await group.readFrame()) !== undefined) { }
35
- }
36
- }
37
- test("compressed snapshot per group round-trips", async () => {
38
- const track = new Track("test");
39
- const producer = new Producer(track, { deltaRatio: 0, compression: true });
40
- producer.update({ a: 1 });
41
- producer.update({ a: 2 });
42
- producer.finish();
43
- // Deltas off: one compressed snapshot per group, reconstructed in order.
44
- expect(await drainCompressed(track)).toEqual([{ a: 1 }, { a: 2 }]);
45
- });
46
- test("compressed live consumer sees each update in order", async () => {
47
- // A live consumer reconstructs each update in order from the shared per-group stream.
48
- const track = new Track("test");
49
- const producer = new Producer(track, { deltaRatio: 100, compression: true });
50
- const consumer = new Consumer(track, { compression: true });
51
- for (let n = 1; n <= 5; n++) {
52
- producer.update({ a: n });
53
- expect(await consumer.next()).toEqual({ a: n });
54
- }
55
- });
56
- test("compressed deltas share one group and reconstruct", async () => {
57
- const track = new Track("test");
58
- const producer = new Producer(track, { deltaRatio: 100, compression: true });
59
- producer.update({ a: 1, b: 1 });
60
- producer.update({ a: 1, b: 2 });
61
- producer.update({ a: 5, b: 2 });
62
- producer.finish();
63
- expect((await drainCompressed(track)).at(-1)).toEqual({ a: 5, b: 2 });
64
- });
65
- test("compressed late joiner reconstructs from snapshot + deltas", async () => {
66
- const track = new Track("test");
67
- const producer = new Producer(track, { deltaRatio: 100, compression: true });
68
- producer.update({ a: 1, b: 1 });
69
- producer.update({ a: 1, b: 2 });
70
- producer.update({ a: 5, b: 2 });
71
- producer.finish();
72
- // A consumer created only now still rebuilds the final value from the snapshot + deltas.
73
- expect((await drainCompressed(track)).at(-1)).toEqual({ a: 5, b: 2 });
74
- });
75
- test("a group's snapshot decodes from a fresh decoder", async () => {
76
- // Frame 0 opens a cold window, so a brand-new decoder reconstructs it, which is what lets a late
77
- // joiner (or the Rust consumer) start mid-stream at any group boundary.
78
- const track = new Track("test");
79
- const producer = new Producer(track, { deltaRatio: 0, compression: true });
80
- producer.update({ hello: "world" });
81
- producer.finish();
82
- const decoder = new Decoder();
83
- expect(JSON.parse(dec.decode(decoder.frame(await firstFrame(track))))).toEqual({ hello: "world" });
84
- });
85
- test("compressed deltas reuse the window", async () => {
86
- // The shared per-group window is the point: a delta restating snapshot content shrinks sharply.
87
- const track = new Track("test");
88
- const producer = new Producer(track, { deltaRatio: 100, compression: true });
89
- const phrase = "Media over QUIC delivers real-time latency at massive scale";
90
- producer.update({ note: phrase });
91
- producer.update({ note: phrase, echo: phrase });
92
- producer.finish();
93
- const group = await track.nextGroupOrdered();
94
- if (!group)
95
- throw new Error("expected a group");
96
- await group.readFrame(); // snapshot
97
- const delta = await group.readFrame();
98
- if (!delta)
99
- throw new Error("expected a delta");
100
- const rawDelta = enc.encode(JSON.stringify({ echo: phrase }));
101
- expect(delta.length).toBeLessThan(rawDelta.length / 2);
102
- });
103
- test("compression shrinks a repetitive frame", async () => {
104
- const value = { renditions: Array(3).fill("video".repeat(50)) };
105
- const plain = new Track("plain");
106
- new Producer(plain, { deltaRatio: 0 }).update(value);
107
- const compressed = new Track("compressed");
108
- new Producer(compressed, { deltaRatio: 0, compression: true }).update(value);
109
- const plainLen = (await firstFrame(plain)).length;
110
- const compressedLen = (await firstFrame(compressed)).length;
111
- expect(compressedLen).toBeLessThan(plainLen);
112
- });
113
- test("compressed deltas roll on the compressed budget", async () => {
114
- // With compression the budget is measured on compressed frame sizes: #snapshotLen and #deltaBytes
115
- // are the written slice lengths, not the raw JSON. A tight ratio over many distinct updates must
116
- // roll more than one group, and a late joiner must still rebuild the final value across the
117
- // compressed group boundary. Guards against the budget regressing to raw lengths. Two identical
118
- // producers (deterministic output) keep the group-count and reconstruction reads independent.
119
- const fill = (track) => {
120
- const producer = new Producer(track, { deltaRatio: 2, compression: true });
121
- for (let n = 0; n <= 40; n++)
122
- producer.update({ n });
123
- producer.finish();
124
- };
125
- const layout = new Track("layout");
126
- fill(layout);
127
- expect(await groupCount(layout)).toBeGreaterThan(1);
128
- const reconstruct = new Track("reconstruct");
129
- fill(reconstruct);
130
- expect((await drainCompressed(reconstruct)).at(-1)).toEqual({ n: 40 });
131
- });
132
- //# sourceMappingURL=compression.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compression.test.js","sourceRoot":"","sources":["../../src/snapshot/compression.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { Decoder } from \"@moq/flate\";\nimport { Track } from \"@moq/net\";\nimport { Consumer } from \"./consumer.ts\";\nimport { Producer } from \"./producer.ts\";\n\ntype Value = Record<string, unknown>;\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\n// Reconstruct every value a compressed consumer yields, in order.\nasync function drainCompressed(track: Track): Promise<Value[]> {\n\tconst out: Value[] = [];\n\tfor await (const value of new Consumer<Value>(track, { compression: true })) out.push(value);\n\treturn out;\n}\n\n// The raw (stored) bytes of a track's first frame, without reconstructing JSON.\nasync function firstFrame(track: Track): Promise<Uint8Array> {\n\tconst group = await track.nextGroupOrdered();\n\tif (!group) throw new Error(\"expected a group\");\n\tconst frame = await group.readFrame();\n\tif (!frame) throw new Error(\"expected a frame\");\n\treturn frame;\n}\n\n// Count the groups a (finished) track published, draining each so the reads terminate.\nasync function groupCount(track: Track): Promise<number> {\n\tlet groups = 0;\n\tfor (;;) {\n\t\tconst group = await track.nextGroupOrdered();\n\t\tif (!group) return groups;\n\t\tgroups++;\n\t\twhile ((await group.readFrame()) !== undefined) {}\n\t}\n}\n\ntest(\"compressed snapshot per group round-trips\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 0, compression: true });\n\tproducer.update({ a: 1 });\n\tproducer.update({ a: 2 });\n\tproducer.finish();\n\n\t// Deltas off: one compressed snapshot per group, reconstructed in order.\n\texpect(await drainCompressed(track)).toEqual([{ a: 1 }, { a: 2 }]);\n});\n\ntest(\"compressed live consumer sees each update in order\", async () => {\n\t// A live consumer reconstructs each update in order from the shared per-group stream.\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100, compression: true });\n\tconst consumer = new Consumer<Value>(track, { compression: true });\n\n\tfor (let n = 1; n <= 5; n++) {\n\t\tproducer.update({ a: n });\n\t\texpect(await consumer.next()).toEqual({ a: n });\n\t}\n});\n\ntest(\"compressed deltas share one group and reconstruct\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100, compression: true });\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 drainCompressed(track)).at(-1)).toEqual({ a: 5, b: 2 });\n});\n\ntest(\"compressed late joiner reconstructs from snapshot + deltas\", async () => {\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100, compression: true });\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\t// A consumer created only now still rebuilds the final value from the snapshot + deltas.\n\texpect((await drainCompressed(track)).at(-1)).toEqual({ a: 5, b: 2 });\n});\n\ntest(\"a group's snapshot decodes from a fresh decoder\", async () => {\n\t// Frame 0 opens a cold window, so a brand-new decoder reconstructs it, which is what lets a late\n\t// joiner (or the Rust consumer) start mid-stream at any group boundary.\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 0, compression: true });\n\tproducer.update({ hello: \"world\" });\n\tproducer.finish();\n\n\tconst decoder = new Decoder();\n\texpect(JSON.parse(dec.decode(decoder.frame(await firstFrame(track))))).toEqual({ hello: \"world\" });\n});\n\ntest(\"compressed deltas reuse the window\", async () => {\n\t// The shared per-group window is the point: a delta restating snapshot content shrinks sharply.\n\tconst track = new Track(\"test\");\n\tconst producer = new Producer<Value>(track, { deltaRatio: 100, compression: true });\n\tconst phrase = \"Media over QUIC delivers real-time latency at massive scale\";\n\tproducer.update({ note: phrase });\n\tproducer.update({ note: phrase, echo: phrase });\n\tproducer.finish();\n\n\tconst group = await track.nextGroupOrdered();\n\tif (!group) throw new Error(\"expected a group\");\n\tawait group.readFrame(); // snapshot\n\tconst delta = await group.readFrame();\n\tif (!delta) throw new Error(\"expected a delta\");\n\n\tconst rawDelta = enc.encode(JSON.stringify({ echo: phrase }));\n\texpect(delta.length).toBeLessThan(rawDelta.length / 2);\n});\n\ntest(\"compression shrinks a repetitive frame\", async () => {\n\tconst value = { renditions: Array(3).fill(\"video\".repeat(50)) };\n\n\tconst plain = new Track(\"plain\");\n\tnew Producer<Value>(plain, { deltaRatio: 0 }).update(value);\n\tconst compressed = new Track(\"compressed\");\n\tnew Producer<Value>(compressed, { deltaRatio: 0, compression: true }).update(value);\n\n\tconst plainLen = (await firstFrame(plain)).length;\n\tconst compressedLen = (await firstFrame(compressed)).length;\n\texpect(compressedLen).toBeLessThan(plainLen);\n});\n\ntest(\"compressed deltas roll on the compressed budget\", async () => {\n\t// With compression the budget is measured on compressed frame sizes: #snapshotLen and #deltaBytes\n\t// are the written slice lengths, not the raw JSON. A tight ratio over many distinct updates must\n\t// roll more than one group, and a late joiner must still rebuild the final value across the\n\t// compressed group boundary. Guards against the budget regressing to raw lengths. Two identical\n\t// producers (deterministic output) keep the group-count and reconstruction reads independent.\n\tconst fill = (track: Track) => {\n\t\tconst producer = new Producer<Value>(track, { deltaRatio: 2, compression: true });\n\t\tfor (let n = 0; n <= 40; n++) producer.update({ n });\n\t\tproducer.finish();\n\t};\n\n\tconst layout = new Track(\"layout\");\n\tfill(layout);\n\texpect(await groupCount(layout)).toBeGreaterThan(1);\n\n\tconst reconstruct = new Track(\"reconstruct\");\n\tfill(reconstruct);\n\texpect((await drainCompressed(reconstruct)).at(-1)).toEqual({ n: 40 });\n});\n"]}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=producer.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"producer.test.d.ts","sourceRoot":"","sources":["../../src/snapshot/producer.test.ts"],"names":[],"mappings":""}
@@ -1,75 +0,0 @@
1
- /* @ts-self-types="./producer.test.d.ts" */
2
- import { expect, test } from "bun:test";
3
- import { Track } from "@norskvideo/moq-net";
4
- import { Effect } from "@norskvideo/moq-signals";
5
- import { Consumer } from "./consumer.js";
6
- import { Producer } from "./producer.js";
7
- test("a track-less producer seeds subscribers and fans out edits", async () => {
8
- const source = new Producer({ initial: {} });
9
- // Edit before anyone subscribes: the value is retained, not lost.
10
- source.mutate((v) => {
11
- v.video = { renditions: {} };
12
- });
13
- const effect = new Effect();
14
- const track = new Track("catalog.json");
15
- source.serve(track, effect);
16
- const consumer = new Consumer(track);
17
- // A new subscriber is seeded with the current value.
18
- expect((await consumer.next())?.video).toEqual({ renditions: {} });
19
- // An independent owner edits its own key; the subscriber sees it, the other key untouched.
20
- source.mutate((v) => {
21
- v.scte35 = { splices: [] };
22
- });
23
- const update = await consumer.next();
24
- expect(update?.video).toEqual({ renditions: {} });
25
- expect(update?.scte35).toEqual({ splices: [] });
26
- effect.close();
27
- });
28
- test("a reconnecting subscriber is seeded with the full current value", async () => {
29
- const source = new Producer({ initial: {} });
30
- source.mutate((v) => {
31
- v.video = { renditions: {} };
32
- v.scte35 = { splices: [] };
33
- });
34
- // The first subscription drains and ends...
35
- const first = new Effect();
36
- source.serve(new Track("catalog.json"), first);
37
- first.close();
38
- // ...and a fresh subscription still gets the current value, not nothing.
39
- const effect = new Effect();
40
- const track = new Track("catalog.json");
41
- source.serve(track, effect);
42
- const seeded = await new Consumer(track).next();
43
- expect(seeded?.video).toEqual({ renditions: {} });
44
- expect(seeded?.scte35).toEqual({ splices: [] });
45
- effect.close();
46
- });
47
- test("mutate without a value or initial throws", () => {
48
- const source = new Producer();
49
- expect(() => source.mutate(() => { })).toThrow();
50
- });
51
- test("serve() can override compression per subscriber", async () => {
52
- // One fan-out producer serves the same value both plaintext and compressed.
53
- const source = new Producer({ initial: {} });
54
- source.mutate((v) => {
55
- v.video = { renditions: { hd: { codec: "avc1.640028" } } };
56
- });
57
- const effect = new Effect();
58
- const plainTrack = new Track("catalog.json");
59
- source.serve(plainTrack, effect);
60
- const plain = await new Consumer(plainTrack).next();
61
- const zTrack = new Track("catalog.json.z");
62
- source.serve(zTrack, effect, { compression: true });
63
- const compressed = await new Consumer(zTrack, { compression: true }).next();
64
- // Both tracks reconstruct the identical value despite different wire encodings.
65
- expect(compressed).toEqual(plain);
66
- expect(compressed?.video).toEqual({ renditions: { hd: { codec: "avc1.640028" } } });
67
- effect.close();
68
- });
69
- test("serve() throws on a track-bound producer", () => {
70
- const producer = new Producer(new Track("meta.json"));
71
- const effect = new Effect();
72
- expect(() => producer.serve(new Track("other"), effect)).toThrow();
73
- effect.close();
74
- });
75
- //# sourceMappingURL=producer.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"producer.test.js","sourceRoot":"","sources":["../../src/snapshot/producer.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { Track } from \"@moq/net\";\nimport { Effect } from \"@moq/signals\";\nimport { Consumer } from \"./consumer.ts\";\nimport { Producer } from \"./producer.ts\";\n\ntest(\"a track-less producer seeds subscribers and fans out edits\", async () => {\n\tconst source = new Producer<Record<string, unknown>>({ initial: {} });\n\n\t// Edit before anyone subscribes: the value is retained, not lost.\n\tsource.mutate((v) => {\n\t\tv.video = { renditions: {} };\n\t});\n\n\tconst effect = new Effect();\n\tconst track = new Track(\"catalog.json\");\n\tsource.serve(track, effect);\n\tconst consumer = new Consumer<Record<string, unknown>>(track);\n\n\t// A new subscriber is seeded with the current value.\n\texpect((await consumer.next())?.video).toEqual({ renditions: {} });\n\n\t// An independent owner edits its own key; the subscriber sees it, the other key untouched.\n\tsource.mutate((v) => {\n\t\tv.scte35 = { splices: [] };\n\t});\n\tconst update = await consumer.next();\n\texpect(update?.video).toEqual({ renditions: {} });\n\texpect(update?.scte35).toEqual({ splices: [] });\n\n\teffect.close();\n});\n\ntest(\"a reconnecting subscriber is seeded with the full current value\", async () => {\n\tconst source = new Producer<Record<string, unknown>>({ initial: {} });\n\tsource.mutate((v) => {\n\t\tv.video = { renditions: {} };\n\t\tv.scte35 = { splices: [] };\n\t});\n\n\t// The first subscription drains and ends...\n\tconst first = new Effect();\n\tsource.serve(new Track(\"catalog.json\"), first);\n\tfirst.close();\n\n\t// ...and a fresh subscription still gets the current value, not nothing.\n\tconst effect = new Effect();\n\tconst track = new Track(\"catalog.json\");\n\tsource.serve(track, effect);\n\tconst seeded = await new Consumer<Record<string, unknown>>(track).next();\n\texpect(seeded?.video).toEqual({ renditions: {} });\n\texpect(seeded?.scte35).toEqual({ splices: [] });\n\n\teffect.close();\n});\n\ntest(\"mutate without a value or initial throws\", () => {\n\tconst source = new Producer<Record<string, unknown>>();\n\texpect(() => source.mutate(() => {})).toThrow();\n});\n\ntest(\"serve() can override compression per subscriber\", async () => {\n\t// One fan-out producer serves the same value both plaintext and compressed.\n\tconst source = new Producer<Record<string, unknown>>({ initial: {} });\n\tsource.mutate((v) => {\n\t\tv.video = { renditions: { hd: { codec: \"avc1.640028\" } } };\n\t});\n\n\tconst effect = new Effect();\n\n\tconst plainTrack = new Track(\"catalog.json\");\n\tsource.serve(plainTrack, effect);\n\tconst plain = await new Consumer<Record<string, unknown>>(plainTrack).next();\n\n\tconst zTrack = new Track(\"catalog.json.z\");\n\tsource.serve(zTrack, effect, { compression: true });\n\tconst compressed = await new Consumer<Record<string, unknown>>(zTrack, { compression: true }).next();\n\n\t// Both tracks reconstruct the identical value despite different wire encodings.\n\texpect(compressed).toEqual(plain);\n\texpect(compressed?.video).toEqual({ renditions: { hd: { codec: \"avc1.640028\" } } });\n\n\teffect.close();\n});\n\ntest(\"serve() throws on a track-bound producer\", () => {\n\tconst producer = new Producer<Record<string, unknown>>(new Track(\"meta.json\"));\n\tconst effect = new Effect();\n\texpect(() => producer.serve(new Track(\"other\"), effect)).toThrow();\n\teffect.close();\n});\n"]}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=snapshot.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"snapshot.test.d.ts","sourceRoot":"","sources":["../../src/snapshot/snapshot.test.ts"],"names":[],"mappings":""}
@@ -1,185 +0,0 @@
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
@@ -1 +0,0 @@
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 DELETED
@@ -1,54 +0,0 @@
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
package/stream.d.ts.map DELETED
@@ -1 +0,0 @@
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 DELETED
@@ -1,92 +0,0 @@
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