Skip to content

Database

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

Each space has one database. It holds the space’s objects, runs queries over them and saves writes to disk. Effect code reaches the same database as Database.Service, which the Effect guide covers.

In plain code the database is a property of the space.

const
const db: EchoDatabase
db
=
const space: Space
space
.
Space.db: EchoDatabase

Echo database.

db
;

add is synchronous. It returns the object, now stored in the database, and saves it in the background. remove marks the object deleted; queries leave it out from then on.

import {
import Obj
Obj
} from '@dxos/echo';
import {
class Person
Person
} from './schema.ts';
const
const ada: BaseObj & Person & OfKind<EntityKind.Object>
ada
=
const db: EchoDatabase
db
.
Database.add<BaseObj & Person & OfKind<EntityKind.Object>>(obj: BaseObj & Person & OfKind<EntityKind.Object>, opts?: AddOptions): BaseObj & Person & OfKind<EntityKind.Object>

Adds an object or relation to the database.

Only Object and Relation entities are accepted. To persist a Type definition use

addType

— passing a Type entity is rejected at compile time (and at runtime).

Pass { to: feed } to append to a feed instead (synchronous; confirm with

flush

).

@performance ― Synchronous; O(n) in object size to write it into a new linked document (or the root), persisted in the background.

add
(
import Obj
Obj
.
function make<typeof Person>(type: typeof Person, props: NoInfer<Obj.MakeProps<typeof Person>>, options?: Obj.MakeOptions): Obj.OfShape<Person & OfKind<EntityKind.Object>>

Creates a new echo object of the given schema or Type.Type.

@param ― typeOrSchema - A static object schema (Type.makeObject(...)) or a Type.Type entity (e.g. one returned by db.addType(schemaEntity)).

@param ― props - Object properties.

@param ―

options - See MakeOptions.

Meta can be passed as a symbol in props.

Example:

const obj = Obj.make(Person, { [Obj.Meta]: { keys: [...] }, name: 'John' });

Note: Only accepts object schemas / object-kind types, not relation schemas. Use Relation.make for relations.

@performance ― O(n) in props size; validates props against the schema and allocates a reactive proxy, with no database I/O.

make
(
class Person
Person
, {
name: string
name
: 'Ada' }));
const db: EchoDatabase
db
.
Database.remove(obj: Unknown, opts?: WriteOptions): void

Removes object from the database.

@performance ― O(1); sets the deletion marker (a soft delete).

remove
(
const ada: BaseObj & Person & OfKind<EntityKind.Object>
ada
);

db.query takes a filter or a query and returns a QueryResult. Call run() for the results once, or subscribe() to follow them. org.dxos.echo#F-5.1 Query covers building queries.

import {
import Filter
Filter
,
import Query
Query
} from '@dxos/echo';
import {
class Person
Person
} from './schema.ts';
const
const people: (Person & OfKind<EntityKind.Object>)[]
people
= await
const db: EchoDatabase
db
.
Database.query: QueryFn
<Query.Query<Person & OfKind<EntityKind.Object>>>(query: Query.Query<Person & OfKind<EntityKind.Object>>) => QueryResult<Person & OfKind<EntityKind.Object>> (+1 overload)
query
(
import Query
Query
.
const select: <Filter.Filter<Person & OfKind<EntityKind.Object>>>(filter: Filter.Filter<Person & OfKind<EntityKind.Object>>) => Query.Query<Person & OfKind<EntityKind.Object>>

Select objects based on a filter.

@param ― filter - Filter to select the objects.

@returns ― Query for the selected objects.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

select
(
import Filter
Filter
.
const type: <typeof Person>(type: typeof Person, props?: Filter.Props<Person & OfKind<EntityKind.Object>> | undefined) => Filter.Filter<Person & OfKind<EntityKind.Object>> (+4 overloads)
type
(
class Person
Person
))).
QueryResult<Person & OfKind<EntityKind.Object>>.run(opts?: RunOptions): Promise<(Person & OfKind<EntityKind.Object>)[]>

Returns all known results.

@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.

run
();

flush resolves once pending writes are on disk. Options also wait for the indexes and for query updates, which is what a test needs before it reads back what it wrote.

await
const db: EchoDatabase
db
.
Database.flush(opts?: FlushOptions): Promise<void>

Wait for all pending changes to be saved to disk. Optionaly waits for changes to be propagated to indexes and event handlers.

@performance ― Async; always waits for pending document creation, then only for the disk, index and update work that opts selects, so it is as slow as that selected backlog.

flush
(); // pending writes are on disk
await
const db: EchoDatabase
db
.
Database.flush(opts?: FlushOptions): Promise<void>

Wait for all pending changes to be saved to disk. Optionaly waits for changes to be propagated to indexes and event handlers.

@performance ― Async; always waits for pending document creation, then only for the disk, index and update work that opts selects, so it is as slow as that selected backlog.

flush
({
indexes?: boolean | undefined

Wait for pending index updates.

@default ― true

indexes
: true,
updates?: boolean | undefined

Flush pending updates to objects and queries.

@default ― false

updates
: true }); // and queries see them

