mobx-keystone 1.24.1 → 2.0.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 (122) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +5 -1
  3. package/dist/mobx-keystone.esm.js +1608 -1072
  4. package/dist/mobx-keystone.esm.mjs +1608 -1072
  5. package/dist/mobx-keystone.umd.js +1613 -1072
  6. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  7. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  8. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  9. package/dist/types/parent/detach.d.ts +1 -1
  10. package/dist/types/parent/findChildren.d.ts +1 -1
  11. package/dist/types/patch/applyPatches.d.ts +2 -0
  12. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  13. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  14. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  15. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  16. package/dist/types/types/TypeCheckError.d.ts +1 -0
  17. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  18. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  19. package/dist/types/utils/index.d.ts +13 -0
  20. package/dist/types/utils/mapUtils.d.ts +2 -2
  21. package/dist/types/utils/setLike.d.ts +1 -0
  22. package/package.json +7 -7
  23. package/src/action/applyAction.ts +1 -1
  24. package/src/action/middleware.ts +17 -36
  25. package/src/action/modelFlow.ts +4 -5
  26. package/src/action/mutationBatch.ts +10 -10
  27. package/src/action/pendingActions.ts +3 -4
  28. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  29. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  30. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  31. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  32. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  33. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  34. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  35. package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
  36. package/src/actionMiddlewares/undoMiddleware.ts +76 -60
  37. package/src/actionMiddlewares/utils.ts +2 -1
  38. package/src/computedTree/computedTree.ts +14 -3
  39. package/src/context/context.ts +17 -45
  40. package/src/dataModel/BaseDataModel.ts +20 -16
  41. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  42. package/src/deepChange/onDeepChange.ts +179 -24
  43. package/src/frozen/Frozen.ts +50 -33
  44. package/src/model/BaseModel.ts +15 -14
  45. package/src/model/newModel.ts +32 -28
  46. package/src/model/utils.ts +2 -2
  47. package/src/modelShared/modelClassInitializer.ts +3 -2
  48. package/src/modelShared/modelDecorator.ts +34 -14
  49. package/src/modelShared/modelInfo.ts +1 -1
  50. package/src/modelShared/prop.ts +6 -9
  51. package/src/modelShared/sharedInternalModel.ts +16 -3
  52. package/src/parent/detach.ts +1 -1
  53. package/src/parent/findChildren.ts +4 -7
  54. package/src/parent/findParent.ts +14 -9
  55. package/src/parent/onChildAttachedTo.ts +45 -40
  56. package/src/parent/path.ts +11 -7
  57. package/src/parent/setParent.ts +33 -14
  58. package/src/parent/walkTree.ts +21 -37
  59. package/src/patch/applyPatches.ts +75 -60
  60. package/src/patch/emitPatch.ts +142 -26
  61. package/src/patch/jsonPatch.ts +5 -0
  62. package/src/patch/patchRecorder.ts +18 -14
  63. package/src/redux/connectReduxDevTools.ts +24 -18
  64. package/src/redux/redux.ts +5 -21
  65. package/src/ref/core.ts +18 -2
  66. package/src/rootStore/attachDetach.ts +27 -16
  67. package/src/rootStore/rootStore.ts +10 -6
  68. package/src/snapshot/SnapshotOf.ts +2 -2
  69. package/src/snapshot/applySnapshot.ts +10 -1
  70. package/src/snapshot/clone.ts +1 -2
  71. package/src/snapshot/cloneTreeValue.ts +1 -2
  72. package/src/snapshot/fromModelSnapshot.ts +6 -2
  73. package/src/snapshot/fromSnapshot.ts +3 -1
  74. package/src/snapshot/internal.ts +22 -9
  75. package/src/snapshot/modelInitialData.ts +14 -19
  76. package/src/snapshot/onSnapshot.ts +12 -1
  77. package/src/snapshot/reconcileModelSnapshot.ts +16 -8
  78. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  79. package/src/snapshot/reconcileSnapshot.ts +13 -4
  80. package/src/standardActions/arrayActions.ts +12 -2
  81. package/src/standardActions/objectActions.ts +3 -2
  82. package/src/transforms/ImmutableDate.ts +4 -0
  83. package/src/transforms/asMap.ts +3 -1
  84. package/src/treeUtils/deepEquals.ts +15 -13
  85. package/src/treeUtils/draft.ts +8 -0
  86. package/src/treeUtils/sandbox.ts +31 -27
  87. package/src/tweaker/core.ts +2 -4
  88. package/src/tweaker/tweak.ts +8 -8
  89. package/src/tweaker/tweakArray.ts +86 -37
  90. package/src/tweaker/tweakFrozen.ts +0 -1
  91. package/src/tweaker/tweakModel.ts +0 -1
  92. package/src/tweaker/tweakPlainObject.ts +78 -29
  93. package/src/tweaker/typeChecking.ts +132 -33
  94. package/src/types/TypeCheckError.ts +17 -19
  95. package/src/types/createCachedTypeCheck.ts +27 -2
  96. package/src/types/objectBased/typesDataModelData.ts +4 -2
  97. package/src/types/objectBased/typesModel.ts +4 -2
  98. package/src/types/objectBased/typesObject.ts +20 -16
  99. package/src/types/objectBased/typesRecord.ts +6 -13
  100. package/src/types/objectBased/typesRef.ts +2 -2
  101. package/src/types/primitiveBased/typesEnum.ts +4 -2
  102. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  103. package/src/types/typeCheckErrorUtils.ts +1 -0
  104. package/src/types/utility/typesCodecSupport.ts +97 -56
  105. package/src/types/utility/typesMap.ts +24 -41
  106. package/src/types/utility/typesRefinement.ts +3 -2
  107. package/src/types/utility/typesSet.ts +24 -0
  108. package/src/utils/ModelPool.ts +3 -3
  109. package/src/utils/chainFns.ts +4 -0
  110. package/src/utils/decorators.ts +1 -1
  111. package/src/utils/errorDiagnostics.ts +6 -2
  112. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  113. package/src/utils/index.ts +34 -2
  114. package/src/utils/mapUtils.ts +3 -7
  115. package/src/utils/setIfDifferent.ts +3 -2
  116. package/src/utils/setLike.ts +134 -0
  117. package/src/wrappers/ArraySet.ts +5 -9
  118. package/src/wrappers/ObjectMap.ts +15 -11
  119. package/src/wrappers/asMap.ts +30 -18
  120. package/src/wrappers/asSet.ts +36 -12
  121. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  122. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -1,6 +1,7 @@
