mobx-keystone 1.22.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +1 -1
  4. package/dist/mobx-keystone.esm.js +7198 -6242
  5. package/dist/mobx-keystone.esm.mjs +7198 -6242
  6. package/dist/mobx-keystone.umd.js +7217 -6243
  7. package/dist/types/action/runUnprotected.d.ts +8 -4
  8. package/dist/types/model/Model.d.ts +5 -0
  9. package/dist/types/modelShared/modelDecorator.d.ts +1 -1
  10. package/dist/types/modelShared/prop.d.ts +8 -4
  11. package/dist/types/snapshot/modelInitialData.d.ts +1 -0
  12. package/dist/types/tweaker/treeNodeMetadata.d.ts +1 -0
  13. package/dist/types/types/createCachedTypeCheck.d.ts +1 -0
  14. package/dist/types/utils/index.d.ts +24 -0
  15. package/package.json +15 -11
  16. package/src/action/middleware.ts +5 -18
  17. package/src/action/mutationBatch.ts +72 -0
  18. package/src/action/runUnprotected.ts +21 -8
  19. package/src/action/wrapInAction.ts +45 -15
  20. package/src/actionMiddlewares/undoMiddleware.ts +19 -13
  21. package/src/dataModel/BaseDataModel.ts +4 -8
  22. package/src/deepChange/onDeepChange.ts +246 -16
  23. package/src/model/BaseModel.ts +4 -12
  24. package/src/model/Model.ts +5 -0
  25. package/src/model/newModel.ts +68 -36
  26. package/src/modelShared/modelDecorator.ts +64 -53
  27. package/src/modelShared/prop.ts +105 -63
  28. package/src/modelShared/sharedInternalModel.ts +91 -60
  29. package/src/parent/core.ts +16 -10
  30. package/src/parent/coreObjectChildren.ts +219 -21
  31. package/src/parent/path.ts +6 -5
  32. package/src/parent/setParent.ts +5 -5
  33. package/src/patch/emitPatch.ts +102 -20
  34. package/src/ref/Ref.ts +4 -5
  35. package/src/ref/core.ts +2 -2
  36. package/src/snapshot/fromArraySnapshot.ts +11 -2
  37. package/src/snapshot/fromModelSnapshot.ts +10 -3
  38. package/src/snapshot/fromPlainObjectSnapshot.ts +18 -7
  39. package/src/snapshot/fromSnapshot.ts +31 -9
  40. package/src/snapshot/getSnapshot.ts +8 -3
  41. package/src/snapshot/internal.ts +243 -82
  42. package/src/snapshot/modelInitialData.ts +41 -0
  43. package/src/snapshot/reconcileArraySnapshot.ts +9 -3
  44. package/src/snapshot/reconcileModelSnapshot.ts +97 -17
  45. package/src/snapshot/reconcilePlainObjectSnapshot.ts +9 -3
  46. package/src/treeUtils/draft.ts +7 -8
  47. package/src/tweaker/core.ts +30 -5
  48. package/src/tweaker/treeNodeMetadata.ts +38 -0
  49. package/src/tweaker/tweak.ts +15 -4
  50. package/src/tweaker/tweakArray.ts +159 -121
  51. package/src/tweaker/tweakFrozen.ts +2 -2
  52. package/src/tweaker/tweakModel.ts +2 -2
  53. package/src/tweaker/tweakPlainObject.ts +117 -124
  54. package/src/tweaker/typeChecking.ts +10 -1
  55. package/src/types/TypeChecker.ts +261 -15
  56. package/src/types/arrayBased/typesArray.ts +38 -40
  57. package/src/types/arrayBased/typesTuple.ts +51 -34
  58. package/src/types/createCachedTypeCheck.ts +181 -0
  59. package/src/types/objectBased/typesArraySet.ts +42 -32
  60. package/src/types/objectBased/typesDataModelData.ts +18 -11
  61. package/src/types/objectBased/typesModel.ts +1 -1
  62. package/src/types/objectBased/typesObject.ts +57 -28
  63. package/src/types/objectBased/typesObjectMap.ts +51 -40
  64. package/src/types/objectBased/typesRecord.ts +59 -39
  65. package/src/types/objectBased/typesRef.ts +1 -1
  66. package/src/types/primitiveBased/typesPrimitive.ts +9 -9
  67. package/src/types/tProp.ts +80 -44
  68. package/src/types/utility/typesOr.ts +35 -24
  69. package/src/types/utility/typesRefinement.ts +21 -8
  70. package/src/types/utility/typesSkipCheck.ts +21 -8
  71. package/src/types/utility/typesTag.ts +21 -9
  72. package/src/types/utility/typesUnchecked.ts +2 -3
  73. package/src/utils/ModelPool.ts +34 -19
  74. package/src/utils/errorDiagnostics.ts +60 -18
  75. package/src/utils/index.ts +142 -9
  76. package/src/types/createPerEntryCachedCheck.ts +0 -166
  77. /package/dist/types/{types/createPerEntryCachedCheck.d.ts → action/mutationBatch.d.ts} +0 -0
