Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Versioned text fields

Preserve concurrent plain-text edits using versioned operations and recover pending intent explicitly.

Declare field.text() when concurrent writers need to preserve insert/delete intent within one plain-text value. Ordinary field.string() fields keep their replacement and stale-write behavior.

This opt-in path currently supports Ablo-owned database and log planes. Versioned text edits on connected customer sources are rejected. The server must have the versioned-text migration installed before this field is enabled.

Open a field buffer

import Ablo from '@abloatai/ablo';
import { defineSchema, model, field } from '@abloatai/ablo/schema';

const schema = defineSchema({
  documents: model({ title: field.string(), body: field.text() }),
});
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });

const body = await ablo.documents.text({ id: 'document-1', field: 'body' });
await body.edit(['Hello ']);
console.log(body.value); // The optimistic text, including your pending edits.
await body.flush();

The row must already exist with a string in the field. A text field is required and non-null. Creation still accepts a plain string; subsequent edits use versioned operations. Update every writer before changing an existing string field to field.text(): whole-value updates to that field will be rejected.

Text shares its commit and reconciliation lifecycle with integer counters. Reactive editors read the normal observable row and enqueue through the normal durable model mutation path. edit stages immediately; flush waits for authoritative confirmation. HTTP editors are explicit staging buffers and send their unsent edits on flush. Retries preserve sealed identity and payload. Validation and stale-basis failures surface to the caller.

Operations and undo

An operation traverses the original value. A positive number retains that many Unicode code points, a string inserts text, and { d: count } deletes code points. { d: 'text' } additionally checks the deleted contents. For example, [1, { d: '🤖' }, '🌍'] changes a🤖b to a🌍b.

Positions count code points, not JavaScript UTF-16 offsets or grapheme clusters. Convert editor offsets before submitting them. A value is limited to 1,000,000 UTF-16 code units, and an operation to 4,096 components.

The humans client refreshes automatically on row invalidation, catch-up and bootstrap. HTTP callers use await body.refresh() when observing a change or polling. It rebases pending edits and undo/redo against accepted operations without discarding unsent edits.

Reactive edits enter the ordinary UndoScope alongside model value changes. Its inverses use accepted operation history so undo preserves collaborators’ intervening text. A standalone HTTP buffer instead exposes undo()/redo(), followed by flush(); it has no reactive model or UndoScope.

Persistence and recovery

Reactive edits use normal client persistence and the primary mutation journal. Chained edits retain their authoring context until their actual server bases are known. Sealing consumes the primary records into the immutable commit envelope. Disposing and reopening an editor does not create another pending-state owner.

For standalone HTTP buffers, pass state to restore body.exportState() and a persist: async state => ... callback to save it. That callback runs before dispatch and after acknowledgement; a failed save prevents dispatch. Without a callback, unsent HTTP buffer edits remain in memory. Their commit outbox protects only dispatched requests. Scope any saved buffer to its account, model, row and field. Reactive editors reject these buffer restoration options: recover unresolved legacy buffers with the previous SDK before upgrading.

The operation journal retains text content and uses the row’s current read permission. It is not a redaction mechanism. The engine’s sync_text_fields table is reserved and cannot be exposed as an application model.

The server retains a checkpoint and up to 256 operations per field. It also checkpoints histories above 8 MiB while retaining the latest operation, so large edits can shorten the offline window. A missing basis or changed epoch rejects with stale_context and leaves the saved pending edits available for explicit recovery. An in-flight edit may already have been accepted: inspect its commit by id before deciding how to recover it, rather than blindly applying it again.

An out-of-band database replacement that disagrees with retained history is preserved. Edits against the old value reject; the server does not silently reset that history or merge over the replacement. Recovering such a fork requires an explicit new base outside this first API. Value comparison cannot detect an external change that returns to the exact same text.

Call body.dispose() when finished to release the editor view and its cached accepted basis. Pending primary writes remain journaled and continue through the queue. The reactive client shares one active editor per field. Background reconciliation failures are visible through body.error and client sync status.

One commit may carry semantic operations on distinct rows, with one field operation per semantic row. See Operation fields for atomic composition through the existing commit API. Claims, fencing, authorization and product validation remain in the normal commit path. Text merge does not establish business validity, acquire a claim, or merge rich-text formatting.

Was this page helpful?