import type { DocumentLiveMap, LiveMapDocumentApi, LiveMapAuthority, LiveMapGraphCommit, } from "../../types/livemap.types.js"; import type { JsonValue } from "../../core/types.js"; import type { LocusDocumentActionName, } from "../../types/locus.types.js"; import { decode_locus_document_attribute_name, decode_locus_document_attrs, decode_locus_document_attribute_value, decode_locus_document_target, is_locus_json_value, } from "./locus.protocol.js"; import { decode_locus_graph_content } from "./locus.graph-content-codec.js"; export type LocusDocumentActionResolution = | Readonly<{ kind: "not-document-action" }> | Readonly<{ kind: "unavailable"; message: string }> | Readonly<{ kind: "invalid"; message: string }> | Readonly<{ kind: "ready"; payload: JsonValue; execute: (targetMap?: LocusDocumentActionTarget) => LiveMapGraphCommit }>; /** Minimal document mutation target shared by solo maps and selected aggregate drafts. */ export type LocusDocumentActionTarget = Readonly<{ mode: "document"; document: Readonly<{ attrs: Readonly<{ set: (...args: Parameters) => LiveMapGraphCommit; drop: (...args: Parameters) => LiveMapGraphCommit; setMany: (...args: Parameters) => LiveMapGraphCommit; dropMany: (...args: Parameters) => LiveMapGraphCommit; clear: (...args: Parameters) => LiveMapGraphCommit; replace: (...args: Parameters) => LiveMapGraphCommit; }>; content: Readonly<{ replace: (...args: Parameters) => LiveMapGraphCommit; insert: (...args: Parameters) => LiveMapGraphCommit; remove: (...args: Parameters) => LiveMapGraphCommit; move: (...args: Parameters) => LiveMapGraphCommit; }>; }>; }>; export function is_locus_document_action_target(value: unknown): value is LocusDocumentActionTarget { return typeof value === "object" && value !== null && "mode" in value && value.mode === "document" && "document" in value; } const DOCUMENT_ACTION_NAMES: ReadonlySet = new Set([ "document.attrs.set", "document.attrs.drop", "document.attrs.setMany", "document.attrs.dropMany", "document.attrs.clear", "document.attrs.replace", "document.content.replace", "document.content.insert", "document.content.remove", "document.content.move", ]); /** Resolve one reserved built-in without mutating. Execution remains in the normal action pipeline. */ export function resolve_locus_document_action( map: LiveMapAuthority | LocusDocumentActionTarget, name: string, payload: JsonValue | undefined, ): LocusDocumentActionResolution { if (!is_document_action_name(name)) return Object.freeze({ kind: "not-document-action" }); if (!is_document_live_map(map)) { return Object.freeze({ kind: "unavailable", message: `Locus action ${name} is unavailable for projected authorities.`, }); } if (!is_record(payload)) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} requires an object payload.` }); } const target = decode_locus_document_target(payload.target); if (target === undefined) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} target is malformed.` }); } if (name === "document.attrs.set") { if (!has_exact_keys(payload, ["target", "name", "value"])) return invalid_fields(name); const attributeName = decode_locus_document_attribute_name(payload.name); if (attributeName === undefined) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} attribute name is invalid.` }); } const value = decode_locus_document_attribute_value(attributeName, payload.value); if (value === undefined) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} attribute value is invalid.` }); } return Object.freeze({ kind: "ready", payload, execute: (targetMap = map) => document_api_for(targetMap).attrs.set(target, attributeName, value), }); } if (name === "document.attrs.drop") { if (!has_exact_keys(payload, ["target", "name"])) return invalid_fields(name); const attributeName = decode_locus_document_attribute_name(payload.name); if (attributeName === undefined) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} attribute name is invalid.` }); } return Object.freeze({ kind: "ready", payload, execute: (targetMap = map) => document_api_for(targetMap).attrs.drop(target, attributeName), }); } if (name === "document.attrs.setMany" || name === "document.attrs.replace") { if (!has_exact_keys(payload, ["target", "values"])) return invalid_fields(name); const values = decode_locus_document_attrs(payload.values); if (values === undefined) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} values are invalid.` }); } return Object.freeze({ kind: "ready", payload: decoded_action_payload({ target, values }), execute: (targetMap = map) => { const api = document_api_for(targetMap); if (name === "document.attrs.setMany") return api.attrs.setMany(target, values); return api.attrs.replace(target, values); }, }); } if (name === "document.attrs.dropMany") { if (!has_exact_keys(payload, ["target", "names"])) return invalid_fields(name); const names = decode_attribute_names(payload.names); if (names === undefined) { return Object.freeze({ kind: "invalid", message: `Locus action ${name} names are invalid.` }); } return Object.freeze({ kind: "ready", payload: decoded_action_payload({ target, names }), execute: (targetMap = map) => document_api_for(targetMap).attrs.dropMany(target, names), }); } if (name === "document.attrs.clear") { if (!has_exact_keys(payload, ["target"])) return invalid_fields(name); return Object.freeze({ kind: "ready", payload: decoded_action_payload({ target }), execute: (targetMap = map) => document_api_for(targetMap).attrs.clear(target), }); } if (name === "document.content.replace") { if (!has_exact_keys(payload, ["target", "index", "replacement"])) return invalid_fields(name); const index = non_negative_integer(payload.index); if (index === undefined) return invalid_index(name); let replacement; try { replacement = decode_locus_graph_content(payload.replacement); } catch { return Object.freeze({ kind: "invalid", message: `Locus action ${name} replacement is invalid.` }); } return Object.freeze({ kind: "ready", payload, execute: (targetMap = map) => document_api_for(targetMap).content.replace(target, index, replacement), }); } if (name === "document.content.insert") { if (!has_exact_keys(payload, ["target", "index", "content"])) return invalid_fields(name); const index = non_negative_integer(payload.index); if (index === undefined) return invalid_index(name); let content; try { content = decode_locus_graph_content(payload.content); } catch { return Object.freeze({ kind: "invalid", message: `Locus action ${name} content is invalid.` }); } return Object.freeze({ kind: "ready", payload, execute: (targetMap = map) => document_api_for(targetMap).content.insert(target, index, content), }); } if (name === "document.content.remove") { if (!has_exact_keys(payload, ["target", "index"])) return invalid_fields(name); const index = non_negative_integer(payload.index); if (index === undefined) return invalid_index(name); return Object.freeze({ kind: "ready", payload, execute: (targetMap = map) => document_api_for(targetMap).content.remove(target, index), }); } if (!has_exact_keys(payload, ["target", "from", "to"])) return invalid_fields(name); const from = non_negative_integer(payload.from); const to = non_negative_integer(payload.to); if (from === undefined || to === undefined) return invalid_index(name); return Object.freeze({ kind: "ready", payload, execute: (targetMap = map) => document_api_for(targetMap).content.move(target, from, to), }); } function is_document_action_name(name: string): name is LocusDocumentActionName { return DOCUMENT_ACTION_NAMES.has(name); } function invalid_fields(name: LocusDocumentActionName): LocusDocumentActionResolution { return Object.freeze({ kind: "invalid", message: `Locus action ${name} payload fields are malformed.` }); } function invalid_index(name: LocusDocumentActionName): LocusDocumentActionResolution { return Object.freeze({ kind: "invalid", message: `Locus action ${name} content index is invalid.` }); } function non_negative_integer(value: JsonValue | undefined): number | undefined { return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined; } function decode_attribute_names(value: JsonValue | undefined): readonly string[] | undefined { if (!Array.isArray(value)) return undefined; const names: string[] = []; for (const item of value) { const name = decode_locus_document_attribute_name(item); if (name === undefined) return undefined; names.push(name); } return Object.freeze(names); } function decoded_action_payload(value: unknown): JsonValue { if (is_locus_json_value(value)) return value; throw new Error("Decoded Locus document action payload is not canonical JSON."); } function is_document_live_map(map: LiveMapAuthority | LocusDocumentActionTarget): map is DocumentLiveMap | LocusDocumentActionTarget { return (map.mode === "document") && "document" in map; } function document_api(map: DocumentLiveMap | LocusDocumentActionTarget): LocusDocumentActionTarget["document"] { return map.document; } function document_api_for(map: LiveMapAuthority | LocusDocumentActionTarget): LocusDocumentActionTarget["document"] { if (is_document_live_map(map)) return document_api(map); throw new Error("Locus document action draft mode is unavailable."); } function is_record(value: unknown): value is Readonly> { return typeof value === "object" && value !== null && !Array.isArray(value); } function has_exact_keys( value: Readonly>, expected: readonly string[], ): boolean { const keys = Object.keys(value); return keys.length === expected.length && keys.every((key) => expected.includes(key)); }