db.add rejects type entities. Persist a type with addType, which replicates it to the space’s other members. If the space already holds a type with the same typename and version, addType returns that one and writes nothing.

import {
import Type
Type
} from '@dxos/echo';
const
const Note: Type.Type<TypeSchema>
Note
=
import Type
Type
.
const makeObjectFromJsonSchema: (props: MakeTypeProps) => Type.Type<TypeSchema>

Construct a new object-kind type entity from raw metadata — for cases where an Effect Schema isn't available (e.g. JSON-Schema arriving over the network or from a UI editor). Parallel to

makeObject

but takes pre-built jsonSchema instead of piping through an Effect schema.

The returned entity is in-memory; persist it with db.addType(entity).

@performance ― O(schema size); validates the JSON schema into a new in-memory type entity, with no database I/O.

makeObjectFromJsonSchema
({
typename?: string | undefined
typename
: 'com.example.type.note',
version?: string | undefined
version
: '0.1.0',
jsonSchema: JsonSchemaType
jsonSchema
: {
type?: "string" | "number" | "boolean" | "object" | "array" | "integer" | "null" | readonly ("string" | "number" | "boolean" | "object" | "array" | "integer" | "null")[] | undefined

Base type of the schema.

type
: 'object',
properties?: {
readonly [x: string]: JsonSchemaType;
} | undefined
properties
: {
title: {
type: "string";
}
title
: {
type?: "string" | "number" | "boolean" | "object" | "array" | "integer" | "null" | readonly ("string" | "number" | "boolean" | "object" | "array" | "integer" | "null")[] | undefined

Base type of the schema.

type
: 'string' } } },
});
const
const persisted: Type.Type<TypeSchema>
persisted
= await
const db: EchoDatabase
db
.
Database.addType<Type.Type<TypeSchema>>(type: Type.Type<TypeSchema>, opts?: WriteOptions): Promise<Type.Type<TypeSchema>>

Persists a Type definition (clones/forks the entity) so it replicates to other peers.

Runs a conflict query first: if a type with the same typename + version already exists in this space, the existing persisted entity is returned and no duplicate is created. This is the only supported way to add Type entities —

add

rejects them.

@performance ― Async; O(t) in the space persisted types, which it queries in full for a duplicate before writing.

addType
(
const Note: Type.Type<TypeSchema>
Note
); // replicates to the other members

A branch is a separate line of changes to an object and the objects it references. createBranch starts one, switchBranch shows it on this device, and mergeBranch merges it into main. db.branch returns a second live copy bound to a branch, without switching; call dispose on it when done. Changes and branches explains the model.

import {
import Obj
Obj
} from '@dxos/echo';
import {
class Event
Event
} from './schema.ts';
const
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
=
const db: EchoDatabase
db
.
Database.add<BaseObj & Event & OfKind<EntityKind.Object>>(obj: BaseObj & Event & OfKind<EntityKind.Object>, opts?: AddOptions): BaseObj & Event & OfKind<EntityKind.Object>

Adds an object or relation to the database.

Only Object and Relation entities are accepted. To persist a Type definition use

addType

— passing a Type entity is rejected at compile time (and at runtime).

Pass { to: feed } to append to a feed instead (synchronous; confirm with

flush

).

@performance ― Synchronous; O(n) in object size to write it into a new linked document (or the root), persisted in the background.

add
(
import Obj
Obj
.
function make<typeof Event>(type: typeof Event, props: NoInfer<Obj.MakeProps<typeof Event>>, options?: Obj.MakeOptions): Obj.OfShape<Event & OfKind<EntityKind.Object>>

Creates a new echo object of the given schema or Type.Type.

@param ― typeOrSchema - A static object schema (Type.makeObject(...)) or a Type.Type entity (e.g. one returned by db.addType(schemaEntity)).

@param ― props - Object properties.

@param ―

options - See MakeOptions.

Meta can be passed as a symbol in props.

Example:

const obj = Obj.make(Person, { [Obj.Meta]: { keys: [...] }, name: 'John' });

Note: Only accepts object schemas / object-kind types, not relation schemas. Use Relation.make for relations.

@performance ― O(n) in props size; validates props against the schema and allocates a reactive proxy, with no database I/O.

make
(
class Event
Event
, {
title: string
title
: 'Launch' }));
await
const db: EchoDatabase
db
.
Database.createBranch(rootObjectId: string, name: string, opts?: {
fromHeads?: readonly string[] | Record<string, readonly string[]>;
}): Promise<void>

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.

createBranch
(
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
.
id: string
id
, 'draft');
const db: EchoDatabase
db
.
Database.listBranches(objectId: string): string[]

All branch names available for an object, including the implicit 'main' (always first).

@performance ― O(1) for a subtree root; a member scans every branch record in the space to find its root.

listBranches
(
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
.
id: string
id
); // ['main', 'draft']
await
const db: EchoDatabase
db
.
Database.switchBranch(rootObjectId: string, name: string): Promise<void>

Switch the object's subtree to a branch (or back to 'main'). Device-local; cascades to children.

@performance ― Async; O(subtree size) rebinds, serialized with other branch operations.

