@quazardous/quarkernel 1.0.9

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 quazardous
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # QuarKernel
2
+
3
+ Micro Custom Event Kernel.
4
+
5
+ ## Features
6
+
7
+ Helps structuring your app with events.
8
+
9
+ - ES6
10
+ - Async() support with Promise
11
+ - Event scope dependency support
12
+ - Composite event
13
+ - Shared context
14
+
15
+ ## Basic usage
16
+
17
+ Define event listeners across your app modules with dependency support.
18
+ Share context between components.
19
+
20
+ ```js
21
+ import { QuarKernel, QuarKernelEvent as QKE } from '@quazardous/quarkernel';
22
+
23
+ // singleton
24
+ const qk = new QuarKernel();
25
+
26
+ qk.addEventListener('my_event', (e) => {
27
+ // something
28
+ notNeeded();
29
+ });
30
+
31
+ // your module foo does some init stuff
32
+ qk.addEventListener('my_event', async (e) => {
33
+ // something async
34
+ e.context.needed = await needed();
35
+ }, 'foo');
36
+
37
+ // somewhere else in your app your module bar is waitint after foo to set a specific context
38
+ qk.addEventListener('my_event', (e) => {
39
+ // something after the async callback
40
+ needing(e.context.needed);
41
+ }, 'bar', 'foo');
42
+
43
+ // call everything
44
+ qk.dispatchEvent(new QKE('my_event')).then(() => {
45
+ // event my_event fully dispatched
46
+ happyEnd();
47
+ });
48
+ // or await qk.dispatchEvent(new QKE('my_event'));
49
+ ```
50
+
51
+ ## Composite event
52
+
53
+ Composite event are auto dispatched when a specific list of events are dispatched.
54
+
55
+ ie. You set a composite event C on A+B. Your code dispatches A, then B. C is auto dispatched after B.
56
+
57
+ ```js
58
+ ...
59
+ // init
60
+ qk.addCompositeEvent(['A','B'], (stack) => new QKE('C'));
61
+ ...
62
+ qk.dispatchEvent(new QKE('A'));
63
+ ...
64
+ // this will auto dispatch C
65
+ qk.dispatchEvent(new QKE('B'));
66
+
67
+ ```
68
+
69
+
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@quazardous/quarkernel",
3
+ "version": "1.0.9",
4
+ "type": "module",
5
+ "description": "Micro Custom Events Kernel",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/quazardous/quarkernel.git"
9
+ },
10
+ "module": "src/index.js",
11
+ "main": "src/index.js",
12
+ "files": [
13
+ "src/",
14
+ "types/"
15
+ ],
16
+ "typings": "./types/QuarKernel.d.ts",
17
+ "scripts": {
18
+ "prepare": "npm run gen-typings",
19
+ "test": "mocha tests/",
20
+ "gen-typings": "npx -p typescript tsc src/lib/*.js --declaration --allowJs --emitDeclarationOnly --outDir types"
21
+ },
22
+ "keywords": [
23
+ "event",
24
+ "kernel",
25
+ "promise"
26
+ ],
27
+ "author": "quazardous <berliozdavid@gmail.com>",
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "@babel/register": "^7.22.5",
31
+ "chai": "^4.3.7",
32
+ "core-js": "^3.29.1",
33
+ "mocha": "^9.2.0"
34
+ },
35
+ "dependencies": {
36
+ "toposort": "^2.0.2"
37
+ }
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { QuarKernel, QuarKernelEvent } from './lib/QuarKernel.js';
2
+
3
+ export { QuarKernel, QuarKernelEvent };
@@ -0,0 +1,342 @@
1
+ /* eslint-disable no-plusplus */
2
+ import toposort from 'toposort';
3
+
4
+ class GraphNode {
5
+ constructor(target, name, requires, callback) {
6
+ this.target = target;
7
+ this.name = name;
8
+ this.requires = requires;
9
+ this.callback = callback;
10
+ }
11
+ }
12
+
13
+ class GraphNodeProcessor {
14
+ /**
15
+ * @param {Array<GraphNode>} nodes
16
+ */
17
+ constructor(nodes) {
18
+ this.processMap = {};
19
+ this.nodes = nodes;
20
+ nodes.forEach(n => {
21
+ this.processMap[n.name] = {
22
+ promise: null,
23
+ node: n
24
+ };
25
+ });
26
+ }
27
+
28
+ processAll(...args) {
29
+ return Promise.all(
30
+ this.nodes.map(n => this.process(n, ...args))
31
+ );
32
+ }
33
+
34
+ process(node, ...args) {
35
+ const process = this.processMap[node.name];
36
+ if (!process.promise) {
37
+ let then = null;
38
+ if (node.callback.constructor.name === 'AsyncFunction') {
39
+ // handle async
40
+ then = () => node.callback(...args, node.target);
41
+ } else {
42
+ then = () => new Promise((resolve) => {
43
+ resolve(node.callback(...args, node.target));
44
+ });
45
+ }
46
+ process.promise = this.processDependencies(node, ...args).then(then);
47
+ }
48
+ return process.promise;
49
+ }
50
+
51
+ processDependencies(node, ...args) {
52
+ if (node.requires.length) {
53
+ const promises = [];
54
+ this.nodes.forEach(n => {
55
+ if (node.requires.includes(n.target)) {
56
+ promises.push(this.process(n, ...args));
57
+ }
58
+ });
59
+ return Promise.all(promises);
60
+ }
61
+ return Promise.resolve();
62
+ }
63
+ }
64
+
65
+ class QuarKernelEvent {
66
+ /**
67
+ * @param {string} type Type of the event
68
+ * @param {Object} [param] Parameters for this event
69
+ * @param {Object} [context] Modifiable context for this event
70
+ */
71
+ constructor(type, param, context) {
72
+ this.type = type;
73
+ this.param = param || {};
74
+ this.context = context || {};
75
+ }
76
+ }
77
+
78
+ // /**
79
+ // * @typedef {Object} Person
80
+ // * @property {string} name how the person is called
81
+ // * @property {number} age how many years the person lived
82
+ // */
83
+
84
+ /**
85
+ * @private
86
+ */
87
+ class CompositeTrigger {
88
+ /**
89
+ * @param {Array<string>} components
90
+ * @param {*} callback
91
+ */
92
+ constructor(components, callback, reset) {
93
+ this.components = components;
94
+ this.callback = callback;
95
+ this.reset = reset;
96
+ /**
97
+ * @type {Object.<string, Array<{e:QuarKernelEvent,p:Promise<*>}>>}
98
+ */
99
+ this.eventPromiseStack = {};
100
+ }
101
+
102
+ /**
103
+ * @param {QuarKernelEvent} e
104
+ * @param {Promise<*>} p
105
+ * @return {Promise<*>|null}
106
+ */
107
+ compose(e, p) {
108
+ if (!this.components.includes(e.type)) {
109
+ return;
110
+ }
111
+ if (typeof this.eventPromiseStack[e.type] === 'undefined') {
112
+ this.eventPromiseStack[e.type] = [];
113
+ }
114
+ this.eventPromiseStack[e.type].push({e, p});
115
+
116
+ let allComponents = true;
117
+ this.components.forEach((type) => {
118
+ if (typeof this.eventPromiseStack[e.type] === 'undefined') {
119
+ allComponents = false;
120
+ }
121
+ });
122
+
123
+ if (!allComponents) {
124
+ return null;
125
+ }
126
+
127
+ // we got all components !
128
+ const stack = this.eventPromiseStack;
129
+ if (this.reset) {
130
+ this.eventPromiseStack = {};
131
+ }
132
+ return new Promise((resolve) => {
133
+ resolve(this.callback(stack));
134
+ });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @callback composeTriggerCallback
140
+ * @param {Object.<string, Array<{e:QuarKernelEvent,p:Promise<*>}>>} [stack] Stack of components events/promises
141
+ */
142
+
143
+ /**
144
+ * @callback composeEventFactory
145
+ * @param {Object.<string, Array<QuarKernelEvent>>} [stack] Stack of components events
146
+ * @return {QuarKernelEvent}
147
+ */
148
+
149
+ /**
150
+ * @callback eventCallback
151
+ * @param {QuarKernelEvent} [e] The event
152
+ * @param {string} [target] The current target
153
+ */
154
+
155
+ /**
156
+ * @callback eventAsyncCallback
157
+ * @async
158
+ * @param {QuarKernelEvent} [e] The event
159
+ * @param {string} [target] The current target
160
+ */
161
+
162
+ class QuarKernel {
163
+ constructor(options = {}) {
164
+ /**
165
+ * For each event type, list of valid target sequences.
166
+ * ie sequence [A, B] means A must be fired before B.
167
+ * @type {Array<string,Array<Array<string>>>}
168
+ * @private
169
+ */
170
+ this.seqGraph = {};
171
+ /**
172
+ * For each event type and target list of direct targets dependencies.
173
+ * @type {Array<string,Array<string,Array<string>>>}
174
+ * @private
175
+ */
176
+ this.dependencies = {};
177
+ /**
178
+ * @type {Array<string,Array<string,Array<eventCallback|eventAsyncCallback>>}
179
+ * @private
180
+ */
181
+ this.callbacks = {};
182
+ /**
183
+ * @private
184
+ */
185
+ this.targetAutoId = 0;
186
+ /**
187
+ * @type {Array<CompositeTrigger>}
188
+ * @private
189
+ */
190
+ this.compositeTriggers = [];
191
+ }
192
+
193
+ /**
194
+ * Register for some event.
195
+ *
196
+ * @param {string} type Type of event to listen to
197
+ * @param {eventCallback|eventAsyncCallback} callback
198
+ * @param {string} [target] A unique code for the target listener
199
+ * @param {string|Array<string>} [dependencies] A list of targets dependencies
200
+ * In the event scope, callbacks will be fired according to dependencies
201
+ */
202
+ addEventListener(type, callback, target, dependencies = []) {
203
+ if (!target) {
204
+ target = `.auto.${this.targetAutoId}`;
205
+ this.targetAutoId++;
206
+ }
207
+ let deps = dependencies;
208
+ if (!Array.isArray(deps)) {
209
+ deps = [deps];
210
+ }
211
+ if (typeof this.seqGraph[type] === 'undefined') {
212
+ this.seqGraph[type] = [];
213
+ }
214
+ deps.forEach((dep) => {
215
+ this.seqGraph[type].push([dep, target]);
216
+ });
217
+ if (typeof this.callbacks[type] === 'undefined') {
218
+ this.callbacks[type] = {};
219
+ }
220
+ if (typeof this.callbacks[type][target] === 'undefined') {
221
+ this.callbacks[type][target] = [];
222
+ }
223
+ this.callbacks[type][target].push(callback);
224
+ }
225
+
226
+ /**
227
+ * Create a composite trigger.
228
+ *
229
+ * @param {Array<string>} components list of event types
230
+ * @param {composeTriggerCallback} callback something to do
231
+ */
232
+ addCompositeTrigger(components, callback, reset = true) {
233
+ this.compositeTriggers.push(new CompositeTrigger(components, callback, reset));
234
+ }
235
+
236
+ /**
237
+ * Create a composite event.
238
+ *
239
+ * @param {Array<string>} components list of event types
240
+ * @param {composeEventFactory} factory event factory
241
+ */
242
+ addCompositeEvent(components, factory, reset = true) {
243
+ this.addCompositeTrigger(components, (stack) => {
244
+ const eventStack = {};
245
+ const list = [];
246
+ for (const type in stack) {
247
+ eventStack[type] = [];
248
+ stack[type].forEach((item) => {
249
+ list.push(item.p);
250
+ eventStack[type].push(item.e);
251
+ })
252
+ }
253
+ const self = this;
254
+ Promise.all(list).then(() => {
255
+ // dispatch after all event promises
256
+ self.dispatchEvent(factory(eventStack));
257
+ });
258
+ }, reset);
259
+ }
260
+
261
+ /**
262
+ * @param {QuarKernelEvent} e
263
+ * @param {Promise<*>} p
264
+ * @return {Promise<*>}
265
+ * @private
266
+ */
267
+ composeTrigger(e, p) {
268
+ const list = [];
269
+ this.compositeTriggers.forEach((ct) => {
270
+ const ctp = ct.compose(e, p);
271
+ if (ctp) {
272
+ list.push(ctp);
273
+ }
274
+ });
275
+ if (list.length > 0) {
276
+ p = p.then(() => Promise.all(list))
277
+ }
278
+ return p;
279
+ }
280
+
281
+ /**
282
+ * Dispatch an event.
283
+ *
284
+ * @param {QuarKernelEvent} e The event
285
+ * @return {Promise<*>}
286
+ */
287
+ dispatchEvent(e) {
288
+ if (!(e instanceof QuarKernelEvent)) {
289
+ throw new Error('Not a QuarKernelEvent');
290
+ }
291
+ if (typeof this.callbacks[e.type] === 'undefined') {
292
+ // no callback registered
293
+ return this.composeTrigger(e, Promise.resolve());
294
+ }
295
+ if (typeof this.seqGraph[e.type] !== 'undefined') {
296
+ // using toposort to early detect dependencies loop
297
+ toposort(this.seqGraph[e.type]);
298
+ }
299
+
300
+ const nodes = [];
301
+
302
+ Object.keys(this.callbacks[e.type]).forEach((target) => {
303
+ this.callbacks[e.type][target].forEach((callback, i) => {
304
+ nodes.push(new GraphNode(
305
+ target,
306
+ `${target}.${i}`, // each callback gets a node
307
+ this.getTargetDependencies(e.type, target),
308
+ callback
309
+ ));
310
+ });
311
+ });
312
+
313
+ return this.composeTrigger(e, (new GraphNodeProcessor(nodes)).processAll(e));
314
+ }
315
+
316
+ /**
317
+ * @param {string} type
318
+ * @param {string} target
319
+ * @return {Array<string>}
320
+ * @private
321
+ */
322
+ getTargetDependencies(type, target) {
323
+ if (typeof this.dependencies[type] === 'undefined') {
324
+ this.dependencies[type] = {};
325
+ }
326
+ if (typeof this.dependencies[type][target] === 'undefined') {
327
+ this.dependencies[type][target] = [];
328
+ if (typeof this.seqGraph[type] !== 'undefined') {
329
+ this.seqGraph[type].forEach((seq) => {
330
+ if (seq[1] === target) {
331
+ this.dependencies[type][target].push(seq[0]);
332
+ }
333
+ });
334
+ // unique trick
335
+ this.dependencies[type][target] = [...new Set(this.dependencies[type][target])];
336
+ }
337
+ }
338
+ return this.dependencies[type][target];
339
+ }
340
+ }
341
+
342
+ export { QuarKernel, QuarKernelEvent };
@@ -0,0 +1,117 @@
1
+ export type composeTriggerCallback = (stack?: {
2
+ [x: string]: Array<{
3
+ e: QuarKernelEvent;
4
+ p: Promise<any>;
5
+ }>;
6
+ }) => any;
7
+ export type composeEventFactory = (stack?: {
8
+ [x: string]: Array<QuarKernelEvent>;
9
+ }) => QuarKernelEvent;
10
+ export type eventCallback = (e?: QuarKernelEvent, target?: string) => any;
11
+ export type eventAsyncCallback = () => any;
12
+ /**
13
+ * @callback composeTriggerCallback
14
+ * @param {Object.<string, Array<{e:QuarKernelEvent,p:Promise<*>}>>} [stack] Stack of components events/promises
15
+ */
16
+ /**
17
+ * @callback composeEventFactory
18
+ * @param {Object.<string, Array<QuarKernelEvent>>} [stack] Stack of components events
19
+ * @return {QuarKernelEvent}
20
+ */
21
+ /**
22
+ * @callback eventCallback
23
+ * @param {QuarKernelEvent} [e] The event
24
+ * @param {string} [target] The current target
25
+ */
26
+ /**
27
+ * @callback eventAsyncCallback
28
+ * @async
29
+ * @param {QuarKernelEvent} [e] The event
30
+ * @param {string} [target] The current target
31
+ */
32
+ export class QuarKernel {
33
+ constructor(options?: {});
34
+ /**
35
+ * For each event type, list of valid target sequences.
36
+ * ie sequence [A, B] means A must be fired before B.
37
+ * @type {Array<string,Array<Array<string>>>}
38
+ * @private
39
+ */
40
+ private seqGraph;
41
+ /**
42
+ * For each event type and target list of direct targets dependencies.
43
+ * @type {Array<string,Array<string,Array<string>>>}
44
+ * @private
45
+ */
46
+ private dependencies;
47
+ /**
48
+ * @type {Array<string,Array<string,Array<eventCallback|eventAsyncCallback>>}
49
+ * @private
50
+ */
51
+ private callbacks;
52
+ /**
53
+ * @private
54
+ */
55
+ private targetAutoId;
56
+ /**
57
+ * @type {Array<CompositeTrigger>}
58
+ * @private
59
+ */
60
+ private compositeTriggers;
61
+ /**
62
+ * Register for some event.
63
+ *
64
+ * @param {string} type Type of event to listen to
65
+ * @param {eventCallback|eventAsyncCallback} callback
66
+ * @param {string} [target] A unique code for the target listener
67
+ * @param {string|Array<string>} [dependencies] A list of targets dependencies
68
+ * In the event scope, callbacks will be fired according to dependencies
69
+ */
70
+ addEventListener(type: string, callback: eventCallback | eventAsyncCallback, target?: string, dependencies?: string | Array<string>): void;
71
+ /**
72
+ * Create a composite trigger.
73
+ *
74
+ * @param {Array<string>} components list of event types
75
+ * @param {composeTriggerCallback} callback something to do
76
+ */
77
+ addCompositeTrigger(components: Array<string>, callback: composeTriggerCallback, reset?: boolean): void;
78
+ /**
79
+ * Create a composite event.
80
+ *
81
+ * @param {Array<string>} components list of event types
82
+ * @param {composeEventFactory} factory event factory
83
+ */
84
+ addCompositeEvent(components: Array<string>, factory: composeEventFactory, reset?: boolean): void;
85
+ /**
86
+ * @param {QuarKernelEvent} e
87
+ * @param {Promise<*>} p
88
+ * @return {Promise<*>}
89
+ * @private
90
+ */
91
+ private composeTrigger;
92
+ /**
93
+ * Dispatch an event.
94
+ *
95
+ * @param {QuarKernelEvent} e The event
96
+ * @return {Promise<*>}
97
+ */
98
+ dispatchEvent(e: QuarKernelEvent): Promise<any>;
99
+ /**
100
+ * @param {string} type
101
+ * @param {string} target
102
+ * @return {Array<string>}
103
+ * @private
104
+ */
105
+ private getTargetDependencies;
106
+ }
107
+ export class QuarKernelEvent {
108
+ /**
109
+ * @param {string} type Type of the event
110
+ * @param {Object} [param] Parameters for this event
111
+ * @param {Object} [context] Modifiable context for this event
112
+ */
113
+ constructor(type: string, param?: any, context?: any);
114
+ type: string;
115
+ param: any;
116
+ context: any;
117
+ }