@fuaran-ui/ops
v0.28.0
Published
Canonical-JSON codec (encoder + structural decoder) and tree-op apply engine for the Fuaran UI wire format — a conformant TypeScript host of the language-neutral contract in fuaran-dotnet/docs/WIRE_FORMAT.md, verified byte-for-byte against the workspace w
Maintainers
Readme
@fuaran-ui/ops
Canonical-JSON codec (encoder + structural decoder) and tree-op apply engine for
the Fuaran UI wire format — a conformant TypeScript host of the
language-neutral contract specified in
fuaran-dotnet/docs/WIRE_FORMAT.md, sibling to the
F# host. Built on the typed shapes from
@fuaran-ui/schema (a peer dependency).
npm install @fuaran-ui/ops @fuaran-ui/schemaWhat it is
The correctness layer of the TypeScript reference implementation. @fuaran-ui/schema
gives you the typed tree; this package makes a TS-authored tree wire-conformant —
it can be serialised, persisted, replayed, and exchanged with the F# tier
byte-for-byte. Verified end-to-end against the workspace
wire-format-fixtures/ corpus: every valid fixture
round-trips byte-identically to the F# encoder, and every reject fixture surfaces
the same DecodeErrorCode at the same JSON path.
API
import { encodeNode, decodeNode, encodeOp, decodeOp, apply } from '@fuaran-ui/ops';
import type { TreeOp, DecodeError, ApplyError } from '@fuaran-ui/ops';
// Encode (typed tree → canonical JSON string)
const json: string = encodeNode(node);
// Decode (canonical JSON string → storage-shape Node<unknown> | DecodeError)
const decoded = decodeNode(json); // Result<Node<unknown>, DecodeError>
if (decoded.ok) {
/* decoded.value : Node<unknown> */
}
// TreeOp codec
const opJson = encodeOp(op);
const op = decodeOp(opJson); // Result<TreeOp<unknown>, DecodeError>
// Apply a tree-op (→ new tree + telemetry | ApplyError)
const result = apply(tree, op); // ApplyResult<TMsg>
if (result.ok) {
/* result.value.newTree, result.value.emittedTelemetry */
}Three modules
| Module | Role |
| -------- | ------------------------------------------------------------------------------------- |
| encode | Symmetric port of the F# CanonicalJson encoder. Deterministic, byte-stable output. |
| decode | Port of the F# JsonDecode decoder. Structural, Result-returning, six error codes. |
| apply | Port of the F# apply engine. Atomic (Batch), revert-on-error, emits telemetry. |
DecodeError codes (WIRE_FORMAT.md §6)
INVALID_JSON, MISSING_FIELD, WRONG_TYPE, UNKNOWN_DU_CASE, WRONG_NODE_KIND,
EMPTY_NODE_ID — byte-identical to the F# decoder's codes, surfaced at a $-rooted
dotted path.
Placement helpers
placeOp / moveOp / nudgeOp / canPlace (plus the clone verbs
duplicateOp / pasteOp and their …With variants taking an injectable
fresh-id strategy) derive the ops that put a node at a stated position —
first, last, before or after a named sibling — from the positionless op
vocabulary.
import { placeOp, moveOp, nudgeOp, canPlace, apply } from '@fuaran-ui/ops';
// Insert `child` immediately before sibling "b" under parent "left".
const r = placeOp(tree, child, {
parentId: 'left',
placement: { kind: 'Before', anchor: 'b' },
}); // Result<TreeOp, PlaceError>
if (r.ok) apply(tree, r.value);These helpers emit only existing TreeOp shapes — a bare InsertChild /
MoveNode when appending already yields the wanted order, otherwise
Batch [InsertChild|MoveNode, ReorderChildren], and ReorderChildren alone
for a nudge. There is no new op case and no wire-format change; anything
they emit encodes, decodes, and applies exactly as if a consumer had assembled
it by hand. Pre-checks mirror the apply engine's own rejections (absent or
childless parent, move-into-self, move-into-descendant, duplicate id), so a
helper refusal and an apply refusal always agree; an anchor that is not among
the destination's children is refused (UnknownAnchor) rather than silently
appended. The clone verbs remap colliding ids across the whole traversal
surface before insert, so a duplicate or paste never trips the tree-wide
duplicate-id check.
Storage-shape erasure
Decode is storage-shape erased: it always yields Node<unknown> / TreeOp<unknown>,
because the wire carries no typed-'Msg information (every 'Msg payload and every
closure encodes as the "<closure>" sentinel). Opaque Binding.Static payloads the
encoder couldn't decompose decode to the literal string "<opaque>". Both placeholders
re-encode to the same sentinel, so the round-trip stays byte-stable; typed re-attachment
of the erased payloads is the host's responsibility.
Conformance is the stability contract
Per the wire-format forward-coupling rule
(WIRE_FORMAT.md §11), adding a NodeKind / Spec
/ TreeOp / Binding / Action case updates the F# encoder + decoder + the corpus
and this TS codec + @fuaran-ui/schema in the same commit. Byte-equality against the
corpus — not just API non-breakage — is the contract.
Licence
Apache-2.0. See STABILITY.md for the per-surface stability
declaration.
