Skip to content

Models and ownership

A Plexus model is a TypeScript object first. Decorating an accessor with @syncing makes it eligible for shared state; merely constructing the object does not put it in a Yjs document.

import { Plexus, PlexusModel, syncing } from '@here.build/plexus';
@syncing('Task')
class Task extends PlexusModel<Project> {
@syncing accessor title = '';
}
@syncing('Project')
class Project extends PlexusModel {
@syncing.child.list accessor tasks: Task[];
}
const root = Plexus.bootstrap(new Project()).root;
const task = new Task({ title: 'draft' }); // doc-free
root.tasks.push(task); // now reachable from the document

Doc-free models support field access and mutation. Adding a child to a document-backed parent materializes the child into that document. This is the key distinction when debugging “why isn’t my new object synced?”

Materialization follows ownership in both directions: a doc-free child under a doc-backed parent joins the parent’s document; a doc-backed child adopted by a doc-free parent brings that parent into its document. An entity already attributed to a different document cannot be moved into another: Plexus raises PlexusDocMismatchError. Materialized entities never switch documents.

.uuid is a document-backed, cross-peer identity and throws before materialization. If you need a local handle for a doc-free object, keep its object reference; .localID is available for deterministic tests and debugging, but is not serialized.

Use @syncing on accessors, not ordinary class fields. The existing Plexus guide also documents @syncing.list, .set, .record, .map, and .child.list. Children participate in ownership; plain references are a different relationship. Derive local display values with MobX rather than trying to replicate a computed property.

Source contracts: plexus/README.md; plexus/docs/src/guide/fields.md; plexus/docs/src/laws/lifecycle.md; plexus/docs/src/laws/seed.md. The law pages specify the edge cases; this introduction does not replace them.