import { is_ordinary_element_node } from "../../core/node-guards.js"; import { is_persisted_quid } from "../../core/persisted-quid.js"; import type { HsonNode, Primitive } from "../../core/types.js"; import type { DocumentLiveMapMode, LiveMapDocumentCommitTarget, LiveMapDocumentRequestTarget, } from "../../types/livemap.types.js"; import type { LiveMapDocumentIdentityOverlay } from "./livemap.document.identity.js"; import { LiveMapDocumentPathError, resolve_document_path, validate_document_path, } from "./livemap.document.path.js"; import { LiveMapDocumentMutationError } from "./livemap.error.js"; export type LiveMapDocumentOperation = LiveMapDocumentMutationError["operation"]; /** Validate and detach one active request target. */ export function normalize_document_request_target( input: unknown, operation: LiveMapDocumentOperation, ): LiveMapDocumentRequestTarget { if (!is_plain_record(input) || input.kind !== "path") { throw document_error("INVALID_DOCUMENT_TARGET", operation, "target must discriminate kind as path"); } if (!has_exact_keys(input, ["kind", "path"])) { throw document_error("INVALID_DOCUMENT_TARGET", operation, "path request must contain only kind and path"); } return Object.freeze({ kind: "path", path: validate_path(input.path, operation) }); } /** Normalize an untrusted path target at the active request boundary. */ export const normalize_document_target = normalize_document_request_target; /** Validate one path-authoritative canonical operation target. */ export function normalize_document_commit_target( input: unknown, operation: LiveMapDocumentOperation, ): LiveMapDocumentCommitTarget { if (!is_plain_record(input) || input.kind !== "path") { throw document_error("INVALID_DOCUMENT_COMMIT_TARGET", operation, "canonical target must discriminate kind as path"); } if (!has_only_keys(input, ["kind", "path", "witness"]) || !Object.hasOwn(input, "path")) { throw document_error("INVALID_DOCUMENT_COMMIT_TARGET", operation, "canonical target contains missing or unknown fields"); } const path = validate_path(input.path, operation); if (!Object.hasOwn(input, "witness")) return Object.freeze({ kind: "path", path }); const witness = normalize_witness(input.witness, operation); return Object.freeze({ kind: "path", path, witness }); } /** Resolve a live request without changing its compatibility semantics. */ export function resolve_document_target( root: HsonNode, mode: DocumentLiveMapMode, overlay: LiveMapDocumentIdentityOverlay, target: LiveMapDocumentRequestTarget, operation: LiveMapDocumentOperation, ): HsonNode | Primitive { void overlay; return resolve_path(root, mode, validate_path(target.path, operation), operation); } /** * Lower one active path request to the path-authoritative target stored in a * new commit. */ export function canonicalize_document_request_target( root: HsonNode, mode: DocumentLiveMapMode, overlay: LiveMapDocumentIdentityOverlay, input: unknown, operation: LiveMapDocumentOperation, ): Readonly<{ target: LiveMapDocumentCommitTarget; endpoint: HsonNode | Primitive; }> { const request = normalize_document_request_target(input, operation); void overlay; const path = validate_path(request.path, operation); return Object.freeze({ target: Object.freeze({ kind: "path", path }), endpoint: resolve_path(root, mode, path, operation), }); } /** Resolve only the path and treat an optional witness as non-routing evidence. */ export function resolve_document_commit_target( root: HsonNode, mode: DocumentLiveMapMode, overlay: LiveMapDocumentIdentityOverlay, input: unknown, operation: LiveMapDocumentOperation, ): Readonly<{ target: LiveMapDocumentCommitTarget; endpoint: HsonNode | Primitive; }> { const target = normalize_document_commit_target(input, operation); const endpoint = resolve_path(root, mode, target.path, operation); if (target.witness !== undefined && is_ordinary_element_node(endpoint)) { const activeQuid = overlay.quidAtPath(target.path); if (activeQuid !== undefined && activeQuid !== target.witness.quid) { throw document_error( "DOCUMENT_WITNESS_MISMATCH", operation, `path endpoint carries ${JSON.stringify(activeQuid)}, not witness ${JSON.stringify(target.witness.quid)}`, ); } } return Object.freeze({ target, endpoint }); } export function require_document_attr_element( endpoint: HsonNode | Primitive, operation: LiveMapDocumentOperation, ): HsonNode { if (!is_ordinary_element_node(endpoint)) { throw document_error("DOCUMENT_TARGET_KIND", operation, "target must resolve to an ordinary document element"); } return endpoint; } function resolve_path( root: HsonNode, mode: DocumentLiveMapMode, path: ReturnType, operation: LiveMapDocumentOperation, ): HsonNode | Primitive { try { return resolve_document_path(root, mode, path); } catch (cause) { throw map_path_error(cause, operation); } } function validate_path( input: unknown, operation: LiveMapDocumentOperation, ): ReturnType { try { return validate_document_path(input); } catch (cause) { throw map_path_error(cause, operation); } } function normalize_witness( input: unknown, operation: LiveMapDocumentOperation, ): Readonly<{ quid: string }> { if (!is_plain_record(input) || !has_exact_keys(input, ["quid"]) || !is_persisted_quid(input.quid)) { throw document_error("INVALID_DOCUMENT_WITNESS", operation, "witness must contain one canonical persisted QUID"); } return Object.freeze({ quid: input.quid }); } function map_path_error(cause: unknown, operation: LiveMapDocumentOperation): LiveMapDocumentMutationError { if (!(cause instanceof LiveMapDocumentPathError)) { return document_error("INVALID_DOCUMENT_PATH", operation, "canonical path processing failed", cause); } if (cause.code === "MALFORMED_DOCUMENT_PATH") { return document_error("INVALID_DOCUMENT_PATH", operation, cause.message, cause); } if (cause.code === "INVALID_DOCUMENT_PATH_INDEX") { return document_error("INVALID_DOCUMENT_PATH_INDEX", operation, cause.message, cause); } if (cause.code === "DOCUMENT_PATH_PRIMITIVE_DESCENT") { return document_error("DOCUMENT_PATH_PRIMITIVE_DESCENT", operation, cause.message, cause); } if (cause.code === "DOCUMENT_PATH_OUT_OF_RANGE") { return document_error("DOCUMENT_PATH_OUT_OF_RANGE", operation, cause.message, cause); } return document_error("DOCUMENT_TARGET_NOT_FOUND", operation, cause.message, cause); } function document_error( code: LiveMapDocumentMutationError["code"], operation: LiveMapDocumentOperation, reason: string, cause?: unknown, ): LiveMapDocumentMutationError { return new LiveMapDocumentMutationError(code, operation, reason, cause === undefined ? undefined : { cause }); } function has_exact_keys(value: Readonly>, keys: readonly string[]): boolean { const actual = Object.keys(value); return actual.length === keys.length && actual.every((key) => keys.includes(key)); } function has_only_keys(value: Readonly>, keys: readonly string[]): boolean { return Object.keys(value).every((key) => keys.includes(key)); } function is_plain_record(value: unknown): value is Readonly> { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; }