Skip to main content

Collaboration Models

This document defines the collaboration models Softmaple supports and the per-engine contract each model implies. It is a companion to collaboration-layers.md (the legal layering contract) and surface-bindings.md (the surface-side glue). A “collaboration model” is a tuple of three things:
  1. State shape — what the convergent document is.
  2. Op shape — what local edits and remote events look like.
  3. Position shape — how cursors and selections are addressed.
The models have distinct contracts, not a shared Operation base type or engine class. An implementation may reuse a lower-level convergence primitive: @softmaple/block-model deliberately builds its stable block and mark semantics on EG-walker’s sequence/DAG. Public model boundaries still use their own TypeScript types rather than a universal collaboration interface.

Why three models, not one

A single “operation” abstraction across plain text, rich text, and canvas would be a lowest-common-denominator API: text would pay for spatial concepts it does not need, and canvas would pay for sequence concepts that do not fit. Keeping the models distinct lets each engine ship the algorithm that is actually correct for its data shape.

1. Sequence model

The collaborative document is a flat UTF-16 sequence. This is the model implemented today by @softmaple/eg-walker.

State shape

(Or any sequence-shaped data; the engine does not interpret the unit.)

Op shape

Indices are UTF-16 code-unit boundaries. Public operations and anchors reject a position that splits a surrogate pair.

Position shape

A single 1D index. When this model is exchanged with @softmaple/awareness, positions take the synthetic-block form { blockId: "root", offset: index } (per the position contract in collaboration-layers.md).

Surfaces that bind to this model

  • <textarea> / <input>
  • CodeMirror (single-document mode)
  • Monaco
  • A deliberately flattened rich-text surface with its own sequence binding

Engine

@softmaple/eg-walker — implements the Eg-walker paper. Its base API is index-based; ./anchors is the advanced stable sequence-position entry.

2. Block model

The collaborative document is an ordered block projection with stable IDs, block-local text, independent inline marks, and parent IDs for nested lists. This model is implemented by @softmaple/block-model.

State shape

The flat array is canonical document order; parentId supplies list nesting without making ordering depend on recursive object traversal.

Op shape

One transaction produces one JSON-safe RichTextEventBatch. Its underlying EG events form a strict causal chain. Remote batches are buffered until their parents are present, deduplicated, then materialized and notified atomically.

Position shape

Resolved positions use { blockId, offset }. Persisted and awareness selection endpoints use { blockId, anchor: SequenceAnchor }, preserving direction and cross-block ranges through concurrent edits.

Surfaces

  • Lexical, ProseMirror, Slate (their native model)
  • Block editors (Notion-style)
  • Outliner-style apps

Engine

@softmaple/block-model — uses a deterministic event-backed bootstrap, stable raw block markers, Peritext-style anchored mark ranges, field-level causal LWW, and remove-wins block deletion. Marker and metadata atoms live in one EG sequence, while the public projection hides them and exposes blocks. This is deliberate reuse of the sequence convergence core, not a transaction log demo: stable atom identities survive snapshots, checkpoints, deletion, and late branches. Lexical binds to this model through @softmaple/binding-lexical, not to raw EG indices. Block drag-reordering remains outside v1.

3. Object model

The collaborative document is a map of independently identified objects, each with its own attribute set. Canvases, whiteboards, and node-based editors fall here. There is no engine for this model yet; this section defines the slot.

State shape (sketch)

Op shape (sketch)

Most attributes are last-writer-wins registers per field. This is fundamentally different from sequence convergence:
  • There is no canonical linear order of characters to merge.
  • “Conflict” is two writers setting the same attribute, resolved by timestamp + tiebreaker, not by tombstoning a character.
  • Reordering is sometimes a fractional index, occasionally a sequence-CRDT — at the attribute level, not the document level.
Do not force this model through the sequence engine. A canvas demo that hand-rolls convergence on top of @softmaple/awareness + a custom transport is an acceptable interim; promotion to a dedicated object engine package happens only after the demo proves the ops shape.

Position shape

{ x: number, y: number } (or an arbitrary opaque blob). Canvas positions are not baked into awareness’s core types; they ride through PresenceMeta’s open-ended [key: string]: unknown field, and renderer components accept post-resolved screen coordinates (LiveCursorPoint, HighlightRect). A canvas integration never has to round-trip through CursorPosition.

Surfaces

  • tldraw, Excalidraw
  • Custom whiteboard / diagram tools
  • Node-based editors (React Flow style)

Per-engine contract

Every collaboration engine — present or future — must cover this informal contract. Exact method names remain model-specific. What an engine must not do, regardless of model:
  • Depend on @softmaple/awareness.
  • Depend on any surface framework (lexical, prosemirror-*, slate*, canvas libraries).
  • Expose surface-shaped types (DOM nodes, editor selections, React components).
These are enforced for both EG-walker and block-model by their package lint configs using the role-specific deny lists in packages/eslint-config/collaboration-layers.js. Future model engines must adopt a rule matching their actual dependency direction.

Cross-model reuse

Block-model reuses EG-walker’s public replica and stable-anchor APIs instead of copying event-ID, DAG, snapshot, or replay code. This direction is intentional: block-model adds semantics without making EG-walker understand blocks. A shared graph package would be justified only if a future model cannot reuse the sequence core but needs the same DAG implementation.

Roadmap

See collaboration-layers.md for the hard rules and the project-wide roadmap. In short:
  • Now — sequence and block models (@softmaple/eg-walker and @softmaple/block-model) with the Lexical block binding.
  • Next — harden persistence/transport and add other surface bindings against the appropriate existing model.
  • Later — object engine package, after a canvas demo proves the ops shape on raw awareness + transport.

When to update this doc

Update this page whenever any of the following change:
  • A new collaboration model is added (or one is retired).
  • An engine ships for a previously model-only slot (block / object).
  • A model’s position shape changes in a way that affects @softmaple/awareness’s mapping/ subpath.