@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* @ts-self-types="./encoder.d.ts" */
|
|
2
|
+
import { DEFAULT_MAX_FRAME_SIZE, Encoder as Flate } from "@norskvideo/moq-flate";
|
|
3
|
+
import { Group } from "@norskvideo/moq-net";
|
|
4
|
+
/** Frames (header included) in one group before a new group is forced, matching the Snapshot cap. */
|
|
5
|
+
const MAX_GROUP_FRAMES = 256;
|
|
6
|
+
/** Op ratio used when {@link ProducerConfig.opRatio} is left unset. */
|
|
7
|
+
export const DEFAULT_OP_RATIO = 8;
|
|
8
|
+
/**
|
|
9
|
+
* Encodes window edits into frame payloads, deciding where the group boundaries fall.
|
|
10
|
+
*
|
|
11
|
+
* The track-free core of {@link Producer}. It owns the retained window, so it can restate it
|
|
12
|
+
* whenever a group rolls; that restatement is the whole point of the mode, and is what an
|
|
13
|
+
* append-only log cannot do.
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export class Encoder {
|
|
18
|
+
#opRatio;
|
|
19
|
+
#compress;
|
|
20
|
+
// The retained window. Records are stored as encoded JSON text so a header restates exactly the
|
|
21
|
+
// bytes a push would have carried, and so re-encoding never re-visits the caller's value.
|
|
22
|
+
#window = [];
|
|
23
|
+
// Absolute index of #window[0]. Only a group header puts this on the wire.
|
|
24
|
+
#offset = 0;
|
|
25
|
+
#flate;
|
|
26
|
+
// Bytes of pushes and pops in this group, excluding its header frame.
|
|
27
|
+
#opBytes = 0;
|
|
28
|
+
// Reference size the op budget is measured against: this group's header frame.
|
|
29
|
+
#headerLen = 0;
|
|
30
|
+
#groupFrames = 0;
|
|
31
|
+
// Whether the next frame must be a header after a lost frame.
|
|
32
|
+
#resync = true;
|
|
33
|
+
// Whether the frame from the last push/pop is still unacknowledged.
|
|
34
|
+
#pending = false;
|
|
35
|
+
// Bumped per frame handed out, so a commit arriving after the encoder moved on cannot clear the
|
|
36
|
+
// flag belonging to a newer frame.
|
|
37
|
+
#generation = 0;
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.#opRatio = config.opRatio ?? DEFAULT_OP_RATIO;
|
|
40
|
+
if (!Number.isSafeInteger(this.#opRatio) || this.#opRatio < 0 || this.#opRatio > 0xffffffff) {
|
|
41
|
+
throw new Error("opRatio must be an unsigned 32-bit integer");
|
|
42
|
+
}
|
|
43
|
+
this.#compress = config.compression ?? false;
|
|
44
|
+
}
|
|
45
|
+
/** The retained window, oldest first. */
|
|
46
|
+
get window() {
|
|
47
|
+
return this.#window.map((text) => JSON.parse(text));
|
|
48
|
+
}
|
|
49
|
+
/** Absolute index of the oldest retained record. */
|
|
50
|
+
get offset() {
|
|
51
|
+
return this.#offset;
|
|
52
|
+
}
|
|
53
|
+
/** Absolute index the next pushed record will take. */
|
|
54
|
+
get end() {
|
|
55
|
+
return this.#offset + this.#window.length;
|
|
56
|
+
}
|
|
57
|
+
/** Discard group-local state after an encoded frame did not reach the wire. */
|
|
58
|
+
#resyncGroup() {
|
|
59
|
+
this.#flate = undefined;
|
|
60
|
+
this.#opBytes = 0;
|
|
61
|
+
this.#headerLen = 0;
|
|
62
|
+
this.#groupFrames = 0;
|
|
63
|
+
this.#resync = true;
|
|
64
|
+
this.#pending = false;
|
|
65
|
+
this.#generation += 1;
|
|
66
|
+
}
|
|
67
|
+
/** Append one record to the back of the window. */
|
|
68
|
+
push(value) {
|
|
69
|
+
if (this.#pending)
|
|
70
|
+
this.#resyncGroup();
|
|
71
|
+
if (this.end >= Number.MAX_SAFE_INTEGER)
|
|
72
|
+
throw new Error("window index exceeds the safe integer range");
|
|
73
|
+
// Encode before touching the window, so a value that can't be serialized leaves the encoder
|
|
74
|
+
// exactly as it was.
|
|
75
|
+
const text = JSON.stringify(value);
|
|
76
|
+
if (text === undefined) {
|
|
77
|
+
throw new Error("record is not representable as JSON");
|
|
78
|
+
}
|
|
79
|
+
const edit = { push: text };
|
|
80
|
+
if (this.#resync || !this.#opAllowed()) {
|
|
81
|
+
return this.#emitHeader(this.#offset, [...this.#window, text], edit);
|
|
82
|
+
}
|
|
83
|
+
const payload = this.#emitOp(`{"push":${text}}`);
|
|
84
|
+
if (payload)
|
|
85
|
+
return this.#pendingFrame(payload, false, edit);
|
|
86
|
+
return this.#emitHeader(this.#offset, [...this.#window, text], edit);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Drop `count` records from the front of the window.
|
|
90
|
+
*
|
|
91
|
+
* Returns `undefined` when there is nothing to drop, so a caller can trim unconditionally.
|
|
92
|
+
* Clamped to what the window holds.
|
|
93
|
+
*/
|
|
94
|
+
pop(count) {
|
|
95
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
96
|
+
throw new Error("pop count must be a nonnegative safe integer");
|
|
97
|
+
}
|
|
98
|
+
if (this.#pending)
|
|
99
|
+
this.#resyncGroup();
|
|
100
|
+
const dropped = Math.min(count, this.#window.length);
|
|
101
|
+
if (dropped <= 0)
|
|
102
|
+
return undefined;
|
|
103
|
+
const offset = this.#offset + dropped;
|
|
104
|
+
const edit = { pop: dropped };
|
|
105
|
+
if (this.#resync || !this.#opAllowed()) {
|
|
106
|
+
return this.#emitHeader(offset, this.#window.slice(dropped), edit);
|
|
107
|
+
}
|
|
108
|
+
const payload = this.#emitOp(`{"pop":${dropped}}`);
|
|
109
|
+
if (payload)
|
|
110
|
+
return this.#pendingFrame(payload, false, edit);
|
|
111
|
+
return this.#emitHeader(offset, this.#window.slice(dropped), edit);
|
|
112
|
+
}
|
|
113
|
+
/** Whether the pending edit may ride as an op in the open group. */
|
|
114
|
+
#opAllowed() {
|
|
115
|
+
return (this.#opRatio !== 0 &&
|
|
116
|
+
this.#groupFrames > 0 &&
|
|
117
|
+
this.#groupFrames < MAX_GROUP_FRAMES &&
|
|
118
|
+
this.#opBytes <= this.#opRatio * this.#headerLen);
|
|
119
|
+
}
|
|
120
|
+
/** Compress an already-serialized op into the open group, if its header remains cached. */
|
|
121
|
+
#emitOp(text) {
|
|
122
|
+
const bytes = new TextEncoder().encode(text);
|
|
123
|
+
if (bytes.length > DEFAULT_MAX_FRAME_SIZE) {
|
|
124
|
+
throw new Error("window frame exceeds the decoder's decompressed size limit");
|
|
125
|
+
}
|
|
126
|
+
const payload = this.#flate ? this.#flate.frame(bytes) : bytes;
|
|
127
|
+
this.#opBytes += payload.length;
|
|
128
|
+
this.#groupFrames += 1;
|
|
129
|
+
if (this.#headerLen + this.#opBytes > Group.MAX_GROUP_CACHE_BYTES) {
|
|
130
|
+
this.#resyncGroup();
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
/** Emit the header restating the whole window and opening a new group. */
|
|
136
|
+
#emitHeader(offset, window, edit) {
|
|
137
|
+
// The records are already JSON text, so splice them in rather than re-encoding each one; the
|
|
138
|
+
// bytes a reader sees are then identical to what the matching push carried.
|
|
139
|
+
const text = `{"offset":${offset},"records":[${window.join(",")}]}`;
|
|
140
|
+
const bytes = new TextEncoder().encode(text);
|
|
141
|
+
if (bytes.length > DEFAULT_MAX_FRAME_SIZE) {
|
|
142
|
+
throw new Error("window header exceeds the decoder's decompressed size limit");
|
|
143
|
+
}
|
|
144
|
+
// A fresh per-group encoder (cold window), with the header as frame 0.
|
|
145
|
+
const flate = this.#compress ? new Flate() : undefined;
|
|
146
|
+
const payload = flate ? flate.frame(bytes) : bytes;
|
|
147
|
+
if (payload.length > Group.MAX_GROUP_CACHE_BYTES) {
|
|
148
|
+
throw new Error("window header exceeds the group cache limit");
|
|
149
|
+
}
|
|
150
|
+
this.#flate = flate;
|
|
151
|
+
this.#headerLen = payload.length;
|
|
152
|
+
this.#opBytes = 0;
|
|
153
|
+
this.#groupFrames = 1;
|
|
154
|
+
this.#resync = false;
|
|
155
|
+
return this.#pendingFrame(payload, true, edit);
|
|
156
|
+
}
|
|
157
|
+
#pendingFrame(payload, keyframe, edit) {
|
|
158
|
+
this.#pending = true;
|
|
159
|
+
const generation = ++this.#generation;
|
|
160
|
+
const encoder = this;
|
|
161
|
+
return {
|
|
162
|
+
payload,
|
|
163
|
+
keyframe,
|
|
164
|
+
commit() {
|
|
165
|
+
if (encoder.#generation !== generation || !encoder.#pending)
|
|
166
|
+
return;
|
|
167
|
+
encoder.#commit(edit);
|
|
168
|
+
encoder.#pending = false;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Apply an edit after its encoded frame reached the wire. */
|
|
173
|
+
#commit(edit) {
|
|
174
|
+
if ("push" in edit) {
|
|
175
|
+
this.#window.push(edit.push);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.#window.splice(0, edit.pop);
|
|
179
|
+
this.#offset += edit.pop;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=encoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/window/encoder.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_MAX_FRAME_SIZE, Encoder as Flate } from \"@moq/flate\";\nimport { Group } from \"@moq/net\";\n\n/** Frames (header included) in one group before a new group is forced, matching the Snapshot cap. */\nconst MAX_GROUP_FRAMES = 256;\n\n/** Op ratio used when {@link ProducerConfig.opRatio} is left unset. */\nexport const DEFAULT_OP_RATIO = 8;\n\ntype Edit = { push: string } | { pop: number };\n\n/** Options shared by an {@link Encoder} and the {@link Producer} that wraps one. */\nexport interface ProducerConfig {\n\t/**\n\t * How much the ops in a group may cost before a fresh group is emitted.\n\t *\n\t * A new group opens once the pushes and pops *already written* exceed `opRatio` times the size of\n\t * the group's header frame. The pending op is excluded, so the one that tips the group over budget\n\t * still lands. `0` disables ops entirely, so every edit is its own single-frame group.\n\t *\n\t * The window's counterpart to Snapshot's `deltaRatio`. Defaults to `8`.\n\t */\n\topRatio?: number;\n\n\t/**\n\t * Compress each group as one sync-flushed `deflate-raw` stream, so every op reuses the header and\n\t * the ops before it as context. A {@link Decoder} reading the frames must set the same flag.\n\t * Defaults to `false`.\n\t */\n\tcompression?: boolean;\n}\n\n/** One encoded frame, and the group boundary it implies. */\nexport interface Encoded {\n\t/** The frame payload. */\n\tpayload: Uint8Array;\n\n\t/**\n\t * Whether this frame is a group header, which must open a new group.\n\t *\n\t * The encoder decides this, never the caller: the op budget and the frame cap force a new group\n\t * independently of which edit was requested.\n\t */\n\tkeyframe: boolean;\n}\n\n/**\n * An encoded frame the caller has not yet acknowledged writing.\n *\n * Write the frame, then {@link commit}. The edit is staged until commit, so leaving the frame\n * uncommitted keeps the window unchanged and makes the next frame open a new group.\n */\nexport interface Pending extends Encoded {\n\t/** Acknowledge that the frame reached the wire, applying its edit to the retained window. */\n\tcommit(): void;\n}\n\n/**\n * Encodes window edits into frame payloads, deciding where the group boundaries fall.\n *\n * The track-free core of {@link Producer}. It owns the retained window, so it can restate it\n * whenever a group rolls; that restatement is the whole point of the mode, and is what an\n * append-only log cannot do.\n *\n * @public\n */\nexport class Encoder<T> {\n\t#opRatio: number;\n\t#compress: boolean;\n\n\t// The retained window. Records are stored as encoded JSON text so a header restates exactly the\n\t// bytes a push would have carried, and so re-encoding never re-visits the caller's value.\n\t#window: string[] = [];\n\t// Absolute index of #window[0]. Only a group header puts this on the wire.\n\t#offset = 0;\n\n\t#flate?: Flate;\n\t// Bytes of pushes and pops in this group, excluding its header frame.\n\t#opBytes = 0;\n\t// Reference size the op budget is measured against: this group's header frame.\n\t#headerLen = 0;\n\t#groupFrames = 0;\n\t// Whether the next frame must be a header after a lost frame.\n\t#resync = true;\n\t// Whether the frame from the last push/pop is still unacknowledged.\n\t#pending = false;\n\t// Bumped per frame handed out, so a commit arriving after the encoder moved on cannot clear the\n\t// flag belonging to a newer frame.\n\t#generation = 0;\n\n\tconstructor(config: ProducerConfig = {}) {\n\t\tthis.#opRatio = config.opRatio ?? DEFAULT_OP_RATIO;\n\t\tif (!Number.isSafeInteger(this.#opRatio) || this.#opRatio < 0 || this.#opRatio > 0xffffffff) {\n\t\t\tthrow new Error(\"opRatio must be an unsigned 32-bit integer\");\n\t\t}\n\t\tthis.#compress = config.compression ?? false;\n\t}\n\n\t/** The retained window, oldest first. */\n\tget window(): T[] {\n\t\treturn this.#window.map((text) => JSON.parse(text) as T);\n\t}\n\n\t/** Absolute index of the oldest retained record. */\n\tget offset(): number {\n\t\treturn this.#offset;\n\t}\n\n\t/** Absolute index the next pushed record will take. */\n\tget end(): number {\n\t\treturn this.#offset + this.#window.length;\n\t}\n\n\t/** Discard group-local state after an encoded frame did not reach the wire. */\n\t#resyncGroup(): void {\n\t\tthis.#flate = undefined;\n\t\tthis.#opBytes = 0;\n\t\tthis.#headerLen = 0;\n\t\tthis.#groupFrames = 0;\n\t\tthis.#resync = true;\n\t\tthis.#pending = false;\n\t\tthis.#generation += 1;\n\t}\n\n\t/** Append one record to the back of the window. */\n\tpush(value: T): Pending {\n\t\tif (this.#pending) this.#resyncGroup();\n\t\tif (this.end >= Number.MAX_SAFE_INTEGER) throw new Error(\"window index exceeds the safe integer range\");\n\n\t\t// Encode before touching the window, so a value that can't be serialized leaves the encoder\n\t\t// exactly as it was.\n\t\tconst text: string | undefined = JSON.stringify(value);\n\t\tif (text === undefined) {\n\t\t\tthrow new Error(\"record is not representable as JSON\");\n\t\t}\n\n\t\tconst edit: Edit = { push: text };\n\t\tif (this.#resync || !this.#opAllowed()) {\n\t\t\treturn this.#emitHeader(this.#offset, [...this.#window, text], edit);\n\t\t}\n\n\t\tconst payload = this.#emitOp(`{\"push\":${text}}`);\n\t\tif (payload) return this.#pendingFrame(payload, false, edit);\n\t\treturn this.#emitHeader(this.#offset, [...this.#window, text], edit);\n\t}\n\n\t/**\n\t * Drop `count` records from the front of the window.\n\t *\n\t * Returns `undefined` when there is nothing to drop, so a caller can trim unconditionally.\n\t * Clamped to what the window holds.\n\t */\n\tpop(count: number): Pending | undefined {\n\t\tif (!Number.isSafeInteger(count) || count < 0) {\n\t\t\tthrow new Error(\"pop count must be a nonnegative safe integer\");\n\t\t}\n\t\tif (this.#pending) this.#resyncGroup();\n\n\t\tconst dropped = Math.min(count, this.#window.length);\n\t\tif (dropped <= 0) return undefined;\n\n\t\tconst offset = this.#offset + dropped;\n\t\tconst edit: Edit = { pop: dropped };\n\t\tif (this.#resync || !this.#opAllowed()) {\n\t\t\treturn this.#emitHeader(offset, this.#window.slice(dropped), edit);\n\t\t}\n\n\t\tconst payload = this.#emitOp(`{\"pop\":${dropped}}`);\n\t\tif (payload) return this.#pendingFrame(payload, false, edit);\n\t\treturn this.#emitHeader(offset, this.#window.slice(dropped), edit);\n\t}\n\n\t/** Whether the pending edit may ride as an op in the open group. */\n\t#opAllowed(): boolean {\n\t\treturn (\n\t\t\tthis.#opRatio !== 0 &&\n\t\t\tthis.#groupFrames > 0 &&\n\t\t\tthis.#groupFrames < MAX_GROUP_FRAMES &&\n\t\t\tthis.#opBytes <= this.#opRatio * this.#headerLen\n\t\t);\n\t}\n\n\t/** Compress an already-serialized op into the open group, if its header remains cached. */\n\t#emitOp(text: string): Uint8Array | undefined {\n\t\tconst bytes = new TextEncoder().encode(text);\n\t\tif (bytes.length > DEFAULT_MAX_FRAME_SIZE) {\n\t\t\tthrow new Error(\"window frame exceeds the decoder's decompressed size limit\");\n\t\t}\n\t\tconst payload = this.#flate ? this.#flate.frame(bytes) : bytes;\n\n\t\tthis.#opBytes += payload.length;\n\t\tthis.#groupFrames += 1;\n\t\tif (this.#headerLen + this.#opBytes > Group.MAX_GROUP_CACHE_BYTES) {\n\t\t\tthis.#resyncGroup();\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn payload;\n\t}\n\n\t/** Emit the header restating the whole window and opening a new group. */\n\t#emitHeader(offset: number, window: string[], edit: Edit): Pending {\n\t\t// The records are already JSON text, so splice them in rather than re-encoding each one; the\n\t\t// bytes a reader sees are then identical to what the matching push carried.\n\t\tconst text = `{\"offset\":${offset},\"records\":[${window.join(\",\")}]}`;\n\t\tconst bytes = new TextEncoder().encode(text);\n\t\tif (bytes.length > DEFAULT_MAX_FRAME_SIZE) {\n\t\t\tthrow new Error(\"window header exceeds the decoder's decompressed size limit\");\n\t\t}\n\n\t\t// A fresh per-group encoder (cold window), with the header as frame 0.\n\t\tconst flate = this.#compress ? new Flate() : undefined;\n\t\tconst payload = flate ? flate.frame(bytes) : bytes;\n\t\tif (payload.length > Group.MAX_GROUP_CACHE_BYTES) {\n\t\t\tthrow new Error(\"window header exceeds the group cache limit\");\n\t\t}\n\n\t\tthis.#flate = flate;\n\t\tthis.#headerLen = payload.length;\n\t\tthis.#opBytes = 0;\n\t\tthis.#groupFrames = 1;\n\t\tthis.#resync = false;\n\n\t\treturn this.#pendingFrame(payload, true, edit);\n\t}\n\n\t#pendingFrame(payload: Uint8Array, keyframe: boolean, edit: Edit): Pending {\n\t\tthis.#pending = true;\n\t\tconst generation = ++this.#generation;\n\t\tconst encoder = this;\n\n\t\treturn {\n\t\t\tpayload,\n\t\t\tkeyframe,\n\t\t\tcommit() {\n\t\t\t\tif (encoder.#generation !== generation || !encoder.#pending) return;\n\t\t\t\tencoder.#commit(edit);\n\t\t\t\tencoder.#pending = false;\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Apply an edit after its encoded frame reached the wire. */\n\t#commit(edit: Edit): void {\n\t\tif (\"push\" in edit) {\n\t\t\tthis.#window.push(edit.push);\n\t\t} else {\n\t\t\tthis.#window.splice(0, edit.pop);\n\t\t\tthis.#offset += edit.pop;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding-window JSON publishing over MoQ tracks.
|
|
3
|
+
*
|
|
4
|
+
* A window is an ordered run of records the publisher appends to the back of and drops from the
|
|
5
|
+
* front of. Unlike the `Stream` module, which preserves a log forever in one group, and `Snapshot`,
|
|
6
|
+
* which keeps only the latest value, a window keeps a bounded stretch of records and lets a reader
|
|
7
|
+
* join it at any point. Interoperable on the wire with the Rust `moq_json::window`.
|
|
8
|
+
*
|
|
9
|
+
* The obvious alternative is an append-only log that rolls its group and re-seeds the new one with
|
|
10
|
+
* the records it still holds. That breaks the reader: re-seeded records are indistinguishable from
|
|
11
|
+
* new ones, so a reader that was keeping up receives them twice. This mode exists to make the
|
|
12
|
+
* restatement explicit, so a reader can tell "you already have these" from "here is another one".
|
|
13
|
+
*
|
|
14
|
+
* The first frame of every group names the retained `records` and the absolute `offset` of the
|
|
15
|
+
* first. Later frames are tagged `push` and `pop` ops. A push takes the next index and a pop drops
|
|
16
|
+
* from the front, both positional against the group header.
|
|
17
|
+
* Indices stop at `Number.MAX_SAFE_INTEGER`, matching the Rust implementation's wire domain.
|
|
18
|
+
* Trimming is therefore an op, not a group boundary, so dropping a record costs one small frame
|
|
19
|
+
* inside the shared compression window instead of a roll that would throw that window away.
|
|
20
|
+
*
|
|
21
|
+
* The publisher rolls a group when the ops in it outgrow {@link ProducerConfig.opRatio} times the
|
|
22
|
+
* header that opened it. That is purely a compression decision: there is no caller-driven cut and no
|
|
23
|
+
* age bound, and a {@link Consumer} never surfaces it.
|
|
24
|
+
*
|
|
25
|
+
* A reader gets a `push` event when a record arrives, `pop` when a contiguous span leaves, and
|
|
26
|
+
* `skip` when a span was dropped before this reader saw it. A reader that keeps up sees pushes and
|
|
27
|
+
* pops; one that falls a group behind learns from the header's offset which records it will never
|
|
28
|
+
* get rather than silently missing them.
|
|
29
|
+
*
|
|
30
|
+
* {@link Producer} and {@link Consumer} own a track. {@link Encoder} and {@link Decoder} are the
|
|
31
|
+
* same logic without it, for when something else is already in charge of the track.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
export { Consumer } from "./consumer";
|
|
36
|
+
export { type ConsumerConfig, Decoder, type Event, type Group, type Span } from "./decoder";
|
|
37
|
+
export { type Encoded, Encoder, type Pending, type ProducerConfig } from "./encoder";
|
|
38
|
+
export { Producer } from "./producer";
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/window/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,cAAc,EAAE,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAC/F,OAAO,EAAE,KAAK,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
package/window/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* @ts-self-types="./index.d.ts" */
|
|
2
|
+
/**
|
|
3
|
+
* Sliding-window JSON publishing over MoQ tracks.
|
|
4
|
+
*
|
|
5
|
+
* A window is an ordered run of records the publisher appends to the back of and drops from the
|
|
6
|
+
* front of. Unlike the `Stream` module, which preserves a log forever in one group, and `Snapshot`,
|
|
7
|
+
* which keeps only the latest value, a window keeps a bounded stretch of records and lets a reader
|
|
8
|
+
* join it at any point. Interoperable on the wire with the Rust `moq_json::window`.
|
|
9
|
+
*
|
|
10
|
+
* The obvious alternative is an append-only log that rolls its group and re-seeds the new one with
|
|
11
|
+
* the records it still holds. That breaks the reader: re-seeded records are indistinguishable from
|
|
12
|
+
* new ones, so a reader that was keeping up receives them twice. This mode exists to make the
|
|
13
|
+
* restatement explicit, so a reader can tell "you already have these" from "here is another one".
|
|
14
|
+
*
|
|
15
|
+
* The first frame of every group names the retained `records` and the absolute `offset` of the
|
|
16
|
+
* first. Later frames are tagged `push` and `pop` ops. A push takes the next index and a pop drops
|
|
17
|
+
* from the front, both positional against the group header.
|
|
18
|
+
* Indices stop at `Number.MAX_SAFE_INTEGER`, matching the Rust implementation's wire domain.
|
|
19
|
+
* Trimming is therefore an op, not a group boundary, so dropping a record costs one small frame
|
|
20
|
+
* inside the shared compression window instead of a roll that would throw that window away.
|
|
21
|
+
*
|
|
22
|
+
* The publisher rolls a group when the ops in it outgrow {@link ProducerConfig.opRatio} times the
|
|
23
|
+
* header that opened it. That is purely a compression decision: there is no caller-driven cut and no
|
|
24
|
+
* age bound, and a {@link Consumer} never surfaces it.
|
|
25
|
+
*
|
|
26
|
+
* A reader gets a `push` event when a record arrives, `pop` when a contiguous span leaves, and
|
|
27
|
+
* `skip` when a span was dropped before this reader saw it. A reader that keeps up sees pushes and
|
|
28
|
+
* pops; one that falls a group behind learns from the header's offset which records it will never
|
|
29
|
+
* get rather than silently missing them.
|
|
30
|
+
*
|
|
31
|
+
* {@link Producer} and {@link Consumer} own a track. {@link Encoder} and {@link Decoder} are the
|
|
32
|
+
* same logic without it, for when something else is already in charge of the track.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
export { Consumer } from "./consumer.js";
|
|
37
|
+
export { Decoder } from "./decoder.js";
|
|
38
|
+
export { Encoder } from "./encoder.js";
|
|
39
|
+
export { Producer } from "./producer.js";
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/window/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAuB,OAAO,EAAqC,MAAM,cAAc,CAAC;AAC/F,OAAO,EAAgB,OAAO,EAAqC,MAAM,cAAc,CAAC;AACxF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * Sliding-window JSON publishing over MoQ tracks.\n *\n * A window is an ordered run of records the publisher appends to the back of and drops from the\n * front of. Unlike the `Stream` module, which preserves a log forever in one group, and `Snapshot`,\n * which keeps only the latest value, a window keeps a bounded stretch of records and lets a reader\n * join it at any point. Interoperable on the wire with the Rust `moq_json::window`.\n *\n * The obvious alternative is an append-only log that rolls its group and re-seeds the new one with\n * the records it still holds. That breaks the reader: re-seeded records are indistinguishable from\n * new ones, so a reader that was keeping up receives them twice. This mode exists to make the\n * restatement explicit, so a reader can tell \"you already have these\" from \"here is another one\".\n *\n * The first frame of every group names the retained `records` and the absolute `offset` of the\n * first. Later frames are tagged `push` and `pop` ops. A push takes the next index and a pop drops\n * from the front, both positional against the group header.\n * Indices stop at `Number.MAX_SAFE_INTEGER`, matching the Rust implementation's wire domain.\n * Trimming is therefore an op, not a group boundary, so dropping a record costs one small frame\n * inside the shared compression window instead of a roll that would throw that window away.\n *\n * The publisher rolls a group when the ops in it outgrow {@link ProducerConfig.opRatio} times the\n * header that opened it. That is purely a compression decision: there is no caller-driven cut and no\n * age bound, and a {@link Consumer} never surfaces it.\n *\n * A reader gets a `push` event when a record arrives, `pop` when a contiguous span leaves, and\n * `skip` when a span was dropped before this reader saw it. A reader that keeps up sees pushes and\n * pops; one that falls a group behind learns from the header's offset which records it will never\n * get rather than silently missing them.\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.\n *\n * @module\n */\n\nexport { Consumer } from \"./consumer.ts\";\nexport { type ConsumerConfig, Decoder, type Event, type Group, type Span } from \"./decoder.ts\";\nexport { type Encoded, Encoder, type Pending, type ProducerConfig } from \"./encoder.ts\";\nexport { Producer } from \"./producer.ts\";\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as Moq from "@norskvideo/moq-net";
|
|
2
|
+
import { type ProducerConfig } from "./encoder";
|
|
3
|
+
/**
|
|
4
|
+
* Publishes a sliding window of JSON records to a track.
|
|
5
|
+
*
|
|
6
|
+
* An {@link Encoder} that owns its track: it writes each encoded frame and rolls a group whenever
|
|
7
|
+
* the encoder emits a header. When something else already owns the track, use the {@link Encoder}
|
|
8
|
+
* directly.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare class Producer<T> {
|
|
13
|
+
#private;
|
|
14
|
+
/** Wrap a track to publish a window into it. */
|
|
15
|
+
constructor(track: Moq.Track.Producer, config?: ProducerConfig);
|
|
16
|
+
/** The retained window, oldest first. */
|
|
17
|
+
get window(): T[];
|
|
18
|
+
/** Absolute index of the oldest retained record. */
|
|
19
|
+
get offset(): number;
|
|
20
|
+
/** Append one record to the back of the window. */
|
|
21
|
+
push(value: T): void;
|
|
22
|
+
/**
|
|
23
|
+
* Drop `count` records from the front of the window.
|
|
24
|
+
*
|
|
25
|
+
* A no-op when the window is already empty, and clamped to what it holds, so a caller can trim
|
|
26
|
+
* unconditionally.
|
|
27
|
+
*/
|
|
28
|
+
pop(count: number): void;
|
|
29
|
+
/** Finish the track, closing any open group. */
|
|
30
|
+
finish(): void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=producer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"producer.d.ts","sourceRoot":"","sources":["../../src/window/producer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAGrC,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAE1E;;;;;;;;GAQG;AACH,qBAAa,QAAQ,CAAC,CAAC;;IAQtB,gDAAgD;IAChD,YAAY,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAE,cAAmB,EAGjE;IAED,yCAAyC;IACzC,IAAI,MAAM,IAAI,CAAC,EAAE,CAEhB;IAED,oDAAoD;IACpD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,mDAAmD;IACnD,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAGnB;IAED;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIvB;IAmCD,gDAAgD;IAChD,MAAM,IAAI,IAAI,CAQb;CACD"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* @ts-self-types="./producer.d.ts" */
|
|
2
|
+
import { Time } from "@norskvideo/moq-net";
|
|
3
|
+
import { Encoder } from "./encoder.js";
|
|
4
|
+
/**
|
|
5
|
+
* Publishes a sliding window of JSON records to a track.
|
|
6
|
+
*
|
|
7
|
+
* An {@link Encoder} that owns its track: it writes each encoded frame and rolls a group whenever
|
|
8
|
+
* the encoder emits a header. When something else already owns the track, use the {@link Encoder}
|
|
9
|
+
* directly.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export class Producer {
|
|
14
|
+
#track;
|
|
15
|
+
#encoder;
|
|
16
|
+
#finished = false;
|
|
17
|
+
// The group an op would be appended to, open after a header.
|
|
18
|
+
#group;
|
|
19
|
+
/** Wrap a track to publish a window into it. */
|
|
20
|
+
constructor(track, config = {}) {
|
|
21
|
+
this.#track = track;
|
|
22
|
+
this.#encoder = new Encoder(config);
|
|
23
|
+
}
|
|
24
|
+
/** The retained window, oldest first. */
|
|
25
|
+
get window() {
|
|
26
|
+
return this.#encoder.window;
|
|
27
|
+
}
|
|
28
|
+
/** Absolute index of the oldest retained record. */
|
|
29
|
+
get offset() {
|
|
30
|
+
return this.#encoder.offset;
|
|
31
|
+
}
|
|
32
|
+
/** Append one record to the back of the window. */
|
|
33
|
+
push(value) {
|
|
34
|
+
this.#assertOpen();
|
|
35
|
+
this.#write(this.#encoder.push(value));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Drop `count` records from the front of the window.
|
|
39
|
+
*
|
|
40
|
+
* A no-op when the window is already empty, and clamped to what it holds, so a caller can trim
|
|
41
|
+
* unconditionally.
|
|
42
|
+
*/
|
|
43
|
+
pop(count) {
|
|
44
|
+
this.#assertOpen();
|
|
45
|
+
const frame = this.#encoder.pop(count);
|
|
46
|
+
if (frame)
|
|
47
|
+
this.#write(frame);
|
|
48
|
+
}
|
|
49
|
+
#assertOpen() {
|
|
50
|
+
if (this.#finished)
|
|
51
|
+
throw new Error("track is closed");
|
|
52
|
+
}
|
|
53
|
+
#write(encoded) {
|
|
54
|
+
// A throw leaves the edit uncommitted and the retained window unchanged. The next edit opens a
|
|
55
|
+
// new group because the attempted frame advanced the group-local compression state.
|
|
56
|
+
if (encoded.keyframe) {
|
|
57
|
+
// The previous group is complete; no more frames will be appended to it. Drop the handle
|
|
58
|
+
// before opening the next one, so a failure below doesn't leave a closed group behind.
|
|
59
|
+
this.#group?.close();
|
|
60
|
+
this.#group = undefined;
|
|
61
|
+
const group = this.#track.appendGroup();
|
|
62
|
+
try {
|
|
63
|
+
group.writeFrame({ payload: encoded.payload, timestamp: Time.Timestamp.now() });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// The group carries no frames, so close it rather than leaving it open on the track. A
|
|
67
|
+
// consumer that already advanced into it would otherwise block with nothing to read.
|
|
68
|
+
group.close();
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
this.#group = group;
|
|
72
|
+
encoded.commit();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!this.#group)
|
|
76
|
+
throw new Error("op with no open group");
|
|
77
|
+
this.#group.writeFrame({ payload: encoded.payload, timestamp: Time.Timestamp.now() });
|
|
78
|
+
encoded.commit();
|
|
79
|
+
}
|
|
80
|
+
/** Finish the track, closing any open group. */
|
|
81
|
+
finish() {
|
|
82
|
+
if (this.#finished)
|
|
83
|
+
return;
|
|
84
|
+
this.#finished = true;
|
|
85
|
+
this.#group?.close();
|
|
86
|
+
this.#group = undefined;
|
|
87
|
+
this.#track.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=producer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"producer.js","sourceRoot":"","sources":["../../src/window/producer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAgB,OAAO,EAAuB,MAAM,cAAc,CAAC;AAE1E;;;;;;;;GAQG;AACH,MAAM,OAAO,QAAQ;IACpB,MAAM,CAAqB;IAC3B,QAAQ,CAAa;IACrB,SAAS,GAAG,KAAK,CAAC;IAElB,6DAA6D;IAC7D,MAAM,CAAsB;IAE5B,gDAAgD;IAChD,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,yCAAyC;IACzC,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,oDAAoD;IACpD,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,mDAAmD;IACnD,IAAI,CAAC,KAAQ;QACZ,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACxC,CAAC;IAED;;;;;OAKG;IACH,GAAG,CAAC,KAAa;QAChB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,KAAK;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,WAAW;QACV,IAAI,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,CAAC,OAAqC;QAC3C,+FAA+F;QAC/F,oFAAoF;QACpF,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtB,yFAAyF;YACzF,uFAAuF;YACvF,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;YAExB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC;gBACJ,KAAK,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACjF,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,uFAAuF;gBACvF,qFAAqF;gBACrF,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,MAAM,GAAG,CAAC;YACX,CAAC;YAED,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;YACpB,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO;QACR,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;QAC3D,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACtF,OAAO,CAAC,MAAM,EAAE,CAAC;IAClB,CAAC;IAED,gDAAgD;IAChD,MAAM;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QAEtB,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QAExB,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 { type Encoded, Encoder, type ProducerConfig } from \"./encoder.ts\";\n\n/**\n * Publishes a sliding window of JSON records to a track.\n *\n * An {@link Encoder} that owns its track: it writes each encoded frame and rolls a group whenever\n * the encoder emits a header. When something else already owns the track, use the {@link Encoder}\n * directly.\n *\n * @public\n */\nexport class Producer<T> {\n\t#track: Moq.Track.Producer;\n\t#encoder: Encoder<T>;\n\t#finished = false;\n\n\t// The group an op would be appended to, open after a header.\n\t#group?: Moq.Group.Producer;\n\n\t/** Wrap a track to publish a window 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/** The retained window, oldest first. */\n\tget window(): T[] {\n\t\treturn this.#encoder.window;\n\t}\n\n\t/** Absolute index of the oldest retained record. */\n\tget offset(): number {\n\t\treturn this.#encoder.offset;\n\t}\n\n\t/** Append one record to the back of the window. */\n\tpush(value: T): void {\n\t\tthis.#assertOpen();\n\t\tthis.#write(this.#encoder.push(value));\n\t}\n\n\t/**\n\t * Drop `count` records from the front of the window.\n\t *\n\t * A no-op when the window is already empty, and clamped to what it holds, so a caller can trim\n\t * unconditionally.\n\t */\n\tpop(count: number): void {\n\t\tthis.#assertOpen();\n\t\tconst frame = this.#encoder.pop(count);\n\t\tif (frame) this.#write(frame);\n\t}\n\n\t#assertOpen(): void {\n\t\tif (this.#finished) throw new Error(\"track is closed\");\n\t}\n\n\t#write(encoded: Encoded & { commit(): void }): void {\n\t\t// A throw leaves the edit uncommitted and the retained window unchanged. The next edit opens a\n\t\t// new group because the attempted frame advanced the group-local compression state.\n\t\tif (encoded.keyframe) {\n\t\t\t// The previous group is complete; no more frames will be appended to it. Drop the handle\n\t\t\t// before opening the next one, so a failure below doesn't leave a closed group behind.\n\t\t\tthis.#group?.close();\n\t\t\tthis.#group = undefined;\n\n\t\t\tconst group = this.#track.appendGroup();\n\t\t\ttry {\n\t\t\t\tgroup.writeFrame({ payload: encoded.payload, timestamp: Time.Timestamp.now() });\n\t\t\t} catch (err) {\n\t\t\t\t// The group carries no frames, so close it rather than leaving it open on the track. A\n\t\t\t\t// consumer that already advanced into it would otherwise block with nothing to read.\n\t\t\t\tgroup.close();\n\t\t\t\tthrow err;\n\t\t\t}\n\n\t\t\tthis.#group = group;\n\t\t\tencoded.commit();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.#group) throw new Error(\"op with no open group\");\n\t\tthis.#group.writeFrame({ payload: encoded.payload, timestamp: Time.Timestamp.now() });\n\t\tencoded.commit();\n\t}\n\n\t/** Finish the track, closing any open group. */\n\tfinish(): void {\n\t\tif (this.#finished) return;\n\t\tthis.#finished = true;\n\n\t\tthis.#group?.close();\n\t\tthis.#group = undefined;\n\n\t\tthis.#track.close();\n\t}\n}\n"]}
|
package/diff.test.d.ts
DELETED
package/diff.test.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"diff.test.d.ts","sourceRoot":"","sources":["../src/diff.test.ts"],"names":[],"mappings":""}
|
package/diff.test.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/* @ts-self-types="./diff.test.d.ts" */
|
|
2
|
-
import { expect, test } from "bun:test";
|
|
3
|
-
import { deepEqual, diff, merge } from "./diff.js";
|
|
4
|
-
// Applying the patch to old should reproduce new (RFC 7396 semantics).
|
|
5
|
-
function assertRoundtrip(oldVal, newVal) {
|
|
6
|
-
const result = diff(oldVal, newVal);
|
|
7
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
8
|
-
expect(merge(oldVal, result.patch)).toEqual(newVal);
|
|
9
|
-
}
|
|
10
|
-
test("changed scalar", () => {
|
|
11
|
-
assertRoundtrip({ a: 1, b: 2 }, { a: 1, b: 3 });
|
|
12
|
-
});
|
|
13
|
-
test("added key", () => {
|
|
14
|
-
const result = diff({ a: 1 }, { a: 1, b: 2 });
|
|
15
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
16
|
-
expect(result.patch).toEqual({ b: 2 });
|
|
17
|
-
});
|
|
18
|
-
test("removed key is null", () => {
|
|
19
|
-
const result = diff({ a: 1, b: 2 }, { a: 1 });
|
|
20
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
21
|
-
expect(result.patch).toEqual({ b: null });
|
|
22
|
-
assertRoundtrip({ a: 1, b: 2 }, { a: 1 });
|
|
23
|
-
});
|
|
24
|
-
test("nested object only includes changed keys", () => {
|
|
25
|
-
const result = diff({ o: { x: 1, y: 2 } }, { o: { x: 1, y: 9 } });
|
|
26
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
27
|
-
expect(result.patch).toEqual({ o: { y: 9 } });
|
|
28
|
-
});
|
|
29
|
-
test("changed array is a wholesale delta", () => {
|
|
30
|
-
const result = diff({ a: [1, 2] }, { a: [1, 2, 3] });
|
|
31
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
32
|
-
expect(result.patch).toEqual({ a: [1, 2, 3] });
|
|
33
|
-
assertRoundtrip({ a: [1, 2] }, { a: [1, 2, 3] });
|
|
34
|
-
});
|
|
35
|
-
test("added array is a delta", () => {
|
|
36
|
-
const result = diff({ a: 1 }, { a: 1, b: [1] });
|
|
37
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
38
|
-
expect(result.patch).toEqual({ b: [1] });
|
|
39
|
-
});
|
|
40
|
-
test("nested array is a delta", () => {
|
|
41
|
-
const result = diff({ o: { x: 1 } }, { o: { x: 1, list: [1] } });
|
|
42
|
-
expect(result.forcedSnapshot).toBe(false);
|
|
43
|
-
expect(result.patch).toEqual({ o: { list: [1] } });
|
|
44
|
-
assertRoundtrip({ o: { x: 1 } }, { o: { x: 1, list: [1] } });
|
|
45
|
-
});
|
|
46
|
-
test("set to null forces snapshot", () => {
|
|
47
|
-
expect(diff({ a: 1 }, { a: null }).forcedSnapshot).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
test("replacing object with scalar", () => {
|
|
50
|
-
assertRoundtrip({ a: { x: 1 } }, { a: 5 });
|
|
51
|
-
});
|
|
52
|
-
test("non-object root forces snapshot", () => {
|
|
53
|
-
expect(diff(1, 2).forcedSnapshot).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
test("deepEqual", () => {
|
|
56
|
-
expect(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] })).toBe(true);
|
|
57
|
-
expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
|
|
58
|
-
expect(deepEqual([1, 2], [1, 2, 3])).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
//# sourceMappingURL=diff.test.js.map
|
package/diff.test.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"diff.test.js","sourceRoot":"","sources":["../src/diff.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, test } from \"bun:test\";\nimport { deepEqual, diff, merge } from \"./diff.ts\";\n\n// Applying the patch to old should reproduce new (RFC 7396 semantics).\nfunction assertRoundtrip(oldVal: unknown, newVal: unknown) {\n\tconst result = diff(oldVal, newVal);\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(merge(oldVal, result.patch)).toEqual(newVal as object);\n}\n\ntest(\"changed scalar\", () => {\n\tassertRoundtrip({ a: 1, b: 2 }, { a: 1, b: 3 });\n});\n\ntest(\"added key\", () => {\n\tconst result = diff({ a: 1 }, { a: 1, b: 2 });\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(result.patch).toEqual({ b: 2 });\n});\n\ntest(\"removed key is null\", () => {\n\tconst result = diff({ a: 1, b: 2 }, { a: 1 });\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(result.patch).toEqual({ b: null });\n\tassertRoundtrip({ a: 1, b: 2 }, { a: 1 });\n});\n\ntest(\"nested object only includes changed keys\", () => {\n\tconst result = diff({ o: { x: 1, y: 2 } }, { o: { x: 1, y: 9 } });\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(result.patch).toEqual({ o: { y: 9 } });\n});\n\ntest(\"changed array is a wholesale delta\", () => {\n\tconst result = diff({ a: [1, 2] }, { a: [1, 2, 3] });\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(result.patch).toEqual({ a: [1, 2, 3] });\n\tassertRoundtrip({ a: [1, 2] }, { a: [1, 2, 3] });\n});\n\ntest(\"added array is a delta\", () => {\n\tconst result = diff({ a: 1 }, { a: 1, b: [1] });\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(result.patch).toEqual({ b: [1] });\n});\n\ntest(\"nested array is a delta\", () => {\n\tconst result = diff({ o: { x: 1 } }, { o: { x: 1, list: [1] } });\n\texpect(result.forcedSnapshot).toBe(false);\n\texpect(result.patch).toEqual({ o: { list: [1] } });\n\tassertRoundtrip({ o: { x: 1 } }, { o: { x: 1, list: [1] } });\n});\n\ntest(\"set to null forces snapshot\", () => {\n\texpect(diff({ a: 1 }, { a: null }).forcedSnapshot).toBe(true);\n});\n\ntest(\"replacing object with scalar\", () => {\n\tassertRoundtrip({ a: { x: 1 } }, { a: 5 });\n});\n\ntest(\"non-object root forces snapshot\", () => {\n\texpect(diff(1, 2).forcedSnapshot).toBe(true);\n});\n\ntest(\"deepEqual\", () => {\n\texpect(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] })).toBe(true);\n\texpect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);\n\texpect(deepEqual([1, 2], [1, 2, 3])).toBe(false);\n});\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"compression.test.d.ts","sourceRoot":"","sources":["../../src/snapshot/compression.test.ts"],"names":[],"mappings":""}
|