mobx-keystone 1.24.0 → 1.25.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.
@@ -2,7 +2,7 @@ import { AnyDataModel } from '../dataModel/BaseDataModel';
2
2
  import { AnyModel } from '../model/BaseModel';
3
3
  import { modelTypeKey } from '../model/metadata';
4
4
  import { Flatten, IsNeverType } from '../utils/types';
5
- import { ModelPropsToSnapshotCreationData, ModelPropsToSnapshotData, ModelPropsToTransformedCreationData, ModelPropsToTransformedData, ModelPropsToUntransformedCreationData } from './prop';
5
+ import { ModelProps, ModelPropsToSnapshotCreationData, ModelPropsToSnapshotData, ModelPropsToTransformedCreationData, ModelPropsToTransformedData, ModelPropsToUntransformedCreationData } from './prop';
6
6
  /**
7
7
  * @ignore
8
8
  */
@@ -27,6 +27,12 @@ export interface ModelClass<M extends AnyModel | AnyDataModel> {
27
27
  * Extracts the instance type of an abstract model class.
28
28
  */
29
29
  export type AbstractModelClass<M extends AnyModel | AnyDataModel> = abstract new (initialData: any) => M;
30
+ /**
31
+ * @ignore
32
+ */
33
+ export interface ModelWithProps {
34
+ [propsTypeSymbol]: ModelProps;
35
+ }
30
36
  /**
31
37
  * The props of a model.
32
38
  */
@@ -7,6 +7,11 @@ import { Flatten, IsNeverType, IsOptionalValue } from '../utils/types';
7
7
  export declare const noDefaultValue: unique symbol;
8
8
  type SetterMode = boolean | "assign";
9
9
  export type ModelPropSetterValueTransform<T> = (value: T) => T;
10
+ /** @ignore */
11
+ export interface FullModelPropTransform {
12
+ transform: (original: unknown, model: object, propName: PropertyKey, setOriginalValue: (newOriginalValue: unknown) => void) => unknown;
13
+ untransform: (transformed: unknown, model: object, propName: PropertyKey) => unknown;
14
+ }
10
15
  /**
11
16
  * A model property.
12
17
  */
@@ -1,4 +1,78 @@
1
+ import { Path } from '../parent/pathTypes';
1
2
  import { AnyStandardType } from './schemas';
3
+ import { TypeCheckError } from './TypeCheckError';
4
+ type CheckFunction = (value: any, path: Path, typeCheckedValue: any) => TypeCheckError | null;
5
+ /** @ignore */
6
+ export type SnapshotProcessor = (snapshot: any) => unknown;
7
+ /**
8
+ * A lazily discovered snapshot-processor node. Dependencies are deliberately
9
+ * TypeCheckers rather than processors so recursive graphs can be discovered
10
+ * before any processor body is compiled.
11
+ *
12
+ * @ignore
13
+ */
14
+ export interface SnapshotProcessorPlan {
15
+ readonly intrinsic?: boolean;
16
+ readonly getDependencies: () => ReadonlyArray<TypeChecker>;
17
+ readonly compile: (dependencyProcessors: ReadonlyArray<SnapshotProcessor | undefined>) => SnapshotProcessor | undefined;
18
+ }
19
+ type SnapshotProcessorSource = SnapshotProcessor | SnapshotProcessorPlan | undefined;
20
+ type SnapshotProcessorDirection = "from" | "to";
21
+ /** @ignore */
22
+ export declare class SnapshotProcessorPlanNode {
23
+ private readonly owner;
24
+ private readonly direction;
25
+ private readonly source;
26
+ private presence;
27
+ private dependencies;
28
+ private discovering;
29
+ private compiling;
30
+ private compiled;
31
+ private compiledProcessor;
32
+ private forwardingProcessor;
33
+ constructor(owner: TypeChecker | undefined, direction: SnapshotProcessorDirection, source: SnapshotProcessorSource);
34
+ resolve(): SnapshotProcessor | undefined;
35
+ private discover;
36
+ private compile;
37
+ private compileForDependency;
38
+ }
39
+ /**
40
+ * @ignore
41
+ */
42
+ export declare enum TypeCheckerBaseType {
43
+ Object = "object",
44
+ Array = "array",
45
+ Primitive = "primitive",
46
+ Any = "any"
47
+ }
48
+ /**
49
+ * @ignore
50
+ */
51
+ export declare class TypeChecker {
52
+ unchecked: boolean;
53
+ skipCheck: boolean;
54
+ readonly baseType: TypeCheckerBaseType;
55
+ private readonly _check;
56
+ readonly getTypeName: (...recursiveTypeCheckers: TypeChecker[]) => string;
57
+ readonly typeInfoGen: TypeInfoGen;
58
+ readonly snapshotType: (sn: unknown) => TypeChecker | null;
59
+ private readonly fromSnapshotProcessorSource;
60
+ private readonly toSnapshotProcessorSource;
61
+ private fromSnapshotProcessorPlanNode;
62
+ private toSnapshotProcessorPlanNode;
63
+ check(value: any, path: Path, typeCheckedValue: any): TypeCheckError | null;
64
+ private _cachedTypeInfoGen;
65
+ get typeInfo(): TypeInfo;
66
+ constructor(baseType: TypeCheckerBaseType, _check: CheckFunction | null, getTypeName: (...recursiveTypeCheckers: TypeChecker[]) => string, typeInfoGen: TypeInfoGen, snapshotType: (sn: unknown) => TypeChecker | null, fromSnapshotProcessor: SnapshotProcessorSource, toSnapshotProcessor: SnapshotProcessorSource);
67
+ getSnapshotProcessorPlanNode(direction: SnapshotProcessorDirection): SnapshotProcessorPlanNode;
68
+ private resolveSnapshotProcessor;
69
+ getFromSnapshotProcessor(): SnapshotProcessor | undefined;
70
+ getToSnapshotProcessor(): SnapshotProcessor | undefined;
71
+ fromSnapshotProcessor: (sn: any) => unknown;
72
+ private readonly _toSnapshotProcessorCache;
73
+ invalidateSnapshotProcessorCachedResult(obj: object): void;
74
+ toSnapshotProcessor: (sn: any) => unknown;
75
+ }
2
76
  /**
3
77
  * Type info base class.
4
78
  */
@@ -7,3 +81,8 @@ export declare class TypeInfo {
7
81
  readonly thisType: AnyStandardType;
8
82
  constructor(thisType: AnyStandardType);
9
83
  }
84
+ /**
85
+ * @ignore
86
+ */
87
+ export type TypeInfoGen = (t: AnyStandardType) => TypeInfo;
88
+ export {};
@@ -3,6 +3,7 @@ import { AnyDataModel } from '../dataModel/BaseDataModel';
3
3
  import { AnyModel } from '../model/BaseModel';
4
4
  import { ModelClass } from '../modelShared/BaseModelShared';
5
5
  import { SnapshotInOf, SnapshotOutOf } from '../snapshot/SnapshotOf';
6
+ import { IsOptionalValue } from '../utils/types';
6
7
  export interface Type<Name, Data> {
7
8
  /** @ignore */
8
9
  $$type: Name;
@@ -36,6 +37,20 @@ export interface ObjectOfTypes {
36
37
  /** @ignore */
37
38
  [k: string]: AnyType;
38
39
  }
40
+ /**
41
+ * Name of the properties of an object that can be set to undefined, any or unknown.
42
+ * @ignore
43
+ */
44
+ export type UndefinablePropsNames<T> = {
45
+ [K in keyof T]: IsOptionalValue<T[K], K, never>;
46
+ }[keyof T];
47
+ /**
48
+ * Computes the optional keys for an object type schema.
49
+ * @ignore
50
+ */
51
+ export type ObjectOptionalKeys<S> = UndefinablePropsNames<{
52
+ [k in keyof S]: TypeToDataOpt<S[k]> extends infer R ? R : never;
53
+ }>;
39
54
  type ObjectData<S> = O.Optional<{
40
55
  [k in keyof S]: TypeToData<S[k]> extends infer R ? R : never;
41
56
  }, ObjectOptionalKeys<S>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more",
5
5
  "keywords": [
6
6
  "mobx",
@@ -54,7 +54,8 @@
54
54
  "quick-build": "tsc",
55
55
  "quick-build-tests": "tsc -p test",
56
56
  "copy-root-files": "shx cp ../../README.md . && shx cp ../../LICENSE . && shx cp ../../CHANGELOG.md .",
57
- "build": "pnpm run quick-build && pnpm run copy-root-files && shx rm -rf dist && vite build && shx cp dist/mobx-keystone.esm.mjs dist/mobx-keystone.esm.js",
57
+ "build": "pnpm run quick-build && pnpm run copy-root-files && shx rm -rf dist && vite build && pnpm run check-declarations && shx cp dist/mobx-keystone.esm.mjs dist/mobx-keystone.esm.js",
58
+ "check-declarations": "tsc -p scripts/tsconfig.check-declarations.json",
58
59
  "test": "vitest run",
59
60
  "test:ci": "vitest run --coverage",
60
61
  "test:perf": "pnpm run build && pnpm run test:perf:run",
@@ -68,15 +69,15 @@
68
69
  "devDependencies": {
69
70
  "@babel/core": "^8.0.1",
70
71
  "@babel/plugin-proposal-decorators": "^8.0.2",
71
- "@babel/plugin-transform-class-static-block": "^8.0.1",
72
72
  "@babel/plugin-transform-class-properties": "^8.0.1",
73
+ "@babel/plugin-transform-class-static-block": "^8.0.1",
73
74
  "@babel/plugin-transform-typescript": "^8.0.1",
74
75
  "@babel/preset-env": "^8.0.2",
75
- "@swc/core": "^1.15.43",
76
+ "@swc/core": "^1.16.2",
76
77
  "@types/babel__core": "^7.20.5",
77
- "@types/node": "^26.1.1",
78
+ "@types/node": "^26.4.1",
78
79
  "@typescript/typescript6": "^6.0.2",
79
- "@vitest/coverage-v8": "^4.1.10",
80
+ "@vitest/coverage-v8": "^5.0.0",
80
81
  "mobx-v4": "npm:mobx@^4.15.7",
81
82
  "mobx-v5": "npm:mobx@^5.15.7",
82
83
  "mobx-v6": "npm:mobx@^6.0.0",
@@ -86,9 +87,9 @@
86
87
  "ts-node": "^10.9.2",
87
88
  "typedoc": "^0.28.20",
88
89
  "typescript": "^7.0.2",
89
- "vite": "^8.1.4",
90
- "vite-plugin-dts": "^5.0.3",
91
- "vitest": "^4.1.10"
90
+ "vite": "^8.2.2",
91
+ "vite-plugin-dts": "^5.1.0",
92
+ "vitest": "^5.0.0"
92
93
  },
93
94
  "dependencies": {
94
95
  "fast-deep-equal": "^3.1.3",
@@ -99,21 +99,16 @@ export interface UndoEventGroup {
99
99
 
100
100
  function toSingleEvents(
101
101
  event: UndoEventWithoutAttachedState,
102
- reverse: boolean
103
- ): ReadonlyArray<UndoSingleEvent> {
102
+ array: UndoSingleEvent[] = []
103
+ ): UndoSingleEvent[] {
104
104
  if (event.type === UndoEventType.Single) {
105
- return [event]
105
+ array.push(event)
106
106
  } else {
107
- const array: UndoSingleEvent[] = []
108
107
  for (const e of event.events) {
109
- if (reverse) {
110
- array.unshift(...toSingleEvents(e, true))
111
- } else {
112
- array.push(...toSingleEvents(e, false))
113
- }
108
+ toSingleEvents(e, array)
114
109
  }
115
- return array
116
110
  }
111
+ return array
117
112
  }
118
113
 
119
114
  /**
@@ -386,9 +381,11 @@ export class UndoManager {
386
381
  const event = this.undoQueue[this.undoQueue.length - 1]
387
382
 
388
383
  withoutUndo(() => {
389
- toSingleEvents(event, true).forEach((e) => {
390
- applyPatches(this.subtreeRoot, e.inversePatches, true)
391
- })
384
+ toSingleEvents(event)
385
+ .reverse()
386
+ .forEach((e) => {
387
+ applyPatches(this.subtreeRoot, e.inversePatches, true)
388
+ })
392
389
 
393
390
  // restore the attached state before the operation was made
394
391
  if (event.attachedState?.beforeEvent) {
@@ -411,7 +408,7 @@ export class UndoManager {
411
408
  const event = this.redoQueue[this.redoQueue.length - 1]
412
409
 
413
410
  withoutUndo(() => {
414
- toSingleEvents(event, false).forEach((e) => {
411
+ toSingleEvents(event).forEach((e) => {
415
412
  applyPatches(this.subtreeRoot, e.patches)
416
413
  })
417
414
 
@@ -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"
@@ -146,11 +147,16 @@ export abstract class BaseModel<
146
147
  // plain new
147
148
  assertIsObject(initialData, "initialData")
148
149
 
149
- internalNewModel(
150
- this,
151
- observable.object(initialData as any, undefined, { deep: false }),
152
- modelClass!
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
 
@@ -19,7 +19,7 @@ import {
19
19
  hasGlobalPatchListeners,
20
20
  } from "../patch/emitPatch"
21
21
  import type { Patch } from "../patch/Patch"
22
- import { invalidateModelInitialDataSnapshot } from "../snapshot/modelInitialData"
22
+ import { updateModelInitialDataSnapshot } from "../snapshot/modelInitialData"
23
23
  import { tweakModel } from "../tweaker/tweakModel"
24
24
  import { tweakPlainObject } from "../tweaker/tweakPlainObject"
25
25
  import { failure, inDevMode, makePropReadonly } from "../utils"
@@ -38,7 +38,8 @@ export const internalNewModel = action(
38
38
  <M extends AnyModel>(
39
39
  origModelObj: M,
40
40
  initialData: ModelCreationData<M>,
41
- modelClass: ModelClass<AnyModel>
41
+ modelClass: ModelClass<AnyModel>,
42
+ useInitialDataSnapshot: boolean
42
43
  ): void => {
43
44
  if (inDevMode) {
44
45
  assertIsModelClass(modelClass, "modelClass")
@@ -56,6 +57,9 @@ export const internalNewModel = action(
56
57
  id = (modelIdPropData._defaultFn as () => string)()
57
58
  }
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>
@@ -99,6 +103,9 @@ export const internalNewModel = action(
99
103
  if (changed) {
100
104
  // setIfDifferent not required
101
105
  set(initialData, k, newValue)
106
+ if (useInitialDataSnapshot) {
107
+ updateModelInitialDataSnapshot(initialData, k, newValue)
108
+ }
102
109
  }
103
110
  }
104
111
 
@@ -166,7 +173,7 @@ export const internalFromSnapshotModel = action(
166
173
  const valueChanged = setIfDifferentWithReturn(initialData, modelIdPropertyName, id)
167
174
 
168
175
  if (valueChanged) {
169
- invalidateModelInitialDataSnapshot(initialData)
176
+ updateModelInitialDataSnapshot(initialData, modelIdPropertyName, id)
170
177
  }
171
178
 
172
179
  if (valueChanged && shouldEmitPatches) {
@@ -210,7 +217,7 @@ export const internalFromSnapshotModel = action(
210
217
  if (changed) {
211
218
  // setIfDifferent not required
212
219
  set(initialData, k, newValue)
213
- invalidateModelInitialDataSnapshot(initialData)
220
+ updateModelInitialDataSnapshot(initialData, k, newValue)
214
221
 
215
222
  if (newValue !== initialValue) {
216
223
  if (shouldEmitPatches) {
@@ -49,7 +49,7 @@ export type AbstractModelClass<M extends AnyModel | AnyDataModel> = abstract new
49
49
  ) => M
50
50
 
51
51
  /**
52
- * @internal
52
+ * @ignore
53
53
  */
54
54
  export interface ModelWithProps {
55
55
  [propsTypeSymbol]: ModelProps
@@ -14,7 +14,7 @@ type SetterMode = boolean | "assign"
14
14
 
15
15
  export type ModelPropSetterValueTransform<T> = (value: T) => T
16
16
 
17
- /** @internal */
17
+ /** @ignore */
18
18
  export interface FullModelPropTransform {
19
19
  transform: (
20
20
  original: unknown,
@@ -19,8 +19,7 @@ export function findParent<T extends object = any>(
19
19
  predicate: (parentNode: object) => boolean,
20
20
  maxDepth = 0
21
21
  ): T | undefined {
22
- const foundParentPath = findParentPath(child, predicate, maxDepth)
23
- return foundParentPath ? foundParentPath.parent : undefined
22
+ return findParentInternal<T>(child, predicate, maxDepth)
24
23
  }
25
24
 
26
25
  /**
@@ -56,22 +55,28 @@ export function findParentPath<T extends object = any>(
56
55
  predicate: (parentNode: object) => boolean,
57
56
  maxDepth = 0
58
57
  ): FoundParentPath<T> | undefined {
59
- assertTweakedObject(child, "child")
60
-
61
58
  const path: WritablePath = []
59
+ const parent = findParentInternal<T>(child, predicate, maxDepth, path)
60
+ return parent ? { parent, path: path.reverse() } : undefined
61
+ }
62
+
63
+ function findParentInternal<T extends object>(
64
+ child: object,
65
+ predicate: (parentNode: object) => boolean,
66
+ maxDepth: number,
67
+ path?: WritablePath
68
+ ): T | undefined {
69
+ assertTweakedObject(child, "child")
62
70
 
63
71
  let current: any = child
64
72
  let depth = 0
65
73
 
66
74
  let parentPath: ParentPath<any> | undefined
67
75
  while ((parentPath = fastGetParentPath(current, true))) {
68
- path.unshift(parentPath.path)
76
+ path?.push(parentPath.path)
69
77
  current = parentPath.parent
70
78
  if (predicate(current)) {
71
- return {
72
- parent: current,
73
- path,
74
- }
79
+ return current
75
80
  }
76
81
 
77
82
  depth++
@@ -59,18 +59,7 @@ export function onChildAttachedTo(
59
59
  const t = target()
60
60
  assertTweakedObject(t, "target()")
61
61
 
62
- const children = getChildrenObjects(t, getChildrenObjectOpts)
63
-
64
- const set = new Set<object>()
65
-
66
- const iter = children.values()
67
- let cur = iter.next()
68
- while (!cur.done) {
69
- set.add(cur.value)
70
- cur = iter.next()
71
- }
72
-
73
- return set
62
+ return new Set(getChildrenObjects(t, getChildrenObjectOpts))
74
63
  }
75
64
 
76
65
  const currentChildren = opts.fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
@@ -175,10 +175,12 @@ export function fastGetRootPath<T extends object = any>(
175
175
  let parentPath: ParentPath<any> | undefined
176
176
  while ((parentPath = fastGetParentPath(root, useAtom))) {
177
177
  root = parentPath.parent
178
- path.unshift(parentPath.path)
179
- pathObjects.unshift(parentPath.parent)
178
+ path.push(parentPath.path)
179
+ pathObjects.push(parentPath.parent)
180
180
  }
181
181
 
182
+ path.reverse()
183
+ pathObjects.reverse()
182
184
  return { root, path, pathObjects } as RootPath<any>
183
185
  }
184
186
 
@@ -376,11 +378,11 @@ export function fastGetParentToChildPath(
376
378
  let current = toChild
377
379
  let parentPath: ParentPath<any> | undefined
378
380
  while ((parentPath = fastGetParentPath(current, useAtom))) {
379
- path.unshift(parentPath.path)
381
+ path.push(parentPath.path)
380
382
 
381
383
  current = parentPath.parent
382
384
  if (current === fromParent) {
383
- return path
385
+ return path.reverse()
384
386
  }
385
387
  }
386
388
  return undefined
@@ -19,6 +19,31 @@ import {
19
19
  import { addObjectChild, removeObjectChild } from "./coreObjectChildren"
20
20
  import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
21
21
 
22
+ /**
23
+ * Updates the unchanged tail of an array before a splice is applied. These
24
+ * children keep their parent, so neither attachment nor child-index work is
25
+ * needed. One action batches path notifications for the entire tail.
26
+ * @internal
27
+ */
28
+ export const reindexArrayChildren = action(
29
+ "reindexArrayChildren",
30
+ (array: readonly unknown[], oldStart: number, newStart: number): void => {
31
+ for (let i = oldStart, j = newStart; i < array.length; i++, j++) {
32
+ const value = array[i]
33
+ if (isPrimitive(value)) {
34
+ continue
35
+ }
36
+ const metadata = treeNodeMetadata.get(value as object)
37
+ if (inDevMode && metadata?.parentPath?.parent !== array) {
38
+ throw failure("assertion failed: reindexed child must already belong to the array")
39
+ }
40
+ // Replace the path object: callers may have retained the previous path.
41
+ metadata!.parentPath = { parent: array, path: j }
42
+ reportParentPathChanged(value as object)
43
+ }
44
+ }
45
+ )
46
+
22
47
  /**
23
48
  * @internal
24
49
  */
@@ -27,7 +52,6 @@ export const setParent = action(
27
52
  (
28
53
  value: any,
29
54
  parentPath: ParentPath<any> | undefined,
30
- indexChangeAllowed: boolean,
31
55
  isDataObject: boolean,
32
56
  cloneIfApplicable: boolean
33
57
  ): any => {
@@ -36,11 +60,6 @@ export const setParent = action(
36
60
  }
37
61
 
38
62
  if (inDevMode) {
39
- if (indexChangeAllowed && cloneIfApplicable) {
40
- throw failure(
41
- "assertion failed: 'indexChangeAllowed' and 'cloneIfApplicable' cannot be set at the same time"
42
- )
43
- }
44
63
  if (typeof value === "function" || typeof value === "symbol") {
45
64
  throw failure(`assertion failed: value cannot be a function or a symbol`)
46
65
  }
@@ -92,14 +111,7 @@ export const setParent = action(
92
111
  }
93
112
 
94
113
  if (oldParentPath && parentPath) {
95
- if (oldParentPath.parent === parentPath.parent && indexChangeAllowed) {
96
- // just changing the index
97
- treeNodeMetadata.get(value)!.parentPath = parentPath
98
- reportParentPathChanged(value)
99
- return value
100
- } else {
101
- throw failure("an object cannot be assigned a new parent when it already has one")
102
- }
114
+ throw failure("an object cannot be assigned a new parent when it already has one")
103
115
  }
104
116
 
105
117
  const postUntweaker = parentPath ? undefined : tryUntweak(value)
@@ -3,7 +3,6 @@ import { BuiltInAction } from "../action/builtInActions"
3
3
  import { ActionContextActionType } from "../action/context"
4
4
  import { wrapInAction } from "../action/wrapInAction"
5
5
  import { modelToDataNode } from "../parent/core"
6
- import type { PathElement } from "../parent/pathTypes"
7
6
  import type { Patch } from "../patch/Patch"
8
7
  import { reconcileSnapshot } from "../snapshot/reconcileSnapshot"
9
8
  import { assertTweakedObject } from "../tweaker/core"
@@ -84,7 +83,16 @@ const wrappedInternalApplyPatches = lazy(() =>
84
83
  )
85
84
 
86
85
  function applySinglePatch(obj: object, patch: Patch, modelPool: ModelPool): void {
87
- const { target, prop } = pathArrayToObjectAndProp(obj, patch.path)
86
+ const path = patch.path
87
+ if (inDevMode && !isArray(path)) {
88
+ throw failure(`invalid path: ${path}`)
89
+ }
90
+
91
+ let target: any = modelToDataNode(obj)
92
+ for (let i = 0; i < path.length - 1; i++) {
93
+ target = modelToDataNode(target[path[i]])
94
+ }
95
+ const prop = path.length > 0 ? path[path.length - 1] : undefined
88
96
 
89
97
  if (isArray(target)) {
90
98
  switch (patch.op) {
@@ -157,31 +165,3 @@ function applySinglePatchWithPath(obj: object, patch: Patch, modelPool: ModelPoo
157
165
  applySinglePatch(obj, patch, modelPool)
158
166
  })
159
167
  }
160
-
161
- function pathArrayToObjectAndProp(
162
- obj: object,
163
- path: Patch["path"]
164
- ): { target: any; prop?: PathElement } {
165
- if (inDevMode) {
166
- if (!isArray(path)) {
167
- throw failure(`invalid path: ${path}`)
168
- }
169
- }
170
-
171
- let target: any = modelToDataNode(obj)
172
-
173
- if (path.length === 0) {
174
- return {
175
- target,
176
- }
177
- }
178
-
179
- for (let i = 0; i <= path.length - 2; i++) {
180
- target = modelToDataNode(target[path[i]])
181
- }
182
-
183
- return {
184
- target,
185
- prop: path[path.length - 1],
186
- }
187
- }
@@ -17,6 +17,7 @@ import { assertTweakedObject } from "../tweaker/core"
17
17
  import { assertIsObject, inDevMode, isArray, isMap, isPlainObject, isSet, lazy } from "../utils"
18
18
  import { runWithErrorDiagnosticsContext } from "../utils/errorDiagnostics"
19
19
  import { ModelPool } from "../utils/ModelPool"
20
+ import { flushInternalSnapshot, getInternalSnapshot } from "./internal"
20
21
  import { reconcileSnapshot } from "./reconcileSnapshot"
21
22
  import type { SnapshotInOf, SnapshotOutOf } from "./SnapshotOf"
22
23
  import { SnapshotProcessingError } from "./SnapshotProcessingError"
@@ -52,6 +53,14 @@ export function internalApplySnapshot<T extends object>(
52
53
  const obj = this
53
54
 
54
55
  const reconcile = () => {
56
+ // Avoid building a model pool (and its deep children traversal) when the
57
+ // node already holds this exact snapshot. Child updates may still await
58
+ // propagation within an action, so flush before trusting the reference.
59
+ flushInternalSnapshot(obj, false)
60
+ if (getInternalSnapshot(obj)?.transformed === sn) {
61
+ return
62
+ }
63
+
55
64
  const modelPool = new ModelPool(obj)
56
65
  const ret = reconcileSnapshot(obj, sn, modelPool, undefined)
57
66
 
@@ -211,7 +211,9 @@ function snapshotToInitialData(
211
211
  }
212
212
  }
213
213
  const observableInitialData = observable.object(initialData, undefined, observableOptions)
214
- setModelInitialDataSnapshot(observableInitialData, initialData, allValuesPrimitive)
214
+ if (allValuesPrimitive) {
215
+ setModelInitialDataSnapshot(observableInitialData, initialData)
216
+ }
215
217
  return observableInitialData
216
218
  }
217
219
 
@@ -1,40 +1,35 @@
1
- interface ModelInitialDataSnapshot {
2
- readonly snapshot: Record<string, unknown>
3
- readonly allValuesPrimitive: boolean
4
- reusable: boolean
5
- }
1
+ import { isPrimitive, setProtoProp } from "../utils"
6
2
 
7
- const modelInitialDataSnapshots = new WeakMap<object, ModelInitialDataSnapshot>()
3
+ // Only primitive-only initial data can double as a snapshot without tweaking.
4
+ const modelInitialDataSnapshots = new WeakMap<object, Record<string, unknown>>()
8
5
 
9
6
  /**
10
7
  * @internal
11
8
  */
12
- export function setModelInitialDataSnapshot(
13
- data: object,
14
- snapshot: Record<string, unknown>,
15
- allValuesPrimitive: boolean
16
- ): void {
17
- modelInitialDataSnapshots.set(data, {
18
- snapshot,
19
- allValuesPrimitive,
20
- reusable: true,
21
- })
9
+ export function setModelInitialDataSnapshot(data: object, snapshot: Record<string, unknown>): void {
10
+ modelInitialDataSnapshots.set(data, snapshot)
22
11
  }
23
12
 
24
13
  /**
25
14
  * @internal
26
15
  */
27
- export function invalidateModelInitialDataSnapshot(data: object): void {
16
+ export function updateModelInitialDataSnapshot(data: object, key: string, value: unknown): void {
28
17
  const initialDataSnapshot = modelInitialDataSnapshots.get(data)
29
18
  if (initialDataSnapshot) {
30
- initialDataSnapshot.reusable = false
19
+ if (!isPrimitive(value)) {
20
+ modelInitialDataSnapshots.delete(data)
21
+ } else if (key === "__proto__") {
22
+ setProtoProp(initialDataSnapshot, value)
23
+ } else {
24
+ initialDataSnapshot[key] = value
25
+ }
31
26
  }
32
27
  }
33
28
 
34
29
  /**
35
30
  * @internal
36
31
  */
37
- export function takeModelInitialDataSnapshot(data: object): ModelInitialDataSnapshot | undefined {
32
+ export function takeModelInitialDataSnapshot(data: object): Record<string, unknown> | undefined {
38
33
  const initialDataSnapshot = modelInitialDataSnapshots.get(data)
39
34
  modelInitialDataSnapshots.delete(data)
40
35
  return initialDataSnapshot