mobx-keystone 1.25.0 → 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 (115) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/mobx-keystone.esm.js +1478 -952
  3. package/dist/mobx-keystone.esm.mjs +1478 -952
  4. package/dist/mobx-keystone.umd.js +1483 -952
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  7. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  8. package/dist/types/parent/detach.d.ts +1 -1
  9. package/dist/types/parent/findChildren.d.ts +1 -1
  10. package/dist/types/patch/applyPatches.d.ts +2 -0
  11. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  12. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  13. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  14. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  15. package/dist/types/types/TypeCheckError.d.ts +1 -0
  16. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  17. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  18. package/dist/types/utils/index.d.ts +13 -0
  19. package/dist/types/utils/mapUtils.d.ts +2 -2
  20. package/dist/types/utils/setLike.d.ts +1 -0
  21. package/package.json +1 -1
  22. package/src/action/applyAction.ts +1 -1
  23. package/src/action/middleware.ts +17 -36
  24. package/src/action/modelFlow.ts +4 -5
  25. package/src/action/mutationBatch.ts +10 -10
  26. package/src/action/pendingActions.ts +3 -4
  27. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  28. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  29. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  30. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  31. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  32. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  33. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  34. package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
  35. package/src/actionMiddlewares/undoMiddleware.ts +65 -46
  36. package/src/actionMiddlewares/utils.ts +2 -1
  37. package/src/computedTree/computedTree.ts +14 -3
  38. package/src/context/context.ts +17 -45
  39. package/src/dataModel/BaseDataModel.ts +20 -16
  40. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  41. package/src/deepChange/onDeepChange.ts +179 -24
  42. package/src/frozen/Frozen.ts +50 -33
  43. package/src/model/BaseModel.ts +4 -9
  44. package/src/model/newModel.ts +21 -24
  45. package/src/model/utils.ts +2 -2
  46. package/src/modelShared/modelClassInitializer.ts +3 -2
  47. package/src/modelShared/modelDecorator.ts +34 -14
  48. package/src/modelShared/modelInfo.ts +1 -1
  49. package/src/modelShared/prop.ts +6 -9
  50. package/src/modelShared/sharedInternalModel.ts +16 -3
  51. package/src/parent/detach.ts +1 -1
  52. package/src/parent/findChildren.ts +4 -7
  53. package/src/parent/onChildAttachedTo.ts +44 -28
  54. package/src/parent/path.ts +5 -3
  55. package/src/parent/setParent.ts +7 -0
  56. package/src/parent/walkTree.ts +21 -37
  57. package/src/patch/applyPatches.ts +69 -34
  58. package/src/patch/emitPatch.ts +142 -26
  59. package/src/patch/jsonPatch.ts +5 -0
  60. package/src/patch/patchRecorder.ts +18 -14
  61. package/src/redux/connectReduxDevTools.ts +24 -18
  62. package/src/redux/redux.ts +5 -21
  63. package/src/ref/core.ts +18 -2
  64. package/src/rootStore/attachDetach.ts +27 -16
  65. package/src/rootStore/rootStore.ts +10 -6
  66. package/src/snapshot/SnapshotOf.ts +2 -2
  67. package/src/snapshot/applySnapshot.ts +1 -1
  68. package/src/snapshot/clone.ts +1 -2
  69. package/src/snapshot/cloneTreeValue.ts +1 -2
  70. package/src/snapshot/fromModelSnapshot.ts +6 -2
  71. package/src/snapshot/internal.ts +22 -9
  72. package/src/snapshot/onSnapshot.ts +12 -1
  73. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  74. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  75. package/src/standardActions/arrayActions.ts +12 -2
  76. package/src/standardActions/objectActions.ts +3 -2
  77. package/src/transforms/ImmutableDate.ts +4 -0
  78. package/src/transforms/asMap.ts +3 -1
  79. package/src/treeUtils/deepEquals.ts +15 -13
  80. package/src/treeUtils/draft.ts +8 -0
  81. package/src/treeUtils/sandbox.ts +31 -27
  82. package/src/tweaker/core.ts +2 -4
  83. package/src/tweaker/tweak.ts +2 -1
  84. package/src/tweaker/tweakArray.ts +83 -20
  85. package/src/tweaker/tweakPlainObject.ts +76 -23
  86. package/src/tweaker/typeChecking.ts +124 -15
  87. package/src/types/TypeCheckError.ts +17 -19
  88. package/src/types/createCachedTypeCheck.ts +27 -2
  89. package/src/types/objectBased/typesDataModelData.ts +4 -2
  90. package/src/types/objectBased/typesModel.ts +4 -2
  91. package/src/types/objectBased/typesObject.ts +20 -16
  92. package/src/types/objectBased/typesRecord.ts +6 -13
  93. package/src/types/objectBased/typesRef.ts +2 -2
  94. package/src/types/primitiveBased/typesEnum.ts +4 -2
  95. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  96. package/src/types/typeCheckErrorUtils.ts +1 -0
  97. package/src/types/utility/typesCodecSupport.ts +97 -56
  98. package/src/types/utility/typesMap.ts +24 -41
  99. package/src/types/utility/typesRefinement.ts +3 -2
  100. package/src/types/utility/typesSet.ts +24 -0
  101. package/src/utils/ModelPool.ts +3 -3
  102. package/src/utils/chainFns.ts +4 -0
  103. package/src/utils/decorators.ts +1 -1
  104. package/src/utils/errorDiagnostics.ts +6 -2
  105. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  106. package/src/utils/index.ts +34 -2
  107. package/src/utils/mapUtils.ts +3 -7
  108. package/src/utils/setIfDifferent.ts +3 -2
  109. package/src/utils/setLike.ts +134 -0
  110. package/src/wrappers/ArraySet.ts +5 -9
  111. package/src/wrappers/ObjectMap.ts +15 -11
  112. package/src/wrappers/asMap.ts +30 -18
  113. package/src/wrappers/asSet.ts +36 -12
  114. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  115. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -5,6 +5,7 @@ import type { PathElement } from "../parent/pathTypes"
