import { assert_invariants } from "../../core/assert-invariants.js"; import { normalize_hson_array_index_order } from "../../core/hson-array-indexes.js"; import { normalize_empty_hson_metadata } from "../../core/normalize-hson-graph.js"; import { is_Node } from "../../core/node-guards.js"; import type { HsonAttrs, HsonMeta, HsonNode, JsonValue, Primitive } from "../../core/types.js"; import { is_hson_metadata_key, } from "../../core/hson-metadata.js"; import { parse_hson } from "../transform/parsers/parse-hson.js"; import { parse_json } from "../transform/parsers/parse-json.js"; import { json_value_from_node } from "../transform/serializers/serialize-json.js"; import { serialize_hson } from "../transform/serializers/serialize-hson.js"; import { detach_hson_root_value } from "../transform/utils/node-utils/detach-hson-root-value.js"; import type { DocumentLiveMapCapture, DocumentLiveMapMode } from "../../types/livemap.types.js"; import { classify_live_root_mode } from "./livemap.document.js"; import { is_typed_css_value } from "../../core/inline-style.js"; import { build_livemap_document_identity_overlay, LiveMapDocumentIdentityError, } from "./livemap.document.identity.js"; import { ViewStateSnapshotCodecError, type ViewStateSnapshotCodecErrorCode, } from "./livemap.document.view-state-codec.error.js"; const FORMAT = "view-state" as const; const CAPTURE_KIND = "hson-document" as const; const DEFAULT_MAX_PAYLOAD_BYTES = 4 * 1_024 * 1_024; const DEFAULT_MAX_DEPTH = 256; const DEFAULT_MAX_NODES = 100_000; const textEncoder = new TextEncoder(); /** @internal */ export type ViewStateSnapshotEncoding = Readonly<{ format: typeof FORMAT; payload: string; }>; /** @internal Internal limits; overrides exist for focused boundary testing only. */ export type ViewStateSnapshotCodecOptions = Readonly<{ maxPayloadBytes?: number; maxDepth?: number; maxNodes?: number; }>; type CodecLimits = Readonly<{ maxPayloadBytes: number; maxDepth: number; maxNodes: number; }>; type CodecEntry = Readonly<{ key: string; value: CodecValue; }>; type CodecBag = | Readonly<{ presence: "absent" }> | Readonly<{ presence: "present"; entries: readonly CodecEntry[] }>; type CodecValue = | Readonly<{ type: "undefined" }> | Readonly<{ type: "node"; tag: string; attrs: CodecBag; meta: CodecBag; content: readonly CodecValue[] }> | Readonly<{ type: "string"; value: string }> | Readonly<{ type: "number"; value: number }> | Readonly<{ type: "boolean"; value: boolean }> | Readonly<{ type: "null" }> | Readonly<{ type: "array"; items: readonly CodecValue[] }> | Readonly<{ type: "record"; entries: readonly CodecEntry[] }>; type CodecPayload = Readonly<{ captureKind: typeof CAPTURE_KIND; mode: DocumentLiveMapMode; revision: number; root: CodecValue; }>; type ExactValuePayload = Readonly<{ valueKind: "canonical-graph-content"; value: CodecValue; }>; type Budget = { nodes: number; }; /** @internal Encode one validated document capture as deterministic compact Hson data. */ export function encode_view_state_snapshot( capture: DocumentLiveMapCapture, options?: ViewStateSnapshotCodecOptions, ): ViewStateSnapshotEncoding { validate_capture_header(capture); const limits = codec_limits(options); validate_canonical_document(capture.root, capture.mode); return Object.freeze({ format: FORMAT, payload: encode_view_state_payload(capture, limits), }); } function encode_view_state_payload( capture: DocumentLiveMapCapture, limits: CodecLimits, preserveEmptyMetadata = false, ): string { const budget: Budget = { nodes: 0 }; const payloadValue: CodecPayload = { captureKind: CAPTURE_KIND, mode: capture.mode, revision: capture.rev, root: encode_value(capture.root, 1, budget, limits, preserveEmptyMetadata), }; let payload: string; try { payload = serialize_hson( detach_hson_root_value(parse_json(codec_json_value(payloadValue))), { noBreak: true }, ); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot representation could not be serialized.", cause, ); } assert_payload_size(payload, limits); return payload; } /** @internal Decode one canonical compact Hson payload into a detached document capture. */ export function decode_view_state_snapshot( encoded: ViewStateSnapshotEncoding, options?: ViewStateSnapshotCodecOptions, ): DocumentLiveMapCapture { validate_encoding_wrapper(encoded); const limits = codec_limits(options); assert_payload_size(encoded.payload, limits); let parsedNode: HsonNode; try { parsedNode = parse_hson(encoded.payload); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_SYNTAX_INVALID", "View-state snapshot payload is not valid Hson.", cause, ); } let representation: JsonValue; try { representation = json_value_from_node(parsedNode); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot payload does not contain a valid codec representation.", cause, ); } const payload = decode_payload(representation, limits); const decodedInput: DocumentLiveMapCapture = { kind: CAPTURE_KIND, mode: payload.mode, rev: payload.revision, root: payload.root, }; const deterministicInputPayload = encode_view_state_payload( decodedInput, limits, true, ); if (deterministicInputPayload !== encoded.payload) { throw codec_error( "VIEW_STATE_SNAPSHOT_ROUND_TRIP_MISMATCH", "View-state snapshot payload is not the deterministic canonical representation.", ); } let normalizedRoot: HsonNode; try { normalizedRoot = normalize_hson_array_index_order( normalize_empty_hson_metadata(payload.root), "decode_view_state_snapshot", ); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_GRAPH_INVALID", "View-state snapshot graph is invalid.", cause, ); } const capture: DocumentLiveMapCapture = Object.freeze({ kind: CAPTURE_KIND, mode: payload.mode, rev: payload.revision, root: normalizedRoot, }); validate_canonical_document(capture.root, capture.mode); return capture; } /** * Internal exact-value primitive shared with Locus's graph-content boundary. * The returned string is deterministic Hson data, not source-style document Hson. */ export function encode_exact_hson_value( value: HsonNode | Primitive, options?: ViewStateSnapshotCodecOptions, ): string { const limits = codec_limits(options); return encode_exact_hson_value_representation(value, limits); } function encode_exact_hson_value_representation( value: HsonNode | Primitive, limits: CodecLimits, preserveEmptyMetadata = false, ): string { const payloadValue: ExactValuePayload = { valueKind: "canonical-graph-content", value: encode_value(value, 1, { nodes: 0 }, limits, preserveEmptyMetadata), }; let payload: string; try { payload = serialize_hson( detach_hson_root_value(parse_json(payloadValue as JsonValue)), { noBreak: true }, ); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "Exact Hson value representation could not be serialized.", cause, ); } assert_payload_size(payload, limits); return payload; } /** Internal inverse of `encode_exact_hson_value`. */ export function decode_exact_hson_value( payload: string, options?: ViewStateSnapshotCodecOptions, ): HsonNode | Primitive { const limits = codec_limits(options); assert_payload_size(payload, limits); let parsedNode: HsonNode; try { parsedNode = parse_hson(payload); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_SYNTAX_INVALID", "Exact Hson value payload is not valid Hson.", cause, ); } const representation = json_value_from_node(parsedNode); const record = exact_record(representation, ["valueKind", "value"]); if (record.valueKind !== "canonical-graph-content") { throw invalid_representation(); } const value = decode_value(record.value, 1, { nodes: 0 }, limits); if (!is_Node(value) && !is_primitive(value)) throw invalid_representation(); const deterministicInput = encode_exact_hson_value_representation(value, limits, true); if (deterministicInput !== payload) { throw codec_error( "VIEW_STATE_SNAPSHOT_ROUND_TRIP_MISMATCH", "Exact Hson value payload is not deterministic.", ); } if (!is_Node(value)) return value; try { const normalized = normalize_hson_array_index_order( normalize_empty_hson_metadata(value), "decode_exact_hson_value", ); assert_invariants(normalized, "decode_exact_hson_value"); return normalized; } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_GRAPH_INVALID", "Exact Hson graph content is invalid.", cause, ); } } function encode_value( value: unknown, depth: number, budget: Budget, limits: CodecLimits, preserveEmptyMetadata = false, allowUndefined = false, ): CodecValue { assert_depth(depth, limits); if (value === undefined && allowUndefined) return { type: "undefined" }; if (is_Node(value)) { budget.nodes += 1; if (budget.nodes > limits.maxNodes) { throw codec_error( "VIEW_STATE_SNAPSHOT_NODE_LIMIT", "View-state snapshot graph exceeds the node-count limit.", ); } return { type: "node", tag: value.$_tag, attrs: encode_bag(value, "$_attrs", value.$_attrs, depth + 1, budget, limits, preserveEmptyMetadata), meta: encode_bag(value, "$_meta", value.$_meta, depth + 1, budget, limits, preserveEmptyMetadata), content: value.$_content.map((item) => encode_value(item, depth + 1, budget, limits, preserveEmptyMetadata)), }; } if (typeof value === "string") return { type: "string", value }; if (typeof value === "number") { if (!Number.isFinite(value)) { throw codec_error( "VIEW_STATE_SNAPSHOT_NON_FINITE_NUMBER", "View-state snapshot contains a non-finite number.", ); } return { type: "number", value }; } if (typeof value === "boolean") return { type: "boolean", value }; if (value === null) return { type: "null" }; if (Array.isArray(value)) { return { type: "array", items: value.map((item) => encode_value(item, depth + 1, budget, limits, preserveEmptyMetadata)) }; } if (is_plain_record(value)) { return { type: "record", entries: encode_entries(value, depth + 1, budget, limits) }; } throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot contains an unsupported value.", ); } function encode_bag( owner: HsonNode, key: "$_attrs" | "$_meta", value: HsonAttrs | HsonMeta | undefined, depth: number, budget: Budget, limits: CodecLimits, preserveEmptyMetadata: boolean, ): CodecBag { if (!Object.hasOwn(owner, key)) return { presence: "absent" }; if (!is_plain_record(value)) { throw codec_error( "VIEW_STATE_SNAPSHOT_GRAPH_INVALID", "View-state snapshot graph contains an invalid field bag.", ); } if (key === "$_meta" && !preserveEmptyMetadata && Object.keys(value).length === 0) { return { presence: "absent" }; } assert_depth(depth, limits); return { presence: "present", entries: encode_entries(value, depth + 1, budget, limits) }; } function encode_entries( value: Readonly>, depth: number, budget: Budget, limits: CodecLimits, ): readonly CodecEntry[] { assert_depth(depth, limits); const typedCssValue = is_typed_css_value(value); return Object.keys(value).sort().map((key) => ({ key, value: encode_value(value[key], depth + 1, budget, limits, false, typedCssValue && key === "unit"), })); } function decode_payload(value: JsonValue, limits: CodecLimits): Readonly<{ mode: DocumentLiveMapMode; revision: number; root: HsonNode; }> { const record = exact_record(value, ["captureKind", "mode", "revision", "root"]); if (record.captureKind !== CAPTURE_KIND) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot payload declares an unsupported capture representation.", ); } const mode = decode_mode(record.mode); if (typeof record.revision !== "number" || !Number.isSafeInteger(record.revision) || record.revision < 0) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot payload has an invalid revision.", ); } const budget: Budget = { nodes: 0 }; const root = decode_value(record.root, 1, budget, limits); if (!is_Node(root)) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot payload root is not a node.", ); } return { mode, revision: record.revision, root }; } function decode_value( input: unknown, depth: number, budget: Budget, limits: CodecLimits, ): unknown { assert_depth(depth, limits); const tagged = require_record(input); const type = tagged.type; if (type === "undefined") { require_exact_keys(tagged, ["type"]); return undefined; } if (type === "null") { require_exact_keys(tagged, ["type"]); return null; } if (type === "string") { require_exact_keys(tagged, ["type", "value"]); if (typeof tagged.value !== "string") throw invalid_representation(); return tagged.value; } if (type === "number") { require_exact_keys(tagged, ["type", "value"]); if (typeof tagged.value !== "number") throw invalid_representation(); if (!Number.isFinite(tagged.value)) { throw codec_error( "VIEW_STATE_SNAPSHOT_NON_FINITE_NUMBER", "View-state snapshot contains a non-finite number.", ); } return tagged.value; } if (type === "boolean") { require_exact_keys(tagged, ["type", "value"]); if (typeof tagged.value !== "boolean") throw invalid_representation(); return tagged.value; } if (type === "array") { require_exact_keys(tagged, ["type", "items"]); if (!Array.isArray(tagged.items)) throw invalid_representation(); return tagged.items.map((item) => decode_value(item, depth + 1, budget, limits)); } if (type === "record") { require_exact_keys(tagged, ["type", "entries"]); return decode_entries(tagged.entries, depth + 1, budget, limits); } if (type === "node") { require_exact_keys(tagged, ["type", "tag", "attrs", "meta", "content"]); if (typeof tagged.tag !== "string" || !Array.isArray(tagged.content)) throw invalid_representation(); budget.nodes += 1; if (budget.nodes > limits.maxNodes) { throw codec_error( "VIEW_STATE_SNAPSHOT_NODE_LIMIT", "View-state snapshot graph exceeds the node-count limit.", ); } const attrs = decode_bag(tagged.attrs, depth + 1, budget, limits); const meta = decode_bag(tagged.meta, depth + 1, budget, limits); const content = tagged.content.map((item) => decode_value(item, depth + 1, budget, limits)); const output: HsonNode = { $_tag: tagged.tag, $_content: content as HsonNode["$_content"] }; if (attrs !== undefined) output.$_attrs = decode_attrs(attrs); if (meta !== undefined) output.$_meta = decode_meta(meta); return output; } throw invalid_representation(); } function decode_bag( input: unknown, depth: number, budget: Budget, limits: CodecLimits, ): Record | undefined { assert_depth(depth, limits); const bag = require_record(input); if (bag.presence === "absent") { require_exact_keys(bag, ["presence"]); return undefined; } if (bag.presence !== "present") throw invalid_representation(); require_exact_keys(bag, ["presence", "entries"]); return decode_entries(bag.entries, depth + 1, budget, limits); } function decode_entries( input: unknown, depth: number, budget: Budget, limits: CodecLimits, ): Record { assert_depth(depth, limits); if (!Array.isArray(input)) throw invalid_representation(); const output: Record = {}; let previous: string | undefined; for (const item of input) { const entry = exact_record(item, ["key", "value"]); if (typeof entry.key !== "string" || (previous !== undefined && entry.key <= previous)) { throw invalid_representation(); } previous = entry.key; Object.defineProperty(output, entry.key, { value: decode_value(entry.value, depth + 1, budget, limits), enumerable: true, writable: true, configurable: true, }); } return output; } function decode_attrs(value: Record): HsonAttrs { const attrs: HsonAttrs = {}; for (const [key, item] of Object.entries(value)) { if (is_primitive(item)) { Object.defineProperty(attrs, key, { value: item, enumerable: true, writable: true, configurable: true, }); continue; } if (key === "style" && is_style_record(item, new WeakSet())) { attrs.style = item; continue; } throw invalid_representation(); } return attrs; } function decode_meta(value: Record): HsonMeta { const meta: HsonMeta = {}; for (const [key, item] of Object.entries(value)) { if (!is_hson_metadata_key(key) || typeof item !== "string") { throw invalid_representation(); } Object.defineProperty(meta, key, { value: item, enumerable: true, writable: true, configurable: true, }); } return meta; } function validate_capture_header(capture: DocumentLiveMapCapture): void { if (!is_plain_record(capture) || capture.kind !== CAPTURE_KIND || Object.keys(capture).length !== 4 || !is_Node(capture.root)) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot capture has an invalid document representation.", ); } decode_mode(capture.mode); if (!Number.isSafeInteger(capture.rev) || capture.rev < 0) { throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot capture has an invalid revision.", ); } } function validate_canonical_document(root: HsonNode, expectedMode: DocumentLiveMapMode): void { try { assert_invariants(root, "view-state snapshot codec"); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_GRAPH_INVALID", "View-state snapshot graph is invalid.", cause, ); } let mode; try { mode = classify_live_root_mode(root); } catch (cause) { throw codec_error( "VIEW_STATE_SNAPSHOT_GRAPH_INVALID", "View-state snapshot graph cannot be classified.", cause, ); } if (mode !== expectedMode) { throw codec_error( "VIEW_STATE_SNAPSHOT_MODE_MISMATCH", "View-state snapshot document mode does not match its graph.", ); } try { build_livemap_document_identity_overlay(root, expectedMode); } catch (cause) { const code: ViewStateSnapshotCodecErrorCode = cause instanceof LiveMapDocumentIdentityError ? "VIEW_STATE_SNAPSHOT_IDENTITY_INVALID" : "VIEW_STATE_SNAPSHOT_GRAPH_INVALID"; throw codec_error(code, "View-state snapshot document identity is invalid.", cause); } } function validate_encoding_wrapper(encoded: ViewStateSnapshotEncoding): void { if (!is_plain_record(encoded) || encoded.format !== FORMAT) { throw codec_error( "VIEW_STATE_SNAPSHOT_FORMAT_UNKNOWN", "View-state snapshot format is unknown.", ); } require_exact_keys(encoded, ["format", "payload"]); if (typeof encoded.payload !== "string") throw invalid_representation(); } function decode_mode(value: unknown): DocumentLiveMapMode { if (value === "document") return value; throw codec_error( "VIEW_STATE_SNAPSHOT_MODE_MISMATCH", "View-state snapshot mode is not a supported document mode.", ); } function codec_limits(options?: ViewStateSnapshotCodecOptions): CodecLimits { return { maxPayloadBytes: bounded_option(options?.maxPayloadBytes, DEFAULT_MAX_PAYLOAD_BYTES), maxDepth: bounded_option(options?.maxDepth, DEFAULT_MAX_DEPTH), maxNodes: bounded_option(options?.maxNodes, DEFAULT_MAX_NODES), }; } function bounded_option(value: number | undefined, fallback: number): number { if (value === undefined) return fallback; if (Number.isInteger(value) && value >= 0) return value; throw codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot codec limit is invalid.", ); } function assert_payload_size(payload: string, limits: CodecLimits): void { if (textEncoder.encode(payload).byteLength > limits.maxPayloadBytes) { throw codec_error( "VIEW_STATE_SNAPSHOT_PAYLOAD_TOO_LARGE", "View-state snapshot payload exceeds the UTF-8 byte limit.", ); } } function assert_depth(depth: number, limits: CodecLimits): void { if (depth > limits.maxDepth) { throw codec_error( "VIEW_STATE_SNAPSHOT_DEPTH_LIMIT", "View-state snapshot graph exceeds the depth limit.", ); } } function codec_json_value(value: CodecPayload): JsonValue { return value as JsonValue; } function exact_record(value: unknown, keys: readonly string[]): Record { const record = require_record(value); require_exact_keys(record, keys); return record; } function require_record(value: unknown): Record { if (!is_plain_record(value)) throw invalid_representation(); return value; } function require_exact_keys(record: Readonly>, keys: readonly string[]): void { const actual = Object.keys(record); if (actual.length !== keys.length || !keys.every((key) => Object.hasOwn(record, key))) { throw invalid_representation(); } } function is_plain_record(value: unknown): value is Record { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function is_primitive(value: unknown): value is Primitive { return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number"; } function is_style_record(value: unknown, ancestors: WeakSet): value is HsonAttrs["style"] { if (!is_plain_record(value) || ancestors.has(value)) return false; if (is_typed_css_value(value)) return true; ancestors.add(value); for (const item of Object.values(value)) { if (is_primitive(item)) continue; if (!is_style_record(item, ancestors)) { ancestors.delete(value); return false; } } ancestors.delete(value); return true; } function invalid_representation(): ViewStateSnapshotCodecError { return codec_error( "VIEW_STATE_SNAPSHOT_REPRESENTATION_INVALID", "View-state snapshot payload has an invalid representation.", ); } function codec_error( code: ViewStateSnapshotCodecErrorCode, message: string, cause?: unknown, ): ViewStateSnapshotCodecError { return new ViewStateSnapshotCodecError(code, message, cause); }