mobx-keystone 1.25.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 (141) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/dist/mobx-keystone.esm.js +3627 -1540
  3. package/dist/mobx-keystone.esm.mjs +3627 -1540
  4. package/dist/mobx-keystone.umd.js +3638 -1543
  5. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
  8. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  9. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/detach.d.ts +1 -1
  12. package/dist/types/parent/findChildren.d.ts +1 -1
  13. package/dist/types/parent/treeOrder.d.ts +1 -0
  14. package/dist/types/patch/applyPatches.d.ts +3 -0
  15. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  16. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  17. package/dist/types/ref/nodesById.d.ts +1 -0
  18. package/dist/types/ref/refIdIndex.d.ts +1 -0
  19. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  20. package/dist/types/snapshot/applySnapshot.d.ts +1 -0
  21. package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
  22. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  23. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  24. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  25. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  26. package/dist/types/types/TypeCheckError.d.ts +1 -0
  27. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  28. package/dist/types/utils/ModelPool.d.ts +7 -1
  29. package/dist/types/utils/chunks.d.ts +1 -0
  30. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  31. package/dist/types/utils/index.d.ts +13 -0
  32. package/dist/types/utils/mapUtils.d.ts +2 -2
  33. package/dist/types/utils/setLike.d.ts +1 -0
  34. package/package.json +2 -2
  35. package/src/action/applyAction.ts +1 -1
  36. package/src/action/middleware.ts +17 -36
  37. package/src/action/modelFlow.ts +4 -5
  38. package/src/action/mutationBatch.ts +10 -10
  39. package/src/action/pendingActions.ts +3 -4
  40. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  41. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  42. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  43. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  44. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  45. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  46. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  47. package/src/actionMiddlewares/transactionMiddleware.ts +55 -25
  48. package/src/actionMiddlewares/undoMiddleware.ts +112 -59
  49. package/src/actionMiddlewares/utils.ts +2 -1
  50. package/src/computedTree/computedTree.ts +14 -3
  51. package/src/context/context.ts +17 -45
  52. package/src/dataModel/BaseDataModel.ts +20 -16
  53. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  54. package/src/deepChange/onDeepChange.ts +179 -24
  55. package/src/frozen/Frozen.ts +50 -33
  56. package/src/model/BaseModel.ts +10 -13
  57. package/src/model/newModel.ts +21 -24
  58. package/src/model/utils.ts +2 -2
  59. package/src/modelShared/modelClassInitializer.ts +3 -2
  60. package/src/modelShared/modelDecorator.ts +39 -39
  61. package/src/modelShared/modelInfo.ts +1 -1
  62. package/src/modelShared/prop.ts +6 -9
  63. package/src/modelShared/sharedInternalModel.ts +16 -3
  64. package/src/parent/arrayPaths.ts +242 -0
  65. package/src/parent/coreObjectChildren.ts +300 -48
  66. package/src/parent/detach.ts +1 -1
  67. package/src/parent/findChildren.ts +4 -7
  68. package/src/parent/onChildAttachedTo.ts +157 -55
  69. package/src/parent/path.ts +15 -5
  70. package/src/parent/setParent.ts +34 -15
  71. package/src/parent/treeOrder.ts +53 -0
  72. package/src/parent/walkTree.ts +12 -101
  73. package/src/patch/applyPatches.ts +74 -34
  74. package/src/patch/emitPatch.ts +142 -26
  75. package/src/patch/jsonPatch.ts +5 -0
  76. package/src/patch/patchRecorder.ts +18 -14
  77. package/src/redux/connectReduxDevTools.ts +24 -18
  78. package/src/redux/redux.ts +5 -21
  79. package/src/ref/core.ts +81 -29
  80. package/src/ref/modelRefIdIndex.ts +131 -0
  81. package/src/ref/nodesById.ts +60 -0
  82. package/src/ref/refIdIndex.ts +209 -0
  83. package/src/rootStore/attachDetach.ts +27 -16
  84. package/src/rootStore/rootStore.ts +10 -6
  85. package/src/snapshot/SnapshotOf.ts +2 -2
  86. package/src/snapshot/applySnapshot.ts +27 -19
  87. package/src/snapshot/clone.ts +1 -2
  88. package/src/snapshot/cloneTreeValue.ts +1 -2
  89. package/src/snapshot/fromModelSnapshot.ts +6 -2
  90. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  91. package/src/snapshot/fromSnapshot.ts +2 -6
  92. package/src/snapshot/internal.ts +89 -19
  93. package/src/snapshot/onSnapshot.ts +12 -1
  94. package/src/snapshot/reconcileModelSnapshot.ts +6 -6
  95. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  96. package/src/standardActions/arrayActions.ts +12 -2
  97. package/src/standardActions/objectActions.ts +3 -2
  98. package/src/transforms/ImmutableDate.ts +4 -0
  99. package/src/transforms/asMap.ts +3 -1
  100. package/src/treeUtils/deepEquals.ts +15 -13
  101. package/src/treeUtils/draft.ts +8 -0
  102. package/src/treeUtils/sandbox.ts +31 -27
  103. package/src/tweaker/changeRollback.ts +218 -0
  104. package/src/tweaker/core.ts +33 -22
  105. package/src/tweaker/treeNodeMetadata.ts +12 -2
  106. package/src/tweaker/tweak.ts +183 -19
  107. package/src/tweaker/tweakArray.ts +176 -33
  108. package/src/tweaker/tweakPlainObject.ts +117 -39
  109. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  110. package/src/tweaker/typeChecking.ts +25 -15
  111. package/src/types/TypeCheckError.ts +17 -19
  112. package/src/types/arrayBased/typesArray.ts +10 -11
  113. package/src/types/createCachedTypeCheck.ts +541 -88
  114. package/src/types/objectBased/typesDataModelData.ts +4 -2
  115. package/src/types/objectBased/typesModel.ts +4 -2
  116. package/src/types/objectBased/typesObject.ts +20 -16
  117. package/src/types/objectBased/typesRecord.ts +8 -18
  118. package/src/types/objectBased/typesRef.ts +2 -2
  119. package/src/types/primitiveBased/typesEnum.ts +4 -2
  120. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  121. package/src/types/typeCheckErrorUtils.ts +1 -0
  122. package/src/types/utility/typesCodecSupport.ts +97 -56
  123. package/src/types/utility/typesMap.ts +24 -41
  124. package/src/types/utility/typesRefinement.ts +3 -2
  125. package/src/types/utility/typesSet.ts +24 -0
  126. package/src/utils/ModelPool.ts +78 -22
  127. package/src/utils/chainFns.ts +4 -0
  128. package/src/utils/chunks.ts +271 -0
  129. package/src/utils/decorators.ts +1 -1
  130. package/src/utils/errorDiagnostics.ts +6 -2
  131. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  132. package/src/utils/index.ts +71 -2
  133. package/src/utils/mapUtils.ts +3 -7
  134. package/src/utils/setIfDifferent.ts +3 -2
  135. package/src/utils/setLike.ts +134 -0
  136. package/src/wrappers/ArraySet.ts +5 -9
  137. package/src/wrappers/ObjectMap.ts +15 -11
  138. package/src/wrappers/asMap.ts +30 -18
  139. package/src/wrappers/asSet.ts +36 -12
  140. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  141. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -4,7 +4,9 @@ import { ActionContextActionType } from "../action/context"
