Every write to an object is a change. Changes from different peers merge without asking either of them, each object
keeps the history of its changes, and a branch lets a set of changes wait apart from the main line until they are
merged.
Each object lives in an Automerge document, a CRDT. A peer writes to its own copy and the write
is finished when the call returns. org.dxos.echo.query#F-2.1 Peers then exchange changes, and every
peer that has seen the same changes ends up with the same state, whatever order the changes arrived in.
What “merged” means depends on the kind of value:
Concurrent change
Result
Different fields of one object
Both writes kept
The same scalar field, such as a status
One value wins, the same on every peer
A whole string assigned twice
One string wins, as with any scalar
Characters inserted or deleted with Text
Both edits kept, interleaved
Items inserted into one array
Both kept; the order of the two new items is decided by ECHO
One peer edits an object another removes
The removal stands
The table explains the choice between assigning a string and editing it with Text. Assignment replaces the value;
Text.splice and Text.update change characters, so two people typing in the same field both keep their work.
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 });
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, () => { ... }).
A local write shows in query results and reaches subscribers before the writing call returns.
org.dxos.echo.query#F-2.1
A subscriber that writes from inside a notification makes a separate commit, with its own notification after the
first. org.dxos.echo.query#F-2.2
A peer that was offline keeps writing to its copy. When it reconnects, peers exchange the changes each is missing and
merge them by the rules above. Nothing is rejected for being late.
Each change records the device that made it, so an app can see that two devices wrote the same field and ask a
person to confirm the result.
1
const
const writers: Set<string>
writers = new
var Set: SetConstructor
new <string>(iterable?: Iterable<string> | null | undefined) =>Set<string> (+1 overload)
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map((
change: ValueChange<string>
change) =>
change: ValueChange<string>
change.
actor: string
Writer that made the change; for a document change, an Automerge actor, which is a device, not a member.
actor));
2
if (
const writers: Set<string>
writers.
Set<string>.size: number
@returns ― the number of (unique) elements in Set.
size > 1) {
3
// Several devices set the title; show the history so a person can confirm
Obj.getChanges lists an object’s changes, each with the device that made it, the time, and the value before and
after. Obj.getVersion reads the object as it was after any change, without changing the live object. Filter.changes
queries changes across a space, to count or list them.
per document change that
touched the object, carrying its snapshot before and after. Given property, only the changes that
touched that property, carrying its value. Pass an entry's heads to
getVersion
to read the
whole object as that change left it.
Changes belong to the object's Automerge document, so time, actor and ops describe the whole
change, which may also have written other objects in the same document. Edits to the content of a
referenced object (e.g. a Ref<Text>) are that object's history, not this one's.
@example
for (const { time, before, after } of Obj.getChanges(task, { property: 'status' })) {
console.log(newDate(time), before, '→', after);
}
@performance ― O(document history): diffs every change of the Automerge document and snapshots each match; not cached.
Narrow the history to the changes that touched this property; before/after then hold its value.
property: 'title' });
4
for (const {
const actor: string
Writer that made the change; for a document change, an Automerge actor, which is a device, not a member.
actor,
const time: number
The author's clock when the change was made, unix ms.
time,
const before: string | undefined
The value before the change; undefined where the object (or property) did not exist yet.
before,
const after: string | undefined
The value after the change; undefined where the change removed the property.
after } of
const changes: ValueChange<string>[]
changes) {
5
var console: Console
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
A global console instance configured to write to process.stdout and
process.stderr. The global console can be used without importing the node:console module.
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()).
Get an immutable snapshot of the object at the given historical heads — a detached instance, not a
pin on the live object. Only the surface that asks for it sees the historical value; the live
object and every other surface are unaffected. The functional alternative to a read-time-travel pin.
@performance ― O(object size) plus the Automerge cost of viewing the document at heads; nothing is cached.
per change to the documents
holding targets, or to every document in the space when called with no argument.
Changes belong to documents, not entities. Entities that share a document share its changes, and
content held by another entity (a document's Text) needs its own target. Only the host answers
these queries; a space-wide query must aggregate, and the index answers it only at hour
granularity (see Aggregate.time).
entries partition the
results into contiguous groups (one record each), keyed by the record field the group is named
after; with no group entries the entire input aggregates into a single record. Each declared
aggregate becomes a top-level field and can be ordered by with a following
orderBy
using
Order.property
.
Groups are ordered by the first occurrence of their key in the incoming stream, so a preceding
orderBy controls group order in the absence of a following one.
Aggregate.items
's own
order option is a separate, explicit per-group member ordering — pass it there rather than
relying on a preceding orderBy, whose only well-defined job once aggregate follows is
establishing initial group order. For example, message threads ordered by their most recent
message, each retaining up to 20 members newest-first:
Order the query results.
Orders are specified in priority order. The first order will be applied first, etc.
Order.property orders by the current result shape's fields, so it works both before and after
an
aggregate
: before, by member (row) properties; after, by the flat record's fields
(any group or aggregate field — e.g. Order.property('lastMessageAt') reorders the groups by
that aggregate).
@param ― order - Order to sort the results.
@returns ― Query for the ordered results.
@performance ― O(k) in the number of orders; builds the AST without executing anything.
@performance ― Async; executes the full query (index plus working set) on every call, not cached; O(n) in the data
the executor examines, not in the result count.
A branch is a separate line of changes to an object and the objects it references, such as a draft of a document.
createBranch starts one from the latest state or from a past version. Changes on a branch do not show on main until
mergeBranch merges them, as one CRDT merge, so main’s own changes since the branch started are kept.
switchBranch shows a branch on this device, for the object and its children. Other devices keep the branch they
are on.
syncBranch brings main’s newer changes into a branch, and deleteBranch drops a branch.
db.branch gives a second live copy of an object, bound to a branch, without switching. Use it to show a branch next
to main; call dispose when the view closes.
1
import {
import Obj
Obj } from'@dxos/echo';
2
3
// A draft of the event and everything it references.
Fork the object and its referenced subtree into a new branch (does not switch to it).
@param ― opts.fromHeads Fork from a historical frontier instead of the tip (a bare heads array
applies to the root only; a map forks each member from its own frontier).
@performance ― Async; O(subtree size), each member document forked as a full history copy.
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 });
Fork the object and its referenced subtree into a new branch (does not switch to it).
@param ― opts.fromHeads Fork from a historical frontier instead of the tip (a bare heads array
applies to the root only; a map forks each member from its own frontier).
@performance ― Async; O(subtree size), each member document forked as a full history copy.
Create a caller-owned, writable binding to one branch of one object — a live object whose reads
resolve the branch document and whose writes land on the branch document only. Multiple bindings
to different branches of the same object may coexist; the device-global current branch and other
bindings are unaffected. Binding to 'main' returns the canonical live object. Bindings are
ephemeral and never persisted — the caller must dispose().
@performance ― Async; O(1) for main, otherwise loads and binds the branch document of this one 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 });
ECHO does not know what your fields mean. When one peer sets status: 'done' and another sets status: 'doing', one
value wins and the other is not flagged. If the loser matters, model the intent instead: record each person’s choice in a
field of its own, or append events to a feed and derive the state from them.
Invariants across fields or objects are not kept. Two peers can each make a valid change whose merge breaks a rule such as
“at most one task is in progress”. Check such rules when you read, not only when you write.
A schema change is not merged with data written under the old version. Use a new version and a
migration.
A switch to a branch is local to the device. Another device of the same person still shows main until it switches
too.