mobx-keystone-yjs 1.5.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -52
- package/dist/mobx-keystone-yjs.esm.js +334 -216
- package/dist/mobx-keystone-yjs.esm.mjs +334 -216
- package/dist/mobx-keystone-yjs.umd.js +332 -214
- package/dist/types/binding/applyMobxChangeToYjsObject.d.ts +3 -0
- package/dist/types/binding/applyYjsEventToMobx.d.ts +8 -0
- package/dist/types/binding/convertJsonToYjsData.d.ts +22 -3
- package/dist/types/binding/resolveYjsPath.d.ts +14 -1
- package/dist/types/binding/yjsSnapshotTracking.d.ts +24 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +90 -88
- package/src/binding/applyMobxChangeToYjsObject.ts +77 -0
- package/src/binding/applyYjsEventToMobx.ts +173 -0
- package/src/binding/bindYjsToMobxKeystone.ts +300 -202
- package/src/binding/convertJsonToYjsData.ts +218 -73
- package/src/binding/resolveYjsPath.ts +51 -27
- package/src/binding/yjsSnapshotTracking.ts +40 -0
- package/src/index.ts +11 -10
- package/src/utils/getOrCreateYjsCollectionAtom.ts +27 -27
- package/dist/types/binding/applyMobxKeystonePatchToYjsObject.d.ts +0 -2
- package/dist/types/binding/convertYjsEventToPatches.d.ts +0 -3
- package/src/binding/applyMobxKeystonePatchToYjsObject.ts +0 -103
- package/src/binding/convertYjsEventToPatches.ts +0 -84
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
export type ReconciliationMap = Map<string, object>;
|
|
3
|
+
/**
|
|
4
|
+
* Applies a Y.js event directly to the MobX model tree using proper mutations
|
|
5
|
+
* (splice for arrays, property assignment for objects).
|
|
6
|
+
* This is more efficient than converting to patches first.
|
|
7
|
+
*/
|
|
8
|
+
export declare function applyYjsEventToMobx(event: Y.YEvent<any>, boundObject: object, reconciliationMap: ReconciliationMap): void;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { YjsData } from './convertYjsDataToJson';
|
|
2
1
|
import { PlainArray, PlainObject, PlainValue } from '../plainTypes';
|
|
2
|
+
import { YjsData } from './convertYjsDataToJson';
|
|
3
3
|
import * as Y from "yjs";
|
|
4
|
+
/**
|
|
5
|
+
* Options for applying JSON data to Y.js data structures.
|
|
6
|
+
*/
|
|
7
|
+
export interface ApplyJsonToYjsOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The mode to use when applying JSON data to Y.js data structures.
|
|
10
|
+
* - `add`: Creates new Y.js containers for objects/arrays (default, backwards compatible)
|
|
11
|
+
* - `merge`: Recursively merges values, preserving existing container references where possible
|
|
12
|
+
*/
|
|
13
|
+
mode?: "add" | "merge";
|
|
14
|
+
}
|
|
4
15
|
/**
|
|
5
16
|
* Converts a plain value to a Y.js data structure.
|
|
6
17
|
* Objects are converted to Y.Maps, arrays to Y.Arrays, primitives are untouched.
|
|
@@ -9,9 +20,17 @@ import * as Y from "yjs";
|
|
|
9
20
|
export declare function convertJsonToYjsData(v: PlainValue): YjsData;
|
|
10
21
|
/**
|
|
11
22
|
* Applies a JSON array to a Y.Array, using the convertJsonToYjsData to convert the values.
|
|
23
|
+
*
|
|
24
|
+
* @param dest The destination Y.Array.
|
|
25
|
+
* @param source The source JSON array.
|
|
26
|
+
* @param options Options for applying the JSON data.
|
|
12
27
|
*/
|
|
13
|
-
export declare const applyJsonArrayToYArray: (dest: Y.Array<any>, source: PlainArray) => void;
|
|
28
|
+
export declare const applyJsonArrayToYArray: (dest: Y.Array<any>, source: PlainArray, options?: ApplyJsonToYjsOptions) => void;
|
|
14
29
|
/**
|
|
15
30
|
* Applies a JSON object to a Y.Map, using the convertJsonToYjsData to convert the values.
|
|
31
|
+
*
|
|
32
|
+
* @param dest The destination Y.Map.
|
|
33
|
+
* @param source The source JSON object.
|
|
34
|
+
* @param options Options for applying the JSON data.
|
|
16
35
|
*/
|
|
17
|
-
export declare const applyJsonObjectToYMap: (dest: Y.Map<any>, source: PlainObject) => void;
|
|
36
|
+
export declare const applyJsonObjectToYMap: (dest: Y.Map<any>, source: PlainObject, options?: ApplyJsonToYjsOptions) => void;
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a path within a Yjs object structure.
|
|
4
|
+
* Returns the Yjs container at the specified path.
|
|
5
|
+
*
|
|
6
|
+
* When a Y.Text is encountered during path resolution (either at the start
|
|
7
|
+
* or mid-path), it is returned immediately since Y.Text doesn't support
|
|
8
|
+
* nested path traversal.
|
|
9
|
+
*
|
|
10
|
+
* @param yjsObject The root Yjs object
|
|
11
|
+
* @param path Array of keys/indices to traverse
|
|
12
|
+
* @returns The Yjs container at the path, or Y.Text if encountered during traversal
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveYjsPath(yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text, path: readonly (string | number)[]): unknown;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
/**
|
|
3
|
+
* WeakMap that tracks which snapshot each Y.js container was last synced from.
|
|
4
|
+
* This is used during reconciliation to skip containers that are already up-to-date.
|
|
5
|
+
*
|
|
6
|
+
* The key is the Y.js container (Y.Map or Y.Array).
|
|
7
|
+
* The value is the snapshot (plain object or array) that was last synced to it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const yjsContainerToSnapshot: WeakMap<Y.Map<any> | Y.Array<any>, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Updates the snapshot tracking for a Y.js container.
|
|
12
|
+
* Call this after syncing a snapshot to a Y.js container.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setYjsContainerSnapshot(container: Y.Map<any> | Y.Array<any>, snapshot: unknown): void;
|
|
15
|
+
/**
|
|
16
|
+
* Gets the last synced snapshot for a Y.js container.
|
|
17
|
+
* Returns undefined if the container has never been synced.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getYjsContainerSnapshot(container: Y.Map<any> | Y.Array<any>): unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a Y.js container is up-to-date with the given snapshot.
|
|
22
|
+
* Uses reference equality to check if the snapshot is the same.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isYjsContainerUpToDate(container: Y.Map<any> | Y.Array<any>, snapshot: unknown): boolean;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { bindYjsToMobxKeystone } from './binding/bindYjsToMobxKeystone';
|
|
2
|
+
export type { ApplyJsonToYjsOptions } from './binding/convertJsonToYjsData';
|
|
2
3
|
export { applyJsonArrayToYArray, applyJsonObjectToYMap, convertJsonToYjsData, } from './binding/convertJsonToYjsData';
|
|
3
4
|
export { YjsTextModel, yjsTextModelId } from './binding/YjsTextModel';
|
|
4
5
|
export type { YjsBindingContext } from './binding/yjsBindingContext';
|
package/package.json
CHANGED
|
@@ -1,88 +1,90 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mobx-keystone-yjs",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Yjs bindings for mobx-keystone",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"mobx",
|
|
7
|
-
"mobx-keystone",
|
|
8
|
-
"yjs",
|
|
9
|
-
"crdt",
|
|
10
|
-
"state
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"url": "https://github.com/xaviergonz/mobx-keystone
|
|
18
|
-
},
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"main": "./dist/mobx-keystone-yjs.umd.js",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"build": "
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"@babel/
|
|
69
|
-
"@babel/
|
|
70
|
-
"@babel/
|
|
71
|
-
"@
|
|
72
|
-
"@
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"ts
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "mobx-keystone-yjs",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Yjs bindings for mobx-keystone",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mobx",
|
|
7
|
+
"mobx-keystone",
|
|
8
|
+
"yjs",
|
|
9
|
+
"crdt",
|
|
10
|
+
"state",
|
|
11
|
+
"state-management",
|
|
12
|
+
"reactive",
|
|
13
|
+
"collaborative"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/xaviergonz/mobx-keystone.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/xaviergonz/mobx-keystone/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://mobx-keystone.js.org",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Javier González Garcés",
|
|
25
|
+
"source": "./src/index.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/types/index.d.ts",
|
|
30
|
+
"script": "./dist/mobx-keystone-yjs.umd.js",
|
|
31
|
+
"import": "./dist/mobx-keystone-yjs.esm.mjs",
|
|
32
|
+
"require": "./dist/mobx-keystone-yjs.umd.js",
|
|
33
|
+
"default": "./dist/mobx-keystone-yjs.esm.mjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"esmodule": "./dist/mobx-keystone-yjs.esm.js",
|
|
37
|
+
"module": "./dist/mobx-keystone-yjs.esm.js",
|
|
38
|
+
"jsnext:main": "./dist/mobx-keystone-yjs.esm.js",
|
|
39
|
+
"react-native": "./dist/mobx-keystone-yjs.umd.js",
|
|
40
|
+
"umd:main": "./dist/mobx-keystone-yjs.umd.js",
|
|
41
|
+
"unpkg": "./dist/mobx-keystone-yjs.umd.js",
|
|
42
|
+
"jsdelivr": "./dist/mobx-keystone-yjs.umd.js",
|
|
43
|
+
"main": "./dist/mobx-keystone-yjs.umd.js",
|
|
44
|
+
"types": "./dist/types/index.d.ts",
|
|
45
|
+
"typings": "./dist/types/index.d.ts",
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"files": [
|
|
48
|
+
"src",
|
|
49
|
+
"dist",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"CHANGELOG.md",
|
|
52
|
+
"README.md"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"quick-build": "tsc",
|
|
56
|
+
"quick-build-tests": "tsc -p test",
|
|
57
|
+
"copy-root-files": "shx cp ../../LICENSE .",
|
|
58
|
+
"build": "yarn quick-build && yarn copy-root-files && shx rm -rf dist && vite build && shx cp dist/mobx-keystone-yjs.esm.mjs dist/mobx-keystone-yjs.esm.js",
|
|
59
|
+
"test": "jest",
|
|
60
|
+
"test:ci": "yarn test -i"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"mobx": "^6.0.0 || ^5.0.0 || ^4.0.0",
|
|
64
|
+
"mobx-keystone": "^1.12.0",
|
|
65
|
+
"yjs": "^13.0.0"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@babel/core": "^7.28.5",
|
|
69
|
+
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
70
|
+
"@babel/plugin-proposal-decorators": "^7.28.0",
|
|
71
|
+
"@babel/preset-env": "^7.28.5",
|
|
72
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
73
|
+
"@types/jest": "^30.0.0",
|
|
74
|
+
"@types/node": "^25.0.3",
|
|
75
|
+
"babel-jest": "^30.2.0",
|
|
76
|
+
"jest": "^30.2.0",
|
|
77
|
+
"mobx-keystone": "workspace:packages/lib",
|
|
78
|
+
"shx": "^0.4.0",
|
|
79
|
+
"spec.ts": "^1.1.3",
|
|
80
|
+
"ts-jest": "^29.4.6",
|
|
81
|
+
"ts-node": "^10.9.2",
|
|
82
|
+
"typescript": "^5.9.3"
|
|
83
|
+
},
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"tslib": "^2.8.1"
|
|
86
|
+
},
|
|
87
|
+
"directories": {
|
|
88
|
+
"test": "test"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { DeepChange, DeepChangeType } from "mobx-keystone"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
import { failure } from "../utils/error"
|
|
4
|
+
import { isYjsValueDeleted } from "../utils/isYjsValueDeleted"
|
|
5
|
+
import { convertJsonToYjsData } from "./convertJsonToYjsData"
|
|
6
|
+
import { resolveYjsPath } from "./resolveYjsPath"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a snapshot value to a Yjs-compatible value.
|
|
10
|
+
* Note: All values passed here are already snapshots (captured at change time).
|
|
11
|
+
*/
|
|
12
|
+
function convertValue(v: unknown): any {
|
|
13
|
+
// Handle primitives directly
|
|
14
|
+
if (v === null || v === undefined || typeof v !== "object") {
|
|
15
|
+
return v
|
|
16
|
+
}
|
|
17
|
+
// Handle plain arrays - used for empty array init
|
|
18
|
+
if (Array.isArray(v) && v.length === 0) {
|
|
19
|
+
return new Y.Array()
|
|
20
|
+
}
|
|
21
|
+
// Value is already a snapshot, convert to Yjs data
|
|
22
|
+
return convertJsonToYjsData(v as any)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function applyMobxChangeToYjsObject(
|
|
26
|
+
change: DeepChange,
|
|
27
|
+
yjsObject: Y.Map<any> | Y.Array<any> | Y.Text
|
|
28
|
+
): void {
|
|
29
|
+
// Check if the YJS object is deleted
|
|
30
|
+
if (isYjsValueDeleted(yjsObject)) {
|
|
31
|
+
throw failure("cannot apply patch to deleted Yjs value")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const yjsContainer = resolveYjsPath(yjsObject, change.path)
|
|
35
|
+
|
|
36
|
+
if (!yjsContainer) {
|
|
37
|
+
// Container not found, skip this change
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (yjsContainer instanceof Y.Array) {
|
|
42
|
+
if (change.type === DeepChangeType.ArraySplice) {
|
|
43
|
+
// splice
|
|
44
|
+
yjsContainer.delete(change.index, change.removedValues.length)
|
|
45
|
+
if (change.addedValues.length > 0) {
|
|
46
|
+
const valuesToInsert = change.addedValues.map(convertValue)
|
|
47
|
+
yjsContainer.insert(change.index, valuesToInsert)
|
|
48
|
+
}
|
|
49
|
+
} else if (change.type === DeepChangeType.ArrayUpdate) {
|
|
50
|
+
// update
|
|
51
|
+
yjsContainer.delete(change.index, 1)
|
|
52
|
+
yjsContainer.insert(change.index, [convertValue(change.newValue)])
|
|
53
|
+
} else {
|
|
54
|
+
throw failure(`unsupported array change type: ${change.type}`)
|
|
55
|
+
}
|
|
56
|
+
} else if (yjsContainer instanceof Y.Map) {
|
|
57
|
+
if (change.type === DeepChangeType.ObjectAdd || change.type === DeepChangeType.ObjectUpdate) {
|
|
58
|
+
const key = change.key
|
|
59
|
+
if (change.newValue === undefined) {
|
|
60
|
+
yjsContainer.delete(key)
|
|
61
|
+
} else {
|
|
62
|
+
yjsContainer.set(key, convertValue(change.newValue))
|
|
63
|
+
}
|
|
64
|
+
} else if (change.type === DeepChangeType.ObjectRemove) {
|
|
65
|
+
const key = change.key
|
|
66
|
+
yjsContainer.delete(key)
|
|
67
|
+
} else {
|
|
68
|
+
throw failure(`unsupported object change type: ${change.type}`)
|
|
69
|
+
}
|
|
70
|
+
} else if (yjsContainer instanceof Y.Text) {
|
|
71
|
+
// Y.Text is handled differently - init changes for text are managed by YjsTextModel
|
|
72
|
+
// Skip init changes for Y.Text containers
|
|
73
|
+
return
|
|
74
|
+
} else {
|
|
75
|
+
throw failure(`unsupported Yjs container type: ${yjsContainer}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { remove } from "mobx"
|
|
2
|
+
import {
|
|
3
|
+
Frozen,
|
|
4
|
+
fromSnapshot,
|
|
5
|
+
frozen,
|
|
6
|
+
getSnapshot,
|
|
7
|
+
getSnapshotModelId,
|
|
8
|
+
isFrozenSnapshot,
|
|
9
|
+
isModel,
|
|
10
|
+
Path,
|
|
11
|
+
resolvePath,
|
|
12
|
+
runUnprotected,
|
|
13
|
+
} from "mobx-keystone"
|
|
14
|
+
import * as Y from "yjs"
|
|
15
|
+
import { failure } from "../utils/error"
|
|
16
|
+
import { convertYjsDataToJson } from "./convertYjsDataToJson"
|
|
17
|
+
|
|
18
|
+
// Represents the map of potential objects to reconcile (ID -> Object)
|
|
19
|
+
export type ReconciliationMap = Map<string, object>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Applies a Y.js event directly to the MobX model tree using proper mutations
|
|
23
|
+
* (splice for arrays, property assignment for objects).
|
|
24
|
+
* This is more efficient than converting to patches first.
|
|
25
|
+
*/
|
|
26
|
+
export function applyYjsEventToMobx(
|
|
27
|
+
event: Y.YEvent<any>,
|
|
28
|
+
boundObject: object,
|
|
29
|
+
reconciliationMap: ReconciliationMap
|
|
30
|
+
): void {
|
|
31
|
+
const path = event.path as Path
|
|
32
|
+
const { value: target } = resolvePath(boundObject, path)
|
|
33
|
+
|
|
34
|
+
if (!target) {
|
|
35
|
+
throw failure(`cannot resolve path ${JSON.stringify(path)}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wrap in runUnprotected since we're modifying the tree from outside a model action
|
|
39
|
+
runUnprotected(() => {
|
|
40
|
+
if (event instanceof Y.YMapEvent) {
|
|
41
|
+
applyYMapEventToMobx(event, target, reconciliationMap)
|
|
42
|
+
} else if (event instanceof Y.YArrayEvent) {
|
|
43
|
+
applyYArrayEventToMobx(event, target, reconciliationMap)
|
|
44
|
+
} else if (event instanceof Y.YTextEvent) {
|
|
45
|
+
applyYTextEventToMobx(event, target)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function processDeletedValue(val: unknown, reconciliationMap: ReconciliationMap) {
|
|
51
|
+
if (val && typeof val === "object" && isModel(val)) {
|
|
52
|
+
const sn = getSnapshot(val)
|
|
53
|
+
const id = getSnapshotModelId(sn)
|
|
54
|
+
if (id) {
|
|
55
|
+
reconciliationMap.set(id, val)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function reviveValue(jsonValue: any, reconciliationMap: ReconciliationMap): any {
|
|
61
|
+
// Handle primitives
|
|
62
|
+
if (jsonValue === null || typeof jsonValue !== "object") {
|
|
63
|
+
return jsonValue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle frozen
|
|
67
|
+
if (isFrozenSnapshot(jsonValue)) {
|
|
68
|
+
return frozen(jsonValue.data)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If we have a reconciliation map and the value looks like a model with an ID, check if we have it
|
|
72
|
+
if (reconciliationMap && jsonValue && typeof jsonValue === "object") {
|
|
73
|
+
const modelId = getSnapshotModelId(jsonValue)
|
|
74
|
+
if (modelId) {
|
|
75
|
+
const existing = reconciliationMap.get(modelId)
|
|
76
|
+
if (existing) {
|
|
77
|
+
reconciliationMap.delete(modelId)
|
|
78
|
+
return existing
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return fromSnapshot(jsonValue)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function applyYMapEventToMobx(
|
|
87
|
+
event: Y.YMapEvent<any>,
|
|
88
|
+
target: Record<string, any>,
|
|
89
|
+
reconciliationMap: ReconciliationMap
|
|
90
|
+
): void {
|
|
91
|
+
const source = event.target
|
|
92
|
+
|
|
93
|
+
event.changes.keys.forEach((change, key) => {
|
|
94
|
+
switch (change.action) {
|
|
95
|
+
case "add":
|
|
96
|
+
case "update": {
|
|
97
|
+
const yjsValue = source.get(key)
|
|
98
|
+
const jsonValue = convertYjsDataToJson(yjsValue)
|
|
99
|
+
|
|
100
|
+
// If updating, the old value is overwritten (deleted conceptually)
|
|
101
|
+
if (change.action === "update") {
|
|
102
|
+
processDeletedValue(target[key], reconciliationMap)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
target[key] = reviveValue(jsonValue, reconciliationMap)
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "delete": {
|
|
110
|
+
processDeletedValue(target[key], reconciliationMap)
|
|
111
|
+
// Use MobX's remove to properly delete the key from the observable object
|
|
112
|
+
// This triggers the "remove" interceptor in mobx-keystone's tweaker
|
|
113
|
+
if (isModel(target)) {
|
|
114
|
+
remove(target.$, key)
|
|
115
|
+
} else {
|
|
116
|
+
remove(target, key)
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
default:
|
|
122
|
+
throw failure(`unsupported Yjs map event action: ${change.action}`)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function applyYArrayEventToMobx(
|
|
128
|
+
event: Y.YArrayEvent<any>,
|
|
129
|
+
target: any[],
|
|
130
|
+
reconciliationMap: ReconciliationMap
|
|
131
|
+
): void {
|
|
132
|
+
// Process delta operations in order
|
|
133
|
+
let currentIndex = 0
|
|
134
|
+
|
|
135
|
+
for (const change of event.changes.delta) {
|
|
136
|
+
if (change.retain) {
|
|
137
|
+
currentIndex += change.retain
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (change.delete) {
|
|
141
|
+
// Capture deleted items for reconciliation
|
|
142
|
+
const deletedItems = target.slice(currentIndex, currentIndex + change.delete)
|
|
143
|
+
deletedItems.forEach((item) => {
|
|
144
|
+
processDeletedValue(item, reconciliationMap)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Delete items at current position
|
|
148
|
+
target.splice(currentIndex, change.delete)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (change.insert) {
|
|
152
|
+
// Insert items at current position
|
|
153
|
+
const insertedItems = Array.isArray(change.insert) ? change.insert : [change.insert]
|
|
154
|
+
const values = insertedItems.map((yjsValue) => {
|
|
155
|
+
const jsonValue = convertYjsDataToJson(yjsValue)
|
|
156
|
+
return reviveValue(jsonValue, reconciliationMap)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
target.splice(currentIndex, 0, ...values)
|
|
160
|
+
currentIndex += values.length
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyYTextEventToMobx(
|
|
166
|
+
event: Y.YTextEvent,
|
|
167
|
+
target: { deltaList?: Frozen<unknown[]>[] }
|
|
168
|
+
): void {
|
|
169
|
+
// YjsTextModel handles text events by appending delta to deltaList
|
|
170
|
+
if (target?.deltaList) {
|
|
171
|
+
target.deltaList.push(frozen(event.delta))
|
|
172
|
+
}
|
|
173
|
+
}
|