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
@@ -3,10 +3,11 @@ import { BuiltInAction } from "../action/builtInActions"
3
3
  import { ActionContextActionType } from "../action/context"
4
4
  import { wrapInAction } from "../action/wrapInAction"
5
5
  import { modelToDataNode } from "../parent/core"
6
- import type { PathElement } from "../parent/pathTypes"
7
6
  import type { Patch } from "../patch/Patch"
7
+ import { internalApplySnapshot } from "../snapshot/applySnapshot"
8
8
  import { reconcileSnapshot } from "../snapshot/reconcileSnapshot"
9
9
  import { assertTweakedObject } from "../tweaker/core"
10
+ import { withTypeCheckingBatch } from "../tweaker/typeChecking"
10
11
  import { failure, inDevMode, isArray, lazy } from "../utils"
11
12
  import { runWithErrorDiagnosticsContext, withErrorPathSegments } from "../utils/errorDiagnostics"
12
13
  import { ModelPool } from "../utils/ModelPool"
@@ -14,6 +15,8 @@ import { setIfDifferent } from "../utils/setIfDifferent"
14
15
 
15
16
  /**
16
17
  * Applies the given patches to the given target object.
18
+ * Automatic type checking validates the completed batch, including nested lists.
19
+ * A rejected batch is rolled back with compensating patches.
17
20
  *
18
21
  * @param node Target object.
19
22
  * @param patches List of patches to apply.
@@ -45,33 +48,35 @@ export function internalApplyPatches(
45
48
  const obj = this
46
49
  const modelPool = new ModelPool(obj)
47
50
 
48
- if (reverse) {
49
- let i = patches.length
50
- while (i--) {
51
- const p = patches[i]
52
- if (isArray(p)) {
53
- let j = p.length
54
- while (j--) {
55
- applySinglePatchWithPath(obj, p[j], modelPool)
51
+ withTypeCheckingBatch(() => {
52
+ if (reverse) {
53
+ let i = patches.length
54
+ while (i--) {
55
+ const p = patches[i]
56
+ if (isArray(p)) {
57
+ let j = p.length
58
+ while (j--) {
59
+ applySinglePatchWithPath(obj, p[j], modelPool)
60
+ }
61
+ } else {
62
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
56
63
  }
57
- } else {
58
- applySinglePatchWithPath(obj, p as Patch, modelPool)
59
64
  }
60
- }
61
- } else {
62
- const len = patches.length
63
- for (let i = 0; i < len; i++) {
64
- const p = patches[i]
65
- if (isArray(p)) {
66
- const len2 = p.length
67
- for (let j = 0; j < len2; j++) {
68
- applySinglePatchWithPath(obj, p[j], modelPool)
65
+ } else {
66
+ const len = patches.length
67
+ for (let i = 0; i < len; i++) {
68
+ const p = patches[i]
69
+ if (isArray(p)) {
70
+ const len2 = p.length
71
+ for (let j = 0; j < len2; j++) {
72
+ applySinglePatchWithPath(obj, p[j], modelPool)
73
+ }
74
+ } else {
75
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
69
76
  }
70
- } else {
71
- applySinglePatchWithPath(obj, p as Patch, modelPool)
72
77
  }
73
78
  }
74
- }
79
+ })
75
80
  })
76
81
  }
77
82
 
@@ -84,16 +89,38 @@ const wrappedInternalApplyPatches = lazy(() =>
84
89
  )
85
90
 
86
91
  function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void {
87
- const { target, prop } = pathArrayToObjectAndProp(obj, patch.path)
92
+ const path = patch.path
93
+ if (inDevMode && !isArray(path)) {
94
+ throw failure(`invalid path: ${path}`)
95
+ }
96
+
97
+ if (path.length === 0) {
98
+ if (patch.op === "add" || patch.op === "replace") {
99
+ internalApplySnapshot.call(obj, patch.value)
100
+ return
101
+ }
102
+ throw failure("a patch cannot remove the root node")
103
+ }
104
+
105
+ let target: any = modelToDataNode(obj)
106
+ for (let i = 0; i < path.length - 1; i++) {
107
+ target = modelToDataNode(target[path[i]])
108
+ }
109
+ const prop = path[path.length - 1]
88
110
 
89
111
  if (isArray(target)) {
112
+ if (patch.op === "replace" && prop === "length") {
113
+ target.length = patch.value
114
+ return
115
+ }
116
+
117
+ const index = getPatchArrayIndex(prop, target.length, patch.op)
90
118
  switch (patch.op) {
91
119
  case "add": {
92
- const index = +prop!
93
120
  // reconcile from the pool if possible
94
121
  const newValue = reconcileSnapshot(undefined, patch.value, modelPool, target)
95
122
  if (index < 0) {
96
- // extension needed by mobx-keystone-yjs
123
+ // non-standard extension: a negative index appends, just like "-"
97
124
  target.push(newValue)
98
125
  } else {
99
126
  target.splice(index, 0, newValue)
@@ -102,21 +129,15 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
102
129
  }
103
130
 
104
131
  case "remove": {
105
- const index = +prop!
106
132
  // no reconciliation, removing
107
133
  target.splice(index, 1)
108
134
  break
109
135
  }
110
136
 
111
137
  case "replace": {
112
- if (prop === "length") {
113
- target.length = patch.value
114
- } else {
115
- const index = +prop!
116
- // try to reconcile
117
- const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target)
118
- setIfDifferent(target, index as any, newValue)
119
- }
138
+ // try to reconcile
139
+ const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target)
140
+ setIfDifferent(target, index as any, newValue)
120
141
  break
121
142
  }
122
143
 
@@ -152,36 +173,30 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
152
173
  }
153
174
  }
154
175
 
155
- function applySinglePatchWithPath(obj: object, patch: Patch, modelPool: ModelPool): void {
156
- withErrorPathSegments(patch.path, () => {
157
- applySinglePatch(obj, patch, modelPool)
158
- })
159
- }
176
+ // canonical decimal integers only, so "01", "1.0" or "1e0" are not mistaken for indexes
177
+ const patchArrayIndexPattern = /^(?:0|-?[1-9]\d*)$/
160
178
 
161
- function pathArrayToObjectAndProp(
162
- obj: object,
163
- path: Patch["path"]
164
- ): { target: any; prop?: PathElement } {
165
- if (inDevMode) {
166
- if (!isArray(path)) {
167
- throw failure(`invalid path: ${path}`)
168
- }
179
+ function getPatchArrayIndex(prop: string | number, length: number, op: Patch["op"]): number {
180
+ // JSON Patch spells "at the end of the array" as "-", which only makes sense when adding
181
+ if (prop === "-" && op === "add") {
182
+ return length
169
183
  }
170
184
 
171
- let target: any = modelToDataNode(obj)
185
+ const index =
186
+ typeof prop === "string" && !patchArrayIndexPattern.test(prop) ? Number.NaN : Number(prop)
172
187
 
173
- if (path.length === 0) {
174
- return {
175
- target,
176
- }
188
+ // additions may also target the end of the array, plus negative indexes as an append extension
189
+ const valid =
190
+ Number.isInteger(index) && (op === "add" ? index <= length : index >= 0 && index < length)
191
+ if (!valid) {
192
+ throw failure(`invalid array index '${String(prop)}' for '${op}' patch`)
177
193
  }
178
194
 
179
- for (let i = 0; i <= path.length - 2; i++) {
180
- target = modelToDataNode(target[path[i]])
181
- }
195
+ return index
196
+ }
182
197
 
183
- return {
184
- target,
185
- prop: path[path.length - 1],
186
- }
198
+ function applySinglePatchWithPath(obj: object, patch: Patch, modelPool: ModelPool): void {
199
+ withErrorPathSegments(patch.path, () => {
200
+ applySinglePatch(obj, patch, modelPool)
201
+ })
187
202
  }
@@ -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
  }