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