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,6 +1,8 @@
1
1
  /**
2
- * Runs a block in unprocted mode, as if it were run inside a model action.
3
- * Consider using a proper model action instead since these kind of actions are not recorded.
2
+ * Runs a block in unprotected mode, as if it were run inside a model action.
3
+ * Structural mutations share one synchronous mutation batch, but the block is
4
+ * not recorded as an action. Consider using a proper model action when action
5
+ * recording is needed.
4
6
  *
5
7
  * @template T Return type.
6
8
  * @param name Mobx action name.
@@ -9,8 +11,10 @@
9
11
  */
10
12
  export declare function runUnprotected<T>(name: string, fn: () => T): T;
11
13
  /**
12
- * Runs a block in unprocted mode, as if it were run inside a model action.
13
- * Consider using a proper model action instead since these kind of actions are not recorded.
14
+ * Runs a block in unprotected mode, as if it were run inside a model action.
15
+ * Structural mutations share one synchronous mutation batch, but the block is
16
+ * not recorded as an action. Consider using a proper model action when action
17
+ * recording is needed.
14
18
  *
15
19
  * @template T Return type.
16
20
  * @param fn Action block.
@@ -76,6 +76,9 @@ export interface ModelOptions<TProps extends ModelProps, FS, TS> {
76
76
  /**
77
77
  * Optional transformation that will be run when converting from a snapshot to the data part of the model.
78
78
  * Useful for example to do versioning and keep the data part up to date with the latest version of the model.
79
+ * Snapshot processors must be pure and deterministic. The model's canonical output snapshot must round-trip
80
+ * through this processor to the same model data. Reconciliation may skip invoking it when the input already
81
+ * equals the model's current canonical output snapshot.
79
82
  *
80
83
  * @param sn The custom input snapshot.
81
84
  * @returns An input snapshot that must match the expected model input snapshot.
@@ -83,6 +86,8 @@ export interface ModelOptions<TProps extends ModelProps, FS, TS> {
83
86
  fromSnapshotProcessor?: (sn: FS) => FromSnapshotDefaultType<TProps>;
84
87
  /**
85
88
  * Optional transformation that will be run when converting the data part of the model into a snapshot.
89
+ * Snapshot processors must be pure and deterministic, and their canonical output must round-trip through the
90
+ * corresponding input processor to the same model data. Snapshot generation may cache processor results.
86
91
  *
87
92
  * @param sn The output snapshot.
88
93
  * @param modelInstance The model instance.
@@ -9,7 +9,7 @@ import { AnyFunction } from '../utils/AnyFunction';
9
9
  * @param name Unique name for the model type. Note that this name must be unique for your whole
10
10
  * application, so it is usually a good idea to use some prefix unique to your application domain.
11
11
  */
12
- export declare const model: (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, ...args: any[]) => MC;
12
+ export declare const model: (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, _context?: ClassDecoratorContext<MC>) => MC;
13
13
  /**
14
14
  * Marks a class (which MUST inherit from the `Model` abstract class)
15
15
  * as a model and decorates some of its methods/properties.
@@ -23,17 +23,18 @@ export interface ModelProp<TPropValue, TPropCreationValue, TTransformedValue, TT
23
23
  _defaultFn: (() => TPropValue) | typeof noDefaultValue;
24
24
  _defaultValue: TPropValue | typeof noDefaultValue;
25
25
  _defaultValueIsTransformed: boolean;
26
+ _getDefaultValueIsTransformed?: () => boolean;
26
27
  _typeChecker: AnyStandardType | undefined;
27
28
  _setter: SetterMode;
28
29
  _setterValueTransform: ((value: unknown) => unknown) | undefined;
29
30
  _isId: boolean;
30
31
  _idGenerator?: (() => string) | undefined;
31
- _transform: {
32
- transform: (original: unknown, model: object, propName: PropertyKey, setOriginalValue: (newOriginalValue: unknown) => void) => unknown;
33
- untransform: (transformed: unknown, model: object, propName: PropertyKey) => unknown;
34
- } | undefined;
32
+ _transform: FullModelPropTransform | undefined;
33
+ _getTransform?: () => FullModelPropTransform | undefined;
35
34
  _fromSnapshotProcessor?: (sn: unknown) => unknown;
36
35
  _toSnapshotProcessor?: (sn: unknown) => unknown;
36
+ _getFromSnapshotProcessor?: () => ((sn: unknown) => unknown) | undefined;
37
+ _getToSnapshotProcessor?: () => ((sn: unknown) => unknown) | undefined;
37
38
  /**
38
39
  * Adds a setter to the property. The setter will be named `set${CapitalizedPropName}`
39
40
  * and will be available in the model instance.
@@ -66,6 +67,9 @@ export interface ModelProp<TPropValue, TPropCreationValue, TTransformedValue, TT
66
67
  *
67
68
  * `fromSnapshot` runs before assigning snapshot data into the model prop.
68
69
  * `toSnapshot` runs when exporting model data back to snapshot form.
70
+ * Both processors must be pure and deterministic, and their canonical output
71
+ * must round-trip to the same stored value. Reconciliation and snapshot
72
+ * generation may skip calls when that canonical snapshot is already current.
69
73
  */
