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
@@ -4,28 +4,27 @@ import { modelIdKey, modelTypeKey } from "../model/metadata"
4
4
  import { getSnapshotModelType, isModel } from "../model/utils"
5
5
  import type { ModelClass } from "../modelShared/BaseModelShared"
6
6
  import { getModelInfoForName } from "../modelShared/modelInfo"
7
- import { dataObjectParent } from "../parent/core"
7
+ import { dataToModelNode } from "../parent/core"
8
8
  import {
9
9
  getDeepObjectChildren,
10
10
  registerDeepObjectChildrenExtension,
11
11
  } from "../parent/coreObjectChildren"
12
12
 
13
- function byModelTypeAndIdKey(modelType: string, modelId: string) {
14
- return JSON.stringify([modelType, modelId])
15
- }
13
+ type ModelsById = Map<string, AnyModel>
14
+ type ModelsByTypeAndId = Map<string, ModelsById>
16
15
 
17
16
  export class ModelPool {
18
- private pool: ReadonlyMap<string, AnyModel>
17
+ private pool: ReadonlyMap<string, ReadonlyMap<string, AnyModel>>
19
18
 
20
19
  constructor(root: object) {
21
20
  // make sure we don't use the sub-data $ object
22
- root = dataObjectParent.get(root) ?? root
21
+ root = dataToModelNode(root)
23
22
 
24
23
  this.pool = getDeepChildrenModels(getDeepObjectChildren(root))
25
24
  }
26
25
 
27
26
  findModelByTypeAndId(modelType: string, modelId: string | undefined): AnyModel | undefined {
28
- return modelId ? this.pool.get(byModelTypeAndIdKey(modelType, modelId)) : undefined
27
+ return modelId ? this.pool.get(modelType)?.get(modelId) : undefined
29
28
  }
30
29
 
31
30
  findModelForSnapshot(sn: any): AnyModel | undefined {
@@ -43,17 +42,33 @@ export class ModelPool {
43
42
  }
44
43
  }
45
44
 
46
- const getDeepChildrenModels = registerDeepObjectChildrenExtension<Map<string, AnyModel>>({
47
- initData() {
48
- return new Map()
49
- },
45
+ type GetDeepChildrenModels = (data: ReturnType<typeof getDeepObjectChildren>) => ModelsByTypeAndId
46
+
47
+ let getDeepChildrenModels: GetDeepChildrenModels = (data) => {
48
+ // Register on first use to keep ModelPool out of the coreObjectChildren
49
+ // module-initialization cycle. The extension getter backfills its index from
50
+ // data.deep, so registering after a deep-children set already exists is safe.
51
+ const getRegisteredDeepChildrenModels = registerDeepObjectChildrenExtension<ModelsByTypeAndId>({
52
+ initData() {
53
+ return new Map()
54
+ },
50
55
 
51
- addNode(node, data) {
52
- if (isModel(node)) {
53
- const id = node[modelIdKey]
54
- if (id) {
55
- data.set(byModelTypeAndIdKey(node[modelTypeKey], id), node)
56
+ addNode(node, extensionData) {
57
+ if (isModel(node)) {
58
+ const id = node[modelIdKey]
59
+ if (id) {
60
+ const modelType = node[modelTypeKey]
61
+ let modelsById = extensionData.get(modelType)
62
+ if (!modelsById) {
63
+ modelsById = new Map()
64
+ extensionData.set(modelType, modelsById)
65
+ }
66
+ modelsById.set(id, node)
67
+ }
56
68
  }
57
- }
58
- },
59
- })
69
+ },
70
+ })
71
+
72
+ getDeepChildrenModels = getRegisteredDeepChildrenModels
73
+ return getRegisteredDeepChildrenModels(data)
74
+ }
@@ -1,13 +1,59 @@
1
1
  import type { Path, PathElement, WritablePath } from "../parent/pathTypes"
2
2
 
3
- interface ErrorDiagnosticsContext {
4
- path: WritablePath
5
- modelTrail: string[]
3
+ /**
4
+ * @internal
5
+ */
6
+ export class ErrorDiagnosticsContext {
7
+ private readonly path: WritablePath = []
8
+ private readonly modelTrailTypes: string[] = []
9
+ private readonly modelTrailIds: (string | undefined)[] = []
10
+
11
+ pushPath(segment: PathElement): void {
12
+ this.path.push(segment)
13
+ }
14
+
15
+ popPath(): void {
16
+ this.path.pop()
17
+ }
18
+
19
+ getPathLength(): number {
20
+ return this.path.length
21
+ }
22
+
23
+ truncatePath(length: number): void {
24
+ this.path.length = length
25
+ }
26
+
27
+ pushModelTrail(modelType: string, modelId: string | undefined): void {
28
+ this.modelTrailTypes.push(modelType)
29
+ this.modelTrailIds.push(modelId)
30
+ }
31
+
32
+ popModelTrail(): void {
33
+ this.modelTrailTypes.pop()
34
+ this.modelTrailIds.pop()
35
+ }
36
+
37
+ getPathSnapshot(): Path {
38
+ return [...this.path]
39
+ }
40
+
41
+ getModelTrailSnapshot(): readonly string[] | undefined {
42
+ if (this.modelTrailTypes.length <= 0) {
43
+ return undefined
44
+ }
45
+ return this.modelTrailTypes.map((modelType, index) =>
46
+ formatErrorModelTrailEntry(modelType, this.modelTrailIds[index])
47
+ )
48
+ }
6
49
  }
7
50
 
8
51
  const errorDiagnosticsContextStack: ErrorDiagnosticsContext[] = []
9
52
 
10
- function getCurrentErrorDiagnosticsContext(): ErrorDiagnosticsContext | undefined {
53
+ /**
54
+ * @internal
55
+ */
56
+ export function getCurrentErrorDiagnosticsContext(): ErrorDiagnosticsContext | undefined {
11
57
  return errorDiagnosticsContextStack.length > 0
12
58
  ? errorDiagnosticsContextStack[errorDiagnosticsContextStack.length - 1]
13
59
  : undefined
@@ -22,10 +68,7 @@ export function runWithErrorDiagnosticsContext<T>(fn: () => T): T {
22
68
  return fn()
23
69
  }
24
70
 
25
- const ctx: ErrorDiagnosticsContext = {
26
- path: [],
27
- modelTrail: [],
28
- }
71
+ const ctx = new ErrorDiagnosticsContext()
29
72
  errorDiagnosticsContextStack.push(ctx)
30
73
  try {
31
74
  return fn()
@@ -43,11 +86,11 @@ export function withErrorPathSegment<T>(segment: PathElement, fn: () => T): T {
43
86
  return fn()
44
87
  }
45
88
 
46
- ctx.path.push(segment)
89
+ ctx.pushPath(segment)
47
90
  try {
48
91
  return fn()
49
92
  } finally {
50
- ctx.path.pop()
93
+ ctx.popPath()
51
94
  }
52
95
  }
53
96
 
@@ -64,16 +107,16 @@ export function withErrorPathSegments<T>(segments: readonly PathElement[], fn: (
64
107
  return fn()
65
108
  }
66
109
 
67
- const initialLen = ctx.path.length
110
+ const initialLen = ctx.getPathLength()
68
111
  const segmentsLen = segments.length
69
112
  for (let i = 0; i < segmentsLen; i++) {
70
- ctx.path.push(segments[i]!)
113
+ ctx.pushPath(segments[i]!)
71
114
  }
72
115
 
73
116
  try {
74
117
  return fn()
75
118
  } finally {
76
- ctx.path.length = initialLen
119
+ ctx.truncatePath(initialLen)
77
120
  }
78
121
  }
79
122
 
@@ -112,12 +155,11 @@ export function withErrorModelTrailEntry<T>(
112
155
  return fn()
113
156
  }
114
157
 
115
- const entry = formatErrorModelTrailEntry(modelType, modelId)
116
- ctx.modelTrail.push(entry)
158
+ ctx.pushModelTrail(modelType, modelId)
117
159
  try {
118
160
  return fn()
119
161
  } finally {
120
- ctx.modelTrail.pop()
162
+ ctx.popModelTrail()
121
163
  }
122
164
  }
123
165
 
@@ -126,7 +168,7 @@ export function withErrorModelTrailEntry<T>(
126
168
  */
127
169
  export function getErrorPathSnapshot(): Path | undefined {
128
170
  const ctx = getCurrentErrorDiagnosticsContext()
129
- return ctx ? [...ctx.path] : undefined
171
+ return ctx?.getPathSnapshot()
130
172
  }
131
173
 
132
174
  /**
@@ -134,7 +176,7 @@ export function getErrorPathSnapshot(): Path | undefined {
134
176
  */
135
177
  export function getErrorModelTrailSnapshot(): readonly string[] | undefined {
136
178
  const ctx = getCurrentErrorDiagnosticsContext()
137
- return ctx && ctx.modelTrail.length > 0 ? [...ctx.modelTrail] : undefined
179
+ return ctx?.getModelTrailSnapshot()
138
180
  }
139
181
 
140
182
  /**
@@ -109,12 +109,7 @@ export let hasOwnProp: (object: object, propName: PropertyKey) => boolean = (
109
109
  /**
110
110
  * @internal
111
111
  */
112
- export function setOwnProp(target: any, propName: PropertyKey, value: unknown): void {
113
- if (propName !== "__proto__") {
114
- target[propName] = value
115
- return
116
- }
117
-
112
+ export function setProtoProp(target: object, value: unknown): void {
118
113
  Object.defineProperty(target, "__proto__", {
119
114
  enumerable: true,
120
115
  writable: true,
@@ -136,7 +131,12 @@ export function copyOwnEnumerableProps<TTarget extends Record<string, any>>(
136
131
  for (let i = 0; i < len; i++) {
137
132
  const key = keys[i]
138
133
  const value = source[key]
139
- setOwnProp(target, key, mapValue ? mapValue(value, key) : value)
134
+ const mappedValue = mapValue ? mapValue(value, key) : value
135
+ if (key === "__proto__") {
136
+ setProtoProp(target, mappedValue)
137
+ } else {
138
+ ;(target as Record<PropertyKey, unknown>)[key] = mappedValue
139
+ }
140
140
  }
141
141
 
142
142
  return target
@@ -432,6 +432,8 @@ export function propNameToSetterName(propName: string): string {
432
432
  export function getMobxVersion(): number {
433
433
  if (mobx6.makeObservable!) {
434
434
  return 6
435
+ } else if ("extendShallowObservable" in mobx) {
436
+ return 4
435
437
  } else {
436
438
  return 5
437
439
  }
@@ -1,166 +0,0 @@
1
- import {
2
- computed,
3
- type IArrayDidChange,
4
- type IComputedValue,
5
- type IObjectDidChange,
6
- observe,
7
- } from "mobx"
8
- import type { Path } from "../parent/pathTypes"
9
- import { isTweakedObject } from "../tweaker/core"
10
- import type { TypeCheckError } from "./TypeCheckError"
11
-
12
- /**
13
- * @internal
14
- *
15
- * Per-entry cache map type used by `createPerEntryCachedCheck`.
16
- */
17
- export type PerEntryCache = Map<PropertyKey, IComputedValue<TypeCheckError | null>>
18
-
19
- /**
20
- * @internal
21
- *
22
- * Callback that sets up reactive pruning of stale per-entry cache entries.
23
- * Called once per observable value when the aggregation is first created.
24
- * Should install an observe listener that removes entries from the cache
25
- * when the collection structure changes (e.g. key removal, array shrink).
26
- */
27
- export type CachePruningSetup = (value: any, capturedEntries: PerEntryCache) => void
28
-
29
- /**
30
- * @internal
31
- *
32
- * Pruning strategy for record / plain-object types.
33
- * Removes a per-entry cached computed when an object key is removed.
34
- */
35
- export const recordCachePruning: CachePruningSetup = (value, capturedEntries) => {
36
- observe(value, (change: IObjectDidChange) => {
37
- if (change.type === "remove") {
38
- capturedEntries.delete(change.name)
39
- }
40
- })
41
- }
42
-
43
- /**
44
- * @internal
45
- *
46
- * Pruning strategy for array types.
47
- * After a splice that shrinks the array, removes per-entry cached computeds
48
- * for indices that no longer exist (i.e. index >= newLength).
49
- */
50
- export const arrayCachePruning: CachePruningSetup = (value, capturedEntries) => {
51
- observe(value, (change: IArrayDidChange) => {
52
- if (change.type === "splice") {
53
- const newLength = (value as unknown[]).length
54
- if (capturedEntries.size > newLength) {
55
- for (const key of capturedEntries.keys()) {
56
- if ((key as number) >= newLength) {
57
- capturedEntries.delete(key)
58
- }
59
- }
60
- }
61
- }
62
- })
63
- }
64
-
65
- /**
66
- * @internal
67
- *
68
- * Debug registry mapping observable values to their per-entry cache Maps.
69
- * Used by tests to verify that stale entries are actually pruned.
70
- */
71
- const __debugPerEntryCaches = new WeakMap<object, PerEntryCache>()
72
-
73
- /**
74
- * @internal
75
- *
76
- * Returns the number of entries in the per-entry cache for the given observable value,
77
- * or `undefined` if the value has no cached entries. Used by tests to verify pruning.
78
- */
79
- export function __getPerEntryCacheSize(value: object): number | undefined {
80
- return __debugPerEntryCaches.get(value)?.size
81
- }
82
-
83
- /**
84
- * @internal
85
- *
86
- * Creates a check function that uses per-entry MobX computeds + an aggregation computed
87
- * for fine-grained reactive type checking.
88
- *
89
- * For tweaked (observable) values:
90
- * 1. Per-entry computeds — each reads only one entry's observable, so changing entry A
91
- * only invalidates entry A's computed.
92
- * 2. Aggregation computed — iterates per-entry computeds. Thanks to MobX's change propagation,
93
- * if a per-entry computed re-evaluates but returns the same result (e.g. null → null),
94
- * the aggregation body is skipped entirely.
95
- * 3. Stale entries are pruned reactively via an optional `setupCachePruning` callback
96
- * (e.g. MobX `observe` listener) that fires on structural mutations, preventing
97
- * memory leaks for dynamic collections.
98
- *
99
- * For non-tweaked values (plain snapshots), falls back to imperative checking.
100
- *
101
- * @param iterateEntries Called both inside the aggregation computed (cached path) and
102
- * for the imperative fallback. Receives a `checkEntry(key)` callback that either
103
- * returns a cached computed result or performs a direct check, depending on the path.
104
- * Should call `checkEntry(key)` for each entry and return the first error, or null.
105
- * @param checkEntry Checks a single entry. For tweaked values, the utility wraps this
106
- * in a non-keepAlive computed automatically. For non-tweaked values, called directly.
107
- * @param setupCachePruning Optional callback to install reactive pruning of stale entries.
108
- * Called once per observable value. Use `recordCachePruning` for records/objects,
109
- * `arrayCachePruning` for arrays, or omit for fixed-schema types (objects, tuples).
110
- */
111
- export function createPerEntryCachedCheck<K extends PropertyKey>(
112
- iterateEntries: (
113
- value: any,
114
- checkEntry: (entryKey: K) => TypeCheckError | null
115
- ) => TypeCheckError | null,
116
- checkEntry: (value: any, entryKey: K, path: Path, typeCheckedValue: any) => TypeCheckError | null,
117
- setupCachePruning?: CachePruningSetup
118
- ): (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null {
119
- const perEntryCache = new WeakMap<object, PerEntryCache>()
120
- const aggregationCache = new WeakMap<object, IComputedValue<TypeCheckError | null>>()
121
-
122
- // Mutable call context updated before each `.get()` so computed closures
123
- // always use the latest path/typeCheckedValue. Safe because MobX computed
124
- // evaluation is synchronous and single-threaded.
125
- let currentPath: Path
126
- let currentTypeCheckedValue: any
127
-
128
- return (value: any, path: Path, typeCheckedValue: any): TypeCheckError | null => {
129
- if (!isTweakedObject(value, true)) {
130
- return iterateEntries(value, (key) => checkEntry(value, key, path, typeCheckedValue))
131
- }
132
-
133
- // Update mutable scope before evaluation.
134
- currentPath = path
135
- currentTypeCheckedValue = typeCheckedValue
136
-
137
- let aggr = aggregationCache.get(value)
138
- if (!aggr) {
139
- let entries = perEntryCache.get(value)
140
- if (!entries) {
141
- entries = new Map()
142
- perEntryCache.set(value, entries)
143
- __debugPerEntryCaches.set(value, entries)
144
- }
145
-
146
- const capturedEntries = entries
147
-
148
- // Install reactive pruning if a strategy was provided.
149
- setupCachePruning?.(value, capturedEntries)
150
-
151
- const cachedCheckEntry = (key: K): TypeCheckError | null => {
152
- let c = capturedEntries.get(key)
153
- if (!c) {
154
- c = computed(() => checkEntry(value, key, currentPath, currentTypeCheckedValue))
155
- capturedEntries.set(key, c)
156
- }
157
- return c.get()
158
- }
159
-
160
- aggr = computed(() => iterateEntries(value, cachedCheckEntry), { keepAlive: true })
161
- aggregationCache.set(value, aggr)
162
- }
163
-
164
- return aggr.get()
165
- }
166
- }