mobx-keystone 1.25.0 → 2.0.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/mobx-keystone.esm.js +1478 -952
  3. package/dist/mobx-keystone.esm.mjs +1478 -952
  4. package/dist/mobx-keystone.umd.js +1483 -952
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  7. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  8. package/dist/types/parent/detach.d.ts +1 -1
  9. package/dist/types/parent/findChildren.d.ts +1 -1
  10. package/dist/types/patch/applyPatches.d.ts +2 -0
  11. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  12. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  13. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  14. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  15. package/dist/types/types/TypeCheckError.d.ts +1 -0
  16. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  17. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  18. package/dist/types/utils/index.d.ts +13 -0
  19. package/dist/types/utils/mapUtils.d.ts +2 -2
  20. package/dist/types/utils/setLike.d.ts +1 -0
  21. package/package.json +1 -1
  22. package/src/action/applyAction.ts +1 -1
  23. package/src/action/middleware.ts +17 -36
  24. package/src/action/modelFlow.ts +4 -5
  25. package/src/action/mutationBatch.ts +10 -10
  26. package/src/action/pendingActions.ts +3 -4
  27. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  28. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  29. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  30. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  31. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  32. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  33. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  34. package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
  35. package/src/actionMiddlewares/undoMiddleware.ts +65 -46
  36. package/src/actionMiddlewares/utils.ts +2 -1
  37. package/src/computedTree/computedTree.ts +14 -3
  38. package/src/context/context.ts +17 -45
  39. package/src/dataModel/BaseDataModel.ts +20 -16
  40. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  41. package/src/deepChange/onDeepChange.ts +179 -24
  42. package/src/frozen/Frozen.ts +50 -33
  43. package/src/model/BaseModel.ts +4 -9
  44. package/src/model/newModel.ts +21 -24
  45. package/src/model/utils.ts +2 -2
  46. package/src/modelShared/modelClassInitializer.ts +3 -2
  47. package/src/modelShared/modelDecorator.ts +34 -14
  48. package/src/modelShared/modelInfo.ts +1 -1
  49. package/src/modelShared/prop.ts +6 -9
  50. package/src/modelShared/sharedInternalModel.ts +16 -3
  51. package/src/parent/detach.ts +1 -1
  52. package/src/parent/findChildren.ts +4 -7
  53. package/src/parent/onChildAttachedTo.ts +44 -28
  54. package/src/parent/path.ts +5 -3
  55. package/src/parent/setParent.ts +7 -0
  56. package/src/parent/walkTree.ts +21 -37
  57. package/src/patch/applyPatches.ts +69 -34
  58. package/src/patch/emitPatch.ts +142 -26
  59. package/src/patch/jsonPatch.ts +5 -0
  60. package/src/patch/patchRecorder.ts +18 -14
  61. package/src/redux/connectReduxDevTools.ts +24 -18
  62. package/src/redux/redux.ts +5 -21
  63. package/src/ref/core.ts +18 -2
  64. package/src/rootStore/attachDetach.ts +27 -16
  65. package/src/rootStore/rootStore.ts +10 -6
  66. package/src/snapshot/SnapshotOf.ts +2 -2
  67. package/src/snapshot/applySnapshot.ts +1 -1
  68. package/src/snapshot/clone.ts +1 -2
  69. package/src/snapshot/cloneTreeValue.ts +1 -2
  70. package/src/snapshot/fromModelSnapshot.ts +6 -2
  71. package/src/snapshot/internal.ts +22 -9
  72. package/src/snapshot/onSnapshot.ts +12 -1
  73. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  74. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  75. package/src/standardActions/arrayActions.ts +12 -2
  76. package/src/standardActions/objectActions.ts +3 -2
  77. package/src/transforms/ImmutableDate.ts +4 -0
  78. package/src/transforms/asMap.ts +3 -1
  79. package/src/treeUtils/deepEquals.ts +15 -13
  80. package/src/treeUtils/draft.ts +8 -0
  81. package/src/treeUtils/sandbox.ts +31 -27
  82. package/src/tweaker/core.ts +2 -4
  83. package/src/tweaker/tweak.ts +2 -1
  84. package/src/tweaker/tweakArray.ts +83 -20
  85. package/src/tweaker/tweakPlainObject.ts +76 -23
  86. package/src/tweaker/typeChecking.ts +124 -15
  87. package/src/types/TypeCheckError.ts +17 -19
  88. package/src/types/createCachedTypeCheck.ts +27 -2
  89. package/src/types/objectBased/typesDataModelData.ts +4 -2
  90. package/src/types/objectBased/typesModel.ts +4 -2
  91. package/src/types/objectBased/typesObject.ts +20 -16
  92. package/src/types/objectBased/typesRecord.ts +6 -13
  93. package/src/types/objectBased/typesRef.ts +2 -2
  94. package/src/types/primitiveBased/typesEnum.ts +4 -2
  95. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  96. package/src/types/typeCheckErrorUtils.ts +1 -0
  97. package/src/types/utility/typesCodecSupport.ts +97 -56
  98. package/src/types/utility/typesMap.ts +24 -41
  99. package/src/types/utility/typesRefinement.ts +3 -2
  100. package/src/types/utility/typesSet.ts +24 -0
  101. package/src/utils/ModelPool.ts +3 -3
  102. package/src/utils/chainFns.ts +4 -0
  103. package/src/utils/decorators.ts +1 -1
  104. package/src/utils/errorDiagnostics.ts +6 -2
  105. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  106. package/src/utils/index.ts +34 -2
  107. package/src/utils/mapUtils.ts +3 -7
  108. package/src/utils/setIfDifferent.ts +3 -2
  109. package/src/utils/setLike.ts +134 -0
  110. package/src/wrappers/ArraySet.ts +5 -9
  111. package/src/wrappers/ObjectMap.ts +15 -11
  112. package/src/wrappers/asMap.ts +30 -18
  113. package/src/wrappers/asSet.ts +36 -12
  114. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  115. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -15,7 +15,7 @@ export interface ModelInfo {
15
15
  */
16
16
  export const modelInfoByName: {
17
17
  [name: string]: ModelInfo
18
- } = {}
18
+ } = Object.create(null)
19
19
 