@@ -1,8 +1,8 @@
1
1
  import { action, isAction } from "mobx"
2
- import { fastGetParentPath } from "../parent/path"
3
- import type { Path, WritablePath } from "../parent/pathTypes"
2
+ import { fastGetParentPath, type ParentPath } from "../parent/path"
3
+ import type { Path, PathElement, WritablePath } from "../parent/pathTypes"
4
4
  import { assertTweakedObject } from "../tweaker/core"
5
- import { assertIsFunction, deleteFromArray } from "../utils"
5
+ import { assertIsFunction, deleteFromArray, failure } from "../utils"
6
6
 
7
7
  /**
8
8
  * Disposer function to stop listening to deep changes.
@@ -99,11 +99,26 @@ export type OnDeepChangeListener = (change: DeepChange) => void
99
99
 
100
100
  const deepChangeListeners = new WeakMap<object, OnDeepChangeListener[]>()
101
101
  const globalDeepChangeListeners: ((target: object, change: DeepChange) => void)[] = []
102
+ const emptyPath: Path = Object.freeze([])
102
103
 
103
104
  let deepChangeListenerCount = 0
104
105
 
105
106
  let initPhaseDepth = 0
106
107
 
108
+ /**
109
+ * @internal
110
+ */
111
+ export function hasAnyDeepChangeListeners(): boolean {
112
+ return deepChangeListenerCount > 0 || hasGlobalDeepChangeListeners()
113
+ }
114
+
115
+ /**
116
+ * @internal
117
+ */
118
+ export function hasGlobalDeepChangeListeners(): boolean {
119
+ return globalDeepChangeListeners.length > 0
120
+ }
121
+
107
122
  /**
108
123
  * @internal
109
124
  * Returns whether we're currently in an initialization phase.
@@ -132,9 +147,7 @@ export function exitInitPhase(): void {
132
147
  * @internal
133
148
  */
