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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/dist/mobx-keystone.esm.js +2561 -1214
- package/dist/mobx-keystone.esm.mjs +2561 -1214
- package/dist/mobx-keystone.umd.js +2566 -1220
- package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
- package/dist/types/actionMiddlewares/undoMiddleware.d.ts +11 -12
- package/dist/types/modelShared/modelDecorator.d.ts +5 -2
- package/dist/types/modelShared/prop.d.ts +42 -29
- package/dist/types/parent/arrayPaths.d.ts +1 -0
- package/dist/types/parent/treeOrder.d.ts +1 -0
- package/dist/types/patch/applyPatches.d.ts +2 -1
- package/dist/types/ref/Ref.d.ts +1 -4
- package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
- package/dist/types/ref/nodesById.d.ts +1 -0
- package/dist/types/ref/refIdIndex.d.ts +1 -0
- package/dist/types/snapshot/SnapshotOf.d.ts +4 -4
- package/dist/types/snapshot/applySnapshot.d.ts +2 -1
- package/dist/types/snapshot/fromSnapshot.d.ts +2 -5
- package/dist/types/snapshot/getSnapshot.d.ts +1 -1
- package/dist/types/tweaker/changeRollback.d.ts +1 -0
- package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/schemas.d.ts +41 -37
- package/dist/types/types/tProp.d.ts +12 -6
- package/dist/types/types/types.d.ts +6 -6
- package/dist/types/types/utility/typeToStoredData.d.ts +14 -10
- package/dist/types/types/utility/typesCodecCore.d.ts +3 -0
- package/dist/types/utils/ModelPool.d.ts +7 -2
- package/dist/types/utils/chunks.d.ts +1 -0
- package/dist/types/utils/types.d.ts +6 -0
- package/dist/types/wrappers/ArraySet.d.ts +1 -0
- package/package.json +5 -3
- package/src/action/modelFlow.ts +35 -66
- package/src/actionMiddlewares/actionTrackingMiddleware.ts +41 -71
- package/src/actionMiddlewares/transactionMiddleware.ts +41 -42
- package/src/actionMiddlewares/undoMiddleware.ts +63 -35
- package/src/context/context.ts +15 -36
- package/src/dataModel/utils.ts +5 -28
- package/src/deepChange/onDeepChange.ts +44 -126
- package/src/model/BaseModel.ts +7 -5
- package/src/model/utils.ts +5 -10
- package/src/modelShared/modelDecorator.ts +16 -30
- package/src/modelShared/prop.ts +115 -111
- package/src/modelShared/sharedInternalModel.ts +2 -2
- package/src/parent/arrayPaths.ts +239 -0
- package/src/parent/coreObjectChildren.ts +300 -48
- package/src/parent/onChildAttachedTo.ts +130 -44
- package/src/parent/path.ts +10 -12
- package/src/parent/setParent.ts +27 -15
- package/src/parent/treeOrder.ts +53 -0
- package/src/parent/walkTree.ts +0 -73
- package/src/patch/applyPatches.ts +31 -26
- package/src/patch/patchRecorder.ts +13 -28
- package/src/ref/core.ts +69 -46
- package/src/ref/modelRefIdIndex.ts +131 -0
- package/src/ref/nodesById.ts +60 -0
- package/src/ref/refIdIndex.ts +209 -0
- package/src/rootStore/rootStore.ts +1 -3
- package/src/snapshot/SnapshotOf.ts +24 -48
- package/src/snapshot/applySnapshot.ts +27 -19
- package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
- package/src/snapshot/fromSnapshot.ts +12 -9
- package/src/snapshot/getSnapshot.ts +1 -1
- package/src/snapshot/internal.ts +67 -12
- package/src/snapshot/reconcileModelSnapshot.ts +5 -3
- package/src/snapshot/reconcilePlainObjectSnapshot.ts +3 -1
- package/src/treeUtils/draft.ts +6 -1
- package/src/treeUtils/sandbox.ts +10 -7
- package/src/tweaker/changeRollback.ts +218 -0
- package/src/tweaker/core.ts +33 -20
- package/src/tweaker/treeNodeMetadata.ts +12 -2
- package/src/tweaker/tweak.ts +181 -18
- package/src/tweaker/tweakArray.ts +127 -77
- package/src/tweaker/tweakPlainObject.ts +65 -46
- package/src/tweaker/tweakedChangeListeners.ts +61 -0
- package/src/tweaker/typeChecking.ts +5 -104
- package/src/types/arrayBased/typesArray.ts +23 -30
- package/src/types/arrayBased/typesTuple.ts +16 -25
- package/src/types/createCachedTypeCheck.ts +509 -94
- package/src/types/index.ts +1 -0
- package/src/types/objectBased/typesDataModelData.ts +7 -8
- package/src/types/objectBased/typesObject.ts +11 -15
- package/src/types/objectBased/typesRecord.ts +12 -19
- package/src/types/schemas.ts +59 -71
- package/src/types/tProp.ts +29 -29
- package/src/types/utility/typeToStoredData.ts +22 -21
- package/src/types/utility/typesCodecCore.ts +5 -0
- package/src/types/utility/typesCodecSupport.ts +3 -3
- package/src/types/utility/typesMap.ts +43 -51
- package/src/types/utility/typesRefinement.ts +7 -8
- package/src/types/utility/typesSkipCheck.ts +7 -8
- package/src/types/utility/typesTag.ts +7 -8
- package/src/utils/ModelPool.ts +76 -37
- package/src/utils/chunks.ts +271 -0
- package/src/utils/decorators.ts +5 -5
- package/src/utils/index.ts +38 -20
- package/src/utils/types.ts +5 -0
- package/src/wrappers/ArraySet.ts +27 -14
- package/src/wrappers/ObjectMap.ts +1 -6
- package/src/wrappers/asMap.ts +1 -21
- 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.
|
|
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
|
|
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",
|
package/src/action/modelFlow.ts
CHANGED
|
@@ -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
|
|
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:
|
|
97
|
+
fn: genFn,
|
|
74
98
|
actionType: ActionContextActionType.Async,
|
|
75
|
-
overrideContext: ctxOverride(
|
|
76
|
-
}).call(target,
|
|
99
|
+
overrideContext: ctxOverride(stepType),
|
|
100
|
+
}).call(target, value)
|
|
77
101
|
} catch (e) {
|
|
78
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
182
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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 (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
40
|
-
function
|
|
41
|
-
ctx.rootContext.data[
|
|
42
|
-
recorder:
|
|
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
|
|
47
|
-
recorder:
|
|
46
|
+
function getRollbackRecorderData(ctx: SimpleActionContext): {
|
|
47
|
+
recorder: ChangeRollbackRecorder
|
|
48
48
|
activeCount: number
|
|
49
49
|
} {
|
|
50
|
-
return ctx.rootContext.data[
|
|
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
|
-
|
|
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 =
|
|
67
|
+
const data = getRollbackRecorderData(ctx)
|
|
68
68
|
data.activeCount++
|
|
69
69
|
data.recorder.recording = true
|
|
70
70
|
},
|
|
71
71
|
onSuspend(ctx) {
|
|
72
|
-
const data =
|
|
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
|
|
78
|
+
const recorder = getRollbackRecorderData(ctx).recorder
|
|
79
|
+
// stop recording, so the rollback is not recorded as well
|
|
80
|
+
recorder.dispose()
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
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.
|
|
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
|
|
427
|
+
const events = this.store.undoEvents
|
|
428
|
+
const event = events[events.length - 1].data
|
|
396
429
|
|
|
397
430
|
withoutUndo(() => {
|
|
398
|
-
|
|
399
|
-
.
|
|
400
|
-
.
|
|
401
|
-
|
|
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
|
|
455
|
+
const events = this.store.redoEvents
|
|
456
|
+
const event = events[events.length - 1].data
|
|
423
457
|
|
|
424
458
|
withoutUndo(() => {
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
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 =
|
|
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
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
597
|
-
|
|
624
|
+
function onRejected(err: unknown): void {
|
|
625
|
+
resume(genThrow, err)
|
|
598
626
|
}
|
|
599
627
|
|
|
600
628
|
function next(ret: any): void {
|