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.
- package/CHANGELOG.md +145 -0
- package/dist/mobx-keystone.esm.js +3627 -1540
- package/dist/mobx-keystone.esm.mjs +3627 -1540
- package/dist/mobx-keystone.umd.js +3638 -1543
- package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
- package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
- package/dist/types/deepChange/onDeepChange.d.ts +9 -0
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/detach.d.ts +1 -1
- package/dist/types/parent/findChildren.d.ts +1 -1
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +3 -0
- package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
- package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
- package/dist/types/ref/nodesById.d.ts +1 -0
- package/dist/types/ref/refIdIndex.d.ts +1 -0
- package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
- package/dist/types/snapshot/applySnapshot.d.ts +1 -0
- package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
- package/dist/types/standardActions/arrayActions.d.ts +1 -1
- package/dist/types/treeUtils/sandbox.d.ts +0 -4
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/TypeCheckError.d.ts +1 -0
- package/dist/types/types/utility/typesRefinement.d.ts +2 -2
- package/dist/types/utils/ModelPool.d.ts +7 -1
- package/dist/types/utils/chunks.d.ts +1 -0
- 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 +2 -2
- 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 +55 -25
- package/src/actionMiddlewares/undoMiddleware.ts +112 -59
- 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 +10 -13
- 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 +39 -39
- package/src/modelShared/modelInfo.ts +1 -1
- package/src/modelShared/prop.ts +6 -9
- package/src/modelShared/sharedInternalModel.ts +16 -3
- package/src/parent/arrayPaths.ts +242 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/detach.ts +1 -1
- package/src/parent/findChildren.ts +4 -7
- package/src/parent/onChildAttachedTo.ts +157 -55
- package/src/parent/path.ts +15 -5
- package/src/parent/setParent.ts +34 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +12 -101
- package/src/patch/applyPatches.ts +74 -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 +81 -29
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- 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 +27 -19
- package/src/snapshot/clone.ts +1 -2
- package/src/snapshot/cloneTreeValue.ts +1 -2
- package/src/snapshot/fromModelSnapshot.ts +6 -2
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +2 -6
- package/src/snapshot/internal.ts +89 -19
- 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/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -22
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +183 -19
- package/src/tweaker/tweakArray.ts +176 -33
- package/src/tweaker/tweakPlainObject.ts +117 -39
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +25 -15
- package/src/types/TypeCheckError.ts +17 -19
- package/src/types/arrayBased/typesArray.ts +10 -11
- package/src/types/createCachedTypeCheck.ts +541 -88
- 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 +8 -18
- 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 +78 -22
- package/src/utils/chainFns.ts +4 -0
- package/src/utils/chunks.ts +271 -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 +71 -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
|
@@ -17,7 +17,7 @@ import type { SnapshotInOfModel, SnapshotOutOfModel } from "../snapshot/Snapshot
|
|
|
17
17
|
import { typesModel } from "../types/objectBased/typesModel"
|
|
18
18
|
import type { TypeCheckError } from "../types/TypeCheckError"
|
|
19
19
|
import { typeCheck } from "../types/typeCheck"
|
|
20
|
-
import { assertIsObject, failure } from "../utils"
|
|
20
|
+
import { assertIsObject, failure, nodeObservableOptions } from "../utils"
|
|
21
21
|
import { getModelIdPropertyName } from "./getModelMetadata"
|
|
22
22
|
import type { ModelConstructorOptions } from "./ModelConstructorOptions"
|
|
23
23
|
import { modelIdKey, modelTypeKey } from "./metadata"
|
|
@@ -70,7 +70,7 @@ export abstract class BaseModel<
|
|
|
70
70
|
*/
|
|
71
71
|
get [modelIdKey](): ModelIdPropertyType<TProps, ModelIdPropertyName> {
|
|
72
72
|
const idProp = getModelIdPropertyName(this.constructor as any)
|
|
73
|
-
return (idProp ? (this.$ as any)[idProp] : undefined) as ModelIdPropertyType<
|
|
73
|
+
return (idProp !== undefined ? (this.$ as any)[idProp] : undefined) as ModelIdPropertyType<
|
|
74
74
|
TProps,
|
|
75
75
|
ModelIdPropertyName
|
|
76
76
|
>
|
|
@@ -78,7 +78,7 @@ export abstract class BaseModel<
|
|
|
78
78
|
|
|
79
79
|
set [modelIdKey](newId: ModelIdPropertyType<TProps, ModelIdPropertyName>) {
|
|
80
80
|
const idProp = getModelIdPropertyName(this.constructor as any)
|
|
81
|
-
if (
|
|
81
|
+
if (idProp === undefined) {
|
|
82
82
|
throw failure("$modelId cannot be set when there is no idProp set in the model")
|
|
83
83
|
}
|
|
84
84
|
;(this.$ as any)[idProp] = newId
|
|
@@ -150,9 +150,11 @@ export abstract class BaseModel<
|
|
|
150
150
|
// Empty input is filled by the constructor below. Track its primitive
|
|
151
151
|
// defaults directly instead of rereading observable fields for the snapshot.
|
|
152
152
|
const useInitialDataSnapshot = Reflect.ownKeys(initialData).length === 0
|
|
153
|
-
const observableInitialData = observable.object(
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
const observableInitialData = observable.object(
|
|
154
|
+
initialData as any,
|
|
155
|
+
undefined,
|
|
156
|
+
nodeObservableOptions
|
|
157
|
+
)
|
|
156
158
|
if (useInitialDataSnapshot) {
|
|
157
159
|
setModelInitialDataSnapshot(observableInitialData, {})
|
|
158
160
|
}
|
|
@@ -161,16 +163,11 @@ export abstract class BaseModel<
|
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
toString(options?: { withData?: boolean }) {
|
|
164
|
-
const
|
|
165
|
-
withData: true,
|
|
166
|
-
...options,
|
|
167
|
-
}
|
|
166
|
+
const { withData = true } = options ?? {}
|
|
168
167
|
|
|
169
168
|
const firstPart = `${this.constructor.name}#${this[modelTypeKey]}`
|
|
170
169
|
|
|
171
|
-
return
|
|
172
|
-
? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]`
|
|
173
|
-
: `[${firstPart}]`
|
|
170
|
+
return withData ? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]` : `[${firstPart}]`
|
|
174
171
|
}
|
|
175
172
|
}
|
|
176
173
|
|
package/src/model/newModel.ts
CHANGED
|
@@ -22,7 +22,8 @@ import type { Patch } from "../patch/Patch"
|
|
|
22
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"
|
|
@@ -49,13 +50,12 @@ export const internalNewModel = action(
|
|
|
49
50
|
getModelDetails(modelClass)
|
|
50
51
|
|
|
51
52
|
// use symbol if provided
|
|
52
|
-
if (modelIdPropertyName && modelIdPropData) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
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)()
|
|
59
59
|
setIfDifferent(initialData, modelIdPropertyName, id)
|
|
60
60
|
if (useInitialDataSnapshot) {
|
|
61
61
|
updateModelInitialDataSnapshot(initialData, modelIdPropertyName, id)
|
|
@@ -77,7 +77,8 @@ export const internalNewModel = action(
|
|
|
77
77
|
|
|
78
78
|
const propData = modelProps[k]
|
|
79
79
|
|
|
80
|
-
const
|
|
80
|
+
const hasInitialValue = hasOwnProp(initialData, k)
|
|
81
|
+
const initialValue = hasInitialValue ? initialData[k] : undefined
|
|
81
82
|
let newValue = initialValue
|
|
82
83
|
let changed = false
|
|
83
84
|
|
|
@@ -94,7 +95,7 @@ export const internalNewModel = action(
|
|
|
94
95
|
if (defaultValue !== noDefaultValue) {
|
|
95
96
|
changed = true
|
|
96
97
|
newValue = defaultValue
|
|
97
|
-
} else if (!
|
|
98
|
+
} else if (!hasInitialValue) {
|
|
98
99
|
// for mobx4, we need to set up properties even if they are undefined
|
|
99
100
|
changed = true
|
|
100
101
|
}
|
|
@@ -113,10 +114,7 @@ export const internalNewModel = action(
|
|
|
113
114
|
|
|
114
115
|
// type check it if needed
|
|
115
116
|
if (isModelAutoTypeCheckingEnabled()) {
|
|
116
|
-
|
|
117
|
-
if (err) {
|
|
118
|
-
err.throw()
|
|
119
|
-
}
|
|
117
|
+
typeCheckAfterCreation(modelObj)
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
)
|
|
@@ -142,10 +140,10 @@ export const internalFromSnapshotModel = action(
|
|
|
142
140
|
let id: string | undefined
|
|
143
141
|
let sn = snapshotInitialData.unprocessedSnapshot
|
|
144
142
|
|
|
145
|
-
if (modelIdPropData && modelIdPropertyName) {
|
|
143
|
+
if (modelIdPropData && modelIdPropertyName !== undefined) {
|
|
146
144
|
if (generateNewIds) {
|
|
147
145
|
id = (modelIdPropData._defaultFn as () => string)()
|
|
148
|
-
} else {
|
|
146
|
+
} else if (hasOwnProp(sn, modelIdPropertyName)) {
|
|
149
147
|
id = sn[modelIdPropertyName]
|
|
150
148
|
}
|
|
151
149
|
}
|
|
@@ -168,7 +166,7 @@ export const internalFromSnapshotModel = action(
|
|
|
168
166
|
let inversePatches: Patch[] | undefined
|
|
169
167
|
let defaultsApplied: { key: string; oldValue: unknown; newValue: unknown }[] | undefined
|
|
170
168
|
|
|
171
|
-
if (modelIdPropertyName) {
|
|
169
|
+
if (modelIdPropertyName !== undefined) {
|
|
172
170
|
const initialValue = initialData[modelIdPropertyName]
|
|
173
171
|
const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id)
|
|
174
172
|
|
|
@@ -198,7 +196,8 @@ export const internalFromSnapshotModel = action(
|
|
|
198
196
|
|
|
199
197
|
const propData = modelProps[k]
|
|
200
198
|
|
|
201
|
-
const
|
|
199
|
+
const hasInitialValue = hasOwnProp(initialData, k)
|
|
200
|
+
const initialValue = hasInitialValue ? initialData[k] : undefined
|
|
202
201
|
let newValue = initialValue
|
|
203
202
|
let changed = false
|
|
204
203
|
|
|
@@ -208,7 +207,7 @@ export const internalFromSnapshotModel = action(
|
|
|
208
207
|
if (defaultValue !== noDefaultValue) {
|
|
209
208
|
changed = true
|
|
210
209
|
newValue = defaultValue
|
|
211
|
-
} else if (!
|
|
210
|
+
} else if (!hasInitialValue) {
|
|
212
211
|
// for mobx4, we need to set up properties even if they are undefined
|
|
213
212
|
changed = true
|
|
214
213
|
}
|
|
@@ -292,10 +291,7 @@ export const internalFromSnapshotModel = action(
|
|
|
292
291
|
|
|
293
292
|
// type check it if needed
|
|
294
293
|
if (isModelAutoTypeCheckingEnabled()) {
|
|
295
|
-
|
|
296
|
-
if (err) {
|
|
297
|
-
err.throw()
|
|
298
|
-
}
|
|
294
|
+
typeCheckAfterCreation(modelObj)
|
|
299
295
|
}
|
|
300
296
|
}
|
|
301
297
|
)
|
|
@@ -310,7 +306,8 @@ function getModelDetails(modelClass: ModelClass<AnyModel>) {
|
|
|
310
306
|
|
|
311
307
|
const modelIdPropertyName = getModelIdPropertyName(modelClass)
|
|
312
308
|
const modelProps = getInternalModelClassPropsInfo(modelClass)
|
|
313
|
-
const modelIdPropData =
|
|
309
|
+
const modelIdPropData =
|
|
310
|
+
modelIdPropertyName !== undefined ? modelProps[modelIdPropertyName] : undefined
|
|
314
311
|
|
|
315
312
|
return { modelInfo, modelIdPropertyName, modelProps, modelIdPropData }
|
|
316
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)
|