Skip to content

Mutating objects

By the end of this page the app will add, edit and remove records, and you will watch a record change.

An object you make with Obj.make exists only in memory until you add it to a database. db.add starts tracking it: from then on the database stores every change and sends it to the space’s other peers.

A tracked object is read like any other, but it is changed only inside Obj.update. The callback gets a mutable view of the object. Each update is one change, which peers apply whole, and which notifies subscribers once.

Writes are local. db.add and Obj.update return once the change is in the local database, and replication runs after. Two peers that change the same object concurrently both keep their edits: ECHO merges them field by field.

  1. Write the app’s add functions. Each takes the database and the props of one type, and returns the tracked object.

    export const
    const addOrganization: (db: Database.Database, props: {
    name: string;
    website?: string;
    }) => BaseObj & Organization & OfKind<EntityKind.Object>
    addOrganization
    = (
    db: Database.Database
    db
    :
    import Database
    Database
    .
    interface Database

    ECHO Database interface.

    Database
    ,
    props: {
    name: string;
    website?: string;
    }
    props
    : {
    name: string
    name
    : string;
    website?: string | undefined
    website
    ?: string }) =>
    db: Database.Database
    db
    .
    Database.add<BaseObj & Organization & OfKind<EntityKind.Object>>(obj: BaseObj & Organization & OfKind<EntityKind.Object>, opts?: Database.AddOptions): BaseObj & Organization & 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 Organization>(type: typeof Organization, props: NoInfer<Obj.MakeProps<typeof Organization>>, options?: Obj.MakeOptions): Obj.OfShape<Organization & 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 Organization
    Organization
    ,
    props: {
    name: string;
    website?: string;
    }
    props
    ));
    export const
    const addPerson: (db: Database.Database, { employer, ...props }: {
    name: string;
    email?: string;
    employer?: Organization;
    }) => BaseObj & Person & OfKind<EntityKind.Object>
    addPerson
    = (
    db: Database.Database
    db
    :
    import Database
    Database
    .
    interface Database

    ECHO Database interface.

    Database
    ,
    {
    employer: Organization | undefined
    employer
    , ...
    props: {
    name: string;
    email?: string;
    }
    props
    }: {
    name: string
    name
    : string;
    email?: string | undefined
    email
    ?: string;
    employer?: Organization | undefined
    employer
    ?:
    class Organization
    Organization
    },
    ) =>
    db: Database.Database
    db
    .
    Database.add<BaseObj & Person & OfKind<EntityKind.Object>>(obj: BaseObj & Person & OfKind<EntityKind.Object>, opts?: Database.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
    , { ...
    props: {
    name: string;
    email?: string;
    }
    props
    , ...(
    employer: Organization | undefined
    employer
    && {
    employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined
    employer
    :
    import Ref
    Ref
    .
    const make: <Organization>(object: Organization) => Ref<Organization>

    Create a reference to an entity.

    @performance ― O(1); allocates a ref that inlines the target, with no lookup.

    make
    (
    employer: Organization
    employer
    ) }) }));
    export const
    const addEvent: (db: Database.Database, { attendees, ...props }: {
    title: string;
    description?: string;
    attendees?: Person[];
    }) => BaseObj & Event & OfKind<EntityKind.Object>
    addEvent
    = (
    db: Database.Database
    db
    :
    import Database
    Database
    .
    interface Database

    ECHO Database interface.

    Database
    ,
    {
    attendees: Person[]
    attendees
    = [], ...
    props: {
    title: string;
    description?: string;
    }
    props
    }: {
    title: string
    title
    : string;
    description?: string | undefined
    description
    ?: string;
    attendees?: Person[] | undefined
    attendees
    ?:
    class Person
    Person
    [] },
    ) =>
    db: Database.Database
    db
    .
    Database.add<BaseObj & Event & OfKind<EntityKind.Object>>(obj: BaseObj & Event & OfKind<EntityKind.Object>, opts?: Database.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
    , { ...
    props: {
    title: string;
    description?: string;
    }
    props
    ,
    attendees?: readonly Ref<Person & OfKind<EntityKind.Object> & Obj.Unknown>[] | undefined
    attendees
    :
    attendees: Person[]
    attendees
    .
    Array<Person>.map<Ref<Person>>(callbackfn: (value: Person, index: number, array: Person[]) => Ref<Person>, thisArg?: any): Ref<Person>[]

    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
    ((
    person: Person
    person
    ) =>
    import Ref
    Ref
    .
    const make: <Person>(object: Person) => Ref<Person>

    Create a reference to an entity.

    @performance ― O(1); allocates a ref that inlines the target, with no lookup.

    make
    (
    person: Person
    person
    )) }));

    A person’s employer is passed as an object and stored as a reference to it.

  2. Add records.

    const
    const northwind: BaseObj & Organization & OfKind<EntityKind.Object>
    northwind
    =
    function addOrganization(db: Database.Database, props: {
    name: string;
    website?: string;
    }): BaseObj & Organization & OfKind<EntityKind.Object>
    addOrganization
    (
    const db: Database.Database
    db
    , {
    name: string
    name
    : 'Northwind Labs' });
    const
    const ada: BaseObj & Person & OfKind<EntityKind.Object>
    ada
    =
    function addPerson(db: Database.Database, { employer, ...props }: {
    name: string;
    email?: string;
    employer?: Organization;
    }): BaseObj & Person & OfKind<EntityKind.Object>
    addPerson
    (
    const db: Database.Database
    db
    , {
    name: string
    name
    : 'Ada Lovelace',
    employer?: Organization | undefined
    employer
    :
    const northwind: BaseObj & Organization & OfKind<EntityKind.Object>
    northwind
    });
  3. Watch a record. Obj.subscribe calls back after each change to the object, and returns a function that stops the subscription.

    const
    const employers: (string | undefined)[]
    employers
    : (string | undefined)[] = [];
    const
    const unsubscribe: () => void
    unsubscribe
    =
    import Obj
    Obj
    .
    const subscribe: (obj: Obj.Unknown, callback: () => void) => (() => void)

    Subscribe to object updates. The callback is called synchronously when the object is modified. Only accepts reactive objects (not snapshots).

    @returns ― Unsubscribe function.

    @performance ― O(1) listener registration; the callback runs synchronously after every committed change.

    subscribe
    (
    const ada: BaseObj & Person & OfKind<EntityKind.Object>
    ada
    , () => {
    const employers: (string | undefined)[]
    employers
    .
    Array<string | undefined>.push(...items: (string | undefined)[]): number

    Appends new elements to the end of an array, and returns the new length of the array.

    @param ― items New elements to add to the array.

    push
    (
    const ada: BaseObj & Person & OfKind<EntityKind.Object>
    ada
    .
    employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined
    employer
    ?.
    Ref<Organization & OfKind<EntityKind.Object> & Unknown>.peek(): (Organization & OfKind<EntityKind.Object> & Obj.Unknown) | undefined

    @returns ― The target when it is already materialized: the pinned target, or a side-effect-free working-set lookup. Never throws and never triggers loading — the synchronous counterpart of tryLoad . A just-added object can resolve here before it has settled into its own document; callers that need a settled document must load instead.

    @performance ― O(1) working-set lookup; never loads and never throws.

    peek
    ()?.
    name: string | undefined
    name
    );
    });
  4. Change a record. The app moves a person to another organization in one update, and its form writes whatever fields the reader changed.

    /** Moves a person to another organization. */
    export const
    const changeEmployer: (person: Person, employer: Organization) => Person

    Moves a person to another organization.

    changeEmployer
    = (
    person: Person
    person
    :
    class Person
    Person
    ,
    employer: Organization
    employer
    :
    class Organization
    Organization
    ) =>
    import Obj
    Obj
    .
    const update: <Person>(obj: Person, callback: ChangeCallback<Person>) => Person

    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
    (
    person: Person
    person
    , (
    person: {
    name: string;
    email?: string | undefined;
    employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined;
    id: EntityId;
    "~@dxos/echo/Kind": EntityKind.Object;
    }
    person
    ) => {
    person: {
    name: string;
    email?: string | undefined;
    employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined;
    id: EntityId;
    "~@dxos/echo/Kind": EntityKind.Object;
    }
    person
    .
    employer?: Ref<Organization & OfKind<EntityKind.Object> & Obj.Unknown> | undefined
    employer
    =
    import Ref
    Ref
    .
    const make: <Organization>(object: Organization) => Ref<Organization>

    Create a reference to an entity.

    @performance ― O(1); allocates a ref that inlines the target, with no lookup.

    make
    (
    employer: Organization
    employer
    );
    });
    /** Writes the values a form returns onto a record, one field at a time. */
    export const
    const updateRecord: (record: Obj.Unknown, values: Record<string, unknown>) => Obj.Unknown

    Writes the values a form returns onto a record, one field at a time.

    updateRecord
    = (
    record: Obj.Unknown
    record
    :
    import Obj
    Obj
    .
    interface Unknown

    Object with no known properties beyond id and kind. Use this when the object's schema/properties are not known. For objects with arbitrary properties, use Obj.AnyProps.

    NOTE: This is a TypeScript type only, not a schema. To validate that a value is an ECHO object, use Schema.is(Type.Obj).

    Runtime Effect schema for any ECHO object. Use for validation, parsing, or as a reference target for collections.

    NOTE: Schema.is(Type.Obj) does STRUCTURAL validation only (checks for id field). Use Obj.isObject() for proper ECHO instance type guards that check the KindId brand.

    @example

    // Structural type guard (accepts any object with id field)
    if (Schema.is(Type.Obj)(unknownValue)) { ... }
    // ECHO instance type guard (checks KindId brand)
    if (Obj.isObject(unknownValue)) { ... }
    // Reference to any object type
    class Collection extends Type.makeObject<Collection>(DXN.make('com.example.type.collection', '0.1.0'))(
    Schema.Struct({ objects: Schema.Array(Ref.Ref(Obj.Unknown)) }),
    ) {}

    Unknown
    ,
    values: Record<string, unknown>
    values
    :
    type Record<K extends keyof any, T> = { [P in K]: T; }

    Construct a type with a set of properties K of type T

    Record
    <string, unknown>) =>
    import Obj
    Obj
    .
    const update: <Obj.Unknown>(obj: Obj.Unknown, callback: ChangeCallback<Obj.Unknown>) => Obj.Unknown

    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
    (
    record: Obj.Unknown
    record
    , (
    record: {
    id: EntityId;
    "~@dxos/echo/Kind": EntityKind.Object;
    }
    record
    ) => {
    for (const [
    const key: string
    key
    ,
    const value: unknown
    value
    ] of
    var Object: ObjectConstructor

    Provides functionality common to all JavaScript objects.

    Object
    .
    ObjectConstructor.entries<unknown>(o: {
    [s: string]: unknown;
    } | ArrayLike<unknown>): [string, unknown][] (+1 overload)

    Returns an array of key/values of the enumerable own properties of an object

    @param ― o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.

    entries
    (
    values: Record<string, unknown>
    values
    )) {
    import Obj
    Obj
    .
    const setValue: (obj: Obj.Mutable<Obj.Unknown>, path: readonly (string | number)[], value: any) => void

    Set a deeply nested property on an object, using the object's schema to determine whether to initialize nested data as an empty object or array.

    Similar to lodash.set and setDeep from

    @dxos ―

    /util, but schema-aware. Must be called within an Obj.update callback.

    NOTE: TypeScript's structural typing allows readonly objects to be passed to Mutable<T> parameters, so there is no compile-time error. Enforcement is runtime-only.

    @param ― obj - The mutable ECHO object to set the property on.

    @param ― path - Path to the property (array of keys).

    @param ― value - Value to set.

    @returns ― The value that was set.

    @example

    const person = Obj.make(Person, { name: 'John' });
    // Person schema has: addresses: Schema.Array(Address)
    Obj.update(person, (obj) => {
    Obj.setValue(obj, ['addresses', 0, 'street'], '123 Main St');
    });
    // Creates: person.addresses = [{ street: '123 Main St' }]

    @performance ― O(path length) schema-guided walk; each missing level scans its schema properties once.

    setValue
    (
    record: {
    id: EntityId;
    "~@dxos/echo/Kind": EntityKind.Object;
    }
    record
    , [
    const key: string
    key
    ],
    const value: unknown
    value
    );
    }
    });
    const
    const halcyon: BaseObj & Organization & OfKind<EntityKind.Object>
    halcyon
    =
    function addOrganization(db: Database.Database, props: {
    name: string;
    website?: string;
    }): BaseObj & Organization & OfKind<EntityKind.Object>
    addOrganization
    (
    const db: Database.Database
    db
    , {
    name: string
    name
    : 'Halcyon Robotics' });
    function changeEmployer(person: Person, employer: Organization): Person

    Moves a person to another organization.

    changeEmployer
    (
    const ada: BaseObj & Person & OfKind<EntityKind.Object>
    ada
    ,
    const halcyon: BaseObj & Organization & OfKind<EntityKind.Object>
    halcyon
    );

    The subscriber from step 3 has run, so employers holds 'Halcyon Robotics'.

  5. Remove a record.

    export const
    const removeRecord: (db: Database.Database, record: Obj.Unknown) => void
    removeRecord
    = (
    db: Database.Database
    db
    :
    import Database
    Database
    .
    interface Database

    ECHO Database interface.

    Database
    ,
    record: Obj.Unknown
    record
    :
    import Obj
    Obj
    .
    interface Unknown

    Object with no known properties beyond id and kind. Use this when the object's schema/properties are not known. For objects with arbitrary properties, use Obj.AnyProps.

    NOTE: This is a TypeScript type only, not a schema. To validate that a value is an ECHO object, use Schema.is(Type.Obj).

    Runtime Effect schema for any ECHO object. Use for validation, parsing, or as a reference target for collections.

    NOTE: Schema.is(Type.Obj) does STRUCTURAL validation only (checks for id field). Use Obj.isObject() for proper ECHO instance type guards that check the KindId brand.

    @example

    // Structural type guard (accepts any object with id field)
    if (Schema.is(Type.Obj)(unknownValue)) { ... }
    // ECHO instance type guard (checks KindId brand)
    if (Obj.isObject(unknownValue)) { ... }
    // Reference to any object type
    class Collection extends Type.makeObject<Collection>(DXN.make('com.example.type.collection', '0.1.0'))(
    Schema.Struct({ objects: Schema.Array(Ref.Ref(Obj.Unknown)) }),
    ) {}

    Unknown
    ) =>
    db: Database.Database
    db
    .
    Database.remove(obj: Unknown, opts?: Database.WriteOptions): void

    Removes object from the database.

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

    remove
    (
    record: Obj.Unknown
    record
    );
    function removeRecord(db: Database.Database, record: Obj.Unknown): void
    removeRecord
    (
    const db: Database.Database
    db
    ,
    const ada: BaseObj & Person & OfKind<EntityKind.Object>
    ada
    );

Select a record and change it in the form, or press New to add one of the chosen type. The × on a row removes it. A person’s employer is a picker of the organizations in the space.

You can add records to a database, change them inside Obj.update, watch them change, and remove them.

Querying: list the records, sorted and searched.