Y.js Binding (mobx-keystone-yjs)
The mobx-keystone-yjs package keeps a Y.js document in sync with a mobx-keystone store and vice versa. This is useful when multiple clients need to stay in sync through peer-to-peer networking, an intermediate server, or another transport. Since Y.js is a CRDT, concurrent edits remain conflict-free, which makes optimistic updates practical even when clients go offline temporarily.
Binding Y.js data to a model instance
const {
// The bound mobx-keystone instance.
boundObject,
// Disposes the binding.
dispose,
// The Y.js origin symbol used for binding transactions.
yjsOrigin,
} = bindYjsToMobxKeystone({
// The mobx-keystone model type.
mobxKeystoneType,
// The Y.js document.
yjsDoc,
// The bound Y.js data structure.
yjsObject,
})
Note that the yjsObject must be a Y.Map, Y.Array or a Y.Text and its structure must be compatible with the provided mobx-keystone type (or, to be more precise, its snapshot form).
First migration - converting JSON to Y.js data
If you already have a model instance snapshot stored somewhere and want to start binding it to a Y.js data structure you can use the convertJsonToYjsData function to make this first migration.
convertJsonToYjsData takes a single argument, a JSON value (usually a snapshot of the model you want to bind) and returns a Y.js data structure (Y.Map, Y.Array, etc.) ready to be bound to that model. Frozen values are a special case and they are kept as immutable plain values.
If you already have an existing Y.Map or Y.Array and want to copy JSON data into it, use the helper functions:
applyJsonObjectToYMap(dest, source, options?)applyJsonArrayToYArray(dest, source, options?)
These are useful when seeding or refreshing part of a document without manually building nested Y.js containers yourself.
The optional options.mode controls how data is applied:
"add"- Create and insert newY.jscontainers from the JSON data. This is the default."merge"- Recursively merge into existingY.Map/Y.Arraycontainers when possible, preserving existing container references.
For example:
const ymap = ydoc.getMap("rootStore")
// First seed
applyJsonObjectToYMap(ymap, snapshot)
// Later, merge an updated snapshot while preserving existing nested containers
applyJsonObjectToYMap(ymap, nextSnapshot, { mode: "merge" })
Using Y.Text as a model node
The special model YjsTextModel can be used to bind a Y.Text to a mobx-keystone model.
const text = YjsTextModel.withText("Hello world!")
// once `text` is part of a bound tree:
text.yjsText.insert(0, "Say: ")
text.text // "Say: Hello world!"
Note that yjsText throws if you access it while the model is not part of a bound tree. This is due to a limitation of Y.js, since it only allows limited manipulation of types while they are outside a Y.Doc tree.
The YjsBindingContext
All nodes inside a bound tree have access to a YjsBindingContext instance.
The instance can be accessed using:
yjsBindingContext.get(nodePartOfTheBoundTree)
And this instance provides access to the following data:
yjsDoc: TheY.jsdocument.yjsObject: The boundY.jsdata structure.mobxKeystoneType: Themobx-keystonemodel type.yjsOrigin: The origin symbol used for transactions.boundObject: The boundmobx-keystoneinstance.isApplyingYjsChangesToMobxKeystone: Whether we are currently applyingY.jschanges to themobx-keystonemodel.
Example
A full example is available here.