134
149
  export function emitDeepChange(obj: object, change: DeepChange): void {
135
- const hasAnyListeners = deepChangeListenerCount > 0 || globalDeepChangeListeners.length > 0
136
-
137
- if (!hasAnyListeners) {
150
+ if (!hasAnyDeepChangeListeners()) {
138
151
  return
139
152
  }
140
153
 
@@ -144,6 +157,200 @@ export function emitDeepChange(obj: object, change: DeepChange): void {
144
157
  }
145
158
  }
146
159
 
160
+ /**
161
+ * @internal
162
+ */
163
+ export function emitObjectAddDeepChange(
164
+ node: object,
165
+ target: object,
166
+ key: string,
167
+ newValue: unknown
168
+ ): void {
169
+ emitDeepChangeFromValues(node, DeepChangeType.ObjectAdd, target, key, newValue, undefined)
170
+ }
171
+
172
+ /**
173
+ * @internal
174
+ */
175
+ export function emitObjectUpdateDeepChange(
176
+ node: object,
177
+ target: object,
178
+ key: string,
179
+ newValue: unknown,
180
+ oldValue: unknown
181
+ ): void {
182
+ emitDeepChangeFromValues(node, DeepChangeType.ObjectUpdate, target, key, newValue, oldValue)
183
+ }
184
+
185
+ /**
186
+ * @internal
187
+ */
188
+ export function emitObjectRemoveDeepChange(
189
+ node: object,
190
+ target: object,
191
+ key: string,
192
+ oldValue: unknown
193
+ ): void {
194
+ emitDeepChangeFromValues(node, DeepChangeType.ObjectRemove, target, key, undefined, oldValue)
195
+ }
196
+
197
+ /**
198
+ * @internal
199
+ */
200
+ export function emitArraySpliceDeepChange(
201
+ node: object,
202
+ target: unknown[],
203
+ index: number,
204
+ addedValues: unknown[],
205
+ removedValues: unknown[]
206
+ ): void {
207
+ emitDeepChangeFromValues(
208
+ node,
209
+ DeepChangeType.ArraySplice,
210
+ target,
211
+ index,
212
+ undefined,
213
+ undefined,
214
+ addedValues,
215
+ removedValues
216
+ )
217
+ }
218
+
219
+ /**
220
+ * @internal
221
+ */
222
+ export function emitArrayUpdateDeepChange(
223
+ node: object,
224
+ target: unknown[],
225
+ index: number,
226
+ newValue: unknown,
227
+ oldValue: unknown
228
+ ): void {
229
+ emitDeepChangeFromValues(node, DeepChangeType.ArrayUpdate, target, index, newValue, oldValue)
230
+ }
231
+
232
+ function emitDeepChangeFromValues(
233
+ node: object,
234
+ type: DeepChangeType,
235
+ target: object | unknown[],
236
+ keyOrIndex: string | number,
237
+ newValue: unknown,
238
+ oldValue: unknown,
239
+ addedValues?: unknown[],
240
+ removedValues?: unknown[]
241
+ ): void {
242
+ let change: DeepChange | undefined
243
+
244
+ if (globalDeepChangeListeners.length > 0) {
245
+ change = createDeepChange(
246
+ type,
247
+ target,
248
+ keyOrIndex,
249
+ newValue,
250
+ oldValue,
251
+ addedValues,
252
+ removedValues
253
+ )
254
+ emitGlobalDeepChange(node, change)
255
+ }
256
+
257
+ if (deepChangeListenerCount === 0) {
258
+ return
259
+ }
260
+
261
+ // Segments are pushed in child-to-root order (O(1) each) rather than unshifted
262
+ // at the front (O(depth) each), keeping the whole walk O(depth) instead of
263
+ // O(depth^2) on deep trees.
264
+ const reversedPrefix: WritablePath = []
265
+ let current: object | undefined = node
266
+ while (current) {
267
+ const listenersForObject = deepChangeListeners.get(current)
268
+ if (listenersForObject && listenersForObject.length > 0) {
269
+ change ??= createDeepChange(
270
+ type,
271
+ target,
272
+ keyOrIndex,
273
+ newValue,
274
+ oldValue,
275
+ addedValues,
276
+ removedValues
277
+ )
278
+ emitDeepChangeForTarget(current, change, reversedPrefix, reversedPrefix.length)
279
+ }
280
+
281
+ const parentPath: ParentPath<object> | undefined = fastGetParentPath(current, false)
282
+ if (!parentPath) {
283
+ break
284
+ }
285
+ reversedPrefix.push(parentPath.path)
286
+ current = parentPath.parent
287
+ }
288
+ }
289
+
290
+ function createDeepChange(
291
+ type: DeepChangeType,
292
+ target: object | unknown[],
293
+ keyOrIndex: string | number,
294
+ newValue: unknown,
295
+ oldValue: unknown,
296
+ addedValues: unknown[] | undefined,
297
+ removedValues: unknown[] | undefined
298
+ ): DeepChange {
299
+ const isInit = getIsInInitPhase()
300
+ switch (type) {
301
+ case DeepChangeType.ObjectAdd:
302
+ return {
303
+ type,
304
+ target: target as object,
305
+ path: emptyPath,
306
+ key: keyOrIndex as string,
307
+ newValue,
308
+ isInit,
309
+ }
310
+ case DeepChangeType.ObjectUpdate:
311
+ return {
312
+ type,
313
+ target: target as object,
314
+ path: emptyPath,
315
+ key: keyOrIndex as string,
316
+ newValue,
317
+ oldValue,
318
+ isInit,
319
+ }
320
+ case DeepChangeType.ObjectRemove:
321
+ return {
322
+ type,
323
+ target: target as object,
324
+ path: emptyPath,
325
+ key: keyOrIndex as string,
326
+ oldValue,
327
+ isInit,
328
+ }
329
+ case DeepChangeType.ArraySplice:
330
+ return {
331
+ type,
332
+ target: target as unknown[],
333
+ path: emptyPath,
334
+ index: keyOrIndex as number,
335
+ addedValues: addedValues!,
336
+ removedValues: removedValues!,
337
+ isInit,
338
+ }
339
+ case DeepChangeType.ArrayUpdate:
340
+ return {
341
+ type,
342
+ target: target as unknown[],
343
+ path: emptyPath,
344
+ index: keyOrIndex as number,
345
+ newValue,
346
+ oldValue,
347
+ isInit,
348
+ }
349
+ default:
350
+ throw failure("unsupported deep change type")
351
+ }
352
+ }
353
+
147
354
  function emitGlobalDeepChange(obj: object, change: DeepChange): void {
148
355
  for (let i = 0; i < globalDeepChangeListeners.length; i++) {
149
356
  const listener = globalDeepChangeListeners[i]
@@ -152,21 +359,32 @@ function emitGlobalDeepChange(obj: object, change: DeepChange): void {
152
359
  }
153
360
 
154
361
  function emitDeepChangeToListeners(obj: object, change: DeepChange): void {
155
- const pathPrefix: WritablePath = []
362
+ // Segments are pushed in child-to-root order (O(1) each) rather than unshifted
363
+ // at the front (O(depth) each), keeping the whole walk O(depth) instead of
364
+ // O(depth^2) on deep trees.
365
+ const reversedPrefix: WritablePath = []
156
366
 
157
- emitDeepChangeForTarget(obj, change, pathPrefix)
367
+ emitDeepChangeForTarget(obj, change, reversedPrefix, 0)
158
368
 
159
369
  // and also emit subtree listeners all the way to the root
160
370
  let parentPath = fastGetParentPath(obj, false)
161
371
  while (parentPath) {
162
- pathPrefix.unshift(parentPath.path)
163
- emitDeepChangeForTarget(parentPath.parent, change, pathPrefix)
372
+ reversedPrefix.push(parentPath.path)
373
+ emitDeepChangeForTarget(parentPath.parent, change, reversedPrefix, reversedPrefix.length)
164
374
 
165
375
  parentPath = fastGetParentPath(parentPath.parent, false)
166
376
  }
167
377
  }
168
378
 
169
- function emitDeepChangeForTarget(obj: object, change: DeepChange, pathPrefix: Path): void {
379
+ // `reversedPrefix` holds the path segments from the changed node up to (but not
380
+ // including) `obj`, in child-to-root order; the emitted path prepends that slice
381
+ // reversed (root-to-child). `prefixCount` is how many leading entries apply.
382
+ function emitDeepChangeForTarget(
383
+ obj: object,
384
+ change: DeepChange,
385
+ reversedPrefix: readonly PathElement[],
386
+ prefixCount: number
387
+ ): void {
170
388
  const listenersForObject = deepChangeListeners.get(obj)
171
389
 
172
390
  if (!listenersForObject || listenersForObject.length === 0) {
@@ -174,11 +392,8 @@ function emitDeepChangeForTarget(obj: object, change: DeepChange, pathPrefix: Pa
174
392
  }
175
393
 
176
394
  const changeWithPath =
177
- pathPrefix.length > 0
178
- ? {
179
- ...change,
180
- path: [...pathPrefix, ...change.path],
181
- }
395
+ prefixCount > 0
396
+ ? { ...change, path: buildPrefixedPath(reversedPrefix, prefixCount, change.path) }
182
397
  : change
183
398
 
184
399
  for (let i = 0; i < listenersForObject.length; i++) {
@@ -187,6 +402,21 @@ function emitDeepChangeForTarget(obj: object, change: DeepChange, pathPrefix: Pa
187
402
  }
188
403
  }
189
404
 
405
+ function buildPrefixedPath(
406
+ reversedPrefix: readonly PathElement[],
407
+ prefixCount: number,
408
+ suffix: Path
409
+ ): WritablePath {
410
+ const newPath: WritablePath = new Array(prefixCount + suffix.length)
411
+ for (let i = 0; i < prefixCount; i++) {
412
+ newPath[i] = reversedPrefix[prefixCount - 1 - i]
413
+ }
414
+ for (let j = 0; j < suffix.length; j++) {
415
+ newPath[prefixCount + j] = suffix[j]
416
+ }
417
+ return newPath
418
+ }
419
+
190
420
  /**
191
421
  * Adds a listener that will be called every time a deep change is generated for the tree of the given target object.
192
422
  * Unlike `onPatches`, this provides raw MobX change information including proper splice detection for arrays.
@@ -53,10 +53,10 @@ export abstract class BaseModel<
53
53
  ModelIdPropertyName extends string = never,
54
54
  > {
55
55
  // just to make typing work properly
56
- [propsTypeSymbol]!: TProps;
57
- [fromSnapshotOverrideTypeSymbol]!: FromSnapshotOverride;
58
- [toSnapshotOverrideTypeSymbol]!: ToSnapshotOverride;
59
- [modelIdPropertyNameSymbol]!: ModelIdPropertyName
56
+ declare [propsTypeSymbol]: TProps
57
+ declare [fromSnapshotOverrideTypeSymbol]: FromSnapshotOverride
58
+ declare [toSnapshotOverrideTypeSymbol]: ToSnapshotOverride
59
+ declare [modelIdPropertyNameSymbol]: ModelIdPropertyName
60
60
 
61
61
  /**
62
62
  * Model type name.
@@ -139,14 +139,6 @@ export abstract class BaseModel<
139
139
 
140
140
  Object.setPrototypeOf(this, modelClass!.prototype)
141
141
 
142
- const self = this as any
143
-
144
- // delete unnecessary props
145
- delete self[propsTypeSymbol]
146
- delete self[fromSnapshotOverrideTypeSymbol]
147
- delete self[toSnapshotOverrideTypeSymbol]
148
- delete self[modelIdPropertyNameSymbol]
149
-
150
142
  if (snapshotInitialData) {
151
143
  // from snapshot
152
144
  internalFromSnapshotModel(this, snapshotInitialData, modelClass!, !!generateNewIds)
@@ -217,6 +217,9 @@ export interface ModelOptions<TProps extends ModelProps, FS, TS> {
217
217
  /**
218
218
  * Optional transformation that will be run when converting from a snapshot to the data part of the model.
219
219
  * Useful for example to do versioning and keep the data part up to date with the latest version of the model.
220
+ * Snapshot processors must be pure and deterministic. The model's canonical output snapshot must round-trip
221
+ * through this processor to the same model data. Reconciliation may skip invoking it when the input already
222
+ * equals the model's current canonical output snapshot.
220
223
  *
221
224
  * @param sn The custom input snapshot.
222
225
  * @returns An input snapshot that must match the expected model input snapshot.
@@ -225,6 +228,8 @@ export interface ModelOptions<TProps extends ModelProps, FS, TS> {
225
228
 
226
229
  /**
227
230
  * Optional transformation that will be run when converting the data part of the model into a snapshot.
231
+ * Snapshot processors must be pure and deterministic, and their canonical output must round-trip through the
232
+ * corresponding input processor to the same model data. Snapshot generation may cache processor results.
228
233
  *
229
234
  * @param sn The output snapshot.
230
235
  * @param modelInstance The model instance.
@@ -3,6 +3,7 @@ import type { O } from "ts-toolbelt"
3
3
  import {
4
4
  DeepChangeType,
5
5
  emitDeepChange,
6
+ hasGlobalDeepChangeListeners,
6
7
  type ObjectAddChange,
7
8
  type ObjectUpdateChange,
8
9
  } from "../deepChange/onDeepChange"
@@ -12,8 +13,13 @@ import { modelInfoByClass } from "../modelShared/modelInfo"
12
13
  import { getInternalModelClassPropsInfo } from "../modelShared/modelPropsInfo"
13
14
  import { applyModelInitializers } from "../modelShared/newModel"
14
15
  import { getModelPropStoredDefaultValue, noDefaultValue } from "../modelShared/prop"
15
- import { createPatchForObjectValueChange, emitPatches } from "../patch/emitPatch"
16
+ import {
17
+ createPatchForObjectValueChange,
18
+ emitPatches,
19
+ hasGlobalPatchListeners,
20
+ } from "../patch/emitPatch"
16
21
  import type { Patch } from "../patch/Patch"
22
+ import { invalidateModelInitialDataSnapshot } from "../snapshot/modelInitialData"
17
23
  import { tweakModel } from "../tweaker/tweakModel"
18
24
  import { tweakPlainObject } from "../tweaker/tweakPlainObject"
19
25
  import { failure, inDevMode, makePropReadonly } from "../utils"
@@ -72,9 +78,10 @@ export const internalNewModel = action(
72
78
  let changed = false
73
79
 
74
80
  // apply untransform (if any) if not in snapshot mode
75
- if (propData._transform) {
81
+ const transform = propData._getTransform ? propData._getTransform() : propData._transform
82
+ if (transform) {
76
83
  changed = true
77
- newValue = propData._transform.untransform(newValue, modelObj, k)
84
+ newValue = transform.untransform(newValue, modelObj, k)
78
85
  }
79
86
 
80
87
  // apply default value (if needed)
@@ -145,17 +152,28 @@ export const internalFromSnapshotModel = action(
145
152
  const modelObj = origModelObj as O.Writable<M>
146
153
  modelObj[modelTypeKey] = modelInfo.name
147
154
 
148
- const patches: Patch[] = []
149
- const inversePatches: Patch[] = []
150
- const defaultsApplied: { key: string; oldValue: unknown; newValue: unknown }[] = []
155
+ // At this point the model is not attached to a tree yet, so initialization
156
+ // corrections can only be observed by global listeners. Tree-local listeners
157
+ // will observe the eventual insertion of the fully initialized model instead.
158
+ const shouldEmitPatches = hasGlobalPatchListeners()
159
+ const shouldEmitDeepChanges = hasGlobalDeepChangeListeners()
160
+ let patches: Patch[] | undefined
161
+ let inversePatches: Patch[] | undefined
162
+ let defaultsApplied: { key: string; oldValue: unknown; newValue: unknown }[] | undefined
151
163
 
152
164
  if (modelIdPropertyName) {
153
165
  const initialValue = initialData[modelIdPropertyName]
154
166
  const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id)
155
167
 
156
168
  if (valueChanged) {
169
+ invalidateModelInitialDataSnapshot(initialData)
170
+ }
171
+
172
+ if (valueChanged && shouldEmitPatches) {
157
173
  const modelIdPath = [modelIdPropertyName]
158
174
 
175
+ patches ??= []
176
+ inversePatches ??= []
159
177
  patches.push(createPatchForObjectValueChange(modelIdPath, initialValue, id))
160
178
  inversePatches.push(createPatchForObjectValueChange(modelIdPath, id, initialValue))
161
179
  }
@@ -192,15 +210,23 @@ export const internalFromSnapshotModel = action(
192
210
  if (changed) {
193
211
  // setIfDifferent not required
194
212
  set(initialData, k, newValue)
213
+ invalidateModelInitialDataSnapshot(initialData)
195
214
 
196
215
  if (newValue !== initialValue) {
197
- const propPath = [k]
216
+ if (shouldEmitPatches) {
217
+ const propPath = [k]
198
218
 
199
- patches.push(createPatchForObjectValueChange(propPath, initialValue, newValue))
200
- inversePatches.push(createPatchForObjectValueChange(propPath, newValue, initialValue))
219
+ patches ??= []
220
+ inversePatches ??= []
221
+ patches.push(createPatchForObjectValueChange(propPath, initialValue, newValue))
222
+ inversePatches.push(createPatchForObjectValueChange(propPath, newValue, initialValue))
223
+ }
201
224
 
202
225
  // Track defaults applied for deep change emission
203
- defaultsApplied.push({ key: k, oldValue: initialValue, newValue })
226
+ if (shouldEmitDeepChanges) {
227
+ defaultsApplied ??= []
228
+ defaultsApplied.push({ key: k, oldValue: initialValue, newValue })
229
+ }
204
230
  }
205
231
  }
206
232
  }
@@ -208,9 +234,11 @@ export const internalFromSnapshotModel = action(
208
234
  // also emit a patch for modelType, since it will get included in the snapshot
209
235
  const initialModelType = snapshotInitialData?.unprocessedModelType
210
236
  const newModelType = modelInfo.name
211
- if (initialModelType !== newModelType) {
237
+ if (initialModelType !== newModelType && shouldEmitPatches) {
212
238
  const modelTypePath = [modelTypeKey]
213
239
 
240
+ patches ??= []
241
+ inversePatches ??= []
214
242
  patches.push(createPatchForObjectValueChange(modelTypePath, initialModelType, newModelType))
215
243
  inversePatches.push(
216
244
  createPatchForObjectValueChange(modelTypePath, newModelType, initialModelType)
@@ -219,35 +247,39 @@ export const internalFromSnapshotModel = action(
219
247
 
220
248
  finalizeNewModel(modelObj, initialData, modelClass)
221
249
 
222
- emitPatches(modelObj, patches, inversePatches)
250
+ if (patches && inversePatches) {
251
+ emitPatches(modelObj, patches, inversePatches)
252
+ }
223
253
 
224
254
  // Emit deep changes for defaults applied during fromSnapshot
225
255
  // These are emitted with isInit: true so bindings can sync them to CRDTs
226
- for (const { key, oldValue } of defaultsApplied) {
227
- const target = modelObj.$
228
- if (oldValue === undefined) {
229
- // Key was added (didn't exist in snapshot)
230
- const change: ObjectAddChange = {
231
- type: DeepChangeType.ObjectAdd,
232
- path: [],
233
- target,
234
- key,
235
- newValue: target[key], // Use the tweaked value from $
236
- isInit: true,
237
- }
238
- emitDeepChange(modelObj, change)
239
- } else {
240
- // Key was updated (had a different value in snapshot, e.g., null -> default)
241
- const change: ObjectUpdateChange = {
242
- type: DeepChangeType.ObjectUpdate,
243
- path: [],
244
- target,
245
- key,
246
- newValue: target[key], // Use the tweaked value from $
247
- oldValue,
248
- isInit: true,
256
+ if (defaultsApplied) {
257
+ for (const { key, oldValue } of defaultsApplied) {
258
+ const target = modelObj.$
259
+ if (oldValue === undefined) {
260
+ // Key was added (didn't exist in snapshot)
261
+ const change: ObjectAddChange = {
262
+ type: DeepChangeType.ObjectAdd,
263
+ path: [],
264
+ target,
265
+ key,
266
+ newValue: target[key], // Use the tweaked value from $
267
+ isInit: true,
268
+ }
269
+ emitDeepChange(modelObj, change)
270
+ } else {
271
+ // Key was updated (had a different value in snapshot, e.g., null -> default)
272
+ const change: ObjectUpdateChange = {
273
+ type: DeepChangeType.ObjectUpdate,
274
+ path: [],
275
+ target,
276
+ key,
277
+ newValue: target[key], // Use the tweaked value from $
278
+ oldValue,
279
+ isInit: true,
280
+ }
281
+ emitDeepChange(modelObj, change)
249
282
  }
250
- emitDeepChange(modelObj, change)
251
283
  }
252
284
  }
253
285