@thi.ng/interceptors 3.2.27 → 3.2.29

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/event-bus.js CHANGED
@@ -6,710 +6,647 @@ import { isPromise } from "@thi.ng/checks/is-promise";
6
6
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
7
7
  import { setInUnsafe } from "@thi.ng/paths/set-in";
8
8
  import { updateInUnsafe } from "@thi.ng/paths/update-in";
9
- import { EV_REDO, EV_SET_VALUE, EV_TOGGLE_VALUE, EV_UNDO, EV_UPDATE_VALUE, FX_CANCEL, FX_DELAY, FX_DISPATCH, FX_DISPATCH_ASYNC, FX_DISPATCH_NOW, FX_FETCH, FX_STATE, LOGGER, } from "./api.js";
10
- /**
11
- * Batched event processor for using composable interceptors for event
12
- * handling and side effects to execute the result of handled events.
13
- *
14
- * @remarks
15
- * Events processed by this class are simple 2-element tuples/arrays of
16
- * this form: `["event-id", payload?]`, where the `payload` is optional
17
- * and can be of any type.
18
- *
19
- * Events are processed by registered handlers which transform each
20
- * event into a number of side effect descriptions to be executed later.
21
- * This separation ensures event handlers themselves are pure functions
22
- * and leads to more efficient reuse of side effecting operations. The
23
- * pure data nature until the last stage of processing (the application
24
- * side effects) too means that event flow can be much easier inspected
25
- * and debugged.
26
- *
27
- * In this model a single event handler itself is an array of objects
28
- * with `pre` and/or `post` keys and functions attached to each key.
29
- * These functions are called interceptors, since each intercepts the
30
- * processing of an event and can contribute their own side effects.
31
- * Each event's interceptor chain is processed bi-directionally (`pre`
32
- * in forward, `post` in reverse order) and the effects returned from
33
- * each interceptor are merged/collected. The outcome of this setup is a
34
- * more aspect-oriented, composable approach to event handling and
35
- * allows to inject common, re-usable behaviors for multiple event types
36
- * (logging, validation, undo/redo triggers etc.).
37
- *
38
- * Side effects are only processed after all event handlers have run.
39
- * Furthermore, their order of execution can be configured with optional
40
- * priorities.
41
- *
42
- * See for further details:
43
- *
44
- * - {@link StatelessEventBus.processQueue}
45
- * - {@link StatelessEventBus.processEvent}
46
- * - {@link StatelessEventBus.processEffects}
47
- * - {@link StatelessEventBus.mergeEffects}
48
- *
49
- * The overall approach of this type of event processing is heavily
50
- * based on the pattern initially pioneered by @Day8/re-frame, with the
51
- * following differences:
52
- *
53
- * - stateless (see {@link EventBus} for the more common stateful
54
- * alternative)
55
- * - standalone implementation (no assumptions about surrounding
56
- * context/framework)
57
- * - manual control over event queue processing
58
- * - supports event cancellation (via FX_CANCEL side effect)
59
- * - side effect collection (multiple side effects for same effect type
60
- * per frame)
61
- * - side effect priorities (to control execution order)
62
- * - dynamic addition/removal of handlers & effects
63
- */
64
- export class StatelessEventBus {
65
- state;
66
- eventQueue;
67
- currQueue;
68
- currCtx;
69
- handlers;
70
- effects;
71
- priorities;
72
- /**
73
- * Creates a new event bus instance with given handler and effect
74
- * definitions (all optional).
75
- *
76
- * @remarks
77
- * In addition to the user provided handlers & effects, a number of
78
- * built-ins are added automatically. See
79
- * {@link StatelessEventBus.addBuiltIns}. User handlers can override
80
- * built-ins.
81
- *
82
- * @param handlers -
83
- * @param effects -
84
- */
85
- constructor(handlers, effects) {
86
- this.handlers = {};
87
- this.effects = {};
88
- this.eventQueue = [];
89
- this.priorities = [];
90
- this.addBuiltIns();
91
- if (handlers) {
92
- this.addHandlers(handlers);
93
- }
94
- if (effects) {
95
- this.addEffects(effects);
96
- }
9
+ import {
10
+ EV_REDO,
11
+ EV_SET_VALUE,
12
+ EV_TOGGLE_VALUE,
13
+ EV_UNDO,
14
+ EV_UPDATE_VALUE,
15
+ FX_CANCEL,
16
+ FX_DELAY,
17
+ FX_DISPATCH,
18
+ FX_DISPATCH_ASYNC,
19
+ FX_DISPATCH_NOW,
20
+ FX_FETCH,
21
+ FX_STATE,
22
+ LOGGER
23
+ } from "./api.js";
24
+ class StatelessEventBus {
25
+ state;
26
+ eventQueue;
27
+ currQueue;
28
+ currCtx;
29
+ handlers;
30
+ effects;
31
+ priorities;
32
+ /**
33
+ * Creates a new event bus instance with given handler and effect
34
+ * definitions (all optional).
35
+ *
36
+ * @remarks
37
+ * In addition to the user provided handlers & effects, a number of
38
+ * built-ins are added automatically. See
39
+ * {@link StatelessEventBus.addBuiltIns}. User handlers can override
40
+ * built-ins.
41
+ *
42
+ * @param handlers -
43
+ * @param effects -
44
+ */
45
+ constructor(handlers, effects) {
46
+ this.handlers = {};
47
+ this.effects = {};
48
+ this.eventQueue = [];
49
+ this.priorities = [];
50
+ this.addBuiltIns();
51
+ if (handlers) {
52
+ this.addHandlers(handlers);
97
53
  }
98
- /**
99
- * Adds built-in event & side effect handlers.
100
- *
101
- * @remarks
102
- * Also see additional built-ins defined by the stateful {@link EventBus}
103
- * extension of this class, as well as comments for these class methods:
104
- *
105
- * - {@link StatelessEventBus.mergeEffects}
106
- * - {@link StatelessEventBus.processEvent}
107
- *
108
- * ### Handlers
109
- *
110
- * currently none...
111
- *
112
- * ### Side effects
113
- *
114
- * #### `FX_CANCEL`
115
- *
116
- * If assigned `true`, cancels processing of current event, though still
117
- * applies any side effects already accumulated.
118
- *
119
- * #### `FX_DISPATCH`
120
- *
121
- * Dispatches assigned events to be processed in next frame.
122
- *
123
- * #### `FX_DISPATCH_ASYNC`
124
- *
125
- * Async wrapper for promise based side effects.
126
- *
127
- * #### `FX_DISPATCH_NOW`
128
- *
129
- * Dispatches assigned events as part of currently processed event queue (no
130
- * delay).
131
- *
132
- * #### `FX_DELAY`
133
- *
134
- * Async side effect. Only to be used in conjunction with
135
- * `FX_DISPATCH_ASYNC`. Triggers given event after `x` milliseconds.
136
- *
137
- * ```
138
- * // this triggers `[EV_SUCCESS, "ok"]` event after 1000 ms
139
- * { [FX_DISPATCH_ASYNC]: [FX_DELAY, [1000, "ok"], EV_SUCCESS, EV_ERROR] }
140
- * ```
141
- *
142
- * #### `FX_FETCH`
143
- *
144
- * Async side effect. Only to be used in conjunction with
145
- * `FX_DISPATCH_ASYNC`. Performs `fetch()` HTTP request and triggers success
146
- * with received response, or if there was an error with response's
147
- * `statusText`. The error event is only triggered if the fetched response's
148
- * `ok` field is non-truthy.
149
- *
150
- * - https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
151
- * - https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
152
- *
153
- * ```
154
- * // fetches "foo.json" and then dispatches EV_SUCCESS or EV_ERROR event
155
- * { [FX_DISPATCH_ASYNC]: [FX_FETCH, "foo.json", EV_SUCCESS, EV_ERROR] }
156
- * ```
157
- */
158
- addBuiltIns() {
159
- this.addEffects({
160
- [FX_DISPATCH]: [(e) => this.dispatch(e), -999],
161
- [FX_DISPATCH_ASYNC]: [
162
- ([id, arg, success, err], bus, ctx) => {
163
- const fx = this.effects[id];
164
- if (fx) {
165
- const p = fx(arg, bus, ctx);
166
- if (isPromise(p)) {
167
- p.then((res) => this.dispatch([success, res])).catch((e) => this.dispatch([err, e]));
168
- }
169
- else {
170
- LOGGER.warn("async effect did not return Promise");
171
- }
172
- }
173
- else {
174
- LOGGER.warn(`skipping invalid async effect: ${id}`);
175
- }
176
- },
177
- -999,
178
- ],
179
- [FX_DELAY]: [
180
- ([x, body]) => new Promise((res) => setTimeout(() => res(body), x)),
181
- 1000,
182
- ],
183
- [FX_FETCH]: [
184
- (req) => fetch(req).then((resp) => {
185
- if (!resp.ok) {
186
- throw new Error(resp.statusText);
187
- }
188
- return resp;
189
- }),
190
- 1000,
191
- ],
192
- });
54
+ if (effects) {
55
+ this.addEffects(effects);
193
56
  }
194
- addHandler(id, spec) {
195
- const iceps = this.interceptorsFromSpec(spec);
196
- if (iceps.length > 0) {
197
- if (this.handlers[id]) {
198
- this.removeHandler(id);
199
- LOGGER.warn(`overriding handler for ID: ${id}`);
57
+ }
58
+ /**
59
+ * Adds built-in event & side effect handlers.
60
+ *
61
+ * @remarks
62
+ * Also see additional built-ins defined by the stateful {@link EventBus}
63
+ * extension of this class, as well as comments for these class methods:
64
+ *
65
+ * - {@link StatelessEventBus.mergeEffects}
66
+ * - {@link StatelessEventBus.processEvent}
67
+ *
68
+ * ### Handlers
69
+ *
70
+ * currently none...
71
+ *
72
+ * ### Side effects
73
+ *
74
+ * #### `FX_CANCEL`
75
+ *
76
+ * If assigned `true`, cancels processing of current event, though still
77
+ * applies any side effects already accumulated.
78
+ *
79
+ * #### `FX_DISPATCH`
80
+ *
81
+ * Dispatches assigned events to be processed in next frame.
82
+ *
83
+ * #### `FX_DISPATCH_ASYNC`
84
+ *
85
+ * Async wrapper for promise based side effects.
86
+ *
87
+ * #### `FX_DISPATCH_NOW`
88
+ *
89
+ * Dispatches assigned events as part of currently processed event queue (no
90
+ * delay).
91
+ *
92
+ * #### `FX_DELAY`
93
+ *
94
+ * Async side effect. Only to be used in conjunction with
95
+ * `FX_DISPATCH_ASYNC`. Triggers given event after `x` milliseconds.
96
+ *
97
+ * ```
98
+ * // this triggers `[EV_SUCCESS, "ok"]` event after 1000 ms
99
+ * { [FX_DISPATCH_ASYNC]: [FX_DELAY, [1000, "ok"], EV_SUCCESS, EV_ERROR] }
100
+ * ```
101
+ *
102
+ * #### `FX_FETCH`
103
+ *
104
+ * Async side effect. Only to be used in conjunction with
105
+ * `FX_DISPATCH_ASYNC`. Performs `fetch()` HTTP request and triggers success
106
+ * with received response, or if there was an error with response's
107
+ * `statusText`. The error event is only triggered if the fetched response's
108
+ * `ok` field is non-truthy.
109
+ *
110
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
111
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText
112
+ *
113
+ * ```
114
+ * // fetches "foo.json" and then dispatches EV_SUCCESS or EV_ERROR event
115
+ * { [FX_DISPATCH_ASYNC]: [FX_FETCH, "foo.json", EV_SUCCESS, EV_ERROR] }
116
+ * ```
117
+ */
118
+ addBuiltIns() {
119
+ this.addEffects({
120
+ [FX_DISPATCH]: [(e) => this.dispatch(e), -999],
121
+ [FX_DISPATCH_ASYNC]: [
122
+ ([id, arg, success, err], bus, ctx) => {
123
+ const fx = this.effects[id];
124
+ if (fx) {
125
+ const p = fx(arg, bus, ctx);
126
+ if (isPromise(p)) {
127
+ p.then(
128
+ (res) => this.dispatch([success, res])
129
+ ).catch((e) => this.dispatch([err, e]));
130
+ } else {
131
+ LOGGER.warn("async effect did not return Promise");
200
132
  }
201
- this.handlers[id] = iceps;
202
- }
203
- else {
204
- illegalArgs(`no handlers in spec for ID: ${id}`);
205
- }
206
- }
207
- addHandlers(specs) {
208
- for (let id in specs) {
209
- this.addHandler(id, specs[id]);
210
- }
133
+ } else {
134
+ LOGGER.warn(`skipping invalid async effect: ${id}`);
135
+ }
136
+ },
137
+ -999
138
+ ],
139
+ [FX_DELAY]: [
140
+ ([x, body]) => new Promise((res) => setTimeout(() => res(body), x)),
141
+ 1e3
142
+ ],
143
+ [FX_FETCH]: [
144
+ (req) => fetch(req).then((resp) => {
145
+ if (!resp.ok) {
146
+ throw new Error(resp.statusText);
147
+ }
148
+ return resp;
149
+ }),
150
+ 1e3
151
+ ]
152
+ });
153
+ }
154
+ addHandler(id, spec) {
155
+ const iceps = this.interceptorsFromSpec(spec);
156
+ if (iceps.length > 0) {
157
+ if (this.handlers[id]) {
158
+ this.removeHandler(id);
159
+ LOGGER.warn(`overriding handler for ID: ${id}`);
160
+ }
161
+ this.handlers[id] = iceps;
162
+ } else {
163
+ illegalArgs(`no handlers in spec for ID: ${id}`);
211
164
  }
212
- addEffect(id, fx, priority = 1) {
213
- if (this.effects[id]) {
214
- this.removeEffect(id);
215
- LOGGER.warn(`overriding effect for ID: ${id}`);
216
- }
217
- this.effects[id] = fx;
218
- const p = [id, priority];
219
- const priors = this.priorities;
220
- for (let i = 0; i < priors.length; i++) {
221
- if (p[1] < priors[i][1]) {
222
- priors.splice(i, 0, p);
223
- return;
224
- }
225
- }
226
- priors.push(p);
227
- }
228
- addEffects(specs) {
229
- for (let id in specs) {
230
- const fx = specs[id];
231
- if (isArray(fx)) {
232
- this.addEffect(id, fx[0], fx[1]);
233
- }
234
- else {
235
- this.addEffect(id, fx);
236
- }
237
- }
165
+ }
166
+ addHandlers(specs) {
167
+ for (let id in specs) {
168
+ this.addHandler(id, specs[id]);
238
169
  }
239
- /**
240
- * Prepends given interceptors (or interceptor functions) to
241
- * selected handlers. If no handler IDs are given, applies
242
- * instrumentation to all currently registered handlers.
243
- *
244
- * @param inject -
245
- * @param ids -
246
- */
247
- instrumentWith(inject, ids) {
248
- const iceps = inject.map(asInterceptor);
249
- const handlers = this.handlers;
250
- for (let id of ids || Object.keys(handlers)) {
251
- const h = handlers[id];
252
- if (h) {
253
- handlers[id] = iceps.concat(h);
254
- }
255
- }
170
+ }
171
+ addEffect(id, fx, priority = 1) {
172
+ if (this.effects[id]) {
173
+ this.removeEffect(id);
174
+ LOGGER.warn(`overriding effect for ID: ${id}`);
256
175
  }
257
- removeHandler(id) {
258
- delete this.handlers[id];
176
+ this.effects[id] = fx;
177
+ const p = [id, priority];
178
+ const priors = this.priorities;
179
+ for (let i = 0; i < priors.length; i++) {
180
+ if (p[1] < priors[i][1]) {
181
+ priors.splice(i, 0, p);
182
+ return;
183
+ }
259
184
  }
260
- removeHandlers(ids) {
261
- for (let id of ids) {
262
- this.removeHandler(id);
263
- }
185
+ priors.push(p);
186
+ }
187
+ addEffects(specs) {
188
+ for (let id in specs) {
189
+ const fx = specs[id];
190
+ if (isArray(fx)) {
191
+ this.addEffect(id, fx[0], fx[1]);
192
+ } else {
193
+ this.addEffect(id, fx);
194
+ }
264
195
  }
265
- removeEffect(id) {
266
- delete this.effects[id];
267
- const p = this.priorities;
268
- for (let i = p.length - 1; i >= 0; i--) {
269
- if (id === p[i][0]) {
270
- p.splice(i, 1);
271
- return;
272
- }
273
- }
196
+ }
197
+ /**
198
+ * Prepends given interceptors (or interceptor functions) to
199
+ * selected handlers. If no handler IDs are given, applies
200
+ * instrumentation to all currently registered handlers.
201
+ *
202
+ * @param inject -
203
+ * @param ids -
204
+ */
205
+ instrumentWith(inject, ids) {
206
+ const iceps = inject.map(asInterceptor);
207
+ const handlers = this.handlers;
208
+ for (let id of ids || Object.keys(handlers)) {
209
+ const h = handlers[id];
210
+ if (h) {
211
+ handlers[id] = iceps.concat(h);
212
+ }
274
213
  }
275
- removeEffects(ids) {
276
- for (let id of ids) {
277
- this.removeEffect(id);
278
- }
214
+ }
215
+ removeHandler(id) {
216
+ delete this.handlers[id];
217
+ }
218
+ removeHandlers(ids) {
219
+ for (let id of ids) {
220
+ this.removeHandler(id);
279
221
  }
280
- /**
281
- * If called during event processing, returns current side effect
282
- * accumulator / interceptor context. Otherwise returns nothing.
283
- */
284
- context() {
285
- return this.currCtx;
222
+ }
223
+ removeEffect(id) {
224
+ delete this.effects[id];
225
+ const p = this.priorities;
226
+ for (let i = p.length - 1; i >= 0; i--) {
227
+ if (id === p[i][0]) {
228
+ p.splice(i, 1);
229
+ return;
230
+ }
286
231
  }
287
- /**
288
- * Adds given events to event queue to be processed by
289
- * {@link StatelessEventBus.processQueue} later on.
290
- *
291
- * @remarks
292
- * It's the user's responsibility to call that latter function
293
- * repeatedly in a timely manner, preferably via
294
- * `requestAnimationFrame()` or similar.
295
- *
296
- * @param e -
297
- */
298
- dispatch(...e) {
299
- this.eventQueue.push(...e);
232
+ }
233
+ removeEffects(ids) {
234
+ for (let id of ids) {
235
+ this.removeEffect(id);
300
236
  }
301
- /**
302
- * Adds given events to whatever is the current event queue. If
303
- * triggered via the `FX_DISPATCH_NOW` side effect from an event
304
- * handler / interceptor, the event will still be executed in the
305
- * currently active batch / frame. If called from elsewhere, the
306
- * result is the same as calling {@link dispatch}.
307
- *
308
- * @param e -
309
- */
310
- dispatchNow(...e) {
311
- (this.currQueue || this.eventQueue).push(...e);
237
+ }
238
+ /**
239
+ * If called during event processing, returns current side effect
240
+ * accumulator / interceptor context. Otherwise returns nothing.
241
+ */
242
+ context() {
243
+ return this.currCtx;
244
+ }
245
+ /**
246
+ * Adds given events to event queue to be processed by
247
+ * {@link StatelessEventBus.processQueue} later on.
248
+ *
249
+ * @remarks
250
+ * It's the user's responsibility to call that latter function
251
+ * repeatedly in a timely manner, preferably via
252
+ * `requestAnimationFrame()` or similar.
253
+ *
254
+ * @param e -
255
+ */
256
+ dispatch(...e) {
257
+ this.eventQueue.push(...e);
258
+ }
259
+ /**
260
+ * Adds given events to whatever is the current event queue. If
261
+ * triggered via the `FX_DISPATCH_NOW` side effect from an event
262
+ * handler / interceptor, the event will still be executed in the
263
+ * currently active batch / frame. If called from elsewhere, the
264
+ * result is the same as calling {@link dispatch}.
265
+ *
266
+ * @param e -
267
+ */
268
+ dispatchNow(...e) {
269
+ (this.currQueue || this.eventQueue).push(...e);
270
+ }
271
+ /**
272
+ * Dispatches given event after `delay` milliseconds (by default
273
+ * 17).
274
+ *
275
+ * @remarks
276
+ * Since events are only processed by calling
277
+ * {@link StatelessEventBus.processQueue}, it's the user's
278
+ * responsibility to call that latter function repeatedly in a
279
+ * timely manner, preferably via `requestAnimationFrame()` or
280
+ * similar.
281
+ *
282
+ * @param e -
283
+ * @param delay -
284
+ */
285
+ dispatchLater(e, delay = 17) {
286
+ setTimeout(() => this.dispatch(e), delay);
287
+ }
288
+ /**
289
+ * Triggers processing of current event queue and returns `true` if
290
+ * any events have been processed.
291
+ *
292
+ * @remarks
293
+ * If an event handler triggers the `FX_DISPATCH_NOW` side effect,
294
+ * the new event will be added to the currently processed batch and
295
+ * therefore executed in the same frame. Also see {@link dispatchNow}.
296
+ *
297
+ * An optional `ctx` (context) object can be provided, which is used
298
+ * to collect any side effect definitions during processing. This
299
+ * can be useful for debugging, inspection or post-processing
300
+ * purposes.
301
+ *
302
+ * @param ctx -
303
+ */
304
+ processQueue(ctx) {
305
+ if (this.eventQueue.length > 0) {
306
+ this.currQueue = [...this.eventQueue];
307
+ this.eventQueue.length = 0;
308
+ ctx = this.currCtx = ctx || {};
309
+ for (let e of this.currQueue) {
310
+ this.processEvent(ctx, e);
311
+ }
312
+ this.currQueue = this.currCtx = void 0;
313
+ this.processEffects(ctx);
314
+ return true;
312
315
  }
313
- /**
314
- * Dispatches given event after `delay` milliseconds (by default
315
- * 17).
316
- *
317
- * @remarks
318
- * Since events are only processed by calling
319
- * {@link StatelessEventBus.processQueue}, it's the user's
320
- * responsibility to call that latter function repeatedly in a
321
- * timely manner, preferably via `requestAnimationFrame()` or
322
- * similar.
323
- *
324
- * @param e -
325
- * @param delay -
326
- */
327
- dispatchLater(e, delay = 17) {
328
- setTimeout(() => this.dispatch(e), delay);
316
+ return false;
317
+ }
318
+ /**
319
+ * Processes a single event using its configured handler/interceptor
320
+ * chain. Logs warning message and skips processing if no handler is
321
+ * available for the event type.
322
+ *
323
+ * @remarks
324
+ * The array of interceptors is processed in bi-directional order.
325
+ * First any `pre` interceptors are processed in forward order. Then
326
+ * `post` interceptors are processed in reverse.
327
+ *
328
+ * Each interceptor can return a result object of side effects,
329
+ * which are being merged and collected for
330
+ * {@link StatelessEventBus.processEffects}.
331
+ *
332
+ * Any interceptor can trigger zero or more known side effects, each
333
+ * (side effect) will be collected in an array to support multiple
334
+ * invocations of the same effect type per frame. If no side effects
335
+ * are requested, an interceptor can return `undefined`.
336
+ *
337
+ * Processing of the current event stops immediately, if an
338
+ * interceptor sets the `FX_CANCEL` side effect key to `true`.
339
+ * However, the results of any previous interceptors (incl. the one
340
+ * which cancelled) are kept and processed further as usual.
341
+ *
342
+ * @param ctx -
343
+ * @param e -
344
+ */
345
+ processEvent(ctx, e) {
346
+ const iceps = this.handlers[e[0]];
347
+ if (!iceps) {
348
+ LOGGER.warn(`missing handler for event type: ${e[0].toString()}`);
349
+ return;
329
350
  }
330
- /**
331
- * Triggers processing of current event queue and returns `true` if
332
- * any events have been processed.
333
- *
334
- * @remarks
335
- * If an event handler triggers the `FX_DISPATCH_NOW` side effect,
336
- * the new event will be added to the currently processed batch and
337
- * therefore executed in the same frame. Also see {@link dispatchNow}.
338
- *
339
- * An optional `ctx` (context) object can be provided, which is used
340
- * to collect any side effect definitions during processing. This
341
- * can be useful for debugging, inspection or post-processing
342
- * purposes.
343
- *
344
- * @param ctx -
345
- */
346
- processQueue(ctx) {
347
- if (this.eventQueue.length > 0) {
348
- this.currQueue = [...this.eventQueue];
349
- this.eventQueue.length = 0;
350
- ctx = this.currCtx = ctx || {};
351
- for (let e of this.currQueue) {
352
- this.processEvent(ctx, e);
353
- }
354
- this.currQueue = this.currCtx = undefined;
355
- this.processEffects(ctx);
356
- return true;
357
- }
358
- return false;
351
+ if (!this.processForward(ctx, iceps, e)) {
352
+ return;
359
353
  }
360
- /**
361
- * Processes a single event using its configured handler/interceptor
362
- * chain. Logs warning message and skips processing if no handler is
363
- * available for the event type.
364
- *
365
- * @remarks
366
- * The array of interceptors is processed in bi-directional order.
367
- * First any `pre` interceptors are processed in forward order. Then
368
- * `post` interceptors are processed in reverse.
369
- *
370
- * Each interceptor can return a result object of side effects,
371
- * which are being merged and collected for
372
- * {@link StatelessEventBus.processEffects}.
373
- *
374
- * Any interceptor can trigger zero or more known side effects, each
375
- * (side effect) will be collected in an array to support multiple
376
- * invocations of the same effect type per frame. If no side effects
377
- * are requested, an interceptor can return `undefined`.
378
- *
379
- * Processing of the current event stops immediately, if an
380
- * interceptor sets the `FX_CANCEL` side effect key to `true`.
381
- * However, the results of any previous interceptors (incl. the one
382
- * which cancelled) are kept and processed further as usual.
383
- *
384
- * @param ctx -
385
- * @param e -
386
- */
387
- processEvent(ctx, e) {
388
- const iceps = this.handlers[e[0]];
389
- if (!iceps) {
390
- LOGGER.warn(`missing handler for event type: ${e[0].toString()}`);
391
- return;
392
- }
393
- if (!this.processForward(ctx, iceps, e)) {
394
- return;
395
- }
396
- this.processReverse(ctx, iceps, e);
354
+ this.processReverse(ctx, iceps, e);
355
+ }
356
+ processForward(ctx, iceps, e) {
357
+ let hasPost = false;
358
+ for (let i = 0, n = iceps.length; i < n && !ctx[FX_CANCEL]; i++) {
359
+ const icep = iceps[i];
360
+ if (icep.pre) {
361
+ this.mergeEffects(ctx, icep.pre(ctx[FX_STATE], e, this, ctx));
362
+ }
363
+ hasPost = hasPost || !!icep.post;
397
364
  }
398
- processForward(ctx, iceps, e) {
399
- let hasPost = false;
400
- for (let i = 0, n = iceps.length; i < n && !ctx[FX_CANCEL]; i++) {
401
- const icep = iceps[i];
402
- if (icep.pre) {
403
- this.mergeEffects(ctx, icep.pre(ctx[FX_STATE], e, this, ctx));
404
- }
405
- hasPost = hasPost || !!icep.post;
406
- }
407
- return hasPost;
365
+ return hasPost;
366
+ }
367
+ processReverse(ctx, iceps, e) {
368
+ for (let i = iceps.length; i-- > 0 && !ctx[FX_CANCEL]; ) {
369
+ const icep = iceps[i];
370
+ if (icep.post) {
371
+ this.mergeEffects(ctx, icep.post(ctx[FX_STATE], e, this, ctx));
372
+ }
408
373
  }
409
- processReverse(ctx, iceps, e) {
410
- for (let i = iceps.length; i-- > 0 && !ctx[FX_CANCEL];) {
411
- const icep = iceps[i];
412
- if (icep.post) {
413
- this.mergeEffects(ctx, icep.post(ctx[FX_STATE], e, this, ctx));
414
- }
415
- }
374
+ }
375
+ /**
376
+ * Takes a collection of side effects generated during event
377
+ * processing and applies them in order of configured priorities.
378
+ *
379
+ * @param ctx -
380
+ */
381
+ processEffects(ctx) {
382
+ const effects = this.effects;
383
+ for (let p of this.priorities) {
384
+ const id = p[0];
385
+ const val = ctx[id];
386
+ val !== void 0 && this.processEffect(ctx, effects, id, val);
416
387
  }
417
- /**
418
- * Takes a collection of side effects generated during event
419
- * processing and applies them in order of configured priorities.
420
- *
421
- * @param ctx -
422
- */
423
- processEffects(ctx) {
424
- const effects = this.effects;
425
- for (let p of this.priorities) {
426
- const id = p[0];
427
- const val = ctx[id];
428
- val !== undefined && this.processEffect(ctx, effects, id, val);
429
- }
388
+ }
389
+ processEffect(ctx, effects, id, val) {
390
+ const fn = effects[id];
391
+ if (id !== FX_STATE) {
392
+ for (let v of val) {
393
+ fn(v, this, ctx);
394
+ }
395
+ } else {
396
+ fn(val, this, ctx);
430
397
  }
431
- processEffect(ctx, effects, id, val) {
432
- const fn = effects[id];
433
- if (id !== FX_STATE) {
434
- for (let v of val) {
435
- fn(v, this, ctx);
436
- }
437
- }
438
- else {
439
- fn(val, this, ctx);
440
- }
398
+ }
399
+ /**
400
+ * Merges the new side effects returned from an interceptor into the
401
+ * internal effect accumulator.
402
+ *
403
+ * @remarks
404
+ * Any events assigned to the `FX_DISPATCH_NOW` effect key are
405
+ * immediately added to the currently active event batch.
406
+ *
407
+ * If an interceptor wishes to cause multiple invocations of a
408
+ * single side effect type (e.g. dispatch multiple other events), it
409
+ * MUST return an array of these values. The only exceptions to this
410
+ * are the following effects, which for obvious reasons can only
411
+ * accept a single value.
412
+ *
413
+ * **Note:** the `FX_STATE` effect is not actually defined by this
414
+ * class here, but is supported to avoid code duplication in
415
+ * {@link EventBus}.
416
+ *
417
+ * - `FX_CANCEL`
418
+ * - `FX_STATE`
419
+ *
420
+ * Because of this support (multiple values), the value of a single
421
+ * side effect MUST NOT be a nested array itself, or rather its
422
+ * first item can't be an array.
423
+ *
424
+ * For example:
425
+ *
426
+ * ```
427
+ * // interceptor result map to dispatch a single event
428
+ * { [FX_DISPATCH]: ["foo", "bar"]}
429
+ *
430
+ * // result map format to dispatch multiple events
431
+ * { [FX_DISPATCH]: [ ["foo", "bar"], ["baz", "beep"] ]}
432
+ * ```
433
+ *
434
+ * Any `null` / `undefined` values directly assigned to a side
435
+ * effect are ignored and will not trigger the effect.
436
+ *
437
+ * @param fx -
438
+ * @param ret -
439
+ */
440
+ mergeEffects(ctx, ret) {
441
+ if (!ret) {
442
+ return;
441
443
  }
442
- /**
443
- * Merges the new side effects returned from an interceptor into the
444
- * internal effect accumulator.
445
- *
446
- * @remarks
447
- * Any events assigned to the `FX_DISPATCH_NOW` effect key are
448
- * immediately added to the currently active event batch.
449
- *
450
- * If an interceptor wishes to cause multiple invocations of a
451
- * single side effect type (e.g. dispatch multiple other events), it
452
- * MUST return an array of these values. The only exceptions to this
453
- * are the following effects, which for obvious reasons can only
454
- * accept a single value.
455
- *
456
- * **Note:** the `FX_STATE` effect is not actually defined by this
457
- * class here, but is supported to avoid code duplication in
458
- * {@link EventBus}.
459
- *
460
- * - `FX_CANCEL`
461
- * - `FX_STATE`
462
- *
463
- * Because of this support (multiple values), the value of a single
464
- * side effect MUST NOT be a nested array itself, or rather its
465
- * first item can't be an array.
466
- *
467
- * For example:
468
- *
469
- * ```
470
- * // interceptor result map to dispatch a single event
471
- * { [FX_DISPATCH]: ["foo", "bar"]}
472
- *
473
- * // result map format to dispatch multiple events
474
- * { [FX_DISPATCH]: [ ["foo", "bar"], ["baz", "beep"] ]}
475
- * ```
476
- *
477
- * Any `null` / `undefined` values directly assigned to a side
478
- * effect are ignored and will not trigger the effect.
479
- *
480
- * @param fx -
481
- * @param ret -
482
- */
483
- mergeEffects(ctx, ret) {
484
- if (!ret) {
485
- return;
444
+ for (let k in ret) {
445
+ const v = ret[k];
446
+ if (v == null) {
447
+ continue;
448
+ }
449
+ if (k === FX_STATE || k === FX_CANCEL) {
450
+ ctx[k] = v;
451
+ } else if (k === FX_DISPATCH_NOW) {
452
+ if (isArray(v[0])) {
453
+ for (let e of v) {
454
+ e && this.dispatchNow(e);
455
+ }
456
+ } else {
457
+ this.dispatchNow(v);
486
458
  }
487
- for (let k in ret) {
488
- const v = ret[k];
489
- if (v == null) {
490
- continue;
491
- }
492
- if (k === FX_STATE || k === FX_CANCEL) {
493
- ctx[k] = v;
494
- }
495
- else if (k === FX_DISPATCH_NOW) {
496
- if (isArray(v[0])) {
497
- for (let e of v) {
498
- e && this.dispatchNow(e);
499
- }
500
- }
501
- else {
502
- this.dispatchNow(v);
503
- }
504
- }
505
- else {
506
- ctx[k] || (ctx[k] = []);
507
- if (isArray(v[0])) {
508
- for (let e of v) {
509
- e !== undefined && ctx[k].push(e);
510
- }
511
- }
512
- else {
513
- ctx[k].push(v);
514
- }
515
- }
459
+ } else {
460
+ ctx[k] || (ctx[k] = []);
461
+ if (isArray(v[0])) {
462
+ for (let e of v) {
463
+ e !== void 0 && ctx[k].push(e);
464
+ }
465
+ } else {
466
+ ctx[k].push(v);
516
467
  }
468
+ }
517
469
  }
518
- interceptorsFromSpec(spec) {
519
- return isArray(spec)
520
- ? spec.map(asInterceptor)
521
- : isFunction(spec)
522
- ? [{ pre: spec }]
523
- : [spec];
524
- }
470
+ }
471
+ interceptorsFromSpec(spec) {
472
+ return isArray(spec) ? spec.map(asInterceptor) : isFunction(spec) ? [{ pre: spec }] : [spec];
473
+ }
525
474
  }
526
- /**
527
- * Stateful version of {@link StatelessEventBus}.
528
- *
529
- * @remarks
530
- * Wraps an [`IAtom`](https://docs.thi.ng/umbrella/atom/interfaces/IAtom.html)
531
- * state container (i.e. `Atom`/`Cursor`/`History`) and provides additional
532
- * pre-defined event handlers and side effects to manipulate wrapped state.
533
- * Prefer this as the default implementation for most use cases.
534
- */
535
- export class EventBus extends StatelessEventBus {
536
- state;
537
- /**
538
- * Creates a new event bus instance with given parent state, handler and
539
- * effect definitions (all optional).
540
- *
541
- * @remarks
542
- * If no state is given, automatically creates an
543
- * [`Atom`](https://docs.thi.ng/umbrella/atom/classes/Atom.html) with empty
544
- * state object.
545
- *
546
- * In addition to the user provided handlers & effects, a number of
547
- * built-ins are added automatically. See {@link EventBus.addBuiltIns}. User
548
- * handlers can override built-ins.
549
- *
550
- * @param state -
551
- * @param handlers -
552
- * @param effects -
553
- */
554
- constructor(state, handlers, effects) {
555
- super(handlers, effects);
556
- this.state = state || new Atom({});
557
- }
558
- /**
559
- * Returns value of internal state. Shorthand for:
560
- * `bus.state.deref()`
561
- */
562
- deref() {
563
- return this.state.deref();
564
- }
565
- /**
566
- * Adds same built-in event & side effect handlers as in
567
- * `StatelessEventBus.addBuiltIns()` and the following additions:
568
- *
569
- * ### Handlers
570
- *
571
- * #### `EV_SET_VALUE`
572
- *
573
- * Resets state path to provided value. See
574
- * [`setIn()`](https://docs.thi.ng/umbrella/paths/functions/setIn.html).
575
- *
576
- * Example event definition:
577
- * ```
578
- * [EV_SET_VALUE, ["path.to.value", val]]
579
- * ```
580
- *
581
- * #### `EV_UPDATE_VALUE`
582
- *
583
- * Updates a state path's value with provided function and optional extra
584
- * arguments. See
585
- * [`updateIn()`](https://docs.thi.ng/umbrella/paths/functions/updateIn.html).
586
- *
587
- * Example event definition:
588
- * ```
589
- * [EV_UPDATE_VALUE, ["path.to.value", (x, y) => x + y, 1]]
590
- * ```
591
- *
592
- * #### `EV_TOGGLE_VALUE`
593
- *
594
- * Negates a boolean state value at given path.
595
- *
596
- * Example event definition:
597
- * ```
598
- * [EV_TOGGLE_VALUE, "path.to.value"]
599
- * ```
600
- *
601
- * #### `EV_UNDO`
602
- *
603
- * Calls `ctx[id].undo()` and uses return value as new state. Assumes
604
- * `ctx[id]` is a
605
- * [`History`](https://docs.thi.ng/umbrella/atom/classes/History.html)
606
- * instance, provided via e.g. `processQueue({ history })`. The event can be
607
- * triggered with or without ID. By default `"history"` is used as default
608
- * key to lookup the `History` instance. Furthermore, an additional event
609
- * can be triggered based on if a previous state has been restored or not
610
- * (basically, if the undo was successful). This is useful for
611
- * resetting/re-initializing stateful resources after a successful undo
612
- * action or to notify the user that no more undo's are possible. The new
613
- * event will be processed in the same frame and has access to the
614
- * (possibly) restored state. The event structure for these options is shown
615
- * below:
616
- *
617
- * ```
618
- * // using default ID
619
- * bus.dispatch([EV_UNDO]);
620
- *
621
- * // using custom history ID
622
- * bus.dispatch([EV_UNDO, ["custom"]]);
623
- *
624
- * // using custom ID and dispatch another event after undo
625
- * bus.dispatch([EV_UNDO, ["custom", ["ev-undo-success"], ["ev-undo-fail"]]]);
626
- * ```
627
- *
628
- * #### `EV_REDO`
629
- *
630
- * Similar to `EV_UNDO`, but for redo actions.
631
- *
632
- * ### Side effects
633
- *
634
- * #### `FX_STATE`
635
- *
636
- * Resets state atom to provided value (only a single update per processing
637
- * frame).
638
- */
639
- addBuiltIns() {
640
- super.addBuiltIns();
641
- // handlers
642
- this.addHandlers({
643
- [EV_SET_VALUE]: (state, [_, [path, val]]) => ({
644
- [FX_STATE]: setInUnsafe(state, path, val),
645
- }),
646
- [EV_UPDATE_VALUE]: (state, [_, [path, fn, ...args]]) => ({
647
- [FX_STATE]: updateInUnsafe(state, path, fn, ...args),
648
- }),
649
- [EV_TOGGLE_VALUE]: (state, [_, path]) => ({
650
- [FX_STATE]: updateInUnsafe(state, path, (x) => !x),
651
- }),
652
- [EV_UNDO]: undoHandler("undo"),
653
- [EV_REDO]: undoHandler("redo"),
654
- });
655
- // effects
656
- this.addEffects({
657
- [FX_STATE]: [(state) => this.state.reset(state), -1000],
658
- });
659
- }
660
- /**
661
- * Triggers processing of current event queue and returns `true` if the any
662
- * of the processed events caused a state change.
663
- *
664
- * If an event handler triggers the `FX_DISPATCH_NOW` side effect, the new
665
- * event will be added to the currently processed batch and therefore
666
- * executed in the same frame. Also see {@link dispatchNow}.
667
- *
668
- * If the optional `ctx` arg is provided it will be merged into the
669
- * {@link InterceptorContext} object passed to each interceptor. Since the
670
- * merged object is also used to collect triggered side effects, care must
671
- * be taken that there're no key name clashes.
672
- *
673
- * In order to use the built-in `EV_UNDO`, `EV_REDO` events, users MUST
674
- * provide a
675
- * [`History`](https://docs.thi.ng/umbrella/atom/classes/History.html) (or
676
- * compatible undo history instance) via the `ctx` arg, e.g.
677
- *
678
- * ```
679
- * bus.processQueue({ history });
680
- * ```
681
- */
682
- processQueue(ctx) {
683
- if (this.eventQueue.length > 0) {
684
- const prev = this.state.deref();
685
- this.currQueue = [...this.eventQueue];
686
- this.eventQueue.length = 0;
687
- ctx = this.currCtx = { ...ctx, [FX_STATE]: prev };
688
- for (let e of this.currQueue) {
689
- this.processEvent(ctx, e);
690
- }
691
- this.currQueue = this.currCtx = undefined;
692
- this.processEffects(ctx);
693
- return this.state.deref() !== prev;
694
- }
695
- return false;
475
+ class EventBus extends StatelessEventBus {
476
+ state;
477
+ /**
478
+ * Creates a new event bus instance with given parent state, handler and
479
+ * effect definitions (all optional).
480
+ *
481
+ * @remarks
482
+ * If no state is given, automatically creates an
483
+ * [`Atom`](https://docs.thi.ng/umbrella/atom/classes/Atom.html) with empty
484
+ * state object.
485
+ *
486
+ * In addition to the user provided handlers & effects, a number of
487
+ * built-ins are added automatically. See {@link EventBus.addBuiltIns}. User
488
+ * handlers can override built-ins.
489
+ *
490
+ * @param state -
491
+ * @param handlers -
492
+ * @param effects -
493
+ */
494
+ constructor(state, handlers, effects) {
495
+ super(handlers, effects);
496
+ this.state = state || new Atom({});
497
+ }
498
+ /**
499
+ * Returns value of internal state. Shorthand for:
500
+ * `bus.state.deref()`
501
+ */
502
+ deref() {
503
+ return this.state.deref();
504
+ }
505
+ /**
506
+ * Adds same built-in event & side effect handlers as in
507
+ * `StatelessEventBus.addBuiltIns()` and the following additions:
508
+ *
509
+ * ### Handlers
510
+ *
511
+ * #### `EV_SET_VALUE`
512
+ *
513
+ * Resets state path to provided value. See
514
+ * [`setIn()`](https://docs.thi.ng/umbrella/paths/functions/setIn.html).
515
+ *
516
+ * Example event definition:
517
+ * ```
518
+ * [EV_SET_VALUE, ["path.to.value", val]]
519
+ * ```
520
+ *
521
+ * #### `EV_UPDATE_VALUE`
522
+ *
523
+ * Updates a state path's value with provided function and optional extra
524
+ * arguments. See
525
+ * [`updateIn()`](https://docs.thi.ng/umbrella/paths/functions/updateIn.html).
526
+ *
527
+ * Example event definition:
528
+ * ```
529
+ * [EV_UPDATE_VALUE, ["path.to.value", (x, y) => x + y, 1]]
530
+ * ```
531
+ *
532
+ * #### `EV_TOGGLE_VALUE`
533
+ *
534
+ * Negates a boolean state value at given path.
535
+ *
536
+ * Example event definition:
537
+ * ```
538
+ * [EV_TOGGLE_VALUE, "path.to.value"]
539
+ * ```
540
+ *
541
+ * #### `EV_UNDO`
542
+ *
543
+ * Calls `ctx[id].undo()` and uses return value as new state. Assumes
544
+ * `ctx[id]` is a
545
+ * [`History`](https://docs.thi.ng/umbrella/atom/classes/History.html)
546
+ * instance, provided via e.g. `processQueue({ history })`. The event can be
547
+ * triggered with or without ID. By default `"history"` is used as default
548
+ * key to lookup the `History` instance. Furthermore, an additional event
549
+ * can be triggered based on if a previous state has been restored or not
550
+ * (basically, if the undo was successful). This is useful for
551
+ * resetting/re-initializing stateful resources after a successful undo
552
+ * action or to notify the user that no more undo's are possible. The new
553
+ * event will be processed in the same frame and has access to the
554
+ * (possibly) restored state. The event structure for these options is shown
555
+ * below:
556
+ *
557
+ * ```
558
+ * // using default ID
559
+ * bus.dispatch([EV_UNDO]);
560
+ *
561
+ * // using custom history ID
562
+ * bus.dispatch([EV_UNDO, ["custom"]]);
563
+ *
564
+ * // using custom ID and dispatch another event after undo
565
+ * bus.dispatch([EV_UNDO, ["custom", ["ev-undo-success"], ["ev-undo-fail"]]]);
566
+ * ```
567
+ *
568
+ * #### `EV_REDO`
569
+ *
570
+ * Similar to `EV_UNDO`, but for redo actions.
571
+ *
572
+ * ### Side effects
573
+ *
574
+ * #### `FX_STATE`
575
+ *
576
+ * Resets state atom to provided value (only a single update per processing
577
+ * frame).
578
+ */
579
+ addBuiltIns() {
580
+ super.addBuiltIns();
581
+ this.addHandlers({
582
+ [EV_SET_VALUE]: (state, [_, [path, val]]) => ({
583
+ [FX_STATE]: setInUnsafe(state, path, val)
584
+ }),
585
+ [EV_UPDATE_VALUE]: (state, [_, [path, fn, ...args]]) => ({
586
+ [FX_STATE]: updateInUnsafe(state, path, fn, ...args)
587
+ }),
588
+ [EV_TOGGLE_VALUE]: (state, [_, path]) => ({
589
+ [FX_STATE]: updateInUnsafe(state, path, (x) => !x)
590
+ }),
591
+ [EV_UNDO]: undoHandler("undo"),
592
+ [EV_REDO]: undoHandler("redo")
593
+ });
594
+ this.addEffects({
595
+ [FX_STATE]: [(state) => this.state.reset(state), -1e3]
596
+ });
597
+ }
598
+ /**
599
+ * Triggers processing of current event queue and returns `true` if the any
600
+ * of the processed events caused a state change.
601
+ *
602
+ * If an event handler triggers the `FX_DISPATCH_NOW` side effect, the new
603
+ * event will be added to the currently processed batch and therefore
604
+ * executed in the same frame. Also see {@link dispatchNow}.
605
+ *
606
+ * If the optional `ctx` arg is provided it will be merged into the
607
+ * {@link InterceptorContext} object passed to each interceptor. Since the
608
+ * merged object is also used to collect triggered side effects, care must
609
+ * be taken that there're no key name clashes.
610
+ *
611
+ * In order to use the built-in `EV_UNDO`, `EV_REDO` events, users MUST
612
+ * provide a
613
+ * [`History`](https://docs.thi.ng/umbrella/atom/classes/History.html) (or
614
+ * compatible undo history instance) via the `ctx` arg, e.g.
615
+ *
616
+ * ```
617
+ * bus.processQueue({ history });
618
+ * ```
619
+ */
620
+ processQueue(ctx) {
621
+ if (this.eventQueue.length > 0) {
622
+ const prev = this.state.deref();
623
+ this.currQueue = [...this.eventQueue];
624
+ this.eventQueue.length = 0;
625
+ ctx = this.currCtx = { ...ctx, [FX_STATE]: prev };
626
+ for (let e of this.currQueue) {
627
+ this.processEvent(ctx, e);
628
+ }
629
+ this.currQueue = this.currCtx = void 0;
630
+ this.processEffects(ctx);
631
+ return this.state.deref() !== prev;
696
632
  }
633
+ return false;
634
+ }
697
635
  }
698
636
  const asInterceptor = (i) => isFunction(i) ? { pre: i } : i;
699
637
  const undoHandler = (action) => (_, [__, ev], bus, ctx) => {
700
- const id = ev ? ev[0] : "history";
701
- if (implementsFunction(ctx[id], action)) {
702
- const ok = ctx[id][action]();
703
- return {
704
- [FX_STATE]: bus.state.deref(),
705
- [FX_DISPATCH_NOW]: ev
706
- ? ok !== undefined
707
- ? ev[1]
708
- : ev[2]
709
- : undefined,
710
- };
711
- }
712
- else {
713
- LOGGER.warn("no history in context");
714
- }
638
+ const id = ev ? ev[0] : "history";
639
+ if (implementsFunction(ctx[id], action)) {
640
+ const ok = ctx[id][action]();
641
+ return {
642
+ [FX_STATE]: bus.state.deref(),
643
+ [FX_DISPATCH_NOW]: ev ? ok !== void 0 ? ev[1] : ev[2] : void 0
644
+ };
645
+ } else {
646
+ LOGGER.warn("no history in context");
647
+ }
648
+ };
649
+ export {
650
+ EventBus,
651
+ StatelessEventBus
715
652
  };