mobx-keystone 2.0.0 → 2.2.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 (102) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/mobx-keystone.esm.js +2561 -1214
  4. package/dist/mobx-keystone.esm.mjs +2561 -1214
  5. package/dist/mobx-keystone.umd.js +2566 -1220
  6. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  7. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
  8. package/dist/types/modelShared/modelDecorator.d.ts +5 -2
  9. package/dist/types/modelShared/prop.d.ts +42 -29
  10. package/dist/types/parent/arrayPaths.d.ts +1 -0
  11. package/dist/types/parent/treeOrder.d.ts +1 -0
  12. package/dist/types/patch/applyPatches.d.ts +2 -1
  13. package/dist/types/ref/Ref.d.ts +1 -4
  14. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  15. package/dist/types/ref/nodesById.d.ts +1 -0
  16. package/dist/types/ref/refIdIndex.d.ts +1 -0
  17. package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
  18. package/dist/types/snapshot/applySnapshot.d.ts +2 -1
  19. package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
  20. package/dist/types/snapshot/getSnapshot.d.ts +1 -1
  21. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  22. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  23. package/dist/types/types/index.d.ts +1 -0
  24. package/dist/types/types/schemas.d.ts +41 -37
  25. package/dist/types/types/tProp.d.ts +12 -6
  26. package/dist/types/types/types.d.ts +6 -6
  27. package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
  28. package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
  29. package/dist/types/utils/ModelPool.d.ts +7 -2
  30. package/dist/types/utils/chunks.d.ts +1 -0
  31. package/dist/types/utils/types.d.ts +6 -0
  32. package/dist/types/wrappers/ArraySet.d.ts +1 -0
  33. package/package.json +5 -3
  34. package/src/action/modelFlow.ts +35 -66
  35. package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
  36. package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
  37. package/src/actionMiddlewares/undoMiddleware.ts +63 -35
  38. package/src/context/context.ts +15 -36
  39. package/src/dataModel/utils.ts +5 -28
  40. package/src/deepChange/onDeepChange.ts +44 -126
  41. package/src/model/BaseModel.ts +7 -5
  42. package/src/model/utils.ts +5 -10
  43. package/src/modelShared/modelDecorator.ts +16 -30
  44. package/src/modelShared/prop.ts +115 -111
  45. package/src/modelShared/sharedInternalModel.ts +2 -2
  46. package/src/parent/arrayPaths.ts +239 -0
  47. package/src/parent/coreObjectChildren.ts +300 -48
  48. package/src/parent/onChildAttachedTo.ts +130 -44
  49. package/src/parent/path.ts +10 -12
  50. package/src/parent/setParent.ts +27 -15
  51. package/src/parent/treeOrder.ts +53 -0
  52. package/src/parent/walkTree.ts +0 -73
  53. package/src/patch/applyPatches.ts +31 -26
  54. package/src/patch/patchRecorder.ts +13 -28
  55. package/src/ref/core.ts +69 -46
  56. package/src/ref/modelRefIdIndex.ts +131 -0
  57. package/src/ref/nodesById.ts +60 -0
  58. package/src/ref/refIdIndex.ts +209 -0
  59. package/src/rootStore/rootStore.ts +1 -3
  60. package/src/snapshot/SnapshotOf.ts +24 -48
  61. package/src/snapshot/applySnapshot.ts +27 -19
  62. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  63. package/src/snapshot/fromSnapshot.ts +12 -9
  64. package/src/snapshot/getSnapshot.ts +1 -1
  65. package/src/snapshot/internal.ts +67 -12
  66. package/src/snapshot/reconcileModelSnapshot.ts +5 -3
  67. package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
  68. package/src/treeUtils/draft.ts +6 -1
  69. package/src/treeUtils/sandbox.ts +10 -7
  70. package/src/tweaker/changeRollback.ts +218 -0
  71. package/src/tweaker/core.ts +33 -20
  72. package/src/tweaker/treeNodeMetadata.ts +12 -2
  73. package/src/tweaker/tweak.ts +181 -18
  74. package/src/tweaker/tweakArray.ts +127 -77
  75. package/src/tweaker/tweakPlainObject.ts +65 -46
  76. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  77. package/src/tweaker/typeChecking.ts +5 -104
  78. package/src/types/arrayBased/typesArray.ts +23 -30
  79. package/src/types/arrayBased/typesTuple.ts +16 -25
  80. package/src/types/createCachedTypeCheck.ts +509 -94
  81. package/src/types/index.ts +1 -0
  82. package/src/types/objectBased/typesDataModelData.ts +7 -8
  83. package/src/types/objectBased/typesObject.ts +11 -15
  84. package/src/types/objectBased/typesRecord.ts +12 -19
  85. package/src/types/schemas.ts +59 -71
  86. package/src/types/tProp.ts +29 -29
  87. package/src/types/utility/typeToStoredData.ts +22 -21
  88. package/src/types/utility/typesCodecCore.ts +5 -0
  89. package/src/types/utility/typesCodecSupport.ts +3 -3
  90. package/src/types/utility/typesMap.ts +43 -51
  91. package/src/types/utility/typesRefinement.ts +7 -8
  92. package/src/types/utility/typesSkipCheck.ts +7 -8
  93. package/src/types/utility/typesTag.ts +7 -8
  94. package/src/utils/ModelPool.ts +76 -37
  95. package/src/utils/chunks.ts +271 -0
  96. package/src/utils/decorators.ts +5 -5
  97. package/src/utils/index.ts +38 -20
  98. package/src/utils/types.ts +5 -0
  99. package/src/wrappers/ArraySet.ts +27 -14
  100. package/src/wrappers/ObjectMap.ts +1 -6
  101. package/src/wrappers/asMap.ts +1 -21
  102. package/src/wrappers/asSet.ts +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx-keystone",
