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.
- package/CHANGELOG.md +123 -0
- package/dist/mobx-keystone.esm.js +1478 -952
- package/dist/mobx-keystone.esm.mjs +1478 -952
- package/dist/mobx-keystone.umd.js +1483 -952
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/patch/applyPatches.d.ts +2 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
- package/dist/types/utils/index.d.ts +13 -0
- package/dist/types/utils/mapUtils.d.ts +2 -2
- package/dist/types/utils/setLike.d.ts +1 -0
- package/package.json +1 -1
- package/src/action/applyAction.ts +1 -1
- package/src/action/middleware.ts +17 -36
- package/src/action/modelFlow.ts +4 -5
- package/src/action/mutationBatch.ts +10 -10
- package/src/action/pendingActions.ts +3 -4
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
- package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
- package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
- package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
- package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
- package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
- package/src/actionMiddlewares/undoMiddleware.ts +65 -46
- package/src/actionMiddlewares/utils.ts +2 -1
- package/src/computedTree/computedTree.ts +14 -3
- package/src/context/context.ts +17 -45
- package/src/dataModel/BaseDataModel.ts +20 -16
- package/src/dataModel/dataModelInstanceCache.ts +8 -0
- package/src/deepChange/onDeepChange.ts +179 -24
- package/src/frozen/Frozen.ts +50 -33
- package/src/model/BaseModel.ts +4 -9
- package/src/model/newModel.ts +21 -24
- package/src/model/utils.ts +2 -2
- package/src/modelShared/modelClassInitializer.ts +3 -2
- package/src/modelShared/modelDecorator.ts +34 -14
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +44 -28
- package/src/parent/path.ts +5 -3
- package/src/parent/setParent.ts +7 -0
- package/src/parent/walkTree.ts +21 -37
- package/src/patch/applyPatches.ts +69 -34
- package/src/patch/emitPatch.ts +142 -26
- package/src/patch/jsonPatch.ts +5 -0
- package/src/patch/patchRecorder.ts +18 -14
- package/src/redux/connectReduxDevTools.ts +24 -18
- package/src/redux/redux.ts +5 -21
- package/src/ref/core.ts +18 -2
- package/src/rootStore/attachDetach.ts +27 -16
- package/src/rootStore/rootStore.ts +10 -6
- package/src/snapshot/SnapshotOf.ts +2 -2
- package/src/snapshot/applySnapshot.ts +1 -1
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/internal.ts +22 -9
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +6 -6
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/standardActions/arrayActions.ts +12 -2
- package/src/standardActions/objectActions.ts +3 -2
- package/src/transforms/ImmutableDate.ts +4 -0
- package/src/transforms/asMap.ts +3 -1
- package/src/treeUtils/deepEquals.ts +15 -13
- package/src/treeUtils/draft.ts +8 -0
- package/src/treeUtils/sandbox.ts +31 -27
- package/src/tweaker/core.ts +2 -4
- package/src/tweaker/tweak.ts +2 -1
- package/src/tweaker/tweakArray.ts +83 -20
- package/src/tweaker/tweakPlainObject.ts +76 -23
- package/src/tweaker/typeChecking.ts +124 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/createCachedTypeCheck.ts +27 -2
- package/src/types/objectBased/typesDataModelData.ts +4 -2
- package/src/types/objectBased/typesModel.ts +4 -2
- package/src/types/objectBased/typesObject.ts +20 -16
- package/src/types/objectBased/typesRecord.ts +6 -13
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesEnum.ts +4 -2
- package/src/types/primitiveBased/typesPrimitive.ts +9 -4
- package/src/types/typeCheckErrorUtils.ts +1 -0
- package/src/types/utility/typesCodecSupport.ts +97 -56
- package/src/types/utility/typesMap.ts +24 -41
- package/src/types/utility/typesRefinement.ts +3 -2
- package/src/types/utility/typesSet.ts +24 -0
- package/src/utils/ModelPool.ts +3 -3
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/decorators.ts +1 -1
- package/src/utils/errorDiagnostics.ts +6 -2
- package/src/utils/forEachWithDelayedThrow.ts +83 -0
- package/src/utils/index.ts +34 -2
- package/src/utils/mapUtils.ts +3 -7
- package/src/utils/setIfDifferent.ts +3 -2
- package/src/utils/setLike.ts +134 -0
- package/src/wrappers/ArraySet.ts +5 -9
- package/src/wrappers/ObjectMap.ts +15 -11
- package/src/wrappers/asMap.ts +30 -18
- package/src/wrappers/asSet.ts +36 -12
- package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
- package/src/action/modelFlowPromiseGenerator.ts +0 -127
package/src/modelShared/prop.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/parent/detach.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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 (
|
|
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
|
}
|
package/src/parent/path.ts
CHANGED
|
@@ -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)) {
|
package/src/parent/setParent.ts
CHANGED
|
@@ -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)
|
package/src/parent/walkTree.ts
CHANGED
|
@@ -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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
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
|
|
140
|
-
const childrenIter = childrenMap.values()
|
|
141
|
-
let ch = childrenIter.next()
|
|
139
|
+
const children = getObjectChildren(target)
|
|
142
140
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
const childMap = recurse(
|
|
151
|
-
|
|
146
|
+
for (const child of children) {
|
|
147
|
+
const childMap = recurse(child)
|
|
152
148
|
if (childMap) {
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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)
|