mobx-keystone 1.24.1 → 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 +137 -0
- package/README.md +5 -1
- package/dist/mobx-keystone.esm.js +1608 -1072
- package/dist/mobx-keystone.esm.mjs +1608 -1072
- package/dist/mobx-keystone.umd.js +1613 -1072
- 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 +7 -7
- 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 +76 -60
- 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 +15 -14
- package/src/model/newModel.ts +32 -28
- 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/findParent.ts +14 -9
- package/src/parent/onChildAttachedTo.ts +45 -40
- package/src/parent/path.ts +11 -7
- package/src/parent/setParent.ts +33 -14
- package/src/parent/walkTree.ts +21 -37
- package/src/patch/applyPatches.ts +75 -60
- 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 +10 -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/fromSnapshot.ts +3 -1
- package/src/snapshot/internal.ts +22 -9
- package/src/snapshot/modelInitialData.ts +14 -19
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +16 -8
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/snapshot/reconcileSnapshot.ts +13 -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 +8 -8
- package/src/tweaker/tweakArray.ts +86 -37
- package/src/tweaker/tweakFrozen.ts +0 -1
- package/src/tweaker/tweakModel.ts +0 -1
- package/src/tweaker/tweakPlainObject.ts +78 -29
- package/src/tweaker/typeChecking.ts +132 -33
- 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/ref/core.ts
CHANGED
|
@@ -88,10 +88,26 @@ export function internalCustomRef<T extends object>(
|
|
|
88
88
|
trackedRefs.add(this as Ref<object>)
|
|
89
89
|
this.internalForceUpdateBackRefs(initialTarget)
|
|
90
90
|
|
|
91
|
+
// MobX 4/5 do not pass the previous value to reaction effects.
|
|
92
|
+
// The reaction is usually created inside an action, so its first tracking run is deferred
|
|
93
|
+
// to the end of the batch and `initialTarget` may be stale by then; capture the value of
|
|
94
|
+
// that first run instead, since that is the one the effect will be compared against.
|
|
95
|
+
let previousTarget = initialTarget
|
|
96
|
+
let firstTrackingRun = true
|
|
91
97
|
// according to mwestrate this won't leak as long as we don't keep the disposer around
|
|
92
98
|
reaction(
|
|
93
|
-
() =>
|
|
94
|
-
|
|
99
|
+
() => {
|
|
100
|
+
const target = this.maybeCurrent
|
|
101
|
+
if (firstTrackingRun) {
|
|
102
|
+
firstTrackingRun = false
|
|
103
|
+
previousTarget = target
|
|
104
|
+
this.internalForceUpdateBackRefs(target)
|
|
105
|
+
}
|
|
106
|
+
return target
|
|
107
|
+
},
|
|
108
|
+
(newTarget) => {
|
|
109
|
+
const oldTarget = previousTarget
|
|
110
|
+
previousTarget = newTarget
|
|
95
111
|
this.internalForceUpdateBackRefs(newTarget)
|
|
96
112
|
|
|
97
113
|
if (onResolvedValueChange && newTarget !== oldTarget) {
|
|
@@ -4,9 +4,10 @@ import { HookAction } from "../action/hookActions"
|
|
|
4
4
|
import { wrapInAction, wrapModelMethodInActionIfNeeded } from "../action/wrapInAction"
|
|
5
5
|
import { type AnyModel, BaseModel } from "../model/BaseModel"
|
|
6
6
|
import { WalkTreeMode, walkTree } from "../parent/walkTree"
|
|
7
|
+
import { forEachWithDelayedThrow } from "../utils/forEachWithDelayedThrow"
|
|
7
8
|
|
|
8
9
|
const onAttachedDisposers = new WeakMap<object, () => void>()
|
|
9
|
-
const attachedToRootStore = new
|
|
10
|
+
const attachedToRootStore = new WeakMap<object, object>()
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @internal
|
|
@@ -16,6 +17,7 @@ export const attachToRootStore = action(
|
|
|
16
17
|
(rootStore: object, child: object): void => {
|
|
17
18
|
// we use an array to ensure they will get called even if the actual hook modifies the tree
|
|
18
19
|
const childrenToCall: AnyModel[] = []
|
|
20
|
+
const attachment = {}
|
|
19
21
|
|
|
20
22
|
walkTree(
|
|
21
23
|
child,
|
|
@@ -25,7 +27,7 @@ export const attachToRootStore = action(
|
|
|
25
27
|
if (attachedToRootStore.has(ch)) {
|
|
26
28
|
return
|
|
27
29
|
}
|
|
28
|
-
attachedToRootStore.
|
|
30
|
+
attachedToRootStore.set(ch, attachment)
|
|
29
31
|
|
|
30
32
|
if (ch instanceof BaseModel && (ch as any).onAttachedToRootStore) {
|
|
31
33
|
wrapModelMethodInActionIfNeeded(
|
|
@@ -40,15 +42,23 @@ export const attachToRootStore = action(
|
|
|
40
42
|
WalkTreeMode.ParentFirst
|
|
41
43
|
)
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
forEachWithDelayedThrow(childrenToCall, (ch) => {
|
|
46
|
+
// An earlier hook may have detached or reattached this node already.
|
|
47
|
+
if (attachedToRootStore.get(ch) !== attachment) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
const disposer = (ch as any).onAttachedToRootStore(rootStore)
|
|
48
52
|
if (disposer) {
|
|
49
|
-
|
|
53
|
+
if (attachedToRootStore.get(ch) === attachment) {
|
|
54
|
+
onAttachedDisposers.set(ch, disposer)
|
|
55
|
+
} else {
|
|
56
|
+
// The hook detached or reattached this node before returning its cleanup.
|
|
57
|
+
// Dispose this attachment without overwriting a newer attachment's cleanup.
|
|
58
|
+
wrapDisposer(ch, disposer)()
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
|
-
}
|
|
61
|
+
})
|
|
52
62
|
}
|
|
53
63
|
)
|
|
54
64
|
|
|
@@ -69,11 +79,7 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
|
|
|
69
79
|
const disposer = onAttachedDisposers.get(ch)
|
|
70
80
|
if (disposer) {
|
|
71
81
|
// wrap disposer in action
|
|
72
|
-
const disposerAction =
|
|
73
|
-
nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
|
|
74
|
-
fn: disposer,
|
|
75
|
-
actionType: ActionContextActionType.Sync,
|
|
76
|
-
}).bind(ch)
|
|
82
|
+
const disposerAction = wrapDisposer(ch, disposer)
|
|
77
83
|
onAttachedDisposers.delete(ch)
|
|
78
84
|
|
|
79
85
|
disposersToCall.push(disposerAction)
|
|
@@ -82,8 +88,13 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
|
|
|
82
88
|
WalkTreeMode.ChildrenFirst
|
|
83
89
|
)
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
for (let i = 0; i < disposersToCallLen; i++) {
|
|
87
|
-
disposersToCall[i]()
|
|
88
|
-
}
|
|
91
|
+
forEachWithDelayedThrow(disposersToCall, (dispose) => dispose())
|
|
89
92
|
})
|
|
93
|
+
|
|
94
|
+
function wrapDisposer(node: object, disposer: () => void): () => void {
|
|
95
|
+
return wrapInAction({
|
|
96
|
+
nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
|
|
97
|
+
fn: disposer,
|
|
98
|
+
actionType: ActionContextActionType.Sync,
|
|
99
|
+
}).bind(node)
|
|
100
|
+
}
|
|
@@ -45,9 +45,11 @@ export const registerRootStore: <T extends object>(node: T) => T = action(
|
|
|
45
45
|
|
|
46
46
|
entry.is = true
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
try {
|
|
49
|
+
attachToRootStore(node, node)
|
|
50
|
+
} finally {
|
|
51
|
+
entry.atom?.reportChanged()
|
|
52
|
+
}
|
|
51
53
|
return node
|
|
52
54
|
}
|
|
53
55
|
)
|
|
@@ -65,9 +67,11 @@ export const unregisterRootStore: (node: object) => void = action("unregisterRoo
|
|
|
65
67
|
const entry = getOrCreateRootStoreEntry(node)
|
|
66
68
|
entry.is = false
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
try {
|
|
71
|
+
detachFromRootStore(node)
|
|
72
|
+
} finally {
|
|
73
|
+
entry.atom?.reportChanged()
|
|
74
|
+
}
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
/**
|
|
@@ -69,13 +69,13 @@ export type SnapshotInOfModel<M extends AnyModel> = ModelFromSnapshot<M>
|
|
|
69
69
|
export interface SnapshotInOfFrozen<F extends Frozen<any>> extends FrozenData<F["data"]> {}
|
|
70
70
|
|
|
71
71
|
export interface SnapshotInOfObjectMap<V> {
|
|
72
|
-
items?: { [k: string]:
|
|
72
|
+
items?: { [k: string]: SnapshotInOf<V> }
|
|
73
73
|
[modelTypeKey]?: string
|
|
74
74
|
[modelIdKey]: string
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface SnapshotInOfArraySet<V> {
|
|
78
|
-
items?:
|
|
78
|
+
items?: SnapshotInOf<V>[]
|
|
79
79
|
[modelTypeKey]?: string
|
|
80
80
|
[modelIdKey]: string
|
|
81
81
|
}
|
|
@@ -17,6 +17,7 @@ import { assertTweakedObject } from "../tweaker/core"
|
|
|
17
17
|
import { assertIsObject, inDevMode, isArray, isMap, isPlainObject, isSet, lazy } from "../utils"
|
|
18
18
|
import { runWithErrorDiagnosticsContext } from "../utils/errorDiagnostics"
|
|
19
19
|
import { ModelPool } from "../utils/ModelPool"
|
|
20
|
+
import { flushInternalSnapshot, getInternalSnapshot } from "./internal"
|
|
20
21
|
import { reconcileSnapshot } from "./reconcileSnapshot"
|
|
21
22
|
import type { SnapshotInOf, SnapshotOutOf } from "./SnapshotOf"
|
|
22
23
|
import { SnapshotProcessingError } from "./SnapshotProcessingError"
|
|
@@ -52,6 +53,14 @@ export function internalApplySnapshot<T extends object>(
|
|
|
52
53
|
const obj = this
|
|
53
54
|
|
|
54
55
|
const reconcile = () => {
|
|
56
|
+
// Avoid building a model pool (and its deep children traversal) when the
|
|
57
|
+
// node already holds this exact snapshot. Child updates may still await
|
|
58
|
+
// propagation within an action, so flush before trusting the reference.
|
|
59
|
+
flushInternalSnapshot(obj, false)
|
|
60
|
+
if (getInternalSnapshot(obj)?.transformed === sn) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
const modelPool = new ModelPool(obj)
|
|
56
65
|
const ret = reconcileSnapshot(obj, sn, modelPool, undefined)
|
|
57
66
|
|
|
@@ -120,7 +129,7 @@ export function internalApplySnapshot<T extends object>(
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
123
|
-
if (modelIdPropertyName) {
|
|
132
|
+
if (modelIdPropertyName !== undefined) {
|
|
124
133
|
const id = (sn as any)[modelIdPropertyName]
|
|
125
134
|
if (obj[modelIdKey] !== id) {
|
|
126
135
|
// different id, no reconciliation possible
|
package/src/snapshot/clone.ts
CHANGED
|
@@ -24,8 +24,7 @@ export function clone<T extends object>(node: T, options?: Partial<CloneOptions>
|
|
|
24
24
|
assertTweakedObject(node, "node")
|
|
25
25
|
|
|
26
26
|
const opts = {
|
|
27
|
-
generateNewIds: true,
|
|
28
|
-
...options,
|
|
27
|
+
generateNewIds: options?.generateNewIds ?? true,
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
const sn = getSnapshot(node)
|
|
@@ -10,8 +10,7 @@ import { fromSnapshot } from "./fromSnapshot"
|
|
|
10
10
|
*/
|
|
11
11
|
export function cloneTreeValue<T>(value: T, options?: Partial<CloneOptions>): T {
|
|
12
12
|
const cloneOptions: CloneOptions = {
|
|
13
|
-
generateNewIds: true,
|
|
14
|
-
...options,
|
|
13
|
+
generateNewIds: options?.generateNewIds ?? true,
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
if (isPrimitive(value)) {
|
|
@@ -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,
|
|
@@ -211,7 +211,9 @@ function snapshotToInitialData(
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
const observableInitialData = observable.object(initialData, undefined, observableOptions)
|
|
214
|
-
|
|
214
|
+
if (allValuesPrimitive) {
|
|
215
|
+
setModelInitialDataSnapshot(observableInitialData, initialData)
|
|
216
|
+
}
|
|
215
217
|
return observableInitialData
|
|
216
218
|
}
|
|
217
219
|
|
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
|
|
@@ -1,40 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
readonly snapshot: Record<string, unknown>
|
|
3
|
-
readonly allValuesPrimitive: boolean
|
|
4
|
-
reusable: boolean
|
|
5
|
-
}
|
|
1
|
+
import { isPrimitive, setProtoProp } from "../utils"
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
// Only primitive-only initial data can double as a snapshot without tweaking.
|
|
4
|
+
const modelInitialDataSnapshots = new WeakMap<object, Record<string, unknown>>()
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* @internal
|
|
11
8
|
*/
|
|
12
|
-
export function setModelInitialDataSnapshot(
|
|
13
|
-
data
|
|
14
|
-
snapshot: Record<string, unknown>,
|
|
15
|
-
allValuesPrimitive: boolean
|
|
16
|
-
): void {
|
|
17
|
-
modelInitialDataSnapshots.set(data, {
|
|
18
|
-
snapshot,
|
|
19
|
-
allValuesPrimitive,
|
|
20
|
-
reusable: true,
|
|
21
|
-
})
|
|
9
|
+
export function setModelInitialDataSnapshot(data: object, snapshot: Record<string, unknown>): void {
|
|
10
|
+
modelInitialDataSnapshots.set(data, snapshot)
|
|
22
11
|
}
|
|
23
12
|
|
|
24
13
|
/**
|
|
25
14
|
* @internal
|
|
26
15
|
*/
|
|
27
|
-
export function
|
|
16
|
+
export function updateModelInitialDataSnapshot(data: object, key: string, value: unknown): void {
|
|
28
17
|
const initialDataSnapshot = modelInitialDataSnapshots.get(data)
|
|
29
18
|
if (initialDataSnapshot) {
|
|
30
|
-
|
|
19
|
+
if (!isPrimitive(value)) {
|
|
20
|
+
modelInitialDataSnapshots.delete(data)
|
|
21
|
+
} else if (key === "__proto__") {
|
|
22
|
+
setProtoProp(initialDataSnapshot, value)
|
|
23
|
+
} else {
|
|
24
|
+
initialDataSnapshot[key] = value
|
|
25
|
+
}
|
|
31
26
|
}
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
/**
|
|
35
30
|
* @internal
|
|
36
31
|
*/
|
|
37
|
-
export function takeModelInitialDataSnapshot(data: object):
|
|
32
|
+
export function takeModelInitialDataSnapshot(data: object): Record<string, unknown> | undefined {
|
|
38
33
|
const initialDataSnapshot = modelInitialDataSnapshots.get(data)
|
|
39
34
|
modelInitialDataSnapshots.delete(data)
|
|
40
35
|
return initialDataSnapshot
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remove } from "mobx"
|
|
1
|
+
import { remove, set } from "mobx"
|
|
2
2
|
import type { AnyModel } from "../model/BaseModel"
|
|
3
3
|
import { getModelIdPropertyName } from "../model/getModelMetadata"
|
|
4
4
|
import { isReservedModelKey, modelIdKey, modelTypeKey } from "../model/metadata"
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runTypeCheckingAfterChange,
|
|
18
18
|
} from "../tweaker/typeChecking"
|
|
19
19
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
20
|
-
import { isArray } 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"
|
|
@@ -51,22 +51,22 @@ function reconcileModelSnapshot(
|
|
|
51
51
|
// the canonical output snapshot. A successful canonical no-op therefore
|
|
52
52
|
// cannot produce a diagnostic, so keep this hottest path outside the
|
|
53
53
|
// model-trail bookkeeping and defer id metadata until needed. The
|
|
54
|
-
// positional value was already flushed by the
|
|
54
|
+
// positional value was already flushed by the internal snapshot read in
|
|
55
55
|
// reconcileSnapshot, so its transformed snapshot is current.
|
|
56
56
|
if (isCanonicalSnapshotNoOp(value, type, sn)) {
|
|
57
57
|
return value
|
|
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
|
|
@@ -157,6 +157,14 @@ function reconcileModelSnapshot(
|
|
|
157
157
|
const v = processedSn[k]
|
|
158
158
|
|
|
159
159
|
const oldValue = data[k]
|
|
160
|
+
// Non-null primitives need neither reconciliation nor detachment.
|
|
161
|
+
// Nullish values must still pass through default handling below.
|
|
162
|
+
if (v != null && isPrimitive(v)) {
|
|
163
|
+
if (v !== oldValue) {
|
|
164
|
+
set(data, k, v)
|
|
165
|
+
}
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
160
168
|
let newValue = withErrorPathSegment(k, () =>
|
|
161
169
|
reconcileSnapshot(oldValue, v, modelPool, modelObj)
|
|
162
170
|
)
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@ import { fastGetParentPathIncludingDataObjects } from "../parent"
|
|
|
5
5
|
import { isMap, isPrimitive, isSet } from "../utils"
|
|
6
6
|
import type { ModelPool } from "../utils/ModelPool"
|
|
7
7
|
import { getSnapshot } from "./getSnapshot"
|
|
8
|
+
import { flushInternalSnapshot, getInternalSnapshot } from "./internal"
|
|
8
9
|
import { registerDefaultReconcilers } from "./registerDefaultReconcilers"
|
|
9
10
|
import { SnapshotProcessingError } from "./SnapshotProcessingError"
|
|
10
11
|
|
|
@@ -28,10 +29,18 @@ export function reconcileSnapshot(value: any, sn: any, modelPool: ModelPool, par
|
|
|
28
29
|
return sn
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// Unlike getSnapshot, this comparison never exposes the snapshot, so it needs
|
|
33
|
+
// neither freezing nor observation registration.
|
|
34
|
+
const currentSnapshot = getInternalSnapshot(value)
|
|
35
|
+
if (currentSnapshot) {
|
|
36
|
+
flushInternalSnapshot(value, false)
|
|
37
|
+
if (currentSnapshot.transformed === sn) {
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
} else if (!isPrimitive(value)) {
|
|
41
|
+
// A non-primitive without an internal snapshot is not a valid reconciliation
|
|
42
|
+
// target (e.g. a non-node object); let getSnapshot raise the proper error.
|
|
43
|
+
getSnapshot(value)
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
registerDefaultReconcilers()
|
|
@@ -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
|