3
- "version": "2.0.0",
3
+ "version": "2.2.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",
@@ -61,7 +61,7 @@
61
61
  "test:perf": "pnpm run build && pnpm run test:perf:run",
62
62
  "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",
63
63
  "memtest": "cd perf_bench && NODE_ENV=production TS_NODE_COMPILER=@typescript/typescript6 node --expose-gc --require ts-node/register ./memtest.ts",
64
- "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
+ "build-docs": "shx rm -rf ../../apps/site/generated-api && node --import ./scripts/typedoc-typescript6-register.mjs ./node_modules/typedoc/dist/cli.js --options ./typedocconfig.js src/index.ts"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "mobx": "^7.0.0 || ^6.0.0 || ^5.0.0 || ^4.0.0"
@@ -78,14 +78,16 @@
78
78
  "@types/node": "^26.4.1",
79
79
  "@typescript/typescript6": "^6.0.2",
80
80
  "@vitest/coverage-v8": "^5.0.0",
81
+ "expect-type": "^1.4.0",
81
82
  "mobx-v4": "npm:mobx@^4.15.7",
82
83
  "mobx-v5": "npm:mobx@^5.15.7",
83
84
  "mobx-v6": "npm:mobx@^6.0.0",
84
85
  "mobx-v7": "npm:mobx@^7.0.0",
85
86
  "shx": "^0.4.0",
86
- "spec.ts": "^1.1.3",
87
87
  "ts-node": "^10.9.2",
88
88
  "typedoc": "^0.28.20",
89
+ "typedoc-docusaurus-theme": "^1.4.3",
90
+ "typedoc-plugin-markdown": "^4.13.1",
89
91
  "typescript": "^7.0.2",
90
92
  "vite": "^8.2.2",
91
93
  "vite-plugin-dts": "^5.1.0",
