Skip to content

Commit

Permalink
feat(interceptors): add undo/redo handlers/fx & snapshot() interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Apr 15, 2018
1 parent a102eb7 commit 3c92f7e
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 24 deletions.
26 changes: 19 additions & 7 deletions packages/interceptors/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
[0]: PropertyKey;
Expand All @@ -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;
Expand Down
49 changes: 36 additions & 13 deletions packages/interceptors/src/event-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
*
Expand All @@ -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`
Expand Down Expand Up @@ -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],
});
}

Expand All @@ -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) {
Expand Down
40 changes: 36 additions & 4 deletions packages/interceptors/src/interceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand Down

0 comments on commit 3c92f7e

Please sign in to comment.