1
1
  import { HookAction } from "../action/hookActions"
2
2
  import { wrapModelMethodInActionIfNeeded } from "../action/wrapInAction"
3
3
  import type { AnyDataModel } from "../dataModel/BaseDataModel"
4
+ import { dataModelInstanceCache } from "../dataModel/dataModelInstanceCache"
4
5
  import { isDataModelClass } from "../dataModel/utils"
5
6
  import { enterInitPhase, exitInitPhase } from "../deepChange/onDeepChange"
6
7
  import { getGlobalConfig } from "../globalConfig"
@@ -41,22 +42,19 @@ export const model =
41
42
  ): MC =>
42
43
  internalModel(name, clazz) as any
43
44
 
45
+ interface AfterClassInitializationData {
46
+ needsMakeObservable: boolean | undefined
47
+ type: "class" | "data"
48
+ }
49
+
44
50
  const afterClassInitializationData = new WeakMap<
45
51
  ModelClass<AnyModel | AnyDataModel>,
46
- {
47
- needsMakeObservable: boolean | undefined
48
- type: "class" | "data"
49
- }
52
+ AfterClassInitializationData
50
53
  >()
51
54
 
52
- const runAfterClassInitialization = (
53
- target: ModelClass<AnyModel | AnyDataModel>,
54
- instance: any
55
- ) => {
55
+ const runAfterClassInitialization = (tag: AfterClassInitializationData, instance: any) => {
56
56
  runLateInitializationFunctions(instance, runAfterNewSymbol)
57
57
 
58
- const tag = afterClassInitializationData.get(target)!
59
-
60
58
  // compatibility with mobx 6 and 7
61
59
  if (tag.needsMakeObservable) {
62
60
  // we know it can be done and shouldn't fail
@@ -112,11 +110,33 @@ const runAfterClassInitialization = (
112
110
 
113
111
  const proxyClassHandler: ProxyHandler<ModelClass<AnyModel | AnyDataModel>> = {
114
112
  construct(clazz, args) {
115
- const instance = new (clazz as any)(...args)
113
+ const tag = afterClassInitializationData.get(clazz)!
116
114
 
117
- runAfterClassInitialization(clazz, instance)
115
+ if (tag.type !== "data") {
116
+ const instance = new (clazz as any)(...args)
117
+ runAfterClassInitialization(tag, instance)
118
+ return instance
119
+ }
120
+
121
+ // Data models are cached by their backing data, so constructing one over data that
122
+ // already has an instance must not rerun its class fields nor its lazy initializer.
123
+ const modelClass = args[1]?.modelClass ?? modelInfoByClass.get(clazz)!.class
124
+ const cached = dataModelInstanceCache.get(modelClass)?.get(args[0])
125
+ if (cached) {
126
+ return cached
127
+ }
118
128
 
119
- return instance
129
+ let instance: any
130
+ try {
131
+ instance = new (clazz as any)(...args)
132
+ runAfterClassInitialization(tag, instance)
133
+ return instance
134
+ } catch (error) {
135
+ // The base constructor cleans up after itself, but class fields and late
136
+ // initialization run after it returns, so discard those instances too.
137
+ dataModelInstanceCache.get(modelClass)?.delete(instance ? instance.$ : args[0])
138
+ throw error
139
+ }
120
140
  },
121
141
  }
122
142
 
@@ -245,7 +265,7 @@ export function decoratedModel<M, MC extends abstract new (...ags: any) => M>(
245
265
  })
246
266
  }
247
267
 
248
- return (name ? model(name)(clazz as unknown as ModelClass<AnyModel>) : clazz) as MC
268
+ return (name !== undefined ? model(name)(clazz as unknown as ModelClass<AnyModel>) : clazz) as MC
249
269
  }
250
270
 
251
271
  type MobxAnnotation = {
@@ -15,7 +15,7 @@ export interface ModelInfo {
15
15
  */
16
16
  export const modelInfoByName: {
17
17
  [name: string]: ModelInfo
18
- } = {}
18
+ } = Object.create(null)
19
19
 
20
20
  /**
21
21
  * @internal
@@ -1,6 +1,6 @@
1
1
  import type { SnapshotInOf, SnapshotOutOf } from "../snapshot/SnapshotOf"
2
2
  import type { AnyStandardType } from "../types/schemas"
3
- import { lazy } from "../utils"
3
+ import { isEqualOrBothNaN, lazy } from "../utils"
4
4
  import { runWithErrorDiagnosticsContext, withErrorPathSegment } from "../utils/errorDiagnostics"
5
5
  import { getOrCreate } from "../utils/mapUtils"
6
6
  import type { Flatten, IsNeverType, IsOptionalValue } from "../utils/types"
@@ -618,11 +618,6 @@ function parseSetterConfig(
618
618
 
619
619
  const propCache = new Map<unknown, AnyModelProp>()
620
620
 
621
- let cacheTransformResult = false
622
- const cacheTransformedValueFn = () => {
623
- cacheTransformResult = true
624
- }
625
-
626
621
  /** @internal */
627
622
  export function toFullModelPropTransform(
628
623
  transformObject: ModelPropTransform<unknown, unknown>
@@ -651,7 +646,7 @@ export function toFullModelPropTransform(
651
646
  const modelCache = getOrCreate(cache, model, () => new Map())
652
647
 
653
648
  let propCache = modelCache.get(propName)
654
- if (propCache?.originalValue !== originalValue) {
649
+ if (!propCache || !isEqualOrBothNaN(propCache.originalValue, originalValue)) {
655
650
  // original changed, invalidate cache
656
651
  modelCache.delete(propName)
657
652
  propCache = undefined
@@ -674,10 +669,12 @@ export function toFullModelPropTransform(
674
669
  untransform(transformedValue: unknown, model: object, propName: PropertyKey) {
675
670
  const modelCache = getOrCreate(cache, model, () => new Map())
676
671
 
677
- cacheTransformResult = false
672
+ let cacheTransformResult = false
678
673
  const originalValue = untransform({
679
674
  transformedValue,
680
- cacheTransformedValue: cacheTransformedValueFn,
675
+ cacheTransformedValue: () => {
676
+ cacheTransformResult = true
677
+ },
681
678
  })
682
679
  if (cacheTransformResult) {
683
680
  modelCache.set(propName, { originalValue, transformedValue })
@@ -19,7 +19,14 @@ import type { AnyType } from "../types/schemas"
19
19
  import { tProp } from "../types/tProp"
20
20
  import { resolveStoredType } from "../types/utility/typesCodec"
21
21
  import { typesUnchecked } from "../types/utility/typesUnchecked"
22
- import { addHiddenProp, assertIsObject, failure, propNameToSetterName } from "../utils"
22
+ import {
23
+ addHiddenProp,
24
+ assertIsObject,
25
+ failure,
26
+ hasOwnProp,
27
+ propNameToSetterName,
28
+ setProtoProp,
29
+ } from "../utils"
23
30
  import { chainFns } from "../utils/chainFns"
24
31
  import { withErrorPathSegment } from "../utils/errorDiagnostics"
25
32
  import { type ModelClass, modelInitializedSymbol } from "./BaseModelShared"
@@ -275,7 +282,9 @@ export function sharedInternalModel<
275
282
  dataTypeChecker = typesObject(() => {
276
283
  const typeCheckerObj: Record<string, AnyType> = {}
277
284
  for (const [k, mp] of Object.entries(composedModelProps)) {
278
- typeCheckerObj[k] = mp._typeChecker ? resolveStoredType(mp._typeChecker) : typesUnchecked()
285
+ const typeChecker = mp._typeChecker ? resolveStoredType(mp._typeChecker) : typesUnchecked()
286
+ if (k === "__proto__") setProtoProp(typeCheckerObj, typeChecker)
287
+ else typeCheckerObj[k] = typeChecker
279
288
  }
280
289
  return typeCheckerObj
281
290
  })
@@ -451,7 +460,11 @@ function getModelPropsSnapshotProcessor(
451
460
  return (sn) => {
452
461
  const newSn = { ...sn }
453
462
  for (const [propName, processor] of propsWithProcessor) {
454
- newSn[propName] = withErrorPathSegment(propName, () => processor(sn[propName]))
463
+ const value = withErrorPathSegment(propName, () =>
464
+ processor(hasOwnProp(sn, propName) ? sn[propName] : undefined)
465
+ )
466
+ if (propName === "__proto__") setProtoProp(newSn, value)
467
+ else newSn[propName] = value
455
468
  }
456
469
  return newSn
457
470
  }
@@ -10,7 +10,7 @@ import { fastGetParentPathIncludingDataObjects } from "./path"
10
10
  * Detaches a given object from a tree.
11
11
  * If the parent is an object / model, detaching will delete the property.
12
12
  * If the parent is an array detaching will remove the node by splicing it.
13
- * If there's no parent it will throw.
13
+ * If there's no parent it does nothing.
14
14
  *
15
15
  * @param node Object to be detached.
16
16
  */
@@ -8,7 +8,7 @@ import { getChildrenObjects } from "./getChildrenObjects"
8
8
  * @param predicate Function that will be run for every child of the root object.
9
9
  * @param [options] An optional object with the `deep` option (defaults to `false`) set to `true` to
10
10
  * get the children deeply or `false` to get them shallowly.
11
- * @returns A readonly observable set with the matching children.
11
+ * @returns A readonly set with the matching children.
12
12
  */
13
13
  export function findChildren<T extends object = any>(
14
14
  root: object,
@@ -21,13 +21,10 @@ export function findChildren<T extends object = any>(
21
21
 
22
22
  const set = new Set<any>()
23
23
 
24
- const iter = children.values()
25
- let cur = iter.next()
26
- while (!cur.done) {
27
- if (predicate(cur.value)) {
28
- set.add(cur.value)
24
+ for (const child of children) {
25
+ if (predicate(child)) {
26
+ set.add(child)
29
27
  }
30
- cur = iter.next()
31
28
  }
32
29
 
33
30
  return set
@@ -19,8 +19,7 @@ export function findParent<T extends object = any>(
19
19
  predicate: (parentNode: object) => boolean,
20
20
  maxDepth = 0
21
21
  ): T | undefined {
22
- const foundParentPath = findParentPath(child, predicate, maxDepth)
23
- return foundParentPath ? foundParentPath.parent : undefined
22
+ return findParentInternal<T>(child, predicate, maxDepth)
24
23
  }
25
24
 
26
25
  /**
@@ -56,22 +55,28 @@ export function findParentPath<T extends object = any>(
56
55
  predicate: (parentNode: object) => boolean,
57
56
  maxDepth = 0
58
57
  ): FoundParentPath<T> | undefined {
59
- assertTweakedObject(child, "child")
60
-
61
58
  const path: WritablePath = []
59
+ const parent = findParentInternal<T>(child, predicate, maxDepth, path)
60
+ return parent ? { parent, path: path.reverse() } : undefined
61
+ }
62
+
63
+ function findParentInternal<T extends object>(
64
+ child: object,
65
+ predicate: (parentNode: object) => boolean,
66
+ maxDepth: number,
67
+ path?: WritablePath
68
+ ): T | undefined {
69
+ assertTweakedObject(child, "child")
62
70
 
63
71
  let current: any = child
64
72
  let depth = 0
65
73
 
66
74
  let parentPath: ParentPath<any> | undefined
67
75
  while ((parentPath = fastGetParentPath(current, true))) {
68
- path.unshift(parentPath.path)
76
+ path?.push(parentPath.path)
69
77
  current = parentPath.parent
70
78
  if (predicate(current)) {
71
- return {
72
- parent: current,
73
- path,
74
- }
79
+ return current
75
80
  }
76
81
 
77
82
  depth++
@@ -32,11 +32,10 @@ export function onChildAttachedTo(
32
32
  assertIsFunction(target, "target")
33
33
  assertIsFunction(fn, "fn")
34
34
 
35
- const opts = {
36
- deep: false,
37
- fireForCurrentChildren: true,
38
- ...options,
39
- }
35
+ const { deep = false, fireForCurrentChildren = true } = options ?? {}
36
+
37
+ let disposed = false
38
+ let runCleanupAfterDispose = false
40
39
 
41
40
  const detachDisposers = new WeakMap<object, () => void>()
42
41
 
@@ -48,32 +47,38 @@ export function onChildAttachedTo(
48
47
  }
49
48
  }
50
49
 
50
+ const runDetachDisposers = (nodes: object[]) => {
51
+ let firstError: { value: unknown } | undefined
52
+ for (let i = nodes.length - 1; i >= 0; i--) {
53
+ try {
54
+ runDetachDisposer(nodes[i])
55
+ } catch (value) {
56
+ firstError ??= { value }
57
+ }
58
+ }
59
+ return firstError
60
+ }
61
+
51
62
  const addDetachDisposer = (n: object, disposer: (() => void) | void) => {
52
63
  if (disposer) {
53
- detachDisposers.set(n, action(disposer))
64
+ const detach = action(disposer)
65
+ if (disposed) {
66
+ if (runCleanupAfterDispose) detach()
67
+ } else {
68
+ detachDisposers.set(n, detach)
69
+ }
54
70
  }
55
71
  }
56
72
 
57
- const getChildrenObjectOpts = { deep: opts.deep }
73
+ const getChildrenObjectOpts = { deep }
58
74
  const getCurrentChildren = () => {
59
75
  const t = target()
60
76
  assertTweakedObject(t, "target()")
61
77
 
62
- const children = getChildrenObjects(t, getChildrenObjectOpts)
63
-
64
- const set = new Set<object>()
65
-
66
- const iter = children.values()
67
- let cur = iter.next()
68
- while (!cur.done) {
69
- set.add(cur.value)
70
- cur = iter.next()
71
- }
72
-
73
- return set
78
+ return new Set(getChildrenObjects(t, getChildrenObjectOpts))
74
79
  }
75
80
 
76
- const currentChildren = opts.fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
81
+ const currentChildren = fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
77
82
 
78
83
  const disposer = reaction(
79
84
  () => getCurrentChildren(),
@@ -95,45 +100,45 @@ export function onChildAttachedTo(
95
100
  currentChildrenCur = currentChildrenIter.next()
96
101
  }
97
102
 
98
- if (disposersToRun.length > 0) {
99
- for (let i = disposersToRun.length - 1; i >= 0; i--) {
100
- runDetachDisposer(disposersToRun[i])
101
- }
102
- }
103
+ let firstError = runDetachDisposers(disposersToRun)
103
104
 
104
105
  // find new
105
106
  const newChildrenIter = newChildren.values()
106
107
  let newChildrenCur = newChildrenIter.next()
107
- while (!newChildrenCur.done) {
108
+ while (!disposed && !newChildrenCur.done) {
108
109
  const n = newChildrenCur.value
109
110
  if (!currentChildren.has(n)) {
110
111
  currentChildren.add(n)
111
112
 
112
- const detachAction = runInAction(() => fn(n))
113
- addDetachDisposer(n, detachAction)
113
+ try {
114
+ const detachAction = runInAction(() => fn(n))
115
+ addDetachDisposer(n, detachAction)
116
+ } catch (value) {
117
+ firstError ??= { value }
118
+ }
114
119
  }
115
120
 
116
121
  newChildrenCur = newChildrenIter.next()
117
122
  }
123
+ if (firstError) {
124
+ throw firstError.value
125
+ }
118
126
  },
119
127
  {
120
128
  fireImmediately: true,
121
129
  }
122
130
  )
123
131
 
124
- return (runDetachDisposers: boolean) => {
132
+ return (runPendingDetachDisposers: boolean) => {
133
+ if (disposed) return
134
+ disposed = true
135
+ runCleanupAfterDispose = runPendingDetachDisposers
125
136
  disposer()
126
-
127
- if (runDetachDisposers) {
128
- const currentChildrenIter = currentChildren.values()
129
- let currentChildrenCur = currentChildrenIter.next()
130
- while (!currentChildrenCur.done) {
131
- const n = currentChildrenCur.value
132
- runDetachDisposer(n)
133
-
134
- currentChildrenCur = currentChildrenIter.next()
135
- }
136
- }
137
+ const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren) : []
137
138
  currentChildren.clear()
139
+ const firstError = runDetachDisposers(pendingChildren)
140
+ if (firstError) {
141
+ throw firstError.value
142
+ }
138
143
  }
139
144
  }
@@ -175,10 +175,12 @@ export function fastGetRootPath<T extends object = any>(
175
175
  let parentPath: ParentPath<any> | undefined
176
176
  while ((parentPath = fastGetParentPath(root, useAtom))) {
177
177
  root = parentPath.parent
178
- path.unshift(parentPath.path)
179
- pathObjects.unshift(parentPath.parent)
178
+ path.push(parentPath.path)
179
+ pathObjects.push(parentPath.parent)
180
180
  }
181
181
 
182
+ path.reverse()
183
+ pathObjects.reverse()
182
184
  return { root, path, pathObjects } as RootPath<any>
183
185
  }
184
186
 
@@ -265,11 +267,13 @@ export function resolvePath<T = any>(
265
267
  const dataNode = modelToDataNode(current)
266
268
  if (p in dataNode) {
267
269
  current = dataNode
268
- } else if (!(p in current)) {
269
- return unresolved
270
270
  }
271
271
  }
272
272
 
273
+ if (!(p in current)) {
274
+ return unresolved
275
+ }
276
+
273
277
  current = current[p]
274
278
  }
275
279
 
@@ -321,7 +325,7 @@ export function resolvePathCheckingIds<T = any>(
321
325
 
322
326
  // check just to avoid mobx warnings about trying to access out of bounds index
323
327
  if (isArray(current)) {
324
- if (+p >= current.length) {
328
+ if (+p >= current.length || !(p in current)) {
325
329
  return { resolved: false }
326
330
  }
327
331
  } else if (!hasOwnProp(current, p)) {
@@ -376,11 +380,11 @@ export function fastGetParentToChildPath(
376
380
  let current = toChild
377
381
  let parentPath: ParentPath<any> | undefined
378
382
  while ((parentPath = fastGetParentPath(current, useAtom))) {
379
- path.unshift(parentPath.path)
383
+ path.push(parentPath.path)
380
384
 
381
385
  current = parentPath.parent
382
386
  if (current === fromParent) {
383
- return path
387
+ return path.reverse()
384
388
  }
385
389
  }
386
390
  return undefined
@@ -6,6 +6,7 @@ import { isModel } from "../model/utils"
6
6
  import { attachToRootStore, detachFromRootStore } from "../rootStore/attachDetach"
7
7
  import { fastIsRootStoreNoAtom } from "../rootStore/rootStore"
8
8
  import { clone } from "../snapshot/clone"
9
+ import { flushInternalSnapshot } from "../snapshot/internal"
9
10
  import { isTweakedObject } from "../tweaker/core"
10
11
  import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
11
12
  import { tryUntweak } from "../tweaker/tweak"
@@ -19,6 +20,31 @@ import {
19
20
  import { addObjectChild, removeObjectChild } from "./coreObjectChildren"
20
21
  import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
21
22
 
23
+ /**
24
+ * Updates the unchanged tail of an array before a splice is applied. These
25
+ * children keep their parent, so neither attachment nor child-index work is
26
+ * needed. One action batches path notifications for the entire tail.
27
+ * @internal
28
+ */
29
+ export const reindexArrayChildren = action(
30
+ "reindexArrayChildren",
31
+ (array: readonly unknown[], oldStart: number, newStart: number): void => {
32
+ for (let i = oldStart, j = newStart; i < array.length; i++, j++) {
33
+ const value = array[i]
34
+ if (isPrimitive(value)) {
35
+ continue
36
+ }
37
+ const metadata = treeNodeMetadata.get(value as object)
38
+ if (inDevMode && metadata?.parentPath?.parent !== array) {
39
+ throw failure("assertion failed: reindexed child must already belong to the array")
40
+ }
41
+ // Replace the path object: callers may have retained the previous path.
42
+ metadata!.parentPath = { parent: array, path: j }
43
+ reportParentPathChanged(value as object)
44
+ }
45
+ }
46
+ )
47
+
22
48
  /**
23
49
  * @internal
24
50
  */
@@ -27,7 +53,6 @@ export const setParent = action(
27
53
  (
28
54
  value: any,
29
55
  parentPath: ParentPath<any> | undefined,
30
- indexChangeAllowed: boolean,
31
56
  isDataObject: boolean,
32
57
  cloneIfApplicable: boolean
33
58
  ): any => {
@@ -36,11 +61,6 @@ export const setParent = action(
36
61
  }
37
62
 
38
63
  if (inDevMode) {
39
- if (indexChangeAllowed && cloneIfApplicable) {
40
- throw failure(
41
- "assertion failed: 'indexChangeAllowed' and 'cloneIfApplicable' cannot be set at the same time"
42
- )
43
- }
44
64
  if (typeof value === "function" || typeof value === "symbol") {
45
65
  throw failure(`assertion failed: value cannot be a function or a symbol`)
46
66
  }
@@ -67,6 +87,12 @@ export const setParent = action(
67
87
  return value
68
88
  }
69
89
 
90
+ // The new parent captures this node's snapshot during attachment. Flush
91
+ // descendant edits made while detached before that snapshot is copied.
92
+ if (parentPath && !oldParentPath) {
93
+ flushInternalSnapshot(value, false)
94
+ }
95
+
70
96
  // make sure the new parent actually points to models when we give model data objs
71
97
  if (parentPath) {
72
98
  const actualParent = dataToModelNode(parentPath.parent)
@@ -92,14 +118,7 @@ export const setParent = action(
92
118
  }
93
119
 
94
120
  if (oldParentPath && parentPath) {
95
- if (oldParentPath.parent === parentPath.parent && indexChangeAllowed) {
96
- // just changing the index
97
- treeNodeMetadata.get(value)!.parentPath = parentPath
98
- reportParentPathChanged(value)
99
- return value
100
- } else {
101
- throw failure("an object cannot be assigned a new parent when it already has one")
102
- }
121
+ throw failure("an object cannot be assigned a new parent when it already has one")
103
122
  }
104
123
 
105
124
  const postUntweaker = parentPath ? undefined : tryUntweak(value)
@@ -75,19 +75,19 @@ function walkTreeChildrenFirst<T = void>(
75
75
  root: object,
76
76
  visit: (node: object) => T | undefined
77
77
  ): T | undefined {
78
- const childrenIter = getObjectChildren(root).values()
79
- let ch = childrenIter.next()
80
- while (!ch.done) {
81
- const ret = walkTreeChildrenFirst(ch.value, visit)
82
- if (ret !== undefined) {
83
- return ret
78
+ const stack = [{ node: root, children: getObjectChildren(root).values() }]
79
+ while (stack.length > 0) {
80
+ const frame = stack[stack.length - 1]
81
+ const child = frame.children.next()
82
+ if (!child.done) {
83
+ stack.push({ node: child.value, children: getObjectChildren(child.value).values() })
84
+ } else {
85
+ stack.pop()
86
+ const ret = visit(frame.node)
87
+ if (ret !== undefined) {
88
+ return ret
89
+ }
84
90
  }
85
- ch = childrenIter.next()
86
- }
87
-
88
- const ret = visit(root)
89
- if (ret !== undefined) {
90
- return ret
91
91
  }
92
92
 
93
93
  return undefined
@@ -136,37 +136,21 @@ function walkTreeAggregate<R>(
136
136
  let map: Map<R, object> | undefined
137
137
  const rootVal = visit(target)
138
138
 
139
- const childrenMap = getObjectChildren(target)
140
- const childrenIter = childrenMap.values()
141
- let ch = childrenIter.next()
139
+ const children = getObjectChildren(target)
142
140
 
143
- // small optimization, if there is only one child and this
144
- // object provides no value we can just reuse the child ones
145
- if (rootVal === undefined && childrenMap.size === 1) {
146
- return recurse(ch.value!)
141
+ // With one child and no local value, the child's map can be reused.
142
+ if (rootVal === undefined && children.size === 1) {
143
+ return recurse(children.values().next().value!)
147
144
  }
148
145
 
149
- while (!ch.done) {
150
- const childMap = recurse(ch.value)
151
-
146
+ for (const child of children) {
147
+ const childMap = recurse(child)
152
148
  if (childMap) {
153
- if (!map) {
154
- map = new Map()
155
- }
156
-
157
- // add child map keys/values to own map
158
- const mapIter = childMap.keys()
159
-
160
- let mapCur = mapIter.next()
161
- while (!mapCur.done) {
162
- const key = mapCur.value
163
- const val = childMap.get(key)!
164
- map.set(key, val)
165
- mapCur = mapIter.next()
149
+ map ??= new Map()
150
+ for (const [key, value] of childMap) {
151
+ map.set(key, value)
166
152
  }
167
153
  }
168
-
169
- ch = childrenIter.next()
170
154
  }
171
155
 
172
156
  // add it at the end so parent resolutions have higher