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.
- package/CHANGELOG.md +123 -0
- package/dist/mobx-keystone.esm.js +1478 -952
- package/dist/mobx-keystone.esm.mjs +1478 -952
- package/dist/mobx-keystone.umd.js +1483 -952
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/patch/applyPatches.d.ts +2 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
- package/dist/types/utils/index.d.ts +13 -0
- package/dist/types/utils/mapUtils.d.ts +2 -2
- package/dist/types/utils/setLike.d.ts +1 -0
- package/package.json +1 -1
- package/src/action/applyAction.ts +1 -1
- package/src/action/middleware.ts +17 -36
- package/src/action/modelFlow.ts +4 -5
- package/src/action/mutationBatch.ts +10 -10
- package/src/action/pendingActions.ts +3 -4
- package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
- package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
- package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
- package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
- package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
- package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
- package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
- package/src/actionMiddlewares/undoMiddleware.ts +65 -46
- package/src/actionMiddlewares/utils.ts +2 -1
- package/src/computedTree/computedTree.ts +14 -3
- package/src/context/context.ts +17 -45
- package/src/dataModel/BaseDataModel.ts +20 -16
- package/src/dataModel/dataModelInstanceCache.ts +8 -0
- package/src/deepChange/onDeepChange.ts +179 -24
- package/src/frozen/Frozen.ts +50 -33
- package/src/model/BaseModel.ts +4 -9
- package/src/model/newModel.ts +21 -24
- package/src/model/utils.ts +2 -2
- package/src/modelShared/modelClassInitializer.ts +3 -2
- package/src/modelShared/modelDecorator.ts +34 -14
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +44 -28
- package/src/parent/path.ts +5 -3
- package/src/parent/setParent.ts +7 -0
- package/src/parent/walkTree.ts +21 -37
- package/src/patch/applyPatches.ts +69 -34
- package/src/patch/emitPatch.ts +142 -26
- package/src/patch/jsonPatch.ts +5 -0
- package/src/patch/patchRecorder.ts +18 -14
- package/src/redux/connectReduxDevTools.ts +24 -18
- package/src/redux/redux.ts +5 -21
- package/src/ref/core.ts +18 -2
- package/src/rootStore/attachDetach.ts +27 -16
- package/src/rootStore/rootStore.ts +10 -6
- package/src/snapshot/SnapshotOf.ts +2 -2
- package/src/snapshot/applySnapshot.ts +1 -1
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/internal.ts +22 -9
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +6 -6
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/standardActions/arrayActions.ts +12 -2
- package/src/standardActions/objectActions.ts +3 -2
- package/src/transforms/ImmutableDate.ts +4 -0
- package/src/transforms/asMap.ts +3 -1
- package/src/treeUtils/deepEquals.ts +15 -13
- package/src/treeUtils/draft.ts +8 -0
- package/src/treeUtils/sandbox.ts +31 -27
- package/src/tweaker/core.ts +2 -4
- package/src/tweaker/tweak.ts +2 -1
- package/src/tweaker/tweakArray.ts +83 -20
- package/src/tweaker/tweakPlainObject.ts +76 -23
- package/src/tweaker/typeChecking.ts +124 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/createCachedTypeCheck.ts +27 -2
- package/src/types/objectBased/typesDataModelData.ts +4 -2
- package/src/types/objectBased/typesModel.ts +4 -2
- package/src/types/objectBased/typesObject.ts +20 -16
- package/src/types/objectBased/typesRecord.ts +6 -13
- package/src/types/objectBased/typesRef.ts +2 -2
- package/src/types/primitiveBased/typesEnum.ts +4 -2
- package/src/types/primitiveBased/typesPrimitive.ts +9 -4
- package/src/types/typeCheckErrorUtils.ts +1 -0
- package/src/types/utility/typesCodecSupport.ts +97 -56
- package/src/types/utility/typesMap.ts +24 -41
- package/src/types/utility/typesRefinement.ts +3 -2
- package/src/types/utility/typesSet.ts +24 -0
- package/src/utils/ModelPool.ts +3 -3
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/decorators.ts +1 -1
- package/src/utils/errorDiagnostics.ts +6 -2
- package/src/utils/forEachWithDelayedThrow.ts +83 -0
- package/src/utils/index.ts +34 -2
- package/src/utils/mapUtils.ts +3 -7
- package/src/utils/setIfDifferent.ts +3 -2
- package/src/utils/setLike.ts +134 -0
- package/src/wrappers/ArraySet.ts +5 -9
- package/src/wrappers/ObjectMap.ts +15 -11
- package/src/wrappers/asMap.ts +30 -18
- package/src/wrappers/asSet.ts +36 -12
- package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
- package/src/action/modelFlowPromiseGenerator.ts +0 -127
package/src/patch/emitPatch.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
178
|
+
const subscription = { listener, synchronous, active: true }
|
|
179
|
+
listenersForObject.push(subscription)
|
|
130
180
|
patchListenerCount++
|
|
131
181
|
return () => {
|
|
132
|
-
|
|
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
|
-
|
|
211
|
+
const subscription = { listener, synchronous, active: true }
|
|
212
|
+
globalPatchListeners.push(subscription)
|
|
153
213
|
return () => {
|
|
154
|
-
|
|
214
|
+
subscription.active = false
|
|
215
|
+
deleteFromArray(globalPatchListeners, subscription)
|
|
155
216
|
}
|
|
156
217
|
}
|
|
157
218
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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)
|
package/src/patch/jsonPatch.ts
CHANGED
|
@@ -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 =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
169
|
+
if (logArgsNearName) {
|
|
164
170
|
let args = ctx.args
|
|
165
171
|
.map((a) => {
|
|
166
172
|
try {
|
package/src/redux/redux.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function asReduxStore<T extends object>(
|
|
|
83
83
|
return getSnapshot(target)
|
|
84
84
|
},
|
|
85
85
|
dispatch(action) {
|
|
86
|
-
return
|
|
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
|
-
|
|
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
|
-
() =>
|
|
94
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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]:
|
|
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?:
|
|
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
|
package/src/snapshot/clone.ts
CHANGED
|
@@ -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)) {
|