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;
Transformis 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:
The string is the document type discriminator the client sends when joining — see Builder API.
Next: pick an engine¶
- Text OT — collaborative plain text
- Rich Text — Quill / ProseMirror
- JSON CRDT — settings / config trees
- Tree CRDT — Notion blocks / outliners
- Table CRDT — spreadsheets / grids
- Form OT — forms / dialogs
- Awareness — cursors / presence
- Undo / Redo — per-peer history