switchBranch
(
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
.
id: string
id
, 'draft'); // this device only
await
const db: EchoDatabase
db
.
Database.switchBranch(rootObjectId: string, name: string): Promise<void>

Switch the object's subtree to a branch (or back to 'main'). Device-local; cascades to children.

@performance ― Async; O(subtree size) rebinds, serialized with other branch operations.

switchBranch
(
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
.
id: string
id
, 'main');
await
const db: EchoDatabase
db
.
Database.mergeBranch(rootObjectId: string, name: string, opts?: {
deleteAfter?: boolean;
}): Promise<void>

Merge a branch back into main across the subtree, then switch back to main.

@performance ― Async; O(subtree size) Automerge merges followed by a switch back to main.

mergeBranch
(
const launch: BaseObj & Event & OfKind<EntityKind.Object>
launch
.
id: string
id
, 'draft', {
deleteAfter?: boolean | undefined
deleteAfter
: true });

client.graph is the Hypergraph: every space this device holds. It queries across spaces, finds a space’s database by id, and holds the registry of types and objects registered in code.

import {
import Filter
Filter
,
import Query
Query
} from '@dxos/echo';
import {
class Person
Person
} from './schema.ts';
const
const everyone: (Person & OfKind<EntityKind.Object>)[]
everyone
= await
const client: Client
client
.
Client.graph: Hypergraph

ECHO graph.

graph
.
Hypergraph.query: QueryFn
<Query.Query<Person & OfKind<EntityKind.Object>>>(query: Query.Query<Person & OfKind<EntityKind.Object>>) => QueryResult<Person & OfKind<EntityKind.Object>> (+1 overload)
query
(
import Query
Query
.
const select: <Filter.Filter<Person & OfKind<EntityKind.Object>>>(filter: Filter.Filter<Person & OfKind<EntityKind.Object>>) => Query.Query<Person & OfKind<EntityKind.Object>>

Select objects based on a filter.

@param ― filter - Filter to select the objects.

@returns ― Query for the selected objects.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

select
(
import Filter
Filter
.
const type: <typeof Person>(type: typeof Person, props?: Filter.Props<Person & OfKind<EntityKind.Object>> | undefined) => Filter.Filter<Person & OfKind<EntityKind.Object>> (+4 overloads)
type
(
class Person
Person
)).
Query<Person & OfKind<EntityKind.Object>>.from(allSpaces: "all-accessible-spaces", options?: {
includeFeeds?: boolean;
}): Query.Query<Person & OfKind<EntityKind.Object>> (+6 overloads)

Query from all accessible spaces.

Example:

Query.select(Filter.type(Person)).from('all-accessible-spaces');

@param ― options.includeFeeds [false] - Whether to include feeds in the query. Default is to query from automerge documents only.

@performance ― O(1); wraps the current AST in a new immutable node without executing anything.

from
('all-accessible-spaces'))
.
QueryResult<Person & OfKind<EntityKind.Object>>.run(opts?: RunOptions): Promise<(Person & OfKind<EntityKind.Object>)[]>

Returns all known results.

@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.

run
();
const
const sameDb: Database | undefined
sameDb
=
const client: Client
client
.
Client.graph: Hypergraph

ECHO graph.

graph
.
Hypergraph.getDatabase(spaceId: SpaceId): Database | undefined

Get a database by space ID.

@returns ― The database for the given space ID, or undefined if not found.

@performance ― O(1) map lookup.

getDatabase
(
const space: Space
space
.
Space.id: SpaceId

Unique space identifier.

id
);

getSyncState reports how many documents this device has not yet synced with the remote peer, and subscribeToSyncState calls back as that changes.

const
const state: SyncState
state
= await
const db: EchoDatabase
db
.
Database.getSyncState(options?: GetSyncStateOptions): Promise<SyncState>

Get the current combined (automerge documents + feed blocks) sync state, reported against a single remote peer.

@performance ― Async; two concurrent service round trips (documents and feeds).

getSyncState
();
const
const unsubscribe: CleanupFn
unsubscribe
=
const db: EchoDatabase
db
.
Database.subscribeToSyncState(cb: (state: SyncState) => void, options?: GetSyncStateOptions): CleanupFn

Subscribe to combined sync state changes.

@performance ― O(1) setup of two service streams, each update recombined in O(peers).

subscribeToSyncState
((
state: SyncState
state
) =>
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(new Error('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(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

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()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
(
state: SyncState
state
));
const unsubscribe: () => void
unsubscribe
();
Member Summary
db.add db.remove Store and delete objects and relations
db.query Query the space; returns a QueryResult
db.addType Persist a type so it replicates
db.makeRef A reference bound to this database
db.flush Wait for writes, indexes and query updates
createBranch switchBranch mergeBranch Branches of objects
syncBranch deleteBranch branch Update, drop or bind a branch
createBlob readBlob getBlobUrl Store and read blobs
appendToFeed forkFeed Write to feeds
client.graph.query getDatabase Query every space; find a space’s database
getSyncState subscribeToSyncState Replication progress

The full list is in the API reference.