@norskvideo/moq-json 0.1.4 → 0.1.6
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/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.6",
|
|
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,12 +13,11 @@
|
|
|
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.6",
|
|
17
|
+
"@norskvideo/moq-net": "^0.1.6",
|
|
18
|
+
"@norskvideo/moq-signals": "^0.1.6"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"
|
|
22
|
-
"zod": "^4.5.0"
|
|
21
|
+
"zod": "^4.4.3"
|
|
23
22
|
}
|
|
24
23
|
}
|
package/snapshot/encoder.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/snapshot/encoder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"encoder.d.ts","sourceRoot":"","sources":["../../src/snapshot/encoder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,CAAC,MAAM,UAAU,CAAC;AAUnC,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"}
|
package/snapshot/encoder.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/snapshot/encoder.ts"],"names":[],"mappings":"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","sourcesContent":["import { Encoder as Flate } from \"@moq/flate\";\nimport type * as z from \"@zod/mini\";\n\nimport { deepEqual, diff } from \"../diff.ts\";\n\n// Maximum frames (snapshot + deltas) in a single group before a new snapshot is forced. Kept\n// well below the per-group frame cap so a late joiner can always read the snapshot at frame 0.\nconst MAX_DELTA_FRAMES = 256;\n\n// Delta ratio used when {@link Config.deltaRatio} is left unset. Not re-exported from the package\n// entrypoint: the Producer needs it to know whether to hold a group open, nothing else does.\nexport const DEFAULT_DELTA_RATIO = 8;\n\n/** Options shared by an {@link Encoder} and the {@link Producer} that wraps one. */\nexport interface Config<T> {\n\t// Controls how aggressively the encoder emits deltas (merge patches) instead of full snapshots.\n\t//\n\t// `0` disables deltas: every change is encoded as a new snapshot.\n\t//\n\t// A positive number enables deltas: a new snapshot is emitted once the deltas already written\n\t// to the current group (excluding the snapshot frame) exceed `deltaRatio` times the snapshot size.\n\t// The pending delta is excluded from that check, so the one that first crosses the budget still\n\t// lands before the group rolls. So `1` allows roughly one snapshot's worth of deltas before rolling.\n\t//\n\t// When {@link compression} is on, both sides of the comparison are measured on the compressed frame\n\t// sizes (the real wire cost).\n\t//\n\t// Defaults to `8` when unset.\n\tdeltaRatio?: number;\n\n\t// Optional zod schema used to validate each value before publishing.\n\tschema?: z.ZodMiniType<T>;\n\n\t// Starting value for {@link Producer.mutate} before anything has been published. Required to\n\t// mutate a producer that hasn't published yet (e.g. a fresh catalog); ignored once a value exists.\n\tinitial?: T;\n\n\t// Compress each group as one sync-flushed `deflate-raw` (RFC 1951) stream, so deltas reuse the\n\t// snapshot as context and shrink sharply. Interoperable with the Rust `moq-json` producer.\n\t// `false`/unset (the default) writes plaintext JSON frames. A {@link Decoder} reading them\n\t// must set the same flag.\n\tcompression?: boolean;\n}\n\n/** One encoded frame, and the group boundary it implies. */\nexport interface Encoded {\n\t/** The frame payload, `deflate-raw` compressed when {@link Config.compression} is set. */\n\tpayload: Uint8Array;\n\n\t/**\n\t * Whether this frame is a full snapshot, which must open a new group.\n\t *\n\t * `true` means the caller writes it as the first frame of a fresh group; `false` means it is a\n\t * merge patch that must be appended to the group the last snapshot opened.\n\t *\n\t * The encoder decides this, never the caller: a value that sets a field to JSON null, or whose\n\t * root isn't an object, cannot be expressed as a merge patch at all, and the delta budget and\n\t * frame cap force a snapshot independently of what the caller wanted.\n\t */\n\tkeyframe: boolean;\n}\n\n/**\n * An encoded frame the caller has not yet acknowledged writing, returned by {@link Encoder.update}.\n *\n * Write the frame, then {@link commit}. A frame that is never committed never reached the wire, so\n * the encoder resynchronizes on the next {@link Encoder.update}: it starts a fresh snapshot rather\n * than emitting deltas against a baseline no consumer received.\n *\n * This is a recovery, not a rollback. Producing a delta payload advances the group's DEFLATE window\n * and that can't be undone, so a snapshot is the only sound way back. Forgetting to commit a frame\n * that *was* written is therefore merely wasteful (one redundant snapshot), never incorrect.\n */\nexport interface Pending extends Encoded {\n\t/**\n\t * Acknowledge that the frame reached the wire, keeping the encoder's state.\n\t *\n\t * Only call this once the write has actually succeeded. Committing a frame that failed to write\n\t * is the one thing that corrupts the stream.\n\t */\n\tcommit(): void;\n}\n\n/**\n * Encodes a JSON value into frame payloads, choosing snapshots and deltas automatically.\n *\n * The track-free core of {@link Producer}: it decides *what bytes go in a frame* and *where the\n * group boundaries fall*, and leaves writing them to the caller. Reach for it when something else\n * already owns the track.\n *\n * Frames must reach the wire in the order they were encoded, and a frame with\n * {@link Encoded.keyframe} set must open a new group: both the merge patches and the group-scoped\n * DEFLATE window depend on it. {@link update} hands back a {@link Pending} rather than a bare\n * {@link Encoded} so a frame that never reaches the wire can't silently desync the encoder: leave it\n * uncommitted and the next update resynchronizes with a fresh snapshot.\n *\n * (The Rust `moq-json` encoder does the same thing through `Drop`, so it resynchronizes the moment\n * the frame is discarded. JavaScript has no destructor, so the check happens on the next update\n * instead. The resulting stream is identical either way.)\n *\n * If the caller closes a group for its own reasons, call\n * {@link reset} so the next value is encoded as a snapshot.\n */\nexport class Encoder<T> {\n\t#config: Config<T>;\n\t#compress: boolean;\n\n\t// The last encoded value, normalized through JSON so it matches what landed on the wire. The\n\t// baseline every delta is diffed against, and `undefined` until the first snapshot.\n\t#last?: unknown;\n\n\t// Bytes of deltas already emitted into the current group, excluding the snapshot frame. Compressed\n\t// frame sizes when compressing, raw otherwise, matching {@link #snapshotLen} so the budget check is\n\t// like-for-like (and identical to the Rust encoder).\n\t#deltaBytes = 0;\n\t// Size of the current group's snapshot frame, the reference the delta budget is measured against.\n\t#snapshotLen = 0;\n\t// Frames emitted into the current group, snapshot included.\n\t#groupFrames = 0;\n\n\t// The current group's `deflate-raw` stream, swapped for a fresh one (cold window) at each\n\t// snapshot, so a snapshot and its deltas share one stream.\n\t#flate?: Flate;\n\n\t// Whether the frame from the last {@link update} is still unacknowledged. An uncommitted frame\n\t// never reached the wire, so the next update resynchronizes before encoding anything new.\n\t#pending = false;\n\t// Bumped for each frame handed out, so a commit that arrives after the encoder has moved on can\n\t// tell that it is acknowledging a frame that is no longer the outstanding one.\n\t#generation = 0;\n\n\t// Whether the next frame has to be a full snapshot, because a frame was lost or the caller closed\n\t// the group. Kept separate from {@link #last} so a resync doesn't erase the value: that field is\n\t// also what {@link Producer.mutate} seeds an edit from, and dropping it there would publish a\n\t// document with every other field missing.\n\t#resync = false;\n\n\tconstructor(config: Config<T> = {}) {\n\t\tthis.#config = config;\n\t\tthis.#compress = config.compression ?? false;\n\t}\n\n\t/**\n\t * A copy of the last encoded value, or `undefined` before the first snapshot.\n\t *\n\t * Copied rather than shared: the baseline is what the next delta is diffed against, so a caller\n\t * mutating it in place would make a later change look unchanged and never reach consumers.\n\t */\n\tget value(): T | undefined {\n\t\treturn this.#last === undefined ? undefined : (structuredClone(this.#last) as T);\n\t}\n\n\t/**\n\t * Force the next {@link update} to emit a full snapshot, even for an unchanged value.\n\t *\n\t * Call this whenever the caller closes the current group behind the encoder's back. Without it\n\t * the next value may be encoded as a delta against a DEFLATE window and a baseline that the new\n\t * group doesn't carry.\n\t *\n\t * {@link value} survives: the snapshot republishes it in full anyway, and it is what a caller\n\t * editing in place starts from.\n\t */\n\treset(): void {\n\t\tthis.#flate = undefined;\n\t\tthis.#deltaBytes = 0;\n\t\tthis.#snapshotLen = 0;\n\t\tthis.#groupFrames = 0;\n\t\tthis.#resync = true;\n\t\tthis.#pending = false;\n\t}\n\n\t/**\n\t * Encode a new value, as a snapshot or a delta.\n\t *\n\t * Returns `undefined` when the value is unchanged from the last one encoded, so nothing needs to\n\t * be written. Otherwise the frame comes back as a {@link Pending} the caller writes and then\n\t * commits; leaving one uncommitted resynchronizes the encoder here, on the next call.\n\t */\n\tupdate(value: T): Pending | undefined {\n\t\t// The previous frame was never acknowledged, so it never reached the wire. Drop the baseline\n\t\t// and the DEFLATE window so this value goes out as a fresh snapshot rather than as a patch\n\t\t// against a state no consumer holds.\n\t\tif (this.#pending) this.reset();\n\n\t\tconst valid = this.#config.schema ? this.#config.schema.parse(value) : value;\n\n\t\t// Serialize once; parse it back to a normalized JSON value for diffing and comparison\n\t\t// (dropping `undefined` fields, matching what lands on the wire).\n\t\tconst text = JSON.stringify(valid);\n\t\tif (text === undefined) {\n\t\t\t// `JSON.stringify` yields undefined for a top-level undefined, function, or symbol. Reject it\n\t\t\t// here rather than letting it reach the wire as an unparseable frame.\n\t\t\tthrow new Error(\"value is not representable as JSON\");\n\t\t}\n\n\t\tconst json = JSON.parse(text);\n\n\t\t// A resync has to emit even for an unchanged value: the frame that carried it may never have\n\t\t// landed, so the consumer's state is unknown.\n\t\tif (!this.#resync && this.#last !== undefined && deepEqual(this.#last, json)) return undefined;\n\n\t\tconst delta = this.#delta(json);\n\n\t\t// Compress only after the diff, and advance the baseline only once the payload exists: framing\n\t\t// can throw, and a baseline ahead of the wire would make the next delta unreadable. Matches the\n\t\t// Rust encoder, which completes every fallible step before mutating state.\n\t\tif (delta) {\n\t\t\tconst payload = this.#frame(delta);\n\t\t\tthis.#last = json;\n\t\t\tthis.#deltaBytes += payload.length;\n\t\t\tthis.#groupFrames += 1;\n\t\t\treturn this.#pend({ payload, keyframe: false });\n\t\t}\n\n\t\tconst payload = this.#snapshot(new TextEncoder().encode(text));\n\t\tthis.#last = json;\n\t\treturn this.#pend({ payload, keyframe: true });\n\t}\n\n\t// Hand a frame to the caller, marking it unacknowledged until they commit.\n\t#pend(encoded: Encoded): Pending {\n\t\tthis.#pending = true;\n\t\tconst generation = ++this.#generation;\n\n\t\treturn {\n\t\t\t...encoded,\n\t\t\tcommit: () => {\n\t\t\t\t// A caller that starts the next update before this frame settles has already made the\n\t\t\t\t// encoder resynchronize around it. Acknowledging it now would clear the flag belonging to\n\t\t\t\t// the newer frame, 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\t// Resolved delta ratio: the configured value, or the default when unset. `0` disables deltas.\n\tget #deltaRatio(): number {\n\t\treturn this.#config.deltaRatio ?? DEFAULT_DELTA_RATIO;\n\t}\n\n\t// Build a delta frame, or `undefined` to signal that a fresh snapshot should be emitted.\n\t//\n\t// The budget gate runs first, against the deltas already written, so rolling a new group costs no\n\t// merge-patch work. Since the gate excludes the frame about to be written, the delta that tips the\n\t// group past `ratio * snapshot` still lands: a group overshoots the budget by at most one delta.\n\t#delta(json: unknown): Uint8Array | undefined {\n\t\t// A lost frame, or a group the caller closed, leaves the consumer's state unknown, so the next\n\t\t// frame has to be a full snapshot.\n\t\tif (this.#resync) return undefined;\n\n\t\tconst ratio = this.#deltaRatio;\n\t\tif (ratio === 0) return undefined;\n\t\tif (this.#last === undefined) return undefined;\n\t\tif (this.#groupFrames === 0 || this.#groupFrames >= MAX_DELTA_FRAMES) return undefined;\n\n\t\t// Gate on the deltas accumulated so far (snapshot frame excluded), before computing the patch.\n\t\tif (this.#deltaBytes > ratio * this.#snapshotLen) return undefined;\n\n\t\tconst result = diff(this.#last, json);\n\t\tif (result.forcedSnapshot) return undefined;\n\n\t\treturn new TextEncoder().encode(JSON.stringify(result.patch));\n\t}\n\n\t// Encode a group's snapshot (frame 0), returning its payload. On the compressed path this opens a\n\t// fresh stream (cold window), so the snapshot and its deltas share one DEFLATE window.\n\t#snapshot(frame: Uint8Array): Uint8Array {\n\t\t// Build the window locally and install it only once framing succeeds. Assigning it first would\n\t\t// leave a throw with a cold window in place, no resync pending, and the old group counters\n\t\t// intact, so the next delta would compress against a window the decoder cannot follow. This is\n\t\t// the same ordering the Rust encoder gets by keeping the encoder in a local.\n\t\tconst flate = this.#compress ? new Flate() : undefined;\n\t\tconst payload = flate ? flate.frame(frame) : frame;\n\n\t\tthis.#flate = flate;\n\t\tthis.#snapshotLen = payload.length;\n\t\tthis.#deltaBytes = 0;\n\t\tthis.#groupFrames = 1;\n\t\tthis.#resync = false;\n\n\t\treturn payload;\n\t}\n\n\t// Compress a frame into the current group's window, or pass it through when uncompressed.\n\t#frame(frame: Uint8Array): Uint8Array {\n\t\treturn this.#flate ? this.#flate.frame(frame) : frame;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/snapshot/encoder.ts"],"names":[],"mappings":"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","sourcesContent":["import { Encoder as Flate } from \"@moq/flate\";\nimport type * as z from \"zod/mini\";\n\nimport { deepEqual, diff } from \"../diff.ts\";\n\n// Maximum frames (snapshot + deltas) in a single group before a new snapshot is forced. Kept\n// well below the per-group frame cap so a late joiner can always read the snapshot at frame 0.\nconst MAX_DELTA_FRAMES = 256;\n\n// Delta ratio used when {@link Config.deltaRatio} is left unset. Not re-exported from the package\n// entrypoint: the Producer needs it to know whether to hold a group open, nothing else does.\nexport const DEFAULT_DELTA_RATIO = 8;\n\n/** Options shared by an {@link Encoder} and the {@link Producer} that wraps one. */\nexport interface Config<T> {\n\t// Controls how aggressively the encoder emits deltas (merge patches) instead of full snapshots.\n\t//\n\t// `0` disables deltas: every change is encoded as a new snapshot.\n\t//\n\t// A positive number enables deltas: a new snapshot is emitted once the deltas already written\n\t// to the current group (excluding the snapshot frame) exceed `deltaRatio` times the snapshot size.\n\t// The pending delta is excluded from that check, so the one that first crosses the budget still\n\t// lands before the group rolls. So `1` allows roughly one snapshot's worth of deltas before rolling.\n\t//\n\t// When {@link compression} is on, both sides of the comparison are measured on the compressed frame\n\t// sizes (the real wire cost).\n\t//\n\t// Defaults to `8` when unset.\n\tdeltaRatio?: number;\n\n\t// Optional zod schema used to validate each value before publishing.\n\tschema?: z.ZodMiniType<T>;\n\n\t// Starting value for {@link Producer.mutate} before anything has been published. Required to\n\t// mutate a producer that hasn't published yet (e.g. a fresh catalog); ignored once a value exists.\n\tinitial?: T;\n\n\t// Compress each group as one sync-flushed `deflate-raw` (RFC 1951) stream, so deltas reuse the\n\t// snapshot as context and shrink sharply. Interoperable with the Rust `moq-json` producer.\n\t// `false`/unset (the default) writes plaintext JSON frames. A {@link Decoder} reading them\n\t// must set the same flag.\n\tcompression?: boolean;\n}\n\n/** One encoded frame, and the group boundary it implies. */\nexport interface Encoded {\n\t/** The frame payload, `deflate-raw` compressed when {@link Config.compression} is set. */\n\tpayload: Uint8Array;\n\n\t/**\n\t * Whether this frame is a full snapshot, which must open a new group.\n\t *\n\t * `true` means the caller writes it as the first frame of a fresh group; `false` means it is a\n\t * merge patch that must be appended to the group the last snapshot opened.\n\t *\n\t * The encoder decides this, never the caller: a value that sets a field to JSON null, or whose\n\t * root isn't an object, cannot be expressed as a merge patch at all, and the delta budget and\n\t * frame cap force a snapshot independently of what the caller wanted.\n\t */\n\tkeyframe: boolean;\n}\n\n/**\n * An encoded frame the caller has not yet acknowledged writing, returned by {@link Encoder.update}.\n *\n * Write the frame, then {@link commit}. A frame that is never committed never reached the wire, so\n * the encoder resynchronizes on the next {@link Encoder.update}: it starts a fresh snapshot rather\n * than emitting deltas against a baseline no consumer received.\n *\n * This is a recovery, not a rollback. Producing a delta payload advances the group's DEFLATE window\n * and that can't be undone, so a snapshot is the only sound way back. Forgetting to commit a frame\n * that *was* written is therefore merely wasteful (one redundant snapshot), never incorrect.\n */\nexport interface Pending extends Encoded {\n\t/**\n\t * Acknowledge that the frame reached the wire, keeping the encoder's state.\n\t *\n\t * Only call this once the write has actually succeeded. Committing a frame that failed to write\n\t * is the one thing that corrupts the stream.\n\t */\n\tcommit(): void;\n}\n\n/**\n * Encodes a JSON value into frame payloads, choosing snapshots and deltas automatically.\n *\n * The track-free core of {@link Producer}: it decides *what bytes go in a frame* and *where the\n * group boundaries fall*, and leaves writing them to the caller. Reach for it when something else\n * already owns the track.\n *\n * Frames must reach the wire in the order they were encoded, and a frame with\n * {@link Encoded.keyframe} set must open a new group: both the merge patches and the group-scoped\n * DEFLATE window depend on it. {@link update} hands back a {@link Pending} rather than a bare\n * {@link Encoded} so a frame that never reaches the wire can't silently desync the encoder: leave it\n * uncommitted and the next update resynchronizes with a fresh snapshot.\n *\n * (The Rust `moq-json` encoder does the same thing through `Drop`, so it resynchronizes the moment\n * the frame is discarded. JavaScript has no destructor, so the check happens on the next update\n * instead. The resulting stream is identical either way.)\n *\n * If the caller closes a group for its own reasons, call\n * {@link reset} so the next value is encoded as a snapshot.\n */\nexport class Encoder<T> {\n\t#config: Config<T>;\n\t#compress: boolean;\n\n\t// The last encoded value, normalized through JSON so it matches what landed on the wire. The\n\t// baseline every delta is diffed against, and `undefined` until the first snapshot.\n\t#last?: unknown;\n\n\t// Bytes of deltas already emitted into the current group, excluding the snapshot frame. Compressed\n\t// frame sizes when compressing, raw otherwise, matching {@link #snapshotLen} so the budget check is\n\t// like-for-like (and identical to the Rust encoder).\n\t#deltaBytes = 0;\n\t// Size of the current group's snapshot frame, the reference the delta budget is measured against.\n\t#snapshotLen = 0;\n\t// Frames emitted into the current group, snapshot included.\n\t#groupFrames = 0;\n\n\t// The current group's `deflate-raw` stream, swapped for a fresh one (cold window) at each\n\t// snapshot, so a snapshot and its deltas share one stream.\n\t#flate?: Flate;\n\n\t// Whether the frame from the last {@link update} is still unacknowledged. An uncommitted frame\n\t// never reached the wire, so the next update resynchronizes before encoding anything new.\n\t#pending = false;\n\t// Bumped for each frame handed out, so a commit that arrives after the encoder has moved on can\n\t// tell that it is acknowledging a frame that is no longer the outstanding one.\n\t#generation = 0;\n\n\t// Whether the next frame has to be a full snapshot, because a frame was lost or the caller closed\n\t// the group. Kept separate from {@link #last} so a resync doesn't erase the value: that field is\n\t// also what {@link Producer.mutate} seeds an edit from, and dropping it there would publish a\n\t// document with every other field missing.\n\t#resync = false;\n\n\tconstructor(config: Config<T> = {}) {\n\t\tthis.#config = config;\n\t\tthis.#compress = config.compression ?? false;\n\t}\n\n\t/**\n\t * A copy of the last encoded value, or `undefined` before the first snapshot.\n\t *\n\t * Copied rather than shared: the baseline is what the next delta is diffed against, so a caller\n\t * mutating it in place would make a later change look unchanged and never reach consumers.\n\t */\n\tget value(): T | undefined {\n\t\treturn this.#last === undefined ? undefined : (structuredClone(this.#last) as T);\n\t}\n\n\t/**\n\t * Force the next {@link update} to emit a full snapshot, even for an unchanged value.\n\t *\n\t * Call this whenever the caller closes the current group behind the encoder's back. Without it\n\t * the next value may be encoded as a delta against a DEFLATE window and a baseline that the new\n\t * group doesn't carry.\n\t *\n\t * {@link value} survives: the snapshot republishes it in full anyway, and it is what a caller\n\t * editing in place starts from.\n\t */\n\treset(): void {\n\t\tthis.#flate = undefined;\n\t\tthis.#deltaBytes = 0;\n\t\tthis.#snapshotLen = 0;\n\t\tthis.#groupFrames = 0;\n\t\tthis.#resync = true;\n\t\tthis.#pending = false;\n\t}\n\n\t/**\n\t * Encode a new value, as a snapshot or a delta.\n\t *\n\t * Returns `undefined` when the value is unchanged from the last one encoded, so nothing needs to\n\t * be written. Otherwise the frame comes back as a {@link Pending} the caller writes and then\n\t * commits; leaving one uncommitted resynchronizes the encoder here, on the next call.\n\t */\n\tupdate(value: T): Pending | undefined {\n\t\t// The previous frame was never acknowledged, so it never reached the wire. Drop the baseline\n\t\t// and the DEFLATE window so this value goes out as a fresh snapshot rather than as a patch\n\t\t// against a state no consumer holds.\n\t\tif (this.#pending) this.reset();\n\n\t\tconst valid = this.#config.schema ? this.#config.schema.parse(value) : value;\n\n\t\t// Serialize once; parse it back to a normalized JSON value for diffing and comparison\n\t\t// (dropping `undefined` fields, matching what lands on the wire).\n\t\tconst text = JSON.stringify(valid);\n\t\tif (text === undefined) {\n\t\t\t// `JSON.stringify` yields undefined for a top-level undefined, function, or symbol. Reject it\n\t\t\t// here rather than letting it reach the wire as an unparseable frame.\n\t\t\tthrow new Error(\"value is not representable as JSON\");\n\t\t}\n\n\t\tconst json = JSON.parse(text);\n\n\t\t// A resync has to emit even for an unchanged value: the frame that carried it may never have\n\t\t// landed, so the consumer's state is unknown.\n\t\tif (!this.#resync && this.#last !== undefined && deepEqual(this.#last, json)) return undefined;\n\n\t\tconst delta = this.#delta(json);\n\n\t\t// Compress only after the diff, and advance the baseline only once the payload exists: framing\n\t\t// can throw, and a baseline ahead of the wire would make the next delta unreadable. Matches the\n\t\t// Rust encoder, which completes every fallible step before mutating state.\n\t\tif (delta) {\n\t\t\tconst payload = this.#frame(delta);\n\t\t\tthis.#last = json;\n\t\t\tthis.#deltaBytes += payload.length;\n\t\t\tthis.#groupFrames += 1;\n\t\t\treturn this.#pend({ payload, keyframe: false });\n\t\t}\n\n\t\tconst payload = this.#snapshot(new TextEncoder().encode(text));\n\t\tthis.#last = json;\n\t\treturn this.#pend({ payload, keyframe: true });\n\t}\n\n\t// Hand a frame to the caller, marking it unacknowledged until they commit.\n\t#pend(encoded: Encoded): Pending {\n\t\tthis.#pending = true;\n\t\tconst generation = ++this.#generation;\n\n\t\treturn {\n\t\t\t...encoded,\n\t\t\tcommit: () => {\n\t\t\t\t// A caller that starts the next update before this frame settles has already made the\n\t\t\t\t// encoder resynchronize around it. Acknowledging it now would clear the flag belonging to\n\t\t\t\t// the newer frame, 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\t// Resolved delta ratio: the configured value, or the default when unset. `0` disables deltas.\n\tget #deltaRatio(): number {\n\t\treturn this.#config.deltaRatio ?? DEFAULT_DELTA_RATIO;\n\t}\n\n\t// Build a delta frame, or `undefined` to signal that a fresh snapshot should be emitted.\n\t//\n\t// The budget gate runs first, against the deltas already written, so rolling a new group costs no\n\t// merge-patch work. Since the gate excludes the frame about to be written, the delta that tips the\n\t// group past `ratio * snapshot` still lands: a group overshoots the budget by at most one delta.\n\t#delta(json: unknown): Uint8Array | undefined {\n\t\t// A lost frame, or a group the caller closed, leaves the consumer's state unknown, so the next\n\t\t// frame has to be a full snapshot.\n\t\tif (this.#resync) return undefined;\n\n\t\tconst ratio = this.#deltaRatio;\n\t\tif (ratio === 0) return undefined;\n\t\tif (this.#last === undefined) return undefined;\n\t\tif (this.#groupFrames === 0 || this.#groupFrames >= MAX_DELTA_FRAMES) return undefined;\n\n\t\t// Gate on the deltas accumulated so far (snapshot frame excluded), before computing the patch.\n\t\tif (this.#deltaBytes > ratio * this.#snapshotLen) return undefined;\n\n\t\tconst result = diff(this.#last, json);\n\t\tif (result.forcedSnapshot) return undefined;\n\n\t\treturn new TextEncoder().encode(JSON.stringify(result.patch));\n\t}\n\n\t// Encode a group's snapshot (frame 0), returning its payload. On the compressed path this opens a\n\t// fresh stream (cold window), so the snapshot and its deltas share one DEFLATE window.\n\t#snapshot(frame: Uint8Array): Uint8Array {\n\t\t// Build the window locally and install it only once framing succeeds. Assigning it first would\n\t\t// leave a throw with a cold window in place, no resync pending, and the old group counters\n\t\t// intact, so the next delta would compress against a window the decoder cannot follow. This is\n\t\t// the same ordering the Rust encoder gets by keeping the encoder in a local.\n\t\tconst flate = this.#compress ? new Flate() : undefined;\n\t\tconst payload = flate ? flate.frame(frame) : frame;\n\n\t\tthis.#flate = flate;\n\t\tthis.#snapshotLen = payload.length;\n\t\tthis.#deltaBytes = 0;\n\t\tthis.#groupFrames = 1;\n\t\tthis.#resync = false;\n\n\t\treturn payload;\n\t}\n\n\t// Compress a frame into the current group's window, or pass it through when uncompressed.\n\t#frame(frame: Uint8Array): Uint8Array {\n\t\treturn this.#flate ? this.#flate.frame(frame) : frame;\n\t}\n}\n"]}
|