Skip to content

Text

import { Text } from '@dxos/echo';

Text edits a string field as a sequence of characters. Assigning a string replaces it, so of two concurrent assignments one wins. An edit with Text inserts and deletes characters, so two people typing in the same field both keep their work. Changes and branches shows what merges and how.

Any string field can be edited this way; it needs no special declaration. Call the functions inside Obj.update.

  • splice removes and inserts characters at an index, like Array.prototype.splice.
  • update replaces the whole value, but writes only the characters that differ.
  • apply runs find-and-replace edits, and throws TextEditNotFoundError when a text to replace is missing.
import {
import Obj
Obj
,
import Text
Text
} from '@dxos/echo';
import Obj
Obj
.
const update: <BaseObj & Event & OfKind<EntityKind.Object>>(obj: BaseObj & Event & OfKind<EntityKind.Object>, callback: ChangeCallback<BaseObj & Event & OfKind<EntityKind.Object>>) => BaseObj & Event & OfKind<EntityKind.Object>

Perform mutations on an echo object within a controlled context.

All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of Obj.update will throw an error for echo objects.

This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.

@param ― obj - The echo object to mutate. Use Relation.update for relations.

@param ― callback - The callback that performs mutations on the object.

@example

const person = Obj.make(Person, { name: 'John', age: 25 });
// Mutate within Obj.update
Obj.update(person, (obj) => {
obj.name = 'Jane';
obj.age = 30;
});
// ONE notification fires here
// Direct mutation throws
person.name = 'Bob'; // Error: Cannot modify outside Obj.update()

Note: Only accepts objects. Use Relation.update for relations.

@performance ― Synchronous; costs the mutations made in the callback plus one batched notification and an O(owning fields) parent pass.

update
(
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
, (
launch: {
id: EntityId;
"~@dxos/echo/Kind": EntityKind.Object;
title: string;
description?: string | undefined;
attendees?: Ref<Person & OfKind<EntityKind.Object> & Obj.Unknown>[] | undefined;
}
launch
) => {
// Insert at a character index, as Array.prototype.splice does.
import Text
Text
.
const splice: (obj: Obj.Unknown, path: Text.KeyPath | string | number, start: number, deleteCount: number, insert?: string) => string

Splice the string CRDT value at path by character index, mirroring Array.prototype.splice: remove deleteCount characters at start and insert insert. Returns the removed substring.

Must be called inside Obj.update(obj, () => { ... }).

@performance ― O(delete count + insert length) CRDT splice.

splice
(
launch: {
id: EntityId;
"~@dxos/echo/Kind": EntityKind.Object;
title: string;
description?: string | undefined;
attendees?: Ref<Person & OfKind<EntityKind.Object> & Obj.Unknown>[] | undefined;
}
launch
, 'description', 8, 0, 'welcome, ');
// Replace the whole value with a minimal diff of characters.
import Text
Text
.
const update: (obj: Obj.Unknown, path: Text.KeyPath | string | number, newText: string) => void

Replace the string CRDT value at path with newText.

For database-backed objects the change is applied as a minimal Automerge diff (preserving cursors, anchors, and merging cleanly with concurrent edits); for in-memory objects it is a plain assignment.

Must be called inside Obj.update(obj, () => { ... }).

@performance ― O(text length) diff, so concurrent edits merge minimally.

update
(
launch: {
id: EntityId;
"~@dxos/echo/Kind": EntityKind.Object;
title: string;
description?: string | undefined;
attendees?: Ref<Person & OfKind<EntityKind.Object> & Obj.Unknown>[] | undefined;
}
launch
, 'description', 'Agenda: welcome, demo, questions.');
// Find and replace.
import Text
Text
.
const apply: (obj: Obj.Unknown, path: Text.KeyPath | string | number, edits: readonly Text.Edit[]) => string

Apply a sequence of find/replace

Edit

s to the string value at path, returning the resulting full string. Implemented on top of

splice

so it works for both backends.

An edit with a missing or empty oldString appends its newString. A non-replaceAll edit whose oldString is not found throws

Error.TextEditNotFoundError

.

Must be called inside Obj.update(obj, () => { ... }).

@performance ― O(N) per edit plus O(M · N) for replaceAll (N = text length, M = matches): it re-reads and searches the whole string after every splice, so one replaceAll edit can be O(N²).

apply
(
launch: {
id: EntityId;
"~@dxos/echo/Kind": EntityKind.Object;
title: string;
description?: string | undefined;
attendees?: Ref<Person & OfKind<EntityKind.Object> & Obj.Unknown>[] | undefined;
}
launch
, 'description', [
{
oldString?: string | undefined
oldString
: 'questions',
newString: string
newString
: 'Q&A' },
]);
});

Read the field as usual: it is a string.

A text field bound to an ECHO string, such as a <textarea> whose value is the field, loses its caret when a peer’s edit arrives, because React sets the whole value again. Save the selection before the remote change renders, and move it by the number of characters the change inserted or removed before it. Write each local keystroke with Text.update, so it merges with what the peer typed.

Member Summary
splice Remove and insert characters at an index
update Replace the value with a minimal diff
apply Find and replace

The full list is in the API reference.