@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.
- package/index.d.ts +13 -3
- package/index.d.ts.map +1 -1
- package/index.js +14 -3
- package/index.js.map +1 -1
- package/package.json +6 -5
- package/snapshot/consumer.d.ts +7 -5
- package/snapshot/consumer.d.ts.map +1 -1
- package/snapshot/consumer.js +31 -35
- package/snapshot/consumer.js.map +1 -1
- package/snapshot/decoder.d.ts +46 -0
- package/snapshot/decoder.d.ts.map +1 -0
- package/snapshot/decoder.js +76 -0
- package/snapshot/decoder.js.map +1 -0
- package/snapshot/encoder.d.ts +96 -0
- package/snapshot/encoder.d.ts.map +1 -0
- package/snapshot/encoder.js +196 -0
- package/snapshot/encoder.js.map +1 -0
- package/snapshot/index.d.ts +14 -1
- package/snapshot/index.d.ts.map +1 -1
- package/snapshot/index.js +13 -0
- package/snapshot/index.js.map +1 -1
- package/snapshot/producer.d.ts +22 -39
- package/snapshot/producer.d.ts.map +1 -1
- package/snapshot/producer.js +69 -185
- package/snapshot/producer.js.map +1 -1
- package/stream/consumer.d.ts +17 -0
- package/stream/consumer.d.ts.map +1 -0
- package/stream/consumer.js +46 -0
- package/stream/consumer.js.map +1 -0
- package/stream/decoder.d.ts +21 -0
- package/stream/decoder.d.ts.map +1 -0
- package/stream/decoder.js +28 -0
- package/stream/decoder.js.map +1 -0
- package/stream/encoder.d.ts +62 -0
- package/stream/encoder.d.ts.map +1 -0
- package/stream/encoder.js +78 -0
- package/stream/encoder.js.map +1 -0
- package/stream/index.d.ts +23 -0
- package/stream/index.d.ts.map +1 -0
- package/stream/index.js +24 -0
- package/stream/index.js.map +1 -0
- package/stream/producer.d.ts +18 -0
- package/stream/producer.d.ts.map +1 -0
- package/stream/producer.js +49 -0
- package/stream/producer.js.map +1 -0
- package/window/consumer.d.ts +26 -0
- package/window/consumer.d.ts.map +1 -0
- package/window/consumer.js +74 -0
- package/window/consumer.js.map +1 -0
- package/window/decoder.d.ts +57 -0
- package/window/decoder.d.ts.map +1 -0
- package/window/decoder.js +169 -0
- package/window/decoder.js.map +1 -0
- package/window/encoder.d.ts +72 -0
- package/window/encoder.d.ts.map +1 -0
- package/window/encoder.js +183 -0
- package/window/encoder.js.map +1 -0
- package/window/index.d.ts +39 -0
- package/window/index.d.ts.map +1 -0
- package/window/index.js +40 -0
- package/window/index.js.map +1 -0
- package/window/producer.d.ts +32 -0
- package/window/producer.d.ts.map +1 -0
- package/window/producer.js +90 -0
- package/window/producer.js.map +1 -0
- package/diff.test.d.ts +0 -2
- package/diff.test.d.ts.map +0 -1
- package/diff.test.js +0 -60
- package/diff.test.js.map +0 -1
- package/snapshot/compression.test.d.ts +0 -2
- package/snapshot/compression.test.d.ts.map +0 -1
- package/snapshot/compression.test.js +0 -132
- package/snapshot/compression.test.js.map +0 -1
- package/snapshot/producer.test.d.ts +0 -2
- package/snapshot/producer.test.d.ts.map +0 -1
- package/snapshot/producer.test.js +0 -75
- package/snapshot/producer.test.js.map +0 -1
- package/snapshot/snapshot.test.d.ts +0 -2
- package/snapshot/snapshot.test.d.ts.map +0 -1
- package/snapshot/snapshot.test.js +0 -185
- package/snapshot/snapshot.test.js.map +0 -1
- package/stream.d.ts +0 -54
- package/stream.d.ts.map +0 -1
- package/stream.js +0 -92
- package/stream.js.map +0 -1
- package/stream.test.d.ts +0 -2
- package/stream.test.d.ts.map +0 -1
- package/stream.test.js +0 -64
- package/stream.test.js.map +0 -1
- package/vectors.test.d.ts +0 -2
- package/vectors.test.d.ts.map +0 -1
- package/vectors.test.js +0 -18
- package/vectors.test.js.map +0 -1
package/index.d.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSON publishing over MoQ tracks, in
|
|
2
|
+
* JSON publishing over MoQ tracks, in three modes:
|
|
3
3
|
*
|
|
4
4
|
* - {@link Snapshot}: **lossy**. One JSON value updated over time; a consumer only gets the most
|
|
5
5
|
* recent value. Intermediate updates are collapsed and older groups are dropped.
|
|
6
6
|
* - {@link Stream}: **lossless**. An ordered append-log of self-contained records; every record
|
|
7
7
|
* is preserved and delivered in order, nothing is ever superseded.
|
|
8
|
+
* - {@link Window}: a **bounded** run of records, appended to the back and dropped from the front,
|
|
9
|
+
* that a reader can join at any point.
|
|
8
10
|
*
|
|
9
11
|
* Pick {@link Snapshot} when consumers care about "what is the value now" (a catalog, a status
|
|
10
|
-
* document)
|
|
12
|
+
* document), {@link Stream} when they care about every record of an unbounded log, and
|
|
13
|
+
* {@link Window} when the publisher retires old records and a late reader should start from what is
|
|
14
|
+
* still retained (a media timeline).
|
|
15
|
+
*
|
|
16
|
+
* Each mode comes in two layers. `Producer`/`Consumer` own a track and manage its groups.
|
|
17
|
+
* `Encoder`/`Decoder` are the same logic without the track: values in, frame payloads out (and
|
|
18
|
+
* back), with the encoder saying where the group boundaries fall. Reach for the codec layer when
|
|
19
|
+
* something else already owns the track.
|
|
11
20
|
*
|
|
12
21
|
* @module
|
|
13
22
|
*/
|
|
14
23
|
export { type Diff, deepEqual, diff, merge } from "./diff";
|
|
15
24
|
export * as Snapshot from "./snapshot/index";
|
|
16
|
-
export * as Stream from "./stream";
|
|
25
|
+
export * as Stream from "./stream/index";
|
|
26
|
+
export * as Window from "./window/index";
|
|
17
27
|
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC"}
|
package/index.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
/* @ts-self-types="./index.d.ts" */
|
|
2
2
|
/**
|
|
3
|
-
* JSON publishing over MoQ tracks, in
|
|
3
|
+
* JSON publishing over MoQ tracks, in three modes:
|
|
4
4
|
*
|
|
5
5
|
* - {@link Snapshot}: **lossy**. One JSON value updated over time; a consumer only gets the most
|
|
6
6
|
* recent value. Intermediate updates are collapsed and older groups are dropped.
|
|
7
7
|
* - {@link Stream}: **lossless**. An ordered append-log of self-contained records; every record
|
|
8
8
|
* is preserved and delivered in order, nothing is ever superseded.
|
|
9
|
+
* - {@link Window}: a **bounded** run of records, appended to the back and dropped from the front,
|
|
10
|
+
* that a reader can join at any point.
|
|
9
11
|
*
|
|
10
12
|
* Pick {@link Snapshot} when consumers care about "what is the value now" (a catalog, a status
|
|
11
|
-
* document)
|
|
13
|
+
* document), {@link Stream} when they care about every record of an unbounded log, and
|
|
14
|
+
* {@link Window} when the publisher retires old records and a late reader should start from what is
|
|
15
|
+
* still retained (a media timeline).
|
|
16
|
+
*
|
|
17
|
+
* Each mode comes in two layers. `Producer`/`Consumer` own a track and manage its groups.
|
|
18
|
+
* `Encoder`/`Decoder` are the same logic without the track: values in, frame payloads out (and
|
|
19
|
+
* back), with the encoder saying where the group boundaries fall. Reach for the codec layer when
|
|
20
|
+
* something else already owns the track.
|
|
12
21
|
*
|
|
13
22
|
* @module
|
|
14
23
|
*/
|
|
15
24
|
export { deepEqual, diff, merge } from "./diff.js";
|
|
16
25
|
import * as Snapshot_1 from "./snapshot/index.js";
|
|
17
26
|
export { Snapshot_1 as Snapshot };
|
|
18
|
-
import * as Stream_1 from "./stream.js";
|
|
27
|
+
import * as Stream_1 from "./stream/index.js";
|
|
19
28
|
export { Stream_1 as Stream };
|
|
29
|
+
import * as Window_1 from "./window/index.js";
|
|
30
|
+
export { Window_1 as Window };
|
|
20
31
|
//# sourceMappingURL=index.js.map
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAa,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;4BACpC,qBAAqB;uBAAnC,QAAQ;0BACI,mBAAmB;qBAA/B,MAAM;0BACM,mBAAmB;qBAA/B,MAAM","sourcesContent":["/**\n * JSON publishing over MoQ tracks, in three modes:\n *\n * - {@link Snapshot}: **lossy**. One JSON value updated over time; a consumer only gets the most\n * recent value. Intermediate updates are collapsed and older groups are dropped.\n * - {@link Stream}: **lossless**. An ordered append-log of self-contained records; every record\n * is preserved and delivered in order, nothing is ever superseded.\n * - {@link Window}: a **bounded** run of records, appended to the back and dropped from the front,\n * that a reader can join at any point.\n *\n * Pick {@link Snapshot} when consumers care about \"what is the value now\" (a catalog, a status\n * document), {@link Stream} when they care about every record of an unbounded log, and\n * {@link Window} when the publisher retires old records and a late reader should start from what is\n * still retained (a media timeline).\n *\n * Each mode comes in two layers. `Producer`/`Consumer` own a track and manage its groups.\n * `Encoder`/`Decoder` are the same logic without the track: values in, frame payloads out (and\n * back), with the encoder saying where the group boundaries fall. Reach for the codec layer when\n * something else already owns the track.\n *\n * @module\n */\n\nexport { type Diff, deepEqual, diff, merge } from \"./diff.ts\";\nexport * as Snapshot from \"./snapshot/index.ts\";\nexport * as Stream from \"./stream/index.ts\";\nexport * as Window from \"./window/index.ts\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/moq-json",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"description": "JSON publishing over MoQ tracks: snapshot/delta (RFC 7396 merge patch) objects, or append-log NDJSON streams.",
|
|
6
6
|
"license": "(MIT OR Apache-2.0)",
|
|
7
7
|
"repository": "github:moq-dev/moq",
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@norskvideo/moq-flate": "^0.1.
|
|
17
|
-
"@norskvideo/moq-net": "^0.1.
|
|
18
|
-
"@norskvideo/moq-signals": "^0.1.
|
|
16
|
+
"@norskvideo/moq-flate": "^0.1.4",
|
|
17
|
+
"@norskvideo/moq-net": "^0.1.4",
|
|
18
|
+
"@norskvideo/moq-signals": "^0.1.4"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"zod": "^4.
|
|
21
|
+
"@zod/mini": "^4.5.0",
|
|
22
|
+
"zod": "^4.5.0"
|
|
22
23
|
}
|
|
23
24
|
}
|
package/snapshot/consumer.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type * as Moq from "@norskvideo/moq-net";
|
|
2
|
-
import type { Config } from "./
|
|
2
|
+
import type { Config } from "./encoder";
|
|
3
3
|
/**
|
|
4
4
|
* Consumes a JSON value from a track, reconstructing it from snapshots and deltas.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* consumer yields each update as it arrives; a consumer that
|
|
8
|
-
* collapses the buffered backlog and yields only the latest
|
|
6
|
+
* A {@link Decoder} that owns its track: it reads groups, routes each frame by its position, and
|
|
7
|
+
* yields the reconstructed value. A live consumer yields each update as it arrives; a consumer that
|
|
8
|
+
* has fallen behind (or just joined) collapses the buffered backlog and yields only the latest
|
|
9
|
+
* value. See {@link next}. When something else already owns the track, use the {@link Decoder}
|
|
10
|
+
* directly.
|
|
9
11
|
*/
|
|
10
12
|
export declare class Consumer<T> {
|
|
11
13
|
#private;
|
|
12
|
-
constructor(track: Moq.Track, config?: Config<T>);
|
|
14
|
+
constructor(track: Moq.Track.Subscriber, config?: Config<T>);
|
|
13
15
|
/**
|
|
14
16
|
* Get the next reconstructed value, or `undefined` once the track ends.
|
|
15
17
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/snapshot/consumer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/snapshot/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAGrC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;;GAQG;AACH,qBAAa,QAAQ,CAAC,CAAC;;IAOtB,YAAY,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAE,MAAM,CAAC,CAAC,CAAM,EAG9D;IAED;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAwCnC;IAEM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,CAMhD;CAWD"}
|
package/snapshot/consumer.js
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
/* @ts-self-types="./consumer.d.ts" */
|
|
2
|
-
import { Decoder } from "
|
|
3
|
-
import { merge } from "../diff.js";
|
|
2
|
+
import { Decoder } from "./decoder.js";
|
|
4
3
|
/**
|
|
5
4
|
* Consumes a JSON value from a track, reconstructing it from snapshots and deltas.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* consumer yields each update as it arrives; a consumer that
|
|
9
|
-
* collapses the buffered backlog and yields only the latest
|
|
6
|
+
* A {@link Decoder} that owns its track: it reads groups, routes each frame by its position, and
|
|
7
|
+
* yields the reconstructed value. A live consumer yields each update as it arrives; a consumer that
|
|
8
|
+
* has fallen behind (or just joined) collapses the buffered backlog and yields only the latest
|
|
9
|
+
* value. See {@link next}. When something else already owns the track, use the {@link Decoder}
|
|
10
|
+
* directly.
|
|
10
11
|
*/
|
|
11
12
|
export class Consumer {
|
|
12
13
|
#track;
|
|
13
|
-
#schema;
|
|
14
|
-
// Whether frames are `deflate-raw` compressed. Must match the producer's {@link Config.compression}.
|
|
15
|
-
#decompress;
|
|
16
|
-
#group;
|
|
17
|
-
// Per-group DEFLATE decoder, built lazily on the first frame of a group and reset at each boundary.
|
|
18
14
|
#decoder;
|
|
19
|
-
#
|
|
15
|
+
#group;
|
|
20
16
|
#framesRead = 0;
|
|
21
17
|
constructor(track, config = {}) {
|
|
22
18
|
this.#track = track;
|
|
23
|
-
this.#
|
|
24
|
-
this.#decompress = config.compression ?? false;
|
|
19
|
+
this.#decoder = new Decoder(config);
|
|
25
20
|
}
|
|
26
21
|
/**
|
|
27
22
|
* Get the next reconstructed value, or `undefined` once the track ends.
|
|
@@ -36,31 +31,40 @@ export class Consumer {
|
|
|
36
31
|
for (;;) {
|
|
37
32
|
if (!this.#group) {
|
|
38
33
|
// Advance to the next group with a higher sequence number (skipping late arrivals).
|
|
39
|
-
this.#group = await this.#track.
|
|
34
|
+
this.#group = await this.#track.nextGroup();
|
|
40
35
|
if (!this.#group)
|
|
41
36
|
return undefined;
|
|
42
|
-
|
|
37
|
+
// The next frame is the new group's snapshot, which also restarts the decoder's window.
|
|
43
38
|
this.#framesRead = 0;
|
|
44
|
-
// Each group is its own compressed stream, so start a fresh decoder.
|
|
45
|
-
this.#decoder = undefined;
|
|
46
39
|
}
|
|
47
|
-
// Drain every frame already buffered, keeping only the latest reconstructed value
|
|
48
|
-
|
|
40
|
+
// Drain every frame already buffered, keeping only the latest reconstructed value: a late
|
|
41
|
+
// joiner (or any consumer that fell behind) catches up to the head in one step.
|
|
49
42
|
let advanced = false;
|
|
50
43
|
for (let frame = this.#group.tryReadFrame(); frame !== undefined; frame = this.#group.tryReadFrame()) {
|
|
51
|
-
|
|
44
|
+
this.#apply(frame.payload);
|
|
52
45
|
advanced = true;
|
|
53
46
|
}
|
|
54
47
|
if (advanced)
|
|
55
|
-
return
|
|
48
|
+
return this.#decoder.decode();
|
|
56
49
|
// Nothing buffered: block for the next frame (or the group's end).
|
|
57
|
-
|
|
50
|
+
let frame;
|
|
51
|
+
try {
|
|
52
|
+
frame = await this.#group.readFrame();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// The group was reset or we fell behind its eviction window. Resync from
|
|
56
|
+
// the next group, which begins with a fresh snapshot (frame 0), so no
|
|
57
|
+
// partial state is presented.
|
|
58
|
+
this.#group = undefined;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
58
61
|
if (frame === undefined) {
|
|
59
62
|
// The group is exhausted; advance to the next one.
|
|
60
63
|
this.#group = undefined;
|
|
61
64
|
continue;
|
|
62
65
|
}
|
|
63
|
-
|
|
66
|
+
this.#apply(frame.payload);
|
|
67
|
+
return this.#decoder.decode();
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
async *[Symbol.asyncIterator]() {
|
|
@@ -71,23 +75,15 @@ export class Consumer {
|
|
|
71
75
|
yield value;
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
|
-
// Frame 0 of a group is a snapshot, the rest are merge patches.
|
|
75
|
-
|
|
76
|
-
#apply(frame) {
|
|
77
|
-
let payload = frame;
|
|
78
|
-
if (this.#decompress) {
|
|
79
|
-
this.#decoder ??= new Decoder();
|
|
80
|
-
payload = this.#decoder.frame(frame);
|
|
81
|
-
}
|
|
82
|
-
const parsed = JSON.parse(new TextDecoder().decode(payload));
|
|
78
|
+
// Frame 0 of a group is a snapshot, the rest are merge patches.
|
|
79
|
+
#apply(payload) {
|
|
83
80
|
if (this.#framesRead === 0) {
|
|
84
|
-
this.#
|
|
81
|
+
this.#decoder.snapshot(payload);
|
|
85
82
|
}
|
|
86
83
|
else {
|
|
87
|
-
this.#
|
|
84
|
+
this.#decoder.delta(payload);
|
|
88
85
|
}
|
|
89
86
|
this.#framesRead += 1;
|
|
90
|
-
return this.#schema ? this.#schema.parse(this.#current) : this.#current;
|
|
91
87
|
}
|
|
92
88
|
}
|
|
93
89
|
//# sourceMappingURL=consumer.js.map
|
package/snapshot/consumer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/snapshot/consumer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/snapshot/consumer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC;;;;;;;;GAQG;AACH,MAAM,OAAO,QAAQ;IACpB,MAAM,CAAuB;IAC7B,QAAQ,CAAa;IAErB,MAAM,CAAsB;IAC5B,WAAW,GAAG,CAAC,CAAC;IAEhB,YAAY,KAA2B,EAAE,MAAM,GAAc,EAAE;QAC9D,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,IAAI;QACT,SAAS,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAClB,oFAAoF;gBACpF,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;gBAC5C,IAAI,CAAC,IAAI,CAAC,MAAM;oBAAE,OAAO,SAAS,CAAC;gBACnC,wFAAwF;gBACxF,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;YACtB,CAAC;YAED,0FAA0F;YAC1F,gFAAgF;YAChF,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,KAAK,KAAK,SAAS,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,CAAC;gBACtG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC3B,QAAQ,GAAG,IAAI,CAAC;YACjB,CAAC;YACD,IAAI,QAAQ;gBAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAE5C,mEAAmE;YACnE,IAAI,KAAkC,CAAC;YACvC,IAAI,CAAC;gBACJ,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACR,yEAAyE;gBACzE,sEAAsE;gBACtE,8BAA8B;gBAC9B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;gBACxB,SAAS;YACV,CAAC;YAED,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACzB,mDAAmD;gBACnD,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;gBACxB,SAAS;YACV,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC3B,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC/B,CAAC;IACF,CAAC;IAED,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;IAED,gEAAgE;IAChE,MAAM,CAAC,OAAmB;QACzB,IAAI,IAAI,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;IACvB,CAAC;CACD","sourcesContent":["import type * as Moq from \"@moq/net\";\n\nimport { Decoder } from \"./decoder.ts\";\nimport type { Config } from \"./encoder.ts\";\n\n/**\n * Consumes a JSON value from a track, reconstructing it from snapshots and deltas.\n *\n * A {@link Decoder} that owns its track: it reads groups, routes each frame by its position, and\n * yields the reconstructed value. A live consumer yields each update as it arrives; a consumer that\n * has fallen behind (or just joined) collapses the buffered backlog and yields only the latest\n * value. See {@link next}. When something else already owns the track, use the {@link Decoder}\n * directly.\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#framesRead = 0;\n\n\tconstructor(track: Moq.Track.Subscriber, config: Config<T> = {}) {\n\t\tthis.#track = track;\n\t\tthis.#decoder = new Decoder(config);\n\t}\n\n\t/**\n\t * Get the next reconstructed value, or `undefined` once the track ends.\n\t *\n\t * Applies every frame already buffered in the group but yields only the latest reconstructed\n\t * value: the intermediate reconstructions are stale, so a late joiner (or any consumer that has\n\t * fallen behind) catches up to the head in one step instead of replaying every superseded state.\n\t * Frames are still decoded in order (the DEFLATE window and merge patches are sequential); only\n\t * the per-frame yield is skipped.\n\t */\n\tasync next(): Promise<T | undefined> {\n\t\tfor (;;) {\n\t\t\tif (!this.#group) {\n\t\t\t\t// Advance to the next group with a higher sequence number (skipping late arrivals).\n\t\t\t\tthis.#group = await this.#track.nextGroup();\n\t\t\t\tif (!this.#group) return undefined;\n\t\t\t\t// The next frame is the new group's snapshot, which also restarts the decoder's window.\n\t\t\t\tthis.#framesRead = 0;\n\t\t\t}\n\n\t\t\t// Drain every frame already buffered, keeping only the latest reconstructed value: a late\n\t\t\t// joiner (or any consumer that fell behind) catches up to the head in one step.\n\t\t\tlet advanced = false;\n\t\t\tfor (let frame = this.#group.tryReadFrame(); frame !== undefined; frame = this.#group.tryReadFrame()) {\n\t\t\t\tthis.#apply(frame.payload);\n\t\t\t\tadvanced = true;\n\t\t\t}\n\t\t\tif (advanced) return this.#decoder.decode();\n\n\t\t\t// Nothing buffered: block for the next frame (or the group's end).\n\t\t\tlet frame: Moq.Group.Frame | undefined;\n\t\t\ttry {\n\t\t\t\tframe = await this.#group.readFrame();\n\t\t\t} catch {\n\t\t\t\t// The group was reset or we fell behind its eviction window. Resync from\n\t\t\t\t// the next group, which begins with a fresh snapshot (frame 0), so no\n\t\t\t\t// partial state is presented.\n\t\t\t\tthis.#group = undefined;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (frame === undefined) {\n\t\t\t\t// The group is exhausted; advance to the next one.\n\t\t\t\tthis.#group = undefined;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tthis.#apply(frame.payload);\n\t\t\treturn this.#decoder.decode();\n\t\t}\n\t}\n\n\tasync *[Symbol.asyncIterator](): AsyncIterator<T> {\n\t\tfor (;;) {\n\t\t\tconst value = await this.next();\n\t\t\tif (value === undefined) return;\n\t\t\tyield value;\n\t\t}\n\t}\n\n\t// Frame 0 of a group is a snapshot, the rest are merge patches.\n\t#apply(payload: Uint8Array): void {\n\t\tif (this.#framesRead === 0) {\n\t\t\tthis.#decoder.snapshot(payload);\n\t\t} else {\n\t\t\tthis.#decoder.delta(payload);\n\t\t}\n\t\tthis.#framesRead += 1;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Config } from "./encoder";
|
|
2
|
+
/**
|
|
3
|
+
* Reconstructs a JSON value from the snapshot and delta frames of a group.
|
|
4
|
+
*
|
|
5
|
+
* The track-free core of {@link Consumer}, and the mirror of {@link Encoder}. The caller reads
|
|
6
|
+
* frames from wherever it likes and routes each one by its position in the group: the first frame of
|
|
7
|
+
* every group is a {@link snapshot}, the rest are {@link delta}s.
|
|
8
|
+
*
|
|
9
|
+
* Applying and materializing are separate on purpose. Frames must be applied in order (the merge
|
|
10
|
+
* patches and the DEFLATE window are both sequential), but a consumer catching up on a backlog only
|
|
11
|
+
* wants the value at the head, so it applies every frame and calls {@link decode} once. A caller
|
|
12
|
+
* that wants a value per frame just calls it every time.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Decoder<T> {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(config?: Config<T>);
|
|
17
|
+
/**
|
|
18
|
+
* Apply a group's first frame: a full snapshot that replaces the current value.
|
|
19
|
+
*
|
|
20
|
+
* Also starts the group's DEFLATE window, so this must be called at every group boundary, not
|
|
21
|
+
* only the first.
|
|
22
|
+
*/
|
|
23
|
+
snapshot(payload: Uint8Array): void;
|
|
24
|
+
/**
|
|
25
|
+
* Apply one of a group's later frames: an RFC 7396 merge patch against the current value.
|
|
26
|
+
*
|
|
27
|
+
* Throws when no snapshot has been applied yet, since a patch has nothing to apply to.
|
|
28
|
+
*/
|
|
29
|
+
delta(payload: Uint8Array): void;
|
|
30
|
+
/**
|
|
31
|
+
* A copy of the reconstructed value as raw JSON, or `undefined` before the first snapshot.
|
|
32
|
+
*
|
|
33
|
+
* Copied rather than shared: it is the base every later {@link delta} merges into, so a caller
|
|
34
|
+
* mutating it would change fields no frame ever carried.
|
|
35
|
+
*/
|
|
36
|
+
get value(): unknown;
|
|
37
|
+
/**
|
|
38
|
+
* Materialize the reconstructed value, or `undefined` before the first snapshot.
|
|
39
|
+
*
|
|
40
|
+
* The result is the caller's own copy, for the same reason as {@link value}. (A schema already
|
|
41
|
+
* produces a fresh object, so it only needs cloning without one. The Rust decoder deserializes a
|
|
42
|
+
* fresh `T` every call and never exposes its internal state at all.)
|
|
43
|
+
*/
|
|
44
|
+
decode(): T | undefined;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=decoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../../src/snapshot/decoder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,qBAAa,OAAO,CAAC,CAAC;;IAUrB,YAAY,MAAM,GAAE,MAAM,CAAC,CAAC,CAAM,EAGjC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAIlC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAG/B;IAED;;;;;OAKG;IACH,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED;;;;;;OAMG;IACH,MAAM,IAAI,CAAC,GAAG,SAAS,CAGtB;CAOD"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* @ts-self-types="./decoder.d.ts" */
|
|
2
|
+
import { Decoder as Flate } from "@norskvideo/moq-flate";
|
|
3
|
+
import { merge } from "../diff.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reconstructs a JSON value from the snapshot and delta frames of a group.
|
|
6
|
+
*
|
|
7
|
+
* The track-free core of {@link Consumer}, and the mirror of {@link Encoder}. The caller reads
|
|
8
|
+
* frames from wherever it likes and routes each one by its position in the group: the first frame of
|
|
9
|
+
* every group is a {@link snapshot}, the rest are {@link delta}s.
|
|
10
|
+
*
|
|
11
|
+
* Applying and materializing are separate on purpose. Frames must be applied in order (the merge
|
|
12
|
+
* patches and the DEFLATE window are both sequential), but a consumer catching up on a backlog only
|
|
13
|
+
* wants the value at the head, so it applies every frame and calls {@link decode} once. A caller
|
|
14
|
+
* that wants a value per frame just calls it every time.
|
|
15
|
+
*/
|
|
16
|
+
export class Decoder {
|
|
17
|
+
#schema;
|
|
18
|
+
// Whether frames are `deflate-raw` compressed. Must match the encoder's {@link Config.compression}.
|
|
19
|
+
#decompress;
|
|
20
|
+
// The current group's DEFLATE window, rebuilt at each snapshot.
|
|
21
|
+
#flate;
|
|
22
|
+
// The reconstructed value, `undefined` until the first snapshot.
|
|
23
|
+
#current;
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.#schema = config.schema;
|
|
26
|
+
this.#decompress = config.compression ?? false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Apply a group's first frame: a full snapshot that replaces the current value.
|
|
30
|
+
*
|
|
31
|
+
* Also starts the group's DEFLATE window, so this must be called at every group boundary, not
|
|
32
|
+
* only the first.
|
|
33
|
+
*/
|
|
34
|
+
snapshot(payload) {
|
|
35
|
+
// Each group is its own compressed stream, so the window starts cold here.
|
|
36
|
+
this.#flate = this.#decompress ? new Flate() : undefined;
|
|
37
|
+
this.#current = this.#parse(payload);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Apply one of a group's later frames: an RFC 7396 merge patch against the current value.
|
|
41
|
+
*
|
|
42
|
+
* Throws when no snapshot has been applied yet, since a patch has nothing to apply to.
|
|
43
|
+
*/
|
|
44
|
+
delta(payload) {
|
|
45
|
+
if (this.#current === undefined)
|
|
46
|
+
throw new Error("delta before snapshot");
|
|
47
|
+
this.#current = merge(this.#current, this.#parse(payload));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A copy of the reconstructed value as raw JSON, or `undefined` before the first snapshot.
|
|
51
|
+
*
|
|
52
|
+
* Copied rather than shared: it is the base every later {@link delta} merges into, so a caller
|
|
53
|
+
* mutating it would change fields no frame ever carried.
|
|
54
|
+
*/
|
|
55
|
+
get value() {
|
|
56
|
+
return this.#current === undefined ? undefined : structuredClone(this.#current);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Materialize the reconstructed value, or `undefined` before the first snapshot.
|
|
60
|
+
*
|
|
61
|
+
* The result is the caller's own copy, for the same reason as {@link value}. (A schema already
|
|
62
|
+
* produces a fresh object, so it only needs cloning without one. The Rust decoder deserializes a
|
|
63
|
+
* fresh `T` every call and never exposes its internal state at all.)
|
|
64
|
+
*/
|
|
65
|
+
decode() {
|
|
66
|
+
if (this.#current === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return this.#schema ? this.#schema.parse(this.#current) : structuredClone(this.#current);
|
|
69
|
+
}
|
|
70
|
+
// Decompress a frame against the group's window when compressing, then parse it as JSON.
|
|
71
|
+
#parse(payload) {
|
|
72
|
+
const plain = this.#flate ? this.#flate.frame(payload) : payload;
|
|
73
|
+
return JSON.parse(new TextDecoder().decode(plain));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=decoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decoder.js","sourceRoot":"","sources":["../../src/snapshot/decoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,OAAO;IACnB,OAAO,CAAuB;IAC9B,oGAAoG;IACpG,WAAW,CAAU;IAErB,gEAAgE;IAChE,MAAM,CAAS;IACf,iEAAiE;IACjE,QAAQ,CAAW;IAEnB,YAAY,MAAM,GAAc,EAAE;QACjC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,KAAK,CAAC;IAChD,CAAC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,OAAmB;QAC3B,2EAA2E;QAC3E,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACzD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAmB;QACxB,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;QAC1E,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;OAKG;IACH,IAAI,KAAK;QACR,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjF,CAAC;IAED;;;;;;OAMG;IACH,MAAM;QACL,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAClD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAO,CAAC;IACjG,CAAC;IAED,yFAAyF;IACzF,MAAM,CAAC,OAAmB;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QACjE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,CAAC;CACD","sourcesContent":["import { Decoder as Flate } from \"@moq/flate\";\n\nimport { merge } from \"../diff.ts\";\nimport type { Config } from \"./encoder.ts\";\n\n/**\n * Reconstructs a JSON value from the snapshot and delta frames of a group.\n *\n * The track-free core of {@link Consumer}, and the mirror of {@link Encoder}. The caller reads\n * frames from wherever it likes and routes each one by its position in the group: the first frame of\n * every group is a {@link snapshot}, the rest are {@link delta}s.\n *\n * Applying and materializing are separate on purpose. Frames must be applied in order (the merge\n * patches and the DEFLATE window are both sequential), but a consumer catching up on a backlog only\n * wants the value at the head, so it applies every frame and calls {@link decode} once. A caller\n * that wants a value per frame just calls it every time.\n */\nexport class Decoder<T> {\n\t#schema?: Config<T>[\"schema\"];\n\t// Whether frames are `deflate-raw` compressed. Must match the encoder's {@link Config.compression}.\n\t#decompress: boolean;\n\n\t// The current group's DEFLATE window, rebuilt at each snapshot.\n\t#flate?: Flate;\n\t// The reconstructed value, `undefined` until the first snapshot.\n\t#current?: unknown;\n\n\tconstructor(config: Config<T> = {}) {\n\t\tthis.#schema = config.schema;\n\t\tthis.#decompress = config.compression ?? false;\n\t}\n\n\t/**\n\t * Apply a group's first frame: a full snapshot that replaces the current value.\n\t *\n\t * Also starts the group's DEFLATE window, so this must be called at every group boundary, not\n\t * only the first.\n\t */\n\tsnapshot(payload: Uint8Array): void {\n\t\t// Each group is its own compressed stream, so the window starts cold here.\n\t\tthis.#flate = this.#decompress ? new Flate() : undefined;\n\t\tthis.#current = this.#parse(payload);\n\t}\n\n\t/**\n\t * Apply one of a group's later frames: an RFC 7396 merge patch against the current value.\n\t *\n\t * Throws when no snapshot has been applied yet, since a patch has nothing to apply to.\n\t */\n\tdelta(payload: Uint8Array): void {\n\t\tif (this.#current === undefined) throw new Error(\"delta before snapshot\");\n\t\tthis.#current = merge(this.#current, this.#parse(payload));\n\t}\n\n\t/**\n\t * A copy of the reconstructed value as raw JSON, or `undefined` before the first snapshot.\n\t *\n\t * Copied rather than shared: it is the base every later {@link delta} merges into, so a caller\n\t * mutating it would change fields no frame ever carried.\n\t */\n\tget value(): unknown {\n\t\treturn this.#current === undefined ? undefined : structuredClone(this.#current);\n\t}\n\n\t/**\n\t * Materialize the reconstructed value, or `undefined` before the first snapshot.\n\t *\n\t * The result is the caller's own copy, for the same reason as {@link value}. (A schema already\n\t * produces a fresh object, so it only needs cloning without one. The Rust decoder deserializes a\n\t * fresh `T` every call and never exposes its internal state at all.)\n\t */\n\tdecode(): T | undefined {\n\t\tif (this.#current === undefined) return undefined;\n\t\treturn this.#schema ? this.#schema.parse(this.#current) : (structuredClone(this.#current) as T);\n\t}\n\n\t// Decompress a frame against the group's window when compressing, then parse it as JSON.\n\t#parse(payload: Uint8Array): unknown {\n\t\tconst plain = this.#flate ? this.#flate.frame(payload) : payload;\n\t\treturn JSON.parse(new TextDecoder().decode(plain));\n\t}\n}\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type * as z from "@zod/mini";
|
|
2
|
+
export declare const DEFAULT_DELTA_RATIO = 8;
|
|
3
|
+
/** Options shared by an {@link Encoder} and the {@link Producer} that wraps one. */
|
|
4
|
+
export interface Config<T> {
|
|
5
|
+
deltaRatio?: number;
|
|
6
|
+
schema?: z.ZodMiniType<T>;
|
|
7
|
+
initial?: T;
|
|
8
|
+
compression?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** One encoded frame, and the group boundary it implies. */
|
|
11
|
+
export interface Encoded {
|
|
12
|
+
/** The frame payload, `deflate-raw` compressed when {@link Config.compression} is set. */
|
|
13
|
+
payload: Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Whether this frame is a full snapshot, which must open a new group.
|
|
16
|
+
*
|
|
17
|
+
* `true` means the caller writes it as the first frame of a fresh group; `false` means it is a
|
|
18
|
+
* merge patch that must be appended to the group the last snapshot opened.
|
|
19
|
+
*
|
|
20
|
+
* The encoder decides this, never the caller: a value that sets a field to JSON null, or whose
|
|
21
|
+
* root isn't an object, cannot be expressed as a merge patch at all, and the delta budget and
|
|
22
|
+
* frame cap force a snapshot independently of what the caller wanted.
|
|
23
|
+
*/
|
|
24
|
+
keyframe: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An encoded frame the caller has not yet acknowledged writing, returned by {@link Encoder.update}.
|
|
28
|
+
*
|
|
29
|
+
* Write the frame, then {@link commit}. A frame that is never committed never reached the wire, so
|
|
30
|
+
* the encoder resynchronizes on the next {@link Encoder.update}: it starts a fresh snapshot rather
|
|
31
|
+
* than emitting deltas against a baseline no consumer received.
|
|
32
|
+
*
|
|
33
|
+
* This is a recovery, not a rollback. Producing a delta payload advances the group's DEFLATE window
|
|
34
|
+
* and that can't be undone, so a snapshot is the only sound way back. Forgetting to commit a frame
|
|
35
|
+
* that *was* written is therefore merely wasteful (one redundant snapshot), never incorrect.
|
|
36
|
+
*/
|
|
37
|
+
export interface Pending extends Encoded {
|
|
38
|
+
/**
|
|
39
|
+
* Acknowledge that the frame reached the wire, keeping the encoder's state.
|
|
40
|
+
*
|
|
41
|
+
* Only call this once the write has actually succeeded. Committing a frame that failed to write
|
|
42
|
+
* is the one thing that corrupts the stream.
|
|
43
|
+
*/
|
|
44
|
+
commit(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Encodes a JSON value into frame payloads, choosing snapshots and deltas automatically.
|
|
48
|
+
*
|
|
49
|
+
* The track-free core of {@link Producer}: it decides *what bytes go in a frame* and *where the
|
|
50
|
+
* group boundaries fall*, and leaves writing them to the caller. Reach for it when something else
|
|
51
|
+
* already owns the track.
|
|
52
|
+
*
|
|
53
|
+
* Frames must reach the wire in the order they were encoded, and a frame with
|
|
54
|
+
* {@link Encoded.keyframe} set must open a new group: both the merge patches and the group-scoped
|
|
55
|
+
* DEFLATE window depend on it. {@link update} hands back a {@link Pending} rather than a bare
|
|
56
|
+
* {@link Encoded} so a frame that never reaches the wire can't silently desync the encoder: leave it
|
|
57
|
+
* uncommitted and the next update resynchronizes with a fresh snapshot.
|
|
58
|
+
*
|
|
59
|
+
* (The Rust `moq-json` encoder does the same thing through `Drop`, so it resynchronizes the moment
|
|
60
|
+
* the frame is discarded. JavaScript has no destructor, so the check happens on the next update
|
|
61
|
+
* instead. The resulting stream is identical either way.)
|
|
62
|
+
*
|
|
63
|
+
* If the caller closes a group for its own reasons, call
|
|
64
|
+
* {@link reset} so the next value is encoded as a snapshot.
|
|
65
|
+
*/
|
|
66
|
+
export declare class Encoder<T> {
|
|
67
|
+
#private;
|
|
68
|
+
constructor(config?: Config<T>);
|
|
69
|
+
/**
|
|
70
|
+
* A copy of the last encoded value, or `undefined` before the first snapshot.
|
|
71
|
+
*
|
|
72
|
+
* Copied rather than shared: the baseline is what the next delta is diffed against, so a caller
|
|
73
|
+
* mutating it in place would make a later change look unchanged and never reach consumers.
|
|
74
|
+
*/
|
|
75
|
+
get value(): T | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Force the next {@link update} to emit a full snapshot, even for an unchanged value.
|
|
78
|
+
*
|
|
79
|
+
* Call this whenever the caller closes the current group behind the encoder's back. Without it
|
|
80
|
+
* the next value may be encoded as a delta against a DEFLATE window and a baseline that the new
|
|
81
|
+
* group doesn't carry.
|
|
82
|
+
*
|
|
83
|
+
* {@link value} survives: the snapshot republishes it in full anyway, and it is what a caller
|
|
84
|
+
* editing in place starts from.
|
|
85
|
+
*/
|
|
86
|
+
reset(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Encode a new value, as a snapshot or a delta.
|
|
89
|
+
*
|
|
90
|
+
* Returns `undefined` when the value is unchanged from the last one encoded, so nothing needs to
|
|
91
|
+
* be written. Otherwise the frame comes back as a {@link Pending} the caller writes and then
|
|
92
|
+
* commits; leaving one uncommitted resynchronizes the encoder here, on the next call.
|
|
93
|
+
*/
|
|
94
|
+
update(value: T): Pending | undefined;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=encoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/snapshot/encoder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,CAAC,MAAM,WAAW,CAAC;AAUpC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,oFAAoF;AACpF,MAAM,WAAW,MAAM,CAAC,CAAC;IAcxB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAI1B,OAAO,CAAC,EAAE,CAAC,CAAC;IAMZ,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,4DAA4D;AAC5D,MAAM,WAAW,OAAO;IACvB,0FAA0F;IAC1F,OAAO,EAAE,UAAU,CAAC;IAEpB;;;;;;;;;OASG;IACH,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,OAAQ,SAAQ,OAAO;IACvC;;;;;OAKG;IACH,MAAM,IAAI,IAAI,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,OAAO,CAAC,CAAC;;IAkCrB,YAAY,MAAM,GAAE,MAAM,CAAC,CAAC,CAAM,EAGjC;IAED;;;;;OAKG;IACH,IAAI,KAAK,IAAI,CAAC,GAAG,SAAS,CAEzB;IAED;;;;;;;;;OASG;IACH,KAAK,IAAI,IAAI,CAOZ;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,GAAG,SAAS,CAuCpC;CAsED"}
|