mobx-keystone 1.24.1 → 2.0.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 (122) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +5 -1
  3. package/dist/mobx-keystone.esm.js +1608 -1072
  4. package/dist/mobx-keystone.esm.mjs +1608 -1072
  5. package/dist/mobx-keystone.umd.js +1613 -1072
  6. package/dist/types/actionMiddlewares/actionSerialization/dateSerializer.d.ts +1 -1
  7. package/dist/types/dataModel/dataModelInstanceCache.d.ts +1 -0
  8. package/dist/types/deepChange/onDeepChange.d.ts +9 -0
  9. package/dist/types/parent/detach.d.ts +1 -1
  10. package/dist/types/parent/findChildren.d.ts +1 -1
  11. package/dist/types/patch/applyPatches.d.ts +2 -0
  12. package/dist/types/redux/connectReduxDevTools.d.ts +2 -1
  13. package/dist/types/snapshot/SnapshotOf.d.ts +2 -2
  14. package/dist/types/standardActions/arrayActions.d.ts +1 -1
  15. package/dist/types/treeUtils/sandbox.d.ts +0 -4
  16. package/dist/types/types/TypeCheckError.d.ts +1 -0
  17. package/dist/types/types/utility/typesRefinement.d.ts +2 -2
  18. package/dist/types/utils/forEachWithDelayedThrow.d.ts +1 -0
  19. package/dist/types/utils/index.d.ts +13 -0
  20. package/dist/types/utils/mapUtils.d.ts +2 -2
  21. package/dist/types/utils/setLike.d.ts +1 -0
  22. package/package.json +7 -7
  23. package/src/action/applyAction.ts +1 -1
  24. package/src/action/middleware.ts +17 -36
  25. package/src/action/modelFlow.ts +4 -5
  26. package/src/action/mutationBatch.ts +10 -10
  27. package/src/action/pendingActions.ts +3 -4
  28. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +19 -14
  29. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +29 -20
  30. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +1 -1
  31. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +4 -3
  32. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +4 -12
  33. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +4 -13
  34. package/src/actionMiddlewares/actionTrackingMiddleware.ts +11 -13
  35. package/src/actionMiddlewares/transactionMiddleware.ts +42 -11
  36. package/src/actionMiddlewares/undoMiddleware.ts +76 -60
  37. package/src/actionMiddlewares/utils.ts +2 -1
  38. package/src/computedTree/computedTree.ts +14 -3
  39. package/src/context/context.ts +17 -45
  40. package/src/dataModel/BaseDataModel.ts +20 -16
  41. package/src/dataModel/dataModelInstanceCache.ts +8 -0
  42. package/src/deepChange/onDeepChange.ts +179 -24
  43. package/src/frozen/Frozen.ts +50 -33
  44. package/src/model/BaseModel.ts +15 -14
  45. package/src/model/newModel.ts +32 -28
  46. package/src/model/utils.ts +2 -2
  47. package/src/modelShared/modelClassInitializer.ts +3 -2
  48. package/src/modelShared/modelDecorator.ts +34 -14
  49. package/src/modelShared/modelInfo.ts +1 -1
  50. package/src/modelShared/prop.ts +6 -9
  51. package/src/modelShared/sharedInternalModel.ts +16 -3
  52. package/src/parent/detach.ts +1 -1
  53. package/src/parent/findChildren.ts +4 -7
  54. package/src/parent/findParent.ts +14 -9
  55. package/src/parent/onChildAttachedTo.ts +45 -40
  56. package/src/parent/path.ts +11 -7
  57. package/src/parent/setParent.ts +33 -14
  58. package/src/parent/walkTree.ts +21 -37
  59. package/src/patch/applyPatches.ts +75 -60
  60. package/src/patch/emitPatch.ts +142 -26
  61. package/src/patch/jsonPatch.ts +5 -0
  62. package/src/patch/patchRecorder.ts +18 -14
  63. package/src/redux/connectReduxDevTools.ts +24 -18
  64. package/src/redux/redux.ts +5 -21
  65. package/src/ref/core.ts +18 -2
  66. package/src/rootStore/attachDetach.ts +27 -16
  67. package/src/rootStore/rootStore.ts +10 -6
  68. package/src/snapshot/SnapshotOf.ts +2 -2
  69. package/src/snapshot/applySnapshot.ts +10 -1
  70. package/src/snapshot/clone.ts +1 -2
  71. package/src/snapshot/cloneTreeValue.ts +1 -2
  72. package/src/snapshot/fromModelSnapshot.ts +6 -2
  73. package/src/snapshot/fromSnapshot.ts +3 -1
  74. package/src/snapshot/internal.ts +22 -9
  75. package/src/snapshot/modelInitialData.ts +14 -19
  76. package/src/snapshot/onSnapshot.ts +12 -1
  77. package/src/snapshot/reconcileModelSnapshot.ts +16 -8
  78. package/src/snapshot/reconcilePlainObjectSnapshot.ts +5 -4
  79. package/src/snapshot/reconcileSnapshot.ts +13 -4
  80. package/src/standardActions/arrayActions.ts +12 -2
  81. package/src/standardActions/objectActions.ts +3 -2
  82. package/src/transforms/ImmutableDate.ts +4 -0
  83. package/src/transforms/asMap.ts +3 -1
  84. package/src/treeUtils/deepEquals.ts +15 -13
  85. package/src/treeUtils/draft.ts +8 -0
  86. package/src/treeUtils/sandbox.ts +31 -27
  87. package/src/tweaker/core.ts +2 -4
  88. package/src/tweaker/tweak.ts +8 -8
  89. package/src/tweaker/tweakArray.ts +86 -37
  90. package/src/tweaker/tweakFrozen.ts +0 -1
  91. package/src/tweaker/tweakModel.ts +0 -1
  92. package/src/tweaker/tweakPlainObject.ts +78 -29
  93. package/src/tweaker/typeChecking.ts +132 -33
  94. package/src/types/TypeCheckError.ts +17 -19
  95. package/src/types/createCachedTypeCheck.ts +27 -2
  96. package/src/types/objectBased/typesDataModelData.ts +4 -2
  97. package/src/types/objectBased/typesModel.ts +4 -2
  98. package/src/types/objectBased/typesObject.ts +20 -16
  99. package/src/types/objectBased/typesRecord.ts +6 -13
  100. package/src/types/objectBased/typesRef.ts +2 -2
  101. package/src/types/primitiveBased/typesEnum.ts +4 -2
  102. package/src/types/primitiveBased/typesPrimitive.ts +9 -4
  103. package/src/types/typeCheckErrorUtils.ts +1 -0
  104. package/src/types/utility/typesCodecSupport.ts +97 -56
  105. package/src/types/utility/typesMap.ts +24 -41
  106. package/src/types/utility/typesRefinement.ts +3 -2
  107. package/src/types/utility/typesSet.ts +24 -0
  108. package/src/utils/ModelPool.ts +3 -3
  109. package/src/utils/chainFns.ts +4 -0
  110. package/src/utils/decorators.ts +1 -1
  111. package/src/utils/errorDiagnostics.ts +6 -2
  112. package/src/utils/forEachWithDelayedThrow.ts +83 -0
  113. package/src/utils/index.ts +34 -2
  114. package/src/utils/mapUtils.ts +3 -7
  115. package/src/utils/setIfDifferent.ts +3 -2
  116. package/src/utils/setLike.ts +134 -0
  117. package/src/wrappers/ArraySet.ts +5 -9
  118. package/src/wrappers/ObjectMap.ts +15 -11
  119. package/src/wrappers/asMap.ts +30 -18
  120. package/src/wrappers/asSet.ts +36 -12
  121. package/dist/types/action/modelFlowPromiseGenerator.d.ts +0 -1
  122. package/src/action/modelFlowPromiseGenerator.ts +0 -127
