mobx-keystone 1.21.0 → 1.22.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.
@@ -1,11 +1,24 @@
1
+ export interface ConnectReduxDevToolsOptions {
2
+ /**
3
+ * If it should show the arguments near the action name.
4
+ *
5
+ * @default true
6
+ */
7
+ logArgsNearName?: boolean;
8
+ /**
9
+ * If it should log child actions when they run inside another action tracked
10
+ * by the same target tree.
11
+ *
12
+ * @default true
13
+ */
14
+ logChildActions?: boolean;
15
+ }
1
16
  /**
2
17
  * Connects a tree node to a redux dev tools instance.
3
18
  *
4
19
  * @param remotedevPackage The remotedev package (usually the result of `require("remoteDev")`) (https://www.npmjs.com/package/remotedev).
5
20
  * @param remotedevConnection The result of a connect method from the remotedev package (usually the result of `remoteDev.connectViaExtension(...)`).
6
21
  * @param target Object to use as root.
7
- * @param [options] Optional options object. `logArgsNearName` if it should show the arguments near the action name (default is `true`).
22
+ * @param [options] Optional options object.
8
23
  */
9
- export declare function connectReduxDevTools(remotedevPackage: any, remotedevConnection: any, target: object, options?: {
10
- logArgsNearName?: boolean;
11
- }): void;
24
+ export declare function connectReduxDevTools(remotedevPackage: any, remotedevConnection: any, target: object, options?: ConnectReduxDevToolsOptions): void;
@@ -19,9 +19,19 @@ export declare class TypeCheckError {
19
19
  readonly modelTrail?: readonly string[];
20
20
  /**
21
21
  * Creates a type check error from an object payload.
22
- * Prefer the object form; the positional form is kept for backwards compatibility.
22
+ * @param data Type check error data.
23
23
  */
24
- constructor(...args: [data: TypeCheckErrorData] | [path: Path, expectedTypeName: string, actualValue: any, typeCheckedValue?: any]);
24
+ constructor(data: TypeCheckErrorData);
25
+ /**
26
+ * @deprecated Use the object payload constructor instead for better readability and maintainability.
27
+ *
28
+ * Creates a type check error from positional parameters.
29
+ * @param path Sub-path (where the root is the value being type checked) where the error occurred.
30
+ * @param expectedTypeName Name of the expected type.
31
+ * @param actualValue Actual value.
32
+ * @param typeCheckedValue The value where the type check was invoked.
33
+ */
34
+ constructor(path: Path, expectedTypeName: string, actualValue: any, typeCheckedValue?: any);
25
35
  /**
26
36
  * Throws the type check error as an actual error.
27
37
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "1.21.0",
3
+ "version": "1.22.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",
@@ -66,25 +66,25 @@
66
66
  "mobx": "^6.0.0 || ^5.0.0 || ^4.0.0"
67
67
  },
68
68
  "devDependencies": {
69
- "@babel/core": "^7.29.0",
70
- "@babel/plugin-proposal-class-properties": "^7.18.6",
71
- "@babel/plugin-proposal-decorators": "^7.29.0",
72
- "@babel/preset-env": "^7.29.0",
73
- "@babel/preset-typescript": "^7.28.5",
74
- "@swc/core": "^1.15.18",
69
+ "@babel/core": "^8.0.1",
70
+ "@babel/plugin-proposal-decorators": "^8.0.2",
71
+ "@babel/plugin-transform-class-properties": "^8.0.1",
72
+ "@babel/plugin-transform-typescript": "^8.0.1",
73
+ "@babel/preset-env": "^8.0.2",
74
+ "@swc/core": "^1.15.43",
75
75
  "@types/babel__core": "^7.20.5",
76
- "@types/node": "^25.5.0",
77
- "@vitest/coverage-v8": "^4.1.0",
76
+ "@types/node": "^26.0.1",
77
+ "@vitest/coverage-v8": "^4.1.9",
78
78
  "mobx-v4": "npm:mobx@^4.15.7",
79
79
  "mobx-v5": "npm:mobx@^5.15.7",
80
80
  "shx": "^0.4.0",
81
81
  "spec.ts": "^1.1.3",
82
82
  "ts-node": "^10.9.2",
83
- "typedoc": "^0.28.18",
84
- "typescript": "^6.0.2",
85
- "vite": "^8.0.0",
86
- "vite-plugin-dts": "^4.5.4",
87
- "vitest": "^4.1.0"
83
+ "typedoc": "^0.28.19",
84
+ "typescript": "^6.0.3",
85
+ "vite": "^8.1.0",
86
+ "vite-plugin-dts": "^5.0.3",
87
+ "vitest": "^4.1.9"
88
88
  },
89
89
  "dependencies": {
90
90
  "fast-deep-equal": "^3.1.3",
@@ -9,26 +9,42 @@ import { applySnapshot } from "../snapshot/applySnapshot"
9
9
  import { getSnapshot } from "../snapshot/getSnapshot"
10
10
  import { assertTweakedObject } from "../tweaker/core"
11
11
 
12
+ export interface ConnectReduxDevToolsOptions {
13
+ /**
14
+ * If it should show the arguments near the action name.
15
+ *
16
+ * @default true
17
+ */
18
+ logArgsNearName?: boolean
19
+
20
+ /**
21
+ * If it should log child actions when they run inside another action tracked
22
+ * by the same target tree.
23
+ *
24
+ * @default true
25
+ */
26
+ logChildActions?: boolean
27
+ }
28
+
12
29
  /**
13
30
  * Connects a tree node to a redux dev tools instance.
14
31
  *
15
32
  * @param remotedevPackage The remotedev package (usually the result of `require("remoteDev")`) (https://www.npmjs.com/package/remotedev).
16
33
  * @param remotedevConnection The result of a connect method from the remotedev package (usually the result of `remoteDev.connectViaExtension(...)`).
17
34
  * @param target Object to use as root.
18
- * @param [options] Optional options object. `logArgsNearName` if it should show the arguments near the action name (default is `true`).
35
+ * @param [options] Optional options object.
19
36
  */
