mobx-keystone 1.22.0 → 1.23.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 +7 -0
- package/dist/mobx-keystone.esm.js +6702 -5797
- package/dist/mobx-keystone.esm.mjs +6702 -5797
- package/dist/mobx-keystone.umd.js +6718 -5798
- package/dist/types/action/runUnprotected.d.ts +8 -4
- package/dist/types/model/Model.d.ts +5 -0
- package/dist/types/modelShared/prop.d.ts +8 -4
- package/dist/types/snapshot/modelInitialData.d.ts +1 -0
- package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
- package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
- package/package.json +11 -10
- package/src/action/middleware.ts +5 -18
- package/src/action/mutationBatch.ts +72 -0
- package/src/action/runUnprotected.ts +21 -8
- package/src/action/wrapInAction.ts +45 -15
- package/src/dataModel/BaseDataModel.ts +4 -8
- package/src/deepChange/onDeepChange.ts +246 -16
- package/src/model/BaseModel.ts +4 -12
- package/src/model/Model.ts +5 -0
- package/src/model/newModel.ts +68 -36
- package/src/modelShared/prop.ts +105 -63
- package/src/modelShared/sharedInternalModel.ts +91 -60
- package/src/parent/core.ts +16 -10
- package/src/parent/coreObjectChildren.ts +219 -21
- package/src/parent/path.ts +6 -5
- package/src/parent/setParent.ts +5 -5
- package/src/patch/emitPatch.ts +102 -20
- package/src/snapshot/fromArraySnapshot.ts +11 -2
- package/src/snapshot/fromModelSnapshot.ts +10 -3
- package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
- package/src/snapshot/fromSnapshot.ts +31 -9
- package/src/snapshot/getSnapshot.ts +8 -3
- package/src/snapshot/internal.ts +243 -82
- package/src/snapshot/modelInitialData.ts +41 -0
- package/src/snapshot/reconcileArraySnapshot.ts +9 -3
- package/src/snapshot/reconcileModelSnapshot.ts +97 -17
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
- package/src/tweaker/core.ts +30 -5
- package/src/tweaker/treeNodeMetadata.ts +38 -0
- package/src/tweaker/tweak.ts +15 -4
- package/src/tweaker/tweakArray.ts +159 -121
- package/src/tweaker/tweakFrozen.ts +2 -2
- package/src/tweaker/tweakModel.ts +2 -2
- package/src/tweaker/tweakPlainObject.ts +114 -122
- package/src/tweaker/typeChecking.ts +10 -1
- package/src/types/TypeChecker.ts +261 -15
- package/src/types/arrayBased/typesArray.ts +38 -40
- package/src/types/arrayBased/typesTuple.ts +51 -34
- package/src/types/createCachedTypeCheck.ts +181 -0
- package/src/types/objectBased/typesArraySet.ts +42 -32
- package/src/types/objectBased/typesDataModelData.ts +18 -11
- package/src/types/objectBased/typesModel.ts +1 -1
- package/src/types/objectBased/typesObject.ts +57 -28
- package/src/types/objectBased/typesObjectMap.ts +51 -40
- package/src/types/objectBased/typesRecord.ts +59 -39
- package/src/types/objectBased/typesRef.ts +1 -1
- package/src/types/primitiveBased/typesPrimitive.ts +9 -9
- package/src/types/tProp.ts +80 -44
- package/src/types/utility/typesOr.ts +35 -24
- package/src/types/utility/typesRefinement.ts +21 -8
- package/src/types/utility/typesSkipCheck.ts +21 -8
- package/src/types/utility/typesTag.ts +21 -9
- package/src/types/utility/typesUnchecked.ts +2 -3
- package/src/utils/ModelPool.ts +34 -19
- package/src/utils/errorDiagnostics.ts +60 -18
- package/src/utils/index.ts +9 -7
- package/src/types/createPerEntryCachedCheck.ts +0 -166
- /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
|
@@ -2,7 +2,7 @@ import { remove } 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"
|
|
5
|
-
import {
|
|
5
|
+
import { getSnapshotModelType, isModel } from "../model/utils"
|
|
6
6
|
import type { ModelClass } from "../modelShared/BaseModelShared"
|
|
7
7
|
import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
|
|
8
8
|
import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
|
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
noDefaultValue,
|
|
13
13
|
} from "../modelShared/prop"
|
|
14
14
|
import { deepEquals } from "../treeUtils/deepEquals"
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
isTypeCheckingAfterChangeEnabled,
|
|
17
|
+
runTypeCheckingAfterChange,
|
|
18
|
+
} from "../tweaker/typeChecking"
|
|
16
19
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
17
20
|
import { isArray } from "../utils"
|
|
18
21
|
import { withErrorModelTrailEntry, withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
@@ -20,6 +23,7 @@ import type { ModelPool } from "../utils/ModelPool"
|
|
|
20
23
|
import { setIfDifferent } from "../utils/setIfDifferent"
|
|
21
24
|
import { fromSnapshot } from "./fromSnapshot"
|
|
22
25
|
import { getSnapshot } from "./getSnapshot"
|
|
26
|
+
import { flushInternalSnapshot, getInternalSnapshot } from "./internal"
|
|
23
27
|
import { detachIfNeeded, reconcileSnapshot, registerReconciler } from "./reconcileSnapshot"
|
|
24
28
|
import type { SnapshotInOfModel } from "./SnapshotOf"
|
|
25
29
|
import { SnapshotProcessingError } from "./SnapshotProcessingError"
|
|
@@ -42,15 +46,45 @@ function reconcileModelSnapshot(
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
const modelClass = modelInfo.class as ModelClass<AnyModel>
|
|
45
|
-
const trailModelId = getModelOrSnapshotTypeAndId(sn)?.modelId
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
// Snapshot processors are pure, deterministic, and required to round-trip
|
|
51
|
+
// the canonical output snapshot. A successful canonical no-op therefore
|
|
52
|
+
// cannot produce a diagnostic, so keep this hottest path outside the
|
|
53
|
+
// model-trail bookkeeping and defer id metadata until needed. The
|
|
54
|
+
// positional value was already flushed by the getSnapshot call in
|
|
55
|
+
// reconcileSnapshot, so its transformed snapshot is current.
|
|
56
|
+
if (isCanonicalSnapshotNoOp(value, type, sn)) {
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const modelIdPropertyName = getModelIdPropertyName(modelClass)
|
|
61
|
+
const incomingModelId = modelIdPropertyName ? sn[modelIdPropertyName] : undefined
|
|
62
|
+
const positionalModelMatches =
|
|
63
|
+
isModel(value) &&
|
|
64
|
+
value[modelTypeKey] === type &&
|
|
65
|
+
(!modelIdPropertyName || value[modelIdKey] === incomingModelId)
|
|
66
|
+
|
|
67
|
+
// The positional model is already the desired instance in the common
|
|
68
|
+
// stable-update case. Only moved identified models need a pool lookup.
|
|
69
|
+
if (!positionalModelMatches && modelIdPropertyName) {
|
|
70
|
+
const modelInPool = modelPool.findModelByTypeAndId(type, incomingModelId)
|
|
50
71
|
if (modelInPool) {
|
|
51
72
|
value = modelInPool
|
|
73
|
+
|
|
74
|
+
// A pool model may carry snapshot updates still pending propagation from
|
|
75
|
+
// earlier mutations in the same action, so flush before comparing.
|
|
76
|
+
flushInternalSnapshot(modelInPool, false)
|
|
77
|
+
|
|
78
|
+
// Repeat the canonical comparison so moved models can also avoid
|
|
79
|
+
// processor and property-by-property reconciliation.
|
|
80
|
+
if (isCanonicalSnapshotNoOp(modelInPool, type, sn)) {
|
|
81
|
+
return modelInPool
|
|
82
|
+
}
|
|
52
83
|
}
|
|
84
|
+
}
|
|
53
85
|
|
|
86
|
+
const trailModelId = typeof incomingModelId === "string" ? incomingModelId : undefined
|
|
87
|
+
return withErrorModelTrailEntry(type, trailModelId, () => {
|
|
54
88
|
// we don't check by actual instance since the class might be a different one due to hot reloading
|
|
55
89
|
if (!isModel(value) || value[modelTypeKey] !== type) {
|
|
56
90
|
// different kind of model type, no reconciliation possible
|
|
@@ -58,12 +92,8 @@ function reconcileModelSnapshot(
|
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
const modelProps = getInternalModelClassPropsInfo(modelClass)
|
|
61
|
-
const modelIdPropertyName = getModelIdPropertyName(modelClass)
|
|
62
|
-
|
|
63
95
|
if (modelIdPropertyName) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (value[modelIdKey] !== id) {
|
|
96
|
+
if (value[modelIdKey] !== incomingModelId) {
|
|
67
97
|
// different id, no reconciliation possible
|
|
68
98
|
return fromSnapshot<AnyModel>(sn)
|
|
69
99
|
}
|
|
@@ -76,13 +106,26 @@ function reconcileModelSnapshot(
|
|
|
76
106
|
}
|
|
77
107
|
|
|
78
108
|
const modelObj: AnyModel = value
|
|
79
|
-
const
|
|
109
|
+
const typeCheckingAfterChangeEnabled = isTypeCheckingAfterChangeEnabled()
|
|
110
|
+
const actualModelClass = (modelObj as any).constructor as ModelClass<AnyModel>
|
|
111
|
+
const fromSnapshotProcessor = actualModelClass.fromSnapshotProcessor
|
|
112
|
+
const snapshotBeforeChanges = typeCheckingAfterChangeEnabled ? getSnapshot(modelObj) : undefined
|
|
80
113
|
|
|
81
114
|
withoutTypeChecking(() => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
const processedSn = fromSnapshotProcessor ? fromSnapshotProcessor(sn) : sn
|
|
116
|
+
|
|
117
|
+
// Non-canonical inputs still run the input processor. Once converted to
|
|
118
|
+
// the model's stored shape, avoid reconciliation when that shape is
|
|
119
|
+
// already current. Comparing against the untransformed snapshot is
|
|
120
|
+
// essential because an output processor may have changed its shape.
|
|
121
|
+
if (fromSnapshotProcessor) {
|
|
122
|
+
if (!snapshotBeforeChanges) {
|
|
123
|
+
flushInternalSnapshot(modelObj, false)
|
|
124
|
+
}
|
|
125
|
+
if (snapshotSlotsAreEqual(getInternalSnapshot(modelObj)!.untransformed, processedSn)) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
}
|
|
86
129
|
|
|
87
130
|
const data = modelObj.$
|
|
88
131
|
|
|
@@ -136,12 +179,49 @@ function reconcileModelSnapshot(
|
|
|
136
179
|
}
|
|
137
180
|
})
|
|
138
181
|
|
|
139
|
-
|
|
182
|
+
if (typeCheckingAfterChangeEnabled) {
|
|
183
|
+
runTypeCheckingAfterChange(modelObj, undefined, snapshotBeforeChanges)
|
|
184
|
+
}
|
|
140
185
|
|
|
141
186
|
return modelObj
|
|
142
187
|
})
|
|
143
188
|
}
|
|
144
189
|
|
|
190
|
+
function isCanonicalSnapshotNoOp(value: any, type: string, sn: any): boolean {
|
|
191
|
+
return (
|
|
192
|
+
isModel(value) &&
|
|
193
|
+
value[modelTypeKey] === type &&
|
|
194
|
+
snapshotSlotsAreEqual(getInternalSnapshot(value)!.transformed, sn)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function snapshotSlotsAreEqual(
|
|
199
|
+
currentSnapshot: Record<string, unknown>,
|
|
200
|
+
incomingSnapshot: any
|
|
201
|
+
): boolean {
|
|
202
|
+
const currentKeys = Object.keys(currentSnapshot)
|
|
203
|
+
const incomingKeys = Object.keys(incomingSnapshot)
|
|
204
|
+
if (currentKeys.length !== incomingKeys.length) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < currentKeys.length; i++) {
|
|
209
|
+
const key = currentKeys[i]
|
|
210
|
+
// a matching key at the same index proves the key is an own property of
|
|
211
|
+
// the incoming snapshot without an Object.hasOwn call; canonical snapshots
|
|
212
|
+
// keep key order, so the fallback is rare (never trust inherited values,
|
|
213
|
+
// e.g. a crafted __proto__ chain, without an own-property proof)
|
|
214
|
+
if (incomingKeys[i] !== key && !Object.hasOwn(incomingSnapshot, key)) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
if (currentSnapshot[key] !== incomingSnapshot[key]) {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return true
|
|
223
|
+
}
|
|
224
|
+
|
|
145
225
|
/**
|
|
146
226
|
* @internal
|
|
147
227
|
*/
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { isObservableObject, remove } from "mobx"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isTypeCheckingAfterChangeEnabled,
|
|
4
|
+
runTypeCheckingAfterChange,
|
|
5
|
+
} from "../tweaker/typeChecking"
|
|
3
6
|
import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
|
|
4
7
|
import { isPlainObject } from "../utils"
|
|
5
8
|
import { withErrorPathSegment } from "../utils/errorDiagnostics"
|
|
@@ -23,7 +26,8 @@ function reconcilePlainObjectSnapshot(
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const plainObj = value
|
|
26
|
-
const
|
|
29
|
+
const typeCheckingAfterChangeEnabled = isTypeCheckingAfterChangeEnabled()
|
|
30
|
+
const snapshotBeforeChanges = typeCheckingAfterChangeEnabled ? getSnapshot(plainObj) : undefined
|
|
27
31
|
|
|
28
32
|
withoutTypeChecking(() => {
|
|
29
33
|
// remove excess props
|
|
@@ -54,7 +58,9 @@ function reconcilePlainObjectSnapshot(
|
|
|
54
58
|
}
|
|
55
59
|
})
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
if (typeCheckingAfterChangeEnabled) {
|
|
62
|
+
runTypeCheckingAfterChange(plainObj, undefined, snapshotBeforeChanges)
|
|
63
|
+
}
|
|
58
64
|
|
|
59
65
|
return plainObj
|
|
60
66
|
}
|
package/src/tweaker/core.ts
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
import { runInAction } from "mobx"
|
|
2
|
-
import {
|
|
2
|
+
import { hasDataObjectParent } from "../parent/core"
|
|
3
3
|
import { failure, isPrimitive } from "../utils"
|
|
4
|
+
import { treeNodeMetadata } from "./treeNodeMetadata"
|
|
5
|
+
|
|
6
|
+
type Untweaker = (() => void) & {
|
|
7
|
+
[secondaryUntweakerSymbol]?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const secondaryUntweakerSymbol = Symbol("secondaryUntweaker")
|
|
4
11
|
|
|
5
12
|
/**
|
|
13
|
+
* Stores two existing cleanup functions without allocating a third function to
|
|
14
|
+
* close over them. The primary function is internal and never exposed.
|
|
15
|
+
*
|
|
6
16
|
* @internal
|
|
7
17
|
*/
|
|
8
|
-
export
|
|
18
|
+
export function setTweakedObjectUntweakers(
|
|
19
|
+
value: object,
|
|
20
|
+
primaryUntweaker: Untweaker,
|
|
21
|
+
secondaryUntweaker: () => void
|
|
22
|
+
): void {
|
|
23
|
+
primaryUntweaker[secondaryUntweakerSymbol] = secondaryUntweaker
|
|
24
|
+
treeNodeMetadata.get(value)!.untweaker = primaryUntweaker
|
|
25
|
+
}
|
|
9
26
|
|
|
10
27
|
/**
|
|
11
28
|
* @internal
|
|
12
29
|
*/
|
|
13
30
|
export function isTweakedObject(value: unknown, canBeDataObject: boolean): value is object {
|
|
14
|
-
if (!canBeDataObject &&
|
|
31
|
+
if (!canBeDataObject && hasDataObjectParent(value as object)) {
|
|
15
32
|
return false
|
|
16
33
|
}
|
|
17
|
-
return
|
|
34
|
+
return treeNodeMetadata.get(value as object)?.tweaked === true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export function runTweakedObjectUntweakers(untweaker: Untweaker): void {
|
|
41
|
+
untweaker()
|
|
42
|
+
untweaker[secondaryUntweakerSymbol]?.()
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
/**
|
|
@@ -35,7 +60,7 @@ export function assertTweakedObject(
|
|
|
35
60
|
argName: string,
|
|
36
61
|
canBeDataObject = false
|
|
37
62
|
): asserts treeNode is object {
|
|
38
|
-
if (!canBeDataObject &&
|
|
63
|
+
if (!canBeDataObject && hasDataObjectParent(treeNode as object)) {
|
|
39
64
|
throw failure(`${argName} must be the model object instance instead of the '$' sub-object`)
|
|
40
65
|
}
|
|
41
66
|
if (isPrimitive(treeNode) || !isTweakedObject(treeNode, true)) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ObjectChildrenData } from "../parent/coreObjectChildren"
|
|
2
|
+
import type { ParentPath } from "../parent/path"
|
|
3
|
+
import type { SnapshotData } from "../snapshot/internal"
|
|
4
|
+
|
|
5
|
+
/** @internal */
|
|
6
|
+
export interface TreeNodeMetadata {
|
|
7
|
+
tweaked: boolean
|
|
8
|
+
untweaker: (() => void) | undefined
|
|
9
|
+
parentPath: ParentPath<object> | undefined
|
|
10
|
+
dataObjectParent: object | undefined
|
|
11
|
+
objectChildren: ObjectChildrenData | undefined
|
|
12
|
+
snapshot: SnapshotData | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
export const treeNodeMetadata = new WeakMap<object, TreeNodeMetadata>()
|
|
17
|
+
|
|
18
|
+
/** @internal */
|
|
19
|
+
export function getOrCreateTreeNodeMetadata(value: object): TreeNodeMetadata {
|
|
20
|
+
let metadata = treeNodeMetadata.get(value)
|
|
21
|
+
if (!metadata) {
|
|
22
|
+
metadata = {
|
|
23
|
+
tweaked: false,
|
|
24
|
+
untweaker: undefined,
|
|
25
|
+
parentPath: undefined,
|
|
26
|
+
dataObjectParent: undefined,
|
|
27
|
+
objectChildren: undefined,
|
|
28
|
+
snapshot: undefined,
|
|
29
|
+
}
|
|
30
|
+
treeNodeMetadata.set(value, metadata)
|
|
31
|
+
}
|
|
32
|
+
return metadata
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @internal */
|
|
36
|
+
export function markAsTweakedObject(value: object): void {
|
|
37
|
+
getOrCreateTreeNodeMetadata(value).tweaked = true
|
|
38
|
+
}
|
package/src/tweaker/tweak.ts
CHANGED
|
@@ -8,8 +8,9 @@ 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 { isTweakedObject,
|
|
11
|
+
import { isTweakedObject, runTweakedObjectUntweakers } from "./core"
|
|
12
12
|
import { registerDefaultTweakers } from "./registerDefaultTweakers"
|
|
13
|
+
import { treeNodeMetadata } from "./treeNodeMetadata"
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Turns an object (array, plain object) into a tree node,
|
|
@@ -153,7 +154,8 @@ export function tryUntweak(value: any): (() => void) | undefined {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
const
|
|
157
|
+
const metadata = treeNodeMetadata.get(value)
|
|
158
|
+
const untweaker = metadata?.untweaker
|
|
157
159
|
if (!untweaker) {
|
|
158
160
|
return undefined
|
|
159
161
|
}
|
|
@@ -176,9 +178,18 @@ export function tryUntweak(value: any): (() => void) | undefined {
|
|
|
176
178
|
|
|
177
179
|
return () => {
|
|
178
180
|
// post-untweaking, call the untweaker
|
|
179
|
-
untweaker
|
|
181
|
+
runTweakedObjectUntweakers(untweaker)
|
|
180
182
|
|
|
181
|
-
|
|
183
|
+
// Match the former independent-map ordering: the node stops being tweaked
|
|
184
|
+
// before its snapshot is unset, but the shared record must stay alive until
|
|
185
|
+
// snapshot cleanup has consumed it.
|
|
186
|
+
metadata.tweaked = false
|
|
187
|
+
metadata.untweaker = undefined
|
|
182
188
|
unsetInternalSnapshot(value)
|
|
189
|
+
// These associations previously lived in independent WeakMaps and remain
|
|
190
|
+
// valid across untweak/retweak, so keep their shared record when present.
|
|
191
|
+
if (metadata.dataObjectParent === undefined && metadata.objectChildren === undefined) {
|
|
192
|
+
treeNodeMetadata.delete(value)
|
|
193
|
+
}
|
|
183
194
|
}
|
|
184
195
|
}
|