mobx-keystone-yjs
Version:
Yjs bindings for mobx-keystone
78 lines (72 loc) • 2.78 kB
text/typescript
import { DeepChange, DeepChangeType } from "mobx-keystone"
import * as Y from "yjs"
import { failure } from "../utils/error"
import { isYjsValueDeleted } from "../utils/isYjsValueDeleted"
import { convertJsonToYjsData } from "./convertJsonToYjsData"
import { resolveYjsPath } from "./resolveYjsPath"
/**
* Converts a snapshot value to a Yjs-compatible value.
* Note: All values passed here are already snapshots (captured at change time).
*/
function convertValue(v: unknown): any {
// Handle primitives directly
if (v === null || v === undefined || typeof v !== "object") {
return v
}
// Handle plain arrays - used for empty array init
if (Array.isArray(v) && v.length === 0) {
return new Y.Array()
}
// Value is already a snapshot, convert to Yjs data
return convertJsonToYjsData(v as any)
}
export function applyMobxChangeToYjsObject(
change: DeepChange,
yjsObject: Y.Map<any> | Y.Array<any> | Y.Text
): void {
// Check if the YJS object is deleted
if (isYjsValueDeleted(yjsObject)) {
throw failure("cannot apply patch to deleted Yjs value")
}
const yjsContainer = resolveYjsPath(yjsObject, change.path)
if (!yjsContainer) {
// Container not found, skip this change
return
}
if (yjsContainer instanceof Y.Array) {
if (change.type === DeepChangeType.ArraySplice) {
// splice
yjsContainer.delete(change.index, change.removedValues.length)
if (change.addedValues.length > 0) {
const valuesToInsert = change.addedValues.map(convertValue)
yjsContainer.insert(change.index, valuesToInsert)
}
} else if (change.type === DeepChangeType.ArrayUpdate) {
// update
yjsContainer.delete(change.index, 1)
yjsContainer.insert(change.index, [convertValue(change.newValue)])
} else {
throw failure(`unsupported array change type: ${change.type}`)
}
} else if (yjsContainer instanceof Y.Map) {
if (change.type === DeepChangeType.ObjectAdd || change.type === DeepChangeType.ObjectUpdate) {
const key = change.key
if (change.newValue === undefined) {
yjsContainer.delete(key)
} else {
yjsContainer.set(key, convertValue(change.newValue))
}
} else if (change.type === DeepChangeType.ObjectRemove) {
const key = change.key
yjsContainer.delete(key)
} else {
throw failure(`unsupported object change type: ${change.type}`)
}
} else if (yjsContainer instanceof Y.Text) {
// Y.Text is handled differently - init changes for text are managed by YjsTextModel
// Skip init changes for Y.Text containers
return
} else {
throw failure(`unsupported Yjs container type: ${yjsContainer}`)
}
}