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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/mobx-keystone.esm.js +6702 -5797
  3. package/dist/mobx-keystone.esm.mjs +6702 -5797
  4. package/dist/mobx-keystone.umd.js +6718 -5798
  5. package/dist/types/action/runUnprotected.d.ts +8 -4
  6. package/dist/types/model/Model.d.ts +5 -0
  7. package/dist/types/modelShared/prop.d.ts +8 -4
  8. package/dist/types/snapshot/modelInitialData.d.ts +1 -0
  9. package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
  10. package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
  11. package/package.json +11 -10
  12. package/src/action/middleware.ts +5 -18
  13. package/src/action/mutationBatch.ts +72 -0
  14. package/src/action/runUnprotected.ts +21 -8
  15. package/src/action/wrapInAction.ts +45 -15
  16. package/src/dataModel/BaseDataModel.ts +4 -8
  17. package/src/deepChange/onDeepChange.ts +246 -16
  18. package/src/model/BaseModel.ts +4 -12
  19. package/src/model/Model.ts +5 -0
  20. package/src/model/newModel.ts +68 -36
  21. package/src/modelShared/prop.ts +105 -63
  22. package/src/modelShared/sharedInternalModel.ts +91 -60
  23. package/src/parent/core.ts +16 -10
  24. package/src/parent/coreObjectChildren.ts +219 -21
  25. package/src/parent/path.ts +6 -5
  26. package/src/parent/setParent.ts +5 -5
  27. package/src/patch/emitPatch.ts +102 -20
  28. package/src/snapshot/fromArraySnapshot.ts +11 -2
  29. package/src/snapshot/fromModelSnapshot.ts +10 -3
  30. package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
  31. package/src/snapshot/fromSnapshot.ts +31 -9
  32. package/src/snapshot/getSnapshot.ts +8 -3
  33. package/src/snapshot/internal.ts +243 -82
  34. package/src/snapshot/modelInitialData.ts +41 -0
  35. package/src/snapshot/reconcileArraySnapshot.ts +9 -3
  36. package/src/snapshot/reconcileModelSnapshot.ts +97 -17
  37. package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
  38. package/src/tweaker/core.ts +30 -5
  39. package/src/tweaker/treeNodeMetadata.ts +38 -0
  40. package/src/tweaker/tweak.ts +15 -4
  41. package/src/tweaker/tweakArray.ts +159 -121
  42. package/src/tweaker/tweakFrozen.ts +2 -2
  43. package/src/tweaker/tweakModel.ts +2 -2
  44. package/src/tweaker/tweakPlainObject.ts +114 -122
  45. package/src/tweaker/typeChecking.ts +10 -1
  46. package/src/types/TypeChecker.ts +261 -15
  47. package/src/types/arrayBased/typesArray.ts +38 -40
  48. package/src/types/arrayBased/typesTuple.ts +51 -34
  49. package/src/types/createCachedTypeCheck.ts +181 -0
  50. package/src/types/objectBased/typesArraySet.ts +42 -32
  51. package/src/types/objectBased/typesDataModelData.ts +18 -11
  52. package/src/types/objectBased/typesModel.ts +1 -1
  53. package/src/types/objectBased/typesObject.ts +57 -28
  54. package/src/types/objectBased/typesObjectMap.ts +51 -40
  55. package/src/types/objectBased/typesRecord.ts +59 -39
  56. package/src/types/objectBased/typesRef.ts +1 -1
  57. package/src/types/primitiveBased/typesPrimitive.ts +9 -9
  58. package/src/types/tProp.ts +80 -44
  59. package/src/types/utility/typesOr.ts +35 -24
  60. package/src/types/utility/typesRefinement.ts +21 -8
  61. package/src/types/utility/typesSkipCheck.ts +21 -8
  62. package/src/types/utility/typesTag.ts +21 -9
  63. package/src/types/utility/typesUnchecked.ts +2 -3
  64. package/src/utils/ModelPool.ts +34 -19
  65. package/src/utils/errorDiagnostics.ts +60 -18
  66. package/src/utils/index.ts +9 -7
  67. package/src/types/createPerEntryCachedCheck.ts +0 -166
  68. /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