@@ -65,68 +65,53 @@ export function flow<R, Args extends any[]>({
65
65
  const genThrow = gen.throw!.bind(gen)
66
66
 
67
67
  const promise = new Promise<R>((resolve, reject) => {
68
- function onFulfilled(res: any): void {
68
+ function finish(
69
+ stepType: ActionContextAsyncStepType,
70
+ resolution: FlowFinisher["resolution"],
71
+ value: unknown
72
+ ): void {
73
+ wrapInAction({
74
+ nameOrNameFn: name,
75
+ // we use a flow finisher to allow middlewares to tweak the return value before resolution
76
+ fn: (val: any): FlowFinisher => ({
77
+ value: val,
78
+ resolution,
79
+ accepter: resolve,
80
+ rejecter: reject,
81
+ }),
82
+ actionType: ActionContextActionType.Async,
83
+ overrideContext: ctxOverride(stepType),
84
+ isFlowFinisher: true,
85
+ }).call(target, value)
86
+ }
87
+
88
+ function resume(
89
+ genFn: (value: any) => IteratorResult<any>,
90
+ stepType: ActionContextAsyncStepType,
91
+ value: unknown
92
+ ): void {
69
93
  let ret: unknown
70
94
  try {
71
95
  ret = wrapInAction({
72
96
  nameOrNameFn: name,
73
- fn: genNext,
97
+ fn: genFn,
74
98
  actionType: ActionContextActionType.Async,
75
- overrideContext: ctxOverride(ActionContextAsyncStepType.Resume),
76
- }).call(target, res)
99
+ overrideContext: ctxOverride(stepType),
100
+ }).call(target, value)
77
101
  } catch (e) {
78
- wrapInAction({
79
- nameOrNameFn: name,
80
- fn: (err: any) => {
81
- // we use a flow finisher to allow middlewares to tweak the return value before resolution
82
- return {
83
- value: err,
84
- resolution: "reject",
85
-
86
- accepter: resolve,
87
- rejecter: reject,
88
- } as FlowFinisher
89
- },
90
- actionType: ActionContextActionType.Async,
91
- overrideContext: ctxOverride(ActionContextAsyncStepType.Throw),
92
- isFlowFinisher: true,
93
- }).call(target, e)
102
+ finish(ActionContextAsyncStepType.Throw, "reject", e)
94
103
  return
95
104
  }
96
105
 
97
106
  next(ret)
98
107
  }
99
108
 
100
- function onRejected(err: unknown): void {
101
- let ret: unknown
102
- try {
103
- ret = wrapInAction({
104
- nameOrNameFn: name,
105
- fn: genThrow,
106
- actionType: ActionContextActionType.Async,
107
- overrideContext: ctxOverride(ActionContextAsyncStepType.ResumeError),
108
- }).call(target, err)
109
- } catch (e) {
110
- wrapInAction({
111
- nameOrNameFn: name,
112
- fn: (err: any) => {
113
- // we use a flow finisher to allow middlewares to tweak the return value before resolution
114
- return {
115
- value: err,
116
- resolution: "reject",
117
-
118
- accepter: resolve,
119
- rejecter: reject,
120
- } as FlowFinisher
121
- },
122
- actionType: ActionContextActionType.Async,
123
- overrideContext: ctxOverride(ActionContextAsyncStepType.Throw),
124
- isFlowFinisher: true,
125
- }).call(target, e)
126
- return
127
- }
109
+ function onFulfilled(res: unknown): void {
110
+ resume(genNext, ActionContextAsyncStepType.Resume, res)
111
+ }
128
112
 
129
- next(ret)
113
+ function onRejected(err: unknown): void {
114
+ resume(genThrow, ActionContextAsyncStepType.ResumeError, err)
130
115
  }
131
116
 
132
117
  function next(ret: any): void {
@@ -134,23 +119,7 @@ export function flow<R, Args extends any[]>({
134
119
  // an async iterator
135
120
  Promise.resolve(ret).then(next, reject).catch(reject)
136
121
  } else if (ret.done) {
137
- // done
138
- wrapInAction({
139
- nameOrNameFn: name,
140
- fn: (val: any) => {
141
- // we use a flow finisher to allow middlewares to tweak the return value before resolution
142
- return {
143
- value: val,
144
- resolution: "accept",
145
-
146
- accepter: resolve,
147
- rejecter: reject,
148
- } as FlowFinisher
149
- },
150
- actionType: ActionContextActionType.Async,
151
- overrideContext: ctxOverride(ActionContextAsyncStepType.Return),
152
- isFlowFinisher: true,
153
- }).call(target, ret.value)
122
+ finish(ActionContextAsyncStepType.Return, "accept", ret.value)
154
123
  } else {
155
124
  // continue
156
125
  Promise.resolve(ret.value).then(onFulfilled, onRejected).catch(reject)
@@ -178,8 +178,11 @@ export function actionTrackingMiddleware(
178
178
  const resumeSuspendSupport = !!hooks.onResume || !!hooks.onSuspend
179
179
 
180
180
  const filter: ActionMiddleware["filter"] = (ctx) => {
181
- if (ctx.type === ActionContextActionType.Sync) {
182
- // start and finish is on the same context
181
+ if (
182
+ ctx.type === ActionContextActionType.Sync ||
183
+ ctx.asyncStepType === ActionContextAsyncStepType.Spawn
184
+ ) {
185
+ // (for sync actions start and finish are on the same context)
183
186
  const accepted = userFilter(ctx)
184
187
  if (accepted) {
185
188
  setCtxData(ctx, {
@@ -188,39 +191,21 @@ export function actionTrackingMiddleware(
188
191
  })
189
192
  }
190
193
  return accepted
191
- } else {
192
- switch (ctx.asyncStepType) {
193
- case ActionContextAsyncStepType.Spawn: {
194
- const accepted = userFilter(ctx)
195
- if (accepted) {
196
- setCtxData(ctx, {
197
- startAccepted: true,
198
- state: State.Idle,
199
- })
200
- }
201
- return accepted
202
- }
194
+ }
203
195
 
204
- case ActionContextAsyncStepType.Return:
205
- case ActionContextAsyncStepType.Throw: {
206
- // depends if the spawn one was accepted or not
207
- const data = getCtxData(ctx.spawnAsyncStepContext!)
208
- return data ? data.startAccepted : false
209
- }
196
+ // the other steps depend on whether the spawn one was accepted or not
197
+ const spawnAccepted = () => !!getCtxData(ctx.spawnAsyncStepContext!)?.startAccepted
198
+ switch (ctx.asyncStepType) {
199
+ case ActionContextAsyncStepType.Return:
200
+ case ActionContextAsyncStepType.Throw:
201
+ return spawnAccepted()
210
202
 
211
- case ActionContextAsyncStepType.Resume:
212
- case ActionContextAsyncStepType.ResumeError:
213
- if (resumeSuspendSupport) {
214
- // depends if the spawn one was accepted or not
215
- const data = getCtxData(ctx.spawnAsyncStepContext!)
216
- return data ? data.startAccepted : false
217
- } else {
218
- return false
219
- }
203
+ case ActionContextAsyncStepType.Resume:
204
+ case ActionContextAsyncStepType.ResumeError:
205
+ return resumeSuspendSupport && spawnAccepted()
220
206
 
221
- default:
222
- return false
223
- }
207
+ default:
208
+ return false
224
209
  }
225
210
  }
226
211
 
@@ -311,57 +296,42 @@ export function actionTrackingMiddleware(
311
296
  }
312
297
  }
313
298
 
314
- if (ctx.type === ActionContextActionType.Sync) {
315
- let retObj = start(simpleCtx)
316
-
317
- if (retObj) {
299
+ if (
300
+ ctx.type === ActionContextActionType.Sync ||
301
+ ctx.asyncStepType === ActionContextAsyncStepType.Spawn
302
+ ) {
303
+ const overriddenRetObj = start(simpleCtx)
304
+ if (overriddenRetObj) {
318
305
  // action canceled / overridden by onStart
319
306
  resume(simpleCtx, true)
320
307
  suspend(simpleCtx)
321
- retObj = finish(simpleCtx, retObj)
322
- } else {
323
- try {
324
- retObj = { result: ActionTrackingResult.Return, value: next() }
325
- } catch (err) {
326
- retObj = { result: ActionTrackingResult.Throw, value: err }
327
- }
328
- retObj = finish(simpleCtx, retObj)
308
+ return returnOrThrowActionTrackingReturn(finish(simpleCtx, overriddenRetObj))
309
+ }
310
+
311
+ if (ctx.type !== ActionContextActionType.Sync) {
312
+ // a flow finishes in its return / throw step
313
+ return next()
329
314
  }
330
315
 
331
- return returnOrThrowActionTrackingReturn(retObj)
316
+ let retObj: ActionTrackingReturn
317
+ try {
318
+ retObj = { result: ActionTrackingResult.Return, value: next() }
319
+ } catch (err) {
320
+ retObj = { result: ActionTrackingResult.Throw, value: err }
321
+ }
322
+ return returnOrThrowActionTrackingReturn(finish(simpleCtx, retObj))
332
323
  } else {
333
324
  // async
334
325
 
335
326
  switch (ctx.asyncStepType) {
336
- case ActionContextAsyncStepType.Spawn: {
337
- let retObj = start(simpleCtx)
338
- if (retObj) {
339
- // action canceled / overridden by onStart
340
- resume(simpleCtx, true)
341
- suspend(simpleCtx)
342
- retObj = finish(simpleCtx, retObj)
343
- return returnOrThrowActionTrackingReturn(retObj)
344
- } else {
345
- return next()
346
- }
347
- }
348
-
349
- case ActionContextAsyncStepType.Return: {
350
- const flowFinisher: FlowFinisher = next()
351
- const retObj = finish(simpleCtx, {
352
- result: ActionTrackingResult.Return,
353
- value: flowFinisher.value,
354
- })
355
- flowFinisher.resolution =
356
- retObj.result === ActionTrackingResult.Return ? "accept" : "reject"
357
- flowFinisher.value = retObj.value
358
- return flowFinisher
359
- }
360
-
327
+ case ActionContextAsyncStepType.Return:
361
328
  case ActionContextAsyncStepType.Throw: {
362
329
  const flowFinisher: FlowFinisher = next()
363
330
  const retObj = finish(simpleCtx, {
364
- result: ActionTrackingResult.Throw,
331
+ result:
332
+ ctx.asyncStepType === ActionContextAsyncStepType.Return
333
+ ? ActionTrackingResult.Return
334
+ : ActionTrackingResult.Throw,
365
335
  value: flowFinisher.value,
366
336
  })
367
337
  flowFinisher.resolution =
@@ -1,10 +1,10 @@
1
1
  import type { ActionMiddlewareDisposer } from "../action/middleware"
2
+ import { runUnprotected } from "../action/runUnprotected"
2
3
  import type { AnyModel } from "../model/BaseModel"
3
4
  import { assertIsModel } from "../model/utils"
4
5
  import { addModelClassInitializer } from "../modelShared/modelClassInitializer"
5
- import { applyPatches } from "../patch"
6
- import { internalPatchRecorder, type PatchRecorder } from "../patch/patchRecorder"
7
- import { withTypeCheckingBatch } from "../tweaker/typeChecking"
6
+ import { ChangeRollbackRecorder } from "../tweaker/changeRollback"
7
+ import { withoutTypeChecking } from "../tweaker/withoutTypeChecking"
8
8
  import { assertIsObject, failure, MobxKeystoneAggregateError } from "../utils"
9
9
  import { checkDecoratorContext } from "../utils/decorators"
10
10
  import { pushDelayedError } from "../utils/forEachWithDelayedThrow"
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  /**
18
18
  * Creates a transaction middleware, which reverts changes made by an action / child
19
- * actions when the root action throws an exception by applying inverse patches.
19
+ * actions when the root action throws an exception, restoring the values those changes replaced.
20
20
  *
21
21
  * @template M Model
22
22
  * @param target Object with the root target model object (`model`) and root action name (`actionName`).
@@ -36,18 +36,18 @@ export function transactionMiddleware<M extends AnyModel>(target: {
36
36
  throw failure("target.actionName must be a string")
37
37
  }
38
38
 
39
- const patchRecorderSymbol = Symbol("patchRecorder")
40
- function initPatchRecorder(ctx: SimpleActionContext) {
41
- ctx.rootContext.data[patchRecorderSymbol] = {
42
- recorder: internalPatchRecorder(undefined, { recording: false }),
39
+ const rollbackRecorderSymbol = Symbol("rollbackRecorder")
40
+ function initRollbackRecorder(ctx: SimpleActionContext) {
41
+ ctx.rootContext.data[rollbackRecorderSymbol] = {
42
+ recorder: new ChangeRollbackRecorder(),
43
43
  activeCount: 0,
44
44
  }
45
45
  }
46
- function getPatchRecorderData(ctx: SimpleActionContext): {
47
- recorder: PatchRecorder
46
+ function getRollbackRecorderData(ctx: SimpleActionContext): {
47
+ recorder: ChangeRollbackRecorder
48
48
  activeCount: number
49
49
  } {
50
- return ctx.rootContext.data[patchRecorderSymbol]
50
+ return ctx.rootContext.data[rollbackRecorderSymbol]
51
51
  }
52
52
 
53
53
  return actionTrackingMiddleware(model, {
@@ -58,55 +58,54 @@ export function transactionMiddleware<M extends AnyModel>(target: {
58
58
  },
59
59
  onStart(ctx) {
60
60
  if (ctx === ctx.rootContext) {
61
- initPatchRecorder(ctx)
61
+ initRollbackRecorder(ctx)
62
62
  }
63
63
  },
64
64
  // Actions nest, so recording must only stop once every action of the transaction
65
65
  // has been suspended, not as soon as the innermost one returns.
66
66
  onResume(ctx) {
67
- const data = getPatchRecorderData(ctx)
67
+ const data = getRollbackRecorderData(ctx)
68
68
  data.activeCount++
69
69
  data.recorder.recording = true
70
70
  },
71
71
  onSuspend(ctx) {
72
- const data = getPatchRecorderData(ctx)
72
+ const data = getRollbackRecorderData(ctx)
73
73
  data.activeCount--
74
74
  data.recorder.recording = data.activeCount > 0
75
75
  },
76
76
  onFinish(ctx, ret) {
77
77
  if (ctx === ctx.rootContext) {
78
- const patchRecorder = getPatchRecorderData(ctx).recorder
78
+ const recorder = getRollbackRecorderData(ctx).recorder
79
+ // stop recording, so the rollback is not recorded as well
80
+ recorder.dispose()
79
81
 
80
- try {
81
- if (ret.result === ActionTrackingResult.Throw) {
82
- // undo changes (backwards for inverse patches)
83
- const { events } = patchRecorder
84
- const rollbackErrors: unknown[] = []
85
- withTypeCheckingBatch(() => {
86
- for (let i = events.length - 1; i >= 0; i--) {
87
- const event = events[i]
88
- // A listener may throw after a patch has already been applied.
89
- // Continue at patch granularity so one failure cannot skip the
90
- // remaining restoration, including patches from the same edit.
91
- for (let j = event.inversePatches.length - 1; j >= 0; j--) {
92
- try {
93
- applyPatches(event.target, [event.inversePatches[j]])
94
- } catch (error) {
95
- pushDelayedError(rollbackErrors, error)
96
- }
82
+ if (ret.result === ActionTrackingResult.Throw) {
83
+ // revert changes (backwards). Restoring the replaced values themselves
84
+ // (rather than applying inverse patches) keeps node identities, so
85
+ // nodes that were detached and then changed are restored too.
86
+ const { reverts } = recorder
87
+ const rollbackErrors: unknown[] = []
88
+ runUnprotected(() => {
89
+ // it restores the state before the action, which was already checked
90
+ withoutTypeChecking(() => {
91
+ for (let i = reverts.length - 1; i >= 0; i--) {
92
+ // A listener may throw after a change has already been reverted.
93
+ // Keep going so one failure cannot skip the remaining restoration.
94
+ try {
95
+ reverts[i]()
96
+ } catch (error) {
97
+ pushDelayedError(rollbackErrors, error)
97
98
  }
98
99
  }
99
100
  })
100
- if (rollbackErrors.length > 0) {
101
- // Keep the action error first so the reason for the rollback is not lost.
102
- throw new MobxKeystoneAggregateError(
103
- [ret.value, ...rollbackErrors],
104
- "transaction rollback callbacks failed"
105
- )
106
- }
101
+ })
102
+ if (rollbackErrors.length > 0) {
103
+ // Keep the action error first so the reason for the rollback is not lost.
104
+ throw new MobxKeystoneAggregateError(
105
+ [ret.value, ...rollbackErrors],
106
+ "transaction rollback callbacks failed"
107
+ )
107
108
  }
108
- } finally {
109
- patchRecorder.dispose()
110
109
  }
111
110
  }
112
111
  },
@@ -123,7 +122,7 @@ export function transaction(...args: any[]): void {
123
122
 
124
123
  checkDecoratorContext("transaction", ctx.name, ctx.static)
125
124
  if (ctx.kind !== "method" && ctx.kind !== "field") {
126
- throw failure(`@transaction can only be used on fields or methods}`)
125
+ throw failure(`@transaction can only be used on fields or methods`)
127
126
  }
128
127
 
129
128
  ctx.addInitializer(function (this: any) {
@@ -1,12 +1,15 @@
1
1
  import type { ActionMiddlewareDisposer } from "../action/middleware"
2
2
  import { modelAction } from "../action/modelAction"
3
+ import { type Frozen, FrozenCheckMode, frozen, isFrozenSnapshot, toFrozenSnapshot } from "../frozen"
3
4
  import { Model } from "../model/Model"
4
5
  import { model } from "../modelShared/modelDecorator"
5
6
  import { fastGetRootPath } from "../parent/path"
6
7
  import type { Path } from "../parent/pathTypes"
7
8
  import { applyPatches, type Patch, type PatchRecorder, patchRecorder } from "../patch"
9
+ import type { FrozenData } from "../snapshot"
8
10
  import { assertTweakedObject } from "../tweaker/core"
9
11
  import { typesArray } from "../types/arrayBased/typesArray"
12
+ import { typesFrozen } from "../types/objectBased/typesObject"
10
13
  import { tProp } from "../types/tProp"
11
14
  import { typesUnchecked } from "../types/utility/typesUnchecked"
12
15
  import {
@@ -121,16 +124,45 @@ function toSingleEvents(
121
124
  return array
122
125
  }
123
126
 
127
+ // snapshots from before events were frozen have them as plain objects
128
+ function toFrozenEventSnapshots(
129
+ events: ReadonlyArray<UndoEvent | FrozenData<UndoEvent>> | undefined
130
+ ): FrozenData<UndoEvent>[] | undefined {
131
+ return events?.map((e) => (isFrozenSnapshot<UndoEvent>(e) ? e : toFrozenSnapshot(e)))
132
+ }
133
+
134
+ function freezeEvent(event: UndoEvent): Frozen<UndoEvent> {
135
+ // events are only read once recorded, so they are not checked or deeply
136
+ // frozen (that would also freeze the attached states given by the user)
137
+ return frozen(event, FrozenCheckMode.Off)
138
+ }
139
+
124
140
  /**
125
141
  * Store model instance for undo/redo actions.
126
- * Do not manipulate directly, other that creating it.
142
+ * Do not manipulate directly, other than creating it.
143
+ *
144
+ * Events are stored as frozen data, since they never change once recorded.
127
145
  */
128
146
  @model(`${namespace}/UndoStore`)
129
- export class UndoStore extends Model({
130
- // TODO: add proper type checking to undo store
131
- undoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
132
- redoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
133
- }) {
147
+ export class UndoStore extends Model(
148
+ {
149
+ // TODO: add proper type checking to undo store
150
+ undoEvents: tProp(typesArray(typesFrozen(typesUnchecked<UndoEvent>())), () => []),
151
+ redoEvents: tProp(typesArray(typesFrozen(typesUnchecked<UndoEvent>())), () => []),
152
+ },
153
+ {
154
+ fromSnapshotProcessor(sn: {
155
+ undoEvents?: ReadonlyArray<UndoEvent | FrozenData<UndoEvent>>
156
+ redoEvents?: ReadonlyArray<UndoEvent | FrozenData<UndoEvent>>
157
+ }) {
158
+ return {
159
+ ...sn,
160
+ undoEvents: toFrozenEventSnapshots(sn.undoEvents),
161
+ redoEvents: toFrozenEventSnapshots(sn.redoEvents),
162
+ }
163
+ },
164
+ }
165
+ ) {
134
166
  /**
135
167
  * @ignore
136
168
  */
@@ -201,7 +233,7 @@ export class UndoStore extends Model({
201
233
  @modelAction
202
234
  _addUndo({ event, maxUndoLevels }: { event: UndoEvent; maxUndoLevels: number | undefined }) {
203
235
  withoutUndo(() => {
204
- this.undoEvents.push(event)
236
+ this.undoEvents.push(freezeEvent(event))
205
237
  // once an undo event is added redo queue is no longer valid
206
238
  this.redoEvents.length = 0
207
239
  this.enforceMaxLevels({ maxUndoLevels })
@@ -323,7 +355,7 @@ export class UndoManager {
323
355
  */
324
356
  @mobxComputed
325
357
  get undoQueue(): ReadonlyArray<UndoEvent> {
326
- return this.store.undoEvents
358
+ return this.store.undoEvents.map((e) => e.data)
327
359
  }
328
360
 
329
361
  /**
@@ -332,7 +364,7 @@ export class UndoManager {
332
364
  */
333
365
  @mobxComputed
334
366
  get redoQueue(): ReadonlyArray<UndoEvent> {
335
- return this.store.redoEvents
367
+ return this.store.redoEvents.map((e) => e.data)
336
368
  }
337
369
 
338
370
  /**
@@ -340,7 +372,7 @@ export class UndoManager {
340
372
  */
341
373
  @mobxComputed
342
374
  get undoLevels() {
343
- return this.undoQueue.length
375
+ return this.store.undoEvents.length
344
376
  }
345
377
 
346
378
  /**
@@ -364,7 +396,7 @@ export class UndoManager {
364
396
  */
365
397
  @mobxComputed
366
398
  get redoLevels() {
367
- return this.redoQueue.length
399
+ return this.store.redoEvents.length
368
400
  }
369
401
 
370
402
  /**
@@ -392,14 +424,15 @@ export class UndoManager {
392
424
  if (!this.canUndo) {
393
425
  throw failure("nothing to undo")
394
426
  }
395
- const event = this.undoQueue[this.undoQueue.length - 1]
427
+ const events = this.store.undoEvents
428
+ const event = events[events.length - 1].data
396
429
 
397
430
  withoutUndo(() => {
398
- toSingleEvents(event)
399
- .reverse()
400
- .forEach((e) => {
401
- applyPatches(this.subtreeRoot, e.inversePatches, true)
402
- })
431
+ applyPatches(
432
+ this.subtreeRoot,
433
+ toSingleEvents(event).map((e) => e.inversePatches),
434
+ true
435
+ )
403
436
 
404
437
  // restore the attached state before the operation was made
405
438
  if (event.attachedState) {
@@ -419,12 +452,14 @@ export class UndoManager {
419
452
  if (!this.canRedo) {
420
453
  throw failure("nothing to redo")
421
454
  }
422
- const event = this.redoQueue[this.redoQueue.length - 1]
455
+ const events = this.store.redoEvents
456
+ const event = events[events.length - 1].data
423
457
 
424
458
  withoutUndo(() => {
425
- toSingleEvents(event).forEach((e) => {
426
- applyPatches(this.subtreeRoot, e.patches)
427
- })
459
+ applyPatches(
460
+ this.subtreeRoot,
461
+ toSingleEvents(event).map((e) => e.patches)
462
+ )
428
463
 
429
464
  // restore the attached state after the operation was made
430
465
  if (event.attachedState) {
@@ -567,11 +602,11 @@ export class UndoManager {
567
602
  const genThrow = gen.throw.bind(gen)
568
603
 
569
604
  const promise = new Promise<R>((resolve, reject) => {
570
- function onFulfilled(res: any): void {
605
+ function resume(genFn: (value: any) => IteratorResult<any>, value: unknown): void {
571
606
  group.resume()
572
607
  let ret: unknown
573
608
  try {
574
- ret = genNext(res)
609
+ ret = genFn(value)
575
610
  } catch (e) {
576
611
  group.end()
577
612
  reject(e as Error)
@@ -582,19 +617,12 @@ export class UndoManager {
582
617
  next(ret)
583
618
  }
584
619
 
585
- function onRejected(err: unknown): void {
586
- group.resume()
587
- let ret: unknown
588
- try {
589
- ret = genThrow(err)
590
- } catch (e) {
591
- group.end()
592
- reject(e as Error)
593
- return
594
- }
620
+ function onFulfilled(res: unknown): void {
621
+ resume(genNext, res)
622
+ }
595
623
 
596
- group.pause()
597
- next(ret)
624
+ function onRejected(err: unknown): void {
625
+ resume(genThrow, err)
598
626
  }
599
627
 
600
628
  function next(ret: any): void {