mobx-keystone 2.1.0 → 2.3.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 (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +555 -516
  4. package/dist/mobx-keystone.esm.mjs +555 -516
  5. package/dist/mobx-keystone.umd.js +558 -519
  6. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +2 -10
  7. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  8. package/dist/types/modelShared/prop.d.ts +42 -29
  9. package/dist/types/ref/Ref.d.ts +1 -4
  10. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  11. package/dist/types/snapshot/applySnapshot.d.ts +1 -1
  12. package/dist/types/snapshot/fromSnapshot.d.ts +2 -2
  13. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  14. package/dist/types/types/SnapshotTypeMismatchError.d.ts +3 -1
  15. package/dist/types/types/index.d.ts +1 -0
  16. package/dist/types/types/modelTypeMatching.d.ts +1 -0
  17. package/dist/types/types/schemas.d.ts +41 -37
  18. package/dist/types/types/tProp.d.ts +12 -6
  19. package/dist/types/types/types.d.ts +6 -6
  20. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  21. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  22. package/dist/types/utils/ModelPool.d.ts +0 -1
  23. package/dist/types/utils/types.d.ts +6 -0
  24. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  25. package/package.json +13 -11
  26. package/src/action/modelFlow.ts +35 -66
  27. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  28. package/src/actionMiddlewares/transactionMiddleware.ts +1 -1
  29. package/src/actionMiddlewares/undoMiddleware.ts +16 -22
  30. package/src/context/context.ts +15 -36
  31. package/src/dataModel/utils.ts +5 -28
  32. package/src/deepChange/onDeepChange.ts +44 -126
  33. package/src/model/BaseModel.ts +1 -1
  34. package/src/model/utils.ts +5 -10
  35. package/src/modelShared/modelDecorator.ts +13 -10
  36. package/src/modelShared/modelInfo.ts +75 -0
  37. package/src/modelShared/prop.ts +115 -111
  38. package/src/modelShared/sharedInternalModel.ts +2 -2
  39. package/src/parent/arrayPaths.ts +1 -4
  40. package/src/parent/path.ts +0 -10
  41. package/src/patch/patchRecorder.ts +13 -28
  42. package/src/ref/core.ts +6 -19
  43. package/src/rootStore/rootStore.ts +1 -3
  44. package/src/snapshot/SnapshotOf.ts +24 -48
  45. package/src/snapshot/applySnapshot.ts +1 -1
  46. package/src/snapshot/fromSnapshot.ts +10 -3
  47. package/src/snapshot/getSnapshot.ts +1 -1
  48. package/src/snapshot/internal.ts +0 -2
  49. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  50. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  51. package/src/treeUtils/draft.ts +6 -1
  52. package/src/treeUtils/sandbox.ts +10 -7
  53. package/src/tweaker/tweakArray.ts +20 -50
  54. package/src/tweaker/tweakPlainObject.ts +9 -15
  55. package/src/types/SnapshotTypeMismatchError.ts +3 -1
  56. package/src/types/arrayBased/typesArray.ts +13 -19
  57. package/src/types/arrayBased/typesTuple.ts +16 -25
  58. package/src/types/createCachedTypeCheck.ts +17 -30
  59. package/src/types/index.ts +1 -0
  60. package/src/types/modelTypeMatching.ts +276 -0
  61. package/src/types/objectBased/typesDataModelData.ts +7 -8
  62. package/src/types/objectBased/typesModel.ts +23 -6
  63. package/src/types/objectBased/typesObject.ts +11 -15
  64. package/src/types/objectBased/typesRecord.ts +11 -15
  65. package/src/types/objectBased/typesRef.ts +23 -4
  66. package/src/types/schemas.ts +59 -71
  67. package/src/types/tProp.ts +29 -29
  68. package/src/types/utility/typeToStoredData.ts +22 -21
  69. package/src/types/utility/typesCodecCore.ts +8 -1
  70. package/src/types/utility/typesCodecSupport.ts +16 -10
  71. package/src/types/utility/typesMap.ts +43 -51
  72. package/src/types/utility/typesOr.ts +38 -8
  73. package/src/types/utility/typesRefinement.ts +7 -8
  74. package/src/types/utility/typesSkipCheck.ts +7 -8
  75. package/src/types/utility/typesTag.ts +7 -8
  76. package/src/utils/ModelPool.ts +1 -18
  77. package/src/utils/decorators.ts +5 -5
  78. package/src/utils/index.ts +1 -20
  79. package/src/utils/types.ts +5 -0
  80. package/src/wrappers/ArraySet.ts +27 -14
  81. package/src/wrappers/ObjectMap.ts +1 -6
  82. package/src/wrappers/asMap.ts +1 -21
  83. package/src/wrappers/asSet.ts +0 -8
@@ -209,21 +209,9 @@ function changeWithPath(change: DeepChange, path: Path): DeepChange {
209
209
  * @internal
210
210
  */
211
211
  export function emitDeepChange(obj: object, change: DeepChange): void {
212
- if (!hasAnyDeepChangeListeners()) {
213
- return
212
+ if (hasAnyDeepChangeListeners()) {
213
+ emitLazyDeepChange(obj, () => change)
214
214
  }
215
-
216
- const emission = beginDeepChangeEmission()
217
- try {
218
- attachEmission(change)
219
- emitGlobalDeepChange(obj, change)
220
- if (deepChangeListenerCount > 0) {
221
- emitDeepChangeToListeners(obj, change)
222
- }
223
- } finally {
224
- currentEmission = emission.parent
225
- }
226
- rethrowDeepChangeListenerError(emission)
227
215
  }
228
216
 
229
217
  /**
@@ -308,98 +296,52 @@ function emitDeepChangeFromValues(
308
296
  addedValues?: unknown[],
309
297
  removedValues?: unknown[]
310
298
  ): void {
311
- if (!hasAnyDeepChangeListeners()) {
312
- return
313
- }
314
-
315
- const emission = beginDeepChangeEmission()
316
- try {
317
- emitDeepChangeFromValuesInEmission(
318
- node,
319
- type,
320
- target,
321
- keyOrIndex,
322
- newValue,
323
- oldValue,
324
- addedValues,
325
- removedValues
299
+ if (hasAnyDeepChangeListeners()) {
300
+ emitLazyDeepChange(node, () =>
301
+ buildDeepChange(type, target, keyOrIndex, newValue, oldValue, addedValues, removedValues)
326
302
  )
327
- } finally {
328
- currentEmission = emission.parent
329
303
  }
330
- rethrowDeepChangeListenerError(emission)
331
304
  }
332
305
 
333
- function emitDeepChangeFromValuesInEmission(
334
- node: object,
335
- type: DeepChangeType,
336
- target: object | unknown[],
337
- keyOrIndex: string | number,
338
- newValue: unknown,
339
- oldValue: unknown,
340
- addedValues?: unknown[],
341
- removedValues?: unknown[]
342
- ): void {
343
- let change: DeepChange | undefined
344
-
345
- if (globalDeepChangeListeners.length > 0) {
346
- change = createDeepChange(
347
- type,
348
- target,
349
- keyOrIndex,
350
- newValue,
351
- oldValue,
352
- addedValues,
353
- removedValues
354
- )
355
- emitGlobalDeepChange(node, change)
356
- }
357
-
358
- if (deepChangeListenerCount === 0) {
359
- return
360
- }
306
+ /**
307
+ * Delivers a change to the global listeners and to the listeners of the node
308
+ * and its ancestors. The change is only created if some listener gets it.
309
+ */
310
+ function emitLazyDeepChange(node: object, createChange: () => DeepChange): void {
311
+ const emission = beginDeepChangeEmission()
312
+ try {
313
+ let change: DeepChange | undefined
361
314
 
362
- // Segments are pushed in child-to-root order (O(1) each) rather than unshifted
363
- // at the front (O(depth) each), keeping the whole walk O(depth) instead of
364
- // O(depth^2) on deep trees.
365
- const reversedPrefix: WritablePath = []
366
- let current: object | undefined = node
367
- while (current) {
368
- const listenersForObject = deepChangeListeners.get(current)
369
- if (listenersForObject && listenersForObject.length > 0) {
370
- change ??= createDeepChange(
371
- type,
372
- target,
373
- keyOrIndex,
374
- newValue,
375
- oldValue,
376
- addedValues,
377
- removedValues
378
- )
379
- emitDeepChangeForTarget(current, change, reversedPrefix, reversedPrefix.length)
315
+ if (globalDeepChangeListeners.length > 0) {
316
+ change = attachEmission(createChange())
317
+ emitGlobalDeepChange(node, change)
380
318
  }
381
319
 
382
- const parentPath: ParentPath<object> | undefined = fastGetParentPath(current, false)
383
- if (!parentPath) {
384
- break
320
+ if (deepChangeListenerCount > 0) {
321
+ // Segments are pushed in child-to-root order (O(1) each) rather than unshifted
322
+ // at the front (O(depth) each), keeping the whole walk O(depth) instead of
323
+ // O(depth^2) on deep trees.
324
+ const reversedPrefix: WritablePath = []
325
+ let current: object | undefined = node
326
+ while (current) {
327
+ const listenersForObject = deepChangeListeners.get(current)
328
+ if (listenersForObject && listenersForObject.length > 0) {
329
+ change ??= attachEmission(createChange())
330
+ emitDeepChangeForTarget(listenersForObject, change, reversedPrefix)
331
+ }
332
+
333
+ const parentPath: ParentPath<object> | undefined = fastGetParentPath(current, false)
334
+ if (!parentPath) {
335
+ break
336
+ }
337
+ reversedPrefix.push(parentPath.path)
338
+ current = parentPath.parent
339
+ }
385
340
  }
386
- reversedPrefix.push(parentPath.path)
387
- current = parentPath.parent
341
+ } finally {
342
+ currentEmission = emission.parent
388
343
  }
389
- }
390
-
391
- function createDeepChange(
392
- type: DeepChangeType,
393
- target: object | unknown[],
394
- keyOrIndex: string | number,
395
- newValue: unknown,
396
- oldValue: unknown,
397
- addedValues: unknown[] | undefined,
398
- removedValues: unknown[] | undefined
399
- ): DeepChange {
400
- return attachEmission(
401
- buildDeepChange(type, target, keyOrIndex, newValue, oldValue, addedValues, removedValues)
402
- )
344
+ rethrowDeepChangeListenerError(emission)
403
345
  }
404
346
 
405
347
  function buildDeepChange(
@@ -489,39 +431,15 @@ function rememberDeepChangeListenerError(error: unknown): void {
489
431
  emission.error = addDelayedError(emission.error, error, "multiple deep change listeners failed")
490
432
  }
491
433
 
492
- function emitDeepChangeToListeners(obj: object, change: DeepChange): void {
493
- // Segments are pushed in child-to-root order (O(1) each) rather than unshifted
494
- // at the front (O(depth) each), keeping the whole walk O(depth) instead of
495
- // O(depth^2) on deep trees.
496
- const reversedPrefix: WritablePath = []
497
-
498
- emitDeepChangeForTarget(obj, change, reversedPrefix, 0)
499
-
500
- // and also emit subtree listeners all the way to the root
501
- let parentPath = fastGetParentPath(obj, false)
502
- while (parentPath) {
503
- reversedPrefix.push(parentPath.path)
504
- emitDeepChangeForTarget(parentPath.parent, change, reversedPrefix, reversedPrefix.length)
505
-
506
- parentPath = fastGetParentPath(parentPath.parent, false)
507
- }
508
- }
509
-
510
434
  // `reversedPrefix` holds the path segments from the changed node up to (but not
511
- // including) `obj`, in child-to-root order; the emitted path prepends that slice
512
- // reversed (root-to-child). `prefixCount` is how many leading entries apply.
435
+ // including) the listened node, in child-to-root order; the emitted path
436
+ // prepends it reversed (root-to-child).
513
437
  function emitDeepChangeForTarget(
514
- obj: object,
438
+ listenersForObject: readonly ListenerSubscription<OnDeepChangeListener>[],
515
439
  change: DeepChange,
516
- reversedPrefix: readonly PathElement[],
517
- prefixCount: number
440
+ reversedPrefix: readonly PathElement[]
518
441
  ): void {
519
- const listenersForObject = deepChangeListeners.get(obj)
520
-
521
- if (!listenersForObject || listenersForObject.length === 0) {
522
- return
523
- }
524
-
442
+ const prefixCount = reversedPrefix.length
525
443
  const prefixedChange =
526
444
  prefixCount > 0
527
445
  ? changeWithPath(change, buildPrefixedPath(reversedPrefix, prefixCount, change.path))
@@ -89,7 +89,7 @@ export abstract class BaseModel<
89
89
  * By default it will use the `idProp` if available or return `undefined` otherwise.
90
90
  */
91
91
  getRefId(): string | undefined {
92
- return this[modelIdKey]
92
+ return this[modelIdKey] as string | undefined
93
93
  }
94
94
 
95
95
  /**
@@ -39,15 +39,10 @@ export function assertIsModel(
39
39
  * @internal
40
40
  */
41
41
  export function isModelClass(modelClass: unknown): modelClass is ModelClass<AnyModel> {
42
- if (typeof modelClass !== "function") {
43
- return false
44
- }
45
-
46
- if (modelClass !== BaseModel && !(modelClass.prototype instanceof BaseModel)) {
47
- return false
48
- }
49
-
50
- return true
42
+ return (
43
+ typeof modelClass === "function" &&
44
+ (modelClass === BaseModel || modelClass.prototype instanceof BaseModel)
45
+ )
51
46
  }
52
47
 
53
48
  /**
@@ -61,7 +56,7 @@ export function assertIsModelClass(
61
56
  throw failure(`${argName} must be a class`)
62
57
  }
63
58
 
64
- if (modelClass !== BaseModel && !(modelClass.prototype instanceof BaseModel)) {
59
+ if (!isModelClass(modelClass)) {
65
60
  throw failure(`${argName} must extend Model`)
66
61
  }
67
62
  }
@@ -9,7 +9,7 @@ import type { AnyModel } from "../model/BaseModel"
9
9
  import { modelTypeKey } from "../model/metadata"
10
10
  import { isModelClass } from "../model/utils"
11
11
  import { type ModelClass, modelInitializedSymbol } from "../modelShared/BaseModelShared"
12
- import { modelInfoByClass, modelInfoByName } from "../modelShared/modelInfo"
12
+ import { modelInfoByClass, modelInfoByName, setModelInfo } from "../modelShared/modelInfo"
13
13
  import {
14
14
  modelUnwrappedClassSymbol,
15
15
  runAfterModelDecoratorSymbol,
@@ -37,11 +37,17 @@ import type { AnyFunction } from "../utils/AnyFunction"
37
37
  */
38
38
  export const model =
39
39
  (name: string) =>
40
- <MC extends ModelClass<AnyModel | AnyDataModel>>(
41
- clazz: MC,
42
- _context?: ClassDecoratorContext<MC>
43
- ): MC =>
44
- internalModel(name, clazz) as any
40
+ <MC extends ModelDecoratorTarget>(clazz: MC, _context?: ClassDecoratorContext<MC>): MC =>
41
+ internalModel(name, clazz as unknown as ModelClass<AnyModel | AnyDataModel>) as any
42
+
43
+ // the model instance type is checked through `prototype` rather than through a typed constructor
44
+ // signature, since comparing constructor signatures would compute the (expensive) model creation data
45
+ // type of every decorated class
46
+ type ModelDecoratorTarget = (new (
47
+ ...args: any
48
+ ) => any) & {
49
+ prototype: AnyModel | AnyDataModel
50
+ }
45
51
 
46
52
  interface AfterClassInitializationData {
47
53
  needsMakeObservable: boolean | undefined
@@ -161,10 +167,7 @@ const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>(
161
167
  class: decoratedClass,
162
168
  }
163
169
 
164
- modelInfoByName[name] = modelInfo
165
-
166
- modelInfoByClass.set(decoratedClass, modelInfo)
167
- modelInfoByClass.set(clazz, modelInfo)
170
+ setModelInfo(modelInfo, clazz)
168
171
 
169
172
  runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol)
170
173
 
@@ -22,6 +22,81 @@ export const modelInfoByName: {
22
22
  */
23
23
  export const modelInfoByClass = new WeakMap<ModelClass<AnyModel | AnyDataModel>, ModelInfo>()
24
24
 
25
+ // registered model prototypes (looked up rather than reading `constructor`, since that might be
26
+ // a model prop)
27
+ const modelInfoByPrototype = new WeakMap<object, ModelInfo>()
28
+
29
+ // for each registered model, how many registered models below each of its base models
30
+ // (including itself, at 0) it is, by name
31
+ const ancestorDistancesByModelInfo = new WeakMap<ModelInfo, ReadonlyMap<string, number>>()
32
+ // the same, by name for the current registration of each model name
33
+ const ancestorDistancesByModelName = new Map<string, ReadonlyMap<string, number>>()
34
+
35
+ /**
36
+ * Finds the info of the first registered model class in a prototype chain.
37
+ *
38
+ * @internal
39
+ */
40
+ export function findModelInfoForPrototype(proto: object | null): ModelInfo | undefined {
41
+ for (let p = proto; p; p = Object.getPrototypeOf(p)) {
42
+ const modelInfo = modelInfoByPrototype.get(p)
43
+ if (modelInfo) {
44
+ return modelInfo
45
+ }
46
+ }
47
+ return undefined
48
+ }
49
+
50
+ /**
51
+ * Registers a model.
52
+ *
53
+ * @internal
54
+ */
55
+ export function setModelInfo(modelInfo: ModelInfo, unwrappedClass: ModelClass<any>): void {
56
+ // the base classes were registered before, so their distances are already known; names are
57
+ // used rather than classes since with hot reloading a subclass might still extend a previous
58
+ // registration of a model
59
+ const baseModelInfo = findModelInfoForPrototype(Object.getPrototypeOf(unwrappedClass.prototype))
60
+ const ancestorDistances = new Map<string, number>()
61
+ baseModelInfo &&
62
+ ancestorDistancesByModelInfo.get(baseModelInfo)?.forEach((distance, name) => {
63
+ ancestorDistances.set(name, distance + 1)
64
+ })
65
+ ancestorDistances.set(modelInfo.name, 0)
66
+ ancestorDistancesByModelInfo.set(modelInfo, ancestorDistances)
67
+ ancestorDistancesByModelName.set(modelInfo.name, ancestorDistances)
68
+
69
+ modelInfoByName[modelInfo.name] = modelInfo
70
+ modelInfoByClass.set(modelInfo.class, modelInfo)
71
+ modelInfoByClass.set(unwrappedClass, modelInfo)
72
+ modelInfoByPrototype.set(unwrappedClass.prototype, modelInfo)
73
+ }
74
+
75
+ /**
76
+ * How many registered models a model is below a base model (0 if it is that model), or -1 if
77
+ * it does not extend it.
78
+ *
79
+ * @internal
80
+ */
81
+ export function getModelSubclassDistance(modelInfo: ModelInfo, baseModelName: string): number {
82
+ return ancestorDistancesByModelInfo.get(modelInfo)?.get(baseModelName) ?? -1
83
+ }
84
+
85
+ /**
86
+ * How many registered models the model currently registered with a name is below a base model
87
+ * (0 if it is that model), -1 if it does not extend it, or `undefined` if the name is not
88
+ * registered.
89
+ *
90
+ * @internal
91
+ */
92
+ export function getModelSubclassDistanceForName(
93
+ modelName: string,
94
+ baseModelName: string
95
+ ): number | undefined {
96
+ const ancestorDistances = ancestorDistancesByModelName.get(modelName)
97
+ return ancestorDistances ? (ancestorDistances.get(baseModelName) ?? -1) : undefined
98
+ }
99
+
25
100
  /**
26
101
  * Returns the model info for a model type name, or `undefined` if not found.
27
102
  *