mobx-keystone 2.0.0 → 2.2.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 (102) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +2561 -1214
  4. package/dist/mobx-keystone.esm.mjs +2561 -1214
  5. package/dist/mobx-keystone.umd.js +2566 -1220
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
  8. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  9. package/dist/types/modelShared/prop.d.ts +42 -29
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/treeOrder.d.ts +1 -0
  12. package/dist/types/patch/applyPatches.d.ts +2 -1
  13. package/dist/types/ref/Ref.d.ts +1 -4
  14. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  15. package/dist/types/ref/nodesById.d.ts +1 -0
  16. package/dist/types/ref/refIdIndex.d.ts +1 -0
  17. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  18. package/dist/types/snapshot/applySnapshot.d.ts +2 -1
  19. package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
  20. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  21. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  22. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  23. package/dist/types/types/index.d.ts +1 -0
  24. package/dist/types/types/schemas.d.ts +41 -37
  25. package/dist/types/types/tProp.d.ts +12 -6
  26. package/dist/types/types/types.d.ts +6 -6
  27. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  28. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  29. package/dist/types/utils/ModelPool.d.ts +7 -2
  30. package/dist/types/utils/chunks.d.ts +1 -0
  31. package/dist/types/utils/types.d.ts +6 -0
  32. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  33. package/package.json +5 -3
  34. package/src/action/modelFlow.ts +35 -66
  35. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  36. package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
  37. package/src/actionMiddlewares/undoMiddleware.ts +63 -35
  38. package/src/context/context.ts +15 -36
  39. package/src/dataModel/utils.ts +5 -28
  40. package/src/deepChange/onDeepChange.ts +44 -126
  41. package/src/model/BaseModel.ts +7 -5
  42. package/src/model/utils.ts +5 -10
  43. package/src/modelShared/modelDecorator.ts +16 -30
  44. package/src/modelShared/prop.ts +115 -111
  45. package/src/modelShared/sharedInternalModel.ts +2 -2
  46. package/src/parent/arrayPaths.ts +239 -0
  47. package/src/parent/coreObjectChildren.ts +300 -48
  48. package/src/parent/onChildAttachedTo.ts +130 -44
  49. package/src/parent/path.ts +10 -12
  50. package/src/parent/setParent.ts +27 -15
  51. package/src/parent/treeOrder.ts +53 -0
  52. package/src/parent/walkTree.ts +0 -73
  53. package/src/patch/applyPatches.ts +31 -26
  54. package/src/patch/patchRecorder.ts +13 -28
  55. package/src/ref/core.ts +69 -46
  56. package/src/ref/modelRefIdIndex.ts +131 -0
  57. package/src/ref/nodesById.ts +60 -0
  58. package/src/ref/refIdIndex.ts +209 -0
  59. package/src/rootStore/rootStore.ts +1 -3
  60. package/src/snapshot/SnapshotOf.ts +24 -48
  61. package/src/snapshot/applySnapshot.ts +27 -19
  62. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  63. package/src/snapshot/fromSnapshot.ts +12 -9
  64. package/src/snapshot/getSnapshot.ts +1 -1
  65. package/src/snapshot/internal.ts +67 -12
  66. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  67. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  68. package/src/treeUtils/draft.ts +6 -1
  69. package/src/treeUtils/sandbox.ts +10 -7
  70. package/src/tweaker/changeRollback.ts +218 -0
  71. package/src/tweaker/core.ts +33 -20
  72. package/src/tweaker/treeNodeMetadata.ts +12 -2
  73. package/src/tweaker/tweak.ts +181 -18
  74. package/src/tweaker/tweakArray.ts +127 -77
  75. package/src/tweaker/tweakPlainObject.ts +65 -46
  76. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  77. package/src/tweaker/typeChecking.ts +5 -104
  78. package/src/types/arrayBased/typesArray.ts +23 -30
  79. package/src/types/arrayBased/typesTuple.ts +16 -25
  80. package/src/types/createCachedTypeCheck.ts +509 -94
  81. package/src/types/index.ts +1 -0
  82. package/src/types/objectBased/typesDataModelData.ts +7 -8
  83. package/src/types/objectBased/typesObject.ts +11 -15
  84. package/src/types/objectBased/typesRecord.ts +12 -19
  85. package/src/types/schemas.ts +59 -71
  86. package/src/types/tProp.ts +29 -29
  87. package/src/types/utility/typeToStoredData.ts +22 -21
  88. package/src/types/utility/typesCodecCore.ts +5 -0
  89. package/src/types/utility/typesCodecSupport.ts +3 -3
  90. package/src/types/utility/typesMap.ts +43 -51
  91. package/src/types/utility/typesRefinement.ts +7 -8
  92. package/src/types/utility/typesSkipCheck.ts +7 -8
  93. package/src/types/utility/typesTag.ts +7 -8
  94. package/src/utils/ModelPool.ts +76 -37
  95. package/src/utils/chunks.ts +271 -0
  96. package/src/utils/decorators.ts +5 -5
  97. package/src/utils/index.ts +38 -20
  98. package/src/utils/types.ts +5 -0
  99. package/src/wrappers/ArraySet.ts +27 -14
  100. package/src/wrappers/ObjectMap.ts +1 -6
  101. package/src/wrappers/asMap.ts +1 -21
  102. package/src/wrappers/asSet.ts +0 -8
