mobx-keystone 2.0.0 → 2.1.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 (53) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/mobx-keystone.esm.js +2341 -780
  3. package/dist/mobx-keystone.esm.mjs +2341 -780
  4. package/dist/mobx-keystone.umd.js +2346 -782
  5. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  6. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
  7. package/dist/types/parent/arrayPaths.d.ts +1 -0
  8. package/dist/types/parent/treeOrder.d.ts +1 -0
  9. package/dist/types/patch/applyPatches.d.ts +2 -1
  10. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  11. package/dist/types/ref/nodesById.d.ts +1 -0
  12. package/dist/types/ref/refIdIndex.d.ts +1 -0
  13. package/dist/types/snapshot/applySnapshot.d.ts +1 -0
  14. package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
  15. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  16. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  17. package/dist/types/utils/ModelPool.d.ts +7 -1
  18. package/dist/types/utils/chunks.d.ts +1 -0
  19. package/package.json +2 -2
  20. package/src/actionMiddlewares/transactionMiddleware.ts +40 -41
  21. package/src/actionMiddlewares/undoMiddleware.ts +47 -13
  22. package/src/model/BaseModel.ts +6 -4
  23. package/src/modelShared/modelDecorator.ts +5 -25
  24. package/src/parent/arrayPaths.ts +242 -0
  25. package/src/parent/coreObjectChildren.ts +300 -48
  26. package/src/parent/onChildAttachedTo.ts +130 -44
  27. package/src/parent/path.ts +10 -2
  28. package/src/parent/setParent.ts +27 -15
  29. package/src/parent/treeOrder.ts +53 -0
  30. package/src/parent/walkTree.ts +0 -73
  31. package/src/patch/applyPatches.ts +31 -26
  32. package/src/ref/core.ts +63 -27
  33. package/src/ref/modelRefIdIndex.ts +131 -0
  34. package/src/ref/nodesById.ts +60 -0
  35. package/src/ref/refIdIndex.ts +209 -0
  36. package/src/snapshot/applySnapshot.ts +26 -18
  37. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  38. package/src/snapshot/fromSnapshot.ts +2 -6
  39. package/src/snapshot/internal.ts +67 -10
  40. package/src/tweaker/changeRollback.ts +218 -0
  41. package/src/tweaker/core.ts +33 -20
  42. package/src/tweaker/treeNodeMetadata.ts +12 -2
  43. package/src/tweaker/tweak.ts +181 -18
  44. package/src/tweaker/tweakArray.ts +107 -27
  45. package/src/tweaker/tweakPlainObject.ts +56 -31
  46. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  47. package/src/tweaker/typeChecking.ts +5 -104
  48. package/src/types/arrayBased/typesArray.ts +10 -11
  49. package/src/types/createCachedTypeCheck.ts +522 -94
  50. package/src/types/objectBased/typesRecord.ts +2 -5
  51. package/src/utils/ModelPool.ts +75 -19
  52. package/src/utils/chunks.ts +271 -0
  53. package/src/utils/index.ts +37 -0
@@ -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,17 @@ 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
+ )
53
52
 
54
53
  const thisTc: TypeChecker = new TypeChecker(
55
54
  TypeCheckerBaseType.Array,