70
74
  withSnapshotProcessor<FS = TFromSnapshotOverride, TS = TToSnapshotOverride, This extends AnyModelProp = this>(processor: {
71
75
  fromSnapshot?: (sn: FS) => ModelPropFromSnapshot<This>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -4,3 +4,27 @@
4
4
  export declare class MobxKeystoneError extends Error {
5
5
  constructor(msg: string);
6
6
  }
7
+ /**
8
+ * MobX method action decorator compatible with both legacy decorators and
9
+ * MobX 7's standard decorator API.
10
+ *
11
+ * This is a decorator compatibility helper, not a replacement for other
12
+ * `action` overloads such as `action(fn)`. Field decorators are not supported.
13
+ *
14
+ * @hidden
15
+ */
16
+ export declare function mobxAction<This, Args extends unknown[], Return>(value: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>): (this: This, ...args: Args) => Return;
17
+ export declare function mobxAction<This, Args extends unknown[], Return>(target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Return>): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> | void;
18
+ /**
19
+ * MobX getter computed decorator compatible with both legacy decorators and
20
+ * MobX 7's standard decorator API.
21
+ *
22
+ * This is a decorator compatibility helper, not a replacement for other
23
+ * `computed` overloads. Field decorators and decorator options are not supported.
24
+ * Under MobX 7's legacy decorator transform the getter is backed by a computed
25
+ * value, but is not registered as a computed property for MobX introspection.
26
+ *
27
+ * @hidden
28
+ */
29
+ export declare function mobxComputed<This, Value>(value: (this: This) => Value, context: ClassGetterDecoratorContext<This, Value>): ((this: This) => Value) | void;
30
+ export declare function mobxComputed<Value>(target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<Value>): TypedPropertyDescriptor<Value> | void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "1.22.0",
3
+ "version": "1.24.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",
@@ -58,33 +58,37 @@
58
58
  "test": "vitest run",
59
59
  "test:ci": "vitest run --coverage",
60
60
  "test:perf": "pnpm run build && pnpm run test:perf:run",
61
- "test:perf:run": "cd perf_bench && NODE_ENV=production /usr/bin/time node --expose-gc --require ts-node/register ./report.ts",
62
- "memtest": "cd perf_bench && NODE_ENV=production node --expose-gc --require ts-node/register ./memtest.ts",
63
- "build-docs": "shx rm -rf api-docs && typedoc --options ./typedocconfig.js src/index.ts && shx rm -rf ../../apps/site/generated-static/api && shx mkdir -p ../../apps/site/generated-static/api && shx cp -R ./api-docs/* ../../apps/site/generated-static/api"
61
+ "test:perf:run": "cd perf_bench && NODE_ENV=production TS_NODE_COMPILER=@typescript/typescript6 /usr/bin/time node --expose-gc --require ts-node/register ./report.ts",
62
+ "memtest": "cd perf_bench && NODE_ENV=production TS_NODE_COMPILER=@typescript/typescript6 node --expose-gc --require ts-node/register ./memtest.ts",
63
+ "build-docs": "shx rm -rf api-docs && node --import ./scripts/typedoc-typescript6-register.mjs ./node_modules/typedoc/dist/cli.js --options ./typedocconfig.js src/index.ts && shx rm -rf ../../apps/site/generated-static/api && shx mkdir -p ../../apps/site/generated-static/api && shx cp -R ./api-docs/* ../../apps/site/generated-static/api"
64
64
  },
65
65
  "peerDependencies": {
66
- "mobx": "^6.0.0 || ^5.0.0 || ^4.0.0"
66
+ "mobx": "^7.0.0 || ^6.0.0 || ^5.0.0 || ^4.0.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@babel/core": "^8.0.1",
70
70
  "@babel/plugin-proposal-decorators": "^8.0.2",
71
+ "@babel/plugin-transform-class-static-block": "^8.0.1",
71
72
  "@babel/plugin-transform-class-properties": "^8.0.1",
72
73
  "@babel/plugin-transform-typescript": "^8.0.1",
73
74
  "@babel/preset-env": "^8.0.2",
74
75
  "@swc/core": "^1.15.43",
75
76
  "@types/babel__core": "^7.20.5",
76
- "@types/node": "^26.0.1",
77
- "@vitest/coverage-v8": "^4.1.9",
77
+ "@types/node": "^26.1.1",
78
+ "@typescript/typescript6": "^6.0.2",
79
+ "@vitest/coverage-v8": "^4.1.10",
78
80
  "mobx-v4": "npm:mobx@^4.15.7",
79
81
  "mobx-v5": "npm:mobx@^5.15.7",
82
+ "mobx-v6": "npm:mobx@^6.0.0",
83
+ "mobx-v7": "npm:mobx@^7.0.0",
80
84
  "shx": "^0.4.0",
81
85
  "spec.ts": "^1.1.3",
82
86
  "ts-node": "^10.9.2",
83
- "typedoc": "^0.28.19",
84
- "typescript": "^6.0.3",
85
- "vite": "^8.1.0",
87
+ "typedoc": "^0.28.20",
88
+ "typescript": "^7.0.2",
89
+ "vite": "^8.1.4",
86
90
  "vite-plugin-dts": "^5.0.3",
87
- "vitest": "^4.1.9"
91
+ "vitest": "^4.1.10"
88
92
  },
89
93
  "dependencies": {
90
94
  "fast-deep-equal": "^3.1.3",
@@ -1,5 +1,4 @@
1
1
  import { fastGetParent } from "../parent/path"
2
- import { isChildOfParent } from "../parent/path2"
3
2
  import { assertTweakedObject } from "../tweaker/core"
4
3
  import { assertIsFunction, assertIsObject, deleteFromArray, failure } from "../utils"
5
4
  import type { ActionContext } from "./context"
@@ -31,7 +30,7 @@ export interface ActionMiddleware {
31
30
  */
32
31
  export type ActionMiddlewareDisposer = () => void
33
32
 
34
- type PartialActionMiddleware = Pick<ActionMiddleware, "filter" | "middleware">
33
+ type PartialActionMiddleware = Pick<ActionMiddleware, "subtreeRoot" | "filter" | "middleware">
35
34
 
36
35
  const perObjectActionMiddlewares = new WeakMap<object, PartialActionMiddleware[]>()
37
36
 
@@ -96,7 +95,7 @@ export function addActionMiddleware(mware: ActionMiddleware): ActionMiddlewareDi
96
95
  assertIsObject(mware, "middleware")
97
96
 
98
97
  const { middleware, subtreeRoot } = mware
99
- let { filter } = mware
98
+ const { filter } = mware
100
99
 
101
100
  assertTweakedObject(subtreeRoot, "middleware.subtreeRoot")
102
101
  assertIsFunction(middleware, "middleware.middleware")
@@ -107,21 +106,10 @@ export function addActionMiddleware(mware: ActionMiddleware): ActionMiddlewareDi
107
106
  // reminder: never turn middlewares into actions or else
108
107
  // reactions will not be picked up by the undo manager
109
108
 
110
- if (subtreeRoot) {
111
- const targetFilter = (ctx: ActionContext) =>
112
- ctx.target === subtreeRoot || isChildOfParent(ctx.target, subtreeRoot)
113
-
114
- if (filter) {
115
- const customFilter = filter
116
- filter = (ctx) => {
117
- return targetFilter(ctx) && customFilter(ctx)
118
- }
119
- } else {
120
- filter = targetFilter
121
- }
122
- }
109
+ // Middleware collection establishes the initial subtree match. The runner
110
+ // revalidates it after user middleware or filter code may have changed parentage.
123
111
 
124
- const actualMware = { middleware, filter }
112
+ const actualMware = { middleware, subtreeRoot, filter }
125
113
 
126
114
  let objMwares = perObjectActionMiddlewares.get(subtreeRoot)
127
115
  if (objMwares) {
@@ -130,7 +118,6 @@ export function addActionMiddleware(mware: ActionMiddleware): ActionMiddlewareDi
130
118
  objMwares = [actualMware]
131
119
  perObjectActionMiddlewares.set(subtreeRoot, objMwares)
132
120
  }
133
-
134
121
  return () => {
135
122
  deleteFromArray(objMwares, actualMware)
136
123
  }
@@ -0,0 +1,72 @@
1
+ let mutationBatchActive = false
2
+ let mutationBatchFinishers: (() => void) | (() => void)[] | undefined
3
+
4
+ /**
5
+ * Starts a synchronous mutation batch if one is not already active.
6
+ *
7
+ * @returns `true` if this call owns the batch and must finish it.
8
+ * @internal
9
+ */
10
+ export function startMutationBatch(): boolean {
11
+ if (mutationBatchActive) {
12
+ return false
13
+ }
14
+
15
+ mutationBatchActive = true
16
+ return true
17
+ }
18
+
19
+ /**
20
+ * @internal
21
+ */
22
+ export function isMutationBatchActive(): boolean {
23
+ return mutationBatchActive
24
+ }
25
+
26
+ /**
27
+ * Registers a callback to run when the outermost synchronous mutation batch
28
+ * ends. The first callback is stored directly so the common single-finisher
29
+ * case does not allocate an array.
30
+ *
31
+ * @internal
32
+ */
33
+ export function addMutationBatchFinisher(finisher: () => void): void {
34
+ const currentFinishers = mutationBatchFinishers
35
+ if (!currentFinishers) {
36
+ mutationBatchFinishers = finisher
37
+ } else if (typeof currentFinishers === "function") {
38
+ mutationBatchFinishers = [currentFinishers, finisher]
39
+ } else {
40
+ currentFinishers.push(finisher)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Finishes a batch started by `startMutationBatch`. Every finisher runs even
46
+ * if an earlier one throws, so a finisher's cleanup of its own module state
47
+ * cannot be skipped by another consumer's exception.
48
+ *
49
+ * @internal
50
+ */
51
+ export function finishMutationBatch(): void {
52
+ mutationBatchActive = false
53
+
54
+ const finishers = mutationBatchFinishers
55
+ mutationBatchFinishers = undefined
56
+
57
+ if (typeof finishers === "function") {
58
+ finishers()
59
+ } else if (finishers) {
60
+ runFinishers(finishers, finishers.length - 1)
61
+ }
62
+ }
63
+
64
+ function runFinishers(finishers: (() => void)[], index: number): void {
65
+ try {
66
+ finishers[index]()
67
+ } finally {
68
+ if (index > 0) {
69
+ runFinishers(finishers, index - 1)
70
+ }
71
+ }
72
+ }
@@ -1,10 +1,13 @@
1
1
  import { action } from "mobx"
2
+ import { finishMutationBatch, startMutationBatch } from "./mutationBatch"
2
3
  import { tryRunPendingActions } from "./pendingActions"
3
4
  import { getActionProtection, setActionProtection } from "./protection"
4
5
 
5
6
  /**
6
- * Runs a block in unprocted mode, as if it were run inside a model action.
7
- * Consider using a proper model action instead since these kind of actions are not recorded.
7
+ * Runs a block in unprotected mode, as if it were run inside a model action.
8
+ * Structural mutations share one synchronous mutation batch, but the block is
9
+ * not recorded as an action. Consider using a proper model action when action
10
+ * recording is needed.
8
11
  *
9
12
  * @template T Return type.
10
13
  * @param name Mobx action name.
@@ -14,8 +17,10 @@ import { getActionProtection, setActionProtection } from "./protection"
14
17
  export function runUnprotected<T>(name: string, fn: () => T): T
15
18
 
16
19
  /**
17
- * Runs a block in unprocted mode, as if it were run inside a model action.
18
- * Consider using a proper model action instead since these kind of actions are not recorded.
20
+ * Runs a block in unprotected mode, as if it were run inside a model action.
21
+ * Structural mutations share one synchronous mutation batch, but the block is
22
+ * not recorded as an action. Consider using a proper model action when action
23
+ * recording is needed.
19
24
  *
20
25
  * @template T Return type.
21
26
  * @param fn Action block.
@@ -28,7 +33,9 @@ export function runUnprotected<T>(arg1: any, arg2?: any): T {
28
33
  const name = typeof arg1 === "string" ? arg1 : undefined
29
34
  const fn: () => T = typeof arg1 === "string" ? arg2 : arg1
30
35
 
36
+ let mutationBatchOwner = false
31
37
  const innerAction = () => {
38
+ mutationBatchOwner = startMutationBatch()
32
39
  const oldActionProtection = getActionProtection()
33
40
  setActionProtection(false)
34
41
 
@@ -41,9 +48,15 @@ export function runUnprotected<T>(arg1: any, arg2?: any): T {
41
48
  }
42
49
  }
43
50
 
44
- if (name) {
45
- return action(name, innerAction)()
46
- } else {
47
- return action(innerAction)()
51
+ try {
52
+ if (name) {
53
+ return action(name, innerAction)()
54
+ } else {
55
+ return action(innerAction)()
56
+ }
57
+ } finally {
58
+ if (mutationBatchOwner) {
59
+ finishMutationBatch()
60
+ }
48
61
  }
49
62
  }
@@ -2,6 +2,7 @@ import { action } from "mobx"
2
2
  import type { O } from "ts-toolbelt"
3
3
  import type { AnyDataModel } from "../dataModel/BaseDataModel"
4
4
  import type { AnyModel } from "../model/BaseModel"
5
+ import { fastGetParent } from "../parent/path"
5
6
  import type { AnyFunction } from "../utils/AnyFunction"
6
7
  import { copyFunctionMetadata } from "../utils/decorators"
7
8
  import {
@@ -13,8 +14,20 @@ import {
13
14
  import { isModelAction, modelActionSymbol } from "./isModelAction"
14
15
  import { getPerObjectActionMiddlewares } from "./middleware"
15
16
  import type { FlowFinisher } from "./modelFlow"
17
+ import { finishMutationBatch, startMutationBatch } from "./mutationBatch"
16
18
  import { tryRunPendingActions } from "./pendingActions"
17
19
 
20
+ function isActionTargetInSubtree(target: object, subtreeRoot: object): boolean {
21
+ let current: object | undefined = target
22
+ while (current) {
23
+ if (current === subtreeRoot) {
24
+ return true
25
+ }
26
+ current = fastGetParent(current, false)
27
+ }
28
+ return false
29
+ }
30
+
18
31
  /**
19
32
  * @internal
20
33
  */
@@ -77,29 +90,44 @@ export function wrapInAction<T extends Function>({
77
90
  setCurrentActionContext(context)
78
91
 
79
92
  const perObjectMiddlewares = getPerObjectActionMiddlewares(context.target)
80
-
81
93
  let objectIndex = perObjectMiddlewares.length - 1 // from topmost parent to the object itself
82
94
  let objectMwareIndex = 0
95
+ let scopeMayHaveChanged = false
83
96
 
84
97
  const runNextMiddleware = (): unknown => {
85
- const objectMwares = perObjectMiddlewares[objectIndex]
86
- if (!objectMwares) {
87
- // no more middlewares, run base action
88
- return fn.apply(this, args)
89
- }
90
- const mwareData = objectMwares[objectMwareIndex]
98
+ while (objectIndex >= 0) {
99
+ const objectMwares = perObjectMiddlewares[objectIndex]
100
+ const mwareData = objectMwares[objectMwareIndex]
101
+
102
+ // Advance before calling user code so next() continues with the following middleware.
103
+ objectMwareIndex++
104
+ if (objectMwareIndex >= objectMwares.length) {
105
+ objectMwareIndex = 0
106
+ objectIndex--
107
+ }
91
108
 
92
- // advance to the next middleware
93
- objectMwareIndex++
94
- if (objectMwareIndex >= objectMwares.length) {
95
- objectMwareIndex = 0
96
- objectIndex--
109
+ if (
110
+ scopeMayHaveChanged &&
111
+ !isActionTargetInSubtree(context.target, mwareData.subtreeRoot)
112
+ ) {
113
+ continue
114
+ }
115
+
116
+ if (mwareData.filter) {
117
+ scopeMayHaveChanged = true
118
+ if (!mwareData.filter(context)) {
119
+ continue
120
+ }
121
+ }
122
+
123
+ scopeMayHaveChanged = true
124
+ return mwareData.middleware(context, runNextMiddleware)
97
125
  }
98
126
 
99
- const filterPassed = mwareData.filter ? mwareData.filter(context) : true
100
- return filterPassed ? mwareData.middleware(context, runNextMiddleware) : runNextMiddleware()
127
+ return fn.apply(this, args)
101
128
  }
102
129
 
130
+ const mutationBatchOwner = startMutationBatch()
103
131
  try {
104
132
  const ret = runNextMiddleware()
105
133
 
@@ -117,7 +145,9 @@ export function wrapInAction<T extends Function>({
117
145
  }
118
146
  } finally {
119
147
  setCurrentActionContext(context.parentContext)
120
-
148
+ if (mutationBatchOwner) {
149
+ finishMutationBatch()
150
+ }
121
151
  tryRunPendingActions()
122
152
  }
123
153
  }
@@ -1,4 +1,3 @@
1
- import { action, computed } from "mobx"
2
1
  import type { ActionMiddlewareDisposer } from "../action/middleware"
3
2
  import { modelAction } from "../action/modelAction"
4
3
  import { Model } from "../model/Model"
@@ -10,7 +9,14 @@ import { assertTweakedObject } from "../tweaker/core"
10
9
  import { typesArray } from "../types/arrayBased/typesArray"
11
10
  import { tProp } from "../types/tProp"
12
11
  import { typesUnchecked } from "../types/utility/typesUnchecked"
13
- import { failure, getMobxVersion, mobx6, namespace } from "../utils"
12
+ import {
13
+ failure,
14
+ getMobxVersion,
15
+ makeObservableCompat,
16
+ mobxAction,
17
+ mobxComputed,
18
+ namespace,
19
+ } from "../utils"
14
20
  import { actionTrackingMiddleware, type SimpleActionContext } from "./actionTrackingMiddleware"
15
21
 
16
22
  /**
@@ -306,7 +312,7 @@ export class UndoManager {
306
312
  * The undo stack, where the first operation to undo will be the last of the array.
307
313
  * Do not manipulate this array directly.
308
314
  */
309
- @computed
315
+ @mobxComputed
310
316
  get undoQueue(): ReadonlyArray<UndoEvent> {
311
317
  return this.store.undoEvents
312
318
  }
@@ -315,7 +321,7 @@ export class UndoManager {
315
321
  * The redo stack, where the first operation to redo will be the last of the array.
316
322
  * Do not manipulate this array directly.
317
323
  */
318
- @computed
324
+ @mobxComputed
319
325
  get redoQueue(): ReadonlyArray<UndoEvent> {
320
326
  return this.store.redoEvents
321
327
  }
@@ -323,7 +329,7 @@ export class UndoManager {
323
329
  /**
324
330
  * The number of undo actions available.
325
331
  */
326
- @computed
332
+ @mobxComputed
327
333
  get undoLevels() {
328
334
  return this.undoQueue.length
329
335
  }
@@ -331,7 +337,7 @@ export class UndoManager {
331
337
  /**
332
338
  * If undo can be performed (if there is at least one undo action available).
333
339
  */
334
- @computed
340
+ @mobxComputed
335
341
  get canUndo() {
336
342
  return this.undoLevels > 0
337
343
  }
@@ -339,7 +345,7 @@ export class UndoManager {
339
345
  /**
340
346
  * Clears the undo queue.
341
347
  */
342
- @action
348
+ @mobxAction
343
349
  clearUndo() {
344
350
  this.store._clearUndo()
345
351
  }
@@ -347,7 +353,7 @@ export class UndoManager {
347
353
  /**
348
354
  * The number of redo actions available.
349
355
  */
350
- @computed
356
+ @mobxComputed
351
357
  get redoLevels() {
352
358
  return this.redoQueue.length
353
359
  }
@@ -355,7 +361,7 @@ export class UndoManager {
355
361
  /**
356
362
  * If redo can be performed (if there is at least one redo action available)
357
363
  */
358
- @computed
364
+ @mobxComputed
359
365
  get canRedo() {
360
366
  return this.redoLevels > 0
361
367
  }
@@ -363,7 +369,7 @@ export class UndoManager {
363
369
  /**
364
370
  * Clears the redo queue.
365
371
  */
366
- @action
372
+ @mobxAction
367
373
  clearRedo() {
368
374
  this.store._clearRedo()
369
375
  }
@@ -372,7 +378,7 @@ export class UndoManager {
372
378
  * Undoes the last action.
373
379
  * Will throw if there is no action to undo.
374
380
  */
375
- @action
381
+ @mobxAction
376
382
  undo() {
377
383
  if (!this.canUndo) {
378
384
  throw failure("nothing to undo")
@@ -397,7 +403,7 @@ export class UndoManager {
397
403
  * Redoes the previous action.
398
404
  * Will throw if there is no action to redo.
399
405
  */
400
- @action
406
+ @mobxAction
401
407
  redo() {
402
408
  if (!this.canRedo) {
403
409
  throw failure("nothing to redo")
@@ -619,7 +625,7 @@ export class UndoManager {
619
625
  this.options = options
620
626
 
621
627
  if (getMobxVersion() >= 6) {
622
- mobx6.makeObservable(this)
628
+ makeObservableCompat(this)
623
629
  }
624
630
 
625
631
  this.store = store ?? new UndoStore({})
@@ -32,7 +32,7 @@ const dataModelInstanceCache = new WeakMap<ModelClass<AnyDataModel>, WeakMap<any
32
32
  */
33
33
  export abstract class BaseDataModel<TProps extends ModelProps> {
34
34
  // just to make typing work properly
35
- [propsTypeSymbol]!: TProps
35
+ declare [propsTypeSymbol]: TProps
36
36
 
37
37
  /**
38
38
  * Called after the instance is created when there's the first call to `fn(M, data)`.
@@ -105,9 +105,10 @@ export abstract class BaseDataModel<TProps extends ModelProps> {
105
105
  let changed = false
106
106
 
107
107
  // apply untransform (if any)
108
- if (propData._transform) {
108
+ const transform = propData._getTransform ? propData._getTransform() : propData._transform
109
+ if (transform) {
109
110
  changed = true
110
- newValue = propData._transform.untransform(newValue, this, k)
111
+ newValue = transform.untransform(newValue, this, k)
111
112
  }
112
113
 
113
114
  // apply default value (if needed)
@@ -143,11 +144,6 @@ export abstract class BaseDataModel<TProps extends ModelProps> {
143
144
 
144
145
  Object.setPrototypeOf(this, modelClass.prototype)
145
146
 
146
- const self = this as any
147
-
148
- // delete unnecessary props
149
- delete self[propsTypeSymbol]
150
-
151
147
  internalNewDataModel(this, tweakedData as any, {
152
148
  modelClass,
153
149
  })