@thi.ng/interceptors 3.2.28 → 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/CHANGELOG.md +1 -1
- package/README.md +1 -1
- package/api.js +30 -26
- package/event-bus.js +615 -678
- package/interceptors.js +45 -216
- package/package.json +12 -9
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 {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
};
|