4
4
  import { wrapInAction } from "../action/wrapInAction"
5
5
  import { modelToDataNode } from "../parent/core"
6
6
  import type { Patch } from "../patch/Patch"
7
+ import { internalApplySnapshot } from "../snapshot/applySnapshot"
7
8
  import { reconcileSnapshot } from "../snapshot/reconcileSnapshot"
9
+ import { withChangeBatch } from "../tweaker/changeRollback"
8
10
  import { assertTweakedObject } from "../tweaker/core"
9
11
  import { failure, inDevMode, isArray, lazy } from "../utils"
10
12
  import { runWithErrorDiagnosticsContext, withErrorPathSegments } from "../utils/errorDiagnostics"
@@ -13,6 +15,9 @@ import { setIfDifferent } from "../utils/setIfDifferent"
13
15
 
14
16
  /**
15
17
  * Applies the given patches to the given target object.
18
+ * Automatic type checking validates the completed batch, including nested lists.
19
+ * A batch that throws or is rejected is rolled back with compensating patches.
20
+ * Errors thrown by listeners do not roll it back.
16
21
  *
17
22
  * @param node Target object.
18
23
  * @param patches List of patches to apply.
@@ -44,32 +49,38 @@ export function internalApplyPatches(
44
49
  const obj = this
45
50
  const modelPool = new ModelPool(obj)
46
51
 
47
- if (reverse) {
48
- let i = patches.length
49
- while (i--) {
50
- const p = patches[i]
51
- if (isArray(p)) {
52
- let j = p.length
53
- while (j--) {
54
- applySinglePatchWithPath(obj, p[j], modelPool)
52
+ try {
53
+ withChangeBatch(() => {
54
+ if (reverse) {
55
+ let i = patches.length
56
+ while (i--) {
57
+ const p = patches[i]
58
+ if (isArray(p)) {
59
+ let j = p.length
60
+ while (j--) {
61
+ applySinglePatchWithPath(obj, p[j], modelPool)
62
+ }
63
+ } else {
64
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
65
+ }
55
66
  }
56
67
  } else {
57
- applySinglePatchWithPath(obj, p as Patch, modelPool)
58
- }
59
- }
60
- } else {
61
- const len = patches.length
62
- for (let i = 0; i < len; i++) {
63
- const p = patches[i]
64
- if (isArray(p)) {
65
- const len2 = p.length
66
- for (let j = 0; j < len2; j++) {
67
- applySinglePatchWithPath(obj, p[j], modelPool)
68
+ const len = patches.length
69
+ for (let i = 0; i < len; i++) {
70
+ const p = patches[i]
71
+ if (isArray(p)) {
72
+ const len2 = p.length
73
+ for (let j = 0; j < len2; j++) {
74
+ applySinglePatchWithPath(obj, p[j], modelPool)
75
+ }
76
+ } else {
77
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
78
+ }
68
79
  }
69
- } else {
70
- applySinglePatchWithPath(obj, p as Patch, modelPool)
71
80
  }
72
- }
81
+ })
82
+ } finally {
83
+ modelPool.release()
73
84
  }
74
85
  })
75
86
  }
@@ -88,20 +99,33 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
88
99
  throw failure(`invalid path: ${path}`)
89
100
  }
90
101
 
102
+ if (path.length === 0) {
103
+ if (patch.op === "add" || patch.op === "replace") {
104
+ internalApplySnapshot.call(obj, patch.value)
105
+ return
106
+ }
107
+ throw failure("a patch cannot remove the root node")
108
+ }
109
+
91
110
  let target: any = modelToDataNode(obj)
92
111
  for (let i = 0; i < path.length - 1; i++) {
93
112
  target = modelToDataNode(target[path[i]])
94
113
  }
95
- const prop = path.length > 0 ? path[path.length - 1] : undefined
114
+ const prop = path[path.length - 1]
96
115
 
97
116
  if (isArray(target)) {
117
+ if (patch.op === "replace" && prop === "length") {
118
+ target.length = patch.value
119
+ return
120
+ }
121
+
122
+ const index = getPatchArrayIndex(prop, target.length, patch.op)
98
123
  switch (patch.op) {
99
124
  case "add": {
100
- const index = +prop!
101
125
  // reconcile from the pool if possible
102
126
  const newValue = reconcileSnapshot(undefined, patch.value, modelPool, target)
103
127
  if (index < 0) {
104
- // extension needed by mobx-keystone-yjs
128
+ // non-standard extension: a negative index appends, just like "-"
105
129
  target.push(newValue)
106
130
  } else {
107
131
  target.splice(index, 0, newValue)
@@ -110,21 +134,15 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
110
134
  }
111
135
 
112
136
  case "remove": {
113
- const index = +prop!
114
137
  // no reconciliation, removing
115
138
  target.splice(index, 1)
116
139
  break
117
140
  }
118
141
 
119
142
  case "replace": {
120
- if (prop === "length") {
121
- target.length = patch.value
122
- } else {
123
- const index = +prop!
124
- // try to reconcile
125
- const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target)
126
- setIfDifferent(target, index as any, newValue)
127
- }
143
+ // try to reconcile
144
+ const newValue = reconcileSnapshot(target[index], patch.value, modelPool, target)
145
+ setIfDifferent(target, index as any, newValue)
128
146
  break
129
147
  }
130
148
 
@@ -160,6 +178,28 @@ function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void
160
178
  }
161
179
  }
162
180
 
181
+ // canonical decimal integers only, so "01", "1.0" or "1e0" are not mistaken for indexes
182
+ const patchArrayIndexPattern = /^(?:0|-?[1-9]\d*)$/
183
+
184
+ function getPatchArrayIndex(prop: string | number, length: number, op: Patch["op"]): number {
185
+ // JSON Patch spells "at the end of the array" as "-", which only makes sense when adding
186
+ if (prop === "-" && op === "add") {
187
+ return length
188
+ }
189
+
190
+ const index =
191
+ typeof prop === "string" && !patchArrayIndexPattern.test(prop) ? Number.NaN : Number(prop)
192
+
193
+ // additions may also target the end of the array, plus negative indexes as an append extension
194
+ const valid =
195
+ Number.isInteger(index) && (op === "add" ? index <= length : index >= 0 && index < length)
196
+ if (!valid) {
197
+ throw failure(`invalid array index '${String(prop)}' for '${op}' patch`)
198
+ }
199
+
200
+ return index
201
+ }
202
+
163
203
  function applySinglePatchWithPath(obj: object, patch: Patch, modelPool: ModelPool): void {
164
204
  withErrorPathSegments(patch.path, () => {
165
205
  applySinglePatch(obj, patch, modelPool)
@@ -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
  }