Composition / foldkit-composition

Editing: Operations

A Document changes by Operations, the way a Model changes by Messages:

Document + Operation → Document, or a refusal with no partial result
ts
const { Op, root, region } = Composition

const result = Composition.apply(
  Site,
  page,
  Op.insert({
    id: NodeId.make('subtitle'),
    block: 'Heading',
    props: { text: 'Welcome', level: 2 },
    at: region(NodeId.make('intro'), 'body', 1),
  }),
)
// Result.succeed({ document, changed: ['subtitle', 'intro'], removed: [] })
// or Result.fail({ code: 'composition:region-rejects', message: '…' })
OperationWhat it does
Op.insert({ id, block, props, at })a new node at a position
Op.insertTree({ tree, at })a subtree, such as a Pattern or a paste
Op.remove(id)the node and everything it holds
Op.move(id, to)the node elsewhere, keeping every id
Op.duplicate({ id, ids, at })a copy of the subtree under the ids given
Op.setProp(id, prop, value), Op.unsetProp(id, prop)one prop, checked against the Block
Op.setWhen, Op.setAppearance, Op.setActionthe reserved fields, stored and cleared
Op.batch(ops)several, in order, all or none
  • Positions are root(index) or region(parent, name, index). An index counts after the node being moved is taken out, so moving the first of three to index 2 puts it last.
  • apply checks what the Operation causes: a Region that would not accept the node or has no room, a Region left below its bound, a node put inside itself, a taken id, props the Block refuses. It does not refuse an edit for something already wrong elsewhere, so an author keeps working beside a Block the deployment no longer knows. A prop of such a Block cannot be set, since nothing can check it.
  • apply never mints an id. Every id an Operation creates is in it, so a replay creates the same nodes. Composition.newIds(count) is an Effect that makes random ones, to run in a Command.
  • Applied says which nodes were added or changed, a parent whose Region changed included, and which were removed, so a view can redraw only those.
  • Operations are data: Composition.Operation is their Schema, for a log, an agent's tool or a replay.
  • Composition.takeTree(document, id) takes a subtree, and Composition.rekey(tree, ids) renames every id in it, which is how a paste is inserted twice without a collision. A reference to a node the tree does not hold is kept as it is, for apply to refuse by name. Composition.treeRefusal(catalog, tree) checks a tree alone, by its own ids, before it is rekeyed: what a paste should say is wrong with it.
  • Any text is an id, constructor and __proto__ included: apply, validate, index and the Renderer read nodes by own key only, so a stored Document naming one it lacks is composition:missing-node, not a crash.