Skip to content

Engines overview

An engine is a pure, side-effect-free implementation of IOpEngine<TDoc, TOp> that defines how a document type merges concurrent edits. OpStream ships eight of them — six for persisted document shapes and two transversal engines that work on top of the others.

Choose by document shape

Your document is… Use Family Notes
Plain text Text OT OT Caret-aware; pairs with editors like Monaco, CodeMirror, contenteditable.
Rich text with attributes (bold, italic, lists…) Rich Text OT Quill / TipTap / ProseMirror style delta ops.
Free-form JSON object JSON CRDT CRDT LWW per path. Best when keys are stable; no array splicing.
Hierarchical tree (outline, blocks, file system) Tree CRDT CRDT Native Move operation; Kleppmann's move-tree algorithm.
Spreadsheet / grid / Airtable-style Table CRDT CRDT Rows + columns + cells with soft tombstones.
Bound form / settings dialog Form OT LWW Flatter and lighter than JSON CRDT.

Cross-cutting engines

Engine What it does
Awareness Presence / cursors / "user is typing". Ephemeral — never persisted.
Undo / Redo Per-peer undo / redo stacks layered on any of the persisted engines.

OT vs CRDT — which family do I want?

Operational Transformation (Text, Rich Text):

  • Smaller wire format for positional edits.
  • Server-authoritative, fits the "one master, many clients" model.
  • Requires a round-trip through the server to converge — no P2P.
  • Engine complexity is concentrated in Transform.

Conflict-free Replicated Data Types (JSON, Tree, Table, Form):

  • Operations commute by construction; Transform is identity.
  • P2P-friendly — peers can merge directly without a server.
  • Late-arriving updates don't break convergence.
  • Larger ops (carry timestamps + peer ids for LWW).
  • Some operations are not "natively atomic" (e.g. Move in a tree CRDT).

OpStream's CRDT engines all use Timestamp + PeerId LWW with deterministic tie-breaking. The Tree engine uses Kleppmann's move-log algorithm and absorbs late-arriving moves correctly.

Typed vs untyped engines

Every engine has a runtime core that operates on JsonElement payloads. Five of them also ship a generic strongly-typed wrapper:

Untyped core Typed wrapper
AwarenessEngine (via TypedAwarenessSession<TPresence>)
TreeCrdtEngine TreeCrdtEngine<TPayload>
TableCrdtEngine TableCrdtEngine<TValue>
FormOtEngine FormOtEngine<TForm>

The typed wrappers serialize / deserialize at the engine boundary so your application code works with domain POCOs. The wire format and storage remain uniform across all clients.

Registering an engine

Every engine plugs in through the builder:

services.AddOpStream()
    .AddEngine<TreeDocument, TreeOpBatch, TreeCrdtEngine>("blocks");

The string is the document type discriminator the client sends when joining — see Builder API.

Next: pick an engine