@@ -7,29 +7,35 @@ import {
7
7
  observe,
8
8
  } from "mobx"
9
9
  import { assertCanWrite } from "../action/protection"
10
- import { DeepChangeType, emitDeepChange, getIsInInitPhase } from "../deepChange/onDeepChange"
10
+ import {
11
+ emitObjectAddDeepChange,
12
+ emitObjectRemoveDeepChange,
13
+ emitObjectUpdateDeepChange,
14
+ } from "../deepChange/onDeepChange"
11
15
  import type { AnyModel } from "../model/BaseModel"
12
16
  import { modelTypeKey } from "../model/metadata"
13
17
  import type { ModelClass } from "../modelShared/BaseModelShared"
14
18
  import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
15
19
  import { dataToModelNode } from "../parent/core"
16
20
  import type { ParentPath } from "../parent/path"
17
- import type { Path } from "../parent/pathTypes"
18
21
  import { setParent } from "../parent/setParent"
19
- import { InternalPatchRecorder } from "../patch/emitPatch"
22
+ import { hasPatchListenersFor, InternalPatchRecorder } from "../patch/emitPatch"
20
23
  import {
24
+ flushInternalSnapshot,
21
25
  freezeInternalSnapshot,
22
26
  getInternalSnapshot,
23
27
  type SnapshotTransformFn,
24
28
  setNewInternalSnapshot,
25
29
  updateInternalSnapshot,
26
30
  } from "../snapshot/internal"
27
- import { failure, isPlainObject, isPrimitive, setOwnProp } from "../utils"
31
+ import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
32
+ import { failure, isPlainObject, isPrimitive, setProtoProp } from "../utils"
28
33
  import { setIfDifferent } from "../utils/setIfDifferent"
29
- import { runningWithoutSnapshotOrPatches, tweakedObjects } from "./core"
34
+ import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
30
35
  import { TweakerPriority } from "./TweakerPriority"
36
+ import { markAsTweakedObject } from "./treeNodeMetadata"
31
37
  import { registerTweaker, tweak } from "./tweak"
32
- import { runTypeCheckingAfterChange } from "./typeChecking"
38
+ import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
33
39
 