20
20
  /**
21
21
  * @internal
@@ -1,6 +1,6 @@
1
1
  import type { SnapshotInOf, SnapshotOutOf } from "../snapshot/SnapshotOf"
2
2
  import type { AnyStandardType } from "../types/schemas"
3
- import { lazy } from "../utils"
3
+ import { isEqualOrBothNaN, lazy } from "../utils"
4
4
  import { runWithErrorDiagnosticsContext, withErrorPathSegment } from "../utils/errorDiagnostics"
5
5
  import { getOrCreate } from "../utils/mapUtils"
6
6
  import type { Flatten, IsNeverType, IsOptionalValue } from "../utils/types"
@@ -618,11 +618,6 @@ function parseSetterConfig(
618
618
 
619
619
  const propCache = new Map<unknown, AnyModelProp>()
620
620
 
621
- let cacheTransformResult = false
622
- const cacheTransformedValueFn = () => {
623
- cacheTransformResult = true
624
- }
625
-
626
621
  /** @internal */
627
622
  export function toFullModelPropTransform(
628
623
  transformObject: ModelPropTransform<unknown, unknown>
@@ -651,7 +646,7 @@ export function toFullModelPropTransform(
651
646
  const modelCache = getOrCreate(cache, model, () => new Map())
652
647
 
653
648
  let propCache = modelCache.get(propName)
654
- if (propCache?.originalValue !== originalValue) {
649
+ if (!propCache || !isEqualOrBothNaN(propCache.originalValue, originalValue)) {
655
650
  // original changed, invalidate cache
656
651
  modelCache.delete(propName)
657
652
  propCache = undefined
@@ -674,10 +669,12 @@ export function toFullModelPropTransform(
674
669
  untransform(transformedValue: unknown, model: object, propName: PropertyKey) {
675
670
  const modelCache = getOrCreate(cache, model, () => new Map())
676
671
 
677
- cacheTransformResult = false
672
+ let cacheTransformResult = false
678
673
  const originalValue = untransform({
679
674
  transformedValue,
680
- cacheTransformedValue: cacheTransformedValueFn,
675
+ cacheTransformedValue: () => {
676
+ cacheTransformResult = true
677
+ },
681
678
  })
682
679
  if (cacheTransformResult) {
683
680
  modelCache.set(propName, { originalValue, transformedValue })
@@ -19,7 +19,14 @@ import type { AnyType } from "../types/schemas"
19
19
  import { tProp } from "../types/tProp"
20
20
  import { resolveStoredType } from "../types/utility/typesCodec"
21
21
  import { typesUnchecked } from "../types/utility/typesUnchecked"
22
- import { addHiddenProp, assertIsObject, failure, propNameToSetterName } from "../utils"
22
+ import {
23
+ addHiddenProp,
24
+ assertIsObject,
25
+ failure,
26
+ hasOwnProp,
27
+ propNameToSetterName,
28
+ setProtoProp,
29
+ } from "../utils"
23
30
  import { chainFns } from "../utils/chainFns"
24
31
  import { withErrorPathSegment } from "../utils/errorDiagnostics"
25
32
  import { type ModelClass, modelInitializedSymbol } from "./BaseModelShared"
@@ -275,7 +282,9 @@ export function sharedInternalModel<
275
282
  dataTypeChecker = typesObject(() => {
276
283
  const typeCheckerObj: Record<string, AnyType> = {}
277
284
  for (const [k, mp] of Object.entries(composedModelProps)) {
278
- typeCheckerObj[k] = mp._typeChecker ? resolveStoredType(mp._typeChecker) : typesUnchecked()
285
+ const typeChecker = mp._typeChecker ? resolveStoredType(mp._typeChecker) : typesUnchecked()
286
+ if (k === "__proto__") setProtoProp(typeCheckerObj, typeChecker)
287
+ else typeCheckerObj[k] = typeChecker
279
288
  }
280
289
  return typeCheckerObj
281
290
  })
@@ -451,7 +460,11 @@ function getModelPropsSnapshotProcessor(
451
460
  return (sn) => {
452
461
  const newSn = { ...sn }
453
462
  for (const [propName, processor] of propsWithProcessor) {
454
- newSn[propName] = withErrorPathSegment(propName, () => processor(sn[propName]))
463
+ const value = withErrorPathSegment(propName, () =>
464
+ processor(hasOwnProp(sn, propName) ? sn[propName] : undefined)
465
+ )
466
+ if (propName === "__proto__") setProtoProp(newSn, value)
467
+ else newSn[propName] = value
455
468
  }
456
469
  return newSn
457
470
  }
@@ -10,7 +10,7 @@ import { fastGetParentPathIncludingDataObjects } from "./path"
10
10
  * Detaches a given object from a tree.
11
11
  * If the parent is an object / model, detaching will delete the property.
12
12
  * If the parent is an array detaching will remove the node by splicing it.
13
- * If there's no parent it will throw.
13
+ * If there's no parent it does nothing.
14
14
  *
15
15
  * @param node Object to be detached.
16
16
  */
@@ -8,7 +8,7 @@ import { getChildrenObjects } from "./getChildrenObjects"
8
8
  * @param predicate Function that will be run for every child of the root object.
9
9
  * @param [options] An optional object with the `deep` option (defaults to `false`) set to `true` to
10
10
  * get the children deeply or `false` to get them shallowly.
11
- * @returns A readonly observable set with the matching children.
11
+ * @returns A readonly set with the matching children.
12
12
  */
13
13
  export function findChildren<T extends object = any>(
14
14
  root: object,
@@ -21,13 +21,10 @@ export function findChildren<T extends object = any>(
21
21
 
22
22
  const set = new Set<any>()
23
23
 
24
- const iter = children.values()
25
- let cur = iter.next()
26
- while (!cur.done) {
27
- if (predicate(cur.value)) {
28
- set.add(cur.value)
24
+ for (const child of children) {
25
+ if (predicate(child)) {
26
+ set.add(child)
29
27
  }
30
- cur = iter.next()
31
28
  }
32
29
 
33
30
  return set
@@ -32,11 +32,10 @@ export function onChildAttachedTo(
32
32
  assertIsFunction(target, "target")
33
33
  assertIsFunction(fn, "fn")
34
34
 
35
- const opts = {
36
- deep: false,
37
- fireForCurrentChildren: true,
38
- ...options,
39
- }
35
+ const { deep = false, fireForCurrentChildren = true } = options ?? {}
36
+
37
+ let disposed = false
38
+ let runCleanupAfterDispose = false
40
39
 
41
40
  const detachDisposers = new WeakMap<object, () => void>()
42
41
 
@@ -48,13 +47,30 @@ export function onChildAttachedTo(
48
47
  }
49
48
  }
50
49
 
50
+ const runDetachDisposers = (nodes: object[]) => {
51
+ let firstError: { value: unknown } | undefined
52
+ for (let i = nodes.length - 1; i >= 0; i--) {
53
+ try {
54
+ runDetachDisposer(nodes[i])
55
+ } catch (value) {
56
+ firstError ??= { value }
57
+ }
58
+ }
59
+ return firstError
60
+ }
61
+
51
62
  const addDetachDisposer = (n: object, disposer: (() => void) | void) => {
52
63
  if (disposer) {
53
- detachDisposers.set(n, action(disposer))
64
+ const detach = action(disposer)
65
+ if (disposed) {
66
+ if (runCleanupAfterDispose) detach()
67
+ } else {
68
+ detachDisposers.set(n, detach)
69
+ }
54
70
  }
55
71
  }
56
72
 
57
- const getChildrenObjectOpts = { deep: opts.deep }
73
+ const getChildrenObjectOpts = { deep }
58
74
  const getCurrentChildren = () => {
59
75
  const t = target()
60
76
  assertTweakedObject(t, "target()")
@@ -62,7 +78,7 @@ export function onChildAttachedTo(
62
78
  return new Set(getChildrenObjects(t, getChildrenObjectOpts))
63
79
  }
64
80
 
65
- const currentChildren = opts.fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
81
+ const currentChildren = fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
66
82
 
67
83
  const disposer = reaction(
68
84
  () => getCurrentChildren(),
@@ -84,45 +100,45 @@ export function onChildAttachedTo(
84
100
  currentChildrenCur = currentChildrenIter.next()
85
101
  }
86
102
 
87
- if (disposersToRun.length > 0) {
88
- for (let i = disposersToRun.length - 1; i >= 0; i--) {
89
- runDetachDisposer(disposersToRun[i])
90
- }
91
- }
103
+ let firstError = runDetachDisposers(disposersToRun)
92
104
 
93
105
  // find new
94
106
  const newChildrenIter = newChildren.values()
95
107
  let newChildrenCur = newChildrenIter.next()
96
- while (!newChildrenCur.done) {
108
+ while (!disposed && !newChildrenCur.done) {
97
109
  const n = newChildrenCur.value
98
110
  if (!currentChildren.has(n)) {
99
111
  currentChildren.add(n)
100
112
 
101
- const detachAction = runInAction(() => fn(n))
102
- addDetachDisposer(n, detachAction)
113
+ try {
114
+ const detachAction = runInAction(() => fn(n))
115
+ addDetachDisposer(n, detachAction)
116
+ } catch (value) {
117
+ firstError ??= { value }
118
+ }
103
119
  }
104
120
 
105
121
  newChildrenCur = newChildrenIter.next()
106
122
  }
123
+ if (firstError) {
124
+ throw firstError.value
125
+ }
107
126
  },
108
127
  {
109
128
  fireImmediately: true,
110
129
  }
111
130
  )
112
131
 
113
- return (runDetachDisposers: boolean) => {
132
+ return (runPendingDetachDisposers: boolean) => {
133
+ if (disposed) return
134
+ disposed = true
135
+ runCleanupAfterDispose = runPendingDetachDisposers
114
136
  disposer()
115
-
116
- if (runDetachDisposers) {
117
- const currentChildrenIter = currentChildren.values()
118
- let currentChildrenCur = currentChildrenIter.next()
119
- while (!currentChildrenCur.done) {
120
- const n = currentChildrenCur.value
121
- runDetachDisposer(n)
122
-
123
- currentChildrenCur = currentChildrenIter.next()
124
- }
125
- }
137
+ const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren) : []
126
138
  currentChildren.clear()
139
+ const firstError = runDetachDisposers(pendingChildren)
140
+ if (firstError) {
141
+ throw firstError.value
142
+ }
127
143
  }
128
144
  }
@@ -267,11 +267,13 @@ export function resolvePath<T = any>(
267
267
  const dataNode = modelToDataNode(current)
268
268
  if (p in dataNode) {
269
269
  current = dataNode
270
- } else if (!(p in current)) {
271
- return unresolved
272
270
  }
273
271
  }
274
272
 
273
+ if (!(p in current)) {
274
+ return unresolved
275
+ }
276
+
275
277
  current = current[p]
276
278
  }
277
279
 
@@ -323,7 +325,7 @@ export function resolvePathCheckingIds<T = any>(
323
325
 
324
326
  // check just to avoid mobx warnings about trying to access out of bounds index
325
327
  if (isArray(current)) {
326
- if (+p >= current.length) {
328
+ if (+p >= current.length || !(p in current)) {
327
329
  return { resolved: false }
328
330
  }
329
331
  } else if (!hasOwnProp(current, p)) {
@@ -6,6 +6,7 @@ import { isModel } from "../model/utils"
6
6
  import { attachToRootStore, detachFromRootStore } from "../rootStore/attachDetach"
7
7
  import { fastIsRootStoreNoAtom } from "../rootStore/rootStore"
8
8
  import { clone } from "../snapshot/clone"
9
+ import { flushInternalSnapshot } from "../snapshot/internal"
9
10
  import { isTweakedObject } from "../tweaker/core"
10
11
  import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
11
12
  import { tryUntweak } from "../tweaker/tweak"
@@ -86,6 +87,12 @@ export const setParent = action(
86
87
  return value
87
88
  }
88
89
 
90
+ // The new parent captures this node's snapshot during attachment. Flush
91
+ // descendant edits made while detached before that snapshot is copied.
92
+ if (parentPath && !oldParentPath) {
93
+ flushInternalSnapshot(value, false)
94
+ }
95
+
89
96
  // make sure the new parent actually points to models when we give model data objs
90
97
  if (parentPath) {
91
98
  const actualParent = dataToModelNode(parentPath.parent)
@@ -75,19 +75,19 @@ function walkTreeChildrenFirst<T = void>(
75
75
  root: object,
76
76
  visit: (node: object) => T | undefined
77
77
  ): T | undefined {
78
- const childrenIter = getObjectChildren(root).values()
79
- let ch = childrenIter.next()
80
- while (!ch.done) {
81
- const ret = walkTreeChildrenFirst(ch.value, visit)
82
- if (ret !== undefined) {
83
- return ret
78
+ const stack = [{ node: root, children: getObjectChildren(root).values() }]
79
+ while (stack.length > 0) {
80
+ const frame = stack[stack.length - 1]
81
+ const child = frame.children.next()
82
+ if (!child.done) {
83
+ stack.push({ node: child.value, children: getObjectChildren(child.value).values() })
84
+ } else {
85
+ stack.pop()
86
+ const ret = visit(frame.node)
87
+ if (ret !== undefined) {
88
+ return ret
89
+ }
84
90
  }
85
- ch = childrenIter.next()
86
- }
87
-
88
- const ret = visit(root)
89
- if (ret !== undefined) {
90
- return ret
91
91
  }
92
92
 
93
93
  return undefined
@@ -136,37 +136,21 @@ function walkTreeAggregate<R>(
136
136
  let map: Map<R, object> | undefined
137
137
  const rootVal = visit(target)
138
138
 
139
- const childrenMap = getObjectChildren(target)
140
- const childrenIter = childrenMap.values()
141
- let ch = childrenIter.next()
139
+ const children = getObjectChildren(target)
142
140
 
143
- // small optimization, if there is only one child and this
144
- // object provides no value we can just reuse the child ones
145
- if (rootVal === undefined && childrenMap.size === 1) {
146
- return recurse(ch.value!)
141
+ // With one child and no local value, the child's map can be reused.
142
+ if (rootVal === undefined && children.size === 1) {
143
+ return recurse(children.values().next().value!)
147
144
  }
148
145
 
149
- while (!ch.done) {
150
- const childMap = recurse(ch.value)
151
-
146
+ for (const child of children) {
147
+ const childMap = recurse(child)
152
148
  if (childMap) {
153
- if (!map) {
154
- map = new Map()
155
- }
156
-
157
- // add child map keys/values to own map
158
- const mapIter = childMap.keys()
159
-
160
- let mapCur = mapIter.next()
161
- while (!mapCur.done) {
162
- const key = mapCur.value
163
- const val = childMap.get(key)!
164
- map.set(key, val)
165
- mapCur = mapIter.next()
149
+ map ??= new Map()
150
+ for (const [key, value] of childMap) {
151
+ map.set(key, value)
166
152
  }
167
153
  }
168
-
169
- ch = childrenIter.next()
170
154
  }
171
155
 
172
156
  // add it at the end so parent resolutions have higher
@@ -4,8 +4,10 @@ import { ActionContextActionType } from "../action/context"
4
4
  import { wrapInAction } from "../action/wrapInAction"
5
5
  import { modelToDataNode } from "../parent/core"
6
6
  import type { Patch } from "../patch/Patch"
7
+ import { internalApplySnapshot } from "../snapshot/applySnapshot"
7
8
  import { reconcileSnapshot } from "../snapshot/reconcileSnapshot"
8
9
  import { assertTweakedObject } from "../tweaker/core"
10
+ import { withTypeCheckingBatch } from "../tweaker/typeChecking"
9
11
  import { failure, inDevMode, isArray, lazy } from "../utils"
10
12
  import { runWithErrorDiagnosticsContext, withErrorPathSegments } from "../utils/errorDiagnostics"
11
13
  import { ModelPool } from "../utils/ModelPool"
@@ -13,6 +15,8 @@ import { setIfDifferent } from "../utils/setIfDifferent"
13
15
 
14
16
  /**
15
17
  * Applies the given patches to the given target object.
18
+ * Automatic type checking validates the completed batch, including nested lists.
19
+ * A rejected batch is rolled back with compensating patches.
16
20
  *
17
21
  * @param node Target object.
18
22
  * @param patches List of patches to apply.
@@ -44,33 +48,35 @@ export function internalApplyPatches(
44
48
  const obj = this
45
49
  const modelPool = new ModelPool(obj)
46
50
 
47
- if (reverse) {
48
- let i = patches.length
49
- while (i--) {
50
- const p = patches[i]
51
- if (isArray(p)) {
52
- let j = p.length
53
- while (j--) {
54
- applySinglePatchWithPath(obj, p[j], modelPool)
51
+ withTypeCheckingBatch(() => {
52
+ if (reverse) {
53
+ let i = patches.length
54
+ while (i--) {
55
+ const p = patches[i]
56
+ if (isArray(p)) {
57
+ let j = p.length
58
+ while (j--) {
59
+ applySinglePatchWithPath(obj, p[j], modelPool)
60
+ }
61
+ } else {
62
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
55
63
  }
56
- } else {
57
- applySinglePatchWithPath(obj, p as Patch, modelPool)
58
64
  }
59
- }
60
- } else {
61
- const len = patches.length
62
- for (let i = 0; i < len; i++) {
63
- const p = patches[i]
64
- if (isArray(p)) {
65
- const len2 = p.length
66
- for (let j = 0; j < len2; j++) {
67
- applySinglePatchWithPath(obj, p[j], modelPool)
65
+ } else {
66
+ const len = patches.length
67
+ for (let i = 0; i < len; i++) {
68
+ const p = patches[i]
69
+ if (isArray(p)) {
70
+ const len2 = p.length
71
+ for (let j = 0; j < len2; j++) {
72
+ applySinglePatchWithPath(obj, p[j], modelPool)
73
+ }
74
+ } else {
75
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
68
76
  }
69
- } else {
70
- applySinglePatchWithPath(obj, p as Patch, modelPool)
71
77
  }
72
78
  }
73
- }
79
+ })
74
80
  })
75
81
  }
76
82
 
@@ -88,20 +94,33 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
88
94
  throw failure(`invalid path: ${path}`)
89
95
  }
90
96
 
97
+ if (path.length === 0) {
98
+ if (patch.op === "add" || patch.op === "replace") {
99
+ internalApplySnapshot.call(obj, patch.value)
100
+ return
101
+ }
102
+ throw failure("a patch cannot remove the root node")
103
+ }
104
+
91
105
  let target: any = modelToDataNode(obj)
92
106
  for (let i = 0; i < path.length - 1; i++) {
93
107
  target = modelToDataNode(target[path[i]])
94
108
  }
95
- const prop = path.length > 0 ? path[path.length - 1] : undefined
109
+ const prop = path[path.length - 1]
96
110
 
97
111
  if (isArray(target)) {
112
+ if (patch.op === "replace" && prop === "length") {
113
+ target.length = patch.value
114
+ return
115
+ }
116
+
117
+ const index = getPatchArrayIndex(prop, target.length, patch.op)
98
118
  switch (patch.op) {
99
119
  case "add": {
100
- const index = +prop!
101
120
  // reconcile from the pool if possible
102
121
  const newValue = reconcileSnapshot(undefined, patch.value, modelPool, target)
103
122
  if (index < 0) {
104
- // extension needed by mobx-keystone-yjs
123
+ // non-standard extension: a negative index appends, just like "-"
105
124
  target.push(newValue)
106
125
  } else {
107
126
  target.splice(index, 0, newValue)
@@ -110,21 +129,15 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
110
129
  }
111
130
 
112
131
  case "remove": {
113
- const index = +prop!
114
132
  // no reconciliation, removing
115
133
  target.splice(index, 1)
116
134
  break
117
135
  }
118
136
 
119
137
  case "replace": {
120
- if (prop === "length") {
121
- target.length = patch.value
122
- } else {
123
- const index = +prop!
124
- // try to reconcile
125
- const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target)
126
- setIfDifferent(target, index as any, newValue)
127
- }
138
+ // try to reconcile
139
+ const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target)
140
+ setIfDifferent(target, index as any, newValue)
128
141
  break
129
142
  }
130
143
 
@@ -160,6 +173,28 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
160
173
  }
161
174
  }
162
175
 
176
+ // canonical decimal integers only, so "01", "1.0" or "1e0" are not mistaken for indexes
177
+ const patchArrayIndexPattern = /^(?:0|-?[1-9]\d*)$/
178
+
179
+ function getPatchArrayIndex(prop: string | number, length: number, op: Patch["op"]): number {
180
+ // JSON Patch spells "at the end of the array" as "-", which only makes sense when adding
181
+ if (prop === "-" && op === "add") {
182
+ return length
183
+ }
184
+
185
+ const index =
186
+ typeof prop === "string" && !patchArrayIndexPattern.test(prop) ? Number.NaN : Number(prop)
187
+
188
+ // additions may also target the end of the array, plus negative indexes as an append extension
189
+ const valid =
190
+ Number.isInteger(index) && (op === "add" ? index <= length : index >= 0 && index < length)
191
+ if (!valid) {
192
+ throw failure(`invalid array index '${String(prop)}' for '${op}' patch`)
193
+ }
194
+
195
+ return index
196
+ }
197
+
163
198
  function applySinglePatchWithPath(obj: object, patch: Patch, modelPool: ModelPool): void {
164
199
  withErrorPathSegments(patch.path, () => {
165
200
  applySinglePatch(obj, patch, modelPool)