mobx-keystone 1.20.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +864 -853
  2. package/README.md +61 -61
  3. package/dist/mobx-keystone.esm.js +11336 -11148
  4. package/dist/mobx-keystone.esm.mjs +11336 -11148
  5. package/dist/mobx-keystone.umd.js +12229 -11738
  6. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +1 -1
  7. package/dist/types/modelShared/prop.d.ts +5 -0
  8. package/dist/types/redux/connectReduxDevTools.d.ts +17 -4
  9. package/dist/types/ref/Ref.d.ts +1 -1
  10. package/dist/types/treeUtils/sandbox.d.ts +1 -1
  11. package/dist/types/types/TypeChecker.d.ts +1 -1
  12. package/dist/types/types/arrayBased/typesArray.d.ts +1 -1
  13. package/dist/types/types/arrayBased/typesTuple.d.ts +1 -1
  14. package/dist/types/types/objectBased/typesArraySet.d.ts +1 -1
  15. package/dist/types/types/objectBased/typesDataModelData.d.ts +1 -1
  16. package/dist/types/types/objectBased/typesModel.d.ts +1 -1
  17. package/dist/types/types/objectBased/typesObject.d.ts +3 -3
  18. package/dist/types/types/objectBased/typesObjectMap.d.ts +1 -1
  19. package/dist/types/types/objectBased/typesRecord.d.ts +1 -1
  20. package/dist/types/types/primitiveBased/typesPrimitive.d.ts +1 -1
  21. package/dist/types/types/types.d.ts +3 -3
  22. package/dist/types/types/utility/typesCodec.d.ts +1 -6
  23. package/dist/types/types/utility/typesCodecCore.d.ts +1 -1
  24. package/dist/types/types/utility/typesOr.d.ts +1 -1
  25. package/dist/types/types/utility/typesRefinement.d.ts +1 -1
  26. package/dist/types/types/utility/typesSkipCheck.d.ts +1 -1
  27. package/dist/types/types/utility/typesTag.d.ts +1 -1
  28. package/dist/types/wrappers/ObjectMap.d.ts +2 -0
  29. package/dist/types/wrappers/asMap.d.ts +10 -6
  30. package/package.json +14 -12
  31. package/src/action/applyAction.ts +125 -114
  32. package/src/action/applyMethodCall.ts +37 -37
  33. package/src/action/isModelAction.ts +16 -16
  34. package/src/action/middleware.ts +137 -137
  35. package/src/action/modelFlow.ts +240 -240
  36. package/src/action/modelFlowPromiseGenerator.ts +127 -125
  37. package/src/action/pendingActions.ts +42 -42
  38. package/src/action/wrapInAction.ts +156 -156
  39. package/src/actionMiddlewares/actionSerialization/actionSerialization.ts +1 -1
  40. package/src/actionMiddlewares/actionSerialization/applySerializedAction.ts +172 -172
  41. package/src/actionMiddlewares/actionSerialization/arraySerializer.ts +20 -20
  42. package/src/actionMiddlewares/actionSerialization/dateSerializer.ts +17 -17
  43. package/src/actionMiddlewares/actionSerialization/mapSerializer.ts +42 -42
  44. package/src/actionMiddlewares/actionSerialization/objectPathSerializer.ts +50 -50
  45. package/src/actionMiddlewares/actionSerialization/objectSnapshotSerializer.ts +21 -21
  46. package/src/actionMiddlewares/actionSerialization/plainObjectSerializer.ts +24 -32
  47. package/src/actionMiddlewares/actionSerialization/primitiveSerializer.ts +51 -51
  48. package/src/actionMiddlewares/actionSerialization/setSerializer.ts +37 -37
  49. package/src/actionMiddlewares/actionTrackingMiddleware.ts +3 -3
  50. package/src/actionMiddlewares/onActionMiddleware.ts +2 -2
  51. package/src/actionMiddlewares/readonlyMiddleware.ts +2 -2
  52. package/src/actionMiddlewares/transactionMiddleware.ts +4 -4
  53. package/src/actionMiddlewares/undoMiddleware.ts +804 -796
  54. package/src/actionMiddlewares/utils.ts +2 -2
  55. package/src/computedTree/computedTree.ts +143 -143
  56. package/src/context/context.ts +1 -1
  57. package/src/dataModel/BaseDataModel.ts +194 -192
  58. package/src/dataModel/actions.ts +1 -1
  59. package/src/dataModel/newDataModel.ts +57 -57
  60. package/src/deepChange/index.ts +1 -1
  61. package/src/deepChange/onDeepChange.ts +244 -244
  62. package/src/frozen/Frozen.ts +146 -146
  63. package/src/globalConfig/globalConfig.ts +107 -107
  64. package/src/index.ts +24 -24
  65. package/src/model/BaseModel.ts +275 -273
  66. package/src/model/ModelConstructorOptions.ts +15 -15
  67. package/src/model/getModelMetadata.ts +51 -51
  68. package/src/model/index.ts +6 -6
  69. package/src/model/mixins.ts +288 -288
  70. package/src/model/newModel.ts +301 -301
  71. package/src/model/utils.ts +249 -249
  72. package/src/modelShared/modelDecorator.ts +260 -253
  73. package/src/modelShared/modelInfo.ts +50 -50
  74. package/src/modelShared/modelSymbols.ts +3 -3
  75. package/src/modelShared/newModel.ts +25 -25
  76. package/src/modelShared/prop.ts +713 -684
  77. package/src/modelShared/sharedInternalModel.ts +427 -417
  78. package/src/modelShared/utils.ts +24 -24
  79. package/src/parent/core.ts +65 -65
  80. package/src/parent/coreObjectChildren.ts +180 -180
  81. package/src/parent/detach.ts +47 -47
  82. package/src/parent/findParent.ts +1 -1
  83. package/src/parent/getChildrenObjects.ts +25 -25
  84. package/src/parent/path.ts +386 -381
  85. package/src/parent/setParent.ts +149 -149
  86. package/src/parent/walkTree.ts +1 -1
  87. package/src/patch/applyPatches.ts +187 -187
  88. package/src/patch/jsonPatch.ts +108 -108
  89. package/src/patch/patchRecorder.ts +6 -1
  90. package/src/redux/connectReduxDevTools.ts +39 -7
  91. package/src/redux/redux.ts +6 -2
  92. package/src/ref/Ref.ts +1 -1
  93. package/src/ref/core.ts +339 -345
  94. package/src/ref/customRef.ts +3 -3
  95. package/src/ref/rootRef.ts +100 -100
  96. package/src/rootStore/attachDetach.ts +89 -89
  97. package/src/rootStore/rootStore.ts +1 -1
  98. package/src/snapshot/SnapshotOf.ts +104 -104
  99. package/src/snapshot/SnapshotProcessingError.ts +45 -45
  100. package/src/snapshot/applySnapshot.ts +177 -181
  101. package/src/snapshot/cloneTreeValue.ts +30 -30
  102. package/src/snapshot/fromArraySnapshot.ts +27 -33
  103. package/src/snapshot/fromModelSnapshot.ts +70 -70
  104. package/src/snapshot/fromPlainObjectSnapshot.ts +47 -42
  105. package/src/snapshot/fromSnapshot.ts +198 -198
  106. package/src/snapshot/getSnapshot.ts +80 -79
  107. package/src/snapshot/index.ts +8 -8
  108. package/src/snapshot/internal.ts +230 -230
  109. package/src/snapshot/reconcileArraySnapshot.ts +70 -70
  110. package/src/snapshot/reconcileModelSnapshot.ts +155 -150
  111. package/src/snapshot/reconcilePlainObjectSnapshot.ts +72 -72
  112. package/src/snapshot/reconcileSnapshot.ts +88 -88
  113. package/src/standardActions/actions.ts +64 -64
  114. package/src/standardActions/objectActions.ts +60 -60
  115. package/src/transforms/ImmutableDate.ts +93 -93
  116. package/src/transforms/asMap.ts +50 -50
  117. package/src/transforms/asSet.ts +22 -22
  118. package/src/transforms/bigint.ts +20 -20
  119. package/src/transforms/date.ts +40 -40
  120. package/src/treeUtils/deepEquals.ts +55 -55
  121. package/src/treeUtils/sandbox.ts +6 -3
  122. package/src/tweaker/TweakerPriority.ts +9 -9
  123. package/src/tweaker/tweak.ts +4 -3
  124. package/src/tweaker/tweakArray.ts +530 -475
  125. package/src/tweaker/tweakFrozen.ts +42 -42
  126. package/src/tweaker/tweakModel.ts +1 -1
  127. package/src/tweaker/tweakPlainObject.ts +355 -355
  128. package/src/tweaker/typeChecking.ts +101 -101
  129. package/src/types/SnapshotTypeMismatchError.ts +46 -46
  130. package/src/types/TypeCheckError.ts +183 -184
  131. package/src/types/TypeChecker.ts +202 -185
  132. package/src/types/arrayBased/typesArray.ts +136 -137
  133. package/src/types/arrayBased/typesTuple.ts +138 -139
  134. package/src/types/createPerEntryCachedCheck.ts +166 -160
  135. package/src/types/index.ts +6 -6
  136. package/src/types/objectBased/typesArraySet.ts +133 -134
  137. package/src/types/objectBased/typesDataModelData.ts +203 -204
  138. package/src/types/objectBased/typesModel.ts +220 -221
  139. package/src/types/objectBased/typesObject.ts +308 -310
  140. package/src/types/objectBased/typesObjectMap.ts +142 -143
  141. package/src/types/objectBased/typesRecord.ts +156 -157
  142. package/src/types/objectBased/typesRef.ts +89 -89
  143. package/src/types/primitiveBased/typesEnum.ts +63 -63
  144. package/src/types/primitiveBased/typesPrimitive.ts +219 -220
  145. package/src/types/registerDefaultStandardTypeResolvers.ts +21 -21
  146. package/src/types/resolveTypeChecker.ts +78 -78
  147. package/src/types/schemas.ts +208 -208
  148. package/src/types/tProp.ts +277 -268
  149. package/src/types/typeCheck.ts +21 -21
  150. package/src/types/typeCheckErrorUtils.ts +23 -23
  151. package/src/types/types.ts +142 -142
  152. package/src/types/utility/typeToStoredData.ts +37 -37
  153. package/src/types/utility/typesBigInt.ts +15 -15
  154. package/src/types/utility/typesCodec.ts +41 -38
  155. package/src/types/utility/typesCodecCore.ts +178 -178
  156. package/src/types/utility/typesCodecSupport.ts +806 -806
  157. package/src/types/utility/typesDate.ts +27 -27
  158. package/src/types/utility/typesMap.ts +253 -222
  159. package/src/types/utility/typesOr.ts +263 -259
  160. package/src/types/utility/typesRefinement.ts +131 -125
  161. package/src/types/utility/typesSet.ts +110 -110
  162. package/src/types/utility/typesSkipCheck.ts +69 -70
  163. package/src/types/utility/typesTag.ts +88 -82
  164. package/src/types/utility/typesUnchecked.ts +40 -40
  165. package/src/utils/AnyFunction.ts +4 -4
  166. package/src/utils/ModelPool.ts +59 -59
  167. package/src/utils/chainFns.ts +18 -18
  168. package/src/utils/decorators.ts +274 -274
  169. package/src/utils/errorDiagnostics.ts +204 -204
  170. package/src/utils/index.ts +443 -372
  171. package/src/wrappers/ArraySet.ts +156 -156
  172. package/src/wrappers/ObjectMap.ts +155 -136
  173. package/src/wrappers/asMap.ts +65 -30
  174. package/src/wrappers/asSet.ts +3 -3