5
5
  import { freezeInternalSnapshot, getInternalSnapshot } from "../snapshot/internal"
6
6
  import { assertTweakedObject } from "../tweaker/core"
7
7
  import { assertIsFunction, deleteFromArray, isPrimitive } from "../utils"
8
+ import { forEachWithDelayedThrow } from "../utils/forEachWithDelayedThrow"
8
9
  import type { Patch } from "./Patch"
9
10
 
10
11
  const emptyPatchArray: Patch[] = []
@@ -31,12 +32,34 @@ export class InternalPatchRecorder {
31
32
  // empty arrays and must not trigger ancestor type checking.
32
33
  this.hasChanges = this.patches.length > 0 || this.invPatches.length > 0
33
34
  }
35
+ }
34
36
 
35
- emit(obj: object) {
36
- if (this.patches.length > 0) {
37
- emitPatches(obj, this.patches, this.invPatches)
38
- }
39
- this.reset()
37
+ const runNotification = (notify: () => void) => notify()
38
+
39
+ let patchEmissionDepth = 0
40
+ let emittingPendingPatches = false
41
+ const pendingPatchNotifications: (() => void)[] = []
42
+
43
+ /** @internal */
44
+ export function beginPatchEmission(): void {
45
+ patchEmissionDepth++
46
+ }
47
+
48
+ /** @internal */
49
+ export function endPatchEmission(): void {
50
+ patchEmissionDepth--
51
+ flushPendingPatches()
52
+ }
53
+
54
+ function flushPendingPatches(): void {
55
+ if (patchEmissionDepth > 0 || emittingPendingPatches || pendingPatchNotifications.length === 0)
56
+ return
57
+ emittingPendingPatches = true
58
+ try {
59
+ forEachWithDelayedThrow(pendingPatchNotifications, (notify) => notify())
60
+ } finally {
61
+ pendingPatchNotifications.length = 0
62
+ emittingPendingPatches = false
40
63
  }
41
64
  }
42
65
 
@@ -44,11 +67,22 @@ export class InternalPatchRecorder {
44
67
  * @internal
45
68
  */
46
69
  export function emitPatches(obj: object, patches: Patch[], invPatches: Patch[]): void {
47
- if (patches.length > 0) {
48
- emitGlobalPatch(obj, patches, invPatches)
70
+ if (patches.length > 0 && (globalPatchListeners.length > 0 || patchListenerCount > 0)) {
71
+ const synchronousNotifications: (() => void)[] = []
72
+ if (globalPatchListeners.length > 0) {
73
+ emitGlobalPatch(obj, patches, invPatches, synchronousNotifications)
74
+ }
49
75
  if (patchListenerCount > 0) {
50
- emitPatch(obj, patches, invPatches)
76
+ emitPatch(obj, patches, invPatches, synchronousNotifications)
51
77
  }
78
+ // Reserve every public notification and its path before recorder callbacks
79
+ // can cause further mutations. Recorders must run within the mutation's
80
+ // action / withoutUndo scope, even while public delivery is deferred.
81
+ // A throwing recorder must not stop the ones registered on ancestor nodes,
82
+ // nor swallow the deferred public delivery, so the flush is just one more
83
+ // step of the same delayed-throw delivery.
84
+ synchronousNotifications.push(flushPendingPatches)
85
+ forEachWithDelayedThrow(synchronousNotifications, runNotification)
52
86
  }
53
87
  }
54
88
 
@@ -71,8 +105,14 @@ export type OnGlobalPatchesListener = (
71
105
  */
72
106
  export type OnPatchesDisposer = () => void
73
107
 
74
- const patchListeners = new WeakMap<object, OnPatchesListener[]>()
75
- const globalPatchListeners: OnGlobalPatchesListener[] = []
108
+ interface PatchSubscription<Listener> {
109
+ listener: Listener
110
+ synchronous: boolean
111
+ active: boolean
112
+ }
113
+
114
+ const patchListeners = new WeakMap<object, PatchSubscription<OnPatchesListener>[]>()
115
+ const globalPatchListeners: PatchSubscription<OnGlobalPatchesListener>[] = []
76
116
  let patchListenerCount = 0
77
117
 
78
118
  /**
@@ -113,6 +153,15 @@ export function hasPatchListenersFor(target: object): boolean {
113
153
  * @returns A disposer to stop listening to patches.
114
154
  */
115
155
  export function onPatches(subtreeRoot: object, listener: OnPatchesListener): OnPatchesDisposer {
156
+ return internalOnPatches(subtreeRoot, listener, false)
157
+ }
158
+
159
+ /** @internal */
160
+ export function internalOnPatches(
161
+ subtreeRoot: object,
162
+ listener: OnPatchesListener,
163
+ synchronous: boolean
164
+ ): OnPatchesDisposer {
116
165
  assertTweakedObject(subtreeRoot, "subtreeRoot")
117
166
  assertIsFunction(listener, "listener")
118
167
 
@@ -126,10 +175,12 @@ export function onPatches(subtreeRoot: object, listener: OnPatchesListener): OnP
126
175
  patchListeners.set(subtreeRoot, listenersForObject)
127
176
  }
128
177
 
129
- listenersForObject.push(listener)
178
+ const subscription = { listener, synchronous, active: true }
179
+ listenersForObject.push(subscription)
130
180
  patchListenerCount++
131
181
  return () => {
132
- if (deleteFromArray(listenersForObject, listener)) {
182
+ subscription.active = false
183
+ if (deleteFromArray(listenersForObject, subscription)) {
133
184
  patchListenerCount--
134
185
  }
135
186
  }
@@ -143,25 +194,81 @@ export function onPatches(subtreeRoot: object, listener: OnPatchesListener): OnP
143
194
  * @returns A disposer to stop listening to patches.
144
195
  */
145
196
  export function onGlobalPatches(listener: OnGlobalPatchesListener): OnPatchesDisposer {
197
+ return internalOnGlobalPatches(listener, false)
198
+ }
199
+
200
+ /** @internal */
201
+ export function internalOnGlobalPatches(
202
+ listener: OnGlobalPatchesListener,
203
+ synchronous: boolean
204
+ ): OnPatchesDisposer {
146
205
  assertIsFunction(listener, "listener")
147
206
 
148
207
  if (!isAction(listener)) {
149
208
  listener = action(listener.name || "onGlobalPatchesListener", listener)
150
209
  }
151
210
 
152
- globalPatchListeners.push(listener)
211
+ const subscription = { listener, synchronous, active: true }
212
+ globalPatchListeners.push(subscription)
153
213
  return () => {
154
- deleteFromArray(globalPatchListeners, listener)
214
+ subscription.active = false
215
+ deleteFromArray(globalPatchListeners, subscription)
155
216
  }
156
217
  }
157
218
 
158
- function emitGlobalPatch(obj: object, patches: Patch[], inversePatches: Patch[]): void {
159
- for (let i = 0; i < globalPatchListeners.length; i++) {
160
- const listener = globalPatchListeners[i]
161
- listener(obj, patches, inversePatches)
219
+ function notifyPatchSubscriptions<Args extends unknown[]>(
220
+ subscriptions: PatchSubscription<(...args: Args) => void>[],
221
+ args: Args,
222
+ synchronous: boolean
223
+ ): void {
224
+ forEachWithDelayedThrow(subscriptions, (subscription) => {
225
+ if (subscription.active && subscription.synchronous === synchronous) {
226
+ subscription.listener(...args)
227
+ }
228
+ })
229
+ }
230
+
231
+ function reservePatchNotifications<Args extends unknown[]>(
232
+ subscriptions: PatchSubscription<(...args: Args) => void>[],
233
+ args: Args,
234
+ synchronousNotifications: (() => void)[]
235
+ ): void {
236
+ let hasSynchronous = false
237
+ let hasDeferred = false
238
+ for (let i = 0; i < subscriptions.length; i++) {
239
+ if (subscriptions[i].synchronous) {
240
+ hasSynchronous = true
241
+ } else {
242
+ hasDeferred = true
243
+ }
244
+ }
245
+ if (!hasSynchronous && !hasDeferred) {
246
+ return
247
+ }
248
+
249
+ // A listener installed by a reentrant action must not receive earlier patches.
250
+ const current = subscriptions.slice()
251
+ if (hasDeferred) {
252
+ pendingPatchNotifications.push(() => notifyPatchSubscriptions(current, args, false))
253
+ }
254
+ if (hasSynchronous) {
255
+ synchronousNotifications.push(() => notifyPatchSubscriptions(current, args, true))
162
256
  }
163
257
  }
164
258
 
259
+ function emitGlobalPatch(
260
+ obj: object,
261
+ patches: Patch[],
262
+ inversePatches: Patch[],
263
+ synchronousNotifications: (() => void)[]
264
+ ): void {
265
+ reservePatchNotifications(
266
+ globalPatchListeners,
267
+ [obj, patches, inversePatches],
268
+ synchronousNotifications
269
+ )
270
+ }
271
+
165
272
  // `reversedPrefix` holds the path segments from the changed node up to (but not
166
273
  // including) the current target, in child-to-root order. The prefix to prepend to
167
274
  // a patch is that slice reversed (root-to-child); `prefixCount` is how many of the
@@ -171,7 +278,8 @@ function emitPatchForTarget(
171
278
  patches: Patch[],
172
279
  inversePatches: Patch[],
173
280
  reversedPrefix: readonly PathElement[],
174
- prefixCount: number
281
+ prefixCount: number,
282
+ synchronousNotifications: (() => void)[]
175
283
  ): void {
176
284
  const listenersForObject = patchListeners.get(obj)
177
285
 
@@ -184,19 +292,26 @@ function emitPatchForTarget(
184
292
  const invPatchesWithPathPrefix =
185
293
  prefixCount > 0 ? prefixPatches(inversePatches, reversedPrefix, prefixCount) : inversePatches
186
294
 
187
- for (let i = 0; i < listenersForObject.length; i++) {
188
- const listener = listenersForObject[i]
189
- listener(patchesWithPathPrefix, invPatchesWithPathPrefix)
190
- }
295
+ // Capture mutation-time paths before a deep-change listener can move nodes.
296
+ reservePatchNotifications(
297
+ listenersForObject,
298
+ [patchesWithPathPrefix, invPatchesWithPathPrefix],
299
+ synchronousNotifications
300
+ )
191
301
  }
192
302
 
193
- function emitPatch(obj: object, patches: Patch[], inversePatches: Patch[]): void {
303
+ function emitPatch(
304
+ obj: object,
305
+ patches: Patch[],
306
+ inversePatches: Patch[],
307
+ synchronousNotifications: (() => void)[]
308
+ ): void {
194
309
  // Segments are pushed in child-to-root order (O(1) each) rather than unshifted
195
310
  // at the front (O(depth) each), keeping the whole walk O(depth) instead of
196
311
  // O(depth^2) on deep trees.
197
312
  const reversedPrefix: PathElement[] = []
198
313
 
199
- emitPatchForTarget(obj, patches, inversePatches, reversedPrefix, 0)
314
+ emitPatchForTarget(obj, patches, inversePatches, reversedPrefix, 0, synchronousNotifications)
200
315
 
201
316
  // and also emit subtree listeners all the way to the root
202
317
  let parentPath = fastGetParentPath(obj, false)
@@ -207,7 +322,8 @@ function emitPatch(obj: object, patches: Patch[], inversePatches: Patch[]): void
207
322
  patches,
208
323
  inversePatches,
209
324
  reversedPrefix,
210
- reversedPrefix.length
325
+ reversedPrefix.length,
326
+ synchronousNotifications
211
327
  )
212
328
 
213
329
  parentPath = fastGetParentPath(parentPath.parent, false)
@@ -41,6 +41,8 @@ function escapePathComponent(path: string | number): string {
41
41
  return path.replace(/~/g, "~0").replace(/\//g, "~1")
42
42
  }
43
43
 
44
+ const invalidPointerEscape = /~(?:[^01]|$)/
45
+
44
46
  /**
45
47
  * Unescapes a json pointer path.
46
48
  *
@@ -48,6 +50,9 @@ function escapePathComponent(path: string | number): string {
48
50
  * @return The unescaped path
49
51
  */
50
52
  function unescapePathComponent(path: string): string {
53
+ if (invalidPointerEscape.test(path)) {
54
+ throw failure("invalid JSON pointer escape: expected ~0 or ~1")
55
+ }
51
56
  return path.replace(/~1/g, "/").replace(/~0/g, "~")
52
57
  }
53
58
 
@@ -1,10 +1,10 @@
1
1
  import { observable } from "mobx"
2
2
  import { assertTweakedObject } from "../tweaker/core"
3
3
  import {
4
+ internalOnGlobalPatches,
5
+ internalOnPatches,
4
6
  type OnPatchesDisposer,
5
7
  type OnPatchesListener,
6
- onGlobalPatches,
7
- onPatches,
8
8
  } from "./emitPatch"
9
9
  import type { Patch } from "./Patch"
10
10
 
@@ -111,18 +111,22 @@ export function internalPatchRecorder(
111
111
  let onPatchesDisposer: OnPatchesDisposer
112
112
 
113
113
  if (subtreeRoot) {
114
- onPatchesDisposer = onPatches(subtreeRoot, (p, invP) => {
115
- if (recording && filter(p, invP)) {
116
- events.push({
117
- target: subtreeRoot,
118
- patches: p,
119
- inversePatches: invP,
120
- })
121
- opts?.onPatches?.(p, invP)
122
- }
123
- })
114
+ onPatchesDisposer = internalOnPatches(
115
+ subtreeRoot,
116
+ (p, invP) => {
117
+ if (recording && filter(p, invP)) {
118
+ events.push({
119
+ target: subtreeRoot,
120
+ patches: p,
121
+ inversePatches: invP,
122
+ })
123
+ opts?.onPatches?.(p, invP)
124
+ }
125
+ },
126
+ true
127
+ )
124
128
  } else {
125
- onPatchesDisposer = onGlobalPatches((target, p, invP) => {
129
+ onPatchesDisposer = internalOnGlobalPatches((target, p, invP) => {
126
130
  if (recording && filter(p, invP)) {
127
131
  events.push({
128
132
  target,
@@ -131,7 +135,7 @@ export function internalPatchRecorder(
131
135
  })
132
136
  opts?.onPatches?.(p, invP)
133
137
  }
134
- })
138
+ }, true)
135
139
  }
136
140
 
137
141
  return {
@@ -33,6 +33,7 @@ export interface ConnectReduxDevToolsOptions {
33
33
  * @param remotedevConnection The result of a connect method from the remotedev package (usually the result of `remoteDev.connectViaExtension(...)`).
34
34
  * @param target Object to use as root.
35
35
  * @param [options] Optional options object.
36
+ * @returns An idempotent disposer that stops logging and unsubscribes from monitor messages.
36
37
  */
37
38
  export function connectReduxDevTools(
38
39
  remotedevPackage: any,
@@ -42,37 +43,35 @@ export function connectReduxDevTools(
42
43
  ) {
43
44
  assertTweakedObject(target, "target")
44
45
 
45
- const opts = {
46
- logArgsNearName: true,
47
- logChildActions: true,
48
- ...options,
49
- }
46
+ const { logArgsNearName = true, logChildActions = true } = options ?? {}
50
47
 
48
+ let disposed = false
51
49
  let handlingMonitorAction = 0
52
50
 
51
+ let initialState = getSnapshot(target)
52
+ let lastLoggedSnapshot = initialState
53
+ remotedevConnection.init(initialState)
54
+
53
55
  // subscribe to change state (if need more than just logging)
54
- remotedevConnection.subscribe((message: any) => {
55
- if (message.type === "DISPATCH") {
56
+ const unsubscribe = remotedevConnection.subscribe((message: any) => {
57
+ if (!disposed && message.type === "DISPATCH") {
56
58
  handleMonitorActions(remotedevConnection, target, message)
57
59
  }
58
60
  })
59
61
 
60
- const initialState = getSnapshot(target)
61
- remotedevConnection.init(initialState)
62
-
63
62
  let currentActionId = 0
64
63
  const actionIdSymbol = Symbol("actionId")
65
64
 
66
- actionTrackingMiddleware(target, {
65
+ const disposeMiddleware = actionTrackingMiddleware(target, {
67
66
  filter(ctx) {
68
- return opts.logChildActions || !hasDevToolsTrackedParentContext(ctx)
67
+ return logChildActions || !hasDevToolsTrackedParentContext(ctx)
69
68
  },
70
69
  onStart(ctx) {
71
70
  ctx.data[actionIdSymbol] = currentActionId++
72
71
  },
73
72
  onResume(ctx) {
74
73
  // give a chance to the parent to log its own changes before the child starts
75
- if (opts.logChildActions && ctx.parentContext) {
74
+ if (logChildActions && ctx.parentContext) {
76
75
  log(ctx.parentContext, undefined)
77
76
  }
78
77
  log(ctx, undefined)
@@ -85,6 +84,13 @@ export function connectReduxDevTools(
85
84
  },
86
85
  })
87
86
 
87
+ return () => {
88
+ if (disposed) return
89
+ disposed = true
90
+ disposeMiddleware()
91
+ if (typeof unsubscribe === "function") unsubscribe()
92
+ }
93
+
88
94
  function handleMonitorActions(remotedev2: any, target2: any, message: any) {
89
95
  try {
90
96
  handlingMonitorAction++
@@ -96,7 +102,8 @@ export function connectReduxDevTools(
96
102
  }
97
103
 
98
104
  case "COMMIT":
99
- return remotedev2.init(getSnapshot(target2))
105
+ initialState = getSnapshot(target2)
106
+ return remotedev2.init(initialState)
100
107
 
101
108
  case "ROLLBACK": {
102
109
  const state = remotedevPackage.extractState(message)
@@ -122,13 +129,12 @@ export function connectReduxDevTools(
122
129
  }
123
130
  } finally {
124
131
  handlingMonitorAction--
132
+ lastLoggedSnapshot = getSnapshot(target2)
125
133
  }
126
134
  }
127
135
 
128
- let lastLoggedSnapshot = initialState
129
-
130
136
  function log(ctx: SimpleActionContext, result: ActionTrackingResult | undefined) {
131
- if (handlingMonitorAction) {
137
+ if (disposed || handlingMonitorAction) {
132
138
  return
133
139
  }
134
140
 
@@ -160,7 +166,7 @@ export function connectReduxDevTools(
160
166
  const pathStr = "[/" + rootPath.path.join("/") + "] "
161
167
  let name = pathStr + ctx.actionName
162
168
 
163
- if (opts.logArgsNearName) {
169
+ if (logArgsNearName) {
164
170
  let args = ctx.args
165
171
  .map((a) => {
166
172
  try {
@@ -83,7 +83,7 @@ export function asReduxStore<T extends object>(
83
83
  return getSnapshot(target)
84
84
  },
85
85
  dispatch(action) {
86
- return runMiddlewares(action, runners, defaultDispatch)
86
+ return dispatch(action)
87
87
  },
88
88
  subscribe(listener) {
89
89
  return onSnapshot(target, listener)
@@ -91,25 +91,9 @@ export function asReduxStore<T extends object>(
91
91
  }
92
92
 
93
93
  const runners = middlewares.map((mw) => mw(store))
94
-
95
- return store
96
- }
97
-
98
- function runMiddlewares<T>(
99
- initialAction: ReduxAction,
100
- runners: ReadonlyArray<ReduxRunner<T>>,
101
- next: ReduxStore<T>["dispatch"]
102
- ): ReduxAction {
103
- let i = 0
104
-
105
- function runNextMiddleware(action: ReduxAction): ReduxAction {
106
- if (i < runners.length) {
107
- const runner = runners[i]
108
- i++
109
- return runner(runNextMiddleware)(action)
110
- }
111
- return next(action)
94
+ let dispatch = defaultDispatch
95
+ for (let i = runners.length - 1; i >= 0; i--) {
96
+ dispatch = runners[i](dispatch)
112
97
  }
113
-
114
- return runNextMiddleware(initialAction)
98
+ return store
115
99
  }
package/src/ref/core.ts CHANGED
@@ -88,10 +88,26 @@ export function internalCustomRef<T extends object>(
88
88
  trackedRefs.add(this as Ref<object>)
89
89
  this.internalForceUpdateBackRefs(initialTarget)
90
90
 
91
+ // MobX 4/5 do not pass the previous value to reaction effects.
92
+ // The reaction is usually created inside an action, so its first tracking run is deferred
93
+ // to the end of the batch and `initialTarget` may be stale by then; capture the value of
94
+ // that first run instead, since that is the one the effect will be compared against.
95
+ let previousTarget = initialTarget
96
+ let firstTrackingRun = true
91
97
  // according to mwestrate this won't leak as long as we don't keep the disposer around
92
98
  reaction(
93
- () => this.maybeCurrent,
94
- (newTarget, oldTarget) => {
99
+ () => {
100
+ const target = this.maybeCurrent
101
+ if (firstTrackingRun) {
102
+ firstTrackingRun = false
103
+ previousTarget = target
104
+ this.internalForceUpdateBackRefs(target)
105
+ }
106
+ return target
107
+ },
108
+ (newTarget) => {
109
+ const oldTarget = previousTarget
110
+ previousTarget = newTarget
95
111
  this.internalForceUpdateBackRefs(newTarget)
96
112
 
97
113
  if (onResolvedValueChange && newTarget !== oldTarget) {
@@ -4,9 +4,10 @@ import { HookAction } from "../action/hookActions"
4
4
  import { wrapInAction, wrapModelMethodInActionIfNeeded } from "../action/wrapInAction"
5
5
  import { type AnyModel, BaseModel } from "../model/BaseModel"
6
6
  import { WalkTreeMode, walkTree } from "../parent/walkTree"
7
+ import { forEachWithDelayedThrow } from "../utils/forEachWithDelayedThrow"
7
8
 
8
9
  const onAttachedDisposers = new WeakMap<object, () => void>()
9
- const attachedToRootStore = new WeakSet<object>()
10
+ const attachedToRootStore = new WeakMap<object, object>()
10
11
 
11
12
  /**
12
13
  * @internal
@@ -16,6 +17,7 @@ export const attachToRootStore = action(
16
17
  (rootStore: object, child: object): void => {
17
18
  // we use an array to ensure they will get called even if the actual hook modifies the tree
18
19
  const childrenToCall: AnyModel[] = []
20
+ const attachment = {}
19
21
 
20
22
  walkTree(
21
23
  child,
@@ -25,7 +27,7 @@ export const attachToRootStore = action(
25
27
  if (attachedToRootStore.has(ch)) {
26
28
  return
27
29
  }
28
- attachedToRootStore.add(ch)
30
+ attachedToRootStore.set(ch, attachment)
29
31
 
30
32
  if (ch instanceof BaseModel && (ch as any).onAttachedToRootStore) {
31
33
  wrapModelMethodInActionIfNeeded(
@@ -40,15 +42,23 @@ export const attachToRootStore = action(
40
42
  WalkTreeMode.ParentFirst
41
43
  )
42
44
 
43
- const childrenToCallLen = childrenToCall.length
44
- for (let i = 0; i < childrenToCallLen; i++) {
45
- const ch = childrenToCall[i]
45
+ forEachWithDelayedThrow(childrenToCall, (ch) => {
46
+ // An earlier hook may have detached or reattached this node already.
47
+ if (attachedToRootStore.get(ch) !== attachment) {
48
+ return
49
+ }
46
50
 
47
51
  const disposer = (ch as any).onAttachedToRootStore(rootStore)
48
52
  if (disposer) {
49
- onAttachedDisposers.set(ch, disposer)
53
+ if (attachedToRootStore.get(ch) === attachment) {
54
+ onAttachedDisposers.set(ch, disposer)
55
+ } else {
56
+ // The hook detached or reattached this node before returning its cleanup.
57
+ // Dispose this attachment without overwriting a newer attachment's cleanup.
58
+ wrapDisposer(ch, disposer)()
59
+ }
50
60
  }
51
- }
61
+ })
52
62
  }
53
63
  )
54
64
 
@@ -69,11 +79,7 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
69
79
  const disposer = onAttachedDisposers.get(ch)
70
80
  if (disposer) {
71
81
  // wrap disposer in action
72
- const disposerAction = wrapInAction({
73
- nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
74
- fn: disposer,
75
- actionType: ActionContextActionType.Sync,
76
- }).bind(ch)
82
+ const disposerAction = wrapDisposer(ch, disposer)
77
83
  onAttachedDisposers.delete(ch)
78
84
 
79
85
  disposersToCall.push(disposerAction)
@@ -82,8 +88,13 @@ export const detachFromRootStore = action("detachFromRootStore", (child: object)
82
88
  WalkTreeMode.ChildrenFirst
83
89
  )
84
90
 
85
- const disposersToCallLen = disposersToCall.length
86
- for (let i = 0; i < disposersToCallLen; i++) {
87
- disposersToCall[i]()
88
- }
91
+ forEachWithDelayedThrow(disposersToCall, (dispose) => dispose())
89
92
  })
93
+
94
+ function wrapDisposer(node: object, disposer: () => void): () => void {
95
+ return wrapInAction({
96
+ nameOrNameFn: HookAction.OnAttachedToRootStoreDisposer,
97
+ fn: disposer,
98
+ actionType: ActionContextActionType.Sync,
99
+ }).bind(node)
100
+ }
@@ -45,9 +45,11 @@ export const registerRootStore: <T extends object>(node: T) => T = action(
45
45
 
46
46
  entry.is = true
47
47
 
48
- attachToRootStore(node, node)
49
-
50
- entry.atom?.reportChanged()
48
+ try {
49
+ attachToRootStore(node, node)
50
+ } finally {
51
+ entry.atom?.reportChanged()
52
+ }
51
53
  return node
52
54
  }
53
55
  )
@@ -65,9 +67,11 @@ export const unregisterRootStore: (node: object) => void = action("unregisterRoo
65
67
  const entry = getOrCreateRootStoreEntry(node)
66
68
  entry.is = false
67
69
 
68
- detachFromRootStore(node)
69
-
70
- entry.atom?.reportChanged()
70
+ try {
71
+ detachFromRootStore(node)
72
+ } finally {
73
+ entry.atom?.reportChanged()
74
+ }
71
75
  })
72
76
 
73
77
  /**
@@ -69,13 +69,13 @@ export type SnapshotInOfModel<M extends AnyModel> = ModelFromSnapshot<M>
69
69
  export interface SnapshotInOfFrozen<F extends Frozen<any>> extends FrozenData<F["data"]> {}
70
70
 
71
71
  export interface SnapshotInOfObjectMap<V> {
72
- items?: { [k: string]: SnapshotOutOf<V> }
72
+ items?: { [k: string]: SnapshotInOf<V> }
73
73
  [modelTypeKey]?: string
74
74
  [modelIdKey]: string
75
75
  }
76
76
 
77
77
  export interface SnapshotInOfArraySet<V> {
78
- items?: SnapshotOutOf<V>[]
78
+ items?: SnapshotInOf<V>[]
79
79
  [modelTypeKey]?: string
80
80
  [modelIdKey]: string
81
81
  }
@@ -129,7 +129,7 @@ export function internalApplySnapshot<T extends object>(
129
129
  }
130
130
 
131
131
  const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
132
- if (modelIdPropertyName) {
132
+ if (modelIdPropertyName !== undefined) {
133
133
  const id = (sn as any)[modelIdPropertyName]
134
134
  if (obj[modelIdKey] !== id) {
135
135
  // different id, no reconciliation possible
@@ -24,8 +24,7 @@ export function clone<T extends object>(node: T, options?: Partial<CloneOptions>
24
24
  assertTweakedObject(node, "node")
25
25
 
26
26
  const opts = {
27
- generateNewIds: true,
28
- ...options,
27
+ generateNewIds: options?.generateNewIds ?? true,
29
28
  }
30
29
 
31
30
  const sn = getSnapshot(node)
@@ -10,8 +10,7 @@ import { fromSnapshot } from "./fromSnapshot"
10
10
  */
11
11
  export function cloneTreeValue<T>(value: T, options?: Partial<CloneOptions>): T {
12
12
  const cloneOptions: CloneOptions = {
13
- generateNewIds: true,
14
- ...options,
13
+ generateNewIds: options?.generateNewIds ?? true,
15
14
  }
16
15
 
17
16
  if (isPrimitive(value)) {