@@ -13,10 +13,12 @@ import {
13
13
  emitObjectUpdateDeepChange,
14
14
  } from "../deepChange/onDeepChange"
15
15
  import type { AnyModel } from "../model/BaseModel"
16
+ import { getModelIdPropertyName } from "../model/getModelMetadata"
16
17
  import { modelTypeKey } from "../model/metadata"
17
18
  import type { ModelClass } from "../modelShared/BaseModelShared"
18
19
  import { getModelInfoForName, getModelNotRegisteredErrorMessage } from "../modelShared/modelInfo"
19
20
  import { dataToModelNode } from "../parent/core"
21
+ import { onModelIdChanged } from "../parent/coreObjectChildren"
20
22
  import type { ParentPath } from "../parent/path"
21
23
  import { setParent } from "../parent/setParent"
22
24
  import {
@@ -36,22 +38,16 @@ import {
36
38
  } from "../snapshot/internal"
37
39
  import { takeModelInitialDataSnapshot } from "../snapshot/modelInitialData"
38
40
  import { withoutCachedTypeChecking } from "../types/createCachedTypeCheck"
39
- import { failure, isPlainObject, isPrimitive, setProtoProp } from "../utils"
40
- import {
41
- addDelayedError,
42
- type DelayedError,
43
- throwDelayedError,
44
- } from "../utils/forEachWithDelayedThrow"
41
+ import { failure, isPlainObject, isPrimitive, nodeObservableOptions, setProtoProp } from "../utils"
42
+ import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
45
43
  import { setIfDifferent } from "../utils/setIfDifferent"
46
- import { runningWithoutSnapshotOrPatches, setTweakedObjectUntweakers } from "./core"
44
+ import { recordChangeForRollback, throwDelayedListenerError } from "./changeRollback"
45
+ import { activateHandlers, runningWithoutSnapshotOrPatches } from "./core"
47
46
  import { TweakerPriority } from "./TweakerPriority"
48
- import { markAsTweakedObject } from "./treeNodeMetadata"
49
- import { registerTweaker, tweak } from "./tweak"
50
- import {
51
- isTypeCheckingAfterChangeEnabled,
52
- recordTypeCheckingBatchChange,
53
- runTypeCheckingAfterChange,
54
- } from "./typeChecking"
47
+ import { markAsTweakedObject, treeNodeMetadata } from "./treeNodeMetadata"
48
+ import { assertCanAttachValue, registerTweaker, tweak } from "./tweak"
49
+ import { notifyTweakedChangeListeners } from "./tweakedChangeListeners"
50
+ import { isTypeCheckingAfterChangeEnabled, runTypeCheckingAfterChange } from "./typeChecking"
55
51
 
56
52
  /**
57
53
  * @internal
@@ -67,7 +63,7 @@ export function tweakPlainObject<T extends Record<string, any>>(
67
63
  const originalIsObservableObject = isObservableObject(originalObj)
68
64
  const tweakedObj = originalIsObservableObject
69
65
  ? originalObj
70
- : observable.object({}, undefined, observableOptions)
66
+ : observable.object({}, undefined, nodeObservableOptions)
71
67
 
72
68
  // Mark it as tweaked before processing children. The cleanup functions are
73
69
  // installed after the MobX handlers have been registered.
@@ -150,38 +146,57 @@ export function tweakPlainObject<T extends Record<string, any>>(
150
146
  transformFn
151
147
  )
152
148
 
153
- const interceptDisposer = intercept(tweakedObj, interceptObjectMutation)
154
- const observeDisposer = observe(tweakedObj, objectDidChange)
155
- setTweakedObjectUntweakers(tweakedObj, interceptDisposer, observeDisposer)
149
+ if (activateHandlers(tweakedObj)) {
150
+ intercept(tweakedObj, interceptObjectMutation)
151
+ observe(tweakedObj, objectDidChange)
152
+ }
156
153
 
157
154
  return tweakedObj as any
158
155
  }
159
156
 
160
- const observableOptions = {
161
- deep: false,
162
- }
163
-
164
- function mutateSet(k: PropertyKey, v: unknown, sn: Record<PropertyKey, unknown>) {
157
+ function mutateSet(
158
+ k: PropertyKey,
159
+ v: unknown,
160
+ sn: Record<PropertyKey, unknown>
161
+ ): readonly unknown[] {
165
162
  if (k === "__proto__") {
166
163
  setProtoProp(sn, v)
167
164
  } else {
168
165
  sn[k] = v
169
166
  }
167
+ return [v]
170
168
  }
171
169
 
172
- function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>) {
170
+ const noValues: readonly unknown[] = []
171
+
172
+ function mutateDelete(k: PropertyKey, sn: Record<PropertyKey, unknown>): readonly unknown[] {
173
173
  delete sn[k]
174
+ return noValues
174
175
  }
175
176
 
176
177
  const patchRecorder = new InternalPatchRecorder()
177
178
 
178
179
  function objectDidChange(change: IObjectDidChange): void {
180
+ const obj = change.object
181
+ const metadata = treeNodeMetadata.get(obj)
182
+ if (!metadata?.handlersActive) {
183
+ // idle handler of an untweaked object
184
+ return
185
+ }
179
186
  if (typeof change.name !== "string") {
180
187
  throw failure("change.name is not a string")
181
188
  }
182
189
 
183
- const obj = change.object
184
- const actualNode = dataToModelNode(obj)
190
+ notifyTweakedChangeListeners(change)
191
+
192
+ const actualNode = metadata.dataObjectParent ?? obj
193
+
194
+ if (
195
+ actualNode !== obj &&
196
+ change.name === getModelIdPropertyName(actualNode.constructor as ModelClass<AnyModel>)
197
+ ) {
198
+ onModelIdChanged(actualNode)
199
+ }
185
200
 
186
201
  patchRecorder.reset()
187
202
 
@@ -189,13 +204,11 @@ function objectDidChange(change: IObjectDidChange): void {
189
204
  return
190
205
  }
191
206
 
192
- recordTypeCheckingBatchChange(change)
193
-
194
207
  const oldUntransformedSn = getInternalSnapshot(actualNode)!.untransformed
195
208
  const shouldEmitPatches = hasPatchListenersFor(actualNode)
196
209
  const shouldBuildInversePatches = shouldEmitPatches || isTypeCheckingAfterChangeEnabled()
197
210
 
198
- let mutate: ((sn: any) => void) | undefined
211
+ let mutate: ((sn: any) => readonly unknown[]) | undefined
199
212
  switch (change.type) {
200
213
  case "add":
201
214
  case "update":
@@ -226,6 +239,8 @@ function objectDidChange(change: IObjectDidChange): void {
226
239
  } else {
227
240
  runTypeCheckingAfterChange(obj, patchRecorder)
228
241
  }
242
+ // (after type checking, which undoes a rejected change, but before listeners)
243
+ recordChangeForRollback(change)
229
244
  updateInternalSnapshot(actualNode, mutate)
230
245
 
231
246
  // Listeners may mutate another node and reuse the shared recorder.
@@ -270,7 +285,7 @@ function objectDidChange(change: IObjectDidChange): void {
270
285
  } catch (error) {
271
286
  delayedError = addDelayedError(delayedError, error)
272
287
  }
273
- throwDelayedError(delayedError)
288
+ throwDelayedListenerError(delayedError)
274
289
  }
275
290
 
276
291
  function objectDidChangeRemove(
@@ -280,16 +295,14 @@ function objectDidChangeRemove(
280
295
  shouldBuildInversePatches: boolean
281
296
  ) {
282
297
  const k = change.name
283
- const oldVal = oldUntransformedSn[k]
284
298
  const mutate = mutateDelete.bind(undefined, k)
285
299
 
286
- if (shouldEmitPatches || shouldBuildInversePatches) {
300
+ if (shouldBuildInversePatches) {
287
301
  const path = [k as string]
288
302
  const patches = shouldEmitPatches ? [{ op: "remove" as const, path }] : undefined
289
- const invPatches = shouldBuildInversePatches
290
- ? [{ op: "add" as const, path, value: freezeInternalSnapshot(oldVal) }]
291
- : undefined
292
- patchRecorder.record(patches, invPatches)
303
+ patchRecorder.record(patches, [
304
+ { op: "add" as const, path, value: freezeInternalSnapshot(oldUntransformedSn[k]) },
305
+ ])
293
306
  }
294
307
  return mutate
295
308
  }
@@ -303,8 +316,6 @@ function objectDidChangeAddOrUpdate(
303
316
  const k = change.name
304
317
  const val = change.newValue
305
318
 
306
- const oldVal = oldUntransformedSn[k]
307
-
308
319
  let newVal: any
309
320
  if (isPrimitive(val)) {
310
321
  newVal = val
@@ -315,29 +326,34 @@ function objectDidChangeAddOrUpdate(
315
326
 
316
327
  const mutate = mutateSet.bind(undefined, k, newVal)
317
328
 
318
- if (shouldEmitPatches || shouldBuildInversePatches) {
329
+ if (shouldBuildInversePatches) {
319
330
  const path = [k as string]
320
331
  if (change.type === "add") {
321
332
  const patches = shouldEmitPatches
322
333
  ? [{ op: "add" as const, path, value: freezeInternalSnapshot(newVal) }]
323
334
  : undefined
324
- const invPatches = shouldBuildInversePatches ? [{ op: "remove" as const, path }] : undefined
325
- patchRecorder.record(patches, invPatches)
335
+ patchRecorder.record(patches, [{ op: "remove" as const, path }])
326
336
  } else {
327
337
  const patches = shouldEmitPatches
328
338
  ? [{ op: "replace" as const, path, value: freezeInternalSnapshot(newVal) }]
329
339
  : undefined
330
- const invPatches = shouldBuildInversePatches
331
- ? [{ op: "replace" as const, path, value: freezeInternalSnapshot(oldVal) }]
332
- : undefined
333
- patchRecorder.record(patches, invPatches)
340
+ patchRecorder.record(patches, [
341
+ { op: "replace" as const, path, value: freezeInternalSnapshot(oldUntransformedSn[k]) },
342
+ ])
334
343
  }
335
344
  }
336
345
 
337
346
  return mutate
338
347
  }
339
348
 
349
+ const noneDetached = () => false
350
+
340
351
  function interceptObjectMutation(change: IObjectWillChange) {
352
+ const metadata = treeNodeMetadata.get(change.object)
353
+ if (!metadata?.handlersActive) {
354
+ // idle handler of an untweaked object
355
+ return change
356
+ }
341
357
  assertCanWrite()
342
358
 
343
359
  if (typeof change.name === "symbol") {
@@ -347,10 +363,11 @@ function interceptObjectMutation(change: IObjectWillChange) {
347
363
  // This must happen before detach/reparent work below. It keeps the old value
348
364
  // available for inverse patches and folds a departing dirty child into its
349
365
  // parent snapshot while its parent path still exists.
350
- flushInternalSnapshot(dataToModelNode(change.object), false)
366
+ flushInternalSnapshot(metadata.dataObjectParent ?? change.object, false)
351
367
 
352
368
  switch (change.type) {
353
369
  case "add":
370
+ assertCanAttachValue(change.newValue, noneDetached)
354
371
  change.newValue = tweak(change.newValue, {
355
372
  parent: change.object,
356
373
  path: String(change.name),
@@ -366,6 +383,8 @@ function interceptObjectMutation(change: IObjectWillChange) {
366
383
  const oldVal = change.object[change.name]
367
384
  const newVal = change.newValue
368
385
  if (newVal !== oldVal) {
386
+ // (first, so a value that cannot be attached leaves the old one in place)
387
+ assertCanAttachValue(newVal, (node) => node === oldVal)
369
388
  tweak(oldVal, undefined)
370
389
 
371
390
  change.newValue = tweak(newVal, {
@@ -0,0 +1,61 @@
1
+ import type { IArrayDidChange, IObjectDidChange } from "mobx"
2
+
3
+ type TweakedChange = IArrayDidChange | IObjectDidChange
4
+
5
+ /**
6
+ * @internal
7
+ */
8
+ export interface TweakedChangeListener<C extends TweakedChange> {
9
+ applyChange(change: C): void
10
+ /**
11
+ * Called when the target is tweaked again after being untweaked, since the
12
+ * changes made in between were not received.
13
+ */
14
+ resync(): void
15
+ }
16
+
17
+ const changeListeners = new WeakMap<object, TweakedChangeListener<any>[]>()
18
+
19
+ /**
20
+ * Listens to the changes of a tweaked array or plain object, in order, before
21
+ * anything else reacts to them (including the type checks after the change).
22
+ * Unlike `observe`, changes made while reacting to a change (such as a
23
+ * type-check rollback) are received after it.
24
+ *
25
+ * @internal
26
+ */
27
+ export function addTweakedChangeListener<T extends object>(
28
+ target: T,
29
+ listener: TweakedChangeListener<T extends readonly unknown[] ? IArrayDidChange : IObjectDidChange>
30
+ ): void {
31
+ let listeners = changeListeners.get(target)
32
+ if (!listeners) {
33
+ listeners = []
34
+ changeListeners.set(target, listeners)
35
+ }
36
+ listeners.push(listener)
37
+ }
38
+
39
+ /**
40
+ * @internal
41
+ */
42
+ export function notifyTweakedChangeListeners(change: TweakedChange): void {
43
+ const listeners = changeListeners.get(change.object)
44
+ if (listeners) {
45
+ for (const listener of listeners) {
46
+ listener.applyChange(change)
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @internal
53
+ */
54
+ export function resyncTweakedChangeListeners(target: object): void {
55
+ const listeners = changeListeners.get(target)
56
+ if (listeners) {
57
+ for (const listener of listeners) {
58
+ listener.resync()
59
+ }
60
+ }
61
+ }
@@ -1,4 +1,3 @@
1
- import { type IArrayDidChange, type IObjectDidChange, remove, set } from "mobx"
2
1
  import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig"
3
2
  import type { AnyModel } from "../model/BaseModel"
4
3
  import { getModelMetadata } from "../model/getModelMetadata"
@@ -8,8 +7,9 @@ import { fastGetParent } from "../parent/path"
8
7
  import { internalApplyPatches } from "../patch/applyPatches"
9
8
  import type { InternalPatchRecorder } from "../patch/emitPatch"
10
9
  import { internalApplySnapshot } from "../snapshot/applySnapshot"
10
+ import { deferModelTypeCheckToBatch } from "./changeRollback"
11
11
  import { runWithoutSnapshotOrPatches } from "./core"
12
- import { isTypeCheckingAllowed, withoutTypeChecking } from "./withoutTypeChecking"
12
+ import { isTypeCheckingAllowed } from "./withoutTypeChecking"
13
13
 
14
14
  // Re-entrancy guard: when true, we are inside a type-check rollback.
15
15
  // During rollback, observe callbacks still fire, but we skip the auto
@@ -37,7 +37,7 @@ function isModelWithTypeChecker(obj: object): obj is AnyModel {
37
37
  * Walks up parent chain from `obj`, calling `callback` for each typed model
38
38
  * including the target itself (nearest-first / bottom-up). Stops if the callback throws.
39
39
  */
40
- function forEachTypedModelAncestor(obj: object, callback: (model: AnyModel) => void): void {
40
+ export function forEachTypedModelAncestor(obj: object, callback: (model: AnyModel) => void): void {
41
41
  // obj might be a $ data object, so resolve to the model if applicable
42
42
  const start = dataToModelNode(obj)
43
43
 
@@ -100,110 +100,11 @@ export function runTypeCheckingAfterChange(
100
100
  }
101
101
  }
102
102
 
103
- interface TypeCheckingBatch {
104
- models: Set<AnyModel>
105
- undo: (() => void)[]
106
- recording: boolean
107
- /** Last recorded change target, so a run of changes to it walks ancestors once. */
108
- previousTarget?: object
109
- }
110
-
111
- let typeCheckingBatch: TypeCheckingBatch | undefined
112
-
113
103
  /** @internal Called only when automatic type checking is enabled. */
114
104
  export function typeCheckAfterCreation(model: AnyModel): void {
115
- if (typeCheckingBatch?.recording) {
116
- typeCheckingBatch.models.add(model)
117
- } else {
105
+ // a change batch validates it once it completes
106
+ if (!deferModelTypeCheckToBatch(model)) {
118
107
  const error = model.typeCheck()
119
108
  if (error) error.throw()
120
109
  }
121
110
  }
122
-
123
- /**
124
- * @internal
125
- * Capture stored values before any listener can mutate them again. Restoring
126
- * live values preserves identity and literal data, without snapshot processors
127
- * or model defaults changing the state being restored.
128
- */
129
- export function recordTypeCheckingBatchChange(change: IObjectDidChange | IArrayDidChange): void {
130
- const batch = typeCheckingBatch
131
- if (!batch?.recording) return
132
- if (
133
- change.type === "splice" &&
134
- change.addedCount === change.removedCount &&
135
- change.added.every((value, index) => value === change.removed[index])
136
- )
137
- return
138
-
139
- const target = change.object
140
- if (target !== batch.previousTarget) {
141
- forEachTypedModelAncestor(target, (model) => batch.models.add(model))
142
- batch.previousTarget = target
143
- }
144
-
145
- if (change.type === "splice") {
146
- const { object, index, addedCount } = change
147
- const removed = change.removed.slice()
148
- batch.undo.push(() => object.spliceWithArray(index, addedCount, removed))
149
- } else if ("index" in change) {
150
- const { object, index, oldValue } = change
151
- batch.undo.push(() => {
152
- object[index] = oldValue
153
- })
154
- } else {
155
- const { object } = change
156
- const key = change.name as string
157
- if (change.type === "add") {
158
- batch.undo.push(() => remove(object, key))
159
- } else {
160
- const { oldValue } = change
161
- batch.undo.push(() => set(object, key, oldValue))
162
- }
163
- }
164
- }
165
-
166
- /**
167
- * @internal
168
- * Nested calls and synchronous listener edits join the same validation batch.
169
- * Rollback publishes compensating changes, just like snapshot reconciliation.
170
- */
171
- export function withTypeCheckingBatch(fn: () => void): void {
172
- if (typeCheckingBatch || !isTypeCheckingAfterChangeEnabled()) {
173
- fn()
174
- return
175
- }
176
-
177
- const batch: TypeCheckingBatch = { models: new Set(), undo: [], recording: true }
178
- typeCheckingBatch = batch
179
- let validating = false
180
- try {
181
- withoutTypeChecking(fn)
182
- // MobX has now invalidated dependencies, including added/removed keys.
183
- validating = true
184
- for (const model of batch.models) {
185
- const error = model.typeCheck()
186
- if (error) error.throw()
187
- }
188
- } catch (error) {
189
- // Only a rejected validation rolls back. Errors thrown by the batch body
190
- // itself (an invalid patch, a throwing listener) keep the same partial
191
- // application they produce while automatic type checking is disabled.
192
- if (validating) {
193
- batch.recording = false
194
- withoutTypeChecking(() => {
195
- for (let i = batch.undo.length - 1; i >= 0; i--) {
196
- try {
197
- batch.undo[i]()
198
- } catch {
199
- // Keep restoring after a rollback listener throws. The original
200
- // failure takes precedence over errors from compensating changes.
201
- }
202
- }
203
- })
204
- }
205
- throw error
206
- } finally {
207
- typeCheckingBatch = undefined
208
- }
209
- }
@@ -1,7 +1,7 @@
1
1
  import type { Path } from "../../parent/pathTypes"
2
2
  import { isArray } from "../../utils"
3
3
  import { withErrorPathSegment } from "../../utils/errorDiagnostics"
4
- import { createWholeContainerCachedCheck } from "../createCachedTypeCheck"
4
+ import { createChunkedArrayCachedCheck } from "../createCachedTypeCheck"
5
5
  import { getTypeInfo } from "../getTypeInfo"
6
6
  import { resolveStandardType, resolveTypeChecker } from "../resolveTypeChecker"
7
7
  import type { AnyStandardType, AnyType, ArrayType } from "../schemas"
@@ -38,18 +38,27 @@ export function typesArray<T extends AnyType>(itemType: T): ArrayType<T[]> {
38
38
  const getTypeName = (...recursiveTypeCheckers: TypeChecker[]) =>
39
39
  `Array<${itemChecker.getTypeName(...recursiveTypeCheckers, itemChecker)}>`
40
40
 
41
- // MobX observable arrays invalidate reads at collection granularity. Per-index
42
- // computeds would therefore all be invalidated by the same mutation while adding
43
- // a computed and cache entry per item.
44
- const checkArrayItems = createWholeContainerCachedCheck((array, path, typeCheckedValue) => {
45
- for (let index = 0; index < array.length; index++) {
46
- const error = itemChecker.check(array[index], emptyChildPath, typeCheckedValue)
47
- if (error) {
48
- return prependPathElementToTypeCheckError(error, path, index, typeCheckedValue)
41
+ const checkArrayItems = createChunkedArrayCachedCheck(
42
+ (items, firstIndex, path, typeCheckedValue) => {
43
+ for (let i = 0; i < items.length; i++) {
44
+ const error = itemChecker.check(items[i], emptyChildPath, typeCheckedValue)
45
+ if (error) {
46
+ return prependPathElementToTypeCheckError(error, path, firstIndex + i, typeCheckedValue)
47
+ }
49
48
  }
49
+ return null
50
50
  }
51
- return null
52
- })
51
+ )
52
+
53
+ const processorPlan = snapshotProcessorPlan(
54
+ () => [itemChecker],
55
+ ([itemProcessor]) =>
56
+ itemProcessor
57
+ ? (sn: unknown[]) => {
58
+ return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
59
+ }
60
+ : undefined
61
+ )
53
62
 
54
63
  const thisTc: TypeChecker = new TypeChecker(
55
64
  TypeCheckerBaseType.Array,
@@ -90,25 +99,9 @@ export function typesArray<T extends AnyType>(itemType: T): ArrayType<T[]> {
90
99
  return thisTc
91
100
  },
92
101
 
93
- snapshotProcessorPlan(
94
- () => [itemChecker],
95
- ([itemProcessor]) =>
96
- itemProcessor
97
- ? (sn: unknown[]) => {
98
- return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
99
- }
100
- : undefined
101
- ),
102
-
103
- snapshotProcessorPlan(
104
- () => [itemChecker],
105
- ([itemProcessor]) =>
106
- itemProcessor
107
- ? (sn: unknown[]) => {
108
- return sn.map((item, i) => withErrorPathSegment(i, () => itemProcessor(item)))
109
- }
110
- : undefined
111
- )
102
+ processorPlan,
103
+
104
+ processorPlan
112
105
  )
113
106
 
114
107
  return thisTc
@@ -68,6 +68,19 @@ export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
68
68
  // computeds cannot isolate tuple-item changes and only add allocation overhead.
69
69
  const checkTupleItems = createWholeContainerCachedCheck(checkTupleItemsImperatively)
70
70
 
71
+ const processorPlan = snapshotProcessorPlan(
72
+ () => checkers,
73
+ (processors) =>
74
+ processors.some(Boolean)
75
+ ? (array: unknown[]) => {
76
+ return array.map((item, i) => {
77
+ const processor = processors[i]
78
+ return processor ? withErrorPathSegment(i, () => processor(item)) : item
79
+ })
80
+ }
81
+ : undefined
82
+ )
83
+
71
84
  const thisTc: TypeChecker = new TypeChecker(
72
85
  TypeCheckerBaseType.Array,
73
86
 
@@ -102,31 +115,9 @@ export function typesTuple<T extends AnyType[]>(...itemTypes: T): ArrayType<T> {
102
115
  return thisTc
103
116
  },
104
117
 
105
- snapshotProcessorPlan(
106
- () => checkers,
107
- (processors) =>
108
- processors.some(Boolean)
109
- ? (array: unknown[]) => {
110
- return array.map((item, i) => {
111
- const processor = processors[i]
112
- return processor ? withErrorPathSegment(i, () => processor(item)) : item
113
- })
114
- }
115
- : undefined
116
- ),
117
-
118
- snapshotProcessorPlan(
119
- () => checkers,
120
- (processors) =>
121
- processors.some(Boolean)
122
- ? (array: unknown[]) => {
123
- return array.map((item, i) => {
124
- const processor = processors[i]
125
- return processor ? withErrorPathSegment(i, () => processor(item)) : item
126
- })
127
- }
128
- : undefined
129
- )
118
+ processorPlan,
119
+
120
+ processorPlan
130
121
  )
131
122
 
132
123
  return thisTc