@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/stream/encoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,YAAY,CAAC;AAoC9C;;;;;;;;;;GAUG;AACH,MAAM,OAAO,OAAO;IACnB,SAAS,CAAU;IACnB,mEAAmE;IACnE,MAAM,CAAS;IAEf,8FAA8F;IAC9F,0FAA0F;IAC1F,SAAS,GAAG,KAAK,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,GAAG,KAAK,CAAC;IACjB,iGAAiG;IACjG,gFAAgF;IAChF,WAAW,GAAG,CAAC,CAAC;IAEhB,YAAY,MAAM,GAAmB,EAAE;QACtC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,WAAW,IAAI,KAAK,CAAC;QAC7C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACxD,CAAC;IAED;;;;;OAKG;IACH,KAAK;QACJ,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACvD,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;IACvB,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAQ;QACd,+FAA+F;QAC/F,sDAAsD;QACtD,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QAC3D,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QACvF,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACxB,0FAA0F;YAC1F,iFAAiF;YACjF,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAE/D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,MAAM,UAAU,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC;QAEtC,OAAO;YACN,OAAO;YACP,MAAM,EAAE,GAAG,EAAE;gBACZ,uFAAuF;gBACvF,2FAA2F;gBAC3F,0DAA0D;gBAC1D,IAAI,IAAI,CAAC,WAAW,KAAK,UAAU;oBAAE,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;YAC5D,CAAC;SACD,CAAC;IACH,CAAC;CACD","sourcesContent":["import { Encoder as Flate } from \"@moq/flate\";\n\n/** Options shared by an {@link Encoder} and the {@link Producer} that wraps one. */\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 Decoder} reading the frames must set the same\n\t * flag. Defaults to `false`.\n\t */\n\tcompression?: boolean;\n}\n\n/**\n * An encoded record the caller has not yet acknowledged writing, returned by {@link Encoder.encode}.\n *\n * Write the {@link payload}, then {@link commit}.\n *\n * A record that is never committed never reached the wire. With compression on that is\n * unrecoverable within the group: the window is ahead of what the consumer holds, and a log has no\n * keyframe to resynchronize on the way `Snapshot` does. So the encoder throws on the next\n * {@link Encoder.encode} until the caller rolls a new group and calls {@link Encoder.reset}. Without\n * compression each record stands alone, so a dropped one leaves a gap in the log but nothing\n * undecodable, and encoding continues.\n */\nexport interface Pending {\n\t/** The frame payload to write. */\n\tpayload: Uint8Array;\n\n\t/**\n\t * Acknowledge that the record reached the wire, keeping the encoder's window.\n\t *\n\t * Only call this once the write has actually succeeded.\n\t */\n\tcommit(): void;\n}\n\n/**\n * Encodes JSON records into frame payloads, sharing one DEFLATE window across the log.\n *\n * The track-free core of {@link Producer}. Unlike the `Snapshot` encoder there are no group\n * boundaries to report: a log is an unbroken sequence of self-contained records, so every payload is\n * simply the next frame.\n *\n * The window spans everything encoded so far, so payloads must reach the wire in order and be\n * decoded in the same order. If the caller does roll a group, call {@link reset} so the next record\n * starts a cold window that the new group's decoder can follow.\n */\nexport class Encoder<T> {\n\t#compress: boolean;\n\t// The DEFLATE window for the whole log, present while compressing.\n\t#flate?: Flate;\n\n\t// Set when a compressed record was encoded but never written. The window is then ahead of the\n\t// consumer for the rest of the group, so encoding stops until the caller rolls a new one.\n\t#desynced = false;\n\t// Whether the record from the last {@link encode} is still unacknowledged.\n\t#pending = false;\n\t// Bumped for each record handed out, so a commit that arrives after the encoder has moved on can\n\t// tell that it is acknowledging a record that is no longer the outstanding one.\n\t#generation = 0;\n\n\tconstructor(config: ProducerConfig = {}) {\n\t\tthis.#compress = config.compression ?? false;\n\t\tthis.#flate = this.#compress ? new Flate() : undefined;\n\t}\n\n\t/**\n\t * Start a cold DEFLATE window, for a caller that has just rolled a group.\n\t *\n\t * This is also how a caller clears a desync: roll a new group so the consumer starts its own cold\n\t * window, then reset.\n\t */\n\treset(): void {\n\t\tthis.#flate = this.#compress ? new Flate() : undefined;\n\t\tthis.#desynced = false;\n\t\tthis.#pending = false;\n\t}\n\n\t/**\n\t * Encode one record into the next frame payload.\n\t *\n\t * The record comes back as a {@link Pending} the caller writes and then commits. Throws if a\n\t * previous compressed record was left uncommitted, since every frame after it would be\n\t * undecodable.\n\t */\n\tencode(value: T): Pending {\n\t\t// An uncompressed record carries no shared state, so losing one leaves a gap in the log rather\n\t\t// than an undecodable stream, and encoding continues.\n\t\tif (this.#pending && this.#compress) this.#desynced = true;\n\t\tif (this.#desynced) {\n\t\t\tthrow new Error(\"compression desynchronized: a record was encoded but never written\");\n\t\t}\n\n\t\tconst text = JSON.stringify(value);\n\t\tif (text === undefined) {\n\t\t\t// `JSON.stringify` yields undefined for a top-level undefined, function, or symbol, which\n\t\t\t// would otherwise frame as empty bytes and fail on the consumer instead of here.\n\t\t\tthrow new Error(\"record is not representable as JSON\");\n\t\t}\n\n\t\tconst bytes = new TextEncoder().encode(text);\n\t\tconst payload = this.#flate ? this.#flate.frame(bytes) : bytes;\n\n\t\tthis.#pending = true;\n\t\tconst generation = ++this.#generation;\n\n\t\treturn {\n\t\t\tpayload,\n\t\t\tcommit: () => {\n\t\t\t\t// A caller that starts the next encode before this record settles has already made the\n\t\t\t\t// encoder account for it. Acknowledging it now would clear the flag belonging to the newer\n\t\t\t\t// record, so a later loss of that one would go unnoticed.\n\t\t\t\tif (this.#generation === generation) this.#pending = false;\n\t\t\t},\n\t\t};\n\t}\n}\n"]}
@@ -0,0 +1,23 @@
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
+ * {@link Producer} and {@link Consumer} own a track. {@link Encoder} and {@link Decoder} are the
14
+ * same logic without it, for when something else is already in charge of the track; they carry the
15
+ * shared DEFLATE window and nothing else, since a log has no group boundaries to report.
16
+ *
17
+ * @module
18
+ */
19
+ export { Consumer } from "./consumer";
20
+ export { type ConsumerConfig, Decoder } from "./decoder";
21
+ export { Encoder, type Pending, type ProducerConfig } from "./encoder";
22
+ export { Producer } from "./producer";
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/stream/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,24 @@
1
+ /* @ts-self-types="./index.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
+ * {@link Producer} and {@link Consumer} own a track. {@link Encoder} and {@link Decoder} are the
15
+ * same logic without it, for when something else is already in charge of the track; they carry the
16
+ * shared DEFLATE window and nothing else, since a log has no group boundaries to report.
17
+ *
18
+ * @module
19
+ */
20
+ export { Consumer } from "./consumer.js";
21
+ export { Decoder } from "./decoder.js";
22
+ export { Encoder } from "./encoder.js";
23
+ export { Producer } from "./producer.js";
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/stream/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAuB,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAqC,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC","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 * {@link Producer} and {@link Consumer} own a track. {@link Encoder} and {@link Decoder} are the\n * same logic without it, for when something else is already in charge of the track; they carry the\n * shared DEFLATE window and nothing else, since a log has no group boundaries to report.\n *\n * @module\n */\n\nexport { Consumer } from \"./consumer.ts\";\nexport { type ConsumerConfig, Decoder } from \"./decoder.ts\";\nexport { Encoder, type Pending, type ProducerConfig } from \"./encoder.ts\";\nexport { Producer } from \"./producer.ts\";\n"]}
@@ -0,0 +1,18 @@
1
+ import type * as Moq from "@norskvideo/moq-net";
2
+ import { type ProducerConfig } from "./encoder";
3
+ /**
4
+ * Publishes an ordered log of JSON records to a track, one record per frame in a single group.
5
+ *
6
+ * An {@link Encoder} that owns its track. When something else already owns the track, use the
7
+ * {@link Encoder} directly.
8
+ */
9
+ export declare class Producer<T> {
10
+ #private;
11
+ /** Wrap a track to publish a record log into it. */
12
+ constructor(track: Moq.Track.Producer, config?: ProducerConfig);
13
+ /** Append one record to the log. */
14
+ append(value: T): void;
15
+ /** Finish the track, closing the group. */
16
+ finish(): void;
17
+ }
18
+ //# sourceMappingURL=producer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"producer.d.ts","sourceRoot":"","sources":["../../src/stream/producer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAGrC,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;;GAKG;AACH,qBAAa,QAAQ,CAAC,CAAC;;IAOtB,oDAAoD;IACpD,YAAY,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAE,cAAmB,EAGjE;IAED,oCAAoC;IACpC,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAoBrB;IAED,2CAA2C;IAC3C,MAAM,IAAI,IAAI,CAIb;CACD"}
@@ -0,0 +1,49 @@
1
+ /* @ts-self-types="./producer.d.ts" */
2
+ import { Time } from "@norskvideo/moq-net";
3
+ import { Encoder } from "./encoder.js";
4
+ /**
5
+ * Publishes an ordered log of JSON records to a track, one record per frame in a single group.
6
+ *
7
+ * An {@link Encoder} that owns its track. When something else already owns the track, use the
8
+ * {@link Encoder} directly.
9
+ */
10
+ export class Producer {
11
+ #track;
12
+ #encoder;
13
+ // The single group carrying the whole log, opened on the first append.
14
+ #group;
15
+ /** Wrap a track to publish a record log into it. */
16
+ constructor(track, config = {}) {
17
+ this.#track = track;
18
+ this.#encoder = new Encoder(config);
19
+ }
20
+ /** Append one record to the log. */
21
+ append(value) {
22
+ // Encode first, so a value that can't be serialized doesn't publish an empty group that
23
+ // subscribers would advance into and wait on. Opening the group afterwards is safe because
24
+ // the record stays uncommitted until the write lands.
25
+ const record = this.#encoder.encode(value);
26
+ try {
27
+ this.#group ??= this.#track.appendGroup();
28
+ this.#group.writeFrame({ payload: record.payload, timestamp: Time.Timestamp.now() });
29
+ }
30
+ catch (err) {
31
+ // The group is already visible on the track, so leaving it open would strand a subscriber
32
+ // that advanced into it. Closing it is also the group roll that recovery needs, so reset the
33
+ // encoder to finish it: the record never landed, and without the reset a compressed encoder
34
+ // refuses every later one even though the fresh group could carry it.
35
+ this.#group?.close();
36
+ this.#group = undefined;
37
+ this.#encoder.reset();
38
+ throw err;
39
+ }
40
+ record.commit();
41
+ }
42
+ /** Finish the track, closing the group. */
43
+ finish() {
44
+ this.#group?.close();
45
+ this.#group = undefined;
46
+ this.#track.close();
47
+ }
48
+ }
49
+ //# sourceMappingURL=producer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"producer.js","sourceRoot":"","sources":["../../src/stream/producer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAE,OAAO,EAAuB,MAAM,cAAc,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,OAAO,QAAQ;IACpB,MAAM,CAAqB;IAC3B,QAAQ,CAAa;IAErB,uEAAuE;IACvE,MAAM,CAAsB;IAE5B,oDAAoD;IACpD,YAAY,KAAyB,EAAE,MAAM,GAAmB,EAAE;QACjE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED,oCAAoC;IACpC,MAAM,CAAC,KAAQ;QACd,wFAAwF;QACxF,2FAA2F;QAC3F,sDAAsD;QACtD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAE3C,IAAI,CAAC;YACJ,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC1C,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACtF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,0FAA0F;YAC1F,6FAA6F;YAC7F,4FAA4F;YAC5F,sEAAsE;YACtE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;YACxB,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACtB,MAAM,GAAG,CAAC;QACX,CAAC;QACD,MAAM,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC;IAED,2CAA2C;IAC3C,MAAM;QACL,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACD","sourcesContent":["import type * as Moq from \"@moq/net\";\nimport { Time } from \"@moq/net\";\n\nimport { Encoder, type ProducerConfig } from \"./encoder.ts\";\n\n/**\n * Publishes an ordered log of JSON records to a track, one record per frame in a single group.\n *\n * An {@link Encoder} that owns its track. When something else already owns the track, use the\n * {@link Encoder} directly.\n */\nexport class Producer<T> {\n\t#track: Moq.Track.Producer;\n\t#encoder: Encoder<T>;\n\n\t// The single group carrying the whole log, opened on the first append.\n\t#group?: Moq.Group.Producer;\n\n\t/** Wrap a track to publish a record log into it. */\n\tconstructor(track: Moq.Track.Producer, config: ProducerConfig = {}) {\n\t\tthis.#track = track;\n\t\tthis.#encoder = new Encoder(config);\n\t}\n\n\t/** Append one record to the log. */\n\tappend(value: T): void {\n\t\t// Encode first, so a value that can't be serialized doesn't publish an empty group that\n\t\t// subscribers would advance into and wait on. Opening the group afterwards is safe because\n\t\t// the record stays uncommitted until the write lands.\n\t\tconst record = this.#encoder.encode(value);\n\n\t\ttry {\n\t\t\tthis.#group ??= this.#track.appendGroup();\n\t\t\tthis.#group.writeFrame({ payload: record.payload, timestamp: Time.Timestamp.now() });\n\t\t} catch (err) {\n\t\t\t// The group is already visible on the track, so leaving it open would strand a subscriber\n\t\t\t// that advanced into it. Closing it is also the group roll that recovery needs, so reset the\n\t\t\t// encoder to finish it: the record never landed, and without the reset a compressed encoder\n\t\t\t// refuses every later one even though the fresh group could carry it.\n\t\t\tthis.#group?.close();\n\t\t\tthis.#group = undefined;\n\t\t\tthis.#encoder.reset();\n\t\t\tthrow err;\n\t\t}\n\t\trecord.commit();\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"]}
@@ -0,0 +1,26 @@
1
+ import type * as Moq from "@norskvideo/moq-net";
2
+ import { type ConsumerConfig, type Event } from "./decoder";
3
+ /**
4
+ * Consumes a sliding window of JSON records from a track, yielding one event per change.
5
+ *
6
+ * A {@link Decoder} that owns its track: it reads groups, starts a cold DEFLATE window at each
7
+ * boundary, and turns each group's header into just the changes this reader has not been told about.
8
+ * When something else already owns the track, use the {@link Decoder} directly.
9
+ *
10
+ * Group rolls never surface. A publisher rolls for compression's sake, and a header restating the
11
+ * window yields nothing for records already delivered, so this reads as one continuous stream of
12
+ * {@link Event}s regardless of how the publisher framed them.
13
+ *
14
+ * @public
15
+ */
16
+ export declare class Consumer<T> {
17
+ #private;
18
+ constructor(track: Moq.Track.Subscriber, config?: ConsumerConfig);
19
+ /** Absolute index of the oldest record in the window. */
20
+ get offset(): number;
21
+ /** Get the next event, or `undefined` once the track ends. */
22
+ next(): Promise<Event<T> | undefined>;
23
+ /** Iterate over events until the track ends. */
24
+ [Symbol.asyncIterator](): AsyncIterator<Event<T>>;
25
+ }
26
+ //# sourceMappingURL=consumer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/window/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC,OAAO,EAAE,KAAK,cAAc,EAAW,KAAK,KAAK,EAAc,MAAM,cAAc,CAAC;AAEpF;;;;;;;;;;;;GAYG;AACH,qBAAa,QAAQ,CAAC,CAAC;;IAQtB,YAAY,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAE,cAAmB,EAGnE;IAED,yDAAyD;IACzD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,8DAA8D;IAC9D,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAMpC;IA4BD,gDAAgD;IACzC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAMvD;CACD"}
@@ -0,0 +1,74 @@
1
+ /* @ts-self-types="./consumer.d.ts" */
2
+ import { Decoder } from "./decoder.js";
3
+ /**
4
+ * Consumes a sliding window of JSON records from a track, yielding one event per change.
5
+ *
6
+ * A {@link Decoder} that owns its track: it reads groups, starts a cold DEFLATE window at each
7
+ * boundary, and turns each group's header into just the changes this reader has not been told about.
8
+ * When something else already owns the track, use the {@link Decoder} directly.
9
+ *
10
+ * Group rolls never surface. A publisher rolls for compression's sake, and a header restating the
11
+ * window yields nothing for records already delivered, so this reads as one continuous stream of
12
+ * {@link Event}s regardless of how the publisher framed them.
13
+ *
14
+ * @public
15
+ */
16
+ export class Consumer {
17
+ #track;
18
+ #decoder;
19
+ #group;
20
+ #codec;
21
+ #reading = false;
22
+ constructor(track, config = {}) {
23
+ this.#track = track;
24
+ this.#decoder = new Decoder(config);
25
+ }
26
+ /** Absolute index of the oldest record in the window. */
27
+ get offset() {
28
+ return this.#decoder.offset;
29
+ }
30
+ /** Get the next event, or `undefined` once the track ends. */
31
+ next() {
32
+ if (this.#reading)
33
+ throw new Error("multiple calls to next not supported");
34
+ this.#reading = true;
35
+ return this.#read().finally(() => {
36
+ this.#reading = false;
37
+ });
38
+ }
39
+ async #read() {
40
+ for (;;) {
41
+ // Drain what the frames already decoded produced before reading more.
42
+ const event = this.#decoder.next();
43
+ if (event)
44
+ return event;
45
+ if (!this.#group) {
46
+ this.#group = await this.#track.nextGroup();
47
+ if (!this.#group)
48
+ return undefined;
49
+ this.#codec = this.#decoder.group();
50
+ }
51
+ const frame = await this.#group.readFrame();
52
+ if (frame === undefined) {
53
+ // This group is exhausted. Wait for a later one, which restates the window; the stream
54
+ // ends only when the track does.
55
+ this.#group = undefined;
56
+ this.#codec = undefined;
57
+ continue;
58
+ }
59
+ if (!this.#codec)
60
+ throw new Error("an open MoQ group has a window codec");
61
+ this.#codec.decode(frame.payload);
62
+ }
63
+ }
64
+ /** Iterate over events until the track ends. */
65
+ async *[Symbol.asyncIterator]() {
66
+ for (;;) {
67
+ const event = await this.next();
68
+ if (event === undefined)
69
+ return;
70
+ yield event;
71
+ }
72
+ }
73
+ }
74
+ //# sourceMappingURL=consumer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/window/consumer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuB,OAAO,EAA0B,MAAM,cAAc,CAAC;AAEpF;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,QAAQ;IACpB,MAAM,CAAuB;IAC7B,QAAQ,CAAa;IAErB,MAAM,CAAsB;IAC5B,MAAM,CAAS;IACf,QAAQ,GAAG,KAAK,CAAC;IAEjB,YAAY,KAA2B,EAAE,MAAM,GAAmB,EAAE;QACnE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED,yDAAyD;IACzD,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,8DAA8D;IAC9D,IAAI;QACH,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAChC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACvB,CAAC,CAAC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,KAAK;QACV,SAAS,CAAC;YACT,sEAAsE;YACtE,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC;YAExB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAClB,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBAC5C,IAAI,CAAC,IAAI,CAAC,MAAM;oBAAE,OAAO,SAAS,CAAC;gBACnC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACrC,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YAC5C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACzB,uFAAuF;gBACvF,iCAAiC;gBACjC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;gBACxB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;gBACxB,SAAS;YACV,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;YAC1E,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;IACF,CAAC;IAED,gDAAgD;IAChD,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QAC5B,SAAS,CAAC;YACT,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;YAChC,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO;YAChC,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC;CACD","sourcesContent":["import type * as Moq from \"@moq/net\";\n\nimport { type ConsumerConfig, Decoder, type Event, type Group } from \"./decoder.ts\";\n\n/**\n * Consumes a sliding window of JSON records from a track, yielding one event per change.\n *\n * A {@link Decoder} that owns its track: it reads groups, starts a cold DEFLATE window at each\n * boundary, and turns each group's header into just the changes this reader has not been told about.\n * When something else already owns the track, use the {@link Decoder} directly.\n *\n * Group rolls never surface. A publisher rolls for compression's sake, and a header restating the\n * window yields nothing for records already delivered, so this reads as one continuous stream of\n * {@link Event}s regardless of how the publisher framed them.\n *\n * @public\n */\nexport class Consumer<T> {\n\t#track: Moq.Track.Subscriber;\n\t#decoder: Decoder<T>;\n\n\t#group?: Moq.Group.Consumer;\n\t#codec?: Group;\n\t#reading = false;\n\n\tconstructor(track: Moq.Track.Subscriber, config: ConsumerConfig = {}) {\n\t\tthis.#track = track;\n\t\tthis.#decoder = new Decoder(config);\n\t}\n\n\t/** Absolute index of the oldest record in the window. */\n\tget offset(): number {\n\t\treturn this.#decoder.offset;\n\t}\n\n\t/** Get the next event, or `undefined` once the track ends. */\n\tnext(): Promise<Event<T> | undefined> {\n\t\tif (this.#reading) throw new Error(\"multiple calls to next not supported\");\n\t\tthis.#reading = true;\n\t\treturn this.#read().finally(() => {\n\t\t\tthis.#reading = false;\n\t\t});\n\t}\n\n\tasync #read(): Promise<Event<T> | undefined> {\n\t\tfor (;;) {\n\t\t\t// Drain what the frames already decoded produced before reading more.\n\t\t\tconst event = this.#decoder.next();\n\t\t\tif (event) return event;\n\n\t\t\tif (!this.#group) {\n\t\t\t\tthis.#group = await this.#track.nextGroup();\n\t\t\t\tif (!this.#group) return undefined;\n\t\t\t\tthis.#codec = this.#decoder.group();\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// This group is exhausted. Wait for a later one, which restates the window; the stream\n\t\t\t\t// ends only when the track does.\n\t\t\t\tthis.#group = undefined;\n\t\t\t\tthis.#codec = undefined;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!this.#codec) throw new Error(\"an open MoQ group has a window codec\");\n\t\t\tthis.#codec.decode(frame.payload);\n\t\t}\n\t}\n\n\t/** Iterate over events until the track ends. */\n\tasync *[Symbol.asyncIterator](): AsyncIterator<Event<T>> {\n\t\tfor (;;) {\n\t\t\tconst event = await this.next();\n\t\t\tif (event === undefined) return;\n\t\t\tyield event;\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /** Options for a {@link Decoder}, and so for the {@link Consumer} wrapping one. */
2
+ export interface ConsumerConfig {
3
+ /** Read frames written with `ProducerConfig.compression` on. Defaults to `false`. */
4
+ compression?: boolean;
5
+ }
6
+ /**
7
+ * One change to the window, as the consumer sees it.
8
+ *
9
+ * A record is `push`ed when it first reaches this consumer. Contiguous spans are `pop`ped when they
10
+ * leave the window or `skip`ped when they were dropped before this consumer saw them.
11
+ */
12
+ export type Event<T> = {
13
+ push: {
14
+ index: number;
15
+ value: T;
16
+ };
17
+ } | {
18
+ pop: Span;
19
+ } | {
20
+ skip: Span;
21
+ };
22
+ /** A half-open range of absolute record indices. */
23
+ export interface Span {
24
+ /** First index in the span. */
25
+ start: number;
26
+ /** One past the last index in the span. */
27
+ end: number;
28
+ }
29
+ /** Decodes the frames in one MoQ group, requiring a header at frame zero. */
30
+ export interface Group {
31
+ /** Decode the next frame in this group. */
32
+ decode(payload: Uint8Array): void;
33
+ }
34
+ /**
35
+ * Reconstructs window events from frame payloads.
36
+ *
37
+ * The track-free core of {@link Consumer}. It tracks indices, not contents: it knows where the
38
+ * window starts and how far it has delivered, which is all it needs to turn a header into the
39
+ * pushes, pops, and skips the reader has not already been told about.
40
+ *
41
+ * Group rolls are invisible here on purpose. A header restates the window, and this decoder emits
42
+ * only what is new, so a reader sees one continuous stream of edits no matter how often the
43
+ * publisher rolled for compression's sake.
44
+ *
45
+ * @public
46
+ */
47
+ export declare class Decoder<T> {
48
+ #private;
49
+ constructor(config?: ConsumerConfig);
50
+ /** Create the decoder for one MoQ group. */
51
+ group(): Group;
52
+ /** Absolute index of the oldest record in the window. */
53
+ get offset(): number;
54
+ /** Take the next event produced by the frames decoded so far. */
55
+ next(): Event<T> | undefined;
56
+ }
57
+ //# sourceMappingURL=decoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../src/window/decoder.ts"],"names":[],"mappings":"AAgBA,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC9B,qFAAqF;IACrF,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IAAE,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAI9F,oDAAoD;AACpD,MAAM,WAAW,IAAI;IACpB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IAEd,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,6EAA6E;AAC7E,MAAM,WAAW,KAAK;IACrB,2CAA2C;IAC3C,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,OAAO,CAAC,CAAC;;IAarB,YAAY,MAAM,GAAE,cAAmB,EAEtC;IAED,4CAA4C;IAC5C,KAAK,IAAI,KAAK,CAkCb;IAED,yDAAyD;IACzD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,iEAAiE;IACjE,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAgB3B;CA4ED"}
@@ -0,0 +1,169 @@
1
+ /* @ts-self-types="./decoder.d.ts" */
2
+ import { Decoder as Flate } from "@norskvideo/moq-flate";
3
+ function object(value, label) {
4
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
5
+ throw new Error(`${label} must be an object`);
6
+ }
7
+ return value;
8
+ }
9
+ function index(value, label) {
10
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
11
+ throw new Error(`${label} must be a nonnegative safe integer`);
12
+ }
13
+ return value;
14
+ }
15
+ /**
16
+ * Reconstructs window events from frame payloads.
17
+ *
18
+ * The track-free core of {@link Consumer}. It tracks indices, not contents: it knows where the
19
+ * window starts and how far it has delivered, which is all it needs to turn a header into the
20
+ * pushes, pops, and skips the reader has not already been told about.
21
+ *
22
+ * Group rolls are invisible here on purpose. A header restates the window, and this decoder emits
23
+ * only what is new, so a reader sees one continuous stream of edits no matter how often the
24
+ * publisher rolled for compression's sake.
25
+ *
26
+ * @public
27
+ */
28
+ export class Decoder {
29
+ #compress;
30
+ #generation = 0;
31
+ // Absolute index of the window's front, once a group header has positioned us.
32
+ #front = 0;
33
+ #len = 0;
34
+ // Next index to deliver, or undefined before the first header. A fresh consumer adopts the first
35
+ // header's offset rather than skipping everything that came before it.
36
+ #delivered;
37
+ #events = [];
38
+ #nextEvent = 0;
39
+ constructor(config = {}) {
40
+ this.#compress = config.compression ?? false;
41
+ }
42
+ /** Create the decoder for one MoQ group. */
43
+ group() {
44
+ const generation = ++this.#generation;
45
+ let flate;
46
+ let positioned = false;
47
+ return {
48
+ decode: (payload) => {
49
+ if (generation !== this.#generation)
50
+ throw new Error("stale window group");
51
+ if (this.#compress)
52
+ flate ??= new Flate();
53
+ const bytes = flate ? flate.frame(payload) : payload;
54
+ const frame = object(JSON.parse(new TextDecoder().decode(bytes)), "window frame");
55
+ if (!positioned) {
56
+ if (!Array.isArray(frame.records))
57
+ throw new Error("window header records must be an array");
58
+ this.#applyHeader(index(frame.offset, "window offset"), frame.records);
59
+ positioned = true;
60
+ return;
61
+ }
62
+ const keys = Object.keys(frame);
63
+ if (keys.length !== 1)
64
+ throw new Error("window op must contain exactly one operation");
65
+ switch (keys[0]) {
66
+ case "push":
67
+ this.#applyPush(frame.push);
68
+ break;
69
+ case "pop":
70
+ this.#applyPop(index(frame.pop, "window pop count"));
71
+ break;
72
+ default:
73
+ throw new Error("unrecognized window op");
74
+ }
75
+ },
76
+ };
77
+ }
78
+ /** Absolute index of the oldest record in the window. */
79
+ get offset() {
80
+ return this.#front;
81
+ }
82
+ /** Take the next event produced by the frames decoded so far. */
83
+ next() {
84
+ const queued = this.#events[this.#nextEvent];
85
+ if (!queued) {
86
+ this.#clearEvents();
87
+ return undefined;
88
+ }
89
+ if ("records" in queued) {
90
+ const next = queued.next++;
91
+ const event = { push: { index: queued.offset + next, value: queued.records[next] } };
92
+ if (queued.next >= queued.records.length)
93
+ this.#advanceEvent();
94
+ return event;
95
+ }
96
+ this.#advanceEvent();
97
+ return queued;
98
+ }
99
+ #advanceEvent() {
100
+ this.#nextEvent += 1;
101
+ if (this.#nextEvent >= this.#events.length)
102
+ this.#clearEvents();
103
+ }
104
+ #clearEvents() {
105
+ this.#events = [];
106
+ this.#nextEvent = 0;
107
+ }
108
+ /** The window is exactly these records. Report what this reader missed, then what is new. */
109
+ #applyHeader(offset, records) {
110
+ const end = offset + records.length;
111
+ if (!Number.isSafeInteger(end))
112
+ throw new Error("window range exceeds the safe integer range");
113
+ let delivered = this.#delivered;
114
+ if (delivered === undefined) {
115
+ // First position: adopt the publisher's offset rather than skipping all of history.
116
+ delivered = offset;
117
+ }
118
+ else {
119
+ if (offset < this.#front || end < delivered)
120
+ throw new Error("window header moved backwards");
121
+ // Records that left the window while we were away. Those we had delivered are pops; those we
122
+ // never saw are skips. Keep each gap compact: the offset is untrusted and may jump by far
123
+ // more indices than a consumer could materialize individually.
124
+ this.#range("pop", this.#front, Math.min(delivered, offset));
125
+ this.#range("skip", delivered, offset);
126
+ }
127
+ // Keep the unseen tail as one batch and materialize each push only when the caller asks for it.
128
+ const next = Math.max(delivered - offset, 0);
129
+ if (next < records.length) {
130
+ this.#events.push({ offset, records, next });
131
+ }
132
+ this.#front = offset;
133
+ this.#len = end - offset;
134
+ this.#delivered = Math.max(delivered, end);
135
+ }
136
+ /** One record joined the back. */
137
+ #applyPush(value) {
138
+ if (this.#delivered === undefined)
139
+ throw new Error("window op before group header");
140
+ const index = this.#front + this.#len;
141
+ if (!Number.isSafeInteger(index + 1))
142
+ throw new Error("window range exceeds the safe integer range");
143
+ this.#len += 1;
144
+ if (index >= this.#delivered) {
145
+ this.#events.push({ push: { index, value } });
146
+ this.#delivered = index + 1;
147
+ }
148
+ }
149
+ /** Records left the front. */
150
+ #applyPop(count) {
151
+ if (this.#delivered === undefined)
152
+ throw new Error("window op before group header");
153
+ if (count > this.#len) {
154
+ throw new Error(`pop of ${count} exceeds the ${this.#len} record(s) in the window`);
155
+ }
156
+ const end = this.#front + count;
157
+ this.#range("pop", this.#front, Math.min(this.#delivered, end));
158
+ this.#range("skip", Math.max(this.#delivered, this.#front), end);
159
+ this.#front = end;
160
+ this.#len -= count;
161
+ this.#delivered = Math.max(this.#delivered, this.#front);
162
+ }
163
+ #range(kind, start, end) {
164
+ if (start >= end)
165
+ return;
166
+ this.#events.push(kind === "pop" ? { pop: { start, end } } : { skip: { start, end } });
167
+ }
168
+ }
169
+ //# sourceMappingURL=decoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder.js","sourceRoot":"","sources":["../../src/window/decoder.ts"],"names":[],"mappings":"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","sourcesContent":["import { Decoder as Flate } from \"@moq/flate\";\n\nfunction object(value: unknown, label: string): Record<string, unknown> {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new Error(`${label} must be an object`);\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction index(value: unknown, label: string): number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 0) {\n\t\tthrow new Error(`${label} must be a nonnegative safe integer`);\n\t}\n\treturn value;\n}\n\n/** Options for a {@link Decoder}, and so for the {@link Consumer} wrapping one. */\nexport interface ConsumerConfig {\n\t/** Read frames written with `ProducerConfig.compression` on. Defaults to `false`. */\n\tcompression?: boolean;\n}\n\n/**\n * One change to the window, as the consumer sees it.\n *\n * A record is `push`ed when it first reaches this consumer. Contiguous spans are `pop`ped when they\n * leave the window or `skip`ped when they were dropped before this consumer saw them.\n */\nexport type Event<T> = { push: { index: number; value: T } } | { pop: Span } | { skip: Span };\n\ntype Queued<T> = Event<T> | { offset: number; records: T[]; next: number };\n\n/** A half-open range of absolute record indices. */\nexport interface Span {\n\t/** First index in the span. */\n\tstart: number;\n\n\t/** One past the last index in the span. */\n\tend: number;\n}\n\n/** Decodes the frames in one MoQ group, requiring a header at frame zero. */\nexport interface Group {\n\t/** Decode the next frame in this group. */\n\tdecode(payload: Uint8Array): void;\n}\n\n/**\n * Reconstructs window events from frame payloads.\n *\n * The track-free core of {@link Consumer}. It tracks indices, not contents: it knows where the\n * window starts and how far it has delivered, which is all it needs to turn a header into the\n * pushes, pops, and skips the reader has not already been told about.\n *\n * Group rolls are invisible here on purpose. A header restates the window, and this decoder emits\n * only what is new, so a reader sees one continuous stream of edits no matter how often the\n * publisher rolled for compression's sake.\n *\n * @public\n */\nexport class Decoder<T> {\n\t#compress: boolean;\n\t#generation = 0;\n\n\t// Absolute index of the window's front, once a group header has positioned us.\n\t#front = 0;\n\t#len = 0;\n\t// Next index to deliver, or undefined before the first header. A fresh consumer adopts the first\n\t// header's offset rather than skipping everything that came before it.\n\t#delivered?: number;\n\t#events: Queued<T>[] = [];\n\t#nextEvent = 0;\n\n\tconstructor(config: ConsumerConfig = {}) {\n\t\tthis.#compress = config.compression ?? false;\n\t}\n\n\t/** Create the decoder for one MoQ group. */\n\tgroup(): Group {\n\t\tconst generation = ++this.#generation;\n\t\tlet flate: Flate | undefined;\n\t\tlet positioned = false;\n\n\t\treturn {\n\t\t\tdecode: (payload) => {\n\t\t\t\tif (generation !== this.#generation) throw new Error(\"stale window group\");\n\t\t\t\tif (this.#compress) flate ??= new Flate();\n\t\t\t\tconst bytes = flate ? flate.frame(payload) : payload;\n\t\t\t\tconst frame = object(JSON.parse(new TextDecoder().decode(bytes)) as unknown, \"window frame\");\n\n\t\t\t\tif (!positioned) {\n\t\t\t\t\tif (!Array.isArray(frame.records)) throw new Error(\"window header records must be an array\");\n\t\t\t\t\tthis.#applyHeader(index(frame.offset, \"window offset\"), frame.records as T[]);\n\t\t\t\t\tpositioned = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst keys = Object.keys(frame);\n\t\t\t\tif (keys.length !== 1) throw new Error(\"window op must contain exactly one operation\");\n\n\t\t\t\tswitch (keys[0]) {\n\t\t\t\t\tcase \"push\":\n\t\t\t\t\t\tthis.#applyPush(frame.push as T);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"pop\":\n\t\t\t\t\t\tthis.#applyPop(index(frame.pop, \"window pop count\"));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthrow new Error(\"unrecognized window op\");\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Absolute index of the oldest record in the window. */\n\tget offset(): number {\n\t\treturn this.#front;\n\t}\n\n\t/** Take the next event produced by the frames decoded so far. */\n\tnext(): Event<T> | undefined {\n\t\tconst queued = this.#events[this.#nextEvent];\n\t\tif (!queued) {\n\t\t\tthis.#clearEvents();\n\t\t\treturn undefined;\n\t\t}\n\n\t\tif (\"records\" in queued) {\n\t\t\tconst next = queued.next++;\n\t\t\tconst event = { push: { index: queued.offset + next, value: queued.records[next] as T } };\n\t\t\tif (queued.next >= queued.records.length) this.#advanceEvent();\n\t\t\treturn event;\n\t\t}\n\n\t\tthis.#advanceEvent();\n\t\treturn queued;\n\t}\n\n\t#advanceEvent(): void {\n\t\tthis.#nextEvent += 1;\n\t\tif (this.#nextEvent >= this.#events.length) this.#clearEvents();\n\t}\n\n\t#clearEvents(): void {\n\t\tthis.#events = [];\n\t\tthis.#nextEvent = 0;\n\t}\n\n\t/** The window is exactly these records. Report what this reader missed, then what is new. */\n\t#applyHeader(offset: number, records: T[]): void {\n\t\tconst end = offset + records.length;\n\t\tif (!Number.isSafeInteger(end)) throw new Error(\"window range exceeds the safe integer range\");\n\t\tlet delivered = this.#delivered;\n\n\t\tif (delivered === undefined) {\n\t\t\t// First position: adopt the publisher's offset rather than skipping all of history.\n\t\t\tdelivered = offset;\n\t\t} else {\n\t\t\tif (offset < this.#front || end < delivered) throw new Error(\"window header moved backwards\");\n\n\t\t\t// Records that left the window while we were away. Those we had delivered are pops; those we\n\t\t\t// never saw are skips. Keep each gap compact: the offset is untrusted and may jump by far\n\t\t\t// more indices than a consumer could materialize individually.\n\t\t\tthis.#range(\"pop\", this.#front, Math.min(delivered, offset));\n\t\t\tthis.#range(\"skip\", delivered, offset);\n\t\t}\n\n\t\t// Keep the unseen tail as one batch and materialize each push only when the caller asks for it.\n\t\tconst next = Math.max(delivered - offset, 0);\n\t\tif (next < records.length) {\n\t\t\tthis.#events.push({ offset, records, next });\n\t\t}\n\n\t\tthis.#front = offset;\n\t\tthis.#len = end - offset;\n\t\tthis.#delivered = Math.max(delivered, end);\n\t}\n\n\t/** One record joined the back. */\n\t#applyPush(value: T): void {\n\t\tif (this.#delivered === undefined) throw new Error(\"window op before group header\");\n\n\t\tconst index = this.#front + this.#len;\n\t\tif (!Number.isSafeInteger(index + 1)) throw new Error(\"window range exceeds the safe integer range\");\n\t\tthis.#len += 1;\n\n\t\tif (index >= this.#delivered) {\n\t\t\tthis.#events.push({ push: { index, value } });\n\t\t\tthis.#delivered = index + 1;\n\t\t}\n\t}\n\n\t/** Records left the front. */\n\t#applyPop(count: number): void {\n\t\tif (this.#delivered === undefined) throw new Error(\"window op before group header\");\n\t\tif (count > this.#len) {\n\t\t\tthrow new Error(`pop of ${count} exceeds the ${this.#len} record(s) in the window`);\n\t\t}\n\n\t\tconst end = this.#front + count;\n\t\tthis.#range(\"pop\", this.#front, Math.min(this.#delivered, end));\n\t\tthis.#range(\"skip\", Math.max(this.#delivered, this.#front), end);\n\n\t\tthis.#front = end;\n\t\tthis.#len -= count;\n\t\tthis.#delivered = Math.max(this.#delivered, this.#front);\n\t}\n\n\t#range(kind: \"pop\" | \"skip\", start: number, end: number): void {\n\t\tif (start >= end) return;\n\t\tthis.#events.push(kind === \"pop\" ? { pop: { start, end } } : { skip: { start, end } });\n\t}\n}\n"]}
@@ -0,0 +1,72 @@
1
+ /** Op ratio used when {@link ProducerConfig.opRatio} is left unset. */
2
+ export declare const DEFAULT_OP_RATIO = 8;
3
+ /** Options shared by an {@link Encoder} and the {@link Producer} that wraps one. */
4
+ export interface ProducerConfig {
5
+ /**
6
+ * How much the ops in a group may cost before a fresh group is emitted.
7
+ *
8
+ * A new group opens once the pushes and pops *already written* exceed `opRatio` times the size of
9
+ * the group's header frame. The pending op is excluded, so the one that tips the group over budget
10
+ * still lands. `0` disables ops entirely, so every edit is its own single-frame group.
11
+ *
12
+ * The window's counterpart to Snapshot's `deltaRatio`. Defaults to `8`.
13
+ */
14
+ opRatio?: number;
15
+ /**
16
+ * Compress each group as one sync-flushed `deflate-raw` stream, so every op reuses the header and
17
+ * the ops before it as context. A {@link Decoder} reading the frames must set the same flag.
18
+ * Defaults to `false`.
19
+ */
20
+ compression?: boolean;
21
+ }
22
+ /** One encoded frame, and the group boundary it implies. */
23
+ export interface Encoded {
24
+ /** The frame payload. */
25
+ payload: Uint8Array;
26
+ /**
27
+ * Whether this frame is a group header, which must open a new group.
28
+ *
29
+ * The encoder decides this, never the caller: the op budget and the frame cap force a new group
30
+ * independently of which edit was requested.
31
+ */
32
+ keyframe: boolean;
33
+ }
34
+ /**
35
+ * An encoded frame the caller has not yet acknowledged writing.
36
+ *
37
+ * Write the frame, then {@link commit}. The edit is staged until commit, so leaving the frame
38
+ * uncommitted keeps the window unchanged and makes the next frame open a new group.
39
+ */
40
+ export interface Pending extends Encoded {
41
+ /** Acknowledge that the frame reached the wire, applying its edit to the retained window. */
42
+ commit(): void;
43
+ }
44
+ /**
45
+ * Encodes window edits into frame payloads, deciding where the group boundaries fall.
46
+ *
47
+ * The track-free core of {@link Producer}. It owns the retained window, so it can restate it
48
+ * whenever a group rolls; that restatement is the whole point of the mode, and is what an
49
+ * append-only log cannot do.
50
+ *
51
+ * @public
52
+ */
53
+ export declare class Encoder<T> {
54
+ #private;
55
+ constructor(config?: ProducerConfig);
56
+ /** The retained window, oldest first. */
57
+ get window(): T[];
58
+ /** Absolute index of the oldest retained record. */
59
+ get offset(): number;
60
+ /** Absolute index the next pushed record will take. */
61
+ get end(): number;
62
+ /** Append one record to the back of the window. */
63
+ push(value: T): Pending;
64
+ /**
65
+ * Drop `count` records from the front of the window.
66
+ *
67
+ * Returns `undefined` when there is nothing to drop, so a caller can trim unconditionally.
68
+ * Clamped to what the window holds.
69
+ */
70
+ pop(count: number): Pending | undefined;
71
+ }
72
+ //# sourceMappingURL=encoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/window/encoder.ts"],"names":[],"mappings":"AAMA,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAIlC,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC9B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,4DAA4D;AAC5D,MAAM,WAAW,OAAO;IACvB,yBAAyB;IACzB,OAAO,EAAE,UAAU,CAAC;IAEpB;;;;;OAKG;IACH,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAQ,SAAQ,OAAO;IACvC,6FAA6F;IAC7F,MAAM,IAAI,IAAI,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBAAa,OAAO,CAAC,CAAC;;IAwBrB,YAAY,MAAM,GAAE,cAAmB,EAMtC;IAED,yCAAyC;IACzC,IAAI,MAAM,IAAI,CAAC,EAAE,CAEhB;IAED,oDAAoD;IACpD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,uDAAuD;IACvD,IAAI,GAAG,IAAI,MAAM,CAEhB;IAaD,mDAAmD;IACnD,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAmBtB;IAED;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAkBtC;CAiFD"}