20
37
  export function connectReduxDevTools(
21
38
  remotedevPackage: any,
22
39
  remotedevConnection: any,
23
40
  target: object,
24
- options?: {
25
- logArgsNearName?: boolean
26
- }
41
+ options?: ConnectReduxDevToolsOptions
27
42
  ) {
28
43
  assertTweakedObject(target, "target")
29
44
 
30
45
  const opts = {
31
46
  logArgsNearName: true,
47
+ logChildActions: true,
32
48
  ...options,
33
49
  }
34
50
 
@@ -48,12 +64,15 @@ export function connectReduxDevTools(
48
64
  const actionIdSymbol = Symbol("actionId")
49
65
 
50
66
  actionTrackingMiddleware(target, {
67
+ filter(ctx) {
68
+ return opts.logChildActions || !hasDevToolsTrackedParentContext(ctx)
69
+ },
51
70
  onStart(ctx) {
52
71
  ctx.data[actionIdSymbol] = currentActionId++
53
72
  },
54
73
  onResume(ctx) {
55
74
  // give a chance to the parent to log its own changes before the child starts
56
- if (ctx.parentContext) {
75
+ if (opts.logChildActions && ctx.parentContext) {
57
76
  log(ctx.parentContext, undefined)
58
77
  }
59
78
  log(ctx, undefined)
@@ -184,4 +203,17 @@ export function connectReduxDevTools(
184
203
 
185
204
  return name
186
205
  }
206
+
207
+ function hasDevToolsTrackedParentContext(ctx: SimpleActionContext) {
208
+ let parentContext = ctx.parentContext
209
+
210
+ while (parentContext) {
211
+ if (parentContext.data[actionIdSymbol] !== undefined) {
212
+ return true
213
+ }
214
+ parentContext = parentContext.parentContext
215
+ }
216
+
217
+ return false
218
+ }
187
219
  }
@@ -1,21 +1,15 @@
1
- import { observable } from "mobx"
2
1
  import { tweakArray } from "../tweaker/tweakArray"
3
2
  import { isArray } from "../utils"
4
3
  import { withErrorPathSegment } from "../utils/errorDiagnostics"
5
- import {
6
- type FromSnapshotContext,
7
- internalFromSnapshot,
8
- observableOptions,
9
- registerSnapshotter,
10
- } from "./fromSnapshot"
4
+ import { type FromSnapshotContext, internalFromSnapshot, registerSnapshotter } from "./fromSnapshot"
11
5
  import type { SnapshotInOfObject } from "./SnapshotOf"
12
6
  import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
13
7
 
14
8
  function fromArraySnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotContext): any[] {
15
- const arr = observable.array([] as any[], observableOptions)
16
9
  const ln = sn.length
10
+ const arr: any[] = new Array(ln)
17
11
  for (let i = 0; i < ln; i++) {
18
- arr.push(withErrorPathSegment(i, () => internalFromSnapshot(sn[i], ctx)))
12
+ arr[i] = withErrorPathSegment(i, () => internalFromSnapshot(sn[i], ctx))
19
13
  }
20
14
  return tweakArray(arr, undefined, true)
21
15
  }
@@ -1,6 +1,6 @@
1
- import { observable, set } from "mobx"
1
+ import { observable } from "mobx"
2
2
  import { tweakPlainObject } from "../tweaker/tweakPlainObject"
3
- import { isPlainObject } from "../utils"
3
+ import { isPlainObject, setOwnProp } from "../utils"
4
4
  import { withErrorPathSegment } from "../utils/errorDiagnostics"
5
5
  import {
6
6
  type FromSnapshotContext,
@@ -12,21 +12,26 @@ import type { SnapshotInOfObject } from "./SnapshotOf"
12
12
  import { SnapshotterAndReconcilerPriority } from "./SnapshotterAndReconcilerPriority"
13
13
 
14
14
  function fromPlainObjectSnapshot(sn: SnapshotInOfObject<any>, ctx: FromSnapshotContext): object {
15
- const plainObj = observable.object({}, undefined, observableOptions)
15
+ const plainObj: Record<string, unknown> = {}
16
16
 
17
17
  const snKeys = Object.keys(sn)
18
18
  const snKeysLen = snKeys.length
19
19
  for (let i = 0; i < snKeysLen; i++) {
20
20
  const k = snKeys[i]
21
21
  const v = sn[k]
22
- // setIfDifferent not required
23
- set(
22
+ setOwnProp(
24
23
  plainObj,
25
24
  k,
26
25
  withErrorPathSegment(k, () => internalFromSnapshot(v, ctx))
27
26
  )
28
27
  }
29
- return tweakPlainObject(plainObj, undefined, undefined, true, false)
28
+ return tweakPlainObject(
29
+ observable.object(plainObj, undefined, observableOptions),
30
+ undefined,
31
+ undefined,
32
+ true,
33
+ false
34
+ )
30
35
  }
31
36
 
32
37
  /**
@@ -1,11 +1,11 @@
1
- import { action, observable, set } from "mobx"
1
+ import { action, observable } from "mobx"
2
2
  import type { AnyModel } from "../model/BaseModel"
3
3
  import { isReservedModelKey } from "../model/metadata"
4
4
  import { resolveStandardTypeNoThrow, resolveTypeChecker } from "../types/resolveTypeChecker"
5
5
  import type { AnyType, TypeToData, TypeToSnapshotIn } from "../types/schemas"
6
6
  import { isLateTypeChecker, TypeChecker } from "../types/TypeChecker"
7
7
  import { resolveCodecSupport } from "../types/utility/typesCodec"
8
- import { isMap, isPrimitive, isSet } from "../utils"
8
+ import { isMap, isPrimitive, isSet, setOwnProp } from "../utils"
9
9
  import { runWithErrorDiagnosticsContext, withErrorPathSegment } from "../utils/errorDiagnostics"
10
10
  import { registerDefaultSnapshotters } from "./registerDefaultSnapshotters"
11
11
  import type { SnapshotInOf, SnapshotInOfModel, SnapshotOutOf } from "./SnapshotOf"
@@ -178,7 +178,7 @@ function snapshotToInitialData(
178
178
  ctx: FromSnapshotContext,
179
179
  processedSn: SnapshotInOfModel<AnyModel>
180
180
  ): any {
181
- const initialData = observable.object({}, undefined, observableOptions)
181
+ const initialData: Record<string, unknown> = {}
182
182
 
183
183
  const processedSnKeys = Object.keys(processedSn)
184
184
  const processedSnKeysLen = processedSnKeys.length
@@ -187,11 +187,10 @@ function snapshotToInitialData(
187
187
  if (!isReservedModelKey(k)) {
188
188
  const v = processedSn[k]
189
189
  const snapshotValue = withErrorPathSegment(k, () => internalFromSnapshot(v, ctx))
190
- // setIfDifferent not required
191
- set(initialData, k, snapshotValue)
190
+ setOwnProp(initialData, k, snapshotValue)
192
191
  }
193
192
  }
194
- return initialData
193
+ return observable.object(initialData, undefined, observableOptions)
195
194
  }
196
195
 
197
196
  export const observableOptions = {
@@ -23,7 +23,6 @@ import {
23
23
  updateInternalSnapshot,
24
24
  } from "../snapshot/internal"
25
25
  import { failure, inDevMode, isArray, isPrimitive } from "../utils"
26
- import { setIfDifferent } from "../utils/setIfDifferent"
27
26
  import { runningWithoutSnapshotOrPatches, tweakedObjects } from "./core"
28
27
  import { TweakerPriority } from "./TweakerPriority"
29
28
  import { registerTweaker, tweak } from "./tweak"
@@ -35,15 +34,25 @@ import { runTypeCheckingAfterChange } from "./typeChecking"
35
34
  export function tweakArray<T extends any[]>(
36
35
  value: T,
37
36
  parentPath: ParentPath<any> | undefined,
38
- doNotTweakChildren: boolean
37
+ childrenAlreadyTweaked: boolean
39
38
  ): T {
40
39
  const originalArr: ReadonlyArray<any> = value
41
40
  const arrLn = originalArr.length
42
- const tweakedArr = isObservableArray(originalArr)
43
- ? originalArr
44
- : observable.array(undefined, observableOptions)
45
- if (tweakedArr !== originalArr) {
46
- tweakedArr.length = originalArr.length
41
+ const originalArrIsObservable = isObservableArray(originalArr)
42
+ let tweakedArr: IObservableArray<any>
43
+ // Fresh arrays whose children still need tweaking are collected into a plain
44
+ // buffer and inserted in one bulk replace at the end.
45
+ let buffer: any[] | undefined
46
+
47
+ if (originalArrIsObservable) {
48
+ tweakedArr = originalArr as IObservableArray<any>
49
+ } else if (childrenAlreadyTweaked) {
50
+ // Snapshot arrays already contain tweaked children, so MobX can initialize
51
+ // the observable array directly from the source values.
52
+ tweakedArr = observable.array(originalArr as any[], observableOptions)
53
+ } else {
54
+ tweakedArr = observable.array(undefined, observableOptions)
55
+ buffer = new Array(arrLn)
47
56
  }
48
57
 
49
58
  let interceptDisposer: () => void
@@ -67,13 +76,47 @@ export function tweakArray<T extends any[]>(
67
76
  const untransformedSn: any[] = []
68
77
  untransformedSn.length = arrLn
69
78
 
79
+ // externally provided observable arrays keep unchanged slots untouched, but
80
+ // consecutive child replacements can still be applied as single splice ranges
81
+ let replacementRangeStart = -1
82
+ let replacementRangeItems: any[] | undefined
83
+
84
+ const flushReplacementRange = () => {
85
+ if (replacementRangeStart >= 0) {
86
+ tweakedArr.spliceWithArray(
87
+ replacementRangeStart,
88
+ replacementRangeItems!.length,
89
+ replacementRangeItems!
90
+ )
91
+ replacementRangeStart = -1
92
+ replacementRangeItems = undefined
93
+ }
94
+ }
95
+
96
+ const queueReplacementIfDifferent = (index: number, value: unknown) => {
97
+ if (tweakedArr[index] === value && index in tweakedArr) {
98
+ flushReplacementRange()
99
+ return
100
+ }
101
+
102
+ if (replacementRangeStart + (replacementRangeItems?.length ?? 0) === index) {
103
+ replacementRangeItems!.push(value)
104
+ } else {
105
+ flushReplacementRange()
106
+ replacementRangeStart = index
107
+ replacementRangeItems = [value]
108
+ }
109
+ }
110
+
70
111
  // substitute initial values by proxied values
71
112
  for (let i = 0; i < arrLn; i++) {
72
113
  const v = originalArr[i]
73
114
 
74
115
  if (isPrimitive(v)) {
75
- if (!doNotTweakChildren) {
76
- setIfDifferent(tweakedArr, i, v)
116
+ if (buffer) {
117
+ buffer[i] = v
118
+ } else if (!childrenAlreadyTweaked) {
119
+ queueReplacementIfDifferent(i, v)
77
120
  }
78
121
 
79
122
  untransformedSn[i] = v
@@ -81,7 +124,7 @@ export function tweakArray<T extends any[]>(
81
124
  const path = { parent: tweakedArr, path: i }
82
125
 
83
126
  let tweakedValue: any
84
- if (doNotTweakChildren) {
127
+ if (childrenAlreadyTweaked) {
85
128
  tweakedValue = v
86
129
  setParent(
87
130
  tweakedValue, // value
@@ -93,7 +136,12 @@ export function tweakArray<T extends any[]>(
93
136
  )
94
137
  } else {
95
138
  tweakedValue = tweak(v, path)
96
- setIfDifferent(tweakedArr, i, tweakedValue)
139
+ }
140
+
141
+ if (buffer) {
142
+ buffer[i] = tweakedValue
143
+ } else if (!childrenAlreadyTweaked) {
144
+ queueReplacementIfDifferent(i, tweakedValue)
97
145
  }
98
146
 
99
147
  const valueSn = getInternalSnapshot(tweakedValue)!
@@ -101,6 +149,13 @@ export function tweakArray<T extends any[]>(
101
149
  }
102
150
  }
103
151
 
152
+ if (buffer) {
153
+ // done before the intercept/observe handlers are installed, so this
154
+ // is a single plain mobx bulk insertion
155
+ tweakedArr.replace(buffer)
156
+ } else {
157
+ flushReplacementRange()
158
+ }
104
159
  setNewInternalSnapshot(tweakedArr, untransformedSn, undefined)
105
160
 
106
161
  interceptDisposer = intercept(tweakedArr, interceptArrayMutation.bind(undefined, tweakedArr))
@@ -30,14 +30,32 @@ export class TypeCheckError {
30
30
 
31
31
  /**
32
32
  * Creates a type check error from an object payload.
33
- * Prefer the object form; the positional form is kept for backwards compatibility.
33
+ * @param data Type check error data.
34
34
  */
35
+ constructor(data: TypeCheckErrorData)
36
+ /**
37
+ * @deprecated Use the object payload constructor instead for better readability and maintainability.
38
+ *
39
+ * Creates a type check error from positional parameters.
40
+ * @param path Sub-path (where the root is the value being type checked) where the error occurred.
41
+ * @param expectedTypeName Name of the expected type.
42
+ * @param actualValue Actual value.
43
+ * @param typeCheckedValue The value where the type check was invoked.
44
+ */
45
+ constructor(path: Path, expectedTypeName: string, actualValue: any, typeCheckedValue?: any)
46
+
35
47
  constructor(
36
- ...args:
37
- | [data: TypeCheckErrorData]
38
- | [path: Path, expectedTypeName: string, actualValue: any, typeCheckedValue?: any]
48
+ dataOrPath: TypeCheckErrorData | Path,
49
+ expectedTypeName?: string,
50
+ actualValue?: any,
51
+ typeCheckedValue?: any
39
52
  ) {
40
- const data = normalizeTypeCheckErrorData(args[0], args[1], args[2], args[3])
53
+ const data = normalizeTypeCheckErrorData(
54
+ dataOrPath,
55
+ expectedTypeName,
56
+ actualValue,
57
+ typeCheckedValue
58
+ )
41
59
  const resolvedData = resolveTypeCheckErrorData(data)
42
60
 
43
61
  this.path = data.path