statepod 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,564 @@
1
+ import { QuasiURL } from "quasiurl";
2
+
3
+ var EventEmitter = class {
4
+ _callbacks = {};
5
+ _active = true;
6
+ /**
7
+ * Adds an event handler.
8
+ *
9
+ * Returns an unsubscription function. Once it's invoked, the given
10
+ * `callback` is removed and no longer called in response to the event.
11
+ */
12
+ on(event, callback) {
13
+ (this._callbacks[event] ??= /* @__PURE__ */ new Set()).add(callback);
14
+ return () => this.off(event, callback);
15
+ }
16
+ /**
17
+ * Adds a one-time event handler: once the event is emitted, the callback
18
+ * is called and immediately removed.
19
+ */
20
+ once(event, callback) {
21
+ let oneTimeCallback = (payload) => {
22
+ this.off(event, oneTimeCallback);
23
+ callback(payload);
24
+ };
25
+ return this.on(event, oneTimeCallback);
26
+ }
27
+ /**
28
+ * Removes the specified `callback` from the handlers of the given event,
29
+ * and removes all handlers of the given event if `callback` is not
30
+ * specified.
31
+ */
32
+ off(event, callback) {
33
+ if (callback === void 0) delete this._callbacks[event];
34
+ else this._callbacks[event]?.delete(callback);
35
+ }
36
+ /**
37
+ * Emits the specified event. Returns `false` if at least one event callback
38
+ * returns `false`, effectively interrupting the callback call chain.
39
+ * Otherwise returns `true`.
40
+ */
41
+ emit(event, payload) {
42
+ let callbacks = this._callbacks[event];
43
+ if (this._active && callbacks?.size) {
44
+ for (let callback of callbacks) if (callback(payload) === false) return false;
45
+ }
46
+ return true;
47
+ }
48
+ get active() {
49
+ return this._active;
50
+ }
51
+ start() {
52
+ if (!this._active) {
53
+ this._active = true;
54
+ this.emit("start");
55
+ }
56
+ }
57
+ stop() {
58
+ if (this._active) {
59
+ this._active = false;
60
+ this.emit("stop");
61
+ }
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Serves as an alternative to `instanceof State` which can lead to a false
67
+ * negative when `State` comes from a transitive dependency.
68
+ */
69
+ function isState(x) {
70
+ return x !== null && typeof x === "object" && "on" in x && typeof x.on === "function" && "emit" in x && typeof x.emit === "function" && "setValue" in x && typeof x.setValue === "function";
71
+ }
72
+
73
+ function isImmediatelyInvokedEvent$1(event) {
74
+ return event === "set";
75
+ }
76
+ /**
77
+ * Data container allowing for subscription to its updates.
78
+ */
79
+ var State = class extends EventEmitter {
80
+ _value;
81
+ _revision = -1;
82
+ _active = false;
83
+ _queue = [];
84
+ constructor(value, options) {
85
+ super();
86
+ this._value = value;
87
+ this._init();
88
+ if (options?.autoStart !== false) this.start();
89
+ }
90
+ _init() {}
91
+ _call(callback) {
92
+ if (this._active) callback();
93
+ else this._queue.push(callback);
94
+ }
95
+ on(event, callback) {
96
+ if (isImmediatelyInvokedEvent$1(event)) this._call(() => {
97
+ let current = this.getValue();
98
+ callback({
99
+ current,
100
+ previous: current
101
+ });
102
+ });
103
+ return super.on(event, callback);
104
+ }
105
+ getValue() {
106
+ return this._value;
107
+ }
108
+ /**
109
+ * Updates the state value.
110
+ *
111
+ * @param update - A new value or an update function `(value) => nextValue`
112
+ * that returns a new state value based on the current state value.
113
+ */
114
+ setValue(update) {
115
+ if (this._active) this._assignValue(this._resolveValue(update));
116
+ }
117
+ _resolveValue(update) {
118
+ return update instanceof Function ? update(this._value) : update;
119
+ }
120
+ _assignValue(value) {
121
+ let previous = this._value;
122
+ let current = value;
123
+ this._value = current;
124
+ this._revision = Math.random();
125
+ this.emit("update", {
126
+ previous,
127
+ current
128
+ });
129
+ this.emit("set", {
130
+ previous,
131
+ current
132
+ });
133
+ }
134
+ get revision() {
135
+ return this._revision;
136
+ }
137
+ start() {
138
+ super.start();
139
+ for (let callback of this._queue) callback();
140
+ this._queue = [];
141
+ }
142
+ };
143
+
144
+ function getStorage(session = false) {
145
+ if (typeof window !== "undefined") return session ? window.sessionStorage : window.localStorage;
146
+ }
147
+ function getStorageEntry({ key, session, serialize = JSON.stringify, deserialize = JSON.parse }) {
148
+ let storage = getStorage(session);
149
+ if (!storage) return {
150
+ read: () => null,
151
+ write: () => {}
152
+ };
153
+ return {
154
+ read() {
155
+ try {
156
+ let serializedValue = storage.getItem(key);
157
+ if (serializedValue !== null) return deserialize(serializedValue);
158
+ } catch {}
159
+ return null;
160
+ },
161
+ write(value) {
162
+ try {
163
+ storage.setItem(key, serialize(value));
164
+ } catch {}
165
+ }
166
+ };
167
+ }
168
+ /**
169
+ * A container for data persistent across page reloads.
170
+ */
171
+ var PersistentState = class extends State {
172
+ /**
173
+ * @param value - Initial state value.
174
+ * @param options - Either of the following:
175
+ * - A set of browser storage settings: `key` points to the target browser
176
+ * storage key where the state value should be saved; `session` set to `true`
177
+ * signals to use `sessionStorage` instead of `localStorage`, with the latter
178
+ * being the default; the optional `serialize` and `deserialize` define the
179
+ * way the state value is saved to and restored from the browser storage
180
+ * entry (default: `JSON.stringify` and `JSON.parse` respectively).
181
+ * - A storage singleton with a `read` and an optional `write` method
182
+ * (synchronous or asynchronous).
183
+ */
184
+ constructor(value, options) {
185
+ super(value, { autoStart: false });
186
+ let { read, write } = "read" in options ? options : getStorageEntry(options);
187
+ let update = (value) => {
188
+ if (value === null) write?.(this.getValue());
189
+ else this.setValue(value);
190
+ };
191
+ let sync = () => {
192
+ let value = read();
193
+ if (value instanceof Promise) value.then(update);
194
+ else update(value);
195
+ };
196
+ if (write) this.on("update", ({ current }) => {
197
+ write(current);
198
+ });
199
+ this.on("sync", sync);
200
+ this.on("start", sync);
201
+ if (options?.autoStart !== false) this.start();
202
+ }
203
+ };
204
+
205
+ function isImmediatelyInvokedNavigationEvent(event) {
206
+ return event === "navigationstart" || event === "navigationcomplete";
207
+ }
208
+ function isImmediatelyInvokedEvent(event) {
209
+ return event === "ready";
210
+ }
211
+ var URLState = class extends State {
212
+ constructor(href = null, options) {
213
+ super(href ?? "", options);
214
+ }
215
+ _init() {
216
+ super._init();
217
+ if (typeof window === "undefined") return;
218
+ let handleURLChange = () => {
219
+ this.setValue(window.location.href, { source: "popstate" });
220
+ };
221
+ this.on("start", () => {
222
+ window.addEventListener("popstate", handleURLChange);
223
+ this.emit("ready");
224
+ });
225
+ this.on("stop", () => {
226
+ window.removeEventListener("popstate", handleURLChange);
227
+ });
228
+ }
229
+ on(event, callback, invokeImmediately) {
230
+ if (invokeImmediately !== false) {
231
+ if (isImmediatelyInvokedNavigationEvent(event)) this._call(() => {
232
+ callback({ href: this.getValue() });
233
+ });
234
+ else if (isImmediatelyInvokedEvent(event)) this._call(() => {
235
+ callback();
236
+ });
237
+ }
238
+ return super.on(event, callback);
239
+ }
240
+ getValue() {
241
+ return this.toValue(this._value);
242
+ }
243
+ setValue(update, options) {
244
+ if (!this._active) return;
245
+ let href = this.toValue(this._resolveValue(update));
246
+ let extendedOptions = {
247
+ ...options,
248
+ href,
249
+ referrer: this.getValue()
250
+ };
251
+ if (this.emit("navigationstart", extendedOptions) && this._transition(extendedOptions) !== false) {
252
+ this._assignValue(href);
253
+ this.emit("navigation", extendedOptions);
254
+ if (this.emit("navigationcomplete", extendedOptions)) {
255
+ this._complete(extendedOptions);
256
+ this.emit("ready");
257
+ }
258
+ }
259
+ }
260
+ _transition(options) {
261
+ if (typeof window === "undefined" || options?.href === void 0 || options?.source === "popstate") return;
262
+ let { href, target, spa, history } = options;
263
+ if (target && target !== "_self") {
264
+ window.open(href, target);
265
+ return false;
266
+ }
267
+ let url = new QuasiURL(href);
268
+ if (spa === "off" || !window.history || url.origin !== "" && url.origin !== window.location.origin) {
269
+ window.location[history === "replace" ? "replace" : "assign"](href);
270
+ return false;
271
+ }
272
+ window.history[history === "replace" ? "replaceState" : "pushState"]({}, "", href);
273
+ }
274
+ _complete(options) {
275
+ if (typeof window === "undefined" || !options || options.scroll === "off") return;
276
+ let { href, target } = options;
277
+ if (href === void 0 || target && target !== "_self") return;
278
+ let { hash } = new QuasiURL(href);
279
+ requestAnimationFrame(() => {
280
+ let targetElement = hash === "" ? null : document.querySelector(`${hash}, a[name="${hash.slice(1)}"]`);
281
+ if (targetElement) targetElement.scrollIntoView();
282
+ else window.scrollTo(0, 0);
283
+ });
284
+ }
285
+ toValue(x) {
286
+ if (typeof window === "undefined") return x;
287
+ let url = new QuasiURL(x || window.location.href);
288
+ if (url.origin === window.location.origin) url.origin = "";
289
+ return url.href;
290
+ }
291
+ };
292
+
293
+ function isLocationObject(x) {
294
+ return x !== null && typeof x === "object" && "exec" in x && "compile" in x && "_schema" in x;
295
+ }
296
+
297
+ function compileURL(urlPattern, data) {
298
+ if (isLocationObject(urlPattern)) return urlPattern.compile(data);
299
+ let url = new QuasiURL(urlPattern ?? "");
300
+ let query = data?.query;
301
+ if (query) url.search = new URLSearchParams(Object.entries(query).reduce((p, [k, v]) => {
302
+ if (v !== null && v !== void 0) p[k] = typeof v === "string" ? v : JSON.stringify(v);
303
+ return p;
304
+ }, {}));
305
+ return url.href;
306
+ }
307
+
308
+ function getNavigationOptions(element) {
309
+ let { id, spa, history, scroll } = element.dataset;
310
+ return {
311
+ href: element.getAttribute("href"),
312
+ target: element.getAttribute("target"),
313
+ spa,
314
+ history,
315
+ scroll,
316
+ id
317
+ };
318
+ }
319
+
320
+ function isRouteEvent(event) {
321
+ return event !== null && typeof event === "object" && (!("button" in event) || event.button === 0) && (!("ctrlKey" in event) || !event.ctrlKey) && (!("shiftKey" in event) || !event.shiftKey) && (!("altKey" in event) || !event.altKey) && (!("metaKey" in event) || !event.metaKey);
322
+ }
323
+
324
+ function toObject(x) {
325
+ return x.reduce((p, v, k) => {
326
+ p[String(k)] = v;
327
+ return p;
328
+ }, {});
329
+ }
330
+ function matchPattern(pattern, href) {
331
+ let query = Object.fromEntries(new URLSearchParams(new QuasiURL(href).search));
332
+ if (typeof pattern === "string") return {
333
+ ok: pattern === "*" || pattern === href,
334
+ href,
335
+ params: {},
336
+ query
337
+ };
338
+ if (pattern instanceof RegExp) {
339
+ let matches = pattern.exec(href);
340
+ return {
341
+ ok: matches !== null,
342
+ href,
343
+ params: matches ? {
344
+ ...toObject(Array.from(matches).slice(1)),
345
+ ...matches.groups
346
+ } : {},
347
+ query
348
+ };
349
+ }
350
+ if (isLocationObject(pattern)) {
351
+ let result = pattern.exec(href);
352
+ if (result === null) return {
353
+ ok: false,
354
+ href,
355
+ params: {},
356
+ query: {}
357
+ };
358
+ return {
359
+ ok: true,
360
+ href,
361
+ params: result.params ?? {},
362
+ query: result.query ?? {}
363
+ };
364
+ }
365
+ return {
366
+ ok: false,
367
+ href,
368
+ params: {},
369
+ query: {}
370
+ };
371
+ }
372
+ function matchURL(pattern, href) {
373
+ if (Array.isArray(pattern)) {
374
+ for (let p of pattern) {
375
+ let result = matchPattern(p, href);
376
+ if (result.ok) return result;
377
+ }
378
+ return {
379
+ ok: false,
380
+ href,
381
+ params: {},
382
+ query: {}
383
+ };
384
+ }
385
+ return matchPattern(pattern, href);
386
+ }
387
+
388
+ function isElementCollection(x) {
389
+ return Array.isArray(x) || x instanceof NodeList || x instanceof HTMLCollection;
390
+ }
391
+ function isLinkElement(x) {
392
+ return x instanceof HTMLAnchorElement || x instanceof HTMLAreaElement;
393
+ }
394
+ function toggleActive(element, route) {
395
+ if (!isLinkElement(element)) return;
396
+ if (element.hasAttribute("href") && route.match(route.toValue(element.href)).ok) element.dataset.active = "true";
397
+ else delete element.dataset.active;
398
+ }
399
+ var Route = class extends URLState {
400
+ _observeInited = false;
401
+ constructor(href = null, options) {
402
+ super(String(href ?? ""), options);
403
+ }
404
+ /**
405
+ * Enables SPA navigation with HTML links inside the specified container.
406
+ *
407
+ * @example
408
+ * ```js
409
+ * let route = new Route();
410
+ * route.observe(document);
411
+ * ```
412
+ */
413
+ observe(container, elements = "a, area") {
414
+ if (typeof window === "undefined") return;
415
+ if (!this._observeInited) {
416
+ let handleClick = (event) => {
417
+ this.emit("documentclick", event);
418
+ };
419
+ this.on("start", () => {
420
+ document.addEventListener("click", handleClick);
421
+ });
422
+ this.on("stop", () => {
423
+ document.removeEventListener("click", handleClick);
424
+ });
425
+ if (this.active) document.addEventListener("click", handleClick);
426
+ this._observeInited = true;
427
+ }
428
+ let resolveParams = () => {
429
+ let resolvedContainer = typeof container === "function" ? container() : container;
430
+ if (!resolvedContainer) return;
431
+ return {
432
+ resolvedContainer,
433
+ targetElements: isElementCollection(elements) ? Array.from(elements) : [elements]
434
+ };
435
+ };
436
+ let removeClickHandlers = this.on("documentclick", (event) => {
437
+ if (event.defaultPrevented || !isRouteEvent(event)) return;
438
+ let resolvedParams = resolveParams();
439
+ if (!resolvedParams) return;
440
+ let { resolvedContainer, targetElements } = resolvedParams;
441
+ let element = null;
442
+ for (let targetElement of targetElements) {
443
+ let target = null;
444
+ if (typeof targetElement === "string") target = event.target instanceof HTMLElement ? event.target.closest(targetElement) : null;
445
+ else target = targetElement;
446
+ if (isLinkElement(target) && resolvedContainer.contains(target)) {
447
+ element = target;
448
+ break;
449
+ }
450
+ }
451
+ if (element) {
452
+ event.preventDefault();
453
+ this.navigate(getNavigationOptions(element));
454
+ }
455
+ });
456
+ let removeURLChangeHandlers = this.on("ready", () => {
457
+ let resolvedParams = resolveParams();
458
+ if (!resolvedParams) return;
459
+ let { resolvedContainer, targetElements } = resolvedParams;
460
+ for (let targetElement of targetElements) if (typeof targetElement === "string") {
461
+ let targets = resolvedContainer.querySelectorAll(targetElement);
462
+ for (let element of targets) toggleActive(element, this);
463
+ } else if (resolvedContainer.contains(targetElement)) toggleActive(targetElement, this);
464
+ });
465
+ return () => {
466
+ removeClickHandlers();
467
+ removeURLChangeHandlers();
468
+ };
469
+ }
470
+ /**
471
+ * Navigates to the URL specified with `options.href`.
472
+ *
473
+ * @example
474
+ * ```js
475
+ * let route = new Route();
476
+ * route.navigate({ href: "/intro", history: "replace", scroll: "off" });
477
+ * ```
478
+ */
479
+ navigate(options) {
480
+ if (!options?.href) return;
481
+ let { href, referrer, ...params } = options;
482
+ let transformedOptions = {
483
+ href: String(href),
484
+ referrer: referrer && String(referrer),
485
+ ...params
486
+ };
487
+ this.setValue(transformedOptions.href, transformedOptions);
488
+ }
489
+ assign(url) {
490
+ this.navigate({ href: url });
491
+ }
492
+ replace(url) {
493
+ this.navigate({
494
+ href: url,
495
+ history: "replace"
496
+ });
497
+ }
498
+ reload() {
499
+ this.assign(this.getValue());
500
+ }
501
+ go(delta) {
502
+ if (typeof window !== "undefined" && window.history) window.history.go(delta);
503
+ }
504
+ back() {
505
+ this.go(-1);
506
+ }
507
+ forward() {
508
+ this.go(1);
509
+ }
510
+ get href() {
511
+ return this.getValue();
512
+ }
513
+ set href(value) {
514
+ this.assign(value);
515
+ }
516
+ get pathname() {
517
+ return new QuasiURL(this.href).pathname;
518
+ }
519
+ set pathname(value) {
520
+ let url = new QuasiURL(this.href);
521
+ url.pathname = String(value);
522
+ this.assign(url.href);
523
+ }
524
+ get search() {
525
+ return new QuasiURL(this.href).search;
526
+ }
527
+ set search(value) {
528
+ let url = new QuasiURL(this.href);
529
+ url.search = value;
530
+ this.assign(url.href);
531
+ }
532
+ get hash() {
533
+ return new QuasiURL(this.href).hash;
534
+ }
535
+ set hash(value) {
536
+ let url = new QuasiURL(this.href);
537
+ url.hash = value;
538
+ this.assign(url.href);
539
+ }
540
+ toString() {
541
+ return this.href;
542
+ }
543
+ /**
544
+ * Matches the current location against `urlPattern`.
545
+ */
546
+ match(urlPattern) {
547
+ return matchURL(urlPattern, this.href);
548
+ }
549
+ /**
550
+ * Compiles `urlPattern` to a URL string by filling out the parameters
551
+ * based on `data`.
552
+ */
553
+ compile(urlPattern, data) {
554
+ return compileURL(urlPattern, data);
555
+ }
556
+ at(urlPattern, x, y) {
557
+ let result = this.match(urlPattern);
558
+ if (x === void 0 && y === void 0) return result.ok;
559
+ if (!result.ok) return typeof y === "function" ? y(result) : y;
560
+ return typeof x === "function" ? x(result) : x;
561
+ }
562
+ };
563
+
564
+ export { EventEmitter, PersistentState, Route, State, URLState, compileURL, getNavigationOptions, getStorageEntry, isLocationObject, isRouteEvent, isState, matchURL };
package/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export * from "./src/EventEmitter.ts";
2
+ export * from "./src/isState.ts";
3
+ export * from "./src/PersistentState.ts";
4
+ export * from "./src/Route.ts";
5
+ export * from "./src/State.ts";
6
+ export * from "./src/types/EventCallback.ts";
7
+ export * from "./src/types/EventCallbackMap.ts";
8
+ export * from "./src/types/LinkElement.ts";
9
+ export * from "./src/types/LocationObject.ts";
10
+ export * from "./src/types/LocationPattern.ts";
11
+ export * from "./src/types/LocationValue.ts";
12
+ export * from "./src/types/MatchHandler.ts";
13
+ export * from "./src/types/MatchState.ts";
14
+ export * from "./src/types/NavigationOptions.ts";
15
+ export * from "./src/types/PersistentStorage.ts";
16
+ export * from "./src/types/URLComponents.ts";
17
+ export * from "./src/types/URLConfig.ts";
18
+ export * from "./src/types/URLData.ts";
19
+ export * from "./src/types/URLSchema.ts";
20
+ export * from "./src/URLState.ts";
21
+ export * from "./src/utils/compileURL.ts";
22
+ export * from "./src/utils/getNavigationOptions.ts";
23
+ export * from "./src/utils/isLocationObject.ts";
24
+ export * from "./src/utils/isRouteEvent.ts";
25
+ export * from "./src/utils/matchURL.ts";
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "statepod",
3
+ "version": "0.4.0",
4
+ "description": "Vanilla TS/JS shared state management and routing",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "type": "module",
9
+ "scripts": {
10
+ "demo": "npx auxsrv tests/URL-based_rendering",
11
+ "preversion": "npx npm-run-all shape test",
12
+ "shape": "npx codeshape",
13
+ "test": "npx npm-run-all test1 test2",
14
+ "test1": "node tests/index.ts",
15
+ "test2": "npx playwright test --project=chromium",
16
+ "typecheck": "npx codeshape typecheck"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/axtk/statepod.git"
21
+ },
22
+ "keywords": [
23
+ "state",
24
+ "shared state",
25
+ "state management",
26
+ "persistent state",
27
+ "routing"
28
+ ],
29
+ "author": "axtk",
30
+ "license": "MIT",
31
+ "devDependencies": {
32
+ "@playwright/test": "^1.61.1",
33
+ "@types/node": "^26.1.1",
34
+ "auxsrv": "^0.3.19",
35
+ "url-shape": "^2.1.6",
36
+ "zod": "^4.4.3"
37
+ },
38
+ "dependencies": {
39
+ "@standard-schema/spec": "^1.1.0",
40
+ "quasiurl": "^1.1.15"
41
+ }
42
+ }
@@ -0,0 +1,72 @@
1
+ import type { EventCallback } from "./types/EventCallback.ts";
2
+ import type { EventCallbackMap } from "./types/EventCallbackMap.ts";
3
+
4
+ export class EventEmitter<
5
+ P extends Record<string, unknown> = Record<string, void>,
6
+ > {
7
+ _callbacks: EventCallbackMap<P> = {};
8
+ _active = true;
9
+ /**
10
+ * Adds an event handler.
11
+ *
12
+ * Returns an unsubscription function. Once it's invoked, the given
13
+ * `callback` is removed and no longer called in response to the event.
14
+ */
15
+ on<E extends string>(event: E, callback: EventCallback<P[E]>) {
16
+ (this._callbacks[event] ??= new Set()).add(callback);
17
+
18
+ return () => this.off(event, callback);
19
+ }
20
+ /**
21
+ * Adds a one-time event handler: once the event is emitted, the callback
22
+ * is called and immediately removed.
23
+ */
24
+ once<E extends string>(event: E, callback: EventCallback<P[E]>) {
25
+ let oneTimeCallback = (payload: P[E]) => {
26
+ this.off(event, oneTimeCallback);
27
+ callback(payload);
28
+ };
29
+
30
+ return this.on(event, oneTimeCallback);
31
+ }
32
+ /**
33
+ * Removes the specified `callback` from the handlers of the given event,
34
+ * and removes all handlers of the given event if `callback` is not
35
+ * specified.
36
+ */
37
+ off<E extends string>(event: E, callback?: EventCallback<P[E]>) {
38
+ if (callback === undefined) delete this._callbacks[event];
39
+ else this._callbacks[event]?.delete(callback);
40
+ }
41
+ /**
42
+ * Emits the specified event. Returns `false` if at least one event callback
43
+ * returns `false`, effectively interrupting the callback call chain.
44
+ * Otherwise returns `true`.
45
+ */
46
+ emit<E extends string>(event: E, payload?: P[E]) {
47
+ let callbacks = this._callbacks[event];
48
+
49
+ if (this._active && callbacks?.size) {
50
+ for (let callback of callbacks) {
51
+ if (callback(payload!) === false) return false;
52
+ }
53
+ }
54
+
55
+ return true;
56
+ }
57
+ get active() {
58
+ return this._active;
59
+ }
60
+ start() {
61
+ if (!this._active) {
62
+ this._active = true;
63
+ this.emit("start");
64
+ }
65
+ }
66
+ stop() {
67
+ if (this._active) {
68
+ this._active = false;
69
+ this.emit("stop");
70
+ }
71
+ }
72
+ }