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
|
@@ -5,6 +5,7 @@ import { modelTypeKey } from "../model/metadata"
|
|
|
5
5
|
import { getModelOrSnapshotTypeAndId, getSnapshotModelType } from "../model/utils"
|
|
6
6
|
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
7
7
|
import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
|
|
8
|
+
import { hasOwnProp } from "../utils"
|
|
8
9
|
import { getCurrentErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
9
10
|
import { type FromSnapshotContext, registerSnapshotter } from "./fromSnapshot"
|
|
10
11
|
import type { SnapshotInOfModel } from "./SnapshotOf"
|
|
@@ -14,7 +15,7 @@ import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPrio
|
|
|
14
15
|
function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotContext): AnyModel {
|
|
15
16
|
const type = sn[modelTypeKey]
|
|
16
17
|
|
|
17
|
-
if (
|
|
18
|
+
if (type === undefined) {
|
|
18
19
|
throw new SnapshotProcessingError({
|
|
19
20
|
message: `a model snapshot must contain a type key (${modelTypeKey}), but none was found`,
|
|
20
21
|
actualSnapshot: sn,
|
|
@@ -30,7 +31,10 @@ function fromModelSnapshot(sn: SnapshotInOfModel<AnyModel>, ctx: FromSnapshotCon
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
33
|
-
if (
|
|
34
|
+
if (
|
|
35
|
+
modelIdPropertyName !== undefined &&
|
|
36
|
+
(!hasOwnProp(sn, modelIdPropertyName) || sn[modelIdPropertyName] === undefined)
|
|
37
|
+
) {
|
|
34
38
|
throw new SnapshotProcessingError({
|
|
35
39
|
message: `a model snapshot of type '${type}' must contain an id key (${modelIdPropertyName}), but none was found`,
|
|
36
40
|
actualSnapshot: sn,
|
package/src/snapshot/internal.ts
CHANGED
|
@@ -31,10 +31,13 @@ interface DirtyChild {
|
|
|
31
31
|
readonly path: PathElement
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// `false` means the generic freezer must traverse this raw value; `true` means
|
|
35
|
-
//
|
|
36
|
-
// must be reflected back
|
|
37
|
-
//
|
|
34
|
+
// `false` means the generic freezer must traverse this raw value; `true` means it
|
|
35
|
+
// is frozen; a SnapshotData value means it is mutable and owned by that snapshot,
|
|
36
|
+
// which is where its frozen state must be reflected back. Both the untransformed
|
|
37
|
+
// and the transformed value of a snapshot point to their owner, so freezing can
|
|
38
|
+
// reach descendants that are only shared through processor wrappers. `undefined`
|
|
39
|
+
// is either external / untracked data or a fused frozen clone, whose owner tracks
|
|
40
|
+
// its state directly.
|
|
38
41
|
const snapshotStates = new WeakMap<object, boolean | SnapshotData>()
|
|
39
42
|
|
|
40
43
|
/**
|
|
@@ -101,7 +104,7 @@ export const setNewInternalSnapshot = action(
|
|
|
101
104
|
snapshotStates.set(untransformed, markAsFrozen ? true : sn)
|
|
102
105
|
|
|
103
106
|
if (transformed !== undefined && transformed !== untransformed) {
|
|
104
|
-
snapshotStates.set(transformed, markAsFrozen)
|
|
107
|
+
snapshotStates.set(transformed, markAsFrozen ? true : sn)
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
getOrCreateTreeNodeMetadata(value).snapshot = sn
|
|
@@ -143,7 +146,7 @@ function setSnapshotData(sn: SnapshotData, untransformed: any, freezeIfCloned =
|
|
|
143
146
|
transformed !== undefined &&
|
|
144
147
|
(!wasFrozen || transformed !== untransformed)
|
|
145
148
|
) {
|
|
146
|
-
snapshotStates.set(transformed,
|
|
149
|
+
snapshotStates.set(transformed, sn)
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
@@ -335,7 +338,7 @@ export const refreshInternalSnapshot = action("refreshInternalSnapshot", (value:
|
|
|
335
338
|
// transformed snapshots created by internal transforms must be tracked as mutable
|
|
336
339
|
// until they are exposed through getSnapshot / freezeInternalSnapshot.
|
|
337
340
|
if (snapshotStates.get(newTransformed) === undefined) {
|
|
338
|
-
snapshotStates.set(newTransformed,
|
|
341
|
+
snapshotStates.set(newTransformed, sn)
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
sn.atom?.reportChanged()
|
|
@@ -370,6 +373,16 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
370
373
|
return data
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
const owner = state === false ? undefined : state
|
|
377
|
+
|
|
378
|
+
// Processors can create untracked wrappers around tracked descendants, and the
|
|
379
|
+
// freezer stops at those (their state is `undefined`). Freeze the source of the
|
|
380
|
+
// transformed value as well, so shared descendants that the transformed
|
|
381
|
+
// traversal cannot reach still switch to copy-on-write.
|
|
382
|
+
if (owner !== undefined && owner.untransformed !== data) {
|
|
383
|
+
freezeInternalSnapshot(owner.untransformed)
|
|
384
|
+
}
|
|
385
|
+
|
|
373
386
|
if (Array.isArray(data)) {
|
|
374
387
|
for (let i = 0; i < data.length; i++) {
|
|
375
388
|
freezeInternalSnapshot(data[i])
|
|
@@ -383,8 +396,8 @@ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data:
|
|
|
383
396
|
|
|
384
397
|
snapshotStates.set(data, true)
|
|
385
398
|
|
|
386
|
-
if (
|
|
387
|
-
|
|
399
|
+
if (owner !== undefined && owner.untransformed === data) {
|
|
400
|
+
owner.untransformedFrozen = true
|
|
388
401
|
}
|
|
389
402
|
|
|
390
403
|
return data
|
|
@@ -31,9 +31,20 @@ export function onSnapshot<T extends object>(
|
|
|
31
31
|
assertTweakedObject(node, "node")
|
|
32
32
|
|
|
33
33
|
let currentSnapshot = getSnapshot(node)
|
|
34
|
+
let firstTrackingRun = true
|
|
34
35
|
|
|
35
36
|
return reaction(
|
|
36
|
-
() =>
|
|
37
|
+
() => {
|
|
38
|
+
const snapshot = getSnapshot(nodeFn())
|
|
39
|
+
// Registration inside an action defers the first tracking run until the
|
|
40
|
+
// action ends. Its snapshot, rather than the earlier registration state,
|
|
41
|
+
// is the baseline for the first notification.
|
|
42
|
+
if (firstTrackingRun) {
|
|
43
|
+
firstTrackingRun = false
|
|
44
|
+
currentSnapshot = snapshot
|
|
45
|
+
}
|
|
46
|
+
return snapshot
|
|
47
|
+
},
|
|
37
48
|
(newSnapshot) => {
|
|
38
49
|
const prevSn = currentSnapshot
|
|
39
50
|
currentSnapshot = newSnapshot
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runTypeCheckingAfterChange,
|
|
18
18
|
} from "../tweaker/typeChecking"
|
|
19
19
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
20
|
-
import { isArray, isPrimitive } from "../utils"
|
|
20
|
+
import { hasOwnProp, isArray, isPrimitive } from "../utils"
|
|
21
21
|
import { withErrorModelTrailEntry, withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
22
22
|
import type { ModelPool } from "../utils/ModelPool"
|
|
23
23
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
@@ -58,15 +58,15 @@ function reconcileModelSnapshot(
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const modelIdPropertyName = getModelIdPropertyName(modelClass)
|
|
61
|
-
const incomingModelId = modelIdPropertyName ? sn[modelIdPropertyName] : undefined
|
|
61
|
+
const incomingModelId = modelIdPropertyName !== undefined ? sn[modelIdPropertyName] : undefined
|
|
62
62
|
const positionalModelMatches =
|
|
63
63
|
isModel(value) &&
|
|
64
64
|
value[modelTypeKey] === type &&
|
|
65
|
-
(
|
|
65
|
+
(modelIdPropertyName === undefined || value[modelIdKey] === incomingModelId)
|
|
66
66
|
|
|
67
67
|
// The positional model is already the desired instance in the common
|
|
68
68
|
// stable-update case. Only moved identified models need a pool lookup.
|
|
69
|
-
if (!positionalModelMatches && modelIdPropertyName) {
|
|
69
|
+
if (!positionalModelMatches && modelIdPropertyName !== undefined) {
|
|
70
70
|
const modelInPool = modelPool.findModelByTypeAndId(type, incomingModelId)
|
|
71
71
|
if (modelInPool) {
|
|
72
72
|
value = modelInPool
|
|
@@ -92,7 +92,7 @@ function reconcileModelSnapshot(
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const modelProps = getInternalModelClassPropsInfo(modelClass)
|
|
95
|
-
if (modelIdPropertyName) {
|
|
95
|
+
if (modelIdPropertyName !== undefined) {
|
|
96
96
|
if (value[modelIdKey] !== incomingModelId) {
|
|
97
97
|
// different id, no reconciliation possible
|
|
98
98
|
return fromSnapshot<AnyModel>(sn)
|
|
@@ -134,7 +134,7 @@ function reconcileModelSnapshot(
|
|
|
134
134
|
const dataKeysLen = dataKeys.length
|
|
135
135
|
for (let i = 0; i < dataKeysLen; i++) {
|
|
136
136
|
const k = dataKeys[i]
|
|
137
|
-
if (!(k
|
|
137
|
+
if (!hasOwnProp(processedSn, k)) {
|
|
138
138
|
// use default value if applicable
|
|
139
139
|
const modelProp = modelProps[k] as AnyModelProp | undefined
|
|
140
140
|
const defaultValue = modelProp
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { isObservableObject, remove } from "mobx"
|
|
2
|
+
import { isModel } from "../model/utils"
|
|
2
3
|
import {
|
|
3
4
|
isTypeCheckingAfterChangeEnabled,
|
|
4
5
|
runTypeCheckingAfterChange,
|
|
5
6
|
} from "../tweaker/typeChecking"
|
|
6
7
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
7
|
-
import { isPlainObject } from "../utils"
|
|
8
|
+
import { hasOwnProp, isPlainObject } from "../utils"
|
|
8
9
|
import { withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
9
10
|
import type { ModelPool } from "../utils/ModelPool"
|
|
10
11
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
@@ -19,8 +20,8 @@ function reconcilePlainObjectSnapshot(
|
|
|
19
20
|
sn: SnapshotInOfObject<any>,
|
|
20
21
|
modelPool: ModelPool
|
|
21
22
|
): object {
|
|
22
|
-
// plain
|
|
23
|
-
if (!(isPlainObject(value) || isObservableObject(value))) {
|
|
23
|
+
// Observable models carry instance state and must be replaced, not edited as plain objects.
|
|
24
|
+
if (isModel(value) || !(isPlainObject(value) || isObservableObject(value))) {
|
|
24
25
|
// no reconciliation possible
|
|
25
26
|
return fromSnapshot(sn)
|
|
26
27
|
}
|
|
@@ -35,7 +36,7 @@ function reconcilePlainObjectSnapshot(
|
|
|
35
36
|
const plainObjKeysLen = plainObjKeys.length
|
|
36
37
|
for (let i = 0; i < plainObjKeysLen; i++) {
|
|
37
38
|
const k = plainObjKeys[i]
|
|
38
|
-
if (!(k
|
|
39
|
+
if (!hasOwnProp(sn, k)) {
|
|
39
40
|
remove(plainObj, k)
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -26,7 +26,7 @@ export const arrayActions = {
|
|
|
26
26
|
|
|
27
27
|
concat: standaloneAction(
|
|
28
28
|
`${namespace}::concat`,
|
|
29
|
-
<T>(array: T[], ...items: ConcatArray<T>[]): T[] => {
|
|
29
|
+
<T>(array: T[], ...items: (T | ConcatArray<T>)[]): T[] => {
|
|
30
30
|
return array.concat(...items)
|
|
31
31
|
}
|
|
32
32
|
),
|
|
@@ -89,9 +89,19 @@ export const arrayActions = {
|
|
|
89
89
|
swap: standaloneAction(
|
|
90
90
|
`${namespace}::swap`,
|
|
91
91
|
<T>(array: T[], index1: number, index2: number): boolean => {
|
|
92
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
!Number.isInteger(index1) ||
|
|
94
|
+
!Number.isInteger(index2) ||
|
|
95
|
+
index1 < 0 ||
|
|
96
|
+
index2 < 0 ||
|
|
97
|
+
index1 >= array.length ||
|
|
98
|
+
index2 >= array.length
|
|
99
|
+
) {
|
|
93
100
|
return false
|
|
94
101
|
}
|
|
102
|
+
if (index1 === index2) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
95
105
|
if (index2 < index1) {
|
|
96
106
|
;[index1, index2] = [index2, index1]
|
|
97
107
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isObservable, remove } from "mobx"
|
|
2
|
+
import { isModel } from "../model/utils"
|
|
2
3
|
import { toTreeNode } from "../tweaker/tweak"
|
|
3
4
|
import { assertIsObject, namespace as ns } from "../utils"
|
|
4
5
|
import type { AnyFunction } from "../utils/AnyFunction"
|
|
@@ -11,7 +12,7 @@ export const objectActions = {
|
|
|
11
12
|
set: standaloneAction(
|
|
12
13
|
`${namespace}::set`,
|
|
13
14
|
<T extends object, K extends keyof T>(target: T, key: K, value: T[K]): void => {
|
|
14
|
-
if (isObservable(target)) {
|
|
15
|
+
if (isObservable(target) && !isModel(target)) {
|
|
15
16
|
setIfDifferent(target, key, value)
|
|
16
17
|
} else {
|
|
17
18
|
target[key] = value
|
|
@@ -25,7 +26,7 @@ export const objectActions = {
|
|
|
25
26
|
assertIsObject(partialObject, "partialObject")
|
|
26
27
|
const keys = Object.keys(partialObject)
|
|
27
28
|
|
|
28
|
-
if (isObservable(target)) {
|
|
29
|
+
if (isObservable(target) && !isModel(target)) {
|
|
29
30
|
for (const key of keys) {
|
|
30
31
|
const newValue = (partialObject as any)[key]
|
|
31
32
|
setIfDifferent(target, key, newValue)
|
package/src/transforms/asMap.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelPropTransform } from "../modelShared/prop"
|
|
2
|
+
import { setProtoProp } from "../utils"
|
|
2
3
|
import { asMap } from "../wrappers/asMap"
|
|
3
4
|
|
|
4
5
|
const _objectToMapTransform: ModelPropTransform<Record<string, unknown>, Map<string, unknown>> = {
|
|
@@ -11,7 +12,8 @@ const _objectToMapTransform: ModelPropTransform<Record<string, unknown>, Map<str
|
|
|
11
12
|
|
|
12
13
|
const obj: Record<string, unknown> = {}
|
|
13
14
|
map.forEach((v, k) => {
|
|
14
|
-
|
|
15
|
+
if (k === "__proto__") setProtoProp(obj, v)
|
|
16
|
+
else obj[k] = v
|
|
15
17
|
})
|
|
16
18
|
|
|
17
19
|
return obj
|
|
@@ -29,21 +29,23 @@ export function deepEquals(a: any, b: any): boolean {
|
|
|
29
29
|
return true
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
a
|
|
37
|
-
} else if (isObservable(a)) {
|
|
38
|
-
a = toJS(a, toJSOptions)
|
|
39
|
-
}
|
|
40
|
-
if (isTreeNode(b)) {
|
|
41
|
-
b = getSnapshot(b)
|
|
42
|
-
} else if (isObservable(b)) {
|
|
43
|
-
b = toJS(b, toJSOptions)
|
|
32
|
+
const aIsObject = a !== null && typeof a === "object"
|
|
33
|
+
const bIsObject = b !== null && typeof b === "object"
|
|
34
|
+
if (!aIsObject && !bIsObject) {
|
|
35
|
+
// Strict equality already handled all equal primitives except NaN.
|
|
36
|
+
return Number.isNaN(a) && Number.isNaN(b)
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
return fastDeepEqual(a, b)
|
|
39
|
+
return fastDeepEqual(aIsObject ? comparisonValue(a) : a, bIsObject ? comparisonValue(b) : b)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function comparisonValue(value: object): unknown {
|
|
43
|
+
if (isTreeNode(value)) {
|
|
44
|
+
// Snapshots preserve structural sharing. Model $ objects are deliberately
|
|
45
|
+
// handled as observable objects rather than standalone tree nodes.
|
|
46
|
+
return getSnapshot(value)
|
|
47
|
+
}
|
|
48
|
+
return isObservable(value) ? toJS(value, toJSOptions) : value
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const toJSOptions =
|
package/src/treeUtils/draft.ts
CHANGED
|
@@ -38,6 +38,10 @@ export class Draft<T extends object> {
|
|
|
38
38
|
*/
|
|
39
39
|
@mobxAction
|
|
40
40
|
commitByPath(path: Path): void {
|
|
41
|
+
if (path.length === 0) {
|
|
42
|
+
this.commit()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
41
45
|
const draftTarget = resolvePath(this.data, path)
|
|
42
46
|
if (!draftTarget.resolved) {
|
|
43
47
|
throw failure(`path ${JSON.stringify(path)} could not be resolved in draft object`)
|
|
@@ -76,6 +80,10 @@ export class Draft<T extends object> {
|
|
|
76
80
|
*/
|
|
77
81
|
@mobxAction
|
|
78
82
|
resetByPath(path: Path): void {
|
|
83
|
+
if (path.length === 0) {
|
|
84
|
+
this.reset()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
79
87
|
const originalTarget = resolvePath(this.originalData, path)
|
|
80
88
|
if (!originalTarget.resolved) {
|
|
81
89
|
throw failure(`path ${JSON.stringify(path)} could not be resolved in original object`)
|
package/src/treeUtils/sandbox.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { createContext } from "../context"
|
|
|
7
7
|
import { getParentToChildPath, resolvePath } from "../parent/path"
|
|
8
8
|
import { applyPatches } from "../patch/applyPatches"
|
|
9
9
|
import { onPatches } from "../patch/emitPatch"
|
|
10
|
-
import type { Patch } from "../patch/Patch"
|
|
11
10
|
import { type PatchRecorder, patchRecorder } from "../patch/patchRecorder"
|
|
12
11
|
import {
|
|
13
12
|
fastIsRootStoreNoAtom,
|
|
@@ -78,11 +77,6 @@ export class SandboxManager {
|
|
|
78
77
|
*/
|
|
79
78
|
private allowWrite: ReadonlyMiddlewareReturn["allowWrite"]
|
|
80
79
|
|
|
81
|
-
/**
|
|
82
|
-
* Whether changes made in the sandbox are currently being committed to the original subtree.
|
|
83
|
-
*/
|
|
84
|
-
private isCommitting = false
|
|
85
|
-
|
|
86
80
|
private readonly subtreeRoot: object
|
|
87
81
|
|
|
88
82
|
/**
|
|
@@ -127,11 +121,9 @@ export class SandboxManager {
|
|
|
127
121
|
if (this.withSandboxPatchRecorder) {
|
|
128
122
|
throw failure("original subtree must not change while 'withSandbox' executes")
|
|
129
123
|
}
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
}
|
|
124
|
+
this.allowWrite(() => {
|
|
125
|
+
applyPatches(this.subtreeRootClone, patches)
|
|
126
|
+
})
|
|
135
127
|
})
|
|
136
128
|
|
|
137
129
|
const { allowWrite, dispose: disposeReadonlyMW } = readonlyMiddleware(this.subtreeRootClone)
|
|
@@ -232,26 +224,38 @@ export class SandboxManager {
|
|
|
232
224
|
}
|
|
233
225
|
if (commit) {
|
|
234
226
|
if (!isNestedWithSandboxCall) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
227
|
+
const events = recorder.events
|
|
228
|
+
// The clone already holds these changes, so revert them and let the regular
|
|
229
|
+
// patch sync mirror the original's final state back in. That way anything a
|
|
230
|
+
// listener changes on the original during the commit reaches the clone too,
|
|
231
|
+
// at a cost proportional to the changes rather than to the tree size.
|
|
232
|
+
// Both halves run in a single action so that observers of the sandbox never
|
|
233
|
+
// see the reverted intermediate state.
|
|
234
|
+
runInAction(() => {
|
|
235
|
+
this.allowWrite(() => {
|
|
236
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
237
|
+
applyPatches(this.subtreeRootClone, events[i].inversePatches, true)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
applyPatches(
|
|
241
|
+
this.subtreeRoot,
|
|
242
|
+
events.map((event) => event.patches)
|
|
243
|
+
)
|
|
244
|
+
})
|
|
248
245
|
}
|
|
249
246
|
} else {
|
|
250
247
|
this.allowWrite(() => {
|
|
251
248
|
runInAction(() => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
249
|
+
const wasRecording = recorder.recording
|
|
250
|
+
recorder.recording = false
|
|
251
|
+
try {
|
|
252
|
+
let i = recorder.events.length
|
|
253
|
+
while (i-- > numRecorderEvents) {
|
|
254
|
+
applyPatches(this.subtreeRootClone, recorder.events[i].inversePatches, true)
|
|
255
|
+
}
|
|
256
|
+
recorder.events.splice(numRecorderEvents)
|
|
257
|
+
} finally {
|
|
258
|
+
recorder.recording = wasRecording
|
|
255
259
|
}
|
|
256
260
|
})
|
|
257
261
|
})
|
package/src/tweaker/core.ts
CHANGED
|
@@ -28,10 +28,8 @@ export function setTweakedObjectUntweakers(
|
|
|
28
28
|
* @internal
|
|
29
29
|
*/
|
|
30
30
|
export function isTweakedObject(value: unknown, canBeDataObject: boolean): value is object {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
return treeNodeMetadata.get(value as object)?.tweaked === true
|
|
31
|
+
const metadata = treeNodeMetadata.get(value as object)
|
|
32
|
+
return metadata?.tweaked === true && (canBeDataObject || metadata.dataObjectParent === undefined)
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
/**
|
package/src/tweaker/tweak.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { unsetInternalSnapshot } from "../snapshot/internal"
|
|
|
8
8
|
import type { AnyStandardType, TypeToData } from "../types/schemas"
|
|
9
9
|
import { typeCheck } from "../types/typeCheck"
|
|
10
10
|
import { failure, inDevMode, isMap, isObject, isPrimitive, isSet } from "../utils"
|
|
11
|
+
import { getSafeErrorValuePreview } from "../utils/errorDiagnostics"
|
|
11
12
|
import { isTweakedObject, runTweakedObjectUntweakers } from "./core"
|
|
12
13
|
import { registerDefaultTweakers } from "./registerDefaultTweakers"
|
|
13
14
|
import { treeNodeMetadata } from "./treeNodeMetadata"
|
|
@@ -126,7 +127,7 @@ function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
throw failure(
|
|
129
|
-
`tweak can only work over models, observable objects/arrays, or primitives, but got ${value} instead`
|
|
130
|
+
`tweak can only work over models, observable objects/arrays, or primitives, but got ${getSafeErrorValuePreview(value)} instead`
|
|
130
131
|
)
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -13,7 +13,13 @@ import { emitArraySpliceDeepChange, emitArrayUpdateDeepChange } from "../deepCha
|
|
|
13
13
|
import { getGlobalConfig } from "../globalConfig"
|
|
14
14
|
import type { ParentPath } from "../parent/path"
|
|
15
15
|
import { reindexArrayChildren, setParent } from "../parent/setParent"
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
beginPatchEmission,
|
|
18
|
+
emitPatches,
|
|
19
|
+
endPatchEmission,
|
|
20
|
+
hasPatchListenersFor,
|
|
21
|
+
InternalPatchRecorder,
|
|
22
|
+
} from "../patch/emitPatch"
|
|
17
23
|
import type { Patch } from "../patch/Patch"
|
|
18
24
|
import {
|
|
19
25
|
flushInternalSnapshot,
|
|
@@ -23,11 +29,20 @@ import {
|
|
|
23
29
|
updateInternalSnapshot,
|
|
24
30
|
} from "../snapshot/internal"
|
|
25
31
|
import { failure, inDevMode, isArray, isPrimitive } from "../utils"
|
|
32
|
+
import {
|
|
33
|
+
addDelayedError,
|
|
34
|
+
type DelayedError,
|
|
35
|
+
throwDelayedError,
|
|
36
|
+
} from "../utils/forEachWithDelayedThrow"
|
|
26
37
|
import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
|
|
27
38
|
import { TweakerPriority } from "./TweakerPriority"
|
|
28
39
|
import { markAsTweakedObject } from "./treeNodeMetadata"
|
|
29
40
|
import { registerTweaker, tweak } from "./tweak"
|
|
30
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
isTypeCheckingAfterChangeEnabled,
|
|
43
|
+
recordTypeCheckingBatchChange,
|
|
44
|
+
runTypeCheckingAfterChange,
|
|
45
|
+
} from "./typeChecking"
|
|
31
46
|
|
|
32
47
|
/**
|
|
33
48
|
* @internal
|
|
@@ -138,8 +153,23 @@ function mutateSet(k: number, v: unknown, sn: unknown[]) {
|
|
|
138
153
|
sn[k] = v
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
// Conservative bound below every engine's argument-count limit for spread calls.
|
|
157
|
+
const maxSpreadableItems = 8192
|
|
158
|
+
|
|
141
159
|
function mutateSplice(index: number, removedCount: number, addedItems: any[], sn: any[]) {
|
|
142
|
-
|
|
160
|
+
if (addedItems.length < maxSpreadableItems) {
|
|
161
|
+
sn.splice(index, removedCount, ...addedItems)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Splice the tail in a single pass instead of spreading the added items as
|
|
166
|
+
// arguments, which would overflow the engine's limit. copyWithin handles overlap.
|
|
167
|
+
const oldLength = sn.length
|
|
168
|
+
const newLength = oldLength - removedCount + addedItems.length
|
|
169
|
+
if (newLength > oldLength) sn.length = newLength
|
|
170
|
+
sn.copyWithin(index + addedItems.length, index + removedCount, oldLength)
|
|
171
|
+
for (let i = 0; i < addedItems.length; i++) sn[index + i] = addedItems[i]
|
|
172
|
+
sn.length = newLength
|
|
143
173
|
}
|
|
144
174
|
|
|
145
175
|
const patchRecorder = new InternalPatchRecorder()
|
|
@@ -149,24 +179,12 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
149
179
|
|
|
150
180
|
patchRecorder.reset()
|
|
151
181
|
|
|
152
|
-
switch (change.type) {
|
|
153
|
-
case "splice":
|
|
154
|
-
emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
|
|
155
|
-
break
|
|
156
|
-
|
|
157
|
-
case "update":
|
|
158
|
-
emitArrayUpdateDeepChange(arr, arr, change.index as number, change.newValue, change.oldValue)
|
|
159
|
-
|
|
160
|
-
break
|
|
161
|
-
|
|
162
|
-
default:
|
|
163
|
-
break
|
|
164
|
-
}
|
|
165
|
-
|
|
166
182
|
if (runningWithoutSnapshotOrPatches) {
|
|
167
183
|
return
|
|
168
184
|
}
|
|
169
185
|
|
|
186
|
+
recordTypeCheckingBatchChange(change)
|
|
187
|
+
|
|
170
188
|
const oldSnapshot = getInternalSnapshot(arr as Array<unknown>)!.untransformed
|
|
171
189
|
const shouldEmitPatches = hasPatchListenersFor(arr)
|
|
172
190
|
const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
|
|
@@ -196,8 +214,53 @@ function arrayDidChange(change: IArrayDidChange) {
|
|
|
196
214
|
runTypeCheckingAfterChange(arr, patchRecorder)
|
|
197
215
|
if (mutate) {
|
|
198
216
|
updateInternalSnapshot(arr, mutate)
|
|
199
|
-
patchRecorder.emit(arr)
|
|
200
217
|
}
|
|
218
|
+
// Listeners may mutate another node and reuse the shared recorder.
|
|
219
|
+
// Keep this change's patches before publishing the validated deep change.
|
|
220
|
+
const { patches, invPatches } = patchRecorder
|
|
221
|
+
patchRecorder.reset()
|
|
222
|
+
|
|
223
|
+
// Reserve chronological patch notifications, then finish deep-change delivery
|
|
224
|
+
// before publishing them. Reentrant edits append behind this mutation.
|
|
225
|
+
// Every step below runs even when an earlier one throws, so a failing patch recorder
|
|
226
|
+
// cannot keep the mutation from reaching the deep change and public patch listeners,
|
|
227
|
+
// and no failure is lost to a later one.
|
|
228
|
+
beginPatchEmission()
|
|
229
|
+
let delayedError: DelayedError
|
|
230
|
+
try {
|
|
231
|
+
emitPatches(arr, patches, invPatches)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
delayedError = addDelayedError(delayedError, error)
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
switch (change.type) {
|
|
237
|
+
case "splice":
|
|
238
|
+
emitArraySpliceDeepChange(arr, arr, change.index as number, change.added, change.removed)
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
case "update":
|
|
242
|
+
emitArrayUpdateDeepChange(
|
|
243
|
+
arr,
|
|
244
|
+
arr,
|
|
245
|
+
change.index as number,
|
|
246
|
+
change.newValue,
|
|
247
|
+
change.oldValue
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
default:
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
delayedError = addDelayedError(delayedError, error)
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
endPatchEmission()
|
|
260
|
+
} catch (error) {
|
|
261
|
+
delayedError = addDelayedError(delayedError, error)
|
|
262
|
+
}
|
|
263
|
+
throwDelayedError(delayedError)
|
|
201
264
|
}
|
|
202
265
|
|
|
203
266
|
const undefinedInsideArrayErrorMsg =
|
|
@@ -317,10 +380,10 @@ function arrayDidChangeSplice(
|
|
|
317
380
|
}
|
|
318
381
|
|
|
319
382
|
if (patches && readdPatches && readdPatches.length > 0) {
|
|
320
|
-
patches.push(
|
|
383
|
+
for (const patch of readdPatches) patches.push(patch)
|
|
321
384
|
}
|
|
322
385
|
if (invPatches && readdInvPatches && readdInvPatches.length > 0) {
|
|
323
|
-
invPatches.push(
|
|
386
|
+
for (const patch of readdInvPatches) invPatches.push(patch)
|
|
324
387
|
}
|
|
325
388
|
// We need to reverse once since inverse patches are applied in reverse.
|
|
326
389
|
// Repeated unshift calls here would make a large splice quadratic.
|