@@ -1,2 +1,2 @@
1
1
  import { ActionCallArgumentSerializer } from './core';
2
- export declare const dateSerializer: ActionCallArgumentSerializer<Date, number>;
2
+ export declare const dateSerializer: ActionCallArgumentSerializer<Date, number | null>;
@@ -0,0 +1 @@
1
+ export {};
@@ -11,6 +11,8 @@ export declare enum DeepChangeType {
11
11
  ObjectRemove = "objectRemove"
12
12
  }
13
13
  export interface DeepChangeBase {
14
+ /** Whether this notification overlaps a mutation made by a change listener. */
15
+ readonly isReentrant?: boolean;
14
16
  readonly type: DeepChangeType;
15
17
  readonly path: Path;
16
18
  /**
@@ -78,6 +80,13 @@ export type OnDeepChangeListener = (change: DeepChange) => void;
78
80
  /**
79
81
  * Adds a listener that will be called every time a deep change is generated for the tree of the given target object.
80
82
  * Unlike `onPatches`, this provides raw MobX change information including proper splice detection for arrays.
83
+ * Individual mutations notify listeners after automatic type checking succeeds and snapshots update;
84
+ * rejected mutations and their quiet rollbacks do not notify listeners.
85
+ * Listener errors do not stop delivery to other listeners. After delivery completes,
86
+ * a single error is rethrown unchanged and multiple errors are grouped in a MobxKeystoneAggregateError.
87
+ * Listeners added while callbacks for the same target are running start with the next
88
+ * change delivered to that target. Listeners disposed during delivery do not receive
89
+ * the current change if their callback has not run yet.
81
90
  *
82
91
  * @param subtreeRoot Subtree root object of the deep change listener.
83
92
  * @param listener The listener function that will be called every time a change is generated for the object or its children.
@@ -2,7 +2,7 @@
2
2
  * Detaches a given object from a tree.
3
3
  * If the parent is an object / model, detaching will delete the property.
4
4
  * If the parent is an array detaching will remove the node by splicing it.
5
- * If there's no parent it will throw.
5
+ * If there's no parent it does nothing.
6
6
  *
7
7
  * @param node Object to be detached.
8
8
  */
@@ -6,7 +6,7 @@
6
6
  * @param predicate Function that will be run for every child of the root object.
7
7
  * @param [options] An optional object with the `deep` option (defaults to `false`) set to `true` to
8
8
  * get the children deeply or `false` to get them shallowly.
9
- * @returns A readonly observable set with the matching children.
9
+ * @returns A readonly set with the matching children.
10
10
  */
11
11
  export declare function findChildren<T extends object = any>(root: object, predicate: (node: object) => boolean, options?: {
12
12
  deep?: boolean;
@@ -1,6 +1,8 @@
1
1
  import { Patch } from '../patch/Patch';
2
2
  /**
3
3
  * Applies the given patches to the given target object.
4
+ * Automatic type checking validates the completed batch, including nested lists.
5
+ * A rejected batch is rolled back with compensating patches.
4
6
  *
5
7
  * @param node Target object.
6
8
  * @param patches List of patches to apply.
@@ -20,5 +20,6 @@ export interface ConnectReduxDevToolsOptions {
20
20
  * @param remotedevConnection The result of a connect method from the remotedev package (usually the result of `remoteDev.connectViaExtension(...)`).
21
21
  * @param target Object to use as root.
22
22
  * @param [options] Optional options object.
23
+ * @returns An idempotent disposer that stops logging and unsubscribes from monitor messages.
23
24
  */
24
- export declare function connectReduxDevTools(remotedevPackage: any, remotedevConnection: any, target: object, options?: ConnectReduxDevToolsOptions): void;
25
+ export declare function connectReduxDevTools(remotedevPackage: any, remotedevConnection: any, target: object, options?: ConnectReduxDevToolsOptions): () => void;
@@ -34,13 +34,13 @@ export interface SnapshotInOfFrozen<F extends Frozen<any>> extends FrozenData<F[
34
34
  }
35
35
  export interface SnapshotInOfObjectMap<V> {
36
36
  items?: {
37
- [k: string]: SnapshotOutOf<V>;
37
+ [k: string]: SnapshotInOf<V>;
38
38
  };
39
39
  [modelTypeKey]?: string;
40
40
  [modelIdKey]: string;
41
41
  }
42
42
  export interface SnapshotInOfArraySet<V> {
43
- items?: SnapshotOutOf<V>[];
43
+ items?: SnapshotInOf<V>[];
44
44
  [modelTypeKey]?: string;
45
45
  [modelIdKey]: string;
46
46
  }
@@ -2,7 +2,7 @@ export declare const arrayActions: {
2
2
  set: <T>(array: T[], index: number, value: any) => void;
3
3
  delete: <T>(array: T[], index: number) => boolean;
4
4
  setLength: <T>(array: T[], length: number) => void;
5
- concat: <T>(array: T[], ...items: ConcatArray<T>[]) => T[];
5
+ concat: <T>(array: T[], ...items: (T | ConcatArray<T>)[]) => T[];
6
6
  copyWithin: <T>(array: T[], target: number, start: number, end?: number) => T[];
7
7
  fill: <T>(array: T[], value: T, start?: number, end?: number) => T[];
8
8
  pop: <T>(array: T[]) => T | undefined;
@@ -42,10 +42,6 @@ export declare class SandboxManager {
42
42
  * code block on a readonly node.
43
43
  */
44
44
  private allowWrite;
45
- /**
46
- * Whether changes made in the sandbox are currently being committed to the original subtree.
47
- */
48
- private isCommitting;
49
45
  private readonly subtreeRoot;
50
46
  /**
51
47
  * Creates an instance of `SandboxManager`.
@@ -1,6 +1,7 @@
1
1
  import { Path } from '../parent/pathTypes';
2
2
  import { MobxKeystoneError } from '../utils';
3
3
  export interface TypeCheckErrorData {
4
+ /** Path relative to the value being type checked. */
4
5
  path: Path;
5
6
  expectedTypeName: string;
6
7
  actualValue: any;
@@ -32,8 +32,8 @@ import { TypeInfo } from '../TypeChecker';
32
32
  * @template T Base type.
33
33
  * @param baseType Base type.
34
34
  * @param checkFn Function that will receive the data (if it passes the base type
35
- * check) and return null or false if there were no errors or either a TypeCheckError instance or
36
- * true if there were.
35
+ * check) and return null or true if there were no errors or either a TypeCheckError instance or
36
+ * false if there were.
37
37
  * @returns
38
38
  */
39
39
  export declare function typesRefinement<T extends AnyType>(baseType: T, checkFn: (data: TypeToData<T>) => TypeCheckError | null | boolean, typeName?: string): T;
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,19 @@
4
4
  export declare class MobxKeystoneError extends Error {
5
5
  constructor(msg: string);
6
6
  }
7
+ /**
8
+ * A mobx-keystone error that groups several failures that happened while delivering
9
+ * a single notification, such as multiple patch listeners or root store hooks throwing.
10
+ *
11
+ * It is only used when more than one callback failed; a lone failure is rethrown as is.
12
+ */
13
+ export declare class MobxKeystoneAggregateError extends MobxKeystoneError {
14
+ /**
15
+ * The values thrown by the failing callbacks, in the order they were thrown.
16
+ */
17
+ readonly errors: readonly unknown[];
18
+ constructor(errors: readonly unknown[], msg: string);
19
+ }
7
20
  /**
8
21
  * MobX method action decorator compatible with both legacy decorators and
9
22
  * MobX 7's standard decorator API.
@@ -1,2 +1,2 @@
1
- export declare function getOrCreate<K, V, C = V>(map: Map<K, V>, key: K, create: () => C): V;
2
- export declare function getOrCreate<K extends object, V, C = V>(map: WeakMap<K, V>, key: K, create: () => C): V;
1
+ export declare function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V;
2
+ export declare function getOrCreate<K extends object, V>(map: WeakMap<K, V>, key: K, create: () => V): V;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "1.24.1",
3
+ "version": "2.0.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",
@@ -73,11 +73,11 @@
73
73
  "@babel/plugin-transform-class-static-block": "^8.0.1",
74
74
  "@babel/plugin-transform-typescript": "^8.0.1",
75
75
  "@babel/preset-env": "^8.0.2",
76
- "@swc/core": "^1.16.0",
76
+ "@swc/core": "^1.16.2",
77
77
  "@types/babel__core": "^7.20.5",
78
- "@types/node": "^26.2.0",
78
+ "@types/node": "^26.4.1",
79
79
  "@typescript/typescript6": "^6.0.2",
80
- "@vitest/coverage-v8": "^4.1.10",
80
+ "@vitest/coverage-v8": "^5.0.0",
81
81
  "mobx-v4": "npm:mobx@^4.15.7",
82
82
  "mobx-v5": "npm:mobx@^5.15.7",
83
83
  "mobx-v6": "npm:mobx@^6.0.0",
@@ -87,9 +87,9 @@
87
87
  "ts-node": "^10.9.2",
88
88
  "typedoc": "^0.28.20",
89
89
  "typescript": "^7.0.2",
90
- "vite": "^8.2.1",
91
- "vite-plugin-dts": "^5.0.3",
92
- "vitest": "^4.1.10"
90
+ "vite": "^8.2.2",
91
+ "vite-plugin-dts": "^5.1.0",
92
+ "vitest": "^5.0.0"
93
93
  },
94
94
  "dependencies": {
95
95
  "fast-deep-equal": "^3.1.3",
@@ -86,7 +86,7 @@ export function applyAction<TRet = any>(subtreeRoot: object, call: ActionCall):
86
86
  )} could not be resolved`
87
87
  )
88
88
  }
89
- assertTweakedObject(current, `resolved ${current}`, true)
89
+ assertTweakedObject(current, "resolved target", true)
90
90
 
91
91
  if (isBuiltInAction(call.actionName)) {
92
92
  const fnToCall = builtInActionToFunction[call.actionName] as AnyFunction | undefined
@@ -1,6 +1,6 @@
1
1
  import { fastGetParent } from "../parent/path"
2
2
  import { assertTweakedObject } from "../tweaker/core"
3
- import { assertIsFunction, assertIsObject, deleteFromArray, failure } from "../utils"
3
+ import { assertIsFunction, assertIsObject, failure } from "../utils"
4
4
  import type { ActionContext } from "./context"
5
5
 
6
6
  /**
@@ -37,33 +37,9 @@ const perObjectActionMiddlewares = new WeakMap<object, PartialActionMiddleware[]
37
37
  /**
38
38
  * @internal
39
39
  *
40
- * Runs a callback for each action middlewares to be run over a given object.
41
- *
42
- * @returns
43
- */
44
- export function forEachActionMiddleware(
45
- obj: object,
46
- callback: (middleware: PartialActionMiddleware) => void
47
- ): void {
48
- // when we call a middleware we will call the middlewares of that object plus all parent objects
49
- // the parent object middlewares are run last
50
-
51
- // since an array like [a, b, c] will be called like c(b(a())) this means that we need to call
52
- // the parent object ones at the end of the array
53
-
54
- let current: unknown = obj
55
- while (current) {
56
- const objMwares = perObjectActionMiddlewares.get(current)
57
- if (objMwares && objMwares.length > 0) {
58
- objMwares.forEach(callback)
59
- }
60
- current = fastGetParent(current, false)
61
- }
62
- }
63
- /**
64
- * @internal
65
- *
66
- * Returns the action middlewares to be run over a given object.
40
+ * Returns the action middlewares to be run over a given object, from the object itself to the
41
+ * topmost parent. Since an array like [a, b, c] will be called like c(b(a())) the caller runs
42
+ * them in reverse, so the parent object ones run last.
67
43
  *
68
44
  * @returns
69
45
  */
@@ -111,14 +87,19 @@ export function addActionMiddleware(mware: ActionMiddleware): ActionMiddlewareDi
111
87
 
112
88
  const actualMware = { middleware, subtreeRoot, filter }
113
89
 
114
- let objMwares = perObjectActionMiddlewares.get(subtreeRoot)
115
- if (objMwares) {
116
- objMwares.push(actualMware)
117
- } else {
118
- objMwares = [actualMware]
119
- perObjectActionMiddlewares.set(subtreeRoot, objMwares)
120
- }
90
+ // Each action keeps the middleware arrays it collected. Update registrations by
91
+ // replacement so user code cannot shift or extend an in-progress chain.
92
+ const objMwares = perObjectActionMiddlewares.get(subtreeRoot) ?? []
93
+ perObjectActionMiddlewares.set(subtreeRoot, [...objMwares, actualMware])
94
+
95
+ let disposed = false
121
96
  return () => {
122
- deleteFromArray(objMwares, actualMware)
97
+ if (disposed) return
98
+ disposed = true
99
+ const current = perObjectActionMiddlewares.get(subtreeRoot)!
100
+ perObjectActionMiddlewares.set(
101
+ subtreeRoot,
102
+ current.filter((entry) => entry !== actualMware)
103
+ )
123
104
  }
124
105
  }
@@ -2,7 +2,6 @@ import type { O } from "ts-toolbelt"
2
2
  import { failure } from "../utils"
3
3
  import { copyFunctionMetadata, decorateWrapMethodOrField } from "../utils/decorators"
4
4
  import { type ActionContext, ActionContextActionType, ActionContextAsyncStepType } from "./context"
5
- import { promiseGenerator } from "./modelFlowPromiseGenerator"
6
5
  import { type WrapInActionOverrideContextFn, wrapInAction } from "./wrapInAction"
7
6
 
8
7
  const modelFlowSymbol = Symbol("modelFlow")
@@ -133,7 +132,7 @@ export function flow<R, Args extends any[]>({
133
132
  function next(ret: any): void {
134
133
  if (ret && typeof ret.then === "function") {
135
134
  // an async iterator
136
- ret.then(next, reject)
135
+ Promise.resolve(ret).then(next, reject).catch(reject)
137
136
  } else if (ret.done) {
138
137
  // done
139
138
  wrapInAction({
@@ -154,7 +153,7 @@ export function flow<R, Args extends any[]>({
154
153
  }).call(target, ret.value)
155
154
  } else {
156
155
  // continue
157
- Promise.resolve(ret.value).then(onFulfilled, onRejected)
156
+ Promise.resolve(ret.value).then(onFulfilled, onRejected).catch(reject)
158
157
  }
159
158
  }
160
159
 
@@ -235,6 +234,6 @@ export function _async<A extends any[], R>(
235
234
  * @param promise Promise.
236
235
  * @returns
237
236
  */
238
- export function _await<T>(promise: Promise<T>): Generator<Promise<T>, T, unknown> {
239
- return promiseGenerator.call(promise)
237
+ export function* _await<T>(promise: Promise<T>): Generator<Promise<T>, T, unknown> {
238
+ return (yield promise) as T
240
239
  }
@@ -57,16 +57,16 @@ export function finishMutationBatch(): void {
57
57
  if (typeof finishers === "function") {
58
58
  finishers()
59
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)
60
+ let lastError: { value: unknown } | undefined
61
+ for (let i = finishers.length - 1; i >= 0; i--) {
62
+ try {
63
+ finishers[i]()
64
+ } catch (value) {
65
+ lastError = { value }
66
+ }
67
+ }
68
+ if (lastError) {
69
+ throw lastError.value
70
70
  }
71
71
  }
72
72
  }
@@ -1,3 +1,4 @@
1
+ import { forEachWithDelayedThrow } from "../utils/forEachWithDelayedThrow"
1
2
  import { getCurrentActionContext } from "./context"
2
3
  import { getActionProtection } from "./protection"
3
4
 
@@ -32,11 +33,9 @@ export function tryRunPendingActions(): void {
32
33
  pendingActionsRunning = true
33
34
 
34
35
  try {
35
- while (pendingActions.length > 0) {
36
- const nextAction = pendingActions.shift()!
37
- nextAction()
38
- }
36
+ forEachWithDelayedThrow(pendingActions, (nextAction) => nextAction())
39
37
  } finally {
38
+ pendingActions.length = 0
40
39
  pendingActionsRunning = false
41
40
  }
42
41
  }
@@ -2,6 +2,7 @@ import type { O } from "ts-toolbelt"
2
2
  import type { ActionCall } from "../../action/applyAction"
3
3
  import { assertTweakedObject } from "../../tweaker/core"
4
4
  import { failure, isJSONPrimitive, isPlainObject } from "../../utils"
5
+ import { getSafeErrorValuePreview } from "../../utils/errorDiagnostics"
5
6
  import type { JSONPrimitiveValue } from "../../utils/types"
6
7
  import { arraySerializer } from "./arraySerializer"
7
8
  import { type ActionCallArgumentSerializer, cannotSerialize } from "./core"
@@ -13,7 +14,7 @@ import { plainObjectSerializer } from "./plainObjectSerializer"
13
14
  import { primitiveSerializer } from "./primitiveSerializer"
14
15
  import { setSerializer } from "./setSerializer"
15
16
 
16
- const serializersArray: ActionCallArgumentSerializer<any, any>[] = []
17
+ let serializersArray: ActionCallArgumentSerializer<any, any>[] = []
17
18
  const serializersMap = new Map<string, ActionCallArgumentSerializer<any, any>>()
18
19
 
19
20
  /**
@@ -32,19 +33,22 @@ export function registerActionCallArgumentSerializer(
32
33
  if (serializersArray.includes(serializer)) {
33
34
  throw failure("action call argument serializer already registered")
34
35
  }
35
- if (serializersMap.has(serializer.id)) {
36
+ const id = serializer.id
37
+ if (serializersMap.has(id)) {
36
38
  throw failure(`action call argument serializer with id '${serializer.id}' already registered`)
37
39
  }
38
40
 
39
- serializersArray.unshift(serializer)
40
- serializersMap.set(serializer.id, serializer)
41
+ serializersArray = [serializer, ...serializersArray]
42
+ serializersMap.set(id, serializer)
41
43
 
44
+ let disposed = false
42
45
  return () => {
43
- const index = serializersArray.indexOf(serializer)
44
- if (index >= 0) {
45
- serializersArray.splice(index, 1)
46
+ if (disposed) {
47
+ return
46
48
  }
47
- serializersMap.delete(serializer.id)
49
+ disposed = true
50
+ serializersArray = serializersArray.filter((entry) => entry !== serializer)
51
+ serializersMap.delete(id)
48
52
  }
49
53
  }
50
54
 
@@ -106,13 +110,12 @@ export function serializeActionCallArgument(
106
110
  return argValue
107
111
  }
108
112
 
109
- const origValue = argValue
110
-
111
113
  const serialize = (v: any) => serializeActionCallArgument(v, targetRoot)
112
114
 
113
115
  // try serializers
114
- for (let i = 0; i < serializersArray.length; i++) {
115
- const serializer = serializersArray[i]
116
+ const serializers = serializersArray
117
+ for (let i = 0; i < serializers.length; i++) {
118
+ const serializer = serializers[i]
116
119
  const serializedValue = serializer.serialize(argValue, serialize, targetRoot)
117
120
  if (serializedValue !== cannotSerialize) {
118
121
  return {
@@ -122,7 +125,9 @@ export function serializeActionCallArgument(
122
125
  }
123
126
  }
124
127
 
125
- throw failure(`serializeActionCallArgument could not serialize the given value: ${origValue}`)
128
+ throw failure(
129
+ `serializeActionCallArgument could not serialize the given value: ${getSafeErrorValuePreview(argValue)}`
130
+ )
126
131
  }
127
132
 
128
133
  /**
@@ -150,7 +155,7 @@ export function serializeActionCall(
150
155
  return {
151
156
  ...actionCall,
152
157
  serialized: true,
153
- args: actionCall.args.map(serialize),
158
+ args: Array.from(actionCall.args, serialize),
154
159
  }
155
160
  }
156
161
 
@@ -1,9 +1,10 @@
1
1
  import { runInAction } from "mobx"
2
2
  import { applyAction } from "../../action/applyAction"
3
3
  import { frozenKey } from "../../frozen/Frozen"
4
+ import type { AnyModel } from "../../model/BaseModel"
4
5
  import { getModelIdPropertyName } from "../../model/getModelMetadata"
5
6
  import { isModel } from "../../model/utils"
6
- import { resolvePath } from "../../parent/path"
7
+ import { fastGetParentToChildPath, resolvePath } from "../../parent/path"
7
8
  import type { WritablePath } from "../../parent/pathTypes"
8
9
  import { applyPatches } from "../../patch/applyPatches"
9
10
  import { onPatches } from "../../patch/emitPatch"
@@ -53,16 +54,28 @@ export function applySerializedActionAndTrackNewModelIds<TRet = any>(
53
54
 
54
55
  const deserializedCall = deserializeActionCall(call, subtreeRoot)
55
56
 
56
- const modelIdOverrides: Patch[] = []
57
+ const modelsWithChangedIds = new Map<AnyModel, string>()
57
58
 
58
59
  // set a patch listener to track changes to model ids
59
60
  const patchDisposer = onPatches(subtreeRoot, (patches) => {
60
- scanPatchesForModelIdChanges(subtreeRoot, modelIdOverrides, patches)
61
+ scanPatchesForModelIdChanges(subtreeRoot, modelsWithChangedIds, patches)
61
62
  })
62
63
 
63
64
  try {
64
65
  const returnValue = applyAction(subtreeRoot, deserializedCall)
65
66
 
67
+ const modelIdOverrides: Patch[] = []
68
+ for (const [model, idProperty] of modelsWithChangedIds) {
69
+ const path = fastGetParentToChildPath(subtreeRoot, model, false)
70
+ if (path) {
71
+ modelIdOverrides.push({
72
+ op: "replace",
73
+ path: [...path, idProperty],
74
+ value: model.$[idProperty],
75
+ })
76
+ }
77
+ }
78
+
66
79
  return {
67
80
  returnValue,
68
81
  serializedActionCall: {
@@ -75,24 +88,23 @@ export function applySerializedActionAndTrackNewModelIds<TRet = any>(
75
88
  }
76
89
  }
77
90
 
78
- function scanPatchesForModelIdChanges(root: object, modelIdOverrides: Patch[], patches: Patch[]) {
91
+ function scanPatchesForModelIdChanges(
92
+ root: object,
93
+ modelsWithChangedIds: Map<AnyModel, string>,
94
+ patches: Patch[]
95
+ ) {
79
96
  const len = patches.length
80
97
  for (let i = 0; i < len; i++) {
81
98
  const patch = patches[i]
82
99
  if (patch.op === "replace" || patch.op === "add") {
83
- deepScanValueForModelIdChanges(
84
- root,
85
- modelIdOverrides,
86
- patch.value,
87
- patch.path as WritablePath
88
- )
100
+ deepScanValueForModelIdChanges(root, modelsWithChangedIds, patch.value, patch.path.slice())
89
101
  }
90
102
  }
91
103
  }
92
104
 
93
105
  function deepScanValueForModelIdChanges(
94
106
  root: object,
95
- modelIdOverrides: Patch[],
107
+ modelsWithChangedIds: Map<AnyModel, string>,
96
108
  value: any,
97
109
  path: WritablePath
98
110
  ) {
@@ -102,20 +114,17 @@ function deepScanValueForModelIdChanges(
102
114
 
103
115
  if (isModel(parent)) {
104
116
  const propertyName = path[path.length - 1]
105
- if (propertyName === getModelIdPropertyName(parent.constructor as any)) {
106
- // found one
107
- modelIdOverrides.push({
108
- op: "replace",
109
- path: path.slice(),
110
- value: value,
111
- })
117
+ const idProperty = getModelIdPropertyName(parent.constructor as any)
118
+ if (idProperty !== undefined && propertyName === idProperty) {
119
+ // Resolve the final path and ID after the action, since this model may move or detach.
120
+ modelsWithChangedIds.set(parent, idProperty)
112
121
  }
113
122
  }
114
123
  } else if (Array.isArray(value)) {
115
124
  const len = value.length
116
125
  for (let i = 0; i < len; i++) {
117
126
  path.push(i)
118
- deepScanValueForModelIdChanges(root, modelIdOverrides, value[i], path)
127
+ deepScanValueForModelIdChanges(root, modelsWithChangedIds, value[i], path)
119
128
  path.pop()
120
129
  }
121
130
  } else if (isObject(value)) {
@@ -128,7 +137,7 @@ function deepScanValueForModelIdChanges(
128
137
  const propValue = value[propName]
129
138
 
130
139
  path.push(propName)
131
- deepScanValueForModelIdChanges(root, modelIdOverrides, propValue, path)
140
+ deepScanValueForModelIdChanges(root, modelsWithChangedIds, propValue, path)
132
141
  path.pop()
133
142
  }
134
143
  }
@@ -11,7 +11,7 @@ export const arraySerializer: ActionCallArgumentSerializer<any[] | IObservableAr
11
11
  }
12
12
 
13
13
  // this will also transform observable arrays into non-observable ones
14
- return value.map(serialize)
14
+ return Array.from(value, serialize)
15
15
  },
16
16
 
17
17
  deserialize(arr, deserialize) {
@@ -1,17 +1,18 @@
1
1
  import { namespace } from "../../utils"
2
2
  import { type ActionCallArgumentSerializer, cannotSerialize } from "./core"
3
3
 
4
- export const dateSerializer: ActionCallArgumentSerializer<Date, number> = {
4
+ export const dateSerializer: ActionCallArgumentSerializer<Date, number | null> = {
5
5
  id: `${namespace}/dateAsTimestamp`,
6
6
 
7
7
  serialize(date) {
8
8
  if (!(date instanceof Date)) {
9
9
  return cannotSerialize
10
10
  }
11
- return +date
11
+ const timestamp = +date
12
+ return Number.isNaN(timestamp) ? null : timestamp
12
13
  },
13
14
 
14
15
  deserialize(timestamp) {
15
- return new Date(timestamp)
16
+ return new Date(timestamp === null ? Number.NaN : timestamp)
16
17
  },
17
18
  }
@@ -15,13 +15,8 @@ export const mapSerializer: ActionCallArgumentSerializer<
15
15
 
16
16
  const arr: [any, any][] = []
17
17
 
18
- const iter = map.keys()
19
- let cur = iter.next()
20
- while (!cur.done) {
21
- const k = cur.value
22
- const v = map.get(k)
23
- arr.push([serialize(k), serialize(v)])
24
- cur = iter.next()
18
+ for (const [key, value] of map.entries()) {
19
+ arr.push([serialize(key), serialize(value)])
25
20
  }
26
21
 
27
22
  return arr
@@ -30,11 +25,8 @@ export const mapSerializer: ActionCallArgumentSerializer<
30
25
  deserialize(arr, deserialize) {
31
26
  const map = new Map()
32
27
 
33
- const len = arr.length
34
- for (let i = 0; i < len; i++) {
35
- const k = arr[i][0]
36
- const v = arr[i][1]
37
- map.set(deserialize(k), deserialize(v))
28
+ for (const [key, value] of arr) {
29
+ map.set(deserialize(key), deserialize(value))
38
30
  }
39
31
 
40
32
  return map
@@ -1,4 +1,4 @@
1
- import type { ObservableSet } from "mobx"
1
+ import { isObservableSet, type ObservableSet } from "mobx"
2
2
  import { namespace } from "../../utils"
3
3
  import { type ActionCallArgumentSerializer, cannotSerialize } from "./core"
4
4
 
@@ -6,21 +6,12 @@ export const setSerializer: ActionCallArgumentSerializer<Set<any> | ObservableSe
6
6
  id: `${namespace}/setAsArray`,
7
7
 
8
8
  serialize(set, serialize) {
9
- if (!(set instanceof Set)) {
9
+ if (!(set instanceof Set || isObservableSet(set))) {
10
10
  return cannotSerialize
11
11
  }
12
12
 
13
- const arr: any[] = []
14
-
15
- const iter = set.keys()
16
- let cur = iter.next()
17
- while (!cur.done) {
18
- const k = cur.value
19
- arr.push(serialize(k))
20
- cur = iter.next()
21
- }
22
-
23
- return arr
13
+ // Array.from closes the iterator if an element cannot be serialized.
14
+ return Array.from(set.keys(), serialize)
24
15
  },
25
16
 
26
17
  deserialize(arr, deserialize) {