diff --git a/packages/interceptors/src/api.ts b/packages/interceptors/src/api.ts index e3074c0c2f..6429af2c90 100644 --- a/packages/interceptors/src/api.ts +++ b/packages/interceptors/src/api.ts @@ -25,19 +25,32 @@ export const FX_FETCH = "--fetch"; export const FX_STATE = "--state"; /** - * Currently unused + * Event ID to trigger redo action. + * See `EventBus.addBuiltIns()` for further details. + * Also see `snapshot()` interceptor docs. */ -export const FX_REDO = "--redo"; +export const EV_REDO = "--redo"; /** - * Currently unused + * Event ID to trigger undo action. + * See `EventBus.addBuiltIns()` for further details. + * Also see `snapshot()` interceptor docs. */ -export const FX_UNDO = "--undo"; +export const EV_UNDO = "--undo"; /** - * Currently unused + * Side effect ID to execute redo action. + * See `EventBus.addBuiltIns()` for further details. + * Also see `snapshot()` interceptor docs. */ -export const FX_UNDO_STORE = "--undo-store"; +export const FX_REDO = EV_REDO; + +/** + * Side effect ID to execute undo action. + * See `EventBus.addBuiltIns()` for further details. + * Also see `snapshot()` interceptor docs. + */ +export const FX_UNDO = EV_UNDO; export interface Event extends Array { [0]: PropertyKey; @@ -61,7 +74,6 @@ export interface InterceptorContext { [FX_DISPATCH]?: Event | Event[]; [FX_DISPATCH_NOW]?: Event | Event[]; [FX_DISPATCH_ASYNC]?: AsyncEffectDef | AsyncEffectDef[]; - [FX_UNDO_STORE]?: string | string[]; [FX_UNDO]?: string | string[]; [FX_REDO]?: string | string[]; [id: string]: any; diff --git a/packages/interceptors/src/event-bus.ts b/packages/interceptors/src/event-bus.ts index a88fa2f1f2..10c65fe17f 100644 --- a/packages/interceptors/src/event-bus.ts +++ b/packages/interceptors/src/event-bus.ts @@ -19,7 +19,7 @@ const FX_STATE = api.FX_STATE; * * Events processed by this class are simple 2-element tuples/arrays of * this form: `["event-id", payload?]`, where the `payload` is optional - * and can be any data. + * and can be of any type. * * Events are processed by registered handlers which transform each * event into a number of side effect descriptions to be executed later. @@ -83,7 +83,8 @@ export class StatelessEventBus implements * definitions (all optional). * * In addition to the user provided handlers & effects, a number of - * built-ins are added automatically. See `addBuiltIns()`. + * built-ins are added automatically. See `addBuiltIns()`. User + * handlers can override built-ins. * * @param handlers * @param effects @@ -500,7 +501,8 @@ export class EventBus extends StatelessEventBus implements * automatically creates an `Atom` with empty state object. * * In addition to the user provided handlers & effects, a number of - * built-ins are added automatically. See `addBuiltIns()`. + * built-ins are added automatically. See `addBuiltIns()`. User + * handlers can override built-ins. * * @param state * @param handlers @@ -521,7 +523,7 @@ export class EventBus extends StatelessEventBus implements /** * Adds same built-in event & side effect handlers as in - * `StatelessEventBus.addBuiltIns()` with the following additions: + * `StatelessEventBus.addBuiltIns()` and the following additions: * * ### Handlers * @@ -544,6 +546,16 @@ export class EventBus extends StatelessEventBus implements * [EV_UPDATE_VALUE, ["path.to.value", (x, y) => x + y, 1]] * ``` * + * #### `EV_UNDO` + * + * Triggers `FX_UNDO` side effect with context key `"history"`. See + * side effect description below. + * + * #### `EV_REDO` + * + * Triggers `FX_REDO` side effect with context key `"history"`. See + * side effect description below. + * * ### Side effects * * #### `FX_STATE` @@ -571,18 +583,20 @@ export class EventBus extends StatelessEventBus implements addBuiltIns(): any { super.addBuiltIns(); // handlers - this.addHandler(api.EV_SET_VALUE, - (state, [_, [path, val]]) => - ({ [FX_STATE]: setIn(state, path, val) })); - this.addHandler(api.EV_UPDATE_VALUE, - (state, [_, [path, fn, ...args]]) => - ({ [FX_STATE]: updateIn(state, path, fn, ...args) })); + this.addHandlers({ + [api.EV_SET_VALUE]: (state, [_, [path, val]]) => + ({ [FX_STATE]: setIn(state, path, val) }), + [api.EV_UPDATE_VALUE]: (state, [_, [path, fn, ...args]]) => + ({ [FX_STATE]: updateIn(state, path, fn, ...args) }), + [api.EV_UNDO]: () => ({ [api.FX_UNDO]: "history" }), + [api.EV_REDO]: () => ({ [api.FX_REDO]: "history" }), + }); // effects this.addEffects({ - [FX_STATE]: [(x) => this.state.reset(x), -1000], - [api.FX_UNDO]: [(x, _, ctx) => ctx[x].undo(), -1000], - [api.FX_REDO]: [(x, _, ctx) => ctx[x].redo(), -1000], + [FX_STATE]: [(state) => this.state.reset(state), -1000], + [api.FX_UNDO]: [(id, _, ctx) => ctx[id].undo(), -1001], + [api.FX_REDO]: [(id, _, ctx) => ctx[id].redo(), -1001], }); } @@ -598,6 +612,15 @@ export class EventBus extends StatelessEventBus implements * `InterceptorContext` object passed to each interceptor. Since the * merged object is also used to collect triggered side effects, * care must be taken that there're no key name clashes. + * + * In order to use the built-in `EV_UNDO`, `EV_REDO` events and + * their related side effects, users MUST provide a + * @thi.ng/atom History (or compatible undo history instance) via + * the `ctx` arg, e.g. + * + * ``` + * bus.processQueue({ history }); + * ``` */ processQueue(ctx?: api.InterceptorContext) { if (this.eventQueue.length > 0) { diff --git a/packages/interceptors/src/interceptors.ts b/packages/interceptors/src/interceptors.ts index 2527cee752..936d2c550c 100644 --- a/packages/interceptors/src/interceptors.ts +++ b/packages/interceptors/src/interceptors.ts @@ -10,13 +10,45 @@ export function trace(_, e) { } /** - * Higher-order interceptor. Return interceptor which unpacks payload + * Higher-order interceptor. Returns interceptor which unpacks payload * from event and assigns it as is to given side effect ID. * - * @param id side effect ID + * @param fxID side effect ID */ -export function forwardSideFx(id: string) { - return (_, [__, body]) => ({ [id]: body }); +export function forwardSideFx(fxID: string): InterceptorFn { + return (_, [__, body]) => ({ [fxID]: body }); +} + +/** + * Higher-order interceptor. Returns interceptor which calls + * `ctx[id].record()`, where `ctx` is the currently active + * `InterceptorContext` passed to all event handlers and `ctx[id]` is + * assumed to be a @thi.ng/atom `History` instance, passed to + * `processQueue()`. The default ID for the history instance is + * `"history"`. + * + * Example usage: + * + * ``` + * state = new Atom({}); + * history = new History(state); + * bus = new EventBus(state); + * // register event handler + * // each time the `foo` event is triggered, a snapshot of + * // current app state is recorded first + * bus.addHandlers({ + * foo: [snapshot(), valueSetter("foo")] + * undo: [forwardSideFx(FX_UNDO)] + * }); + * ... + * // pass history instance via interceptor context to handlers + * bus.processQueue({ history }); + * ``` + * + * @param id + */ +export function snapshot(id = "history"): InterceptorFn { + return (_, __, ___, ctx) => (ctx[id].record()); } /**