34
40
  /**
35
41
  * @internal
@@ -46,15 +52,9 @@ export function tweakPlainObject<T extends Record<string, any>>(
46
52
  ? originalObj
47
53
  : observable.object({}, undefined, observableOptions)
48
54
 
49
- let interceptDisposer: () => void
50
- let observeDisposer: () => void
51
-
52
- const untweak = () => {
53
- interceptDisposer()
54
- observeDisposer()
55
- }
56
-
57
- tweakedObjects.set(tweakedObj, untweak)
55
+ // Mark it as tweaked before processing children. The cleanup functions are
56
+ // installed after the MobX handlers have been registered.
57
+ markAsTweakedObject(tweakedObj)
58
58
  setParent(
59
59
  tweakedObj, // value
60
60
  parentPath,
@@ -64,10 +64,14 @@ export function tweakPlainObject<T extends Record<string, any>>(
64
64
  false // cloneIfApplicable
65
65
  )
66
66
 
67
- const untransformedSn: any = {}
67
+ const initialDataSnapshot =
68
+ isDataObject && snapshotModelType ? takeModelInitialDataSnapshot(tweakedObj) : undefined
69
+ const reuseInitialDataSnapshot =
70
+ !!initialDataSnapshot?.reusable && initialDataSnapshot.allValuesPrimitive
71
+ const untransformedSn: any = reuseInitialDataSnapshot ? initialDataSnapshot.snapshot : {}
68
72
 
69
73
  // substitute initial values by tweaked values
70
- const originalObjKeys = Object.keys(originalObj)
74
+ const originalObjKeys = reuseInitialDataSnapshot ? [] : Object.keys(originalObj)
71
75
  const originalObjKeysLen = originalObjKeys.length
72
76
  for (let i = 0; i < originalObjKeysLen; i++) {
73
77
  const k = originalObjKeys[i]
@@ -77,7 +81,11 @@ export function tweakPlainObject<T extends Record<string, any>>(
77
81
  if (!doNotTweakChildren) {
78
82
  setIfDifferent(tweakedObj, k, v)
79
83
  }
80
- setOwnProp(untransformedSn, k, v)
84
+ if (k === "__proto__") {
85
+ setProtoProp(untransformedSn, v)
86
+ } else {
87
+ untransformedSn[k] = v
88
+ }
81
89
  } else {
82
90
  const path = { parent: tweakedObj, path: k }
83
91
 
@@ -98,13 +106,17 @@ export function tweakPlainObject<T extends Record<string, any>>(
98
106
  }
99
107
 
100
108
  const valueSn = getInternalSnapshot(tweakedValue)!
101
- setOwnProp(untransformedSn, k, valueSn.transformed)
109
+ if (k === "__proto__") {
110
+ setProtoProp(untransformedSn, valueSn.transformed)
111
+ } else {
112
+ untransformedSn[k] = valueSn.transformed
113
+ }
102
114
  }
103
115
  }
104
116
 
105
117
  let transformFn: SnapshotTransformFn | undefined
106
118
  if (snapshotModelType) {
107
- setOwnProp(untransformedSn, modelTypeKey, snapshotModelType)
119
+ untransformedSn[modelTypeKey] = snapshotModelType
108
120
 
109
121
  const modelInfo = getModelInfoForName(snapshotModelType)
110
122
  if (!modelInfo) {
@@ -123,8 +135,9 @@ export function tweakPlainObject<T extends Record<string, any>>(
123
135
  transformFn
124
136
  )
125
137
 
126
- interceptDisposer = intercept(tweakedObj, interceptObjectMutation)
127
- observeDisposer = observe(tweakedObj, objectDidChange)
138
+ const interceptDisposer = intercept(tweakedObj, interceptObjectMutation)
139
+ const observeDisposer = observe(tweakedObj, objectDidChange)
140
+ setTweakedObjectUntweakers(tweakedObj, interceptDisposer, observeDisposer)
128
141
 
129
142
  return tweakedObj as any
130
143
  }
@@ -134,7 +147,11 @@ const observableOptions = {
134
147
  }
135
148
 
136
149
  function mutateSet(k: PropertyKey, v: unknown, sn: Record<PropertyKey, unknown>) {
137
- setOwnProp(sn, k, v)
150
+ if (k === "__proto__") {
151
+ setProtoProp(sn, v)
152
+ } else {
153
+ sn[k] = v
154
+ }
138
155
  }
139
156
 
140
157
  function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>) {
@@ -143,8 +160,6 @@ function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>) {
143
160
 
144
161
  const patchRecorder = new InternalPatchRecorder()
145
162
 
146
- const emptyPath: Path = Object.freeze([])
147
-
148
163
  function objectDidChange(change: IObjectDidChange): void {
149
164
  if (typeof change.name !== "string") {
150
165
  throw failure("change.name is not a string")
@@ -152,99 +167,88 @@ function objectDidChange(change: IObjectDidChange): void {
152
167
 
153
168
  const obj = change.object
154
169
  const actualNode = dataToModelNode(obj)
155
- const oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed
156
170
 
157
171
  patchRecorder.reset()
158
172
 
159
- let mutate: ((sn: any) => void) | undefined
160
-
161
173
  switch (change.type) {
162
- case "add": {
163
- mutate = objectDidChangeAddOrUpdate(change, oldUntransformedSn)
164
- // Emit deep change for add
165
- emitDeepChange(actualNode, {
166
- type: DeepChangeType.ObjectAdd,
167
- target: obj as object,
168
- path: emptyPath,
169
- key: change.name,
170
- newValue: change.newValue,
171
- isInit: getIsInInitPhase(),
172
- })
174
+ case "add":
175
+ emitObjectAddDeepChange(actualNode, obj, change.name, change.newValue)
173
176
  break
174
- }
175
177
 
176
- case "update": {
177
- mutate = objectDidChangeAddOrUpdate(change, oldUntransformedSn)
178
- // Emit deep change for update
179
- emitDeepChange(actualNode, {
180
- type: DeepChangeType.ObjectUpdate,
181
- target: obj as object,
182
- path: emptyPath,
183
- key: change.name,
184
- newValue: change.newValue,
185
- oldValue: change.oldValue,
186
- isInit: getIsInInitPhase(),
187
- })
178
+ case "update":
179
+ emitObjectUpdateDeepChange(actualNode, obj, change.name, change.newValue, change.oldValue)
188
180
  break
189
- }
190
181
 
191
- case "remove": {
192
- mutate = objectDidChangeRemove(change, oldUntransformedSn)
193
- // Emit deep change for remove
194
- emitDeepChange(actualNode, {
195
- type: DeepChangeType.ObjectRemove,
196
- target: obj as object,
197
- path: emptyPath,
198
- key: change.name,
199
- oldValue: change.oldValue,
200
- isInit: getIsInInitPhase(),
201
- })
182
+ case "remove":
183
+ emitObjectRemoveDeepChange(actualNode, obj, change.name, change.oldValue)
202
184
  break
203
- }
204
185
 
205
186
  default:
206
187
  throw failure("assertion error: unsupported object change type")
207
188
  }
208
189
 
209
- runTypeCheckingAfterChange(obj, patchRecorder)
190
+ if (runningWithoutSnapshotOrPatches) {
191
+ return
192
+ }
193
+
194
+ const oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed
195
+ const shouldEmitPatches = hasPatchListenersFor(actualNode)
196
+ const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
210
197
 
211
- if (!runningWithoutSnapshotOrPatches) {
212
- updateInternalSnapshot(actualNode, mutate)
213
- patchRecorder.emit(actualNode)
198
+ let mutate: ((sn: any) => void) | undefined
199
+ switch (change.type) {
200
+ case "add":
201
+ case "update":
202
+ mutate = objectDidChangeAddOrUpdate(
203
+ change,
204
+ oldUntransformedSn,
205
+ shouldEmitPatches,
206
+ shouldBuildInversePatches
207
+ )
208
+ break
209
+ case "remove":
210
+ mutate = objectDidChangeRemove(
211
+ change,
212
+ oldUntransformedSn,
213
+ shouldEmitPatches,
214
+ shouldBuildInversePatches
215
+ )
216
+ break
217
+ default:
218
+ throw failure("assertion error: unsupported object change type")
214
219
  }
220
+
221
+ runTypeCheckingAfterChange(obj, patchRecorder)
222
+ updateInternalSnapshot(actualNode, mutate)
223
+ patchRecorder.emit(actualNode)
215
224
  }
216
225
 
217
226
  function objectDidChangeRemove(
218
227
  change: IObjectDidChange & { type: "remove" },
219
- oldUntransformedSn: any
228
+ oldUntransformedSn: any,
229
+ shouldEmitPatches: boolean,
230
+ shouldBuildInversePatches: boolean
220
231
  ) {
221
232
  const k = change.name
222
233
  const oldVal = oldUntransformedSn[k]
223
234
  const mutate = mutateDelete.bind(undefined, k)
224
235
 
225
- const path = [k as string]
226
-
227
- patchRecorder.record(
228
- [
229
- {
230
- op: "remove",
231
- path,
232
- },
233
- ],
234
- [
235
- {
236
- op: "add",
237
- path,
238
- value: freezeInternalSnapshot(oldVal),
239
- },
240
- ]
241
- )
236
+ if (shouldEmitPatches || shouldBuildInversePatches) {
237
+ const path = [k as string]
238
+ const patches = shouldEmitPatches ? [{ op: "remove" as const, path }] : undefined
239
+ const invPatches = shouldBuildInversePatches
240
+ ? [{ op: "add" as const, path, value: freezeInternalSnapshot(oldVal) }]
241
+ : undefined
242
+ patchRecorder.record(patches, invPatches)
243
+ }
242
244
  return mutate
243
245
  }
244
246
 
245
247
  function objectDidChangeAddOrUpdate(
246
248
  change: IObjectWillChange & { type: "add" | "update" },
247
- oldUntransformedSn: any
249
+ oldUntransformedSn: any,
250
+ shouldEmitPatches: boolean,
251
+ shouldBuildInversePatches: boolean
248
252
  ) {
249
253
  const k = change.name
250
254
  const val = change.newValue
@@ -261,40 +265,23 @@ function objectDidChangeAddOrUpdate(
261
265
 
262
266
  const mutate = mutateSet.bind(undefined, k, newVal)
263
267
 
264
- const path = [k as string]
265
- if (change.type === "add") {
266
- patchRecorder.record(
267
- [
268
- {
269
- op: "add",
270
- path,
271
- value: freezeInternalSnapshot(newVal),
272
- },
273
- ],
274
- [
275
- {
276
- op: "remove",
277
- path,
278
- },
279
- ]
280
- )
281
- } else {
282
- patchRecorder.record(
283
- [
284
- {
285
- op: "replace",
286
- path,
287
- value: freezeInternalSnapshot(newVal),
288
- },
289
- ],
290
- [
291
- {
292
- op: "replace",
293
- path,
294
- value: freezeInternalSnapshot(oldVal),
295
- },
296
- ]
297
- )
268
+ if (shouldEmitPatches || shouldBuildInversePatches) {
269
+ const path = [k as string]
270
+ if (change.type === "add") {
271
+ const patches = shouldEmitPatches
272
+ ? [{ op: "add" as const, path, value: freezeInternalSnapshot(newVal) }]
273
+ : undefined
274
+ const invPatches = shouldBuildInversePatches ? [{ op: "remove" as const, path }] : undefined
275
+ patchRecorder.record(patches, invPatches)
276
+ } else {
277
+ const patches = shouldEmitPatches
278
+ ? [{ op: "replace" as const, path, value: freezeInternalSnapshot(newVal) }]
279
+ : undefined
280
+ const invPatches = shouldBuildInversePatches
281
+ ? [{ op: "replace" as const, path, value: freezeInternalSnapshot(oldVal) }]
282
+ : undefined
283
+ patchRecorder.record(patches, invPatches)
284
+ }
298
285
  }
299
286
 
300
287
  return mutate
@@ -307,6 +294,11 @@ function interceptObjectMutation(change: IObjectWillChange) {
307
294
  throw failure("symbol properties are not supported")
308
295
  }
309
296
 
297
+ // This must happen before detach/reparent work below. It keeps the old value
298
+ // available for inverse patches and folds a departing dirty child into its
299
+ // parent snapshot while its parent path still exists.
300
+ flushInternalSnapshot(dataToModelNode(change.object), false)
301
+
310
302
  switch (change.type) {
311
303
  case "add":
312
304
  change.newValue = tweak(change.newValue, {
@@ -18,6 +18,15 @@ import { isTypeCheckingAllowed } from "./withoutTypeChecking"
18
18
  // still be invalid, causing another rollback attempt).
19
19
  let isRollingBackTypeCheckFailure = false
20
20
 
21
+ /**
22
+ * @internal
23
+ */
24
+ export function isTypeCheckingAfterChangeEnabled(): boolean {
25
+ return (
26
+ isTypeCheckingAllowed() && isModelAutoTypeCheckingEnabled() && !isRollingBackTypeCheckFailure
27
+ )
28
+ }
29
+
21
30
  function isModelWithTypeChecker(obj: object): obj is AnyModel {
22
31
  return isModel(obj) && !!getModelMetadata(obj).dataType
23
32
  }
@@ -68,7 +77,7 @@ export function runTypeCheckingAfterChange(
68
77
  return
69
78
  }
70
79
 
71
- if (patchRecorder && patchRecorder.patches.length <= 0) {
80
+ if (patchRecorder && !patchRecorder.hasChanges) {
72
81
  // No patches means no effective change.
73
82
  return
74
83
  }