@@ -1,796 +1,804 @@
1
- import { action, computed } from "mobx"
2
- import type { ActionMiddlewareDisposer } from "../action/middleware"
3
- import { modelAction } from "../action/modelAction"
4
- import { Model } from "../model/Model"
5
- import { model } from "../modelShared/modelDecorator"
6
- import { fastGetRootPath } from "../parent/path"
7
- import type { Path } from "../parent/pathTypes"
8
- import { applyPatches, Patch, PatchRecorder, patchRecorder } from "../patch"
9
- import { assertTweakedObject } from "../tweaker/core"
10
- import { typesArray } from "../types/arrayBased/typesArray"
11
- import { tProp } from "../types/tProp"
12
- import { typesUnchecked } from "../types/utility/typesUnchecked"
13
- import { failure, getMobxVersion, mobx6, namespace } from "../utils"
14
- import { actionTrackingMiddleware, SimpleActionContext } from "./actionTrackingMiddleware"
15
-
16
- /**
17
- * An undo/redo event without attached state.
18
- */
19
- export type UndoEventWithoutAttachedState = UndoSingleEvent | UndoEventGroup
20
-
21
- /**
22
- * An undo/redo event.
23
- */
24
- export type UndoEvent = UndoEventWithoutAttachedState & {
25
- /**
26
- * The state saved before the event actions started / after the event actions finished.
27
- */
28
- attachedState: {
29
- /**
30
- * The state saved before the event actions started.
31
- */
32
- beforeEvent?: unknown
33
- /**
34
- * The state saved after the event actions finished.
35
- */
36
- afterEvent?: unknown
37
- }
38
- }
39
-
40
- /**
41
- * Undo event type.
42
- */
43
- export enum UndoEventType {
44
- Single = "single",
45
- Group = "group",
46
- }
47
-
48
- /**
49
- * An undo/redo single event.
50
- */
51
- export interface UndoSingleEvent {
52
- /**
53
- * Expresses this is a single event.
54
- */
55
- readonly type: UndoEventType.Single
56
- /**
57
- * Path to the object that invoked the action from its root.
58
- */
59
- readonly targetPath: Path
60
- /**
61
- * Name of the action that was invoked.
62
- */
63
- readonly actionName: string
64
- /**
65
- * Patches with changes done inside the action.
66
- * Use `redo()` in the `UndoManager` to apply them.
67
- */
68
- readonly patches: ReadonlyArray<Patch>
69
- /**
70
- * Patches to undo the changes done inside the action.
71
- * Use `undo()` in the `UndoManager` to apply them.
72
- */
73
- readonly inversePatches: ReadonlyArray<Patch>
74
- }
75
-
76
- /**
77
- * An undo/redo event group.
78
- */
79
- export interface UndoEventGroup {
80
- /**
81
- * Expresses this is an event group.
82
- */
83
- readonly type: UndoEventType.Group
84
- /**
85
- * Name of the group (if any).
86
- */
87
- readonly groupName?: string
88
- /**
89
- * Events that conform this group (might be single events or other nested groups).
90
- */
91
- readonly events: ReadonlyArray<UndoEventWithoutAttachedState>
92
- }
93
-
94
- function toSingleEvents(
95
- event: UndoEventWithoutAttachedState,
96
- reverse: boolean
97
- ): ReadonlyArray<UndoSingleEvent> {
98
- if (event.type === UndoEventType.Single) {
99
- return [event]
100
- } else {
101
- const array: UndoSingleEvent[] = []
102
- for (const e of event.events) {
103
- if (reverse) {
104
- array.unshift(...toSingleEvents(e, true))
105
- } else {
106
- array.push(...toSingleEvents(e, false))
107
- }
108
- }
109
- return array
110
- }
111
- }
112
-
113
- /**
114
- * Store model instance for undo/redo actions.
115
- * Do not manipulate directly, other that creating it.
116
- */
117
- @model(`${namespace}/UndoStore`)
118
- export class UndoStore extends Model({
119
- // TODO: add proper type checking to undo store
120
- undoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
121
- redoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
122
- }) {
123
- /**
124
- * @ignore
125
- */
126
- @modelAction
127
- _clearUndo() {
128
- withoutUndo(() => {
129
- this.undoEvents.length = 0
130
- })
131
- }
132
-
133
- /**
134
- * @ignore
135
- */
136
- @modelAction
137
- _clearRedo() {
138
- withoutUndo(() => {
139
- this.redoEvents.length = 0
140
- })
141
- }
142
-
143
- /**
144
- * @ignore
145
- */
146
- enforceMaxLevels({
147
- maxUndoLevels,
148
- maxRedoLevels,
149
- }: {
150
- maxUndoLevels?: number
151
- maxRedoLevels?: number
152
- }) {
153
- if (maxUndoLevels !== undefined) {
154
- while (this.undoEvents.length > maxUndoLevels) {
155
- this.undoEvents.shift()
156
- }
157
- }
158
- if (maxRedoLevels !== undefined) {
159
- while (this.redoEvents.length > maxRedoLevels) {
160
- this.redoEvents.shift()
161
- }
162
- }
163
- }
164
-
165
- /**
166
- * @ignore
167
- */
168
- @modelAction
169
- _undo({ maxRedoLevels }: { maxRedoLevels: number | undefined }) {
170
- withoutUndo(() => {
171
- const event = this.undoEvents.pop()!
172
- this.redoEvents.push(event)
173
- this.enforceMaxLevels({ maxRedoLevels })
174
- })
175
- }
176
-
177
- /**
178
- * @ignore
179
- */
180
- @modelAction
181
- _redo({ maxUndoLevels }: { maxUndoLevels: number | undefined }) {
182
- withoutUndo(() => {
183
- const event = this.redoEvents.pop()!
184
- this.undoEvents.push(event)
185
- this.enforceMaxLevels({ maxUndoLevels })
186
- })
187
- }
188
-
189
- /**
190
- * @ignore
191
- */
192
- @modelAction
193
- _addUndo({ event, maxUndoLevels }: { event: UndoEvent; maxUndoLevels: number | undefined }) {
194
- withoutUndo(() => {
195
- this.undoEvents.push(event)
196
- // once an undo event is added redo queue is no longer valid
197
- this.redoEvents.length = 0
198
- this.enforceMaxLevels({ maxUndoLevels })
199
- })
200
- }
201
-
202
- private _groupStack: UndoEventGroup[] = []
203
-
204
- /**
205
- * @ignore
206
- */
207
- _addUndoToParentGroup(parentGroup: UndoEventGroup, event: UndoEventWithoutAttachedState) {
208
- ;(parentGroup.events as UndoEventWithoutAttachedState[]).push(event)
209
- }
210
-
211
- /**
212
- * @ignore
213
- */
214
- get _currentGroup(): UndoEventGroup | undefined {
215
- return this._groupStack[this._groupStack.length - 1]
216
- }
217
-
218
- /**
219
- * @ignore
220
- */
221
- _startGroup(
222
- groupName: string | undefined,
223
- startRunning: boolean,
224
- options: UndoMiddlewareOptions<unknown> | undefined
225
- ) {
226
- let running = false
227
- let ended = false
228
- const parentGroup = this._currentGroup
229
-
230
- const group: UndoEventGroup = {
231
- type: UndoEventType.Group,
232
- groupName,
233
- events: [],
234
- }
235
-
236
- const attachedStateBeforeEvent = parentGroup ? undefined : options?.attachedState?.save()
237
-
238
- const api = {
239
- pause: () => {
240
- if (ended) {
241
- throw failure("cannot pause a group when it is already ended")
242
- }
243
- if (!running) {
244
- throw failure("cannot pause a group when it is not running")
245
- }
246
- if (this._currentGroup !== group) {
247
- throw failure("group out of order")
248
- }
249
- this._groupStack.pop()
250
- running = false
251
- },
252
- resume: () => {
253
- if (ended) {
254
- throw failure("cannot resume a group when it is already ended")
255
- }
256
- if (running) {
257
- throw failure("cannot resume a group when it is already running")
258
- }
259
- this._groupStack.push(group)
260
- running = true
261
- },
262
- end: () => {
263
- if (running) {
264
- api.pause()
265
- }
266
- ended = true
267
- if (parentGroup) {
268
- this._addUndoToParentGroup(parentGroup, group)
269
- } else {
270
- this._addUndo({
271
- event: {
272
- ...group,
273
- attachedState: {
274
- beforeEvent: attachedStateBeforeEvent,
275
- afterEvent: options?.attachedState?.save(),
276
- },
277
- },
278
- maxUndoLevels: options?.maxUndoLevels,
279
- })
280
- }
281
- },
282
- }
283
-
284
- if (startRunning) {
285
- api.resume()
286
- }
287
-
288
- return api
289
- }
290
- }
291
-
292
- /**
293
- * Manager class returned by `undoMiddleware` that allows you to perform undo/redo actions.
294
- */
295
- export class UndoManager {
296
- /**
297
- * The store currently being used to store undo/redo action events.
298
- */
299
- readonly store: UndoStore
300
-
301
- /**
302
- * The undo stack, where the first operation to undo will be the last of the array.
303
- * Do not manipulate this array directly.
304
- */
305
- @computed
306
- get undoQueue(): ReadonlyArray<UndoEvent> {
307
- return this.store.undoEvents
308
- }
309
-
310
- /**
311
- * The redo stack, where the first operation to redo will be the last of the array.
312
- * Do not manipulate this array directly.
313
- */
314
- @computed
315
- get redoQueue(): ReadonlyArray<UndoEvent> {
316
- return this.store.redoEvents
317
- }
318
-
319
- /**
320
- * The number of undo actions available.
321
- */
322
- @computed
323
- get undoLevels() {
324
- return this.undoQueue.length
325
- }
326
-
327
- /**
328
- * If undo can be performed (if there is at least one undo action available).
329
- */
330
- @computed
331
- get canUndo() {
332
- return this.undoLevels > 0
333
- }
334
-
335
- /**
336
- * Clears the undo queue.
337
- */
338
- @action
339
- clearUndo() {
340
- this.store._clearUndo()
341
- }
342
-
343
- /**
344
- * The number of redo actions available.
345
- */
346
- @computed
347
- get redoLevels() {
348
- return this.redoQueue.length
349
- }
350
-
351
- /**
352
- * If redo can be performed (if there is at least one redo action available)
353
- */
354
- @computed
355
- get canRedo() {
356
- return this.redoLevels > 0
357
- }
358
-
359
- /**
360
- * Clears the redo queue.
361
- */
362
- @action
363
- clearRedo() {
364
- this.store._clearRedo()
365
- }
366
-
367
- /**
368
- * Undoes the last action.
369
- * Will throw if there is no action to undo.
370
- */
371
- @action
372
- undo() {
373
- if (!this.canUndo) {
374
- throw failure("nothing to undo")
375
- }
376
- const event = this.undoQueue[this.undoQueue.length - 1]
377
-
378
- withoutUndo(() => {
379
- toSingleEvents(event, true).forEach((e) => {
380
- applyPatches(this.subtreeRoot, e.inversePatches, true)
381
- })
382
-
383
- // restore the attached state before the operation was made
384
- if (event.attachedState?.beforeEvent) {
385
- this.options?.attachedState?.restore(event.attachedState.beforeEvent)
386
- }
387
- })
388
-
389
- this.store._undo({ maxRedoLevels: this.options?.maxRedoLevels })
390
- }
391
-
392
- /**
393
- * Redoes the previous action.
394
- * Will throw if there is no action to redo.
395
- */
396
- @action
397
- redo() {
398
- if (!this.canRedo) {
399
- throw failure("nothing to redo")
400
- }
401
- const event = this.redoQueue[this.redoQueue.length - 1]
402
-
403
- withoutUndo(() => {
404
- toSingleEvents(event, false).forEach((e) => {
405
- applyPatches(this.subtreeRoot, e.patches)
406
- })
407
-
408
- // restore the attached state after the operation was made
409
- if (event.attachedState?.afterEvent) {
410
- this.options?.attachedState?.restore(event.attachedState.afterEvent)
411
- }
412
- })
413
-
414
- this.store._redo({ maxUndoLevels: this.options?.maxUndoLevels })
415
- }
416
-
417
- /**
418
- * Disposes the undo middleware.
419
- */
420
- dispose() {
421
- this.disposer()
422
- }
423
-
424
- private _isUndoRecordingDisabled = false
425
-
426
- /**
427
- * Returns if undo recording is currently disabled or not for this particular `UndoManager`.
428
- */
429
- get isUndoRecordingDisabled() {
430
- return this._isUndoRecordingDisabled
431
- }
432
-
433
- /**
434
- * Skips the undo recording mechanism for the code block that gets run synchronously inside.
435
- *
436
- * @template T Code block return type.
437
- * @param fn Code block to run.
438
- * @returns The value returned by the code block.
439
- */
440
- withoutUndo<T>(fn: () => T): T {
441
- const savedUndoDisabled = this._isUndoRecordingDisabled
442
- this._isUndoRecordingDisabled = true
443
- try {
444
- return fn()
445
- } finally {
446
- this._isUndoRecordingDisabled = savedUndoDisabled
447
- }
448
- }
449
-
450
- /**
451
- * Creates a custom group that can be continued multiple times and then ended.
452
- * @param groupName Optional group name.
453
- * @returns An API to continue/end the group.
454
- */
455
- createGroup(groupName?: string) {
456
- const group = this.store._startGroup(groupName, false, this.options)
457
-
458
- return {
459
- continue<T>(fn: () => T): T {
460
- group.resume()
461
- try {
462
- return fn()
463
- } finally {
464
- group.pause()
465
- }
466
- },
467
-
468
- end() {
469
- group.end()
470
- },
471
- }
472
- }
473
-
474
- /**
475
- * Runs a synchronous code block as an undo group.
476
- * Note that nested groups are allowed.
477
- *
478
- * @param groupName Group name.
479
- * @param fn Code block.
480
- * @returns Code block return value.
481
- */
482
- withGroup<T>(groupName: string, fn: () => T): T
483
-
484
- /**
485
- * Runs a synchronous code block as an undo group.
486
- * Note that nested groups are allowed.
487
- *
488
- * @param fn Code block.
489
- * @returns Code block return value.
490
- */
491
- withGroup<T>(fn: () => T): T
492
-
493
- withGroup<T>(arg1: any, arg2?: any): T {
494
- let groupName: string | undefined
495
- let fn: () => T
496
- if (typeof arg1 === "string") {
497
- groupName = arg1
498
- fn = arg2
499
- } else {
500
- fn = arg1
501
- }
502
-
503
- const group = this.store._startGroup(groupName, true, this.options)
504
- try {
505
- return fn()
506
- } finally {
507
- group.end()
508
- }
509
- }
510
-
511
- /**
512
- * Runs an asynchronous code block as an undo group.
513
- * Note that nested groups are allowed.
514
- *
515
- * @param groupName Group name.
516
- * @param fn Flow function.
517
- * @returns Flow function return value.
518
- */
519
- withGroupFlow<R>(groupName: string, fn: () => Generator<any, R, any>): Promise<R>
520
-
521
- /**
522
- * Runs an asynchronous code block as an undo group.
523
- * Note that nested groups are allowed.
524
- *
525
- * @param fn Flow function.
526
- * @returns Flow function return value.
527
- */
528
- withGroupFlow<R>(fn: () => Generator<any, R, any>): Promise<R>
529
-
530
- withGroupFlow<R>(arg1: any, arg2?: any): Promise<R> {
531
- let groupName: string | undefined
532
- let fn: () => Generator<any, R, any>
533
- if (typeof arg1 === "string") {
534
- groupName = arg1
535
- fn = arg2
536
- } else {
537
- fn = arg1
538
- }
539
-
540
- const gen = fn()
541
-
542
- const group = this.store._startGroup(groupName, false, this.options)
543
-
544
- // use bound functions to fix es6 compilation
545
- const genNext = gen.next.bind(gen)
546
- const genThrow = gen.throw.bind(gen)
547
-
548
- const promise = new Promise<R>((resolve, reject) => {
549
- function onFulfilled(res: any): void {
550
- group.resume()
551
- let ret: unknown
552
- try {
553
- ret = genNext(res)
554
- } catch (e) {
555
- group.end()
556
- reject(e as Error)
557
- return
558
- }
559
-
560
- group.pause()
561
- next(ret)
562
- }
563
-
564
- function onRejected(err: unknown): void {
565
- group.resume()
566
- let ret: unknown
567
- try {
568
- ret = genThrow(err)
569
- } catch (e) {
570
- group.end()
571
- reject(e as Error)
572
- return
573
- }
574
-
575
- group.pause()
576
- next(ret)
577
- }
578
-
579
- function next(ret: any): void {
580
- if (ret && typeof ret.then === "function") {
581
- // an async iterator
582
- ret.then(next, reject)
583
- } else if (ret.done) {
584
- // done
585
- group.end()
586
- resolve(ret.value)
587
- } else {
588
- // continue
589
- Promise.resolve(ret.value).then(onFulfilled, onRejected)
590
- }
591
- }
592
-
593
- onFulfilled(undefined) // kick off the process
594
- })
595
-
596
- return promise
597
- }
598
-
599
- /**
600
- * Creates an instance of `UndoManager`.
601
- * Do not use directly, use `undoMiddleware` instead.
602
- *
603
- * @param disposer
604
- * @param subtreeRoot
605
- * @param [store]
606
- */
607
- constructor(
608
- private readonly disposer: ActionMiddlewareDisposer,
609
- private readonly subtreeRoot: object,
610
- store: UndoStore | undefined,
611
- private readonly options: UndoMiddlewareOptions<unknown> | undefined
612
- ) {
613
- if (getMobxVersion() >= 6) {
614
- mobx6.makeObservable(this)
615
- }
616
-
617
- this.store = store ?? new UndoStore({})
618
- }
619
- }
620
-
621
- /**
622
- * Undo middleware options.
623
- */
624
- export interface UndoMiddlewareOptions<S> {
625
- /**
626
- * Max number of undo levels to keep, or undefined for infinite.
627
- */
628
- maxUndoLevels?: number
629
-
630
- /**
631
- * Max number of redo levels to keep, or undefined for infinite.
632
- */
633
- maxRedoLevels?: number
634
-
635
- /**
636
- * Attached states are states that are saved/restored when undoing/redoing.
637
- * Usually used to restore state that is not part of the document model such as focus, selection, scroll position, etc.
638
- */
639
- attachedState?: {
640
- /**
641
- * Saves a certain state and associates it with the undo step. This state can be restored when undoing/redoing.
642
- */
643
- save(): S
644
- /**
645
- * Restores a certain state previously saved.
646
- * @param s State to restore.
647
- */
648
- restore(s: S): void
649
- }
650
- }
651
-
652
- /**
653
- * Creates an undo middleware.
654
- *
655
- * @param subtreeRoot Subtree root target object.
656
- * @param store Optional `UndoStore` where to store the undo/redo queues. Use this if you want to
657
- * store such queues somewhere in your models. If none is provided it will reside in memory.
658
- * @param options Extra options, such as how to save / restore certain snapshot of the state to be restored when undoing/redoing.
659
- * @returns An `UndoManager` which allows you to do the manage the undo/redo operations and dispose of the middleware.
660
- */
661
- export function undoMiddleware<S>(
662
- subtreeRoot: object,
663
- store?: UndoStore,
664
- options?: UndoMiddlewareOptions<S>
665
- ): UndoManager {
666
- assertTweakedObject(subtreeRoot, "subtreeRoot")
667
-
668
- let manager: UndoManager
669
-
670
- interface PatchRecorderData {
671
- recorder: PatchRecorder
672
- recorderStack: number
673
- undoRootContext: SimpleActionContext
674
- group: UndoEventGroup | undefined
675
- attachedStateBeforeEvent: S | undefined
676
- }
677
-
678
- const patchRecorderSymbol = Symbol("patchRecorder")
679
-
680
- function initPatchRecorder(ctx: SimpleActionContext) {
681
- const group = manager.store._currentGroup
682
-
683
- const patchRecorderData: PatchRecorderData = {
684
- recorder: patchRecorder(subtreeRoot, {
685
- recording: false,
686
- filter: () => {
687
- return !(_isGlobalUndoRecordingDisabled || manager.isUndoRecordingDisabled)
688
- },
689
- }),
690
- recorderStack: 0,
691
- undoRootContext: ctx,
692
- group,
693
-
694
- attachedStateBeforeEvent: options?.attachedState?.save(),
695
- }
696
-
697
- ctx.rootContext.data[patchRecorderSymbol] = patchRecorderData
698
- }
699
-
700
- function getPatchRecorderData(ctx: SimpleActionContext): PatchRecorderData {
701
- return ctx.rootContext.data[patchRecorderSymbol]
702
- }
703
-
704
- const middlewareDisposer = actionTrackingMiddleware(subtreeRoot, {
705
- onStart(ctx) {
706
- if (!getPatchRecorderData(ctx)) {
707
- initPatchRecorder(ctx)
708
- }
709
- },
710
- onResume(ctx) {
711
- const patchRecorderData = getPatchRecorderData(ctx)
712
- patchRecorderData.recorderStack++
713
- patchRecorderData.recorder.recording = patchRecorderData.recorderStack > 0
714
- },
715
- onSuspend(ctx) {
716
- const patchRecorderData = getPatchRecorderData(ctx)
717
- patchRecorderData.recorderStack--
718
- patchRecorderData.recorder.recording = patchRecorderData.recorderStack > 0
719
- },
720
- onFinish(ctx) {
721
- const patchRecorderData = getPatchRecorderData(ctx)
722
- if (patchRecorderData && patchRecorderData.undoRootContext === ctx) {
723
- const patchRecorder = patchRecorderData.recorder
724
-
725
- if (patchRecorder.events.length > 0) {
726
- const patches: Patch[] = []
727
- const inversePatches: Patch[] = []
728
-
729
- for (const event of patchRecorder.events) {
730
- patches.push(...event.patches)
731
- inversePatches.push(...event.inversePatches)
732
- }
733
-
734
- const event = {
735
- type: UndoEventType.Single,
736
- targetPath: fastGetRootPath(ctx.target, false).path,
737
- actionName: ctx.actionName,
738
- patches,
739
- inversePatches,
740
- } as const
741
-
742
- const parentGroup = patchRecorderData.group
743
-
744
- if (parentGroup) {
745
- manager.store._addUndoToParentGroup(parentGroup, event)
746
- } else {
747
- manager.store._addUndo({
748
- event: {
749
- ...event,
750
- attachedState: {
751
- beforeEvent: patchRecorderData.attachedStateBeforeEvent,
752
- afterEvent: options?.attachedState?.save(),
753
- },
754
- },
755
- maxUndoLevels: options?.maxUndoLevels,
756
- })
757
- }
758
- }
759
-
760
- patchRecorder.dispose()
761
- }
762
- },
763
- })
764
-
765
- manager = new UndoManager(middlewareDisposer, subtreeRoot, store, options)
766
- return manager
767
- }
768
-
769
- let _isGlobalUndoRecordingDisabled = false
770
-
771
- /**
772
- * Returns if the undo recording mechanism is currently disabled.
773
- *
774
- * @returns `true` if it is currently disabled, `false` otherwise.
775
- */
776
- export function isGlobalUndoRecordingDisabled() {
777
- return _isGlobalUndoRecordingDisabled
778
- }
779
-
780
- /**
781
- * Globally skips the undo recording mechanism for the code block that gets run synchronously inside.
782
- * Consider using the `withoutUndo` method of a particular `UndoManager` instead.
783
- *
784
- * @template T Code block return type.
785
- * @param fn Code block to run.
786
- * @returns The value returned by the code block.
787
- */
788
- export function withoutUndo<T>(fn: () => T): T {
789
- const savedUndoDisabled = _isGlobalUndoRecordingDisabled
790
- _isGlobalUndoRecordingDisabled = true
791
- try {
792
- return fn()
793
- } finally {
794
- _isGlobalUndoRecordingDisabled = savedUndoDisabled
795
- }
796
- }
1
+ import { action, computed } from "mobx"
2
+ import type { ActionMiddlewareDisposer } from "../action/middleware"
3
+ import { modelAction } from "../action/modelAction"
4
+ import { Model } from "../model/Model"
5
+ import { model } from "../modelShared/modelDecorator"
6
+ import { fastGetRootPath } from "../parent/path"
7
+ import type { Path } from "../parent/pathTypes"
8
+ import { applyPatches, type Patch, type PatchRecorder, patchRecorder } from "../patch"
9
+ import { assertTweakedObject } from "../tweaker/core"
10
+ import { typesArray } from "../types/arrayBased/typesArray"
11
+ import { tProp } from "../types/tProp"
12
+ import { typesUnchecked } from "../types/utility/typesUnchecked"
13
+ import { failure, getMobxVersion, mobx6, namespace } from "../utils"
14
+ import { actionTrackingMiddleware, type SimpleActionContext } from "./actionTrackingMiddleware"
15
+
16
+ /**
17
+ * An undo/redo event without attached state.
18
+ */
19
+ export type UndoEventWithoutAttachedState = UndoSingleEvent | UndoEventGroup
20
+
21
+ /**
22
+ * An undo/redo event.
23
+ */
24
+ export type UndoEvent = UndoEventWithoutAttachedState & {
25
+ /**
26
+ * The state saved before the event actions started / after the event actions finished.
27
+ */
28
+ attachedState: {
29
+ /**
30
+ * The state saved before the event actions started.
31
+ */
32
+ beforeEvent?: unknown
33
+ /**
34
+ * The state saved after the event actions finished.
35
+ */
36
+ afterEvent?: unknown
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Undo event type.
42
+ */
43
+ export enum UndoEventType {
44
+ Single = "single",
45
+ Group = "group",
46
+ }
47
+
48
+ /**
49
+ * An undo/redo single event.
50
+ */
51
+ export interface UndoSingleEvent {
52
+ /**
53
+ * Expresses this is a single event.
54
+ */
55
+ readonly type: UndoEventType.Single
56
+ /**
57
+ * Path to the object that invoked the action from its root.
58
+ */
59
+ readonly targetPath: Path
60
+ /**
61
+ * Name of the action that was invoked.
62
+ */
63
+ readonly actionName: string
64
+ /**
65
+ * Patches with changes done inside the action.
66
+ * Use `redo()` in the `UndoManager` to apply them.
67
+ */
68
+ readonly patches: ReadonlyArray<Patch>
69
+ /**
70
+ * Patches to undo the changes done inside the action.
71
+ * Use `undo()` in the `UndoManager` to apply them.
72
+ */
73
+ readonly inversePatches: ReadonlyArray<Patch>
74
+ }
75
+
76
+ /**
77
+ * An undo/redo event group.
78
+ */
79
+ export interface UndoEventGroup {
80
+ /**
81
+ * Expresses this is an event group.
82
+ */
83
+ readonly type: UndoEventType.Group
84
+ /**
85
+ * Name of the group (if any).
86
+ */
87
+ readonly groupName?: string
88
+ /**
89
+ * Events that conform this group (might be single events or other nested groups).
90
+ */
91
+ readonly events: ReadonlyArray<UndoEventWithoutAttachedState>
92
+ }
93
+
94
+ function toSingleEvents(
95
+ event: UndoEventWithoutAttachedState,
96
+ reverse: boolean
97
+ ): ReadonlyArray<UndoSingleEvent> {
98
+ if (event.type === UndoEventType.Single) {
99
+ return [event]
100
+ } else {
101
+ const array: UndoSingleEvent[] = []
102
+ for (const e of event.events) {
103
+ if (reverse) {
104
+ array.unshift(...toSingleEvents(e, true))
105
+ } else {
106
+ array.push(...toSingleEvents(e, false))
107
+ }
108
+ }
109
+ return array
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Store model instance for undo/redo actions.
115
+ * Do not manipulate directly, other that creating it.
116
+ */
117
+ @model(`${namespace}/UndoStore`)
118
+ export class UndoStore extends Model({
119
+ // TODO: add proper type checking to undo store
120
+ undoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
121
+ redoEvents: tProp(typesArray(typesUnchecked<UndoEvent>()), () => []),
122
+ }) {
123
+ /**
124
+ * @ignore
125
+ */
126
+ @modelAction
127
+ _clearUndo() {
128
+ withoutUndo(() => {
129
+ this.undoEvents.length = 0
130
+ })
131
+ }
132
+
133
+ /**
134
+ * @ignore
135
+ */
136
+ @modelAction
137
+ _clearRedo() {
138
+ withoutUndo(() => {
139
+ this.redoEvents.length = 0
140
+ })
141
+ }
142
+
143
+ /**
144
+ * @ignore
145
+ */
146
+ enforceMaxLevels({
147
+ maxUndoLevels,
148
+ maxRedoLevels,
149
+ }: {
150
+ maxUndoLevels?: number
151
+ maxRedoLevels?: number
152
+ }) {
153
+ if (maxUndoLevels !== undefined) {
154
+ while (this.undoEvents.length > maxUndoLevels) {
155
+ this.undoEvents.shift()
156
+ }
157
+ }
158
+ if (maxRedoLevels !== undefined) {
159
+ while (this.redoEvents.length > maxRedoLevels) {
160
+ this.redoEvents.shift()
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * @ignore
167
+ */
168
+ @modelAction
169
+ _undo({ maxRedoLevels }: { maxRedoLevels: number | undefined }) {
170
+ withoutUndo(() => {
171
+ const event = this.undoEvents.pop()!
172
+ this.redoEvents.push(event)
173
+ this.enforceMaxLevels({ maxRedoLevels })
174
+ })
175
+ }
176
+
177
+ /**
178
+ * @ignore
179
+ */
180
+ @modelAction
181
+ _redo({ maxUndoLevels }: { maxUndoLevels: number | undefined }) {
182
+ withoutUndo(() => {
183
+ const event = this.redoEvents.pop()!
184
+ this.undoEvents.push(event)
185
+ this.enforceMaxLevels({ maxUndoLevels })
186
+ })
187
+ }
188
+
189
+ /**
190
+ * @ignore
191
+ */
192
+ @modelAction
193
+ _addUndo({ event, maxUndoLevels }: { event: UndoEvent; maxUndoLevels: number | undefined }) {
194
+ withoutUndo(() => {
195
+ this.undoEvents.push(event)
196
+ // once an undo event is added redo queue is no longer valid
197
+ this.redoEvents.length = 0
198
+ this.enforceMaxLevels({ maxUndoLevels })
199
+ })
200
+ }
201
+
202
+ private _groupStack: UndoEventGroup[] = []
203
+
204
+ /**
205
+ * @ignore
206
+ */
207
+ _addUndoToParentGroup(parentGroup: UndoEventGroup, event: UndoEventWithoutAttachedState) {
208
+ ;(parentGroup.events as UndoEventWithoutAttachedState[]).push(event)
209
+ }
210
+
211
+ /**
212
+ * @ignore
213
+ */
214
+ get _currentGroup(): UndoEventGroup | undefined {
215
+ return this._groupStack[this._groupStack.length - 1]
216
+ }
217
+
218
+ /**
219
+ * @ignore
220
+ */
221
+ _startGroup(
222
+ groupName: string | undefined,
223
+ startRunning: boolean,
224
+ options: UndoMiddlewareOptions<unknown> | undefined
225
+ ) {
226
+ let running = false
227
+ let ended = false
228
+ const parentGroup = this._currentGroup
229
+
230
+ const group: UndoEventGroup = {
231
+ type: UndoEventType.Group,
232
+ groupName,
233
+ events: [],
234
+ }
235
+
236
+ const attachedStateBeforeEvent = parentGroup ? undefined : options?.attachedState?.save()
237
+
238
+ const api = {
239
+ pause: () => {
240
+ if (ended) {
241
+ throw failure("cannot pause a group when it is already ended")
242
+ }
243
+ if (!running) {
244
+ throw failure("cannot pause a group when it is not running")
245
+ }
246
+ if (this._currentGroup !== group) {
247
+ throw failure("group out of order")
248
+ }
249
+ this._groupStack.pop()
250
+ running = false
251
+ },
252
+ resume: () => {
253
+ if (ended) {
254
+ throw failure("cannot resume a group when it is already ended")
255
+ }
256
+ if (running) {
257
+ throw failure("cannot resume a group when it is already running")
258
+ }
259
+ this._groupStack.push(group)
260
+ running = true
261
+ },
262
+ end: () => {
263
+ if (running) {
264
+ api.pause()
265
+ }
266
+ ended = true
267
+ if (parentGroup) {
268
+ this._addUndoToParentGroup(parentGroup, group)
269
+ } else {
270
+ this._addUndo({
271
+ event: {
272
+ ...group,
273
+ attachedState: {
274
+ beforeEvent: attachedStateBeforeEvent,
275
+ afterEvent: options?.attachedState?.save(),
276
+ },
277
+ },
278
+ maxUndoLevels: options?.maxUndoLevels,
279
+ })
280
+ }
281
+ },
282
+ }
283
+
284
+ if (startRunning) {
285
+ api.resume()
286
+ }
287
+
288
+ return api
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Manager class returned by `undoMiddleware` that allows you to perform undo/redo actions.
294
+ */
295
+ export class UndoManager {
296
+ private readonly disposer: ActionMiddlewareDisposer
297
+ private readonly subtreeRoot: object
298
+ private readonly options: UndoMiddlewareOptions<unknown> | undefined
299
+
300
+ /**
301
+ * The store currently being used to store undo/redo action events.
302
+ */
303
+ readonly store: UndoStore
304
+
305
+ /**
306
+ * The undo stack, where the first operation to undo will be the last of the array.
307
+ * Do not manipulate this array directly.
308
+ */
309
+ @computed
310
+ get undoQueue(): ReadonlyArray<UndoEvent> {
311
+ return this.store.undoEvents
312
+ }
313
+
314
+ /**
315
+ * The redo stack, where the first operation to redo will be the last of the array.
316
+ * Do not manipulate this array directly.
317
+ */
318
+ @computed
319
+ get redoQueue(): ReadonlyArray<UndoEvent> {
320
+ return this.store.redoEvents
321
+ }
322
+
323
+ /**
324
+ * The number of undo actions available.
325
+ */
326
+ @computed
327
+ get undoLevels() {
328
+ return this.undoQueue.length
329
+ }
330
+
331
+ /**
332
+ * If undo can be performed (if there is at least one undo action available).
333
+ */
334
+ @computed
335
+ get canUndo() {
336
+ return this.undoLevels > 0
337
+ }
338
+
339
+ /**
340
+ * Clears the undo queue.
341
+ */
342
+ @action
343
+ clearUndo() {
344
+ this.store._clearUndo()
345
+ }
346
+
347
+ /**
348
+ * The number of redo actions available.
349
+ */
350
+ @computed
351
+ get redoLevels() {
352
+ return this.redoQueue.length
353
+ }
354
+
355
+ /**
356
+ * If redo can be performed (if there is at least one redo action available)
357
+ */
358
+ @computed
359
+ get canRedo() {
360
+ return this.redoLevels > 0
361
+ }
362
+
363
+ /**
364
+ * Clears the redo queue.
365
+ */
366
+ @action
367
+ clearRedo() {
368
+ this.store._clearRedo()
369
+ }
370
+
371
+ /**
372
+ * Undoes the last action.
373
+ * Will throw if there is no action to undo.
374
+ */
375
+ @action
376
+ undo() {
377
+ if (!this.canUndo) {
378
+ throw failure("nothing to undo")
379
+ }
380
+ const event = this.undoQueue[this.undoQueue.length - 1]
381
+
382
+ withoutUndo(() => {
383
+ toSingleEvents(event, true).forEach((e) => {
384
+ applyPatches(this.subtreeRoot, e.inversePatches, true)
385
+ })
386
+
387
+ // restore the attached state before the operation was made
388
+ if (event.attachedState?.beforeEvent) {
389
+ this.options?.attachedState?.restore(event.attachedState.beforeEvent)
390
+ }
391
+ })
392
+
393
+ this.store._undo({ maxRedoLevels: this.options?.maxRedoLevels })
394
+ }
395
+
396
+ /**
397
+ * Redoes the previous action.
398
+ * Will throw if there is no action to redo.
399
+ */
400
+ @action
401
+ redo() {
402
+ if (!this.canRedo) {
403
+ throw failure("nothing to redo")
404
+ }
405
+ const event = this.redoQueue[this.redoQueue.length - 1]
406
+
407
+ withoutUndo(() => {
408
+ toSingleEvents(event, false).forEach((e) => {
409
+ applyPatches(this.subtreeRoot, e.patches)
410
+ })
411
+
412
+ // restore the attached state after the operation was made
413
+ if (event.attachedState?.afterEvent) {
414
+ this.options?.attachedState?.restore(event.attachedState.afterEvent)
415
+ }
416
+ })
417
+
418
+ this.store._redo({ maxUndoLevels: this.options?.maxUndoLevels })
419
+ }
420
+
421
+ /**
422
+ * Disposes the undo middleware.
423
+ */
424
+ dispose() {
425
+ this.disposer()
426
+ }
427
+
428
+ private _isUndoRecordingDisabled = false
429
+
430
+ /**
431
+ * Returns if undo recording is currently disabled or not for this particular `UndoManager`.
432
+ */
433
+ get isUndoRecordingDisabled() {
434
+ return this._isUndoRecordingDisabled
435
+ }
436
+
437
+ /**
438
+ * Skips the undo recording mechanism for the code block that gets run synchronously inside.
439
+ *
440
+ * @template T Code block return type.
441
+ * @param fn Code block to run.
442
+ * @returns The value returned by the code block.
443
+ */
444
+ withoutUndo<T>(fn: () => T): T {
445
+ const savedUndoDisabled = this._isUndoRecordingDisabled
446
+ this._isUndoRecordingDisabled = true
447
+ try {
448
+ return fn()
449
+ } finally {
450
+ this._isUndoRecordingDisabled = savedUndoDisabled
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Creates a custom group that can be continued multiple times and then ended.
456
+ * @param groupName Optional group name.
457
+ * @returns An API to continue/end the group.
458
+ */
459
+ createGroup(groupName?: string) {
460
+ const group = this.store._startGroup(groupName, false, this.options)
461
+
462
+ return {
463
+ continue<T>(fn: () => T): T {
464
+ group.resume()
465
+ try {
466
+ return fn()
467
+ } finally {
468
+ group.pause()
469
+ }
470
+ },
471
+
472
+ end() {
473
+ group.end()
474
+ },
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Runs a synchronous code block as an undo group.
480
+ * Note that nested groups are allowed.
481
+ *
482
+ * @param groupName Group name.
483
+ * @param fn Code block.
484
+ * @returns Code block return value.
485
+ */
486
+ withGroup<T>(groupName: string, fn: () => T): T
487
+
488
+ /**
489
+ * Runs a synchronous code block as an undo group.
490
+ * Note that nested groups are allowed.
491
+ *
492
+ * @param fn Code block.
493
+ * @returns Code block return value.
494
+ */
495
+ withGroup<T>(fn: () => T): T
496
+
497
+ withGroup<T>(arg1: any, arg2?: any): T {
498
+ let groupName: string | undefined
499
+ let fn: () => T
500
+ if (typeof arg1 === "string") {
501
+ groupName = arg1
502
+ fn = arg2
503
+ } else {
504
+ fn = arg1
505
+ }
506
+
507
+ const group = this.store._startGroup(groupName, true, this.options)
508
+ try {
509
+ return fn()
510
+ } finally {
511
+ group.end()
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Runs an asynchronous code block as an undo group.
517
+ * Note that nested groups are allowed.
518
+ *
519
+ * @param groupName Group name.
520
+ * @param fn Flow function.
521
+ * @returns Flow function return value.
522
+ */
523
+ withGroupFlow<R>(groupName: string, fn: () => Generator<any, R, any>): Promise<R>
524
+
525
+ /**
526
+ * Runs an asynchronous code block as an undo group.
527
+ * Note that nested groups are allowed.
528
+ *
529
+ * @param fn Flow function.
530
+ * @returns Flow function return value.
531
+ */
532
+ withGroupFlow<R>(fn: () => Generator<any, R, any>): Promise<R>
533
+
534
+ withGroupFlow<R>(arg1: any, arg2?: any): Promise<R> {
535
+ let groupName: string | undefined
536
+ let fn: () => Generator<any, R, any>
537
+ if (typeof arg1 === "string") {
538
+ groupName = arg1
539
+ fn = arg2
540
+ } else {
541
+ fn = arg1
542
+ }
543
+
544
+ const gen = fn()
545
+
546
+ const group = this.store._startGroup(groupName, false, this.options)
547
+
548
+ // use bound functions to fix es6 compilation
549
+ const genNext = gen.next.bind(gen)
550
+ const genThrow = gen.throw.bind(gen)
551
+
552
+ const promise = new Promise<R>((resolve, reject) => {
553
+ function onFulfilled(res: any): void {
554
+ group.resume()
555
+ let ret: unknown
556
+ try {
557
+ ret = genNext(res)
558
+ } catch (e) {
559
+ group.end()
560
+ reject(e as Error)
561
+ return
562
+ }
563
+
564
+ group.pause()
565
+ next(ret)
566
+ }
567
+
568
+ function onRejected(err: unknown): void {
569
+ group.resume()
570
+ let ret: unknown
571
+ try {
572
+ ret = genThrow(err)
573
+ } catch (e) {
574
+ group.end()
575
+ reject(e as Error)
576
+ return
577
+ }
578
+
579
+ group.pause()
580
+ next(ret)
581
+ }
582
+
583
+ function next(ret: any): void {
584
+ if (ret && typeof ret.then === "function") {
585
+ // an async iterator
586
+ ret.then(next, reject)
587
+ } else if (ret.done) {
588
+ // done
589
+ group.end()
590
+ resolve(ret.value)
591
+ } else {
592
+ // continue
593
+ Promise.resolve(ret.value).then(onFulfilled, onRejected)
594
+ }
595
+ }
596
+
597
+ onFulfilled(undefined) // kick off the process
598
+ })
599
+
600
+ return promise
601
+ }
602
+
603
+ /**
604
+ * Creates an instance of `UndoManager`.
605
+ * Do not use directly, use `undoMiddleware` instead.
606
+ *
607
+ * @param disposer
608
+ * @param subtreeRoot
609
+ * @param [store]
610
+ */
611
+ constructor(
612
+ disposer: ActionMiddlewareDisposer,
613
+ subtreeRoot: object,
614
+ store: UndoStore | undefined,
615
+ options: UndoMiddlewareOptions<unknown> | undefined
616
+ ) {
617
+ this.disposer = disposer
618
+ this.subtreeRoot = subtreeRoot
619
+ this.options = options
620
+
621
+ if (getMobxVersion() >= 6) {
622
+ mobx6.makeObservable(this)
623
+ }
624
+
625
+ this.store = store ?? new UndoStore({})
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Undo middleware options.
631
+ */
632
+ export interface UndoMiddlewareOptions<S> {
633
+ /**
634
+ * Max number of undo levels to keep, or undefined for infinite.
635
+ */
636
+ maxUndoLevels?: number
637
+
638
+ /**
639
+ * Max number of redo levels to keep, or undefined for infinite.
640
+ */
641
+ maxRedoLevels?: number
642
+
643
+ /**
644
+ * Attached states are states that are saved/restored when undoing/redoing.
645
+ * Usually used to restore state that is not part of the document model such as focus, selection, scroll position, etc.
646
+ */
647
+ attachedState?: {
648
+ /**
649
+ * Saves a certain state and associates it with the undo step. This state can be restored when undoing/redoing.
650
+ */
651
+ save(): S
652
+ /**
653
+ * Restores a certain state previously saved.
654
+ * @param s State to restore.
655
+ */
656
+ restore(s: S): void
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Creates an undo middleware.
662
+ *
663
+ * @param subtreeRoot Subtree root target object.
664
+ * @param store Optional `UndoStore` where to store the undo/redo queues. Use this if you want to
665
+ * store such queues somewhere in your models. If none is provided it will reside in memory.
666
+ * @param options Extra options, such as how to save / restore certain snapshot of the state to be restored when undoing/redoing.
667
+ * @returns An `UndoManager` which allows you to do the manage the undo/redo operations and dispose of the middleware.
668
+ */
669
+ export function undoMiddleware<S>(
670
+ subtreeRoot: object,
671
+ store?: UndoStore,
672
+ options?: UndoMiddlewareOptions<S>
673
+ ): UndoManager {
674
+ assertTweakedObject(subtreeRoot, "subtreeRoot")
675
+
676
+ let manager: UndoManager
677
+
678
+ interface PatchRecorderData {
679
+ recorder: PatchRecorder
680
+ recorderStack: number
681
+ undoRootContext: SimpleActionContext
682
+ group: UndoEventGroup | undefined
683
+ attachedStateBeforeEvent: S | undefined
684
+ }
685
+
686
+ const patchRecorderSymbol = Symbol("patchRecorder")
687
+
688
+ function initPatchRecorder(ctx: SimpleActionContext) {
689
+ const group = manager.store._currentGroup
690
+
691
+ const patchRecorderData: PatchRecorderData = {
692
+ recorder: patchRecorder(subtreeRoot, {
693
+ recording: false,
694
+ filter: () => {
695
+ return !(_isGlobalUndoRecordingDisabled || manager.isUndoRecordingDisabled)
696
+ },
697
+ }),
698
+ recorderStack: 0,
699
+ undoRootContext: ctx,
700
+ group,
701
+
702
+ attachedStateBeforeEvent: options?.attachedState?.save(),
703
+ }
704
+
705
+ ctx.rootContext.data[patchRecorderSymbol] = patchRecorderData
706
+ }
707
+
708
+ function getPatchRecorderData(ctx: SimpleActionContext): PatchRecorderData {
709
+ return ctx.rootContext.data[patchRecorderSymbol]
710
+ }
711
+
712
+ const middlewareDisposer = actionTrackingMiddleware(subtreeRoot, {
713
+ onStart(ctx) {
714
+ if (!getPatchRecorderData(ctx)) {
715
+ initPatchRecorder(ctx)
716
+ }
717
+ },
718
+ onResume(ctx) {
719
+ const patchRecorderData = getPatchRecorderData(ctx)
720
+ patchRecorderData.recorderStack++
721
+ patchRecorderData.recorder.recording = patchRecorderData.recorderStack > 0
722
+ },
723
+ onSuspend(ctx) {
724
+ const patchRecorderData = getPatchRecorderData(ctx)
725
+ patchRecorderData.recorderStack--
726
+ patchRecorderData.recorder.recording = patchRecorderData.recorderStack > 0
727
+ },
728
+ onFinish(ctx) {
729
+ const patchRecorderData = getPatchRecorderData(ctx)
730
+ if (patchRecorderData && patchRecorderData.undoRootContext === ctx) {
731
+ const patchRecorder = patchRecorderData.recorder
732
+
733
+ if (patchRecorder.events.length > 0) {
734
+ const patches: Patch[] = []
735
+ const inversePatches: Patch[] = []
736
+
737
+ for (const event of patchRecorder.events) {
738
+ patches.push(...event.patches)
739
+ inversePatches.push(...event.inversePatches)
740
+ }
741
+
742
+ const event = {
743
+ type: UndoEventType.Single,
744
+ targetPath: fastGetRootPath(ctx.target, false).path,
745
+ actionName: ctx.actionName,
746
+ patches,
747
+ inversePatches,
748
+ } as const
749
+
750
+ const parentGroup = patchRecorderData.group
751
+
752
+ if (parentGroup) {
753
+ manager.store._addUndoToParentGroup(parentGroup, event)
754
+ } else {
755
+ manager.store._addUndo({
756
+ event: {
757
+ ...event,
758
+ attachedState: {
759
+ beforeEvent: patchRecorderData.attachedStateBeforeEvent,
760
+ afterEvent: options?.attachedState?.save(),
761
+ },
762
+ },
763
+ maxUndoLevels: options?.maxUndoLevels,
764
+ })
765
+ }
766
+ }
767
+
768
+ patchRecorder.dispose()
769
+ }
770
+ },
771
+ })
772
+
773
+ manager = new UndoManager(middlewareDisposer, subtreeRoot, store, options)
774
+ return manager
775
+ }
776
+
777
+ let _isGlobalUndoRecordingDisabled = false
778
+
779
+ /**
780
+ * Returns if the undo recording mechanism is currently disabled.
781
+ *
782
+ * @returns `true` if it is currently disabled, `false` otherwise.
783
+ */
784
+ export function isGlobalUndoRecordingDisabled() {
785
+ return _isGlobalUndoRecordingDisabled
786
+ }
787
+
788
+ /**
789
+ * Globally skips the undo recording mechanism for the code block that gets run synchronously inside.
790
+ * Consider using the `withoutUndo` method of a particular `UndoManager` instead.
791
+ *
792
+ * @template T Code block return type.
793
+ * @param fn Code block to run.
794
+ * @returns The value returned by the code block.
795
+ */
796
+ export function withoutUndo<T>(fn: () => T): T {
797
+ const savedUndoDisabled = _isGlobalUndoRecordingDisabled
798
+ _isGlobalUndoRecordingDisabled = true
799
+ try {
800
+ return fn()
801
+ } finally {
802
+ _isGlobalUndoRecordingDisabled = savedUndoDisabled
803
+ }
804
+ }