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.
- package/CHANGELOG.md +137 -0
- package/README.md +5 -1
- package/dist/mobx-keystone.esm.js +1608 -1072
- package/dist/mobx-keystone.esm.mjs +1608 -1072
- package/dist/mobx-keystone.umd.js +1613 -1072
- 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 +7 -7
- 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 +76 -60
- 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 +15 -14
- package/src/model/newModel.ts +32 -28
- 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/findParent.ts +14 -9
- package/src/parent/onChildAttachedTo.ts +45 -40
- package/src/parent/path.ts +11 -7
- package/src/parent/setParent.ts +33 -14
- package/src/parent/walkTree.ts +21 -37
- package/src/patch/applyPatches.ts +75 -60
- 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 +10 -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/fromSnapshot.ts +3 -1
- package/src/snapshot/internal.ts +22 -9
- package/src/snapshot/modelInitialData.ts +14 -19
- package/src/snapshot/onSnapshot.ts +12 -1
- package/src/snapshot/reconcileModelSnapshot.ts +16 -8
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
- package/src/snapshot/reconcileSnapshot.ts +13 -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 +8 -8
- package/src/tweaker/tweakArray.ts +86 -37
- package/src/tweaker/tweakFrozen.ts +0 -1
- package/src/tweaker/tweakModel.ts +0 -1
- package/src/tweaker/tweakPlainObject.ts +78 -29
- package/src/tweaker/typeChecking.ts +132 -33
- 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
|
@@ -2,7 +2,8 @@ import { action, isAction } from "mobx"
|
|
|
2
2
|
import { fastGetParentPath, type ParentPath } from "../parent/path"
|
|
3
3
|
import type { Path, PathElement, WritablePath } from "../parent/pathTypes"
|
|
4
4
|
import { assertTweakedObject } from "../tweaker/core"
|
|
5
|
-
import { assertIsFunction,
|
|
5
|
+
import { assertIsFunction, failure } from "../utils"
|
|
6
|
+
import { addDelayedError, type DelayedError } from "../utils/forEachWithDelayedThrow"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Disposer function to stop listening to deep changes.
|
|
@@ -18,6 +19,8 @@ export enum DeepChangeType {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface DeepChangeBase {
|
|
22
|
+
/** Whether this notification overlaps a mutation made by a change listener. */
|
|
23
|
+
readonly isReentrant?: boolean
|
|
21
24
|
readonly type: DeepChangeType
|
|
22
25
|
readonly path: Path
|
|
23
26
|
/**
|
|
@@ -97,8 +100,17 @@ export type DeepChange =
|
|
|
97
100
|
*/
|
|
98
101
|
export type OnDeepChangeListener = (change: DeepChange) => void
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
interface ListenerSubscription<Listener> {
|
|
104
|
+
readonly listener: Listener
|
|
105
|
+
active: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const deepChangeListeners = new WeakMap<object, ListenerSubscription<OnDeepChangeListener>[]>()
|
|
109
|
+
|
|
110
|
+
let globalDeepChangeListeners: ListenerSubscription<
|
|
111
|
+
(target: object, change: DeepChange) => void
|
|
112
|
+
>[] = []
|
|
113
|
+
|
|
102
114
|
const emptyPath: Path = Object.freeze([])
|
|
103
115
|
|
|
104
116
|
let deepChangeListenerCount = 0
|
|
@@ -143,6 +155,56 @@ export function exitInitPhase(): void {
|
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
157
|
|
|
158
|
+
interface DeepChangeEmission {
|
|
159
|
+
reentrant: boolean
|
|
160
|
+
/** Failures thrown by the listeners of this emission, reported once it has been delivered. */
|
|
161
|
+
error: DelayedError
|
|
162
|
+
parent: DeepChangeEmission | undefined
|
|
163
|
+
/** Shared by every change of this emission, so they all observe `reentrant` live. */
|
|
164
|
+
isReentrantDescriptor: PropertyDescriptor | undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let currentEmission: DeepChangeEmission | undefined
|
|
168
|
+
|
|
169
|
+
function rethrowDeepChangeListenerError(emission: DeepChangeEmission): void {
|
|
170
|
+
const error = emission.error
|
|
171
|
+
if (error) {
|
|
172
|
+
// Listeners may keep change objects around, and those keep this emission alive
|
|
173
|
+
// through their `isReentrant` getter, so don't retain the errors with it.
|
|
174
|
+
emission.error = undefined
|
|
175
|
+
throw error.value
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function beginDeepChangeEmission(): DeepChangeEmission {
|
|
180
|
+
for (let parent = currentEmission; parent; parent = parent.parent) parent.reentrant = true
|
|
181
|
+
const emission: DeepChangeEmission = {
|
|
182
|
+
reentrant: currentEmission !== undefined,
|
|
183
|
+
error: undefined,
|
|
184
|
+
parent: currentEmission,
|
|
185
|
+
isReentrantDescriptor: undefined,
|
|
186
|
+
}
|
|
187
|
+
currentEmission = emission
|
|
188
|
+
return emission
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// `isReentrant` is deliberately non-enumerable so that object spread and JSON
|
|
192
|
+
// serialization of a change do not copy it.
|
|
193
|
+
function attachEmission(change: DeepChange): DeepChange {
|
|
194
|
+
const emission = currentEmission!
|
|
195
|
+
emission.isReentrantDescriptor ??= {
|
|
196
|
+
get: () => emission.reentrant,
|
|
197
|
+
configurable: true,
|
|
198
|
+
}
|
|
199
|
+
Object.defineProperty(change, "isReentrant", emission.isReentrantDescriptor)
|
|
200
|
+
return change
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function changeWithPath(change: DeepChange, path: Path): DeepChange {
|
|
204
|
+
// The copy belongs to the same (still current) emission as the original.
|
|
205
|
+
return attachEmission({ ...change, path })
|
|
206
|
+
}
|
|
207
|
+
|
|
146
208
|
/**
|
|
147
209
|
* @internal
|
|
148
210
|
*/
|
|
@@ -151,10 +213,17 @@ export function emitDeepChange(obj: object, change: DeepChange): void {
|
|
|
151
213
|
return
|
|
152
214
|
}
|
|
153
215
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
216
|
+
const emission = beginDeepChangeEmission()
|
|
217
|
+
try {
|
|
218
|
+
attachEmission(change)
|
|
219
|
+
emitGlobalDeepChange(obj, change)
|
|
220
|
+
if (deepChangeListenerCount > 0) {
|
|
221
|
+
emitDeepChangeToListeners(obj, change)
|
|
222
|
+
}
|
|
223
|
+
} finally {
|
|
224
|
+
currentEmission = emission.parent
|
|
157
225
|
}
|
|
226
|
+
rethrowDeepChangeListenerError(emission)
|
|
158
227
|
}
|
|
159
228
|
|
|
160
229
|
/**
|
|
@@ -238,6 +307,38 @@ function emitDeepChangeFromValues(
|
|
|
238
307
|
oldValue: unknown,
|
|
239
308
|
addedValues?: unknown[],
|
|
240
309
|
removedValues?: unknown[]
|
|
310
|
+
): void {
|
|
311
|
+
if (!hasAnyDeepChangeListeners()) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const emission = beginDeepChangeEmission()
|
|
316
|
+
try {
|
|
317
|
+
emitDeepChangeFromValuesInEmission(
|
|
318
|
+
node,
|
|
319
|
+
type,
|
|
320
|
+
target,
|
|
321
|
+
keyOrIndex,
|
|
322
|
+
newValue,
|
|
323
|
+
oldValue,
|
|
324
|
+
addedValues,
|
|
325
|
+
removedValues
|
|
326
|
+
)
|
|
327
|
+
} finally {
|
|
328
|
+
currentEmission = emission.parent
|
|
329
|
+
}
|
|
330
|
+
rethrowDeepChangeListenerError(emission)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function emitDeepChangeFromValuesInEmission(
|
|
334
|
+
node: object,
|
|
335
|
+
type: DeepChangeType,
|
|
336
|
+
target: object | unknown[],
|
|
337
|
+
keyOrIndex: string | number,
|
|
338
|
+
newValue: unknown,
|
|
339
|
+
oldValue: unknown,
|
|
340
|
+
addedValues?: unknown[],
|
|
341
|
+
removedValues?: unknown[]
|
|
241
342
|
): void {
|
|
242
343
|
let change: DeepChange | undefined
|
|
243
344
|
|
|
@@ -295,6 +396,20 @@ function createDeepChange(
|
|
|
295
396
|
oldValue: unknown,
|
|
296
397
|
addedValues: unknown[] | undefined,
|
|
297
398
|
removedValues: unknown[] | undefined
|
|
399
|
+
): DeepChange {
|
|
400
|
+
return attachEmission(
|
|
401
|
+
buildDeepChange(type, target, keyOrIndex, newValue, oldValue, addedValues, removedValues)
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function buildDeepChange(
|
|
406
|
+
type: DeepChangeType,
|
|
407
|
+
target: object | unknown[],
|
|
408
|
+
keyOrIndex: string | number,
|
|
409
|
+
newValue: unknown,
|
|
410
|
+
oldValue: unknown,
|
|
411
|
+
addedValues: unknown[] | undefined,
|
|
412
|
+
removedValues: unknown[] | undefined
|
|
298
413
|
): DeepChange {
|
|
299
414
|
const isInit = getIsInInitPhase()
|
|
300
415
|
switch (type) {
|
|
@@ -352,12 +467,28 @@ function createDeepChange(
|
|
|
352
467
|
}
|
|
353
468
|
|
|
354
469
|
function emitGlobalDeepChange(obj: object, change: DeepChange): void {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
470
|
+
const listeners = globalDeepChangeListeners
|
|
471
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
472
|
+
const subscription = listeners[i]
|
|
473
|
+
if (subscription.active) {
|
|
474
|
+
try {
|
|
475
|
+
subscription.listener(obj, change)
|
|
476
|
+
} catch (error) {
|
|
477
|
+
rememberDeepChangeListenerError(error)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
358
480
|
}
|
|
359
481
|
}
|
|
360
482
|
|
|
483
|
+
// The mutation is already applied by the time listeners run, so a failing listener
|
|
484
|
+
// must not keep it from reaching bindings and other observers. Once the whole emission
|
|
485
|
+
// has been delivered a lone failure is rethrown as is, and several failures are grouped
|
|
486
|
+
// in a MobxKeystoneAggregateError.
|
|
487
|
+
function rememberDeepChangeListenerError(error: unknown): void {
|
|
488
|
+
const emission = currentEmission!
|
|
489
|
+
emission.error = addDelayedError(emission.error, error, "multiple deep change listeners failed")
|
|
490
|
+
}
|
|
491
|
+
|
|
361
492
|
function emitDeepChangeToListeners(obj: object, change: DeepChange): void {
|
|
362
493
|
// Segments are pushed in child-to-root order (O(1) each) rather than unshifted
|
|
363
494
|
// at the front (O(depth) each), keeping the whole walk O(depth) instead of
|
|
@@ -391,14 +522,20 @@ function emitDeepChangeForTarget(
|
|
|
391
522
|
return
|
|
392
523
|
}
|
|
393
524
|
|
|
394
|
-
const
|
|
525
|
+
const prefixedChange =
|
|
395
526
|
prefixCount > 0
|
|
396
|
-
?
|
|
527
|
+
? changeWithPath(change, buildPrefixedPath(reversedPrefix, prefixCount, change.path))
|
|
397
528
|
: change
|
|
398
529
|
|
|
399
530
|
for (let i = 0; i < listenersForObject.length; i++) {
|
|
400
|
-
const
|
|
401
|
-
|
|
531
|
+
const subscription = listenersForObject[i]
|
|
532
|
+
if (subscription.active) {
|
|
533
|
+
try {
|
|
534
|
+
subscription.listener(prefixedChange)
|
|
535
|
+
} catch (error) {
|
|
536
|
+
rememberDeepChangeListenerError(error)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
402
539
|
}
|
|
403
540
|
}
|
|
404
541
|
|
|
@@ -420,6 +557,13 @@ function buildPrefixedPath(
|
|
|
420
557
|
/**
|
|
421
558
|
* Adds a listener that will be called every time a deep change is generated for the tree of the given target object.
|
|
422
559
|
* Unlike `onPatches`, this provides raw MobX change information including proper splice detection for arrays.
|
|
560
|
+
* Individual mutations notify listeners after automatic type checking succeeds and snapshots update;
|
|
561
|
+
* rejected mutations and their quiet rollbacks do not notify listeners.
|
|
562
|
+
* Listener errors do not stop delivery to other listeners. After delivery completes,
|
|
563
|
+
* a single error is rethrown unchanged and multiple errors are grouped in a MobxKeystoneAggregateError.
|
|
564
|
+
* Listeners added while callbacks for the same target are running start with the next
|
|
565
|
+
* change delivered to that target. Listeners disposed during delivery do not receive
|
|
566
|
+
* the current change if their callback has not run yet.
|
|
423
567
|
*
|
|
424
568
|
* @param subtreeRoot Subtree root object of the deep change listener.
|
|
425
569
|
* @param listener The listener function that will be called every time a change is generated for the object or its children.
|
|
@@ -436,18 +580,23 @@ export function onDeepChange(
|
|
|
436
580
|
listener = action(listener.name || "onDeepChangeListener", listener)
|
|
437
581
|
}
|
|
438
582
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
listenersForObject.push(listener)
|
|
583
|
+
// Keep the array held by an ongoing delivery unchanged. Subscription changes
|
|
584
|
+
// are infrequent, so copy here rather than on every mutation notification.
|
|
585
|
+
const listeners = deepChangeListeners.get(subtreeRoot) ?? []
|
|
586
|
+
const subscription = { listener, active: true }
|
|
587
|
+
deepChangeListeners.set(subtreeRoot, [...listeners, subscription])
|
|
446
588
|
deepChangeListenerCount++
|
|
589
|
+
|
|
447
590
|
return () => {
|
|
448
|
-
if (
|
|
449
|
-
|
|
591
|
+
if (!subscription.active) {
|
|
592
|
+
return
|
|
450
593
|
}
|
|
594
|
+
subscription.active = false
|
|
595
|
+
deepChangeListenerCount--
|
|
596
|
+
deepChangeListeners.set(
|
|
597
|
+
subtreeRoot,
|
|
598
|
+
deepChangeListeners.get(subtreeRoot)!.filter((s) => s !== subscription)
|
|
599
|
+
)
|
|
451
600
|
}
|
|
452
601
|
}
|
|
453
602
|
|
|
@@ -467,8 +616,14 @@ export function onGlobalDeepChange(
|
|
|
467
616
|
listener = action(listener.name || "onGlobalDeepChangeListener", listener)
|
|
468
617
|
}
|
|
469
618
|
|
|
470
|
-
|
|
619
|
+
const subscription = { listener, active: true }
|
|
620
|
+
globalDeepChangeListeners = [...globalDeepChangeListeners, subscription]
|
|
621
|
+
|
|
471
622
|
return () => {
|
|
472
|
-
|
|
623
|
+
if (!subscription.active) {
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
subscription.active = false
|
|
627
|
+
globalDeepChangeListeners = globalDeepChangeListeners.filter((s) => s !== subscription)
|
|
473
628
|
}
|
|
474
629
|
}
|
package/src/frozen/Frozen.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getGlobalConfig } from "../globalConfig"
|
|
|
3
3
|
import type { FrozenData } from "../snapshot"
|
|
4
4
|
import { tweak } from "../tweaker/tweak"
|
|
5
5
|
import { failure, inDevMode, isPlainObject, isPrimitive } from "../utils"
|
|
6
|
+
import { getSafeErrorValuePreview } from "../utils/errorDiagnostics"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Should freeze and plain json checks be done when creating the frozen object?
|
|
@@ -54,7 +55,7 @@ export class Frozen<T> {
|
|
|
54
55
|
this.data = dataToFreeze
|
|
55
56
|
|
|
56
57
|
if (check) {
|
|
57
|
-
Object.
|
|
58
|
+
Object.defineProperty(this, "data", { writable: false, configurable: false })
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
tweak(this, undefined)
|
|
@@ -95,44 +96,60 @@ export function toFrozenSnapshot<T>(data: T): FrozenData<T> {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
function checkDataIsSerializableAndFreeze(data:
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
checkDataIsSerializableAndFreeze(v)
|
|
99
|
+
function checkDataIsSerializableAndFreeze(data: unknown) {
|
|
100
|
+
// false means the object is still on the traversal stack; true means fully checked.
|
|
101
|
+
const checked = new Map<object, boolean>()
|
|
102
|
+
const stack: {
|
|
103
|
+
data: object
|
|
104
|
+
keys: string[] | undefined
|
|
105
|
+
length: number
|
|
106
|
+
nextIndex: number
|
|
107
|
+
}[] = []
|
|
108
|
+
|
|
109
|
+
const visit = (value: unknown) => {
|
|
110
|
+
if (isPrimitive(value)) return
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(value) && !isPlainObject(value)) {
|
|
113
|
+
throw failure(
|
|
114
|
+
`frozen data must be plainly serializable to JSON, but ${getSafeErrorValuePreview(value)} is not`
|
|
115
|
+
)
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
return
|
|
117
|
+
const state = checked.get(value)
|
|
118
|
+
if (state === true) return
|
|
119
|
+
if (state === false) {
|
|
120
|
+
throw failure("frozen data must not contain cycles")
|
|
121
|
+
}
|
|
122
|
+
checked.set(value, false)
|
|
123
|
+
const keys = Array.isArray(value) ? undefined : Object.keys(value)
|
|
124
|
+
stack.push({
|
|
125
|
+
data: value,
|
|
126
|
+
keys,
|
|
127
|
+
length: keys ? keys.length : (value as unknown[]).length,
|
|
128
|
+
nextIndex: 0,
|
|
129
|
+
})
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
visit(data)
|
|
133
|
+
while (stack.length > 0) {
|
|
134
|
+
const frame = stack[stack.length - 1]
|
|
135
|
+
if (frame.nextIndex === frame.length) {
|
|
136
|
+
Object.freeze(frame.data)
|
|
137
|
+
checked.set(frame.data, true)
|
|
138
|
+
stack.pop()
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
const index = frame.nextIndex++
|
|
143
|
+
const value = frame.keys
|
|
144
|
+
? (frame.data as Record<string, unknown>)[frame.keys[index]]
|
|
145
|
+
: (frame.data as unknown[])[index]
|
|
146
|
+
if (!frame.keys && value === undefined && !getGlobalConfig().allowUndefinedArrayElements) {
|
|
147
|
+
throw failure(
|
|
148
|
+
"undefined is not supported inside arrays since it is not serializable in JSON, consider using null instead"
|
|
149
|
+
)
|
|
130
150
|
}
|
|
131
|
-
|
|
132
|
-
return
|
|
151
|
+
visit(value)
|
|
133
152
|
}
|
|
134
|
-
|
|
135
|
-
throw failure(`frozen data must be plainly serializable to JSON, but ${data} is not`)
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
/**
|
package/src/model/BaseModel.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ModelPropsToUntransformedData,
|
|
13
13
|
} from "../modelShared/prop"
|
|
14
14
|
import { getSnapshot } from "../snapshot/getSnapshot"
|
|
15
|
+
import { setModelInitialDataSnapshot } from "../snapshot/modelInitialData"
|
|
15
16
|
import type { SnapshotInOfModel, SnapshotOutOfModel } from "../snapshot/SnapshotOf"
|
|
16
17
|
import { typesModel } from "../types/objectBased/typesModel"
|
|
17
18
|
import type { TypeCheckError } from "../types/TypeCheckError"
|
|
@@ -69,7 +70,7 @@ export abstract class BaseModel<
|
|
|
69
70
|
*/
|
|
70
71
|
get [modelIdKey](): ModelIdPropertyType<TProps, ModelIdPropertyName> {
|
|
71
72
|
const idProp = getModelIdPropertyName(this.constructor as any)
|
|
72
|
-
return (idProp ? (this.$ as any)[idProp] : undefined) as ModelIdPropertyType<
|
|
73
|
+
return (idProp !== undefined ? (this.$ as any)[idProp] : undefined) as ModelIdPropertyType<
|
|
73
74
|
TProps,
|
|
74
75
|
ModelIdPropertyName
|
|
75
76
|
>
|
|
@@ -77,7 +78,7 @@ export abstract class BaseModel<
|
|
|
77
78
|
|
|
78
79
|
set [modelIdKey](newId: ModelIdPropertyType<TProps, ModelIdPropertyName>) {
|
|
79
80
|
const idProp = getModelIdPropertyName(this.constructor as any)
|
|
80
|
-
if (
|
|
81
|
+
if (idProp === undefined) {
|
|
81
82
|
throw failure("$modelId cannot be set when there is no idProp set in the model")
|
|
82
83
|
}
|
|
83
84
|
;(this.$ as any)[idProp] = newId
|
|
@@ -146,25 +147,25 @@ export abstract class BaseModel<
|
|
|
146
147
|
// plain new
|
|
147
148
|
assertIsObject(initialData, "initialData")
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
// Empty input is filled by the constructor below. Track its primitive
|
|
151
|
+
// defaults directly instead of rereading observable fields for the snapshot.
|
|
152
|
+
const useInitialDataSnapshot = Reflect.ownKeys(initialData).length === 0
|
|
153
|
+
const observableInitialData = observable.object(initialData as any, undefined, {
|
|
154
|
+
deep: false,
|
|
155
|
+
})
|
|
156
|
+
if (useInitialDataSnapshot) {
|
|
157
|
+
setModelInitialDataSnapshot(observableInitialData, {})
|
|
158
|
+
}
|
|
159
|
+
internalNewModel(this, observableInitialData, modelClass!, useInitialDataSnapshot)
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
toString(options?: { withData?: boolean }) {
|
|
158
|
-
const
|
|
159
|
-
withData: true,
|
|
160
|
-
...options,
|
|
161
|
-
}
|
|
164
|
+
const { withData = true } = options ?? {}
|
|
162
165
|
|
|
163
166
|
const firstPart = `${this.constructor.name}#${this[modelTypeKey]}`
|
|
164
167
|
|
|
165
|
-
return
|
|
166
|
-
? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]`
|
|
167
|
-
: `[${firstPart}]`
|
|
168
|
+
return withData ? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]` : `[${firstPart}]`
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
package/src/model/newModel.ts
CHANGED
|
@@ -19,10 +19,11 @@ import {
|
|
|
19
19
|
hasGlobalPatchListeners,
|
|
20
20
|
} from "../patch/emitPatch"
|
|
21
21
|
import type { Patch } from "../patch/Patch"
|
|
22
|
-
import {
|
|
22
|
+
import { updateModelInitialDataSnapshot } from "../snapshot/modelInitialData"
|
|
23
23
|
import { tweakModel } from "../tweaker/tweakModel"
|
|
24
24
|
import { tweakPlainObject } from "../tweaker/tweakPlainObject"
|
|
25
|
-
import {
|
|
25
|
+
import { typeCheckAfterCreation } from "../tweaker/typeChecking"
|
|
26
|
+
import { failure, hasOwnProp, inDevMode, makePropReadonly } from "../utils"
|
|
26
27
|
import { setIfDifferent, setIfDifferentWithReturn } from "../utils/setIfDifferent"
|
|
27
28
|
import type { AnyModel } from "./BaseModel"
|
|
28
29
|
import { getModelIdPropertyName } from "./getModelMetadata"
|
|
@@ -38,7 +39,8 @@ export const internalNewModel = action(
|
|
|
38
39
|
<M extends AnyModel>(
|
|
39
40
|
origModelObj: M,
|
|
40
41
|
initialData: ModelCreationData<M>,
|
|
41
|
-
modelClass: ModelClass<AnyModel
|
|
42
|
+
modelClass: ModelClass<AnyModel>,
|
|
43
|
+
useInitialDataSnapshot: boolean
|
|
42
44
|
): void => {
|
|
43
45
|
if (inDevMode) {
|
|
44
46
|
assertIsModelClass(modelClass, "modelClass")
|
|
@@ -48,14 +50,16 @@ export const internalNewModel = action(
|
|
|
48
50
|
getModelDetails(modelClass)
|
|
49
51
|
|
|
50
52
|
// use symbol if provided
|
|
51
|
-
if (modelIdPropertyName && modelIdPropData) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
53
|
+
if (modelIdPropertyName !== undefined && modelIdPropData) {
|
|
54
|
+
const providedId = hasOwnProp(initialData, modelIdPropertyName)
|
|
55
|
+
? initialData[modelIdPropertyName]
|
|
56
|
+
: undefined
|
|
57
|
+
const id: string =
|
|
58
|
+
providedId != null ? providedId : (modelIdPropData._defaultFn as () => string)()
|
|
58
59
|
setIfDifferent(initialData, modelIdPropertyName, id)
|
|
60
|
+
if (useInitialDataSnapshot) {
|
|
61
|
+
updateModelInitialDataSnapshot(initialData, modelIdPropertyName, id)
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
const modelObj = origModelObj as O.Writable<M>
|
|
@@ -73,7 +77,8 @@ export const internalNewModel = action(
|
|
|
73
77
|
|
|
74
78
|
const propData = modelProps[k]
|
|
75
79
|
|
|
76
|
-
const
|
|
80
|
+
const hasInitialValue = hasOwnProp(initialData, k)
|
|
81
|
+
const initialValue = hasInitialValue ? initialData[k] : undefined
|
|
77
82
|
let newValue = initialValue
|
|
78
83
|
let changed = false
|
|
79
84
|
|
|
@@ -90,7 +95,7 @@ export const internalNewModel = action(
|
|
|
90
95
|
if (defaultValue !== noDefaultValue) {
|
|
91
96
|
changed = true
|
|
92
97
|
newValue = defaultValue
|
|
93
|
-
} else if (!
|
|
98
|
+
} else if (!hasInitialValue) {
|
|
94
99
|
// for mobx4, we need to set up properties even if they are undefined
|
|
95
100
|
changed = true
|
|
96
101
|
}
|
|
@@ -99,6 +104,9 @@ export const internalNewModel = action(
|
|
|
99
104
|
if (changed) {
|
|
100
105
|
// setIfDifferent not required
|
|
101
106
|
set(initialData, k, newValue)
|
|
107
|
+
if (useInitialDataSnapshot) {
|
|
108
|
+
updateModelInitialDataSnapshot(initialData, k, newValue)
|
|
109
|
+
}
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -106,10 +114,7 @@ export const internalNewModel = action(
|
|
|
106
114
|
|
|
107
115
|
// type check it if needed
|
|
108
116
|
if (isModelAutoTypeCheckingEnabled()) {
|
|
109
|
-
|
|
110
|
-
if (err) {
|
|
111
|
-
err.throw()
|
|
112
|
-
}
|
|
117
|
+
typeCheckAfterCreation(modelObj)
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
)
|
|
@@ -135,10 +140,10 @@ export const internalFromSnapshotModel = action(
|
|
|
135
140
|
let id: string | undefined
|
|
136
141
|
let sn = snapshotInitialData.unprocessedSnapshot
|
|
137
142
|
|
|
138
|
-
if (modelIdPropData && modelIdPropertyName) {
|
|
143
|
+
if (modelIdPropData && modelIdPropertyName !== undefined) {
|
|
139
144
|
if (generateNewIds) {
|
|
140
145
|
id = (modelIdPropData._defaultFn as () => string)()
|
|
141
|
-
} else {
|
|
146
|
+
} else if (hasOwnProp(sn, modelIdPropertyName)) {
|
|
142
147
|
id = sn[modelIdPropertyName]
|
|
143
148
|
}
|
|
144
149
|
}
|
|
@@ -161,12 +166,12 @@ export const internalFromSnapshotModel = action(
|
|
|
161
166
|
let inversePatches: Patch[] | undefined
|
|
162
167
|
let defaultsApplied: { key: string; oldValue: unknown; newValue: unknown }[] | undefined
|
|
163
168
|
|
|
164
|
-
if (modelIdPropertyName) {
|
|
169
|
+
if (modelIdPropertyName !== undefined) {
|
|
165
170
|
const initialValue = initialData[modelIdPropertyName]
|
|
166
171
|
const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id)
|
|
167
172
|
|
|
168
173
|
if (valueChanged) {
|
|
169
|
-
|
|
174
|
+
updateModelInitialDataSnapshot(initialData, modelIdPropertyName, id)
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
if (valueChanged && shouldEmitPatches) {
|
|
@@ -191,7 +196,8 @@ export const internalFromSnapshotModel = action(
|
|
|
191
196
|
|
|
192
197
|
const propData = modelProps[k]
|
|
193
198
|
|
|
194
|
-
const
|
|
199
|
+
const hasInitialValue = hasOwnProp(initialData, k)
|
|
200
|
+
const initialValue = hasInitialValue ? initialData[k] : undefined
|
|
195
201
|
let newValue = initialValue
|
|
196
202
|
let changed = false
|
|
197
203
|
|
|
@@ -201,7 +207,7 @@ export const internalFromSnapshotModel = action(
|
|
|
201
207
|
if (defaultValue !== noDefaultValue) {
|
|
202
208
|
changed = true
|
|
203
209
|
newValue = defaultValue
|
|
204
|
-
} else if (!
|
|
210
|
+
} else if (!hasInitialValue) {
|
|
205
211
|
// for mobx4, we need to set up properties even if they are undefined
|
|
206
212
|
changed = true
|
|
207
213
|
}
|
|
@@ -210,7 +216,7 @@ export const internalFromSnapshotModel = action(
|
|
|
210
216
|
if (changed) {
|
|
211
217
|
// setIfDifferent not required
|
|
212
218
|
set(initialData, k, newValue)
|
|
213
|
-
|
|
219
|
+
updateModelInitialDataSnapshot(initialData, k, newValue)
|
|
214
220
|
|
|
215
221
|
if (newValue !== initialValue) {
|
|
216
222
|
if (shouldEmitPatches) {
|
|
@@ -285,10 +291,7 @@ export const internalFromSnapshotModel = action(
|
|
|
285
291
|
|
|
286
292
|
// type check it if needed
|
|
287
293
|
if (isModelAutoTypeCheckingEnabled()) {
|
|
288
|
-
|
|
289
|
-
if (err) {
|
|
290
|
-
err.throw()
|
|
291
|
-
}
|
|
294
|
+
typeCheckAfterCreation(modelObj)
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
)
|
|
@@ -303,7 +306,8 @@ function getModelDetails(modelClass: ModelClass<AnyModel>) {
|
|
|
303
306
|
|
|
304
307
|
const modelIdPropertyName = getModelIdPropertyName(modelClass)
|
|
305
308
|
const modelProps = getInternalModelClassPropsInfo(modelClass)
|
|
306
|
-
const modelIdPropData =
|
|
309
|
+
const modelIdPropData =
|
|
310
|
+
modelIdPropertyName !== undefined ? modelProps[modelIdPropertyName] : undefined
|
|
307
311
|
|
|
308
312
|
return { modelInfo, modelIdPropertyName, modelProps, modelIdPropData }
|
|
309
313
|
}
|
package/src/model/utils.ts
CHANGED
|
@@ -175,7 +175,7 @@ export function getSnapshotModelTypeAndId(snapshot: unknown): ModelTypeAndId | u
|
|
|
175
175
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
176
176
|
|
|
177
177
|
let modelId: string | undefined
|
|
178
|
-
if (modelIdPropertyName) {
|
|
178
|
+
if (modelIdPropertyName !== undefined) {
|
|
179
179
|
const id = (snapshot as Record<string, unknown>)[modelIdPropertyName]
|
|
180
180
|
modelId = typeof id === "string" ? id : undefined
|
|
181
181
|
}
|
|
@@ -214,7 +214,7 @@ export function getModelTypeAndId(model: AnyModel | AnyDataModel): ModelTypeAndI
|
|
|
214
214
|
const modelIdPropertyName = getModelIdPropertyName(modelInfo.class as ModelClass<AnyModel>)
|
|
215
215
|
|
|
216
216
|
let modelId: string | undefined
|
|
217
|
-
if (modelIdPropertyName) {
|
|
217
|
+
if (modelIdPropertyName !== undefined) {
|
|
218
218
|
const id = (model as any)[modelIdKey] as string | undefined
|
|
219
219
|
modelId = typeof id === "string" ? id : undefined
|
|
220
220
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnyDataModel } from "../dataModel/BaseDataModel"
|
|
2
2
|
import type { AnyModel } from "../model/BaseModel"
|
|
3
|
+
import { hasOwnProp } from "../utils"
|
|
3
4
|
import type { ModelClass } from "./BaseModelShared"
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -20,8 +21,8 @@ export function addModelClassInitializer(
|
|
|
20
21
|
init: ModelClassInitializer
|
|
21
22
|
) {
|
|
22
23
|
let initializers: ModelClassInitializer[] = (modelClass as any)[modelInitializersSymbol]
|
|
23
|
-
if (!
|
|
24
|
-
initializers = []
|
|
24
|
+
if (!hasOwnProp(modelClass, modelInitializersSymbol)) {
|
|
25
|
+
initializers = initializers ? initializers.slice() : []
|
|
25
26
|
;(modelClass as any)[modelInitializersSymbol] = initializers
|
|
26
27
|
}
|
|
27
28
|
initializers.push(init)
|