mobx-keystone-yjs 1.4.0 → 1.5.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.
@@ -1,72 +1,78 @@
1
- import * as Y from "yjs"
2
- import { JsonValue, JsonArray, JsonObject, JsonPrimitive } from "../jsonTypes"
3
- import { YjsTextModel, yjsTextModelId } from "./YjsTextModel"
4
- import { SnapshotOutOf } from "mobx-keystone"
5
-
6
- function isJsonPrimitive(v: JsonValue): v is JsonPrimitive {
7
- const t = typeof v
8
- return t === "string" || t === "number" || t === "boolean" || v === null
9
- }
10
-
11
- function isJsonArray(v: JsonValue): v is JsonArray {
12
- return Array.isArray(v)
13
- }
14
-
15
- function isJsonObject(v: JsonValue): v is JsonObject {
16
- return !isJsonArray(v) && typeof v === "object"
17
- }
18
-
19
- /**
20
- * Converts a JSON value to a Y.js data structure.
21
- * Objects are converted to Y.Maps, arrays to Y.Arrays, primitives are untouched.
22
- * Frozen values are a special case and they are kept as immutable plain values.
23
- */
24
- export function convertJsonToYjsData(v: JsonValue) {
25
- if (v === undefined || isJsonPrimitive(v)) {
26
- return v
27
- }
28
-
29
- if (isJsonArray(v)) {
30
- const arr = new Y.Array()
31
- applyJsonArrayToYArray(arr, v)
32
- return arr
33
- }
34
-
35
- if (isJsonObject(v)) {
36
- if (v.$frozen === true) {
37
- // frozen value, save as immutable object
38
- return v
39
- }
40
-
41
- if (v.$modelType === yjsTextModelId) {
42
- const text = new Y.Text()
43
- const yjsTextModel = v as SnapshotOutOf<YjsTextModel>
44
- yjsTextModel.deltaList.forEach((frozenDeltas) => {
45
- text.applyDelta(frozenDeltas.data)
46
- })
47
- return text
48
- }
49
-
50
- const map = new Y.Map()
51
- applyJsonObjectToYMap(map, v)
52
- return map
53
- }
54
-
55
- throw new Error(`unsupported value type: ${v}`)
56
- }
57
-
58
- /**
59
- * Applies a JSON array to a Y.Array, using the convertJsonToYjsData to convert the values.
60
- */
61
- export function applyJsonArrayToYArray(dest: Y.Array<unknown>, source: JsonArray) {
62
- dest.push(source.map(convertJsonToYjsData))
63
- }
64
-
65
- /**
66
- * Applies a JSON object to a Y.Map, using the convertJsonToYjsData to convert the values.
67
- */
68
- export function applyJsonObjectToYMap(dest: Y.Map<unknown>, source: JsonObject) {
69
- Object.entries(source).forEach(([k, v]) => {
70
- dest.set(k, convertJsonToYjsData(v))
71
- })
72
- }
1
+ import * as Y from "yjs"
2
+ import { YjsTextModel, yjsTextModelId } from "./YjsTextModel"
3
+ import { SnapshotOutOf } from "mobx-keystone"
4
+ import { YjsData } from "./convertYjsDataToJson"
5
+ import {
6
+ JsonArrayWithUndefined,
7
+ JsonObjectWithUndefined,
8
+ JsonPrimitiveWithUndefined,
9
+ JsonValueWithUndefined,
10
+ } from "jsonTypes"
11
+
12
+ function isJsonPrimitiveWithUndefined(v: JsonValueWithUndefined): v is JsonPrimitiveWithUndefined {
13
+ const t = typeof v
14
+ return t === "string" || t === "number" || t === "boolean" || v === null || v === undefined
15
+ }
16
+
17
+ function isJsonArrayWithUndefined(v: JsonValueWithUndefined): v is JsonArrayWithUndefined {
18
+ return Array.isArray(v)
19
+ }
20
+
21
+ function isJsonObjectWithUndefined(v: JsonValueWithUndefined): v is JsonObjectWithUndefined {
22
+ return !isJsonArrayWithUndefined(v) && typeof v === "object"
23
+ }
24
+
25
+ /**
26
+ * Converts a JSON value to a Y.js data structure.
27
+ * Objects are converted to Y.Maps, arrays to Y.Arrays, primitives are untouched.
28
+ * Frozen values are a special case and they are kept as immutable plain values.
29
+ */
30
+ export function convertJsonToYjsData(v: JsonValueWithUndefined | undefined): YjsData {
31
+ if (v === undefined || isJsonPrimitiveWithUndefined(v)) {
32
+ return v
33
+ }
34
+
35
+ if (isJsonArrayWithUndefined(v)) {
36
+ const arr = new Y.Array()
37
+ applyJsonArrayToYArray(arr, v)
38
+ return arr as YjsData
39
+ }
40
+
41
+ if (isJsonObjectWithUndefined(v)) {
42
+ if (v.$frozen === true) {
43
+ // frozen value, save as immutable object
44
+ return v
45
+ }
46
+
47
+ if (v.$modelType === yjsTextModelId) {
48
+ const text = new Y.Text()
49
+ const yjsTextModel = v as unknown as SnapshotOutOf<YjsTextModel>
50
+ yjsTextModel.deltaList.forEach((frozenDeltas) => {
51
+ text.applyDelta(frozenDeltas.data)
52
+ })
53
+ return text
54
+ }
55
+
56
+ const map = new Y.Map()
57
+ applyJsonObjectToYMap(map, v)
58
+ return map as YjsData
59
+ }
60
+
61
+ throw new Error(`unsupported value type: ${v}`)
62
+ }
63
+
64
+ /**
65
+ * Applies a JSON array to a Y.Array, using the convertJsonToYjsData to convert the values.
66
+ */
67
+ export function applyJsonArrayToYArray(dest: Y.Array<unknown>, source: JsonArrayWithUndefined) {
68
+ dest.push(source.map(convertJsonToYjsData))
69
+ }
70
+
71
+ /**
72
+ * Applies a JSON object to a Y.Map, using the convertJsonToYjsData to convert the values.
73
+ */
74
+ export function applyJsonObjectToYMap(dest: Y.Map<unknown>, source: JsonObjectWithUndefined) {
75
+ Object.entries(source).forEach(([k, v]) => {
76
+ dest.set(k, convertJsonToYjsData(v))
77
+ })
78
+ }
@@ -1,31 +1,31 @@
1
- import { modelSnapshotOutWithMetadata } from "mobx-keystone"
2
- import * as Y from "yjs"
3
- import { JsonValue } from "../jsonTypes"
4
- import { YjsTextModel } from "./YjsTextModel"
5
-
6
- export function convertYjsDataToJson(
7
- yjsData: Y.Array<unknown> | Y.Map<unknown> | Y.Text | unknown
8
- ): JsonValue {
9
- if (yjsData instanceof Y.Array) {
10
- return yjsData.map((v) => convertYjsDataToJson(v))
11
- }
12
-
13
- if (yjsData instanceof Y.Map) {
14
- const obj: Record<string, JsonValue> = {}
15
- yjsData.forEach((v, k) => {
16
- obj[k] = convertYjsDataToJson(v)
17
- })
18
- return obj
19
- }
20
-
21
- if (yjsData instanceof Y.Text) {
22
- const deltas = yjsData.toDelta()
23
-
24
- return modelSnapshotOutWithMetadata(YjsTextModel, {
25
- deltaList: deltas.length > 0 ? [{ $frozen: true, data: deltas }] : [],
26
- }) as JsonValue
27
- }
28
-
29
- // assume it's a primitive
30
- return yjsData as JsonValue
31
- }
1
+ import { modelSnapshotOutWithMetadata } from "mobx-keystone"
2
+ import * as Y from "yjs"
3
+ import { JsonObjectWithUndefined, JsonValueWithUndefined } from "../jsonTypes"
4
+ import { YjsTextModel } from "./YjsTextModel"
5
+
6
+ export type YjsData = Y.Array<YjsData> | Y.Map<YjsData> | Y.Text | JsonValueWithUndefined
7
+
8
+ export function convertYjsDataToJson(yjsData: YjsData): JsonValueWithUndefined {
9
+ if (yjsData instanceof Y.Array) {
10
+ return yjsData.map((v) => convertYjsDataToJson(v))
11
+ }
12
+
13
+ if (yjsData instanceof Y.Map) {
14
+ const obj: JsonObjectWithUndefined = {}
15
+ yjsData.forEach((v, k) => {
16
+ obj[k] = convertYjsDataToJson(v)
17
+ })
18
+ return obj
19
+ }
20
+
21
+ if (yjsData instanceof Y.Text) {
22
+ const deltas = yjsData.toDelta() as unknown[]
23
+
24
+ return modelSnapshotOutWithMetadata(YjsTextModel, {
25
+ deltaList: deltas.length > 0 ? [{ $frozen: true, data: deltas }] : [],
26
+ }) as unknown as JsonValueWithUndefined
27
+ }
28
+
29
+ // assume it's a primitive
30
+ return yjsData
31
+ }
@@ -1,92 +1,96 @@
1
- import { Patch } from "mobx-keystone"
2
- import * as Y from "yjs"
3
- import { JsonArray, JsonObject, JsonValue } from "../jsonTypes"
4
- import { failure } from "../utils/error"
5
-
6
- export function convertYjsEventToPatches(event: Y.YEvent<any>): Patch[] {
7
- const patches: Patch[] = []
8
-
9
- if (event instanceof Y.YMapEvent) {
10
- const source = event.target as Y.Map<any>
11
-
12
- event.changes.keys.forEach((change, key) => {
13
- const path = [...event.path, key]
14
-
15
- switch (change.action) {
16
- case "add":
17
- patches.push({
18
- op: "add",
19
- path,
20
- value: toPlainValue(source.get(key)),
21
- })
22
- break
23
-
24
- case "update":
25
- patches.push({
26
- op: "replace",
27
- path,
28
- value: toPlainValue(source.get(key)),
29
- })
30
- break
31
-
32
- case "delete":
33
- patches.push({
34
- op: "remove",
35
- path,
36
- })
37
- break
38
-
39
- default:
40
- throw failure(`unsupported Yjs map event action: ${change.action}`)
41
- }
42
- })
43
- } else if (event instanceof Y.YArrayEvent) {
44
- let retain = 0
45
- event.changes.delta.forEach((change) => {
46
- if (change.retain) {
47
- retain += change.retain
48
- }
49
-
50
- if (change.delete) {
51
- // remove X items at retain position
52
- const path = [...event.path, retain]
53
- for (let i = 0; i < change.delete; i++) {
54
- patches.push({
55
- op: "remove",
56
- path,
57
- })
58
- }
59
- }
60
-
61
- if (change.insert) {
62
- const newValues = Array.isArray(change.insert) ? change.insert : [change.insert]
63
- newValues.forEach((v) => {
64
- const path = [...event.path, retain]
65
- patches.push({
66
- op: "add",
67
- path,
68
- value: toPlainValue(v),
69
- })
70
- retain++
71
- })
72
- }
73
- })
74
- } else if (event instanceof Y.YTextEvent) {
75
- const path = [...event.path, "deltaList", -1 /* last item */]
76
- patches.push({
77
- op: "add",
78
- path,
79
- value: { $frozen: true, data: event.delta },
80
- })
81
- }
82
-
83
- return patches
84
- }
85
-
86
- function toPlainValue(v: Y.Map<any> | Y.Array<any> | JsonValue) {
87
- if (v instanceof Y.Map || v instanceof Y.Array) {
88
- return v.toJSON() as JsonObject | JsonArray
89
- } else {
90
- return v
91
- }
92
- }
1
+ import { Patch } from "mobx-keystone"
2
+ import * as Y from "yjs"
3
+ import {
4
+ JsonArrayWithUndefined,
5
+ JsonObjectWithUndefined,
6
+ JsonValueWithUndefined,
7
+ } from "../jsonTypes"
8
+ import { failure } from "../utils/error"
9
+
10
+ export function convertYjsEventToPatches(event: Y.YEvent<any>): Patch[] {
11
+ const patches: Patch[] = []
12
+
13
+ if (event instanceof Y.YMapEvent) {
14
+ const source = event.target
15
+
16
+ event.changes.keys.forEach((change, key) => {
17
+ const path = [...event.path, key]
18
+
19
+ switch (change.action) {
20
+ case "add":
21
+ patches.push({
22
+ op: "add",
23
+ path,
24
+ value: toPlainValue(source.get(key)),
25
+ })
26
+ break
27
+
28
+ case "update":
29
+ patches.push({
30
+ op: "replace",
31
+ path,
32
+ value: toPlainValue(source.get(key)),
33
+ })
34
+ break
35
+
36
+ case "delete":
37
+ patches.push({
38
+ op: "remove",
39
+ path,
40
+ })
41
+ break
42
+
43
+ default:
44
+ throw failure(`unsupported Yjs map event action: ${change.action}`)
45
+ }
46
+ })
47
+ } else if (event instanceof Y.YArrayEvent) {
48
+ let retain = 0
49
+ event.changes.delta.forEach((change) => {
50
+ if (change.retain) {
51
+ retain += change.retain
52
+ }
53
+
54
+ if (change.delete) {
55
+ // remove X items at retain position
56
+ const path = [...event.path, retain]
57
+ for (let i = 0; i < change.delete; i++) {
58
+ patches.push({
59
+ op: "remove",
60
+ path,
61
+ })
62
+ }
63
+ }
64
+
65
+ if (change.insert) {
66
+ const newValues = Array.isArray(change.insert) ? change.insert : [change.insert]
67
+ newValues.forEach((v) => {
68
+ const path = [...event.path, retain]
69
+ patches.push({
70
+ op: "add",
71
+ path,
72
+ value: toPlainValue(v),
73
+ })
74
+ retain++
75
+ })
76
+ }
77
+ })
78
+ } else if (event instanceof Y.YTextEvent) {
79
+ const path = [...event.path, "deltaList", -1 /* last item */]
80
+ patches.push({
81
+ op: "add",
82
+ path,
83
+ value: { $frozen: true, data: event.delta },
84
+ })
85
+ }
86
+
87
+ return patches
88
+ }
89
+
90
+ function toPlainValue(v: Y.Map<any> | Y.Array<any> | JsonValueWithUndefined) {
91
+ if (v instanceof Y.Map || v instanceof Y.Array) {
92
+ return v.toJSON() as JsonObjectWithUndefined | JsonArrayWithUndefined
93
+ } else {
94
+ return v
95
+ }
96
+ }
@@ -1,27 +1,27 @@
1
- import * as Y from "yjs"
2
- import { failure } from "../utils/error"
3
- import { getOrCreateYjsCollectionAtom } from "../utils/getOrCreateYjsCollectionAtom"
4
-
5
- export function resolveYjsPath(yjsObject: unknown, path: readonly (string | number)[]): unknown {
6
- let currentYjsObject: unknown = yjsObject
7
-
8
- path.forEach((pathPart, i) => {
9
- if (currentYjsObject instanceof Y.Map) {
10
- getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
11
- const key = String(pathPart)
12
- currentYjsObject = currentYjsObject.get(key)
13
- } else if (currentYjsObject instanceof Y.Array) {
14
- getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
15
- const key = Number(pathPart)
16
- currentYjsObject = currentYjsObject.get(key)
17
- } else {
18
- throw failure(
19
- `Y.Map or Y.Array was expected at path ${JSON.stringify(
20
- path.slice(0, i)
21
- )} in order to resolve path ${JSON.stringify(path)}, but got ${currentYjsObject} instead`
22
- )
23
- }
24
- })
25
-
26
- return currentYjsObject
27
- }
1
+ import * as Y from "yjs"
2
+ import { failure } from "../utils/error"
3
+ import { getOrCreateYjsCollectionAtom } from "../utils/getOrCreateYjsCollectionAtom"
4
+
5
+ export function resolveYjsPath(yjsObject: unknown, path: readonly (string | number)[]): unknown {
6
+ let currentYjsObject: unknown = yjsObject
7
+
8
+ path.forEach((pathPart, i) => {
9
+ if (currentYjsObject instanceof Y.Map) {
10
+ getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
11
+ const key = String(pathPart)
12
+ currentYjsObject = currentYjsObject.get(key)
13
+ } else if (currentYjsObject instanceof Y.Array) {
14
+ getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
15
+ const key = Number(pathPart)
16
+ currentYjsObject = currentYjsObject.get(key)
17
+ } else {
18
+ throw failure(
19
+ `Y.Map or Y.Array was expected at path ${JSON.stringify(
20
+ path.slice(0, i)
21
+ )} in order to resolve path ${JSON.stringify(path)}, but got ${currentYjsObject} instead`
22
+ )
23
+ }
24
+ })
25
+
26
+ return currentYjsObject
27
+ }
@@ -1,42 +1,42 @@
1
- import { AnyType, createContext } from "mobx-keystone"
2
- import * as Y from "yjs"
3
-
4
- /**
5
- * Context with info on how a mobx-keystone model is bound to a Y.js data structure.
6
- */
7
- export interface YjsBindingContext {
8
- /**
9
- * The Y.js document.
10
- */
11
- yjsDoc: Y.Doc
12
-
13
- /**
14
- * The bound Y.js data structure.
15
- */
16
- yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text
17
-
18
- /**
19
- * The mobx-keystone model type.
20
- */
21
- mobxKeystoneType: AnyType
22
-
23
- /**
24
- * The origin symbol used for transactions.
25
- */
26
- yjsOrigin: symbol
27
-
28
- /**
29
- * The bound mobx-keystone instance.
30
- */
31
- boundObject: unknown | undefined
32
-
33
- /**
34
- * Whether we are currently applying Y.js changes to the mobx-keystone model.
35
- */
36
- isApplyingYjsChangesToMobxKeystone: boolean
37
- }
38
-
39
- /**
40
- * Context with info on how a mobx-keystone model is bound to a Y.js data structure.
41
- */
42
- export const yjsBindingContext = createContext<YjsBindingContext | undefined>(undefined)
1
+ import { AnyType, createContext } from "mobx-keystone"
2
+ import * as Y from "yjs"
3
+
4
+ /**
5
+ * Context with info on how a mobx-keystone model is bound to a Y.js data structure.
6
+ */
7
+ export interface YjsBindingContext {
8
+ /**
9
+ * The Y.js document.
10
+ */
11
+ yjsDoc: Y.Doc
12
+
13
+ /**
14
+ * The bound Y.js data structure.
15
+ */
16
+ yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text
17
+
18
+ /**
19
+ * The mobx-keystone model type.
20
+ */
21
+ mobxKeystoneType: AnyType
22
+
23
+ /**
24
+ * The origin symbol used for transactions.
25
+ */
26
+ yjsOrigin: symbol
27
+
28
+ /**
29
+ * The bound mobx-keystone instance.
30
+ */
31
+ boundObject: unknown
32
+
33
+ /**
34
+ * Whether we are currently applying Y.js changes to the mobx-keystone model.
35
+ */
36
+ isApplyingYjsChangesToMobxKeystone: boolean
37
+ }
38
+
39
+ /**
40
+ * Context with info on how a mobx-keystone model is bound to a Y.js data structure.
41
+ */
42
+ export const yjsBindingContext = createContext<YjsBindingContext | undefined>(undefined)
package/src/jsonTypes.ts CHANGED
@@ -1,4 +1,9 @@
1
- export type JsonPrimitive = string | number | boolean | null
2
- export type JsonValue = JsonPrimitive | JsonObject | JsonArray
3
- export type JsonObject = { [key: string]: JsonValue }
4
- export interface JsonArray extends Array<JsonValue> {}
1
+ export type JsonPrimitiveWithUndefined = string | number | boolean | null | undefined
2
+ export type JsonValueWithUndefined =
3
+ | JsonPrimitiveWithUndefined
4
+ | JsonObjectWithUndefined
5
+ | JsonArrayWithUndefined
6
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
7
+ export type JsonObjectWithUndefined = { [key: string]: JsonValueWithUndefined }
8
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
9
+ export interface JsonArrayWithUndefined extends Array<JsonValueWithUndefined> {}
@@ -1,18 +1,27 @@
1
- import { IAtom, createAtom } from "mobx"
2
- import * as Y from "yjs"
3
-
4
- const yjsCollectionAtoms = new WeakMap<Y.Map<unknown> | Y.Array<unknown>, IAtom>()
5
-
6
- /**
7
- * @internal
8
- */
9
- export const getOrCreateYjsCollectionAtom = (
10
- yjsCollection: Y.Map<unknown> | Y.Array<unknown>
11
- ): IAtom => {
12
- let atom = yjsCollectionAtoms.get(yjsCollection)
13
- if (!atom) {
14
- atom = createAtom(`yjsCollectionAtom`)
15
- yjsCollectionAtoms.set(yjsCollection, atom)
16
- }
17
- return atom
18
- }
1
+ import { IAtom, createAtom } from "mobx"
2
+ import * as Y from "yjs"
3
+
4
+ const yjsCollectionAtoms = new WeakMap<Y.Map<unknown> | Y.Array<unknown>, IAtom>()
5
+
6
+ /**
7
+ * @internal
8
+ */
9
+ export const getYjsCollectionAtom = (
10
+ yjsCollection: Y.Map<unknown> | Y.Array<unknown>
11
+ ): IAtom | undefined => {
12
+ return yjsCollectionAtoms.get(yjsCollection)
13
+ }
14
+
15
+ /**
16
+ * @internal
17
+ */
18
+ export const getOrCreateYjsCollectionAtom = (
19
+ yjsCollection: Y.Map<unknown> | Y.Array<unknown>
20
+ ): IAtom => {
21
+ let atom = yjsCollectionAtoms.get(yjsCollection)
22
+ if (!atom) {
23
+ atom = createAtom(`yjsCollectionAtom`)
24
+ yjsCollectionAtoms.set(yjsCollection, atom)
25
+ }
26
+ return atom
27
+ }