@quazardous/quarkernel 1.0.12 → 2.2.3
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/README.md +168 -47
- package/dist/create-machine-CYsscHPX.d.cts +275 -0
- package/dist/create-machine-CYsscHPX.d.ts +275 -0
- package/dist/fsm.cjs +1436 -0
- package/dist/fsm.cjs.map +1 -0
- package/dist/fsm.d.cts +2 -0
- package/dist/fsm.d.ts +2 -0
- package/dist/fsm.js +1432 -0
- package/dist/fsm.js.map +1 -0
- package/dist/index-BDf1xZi6.d.cts +896 -0
- package/dist/index-BPMXiW32.d.ts +896 -0
- package/dist/index.cjs +1750 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +1722 -0
- package/dist/index.js.map +1 -0
- package/dist/xstate.cjs +190 -0
- package/dist/xstate.cjs.map +1 -0
- package/dist/xstate.d.cts +152 -0
- package/dist/xstate.d.ts +152 -0
- package/dist/xstate.js +186 -0
- package/dist/xstate.js.map +1 -0
- package/package.json +63 -24
- package/LICENSE +0 -21
- package/src/index.js +0 -3
- package/src/lib/QuarKernel.js +0 -405
- package/types/QuarKernel.d.ts +0 -132
package/dist/fsm.js
ADDED
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
// src/kernel-event.ts
|
|
2
|
+
var KernelEvent = class {
|
|
3
|
+
/**
|
|
4
|
+
* Event name/type
|
|
5
|
+
*/
|
|
6
|
+
name;
|
|
7
|
+
/**
|
|
8
|
+
* Event payload (typed, immutable)
|
|
9
|
+
*/
|
|
10
|
+
data;
|
|
11
|
+
/**
|
|
12
|
+
* Shared mutable context for passing data between listeners
|
|
13
|
+
*/
|
|
14
|
+
context;
|
|
15
|
+
/**
|
|
16
|
+
* Event creation timestamp (milliseconds since epoch)
|
|
17
|
+
*/
|
|
18
|
+
timestamp;
|
|
19
|
+
/**
|
|
20
|
+
* Internal flag: stop propagation to remaining listeners
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
_propagationStopped = false;
|
|
24
|
+
constructor(name, data, context = {}) {
|
|
25
|
+
this.name = name;
|
|
26
|
+
this.data = data;
|
|
27
|
+
this.context = context;
|
|
28
|
+
this.timestamp = Date.now();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Stop propagation to remaining listeners in the chain
|
|
32
|
+
* Similar to DOM Event.stopPropagation()
|
|
33
|
+
*
|
|
34
|
+
* After calling this, no more listeners will execute.
|
|
35
|
+
*/
|
|
36
|
+
stopPropagation = () => {
|
|
37
|
+
this._propagationStopped = true;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Check if propagation was stopped
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
get isPropagationStopped() {
|
|
44
|
+
return this._propagationStopped;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/listener-context.ts
|
|
49
|
+
var ListenerContext = class {
|
|
50
|
+
/** Unique identifier for this listener instance */
|
|
51
|
+
id;
|
|
52
|
+
/** Event name this listener is registered for */
|
|
53
|
+
eventName;
|
|
54
|
+
/** Listener priority (higher = earlier execution) */
|
|
55
|
+
priority;
|
|
56
|
+
/** Listener dependencies (IDs of listeners that must execute first) */
|
|
57
|
+
dependencies;
|
|
58
|
+
/** AbortSignal for cancellation (if provided during registration) */
|
|
59
|
+
signal;
|
|
60
|
+
/** Reference to the kernel instance for emit/off operations */
|
|
61
|
+
kernel;
|
|
62
|
+
/** Reference to the listener function for removal */
|
|
63
|
+
listenerFn;
|
|
64
|
+
/** Current event being processed (set during emit) */
|
|
65
|
+
currentEvent;
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new ListenerContext.
|
|
68
|
+
* @internal Use Kernel.on() to register listeners, which creates contexts automatically.
|
|
69
|
+
*/
|
|
70
|
+
constructor(id, eventName, priority, dependencies, kernel, listenerFn, signal) {
|
|
71
|
+
this.id = id;
|
|
72
|
+
this.eventName = eventName;
|
|
73
|
+
this.priority = priority;
|
|
74
|
+
this.dependencies = dependencies;
|
|
75
|
+
this.kernel = kernel;
|
|
76
|
+
this.listenerFn = listenerFn;
|
|
77
|
+
this.signal = signal;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Sets the current event being processed.
|
|
81
|
+
* @internal Called by Kernel during emit()
|
|
82
|
+
*/
|
|
83
|
+
setCurrentEvent = (event) => {
|
|
84
|
+
this.currentEvent = event;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Clears the current event after processing.
|
|
88
|
+
* @internal Called by Kernel after listener execution
|
|
89
|
+
*/
|
|
90
|
+
clearCurrentEvent = () => {
|
|
91
|
+
this.currentEvent = void 0;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Removes this listener from the kernel.
|
|
95
|
+
* Alias for kernel.off() with this listener's reference.
|
|
96
|
+
*/
|
|
97
|
+
cancel = () => {
|
|
98
|
+
this.kernel.off(this.eventName, this.listenerFn);
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Alias for cancel() to match common naming patterns.
|
|
102
|
+
*/
|
|
103
|
+
off = () => {
|
|
104
|
+
this.cancel();
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Emits an event from within this listener.
|
|
108
|
+
* Delegates to kernel.emit().
|
|
109
|
+
*/
|
|
110
|
+
emit = async (eventName, data) => {
|
|
111
|
+
return this.kernel.emit(eventName, data);
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Stops propagation of the current event to remaining listeners.
|
|
115
|
+
* Requires an event to be currently processing.
|
|
116
|
+
*/
|
|
117
|
+
stopPropagation = () => {
|
|
118
|
+
if (!this.currentEvent) {
|
|
119
|
+
throw new Error("stopPropagation() can only be called during event processing");
|
|
120
|
+
}
|
|
121
|
+
this.currentEvent.stopPropagation();
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/wildcard.ts
|
|
126
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
127
|
+
var MAX_CACHE_SIZE = 100;
|
|
128
|
+
var patternToRegex = (pattern, delimiter = ":") => {
|
|
129
|
+
const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
130
|
+
if (pattern === "*") {
|
|
131
|
+
return new RegExp(`^[^${escapedDelimiter}]+$`);
|
|
132
|
+
}
|
|
133
|
+
if (pattern === "**") {
|
|
134
|
+
return new RegExp("^.*$");
|
|
135
|
+
}
|
|
136
|
+
const regexPattern = pattern.replace(/\*\*/g, "___DOUBLE_WILDCARD___").replace(/\*/g, `[^${escapedDelimiter}]+`).replace(/___DOUBLE_WILDCARD___/g, ".*");
|
|
137
|
+
return new RegExp(`^${regexPattern}$`);
|
|
138
|
+
};
|
|
139
|
+
var hasWildcard = (pattern) => {
|
|
140
|
+
return pattern.includes("*");
|
|
141
|
+
};
|
|
142
|
+
var getPatternRegex = (pattern, delimiter = ":") => {
|
|
143
|
+
const cacheKey = `${pattern}::${delimiter}`;
|
|
144
|
+
let regex = patternCache.get(cacheKey);
|
|
145
|
+
if (!regex) {
|
|
146
|
+
regex = patternToRegex(pattern, delimiter);
|
|
147
|
+
if (patternCache.size >= MAX_CACHE_SIZE) {
|
|
148
|
+
const firstKey = patternCache.keys().next().value;
|
|
149
|
+
if (firstKey !== void 0) {
|
|
150
|
+
patternCache.delete(firstKey);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
patternCache.set(cacheKey, regex);
|
|
154
|
+
}
|
|
155
|
+
return regex;
|
|
156
|
+
};
|
|
157
|
+
var matchesPattern = (eventName, pattern, delimiter = ":") => {
|
|
158
|
+
if (!hasWildcard(pattern)) {
|
|
159
|
+
return eventName === pattern;
|
|
160
|
+
}
|
|
161
|
+
const regex = getPatternRegex(pattern, delimiter);
|
|
162
|
+
return regex.test(eventName);
|
|
163
|
+
};
|
|
164
|
+
var findMatchingPatterns = (eventName, patterns, delimiter = ":") => {
|
|
165
|
+
return patterns.filter((pattern) => matchesPattern(eventName, pattern, delimiter));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/toposort.ts
|
|
169
|
+
var CyclicDependencyError = class extends Error {
|
|
170
|
+
constructor(cycle) {
|
|
171
|
+
super(`Cyclic dependency detected: ${cycle.join(" -> ")}`);
|
|
172
|
+
this.cycle = cycle;
|
|
173
|
+
this.name = "CyclicDependencyError";
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var toposort = (nodes) => {
|
|
177
|
+
const graph = /* @__PURE__ */ new Map();
|
|
178
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
179
|
+
const allNodes = /* @__PURE__ */ new Set();
|
|
180
|
+
nodes.forEach((node) => {
|
|
181
|
+
allNodes.add(node.id);
|
|
182
|
+
if (!graph.has(node.id)) {
|
|
183
|
+
graph.set(node.id, []);
|
|
184
|
+
}
|
|
185
|
+
if (!inDegree.has(node.id)) {
|
|
186
|
+
inDegree.set(node.id, 0);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
nodes.forEach((node) => {
|
|
190
|
+
node.after.forEach((dep) => {
|
|
191
|
+
if (!allNodes.has(dep)) {
|
|
192
|
+
allNodes.add(dep);
|
|
193
|
+
graph.set(dep, []);
|
|
194
|
+
inDegree.set(dep, 0);
|
|
195
|
+
}
|
|
196
|
+
graph.get(dep).push(node.id);
|
|
197
|
+
inDegree.set(node.id, (inDegree.get(node.id) || 0) + 1);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
const queue = [];
|
|
201
|
+
const result = [];
|
|
202
|
+
allNodes.forEach((id) => {
|
|
203
|
+
if (inDegree.get(id) === 0) {
|
|
204
|
+
queue.push(id);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
while (queue.length > 0) {
|
|
208
|
+
const current = queue.shift();
|
|
209
|
+
result.push(current);
|
|
210
|
+
const neighbors = graph.get(current) || [];
|
|
211
|
+
neighbors.forEach((neighbor) => {
|
|
212
|
+
const newDegree = inDegree.get(neighbor) - 1;
|
|
213
|
+
inDegree.set(neighbor, newDegree);
|
|
214
|
+
if (newDegree === 0) {
|
|
215
|
+
queue.push(neighbor);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (result.length !== allNodes.size) {
|
|
220
|
+
const remaining = Array.from(allNodes).filter((id) => !result.includes(id));
|
|
221
|
+
const cycle = detectCycle(remaining, graph);
|
|
222
|
+
throw new CyclicDependencyError(cycle);
|
|
223
|
+
}
|
|
224
|
+
return result.filter((id) => nodes.some((n) => n.id === id));
|
|
225
|
+
};
|
|
226
|
+
var detectCycle = (remaining, graph) => {
|
|
227
|
+
const visited = /* @__PURE__ */ new Set();
|
|
228
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
229
|
+
const path = [];
|
|
230
|
+
const dfs = (node) => {
|
|
231
|
+
visited.add(node);
|
|
232
|
+
recStack.add(node);
|
|
233
|
+
path.push(node);
|
|
234
|
+
const neighbors = graph.get(node) || [];
|
|
235
|
+
for (const neighbor of neighbors) {
|
|
236
|
+
if (!remaining.includes(neighbor)) continue;
|
|
237
|
+
if (!visited.has(neighbor)) {
|
|
238
|
+
if (dfs(neighbor)) return true;
|
|
239
|
+
} else if (recStack.has(neighbor)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
recStack.delete(node);
|
|
244
|
+
path.pop();
|
|
245
|
+
return false;
|
|
246
|
+
};
|
|
247
|
+
for (const node of remaining) {
|
|
248
|
+
if (!visited.has(node)) {
|
|
249
|
+
if (dfs(node)) {
|
|
250
|
+
return [...path];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return remaining;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/composition/mergers/namespaced.ts
|
|
258
|
+
var createNamespacedMerger = () => ({
|
|
259
|
+
merge: (contexts, _sources) => {
|
|
260
|
+
const result = {};
|
|
261
|
+
for (const [eventName, context] of contexts) {
|
|
262
|
+
for (const [key, value] of Object.entries(context)) {
|
|
263
|
+
result[`${eventName}:${key}`] = value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
},
|
|
268
|
+
mergeWithConflicts: (contexts, _sources) => {
|
|
269
|
+
const result = {};
|
|
270
|
+
for (const [eventName, context] of contexts) {
|
|
271
|
+
for (const [key, value] of Object.entries(context)) {
|
|
272
|
+
result[`${eventName}:${key}`] = value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
context: result,
|
|
277
|
+
conflicts: []
|
|
278
|
+
// No conflicts possible with namespacing
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// src/composition/composition.ts
|
|
284
|
+
var COMPOSED_EVENT = "__qk:composed__";
|
|
285
|
+
var Composition = class {
|
|
286
|
+
kernel;
|
|
287
|
+
subscriptions = [];
|
|
288
|
+
buffers = /* @__PURE__ */ new Map();
|
|
289
|
+
merger;
|
|
290
|
+
bufferLimit;
|
|
291
|
+
reset;
|
|
292
|
+
eventTTL;
|
|
293
|
+
eventTTLs;
|
|
294
|
+
sourceEvents = /* @__PURE__ */ new Set();
|
|
295
|
+
firedSinceLastComposite = /* @__PURE__ */ new Set();
|
|
296
|
+
lastConflicts = [];
|
|
297
|
+
expirationTimers = /* @__PURE__ */ new Map();
|
|
298
|
+
/**
|
|
299
|
+
* Create a new Composition
|
|
300
|
+
*
|
|
301
|
+
* @param kernels - Array of [kernel, eventName] tuples to subscribe to
|
|
302
|
+
* @param options - Composition options
|
|
303
|
+
*/
|
|
304
|
+
constructor(kernels, options = {}) {
|
|
305
|
+
if (kernels.length === 0) {
|
|
306
|
+
throw new Error("Composition requires at least one kernel");
|
|
307
|
+
}
|
|
308
|
+
this.merger = options.merger ?? createNamespacedMerger();
|
|
309
|
+
this.bufferLimit = options.bufferLimit ?? 100;
|
|
310
|
+
this.reset = options.reset ?? true;
|
|
311
|
+
this.eventTTL = options.eventTTL ?? 0;
|
|
312
|
+
this.eventTTLs = options.eventTTLs ?? {};
|
|
313
|
+
this.kernel = new Kernel({
|
|
314
|
+
debug: false,
|
|
315
|
+
errorBoundary: true,
|
|
316
|
+
onContextConflict: options.onConflict ? (key, values) => {
|
|
317
|
+
options.onConflict({
|
|
318
|
+
key,
|
|
319
|
+
sources: [],
|
|
320
|
+
values
|
|
321
|
+
});
|
|
322
|
+
} : void 0
|
|
323
|
+
});
|
|
324
|
+
for (const [kernel, eventName] of kernels) {
|
|
325
|
+
this.subscribeToKernel(kernel, eventName);
|
|
326
|
+
this.sourceEvents.add(eventName);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Subscribe to events from a source kernel
|
|
331
|
+
*/
|
|
332
|
+
subscribeToKernel(kernel, eventName) {
|
|
333
|
+
this.buffers.set(eventName, []);
|
|
334
|
+
const unbind = kernel.on(
|
|
335
|
+
eventName,
|
|
336
|
+
async (event) => {
|
|
337
|
+
await this.handleSourceEvent(eventName, event);
|
|
338
|
+
},
|
|
339
|
+
{ priority: -Infinity }
|
|
340
|
+
);
|
|
341
|
+
this.subscriptions.push({
|
|
342
|
+
kernel,
|
|
343
|
+
eventName,
|
|
344
|
+
unbind
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get the effective TTL for a specific event
|
|
349
|
+
* Priority: per-event TTL > global TTL > 0 (permanent)
|
|
350
|
+
*/
|
|
351
|
+
getEffectiveTTL(eventName) {
|
|
352
|
+
const perEventTTL = this.eventTTLs[eventName];
|
|
353
|
+
if (perEventTTL !== void 0) {
|
|
354
|
+
return perEventTTL;
|
|
355
|
+
}
|
|
356
|
+
return this.eventTTL > 0 ? this.eventTTL : "permanent";
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Handle an event from a source kernel
|
|
360
|
+
*/
|
|
361
|
+
async handleSourceEvent(eventName, event) {
|
|
362
|
+
const buffer = this.buffers.get(eventName);
|
|
363
|
+
if (!buffer) return;
|
|
364
|
+
const effectiveTTL = this.getEffectiveTTL(eventName);
|
|
365
|
+
const eventId = `${eventName}:${event.timestamp}:${Math.random().toString(36).slice(2, 8)}`;
|
|
366
|
+
buffer.push({
|
|
367
|
+
name: event.name,
|
|
368
|
+
data: event.data,
|
|
369
|
+
context: { ...event.context },
|
|
370
|
+
timestamp: event.timestamp
|
|
371
|
+
});
|
|
372
|
+
if (buffer.length > this.bufferLimit) {
|
|
373
|
+
buffer.shift();
|
|
374
|
+
}
|
|
375
|
+
this.firedSinceLastComposite.add(eventName);
|
|
376
|
+
const compositionCompleted = await this.checkAndEmitComposite();
|
|
377
|
+
if (effectiveTTL === "instant" && !compositionCompleted) {
|
|
378
|
+
buffer.pop();
|
|
379
|
+
this.firedSinceLastComposite.delete(eventName);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (typeof effectiveTTL === "number" && effectiveTTL > 0) {
|
|
383
|
+
const timer = setTimeout(() => {
|
|
384
|
+
this.expireEvent(eventName, event.timestamp);
|
|
385
|
+
this.expirationTimers.delete(eventId);
|
|
386
|
+
}, effectiveTTL);
|
|
387
|
+
this.expirationTimers.set(eventId, timer);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Expire an event from the buffer based on timestamp
|
|
392
|
+
*/
|
|
393
|
+
expireEvent(eventName, timestamp) {
|
|
394
|
+
const buffer = this.buffers.get(eventName);
|
|
395
|
+
if (!buffer) return;
|
|
396
|
+
const filtered = buffer.filter((e) => e.timestamp > timestamp);
|
|
397
|
+
this.buffers.set(eventName, filtered);
|
|
398
|
+
if (filtered.length === 0) {
|
|
399
|
+
this.firedSinceLastComposite.delete(eventName);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Check if all source events have fired and emit composite event
|
|
404
|
+
* @returns true if composite was emitted, false otherwise
|
|
405
|
+
*/
|
|
406
|
+
async checkAndEmitComposite() {
|
|
407
|
+
for (const eventName of this.sourceEvents) {
|
|
408
|
+
const buffer = this.buffers.get(eventName);
|
|
409
|
+
if (!buffer || buffer.length === 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const eventName of this.sourceEvents) {
|
|
414
|
+
if (!this.firedSinceLastComposite.has(eventName)) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (this.kernel.listenerCount(COMPOSED_EVENT) === 0) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
422
|
+
const sources = Array.from(this.sourceEvents);
|
|
423
|
+
for (const eventName of sources) {
|
|
424
|
+
const buffer = this.buffers.get(eventName);
|
|
425
|
+
if (!buffer || buffer.length === 0) continue;
|
|
426
|
+
const latestEvent = buffer[buffer.length - 1];
|
|
427
|
+
const eventData = latestEvent.data && typeof latestEvent.data === "object" ? latestEvent.data : {};
|
|
428
|
+
const combined = { ...eventData, ...latestEvent.context };
|
|
429
|
+
contexts.set(eventName, combined);
|
|
430
|
+
}
|
|
431
|
+
const mergeResult = this.merger.mergeWithConflicts(contexts, sources);
|
|
432
|
+
this.lastConflicts = mergeResult.conflicts;
|
|
433
|
+
await this.kernel.emit(COMPOSED_EVENT, {
|
|
434
|
+
sources,
|
|
435
|
+
contexts: Object.fromEntries(contexts),
|
|
436
|
+
merged: mergeResult.context
|
|
437
|
+
});
|
|
438
|
+
this.firedSinceLastComposite.clear();
|
|
439
|
+
if (this.reset) {
|
|
440
|
+
for (const eventName of this.sourceEvents) {
|
|
441
|
+
const buffer = this.buffers.get(eventName);
|
|
442
|
+
if (buffer && buffer.length > 0) {
|
|
443
|
+
const latest = buffer[buffer.length - 1];
|
|
444
|
+
this.buffers.set(eventName, [latest]);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Register a listener for when all source events have fired
|
|
452
|
+
* This is the primary way to react to composition completion
|
|
453
|
+
*
|
|
454
|
+
* @param listener - Function called with merged context when composition completes
|
|
455
|
+
* @param options - Listener options (priority, id, etc.)
|
|
456
|
+
* @returns Unbind function to remove the listener
|
|
457
|
+
*/
|
|
458
|
+
onComposed(listener, options) {
|
|
459
|
+
return this.kernel.on(COMPOSED_EVENT, listener, options);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Remove a listener for composed events
|
|
463
|
+
*/
|
|
464
|
+
offComposed(listener) {
|
|
465
|
+
this.kernel.off(COMPOSED_EVENT, listener);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get number of composed event listeners
|
|
469
|
+
*/
|
|
470
|
+
composedListenerCount() {
|
|
471
|
+
return this.kernel.listenerCount(COMPOSED_EVENT);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Register a listener for events on internal kernel
|
|
475
|
+
* Note: Use onComposed() to listen for composition completion
|
|
476
|
+
*/
|
|
477
|
+
on(eventName, listener, options) {
|
|
478
|
+
return this.kernel.on(eventName, listener, options);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Remove a listener
|
|
482
|
+
* Delegates to internal kernel
|
|
483
|
+
*/
|
|
484
|
+
off(eventName, listener) {
|
|
485
|
+
this.kernel.off(eventName, listener);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Emit an event through the composition
|
|
489
|
+
* Note: Reserved internal events (prefixed with __qk:) cannot be emitted
|
|
490
|
+
*/
|
|
491
|
+
async emit(eventName, data) {
|
|
492
|
+
if (String(eventName).startsWith("__qk:")) {
|
|
493
|
+
throw new Error(`Cannot emit reserved event: ${String(eventName)}`);
|
|
494
|
+
}
|
|
495
|
+
return this.kernel.emit(eventName, data);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get merged context from latest buffered events
|
|
499
|
+
* Does not emit - just returns the merged context
|
|
500
|
+
*/
|
|
501
|
+
getContext() {
|
|
502
|
+
for (const eventName of this.sourceEvents) {
|
|
503
|
+
const buffer = this.buffers.get(eventName);
|
|
504
|
+
if (!buffer || buffer.length === 0) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
509
|
+
const sources = Array.from(this.sourceEvents);
|
|
510
|
+
for (const eventName of sources) {
|
|
511
|
+
const buffer = this.buffers.get(eventName);
|
|
512
|
+
if (!buffer || buffer.length === 0) continue;
|
|
513
|
+
const latestEvent = buffer[buffer.length - 1];
|
|
514
|
+
const eventData = latestEvent.data && typeof latestEvent.data === "object" ? latestEvent.data : {};
|
|
515
|
+
const combined = { ...eventData, ...latestEvent.context };
|
|
516
|
+
contexts.set(eventName, combined);
|
|
517
|
+
}
|
|
518
|
+
const mergeResult = this.merger.mergeWithConflicts(contexts, sources);
|
|
519
|
+
this.lastConflicts = mergeResult.conflicts;
|
|
520
|
+
return mergeResult.context;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Get number of listeners for an event
|
|
524
|
+
*/
|
|
525
|
+
listenerCount(eventName) {
|
|
526
|
+
return this.kernel.listenerCount(eventName);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Get all event names with registered listeners
|
|
530
|
+
*/
|
|
531
|
+
eventNames() {
|
|
532
|
+
return this.kernel.eventNames();
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Remove all listeners
|
|
536
|
+
*/
|
|
537
|
+
offAll(eventName) {
|
|
538
|
+
this.kernel.offAll(eventName);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Enable/disable debug mode
|
|
542
|
+
*/
|
|
543
|
+
debug(enabled) {
|
|
544
|
+
this.kernel.debug(enabled);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Get buffer for a specific source event (for debugging)
|
|
548
|
+
*/
|
|
549
|
+
getBuffer(eventName) {
|
|
550
|
+
return this.buffers.get(eventName);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Clear all buffers
|
|
554
|
+
*/
|
|
555
|
+
clearBuffers() {
|
|
556
|
+
for (const eventName of this.sourceEvents) {
|
|
557
|
+
this.buffers.set(eventName, []);
|
|
558
|
+
}
|
|
559
|
+
this.firedSinceLastComposite.clear();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get conflicts detected during the last merge operation
|
|
563
|
+
*
|
|
564
|
+
* Returns an array of ConflictInfo objects describing which source events
|
|
565
|
+
* provided conflicting values for the same context keys.
|
|
566
|
+
*
|
|
567
|
+
* @returns Array of conflicts from the last merge, or empty array if no conflicts
|
|
568
|
+
*/
|
|
569
|
+
getConflicts() {
|
|
570
|
+
return this.lastConflicts;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Cleanup all subscriptions and listeners
|
|
574
|
+
*/
|
|
575
|
+
dispose() {
|
|
576
|
+
for (const sub of this.subscriptions) {
|
|
577
|
+
sub.unbind();
|
|
578
|
+
}
|
|
579
|
+
this.subscriptions = [];
|
|
580
|
+
for (const timer of this.expirationTimers.values()) {
|
|
581
|
+
clearTimeout(timer);
|
|
582
|
+
}
|
|
583
|
+
this.expirationTimers.clear();
|
|
584
|
+
this.kernel.offAll();
|
|
585
|
+
this.buffers.clear();
|
|
586
|
+
this.sourceEvents.clear();
|
|
587
|
+
this.firedSinceLastComposite.clear();
|
|
588
|
+
this.lastConflicts = [];
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get the configured global event TTL in milliseconds
|
|
592
|
+
* @returns The TTL value, or 0 if no TTL is configured
|
|
593
|
+
*/
|
|
594
|
+
getEventTTL() {
|
|
595
|
+
return this.eventTTL;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Set the global event TTL in milliseconds
|
|
599
|
+
* @param ttl - TTL in milliseconds (0 = permanent)
|
|
600
|
+
*/
|
|
601
|
+
setEventTTL(ttl) {
|
|
602
|
+
this.eventTTL = ttl;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Get per-event TTL configuration
|
|
606
|
+
* @returns The eventTTLs configuration object
|
|
607
|
+
*/
|
|
608
|
+
getEventTTLs() {
|
|
609
|
+
return this.eventTTLs;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Set TTL for a specific event
|
|
613
|
+
* @param eventName - The event name to configure
|
|
614
|
+
* @param ttl - TTL value: number (ms), 'permanent', or 'instant'
|
|
615
|
+
*/
|
|
616
|
+
setEventTTLFor(eventName, ttl) {
|
|
617
|
+
this.eventTTLs[eventName] = ttl;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Remove per-event TTL configuration (falls back to global TTL)
|
|
621
|
+
* @param eventName - The event name to reset
|
|
622
|
+
*/
|
|
623
|
+
clearEventTTLFor(eventName) {
|
|
624
|
+
delete this.eventTTLs[eventName];
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/kernel.ts
|
|
629
|
+
var Kernel = class _Kernel {
|
|
630
|
+
listeners = /* @__PURE__ */ new Map();
|
|
631
|
+
options;
|
|
632
|
+
listenerIdCounter = 0;
|
|
633
|
+
executionErrors = [];
|
|
634
|
+
constructor(options = {}) {
|
|
635
|
+
this.options = {
|
|
636
|
+
delimiter: options.delimiter ?? ":",
|
|
637
|
+
wildcard: options.wildcard ?? true,
|
|
638
|
+
maxListeners: options.maxListeners ?? Infinity,
|
|
639
|
+
debug: options.debug ?? false,
|
|
640
|
+
errorBoundary: options.errorBoundary ?? true,
|
|
641
|
+
onError: options.onError ?? ((error) => {
|
|
642
|
+
console.error("Kernel error:", error);
|
|
643
|
+
}),
|
|
644
|
+
contextMerger: options.contextMerger ?? void 0,
|
|
645
|
+
onContextConflict: options.onContextConflict ?? void 0
|
|
646
|
+
};
|
|
647
|
+
if (this.options.debug) {
|
|
648
|
+
console.debug("[QuarKernel] Kernel initialized", {
|
|
649
|
+
delimiter: this.options.delimiter,
|
|
650
|
+
wildcard: this.options.wildcard,
|
|
651
|
+
maxListeners: this.options.maxListeners,
|
|
652
|
+
errorBoundary: this.options.errorBoundary
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Register an event listener
|
|
658
|
+
* Returns an unbind function for cleanup
|
|
659
|
+
*/
|
|
660
|
+
on(eventName, listener, options = {}) {
|
|
661
|
+
const event = String(eventName);
|
|
662
|
+
if (options.signal?.aborted) {
|
|
663
|
+
if (this.options.debug) {
|
|
664
|
+
console.debug("[QuarKernel] Listener not added (signal already aborted)", {
|
|
665
|
+
event
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return () => {
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const priority = options.priority ?? 0;
|
|
672
|
+
const id = options.id ?? `listener_${++this.listenerIdCounter}`;
|
|
673
|
+
const after = Array.isArray(options.after) ? options.after : options.after ? [options.after] : [];
|
|
674
|
+
let abortListener;
|
|
675
|
+
if (options.signal) {
|
|
676
|
+
abortListener = () => this.off(event, listener);
|
|
677
|
+
}
|
|
678
|
+
const entry = {
|
|
679
|
+
id,
|
|
680
|
+
callback: listener,
|
|
681
|
+
after,
|
|
682
|
+
priority,
|
|
683
|
+
once: options.once ?? false,
|
|
684
|
+
original: listener,
|
|
685
|
+
signal: options.signal,
|
|
686
|
+
abortListener
|
|
687
|
+
};
|
|
688
|
+
const entries = this.listeners.get(event) ?? [];
|
|
689
|
+
entries.push(entry);
|
|
690
|
+
entries.sort((a, b) => b.priority - a.priority);
|
|
691
|
+
this.listeners.set(event, entries);
|
|
692
|
+
if (this.options.debug) {
|
|
693
|
+
console.debug("[QuarKernel] Listener added", {
|
|
694
|
+
event,
|
|
695
|
+
listenerId: id,
|
|
696
|
+
priority,
|
|
697
|
+
after,
|
|
698
|
+
once: entry.once,
|
|
699
|
+
totalListeners: entries.length
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (this.options.maxListeners > 0 && entries.length > this.options.maxListeners) {
|
|
703
|
+
console.warn(
|
|
704
|
+
`MaxListenersExceeded: Event "${event}" has ${entries.length} listeners (limit: ${this.options.maxListeners})`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (options.signal && abortListener) {
|
|
708
|
+
options.signal.addEventListener("abort", abortListener, { once: true });
|
|
709
|
+
}
|
|
710
|
+
return () => this.off(event, listener);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Remove an event listener
|
|
714
|
+
* If no listener provided, removes all listeners for the event
|
|
715
|
+
*/
|
|
716
|
+
off(eventName, listener) {
|
|
717
|
+
const entries = this.listeners.get(eventName);
|
|
718
|
+
if (!entries) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (!listener) {
|
|
722
|
+
if (this.options.debug) {
|
|
723
|
+
console.debug("[QuarKernel] All listeners removed", {
|
|
724
|
+
event: eventName,
|
|
725
|
+
count: entries.length
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
for (const entry of entries) {
|
|
729
|
+
if (entry.signal && entry.abortListener) {
|
|
730
|
+
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
this.listeners.delete(eventName);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const entryToRemove = entries.find((entry) => entry.original === listener);
|
|
737
|
+
if (entryToRemove?.signal && entryToRemove.abortListener) {
|
|
738
|
+
entryToRemove.signal.removeEventListener("abort", entryToRemove.abortListener);
|
|
739
|
+
}
|
|
740
|
+
const filtered = entries.filter((entry) => entry.original !== listener);
|
|
741
|
+
const removed = entries.length - filtered.length;
|
|
742
|
+
if (this.options.debug && removed > 0) {
|
|
743
|
+
console.debug("[QuarKernel] Listener removed", {
|
|
744
|
+
event: eventName,
|
|
745
|
+
removed,
|
|
746
|
+
remaining: filtered.length
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if (filtered.length === 0) {
|
|
750
|
+
this.listeners.delete(eventName);
|
|
751
|
+
} else {
|
|
752
|
+
this.listeners.set(eventName, filtered);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Emit an event
|
|
757
|
+
* Executes all registered listeners in parallel (by default)
|
|
758
|
+
* Returns a Promise that resolves when all listeners complete
|
|
759
|
+
* Throws AggregateError if any listeners failed
|
|
760
|
+
*/
|
|
761
|
+
async emit(eventName, data) {
|
|
762
|
+
const event = String(eventName);
|
|
763
|
+
const allPatterns = Array.from(this.listeners.keys());
|
|
764
|
+
const matchingPatterns = this.options.wildcard ? findMatchingPatterns(event, allPatterns, this.options.delimiter) : allPatterns.filter((p) => p === event);
|
|
765
|
+
const allEntries = [];
|
|
766
|
+
for (const pattern of matchingPatterns) {
|
|
767
|
+
const entries = this.listeners.get(pattern);
|
|
768
|
+
if (entries) {
|
|
769
|
+
allEntries.push(...entries);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (allEntries.length === 0) {
|
|
773
|
+
if (this.options.debug) {
|
|
774
|
+
console.debug("[QuarKernel] Event emitted (no listeners)", { event });
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (this.options.debug) {
|
|
779
|
+
console.debug("[QuarKernel] Event emitted", {
|
|
780
|
+
event,
|
|
781
|
+
listenerCount: allEntries.length,
|
|
782
|
+
data: data !== void 0 ? JSON.stringify(data).substring(0, 100) : void 0
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
this.executionErrors = [];
|
|
786
|
+
const kernelEvent = new KernelEvent(
|
|
787
|
+
event,
|
|
788
|
+
data,
|
|
789
|
+
{}
|
|
790
|
+
);
|
|
791
|
+
const sortedEntries = this.sortListenersByDependencies(allEntries);
|
|
792
|
+
const promises = sortedEntries.map(
|
|
793
|
+
(entry) => this.executeListener(entry, kernelEvent, event)
|
|
794
|
+
);
|
|
795
|
+
const results = await Promise.allSettled(promises);
|
|
796
|
+
this.removeOnceListeners(event, sortedEntries, kernelEvent);
|
|
797
|
+
if (!this.options.errorBoundary) {
|
|
798
|
+
const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
799
|
+
if (errors.length > 0) {
|
|
800
|
+
throw new AggregateError(errors, `${errors.length} listener(s) failed for event "${event}"`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (this.options.debug) {
|
|
804
|
+
console.debug("[QuarKernel] Event completed", {
|
|
805
|
+
event
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Emit an event with serial execution
|
|
811
|
+
* Executes listeners sequentially (one after another) instead of in parallel
|
|
812
|
+
* Respects the same dependency and priority ordering as emit()
|
|
813
|
+
* Stops on first error if errorBoundary is false, otherwise continues and collects errors
|
|
814
|
+
*/
|
|
815
|
+
async emitSerial(eventName, data) {
|
|
816
|
+
const event = String(eventName);
|
|
817
|
+
const allPatterns = Array.from(this.listeners.keys());
|
|
818
|
+
const matchingPatterns = this.options.wildcard ? findMatchingPatterns(event, allPatterns, this.options.delimiter) : allPatterns.filter((p) => p === event);
|
|
819
|
+
const allEntries = [];
|
|
820
|
+
for (const pattern of matchingPatterns) {
|
|
821
|
+
const entries = this.listeners.get(pattern);
|
|
822
|
+
if (entries) {
|
|
823
|
+
allEntries.push(...entries);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (allEntries.length === 0) {
|
|
827
|
+
if (this.options.debug) {
|
|
828
|
+
console.debug("[QuarKernel] Event emitted serially (no listeners)", { event });
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (this.options.debug) {
|
|
833
|
+
console.debug("[QuarKernel] Event emitted serially", {
|
|
834
|
+
event,
|
|
835
|
+
listenerCount: allEntries.length,
|
|
836
|
+
data: data !== void 0 ? JSON.stringify(data).substring(0, 100) : void 0
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
this.executionErrors = [];
|
|
840
|
+
const kernelEvent = new KernelEvent(
|
|
841
|
+
event,
|
|
842
|
+
data,
|
|
843
|
+
{}
|
|
844
|
+
);
|
|
845
|
+
const sortedEntries = this.sortListenersByDependencies(allEntries);
|
|
846
|
+
const errors = [];
|
|
847
|
+
for (const entry of sortedEntries) {
|
|
848
|
+
try {
|
|
849
|
+
await this.executeListener(entry, kernelEvent, event);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (!this.options.errorBoundary) {
|
|
852
|
+
this.removeOnceListeners(event, sortedEntries, kernelEvent);
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
errors.push(error);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
this.removeOnceListeners(event, sortedEntries, kernelEvent);
|
|
859
|
+
if (!this.options.errorBoundary && errors.length > 0) {
|
|
860
|
+
throw new AggregateError(errors, `${errors.length} listener(s) failed for event "${event}"`);
|
|
861
|
+
}
|
|
862
|
+
if (this.options.debug) {
|
|
863
|
+
console.debug("[QuarKernel] Event completed serially", {
|
|
864
|
+
event
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Sort listeners by dependencies and priority
|
|
870
|
+
* Uses topological sort for dependency resolution
|
|
871
|
+
*/
|
|
872
|
+
sortListenersByDependencies(entries) {
|
|
873
|
+
const hasDependencies = entries.some((e) => e.after.length > 0);
|
|
874
|
+
if (!hasDependencies) {
|
|
875
|
+
return [...entries].sort((a, b) => b.priority - a.priority);
|
|
876
|
+
}
|
|
877
|
+
const listenerIds = new Set(entries.map((e) => e.id));
|
|
878
|
+
for (const entry of entries) {
|
|
879
|
+
for (const dep of entry.after) {
|
|
880
|
+
if (!listenerIds.has(dep)) {
|
|
881
|
+
throw new Error(`Listener "${entry.id}" depends on missing listener "${dep}"`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const nodes = entries.map((e) => ({
|
|
886
|
+
id: e.id,
|
|
887
|
+
after: e.after
|
|
888
|
+
}));
|
|
889
|
+
toposort(nodes);
|
|
890
|
+
const levelMap = /* @__PURE__ */ new Map();
|
|
891
|
+
const assignLevel = (id, visited = /* @__PURE__ */ new Set()) => {
|
|
892
|
+
if (levelMap.has(id)) {
|
|
893
|
+
return levelMap.get(id);
|
|
894
|
+
}
|
|
895
|
+
if (visited.has(id)) {
|
|
896
|
+
return 0;
|
|
897
|
+
}
|
|
898
|
+
visited.add(id);
|
|
899
|
+
const entry = entries.find((e) => e.id === id);
|
|
900
|
+
if (!entry || entry.after.length === 0) {
|
|
901
|
+
levelMap.set(id, 0);
|
|
902
|
+
return 0;
|
|
903
|
+
}
|
|
904
|
+
const maxDepLevel = Math.max(...entry.after.map((dep) => assignLevel(dep, visited)));
|
|
905
|
+
const level = maxDepLevel + 1;
|
|
906
|
+
levelMap.set(id, level);
|
|
907
|
+
return level;
|
|
908
|
+
};
|
|
909
|
+
entries.forEach((e) => assignLevel(e.id));
|
|
910
|
+
return [...entries].sort((a, b) => {
|
|
911
|
+
const levelA = levelMap.get(a.id) ?? 0;
|
|
912
|
+
const levelB = levelMap.get(b.id) ?? 0;
|
|
913
|
+
if (levelA !== levelB) {
|
|
914
|
+
return levelA - levelB;
|
|
915
|
+
}
|
|
916
|
+
return b.priority - a.priority;
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Execute a single listener with error handling
|
|
921
|
+
*/
|
|
922
|
+
async executeListener(entry, event, eventName) {
|
|
923
|
+
if (event.isPropagationStopped) {
|
|
924
|
+
if (this.options.debug) {
|
|
925
|
+
console.debug("[QuarKernel] Listener skipped (propagation stopped)", {
|
|
926
|
+
listenerId: entry.id
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const startTime = Date.now();
|
|
932
|
+
if (this.options.debug) {
|
|
933
|
+
console.debug("[QuarKernel] Listener executing", {
|
|
934
|
+
listenerId: entry.id,
|
|
935
|
+
event: eventName,
|
|
936
|
+
priority: entry.priority
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const ctx = new ListenerContext(
|
|
941
|
+
entry.id,
|
|
942
|
+
eventName,
|
|
943
|
+
entry.priority,
|
|
944
|
+
entry.after,
|
|
945
|
+
this,
|
|
946
|
+
entry.original,
|
|
947
|
+
entry.signal
|
|
948
|
+
);
|
|
949
|
+
ctx.setCurrentEvent(event);
|
|
950
|
+
try {
|
|
951
|
+
await entry.callback(event, ctx);
|
|
952
|
+
if (this.options.debug) {
|
|
953
|
+
const duration = Date.now() - startTime;
|
|
954
|
+
console.debug("[QuarKernel] Listener completed", {
|
|
955
|
+
listenerId: entry.id,
|
|
956
|
+
duration: `${duration}ms`
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
} finally {
|
|
960
|
+
ctx.clearCurrentEvent();
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
const executionError = {
|
|
964
|
+
listenerId: entry.id,
|
|
965
|
+
error,
|
|
966
|
+
timestamp: Date.now(),
|
|
967
|
+
eventName
|
|
968
|
+
};
|
|
969
|
+
this.executionErrors.push(executionError);
|
|
970
|
+
if (this.options.debug) {
|
|
971
|
+
console.debug("[QuarKernel] Listener error", {
|
|
972
|
+
listenerId: entry.id,
|
|
973
|
+
error: error.message
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (this.options.errorBoundary) {
|
|
977
|
+
this.options.onError(error, event);
|
|
978
|
+
} else {
|
|
979
|
+
throw error;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Remove listeners marked with once: true or whose predicate returns true after execution
|
|
985
|
+
*
|
|
986
|
+
* This method is called AFTER all listeners have executed for an event.
|
|
987
|
+
* The predicate functions receive the event object with the final state after all listeners ran.
|
|
988
|
+
*
|
|
989
|
+
* Behavior:
|
|
990
|
+
* - If once: true, the listener is always removed after execution
|
|
991
|
+
* - If once is a predicate function, it's evaluated with the post-execution event state
|
|
992
|
+
* - Predicates can examine event.context to make decisions based on listener modifications
|
|
993
|
+
* - Listeners are removed even if they threw errors (when errorBoundary: true)
|
|
994
|
+
*
|
|
995
|
+
* @param eventName - The event name being processed
|
|
996
|
+
* @param entries - Listeners that executed (or were scheduled to execute)
|
|
997
|
+
* @param event - The event object with final state after all listeners executed
|
|
998
|
+
*/
|
|
999
|
+
removeOnceListeners(eventName, entries, event) {
|
|
1000
|
+
const listenersToRemove = entries.filter((entry) => {
|
|
1001
|
+
if (!entry.once) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
if (entry.once === true) {
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof entry.once === "function") {
|
|
1008
|
+
return entry.once(event);
|
|
1009
|
+
}
|
|
1010
|
+
return false;
|
|
1011
|
+
});
|
|
1012
|
+
if (listenersToRemove.length === 0) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (this.options.debug) {
|
|
1016
|
+
console.debug("[QuarKernel] Removing once listeners", {
|
|
1017
|
+
event: eventName,
|
|
1018
|
+
count: listenersToRemove.length
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
for (const entry of listenersToRemove) {
|
|
1022
|
+
for (const [pattern, entries2] of this.listeners.entries()) {
|
|
1023
|
+
if (entries2.includes(entry)) {
|
|
1024
|
+
this.off(pattern, entry.original);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Get number of listeners for an event
|
|
1032
|
+
*/
|
|
1033
|
+
listenerCount(eventName) {
|
|
1034
|
+
if (!eventName) {
|
|
1035
|
+
let total = 0;
|
|
1036
|
+
for (const entries2 of this.listeners.values()) {
|
|
1037
|
+
total += entries2.length;
|
|
1038
|
+
}
|
|
1039
|
+
return total;
|
|
1040
|
+
}
|
|
1041
|
+
const event = String(eventName);
|
|
1042
|
+
const entries = this.listeners.get(event);
|
|
1043
|
+
return entries?.length ?? 0;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Get all event names with registered listeners
|
|
1047
|
+
*/
|
|
1048
|
+
eventNames() {
|
|
1049
|
+
return Array.from(this.listeners.keys());
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Remove all listeners for all events (or specific event)
|
|
1053
|
+
*/
|
|
1054
|
+
offAll(eventName) {
|
|
1055
|
+
if (!eventName) {
|
|
1056
|
+
for (const entries2 of this.listeners.values()) {
|
|
1057
|
+
for (const entry of entries2) {
|
|
1058
|
+
if (entry.signal && entry.abortListener) {
|
|
1059
|
+
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
this.listeners.clear();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const event = String(eventName);
|
|
1067
|
+
const entries = this.listeners.get(event);
|
|
1068
|
+
if (entries) {
|
|
1069
|
+
for (const entry of entries) {
|
|
1070
|
+
if (entry.signal && entry.abortListener) {
|
|
1071
|
+
entry.signal.removeEventListener("abort", entry.abortListener);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
this.listeners.delete(event);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Enable/disable debug mode
|
|
1079
|
+
* In T115, this is a placeholder - full debug implementation in T129
|
|
1080
|
+
*/
|
|
1081
|
+
debug(enabled) {
|
|
1082
|
+
this.options.debug = enabled;
|
|
1083
|
+
if (enabled) {
|
|
1084
|
+
console.debug("[QuarKernel] Debug mode enabled");
|
|
1085
|
+
} else {
|
|
1086
|
+
console.debug("[QuarKernel] Debug mode disabled");
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Get collected execution errors from the last emit
|
|
1091
|
+
* Useful for error aggregation and reporting
|
|
1092
|
+
*/
|
|
1093
|
+
getExecutionErrors() {
|
|
1094
|
+
return this.executionErrors;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Clear collected execution errors
|
|
1098
|
+
*/
|
|
1099
|
+
clearExecutionErrors() {
|
|
1100
|
+
this.executionErrors = [];
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Create a composition from multiple kernels
|
|
1104
|
+
*
|
|
1105
|
+
* @param kernels - Rest parameters of [kernel, eventName] tuples
|
|
1106
|
+
* @param options - Optional composition options (if last argument is not a tuple)
|
|
1107
|
+
* @returns Composition instance that merges events from all kernels
|
|
1108
|
+
*
|
|
1109
|
+
* @example
|
|
1110
|
+
* ```ts
|
|
1111
|
+
* const userKernel = createKernel();
|
|
1112
|
+
* const profileKernel = createKernel();
|
|
1113
|
+
*
|
|
1114
|
+
* const composition = Kernel.compose(
|
|
1115
|
+
* [userKernel, 'user:loaded'],
|
|
1116
|
+
* [profileKernel, 'profile:loaded'],
|
|
1117
|
+
* { merger: createNamespacedMerger() }
|
|
1118
|
+
* );
|
|
1119
|
+
*
|
|
1120
|
+
* composition.onComposed((event) => {
|
|
1121
|
+
* console.log('All sources ready:', event.data.merged);
|
|
1122
|
+
* });
|
|
1123
|
+
* ```
|
|
1124
|
+
*/
|
|
1125
|
+
static compose(...args) {
|
|
1126
|
+
const kernels = [];
|
|
1127
|
+
let options;
|
|
1128
|
+
for (const arg of args) {
|
|
1129
|
+
if (Array.isArray(arg) && arg.length === 2 && arg[0] instanceof _Kernel) {
|
|
1130
|
+
kernels.push(arg);
|
|
1131
|
+
} else if (typeof arg === "object" && !Array.isArray(arg)) {
|
|
1132
|
+
options = arg;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return new Composition(kernels, options);
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/fsm/machine.ts
|
|
1140
|
+
function useMachine(kernel, config) {
|
|
1141
|
+
const {
|
|
1142
|
+
prefix,
|
|
1143
|
+
initial,
|
|
1144
|
+
states,
|
|
1145
|
+
allowForce = true,
|
|
1146
|
+
snapshot,
|
|
1147
|
+
trackHistory = false,
|
|
1148
|
+
maxHistory = 100
|
|
1149
|
+
} = config;
|
|
1150
|
+
let currentState = snapshot?.state ?? initial;
|
|
1151
|
+
let context = snapshot?.context ?? config.context ?? {};
|
|
1152
|
+
let history = snapshot?.history ? [...snapshot.history] : [];
|
|
1153
|
+
if (!states[currentState]) {
|
|
1154
|
+
throw new Error(`Invalid initial state "${currentState}" - not defined in states`);
|
|
1155
|
+
}
|
|
1156
|
+
const cleanupFns = [];
|
|
1157
|
+
const emitFSM = async (eventType, data) => {
|
|
1158
|
+
const eventData = {
|
|
1159
|
+
machine: prefix,
|
|
1160
|
+
...data
|
|
1161
|
+
};
|
|
1162
|
+
await kernel.emit(`${prefix}:${eventType}`, eventData);
|
|
1163
|
+
};
|
|
1164
|
+
const getTransition = (event) => {
|
|
1165
|
+
const stateNode = states[currentState];
|
|
1166
|
+
if (!stateNode?.on) return null;
|
|
1167
|
+
const transition = stateNode.on[event];
|
|
1168
|
+
if (!transition) return null;
|
|
1169
|
+
if (typeof transition === "string") {
|
|
1170
|
+
return { target: transition };
|
|
1171
|
+
}
|
|
1172
|
+
return transition;
|
|
1173
|
+
};
|
|
1174
|
+
const doTransition = async (event, targetState, payload, forced = false) => {
|
|
1175
|
+
const fromState = currentState;
|
|
1176
|
+
const fromNode = states[fromState];
|
|
1177
|
+
const toNode = states[targetState];
|
|
1178
|
+
if (!toNode) {
|
|
1179
|
+
throw new Error(`Invalid target state "${targetState}" - not defined in states`);
|
|
1180
|
+
}
|
|
1181
|
+
if (fromNode?.exit) {
|
|
1182
|
+
await fromNode.exit(context, event, payload);
|
|
1183
|
+
}
|
|
1184
|
+
await emitFSM(`exit:${fromState}`, {
|
|
1185
|
+
state: fromState,
|
|
1186
|
+
from: fromState,
|
|
1187
|
+
to: targetState,
|
|
1188
|
+
event,
|
|
1189
|
+
payload,
|
|
1190
|
+
forced
|
|
1191
|
+
});
|
|
1192
|
+
currentState = targetState;
|
|
1193
|
+
if (trackHistory) {
|
|
1194
|
+
history.push({
|
|
1195
|
+
from: fromState,
|
|
1196
|
+
to: targetState,
|
|
1197
|
+
event,
|
|
1198
|
+
timestamp: Date.now()
|
|
1199
|
+
});
|
|
1200
|
+
if (history.length > maxHistory) {
|
|
1201
|
+
history = history.slice(-maxHistory);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
await emitFSM("transition", {
|
|
1205
|
+
state: targetState,
|
|
1206
|
+
from: fromState,
|
|
1207
|
+
to: targetState,
|
|
1208
|
+
event,
|
|
1209
|
+
payload,
|
|
1210
|
+
forced
|
|
1211
|
+
});
|
|
1212
|
+
await emitFSM(`transition:${event}`, {
|
|
1213
|
+
state: targetState,
|
|
1214
|
+
from: fromState,
|
|
1215
|
+
to: targetState,
|
|
1216
|
+
event,
|
|
1217
|
+
payload,
|
|
1218
|
+
forced
|
|
1219
|
+
});
|
|
1220
|
+
await emitFSM(`enter:${targetState}`, {
|
|
1221
|
+
state: targetState,
|
|
1222
|
+
from: fromState,
|
|
1223
|
+
to: targetState,
|
|
1224
|
+
event,
|
|
1225
|
+
payload,
|
|
1226
|
+
forced
|
|
1227
|
+
});
|
|
1228
|
+
if (toNode.entry) {
|
|
1229
|
+
await toNode.entry(context, event, payload);
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
const machine = {
|
|
1233
|
+
prefix,
|
|
1234
|
+
getState() {
|
|
1235
|
+
return currentState;
|
|
1236
|
+
},
|
|
1237
|
+
getContext() {
|
|
1238
|
+
return context;
|
|
1239
|
+
},
|
|
1240
|
+
setContext(updater) {
|
|
1241
|
+
if (typeof updater === "function") {
|
|
1242
|
+
context = updater(context);
|
|
1243
|
+
} else {
|
|
1244
|
+
context = { ...context, ...updater };
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
async send(event, payload, options = {}) {
|
|
1248
|
+
const { force = false, target, guard: inlineGuard, fallback } = options;
|
|
1249
|
+
if (force && allowForce) {
|
|
1250
|
+
const targetState = target ?? initial;
|
|
1251
|
+
await doTransition(event, targetState, payload, true);
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1254
|
+
const transition = getTransition(event);
|
|
1255
|
+
if (!transition) {
|
|
1256
|
+
if (force && !allowForce) {
|
|
1257
|
+
throw new Error(`Force transitions not allowed on machine "${prefix}"`);
|
|
1258
|
+
}
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
const guardFn = inlineGuard ?? transition.guard;
|
|
1262
|
+
if (guardFn && !guardFn(context, event, payload)) {
|
|
1263
|
+
await emitFSM("guard:rejected", {
|
|
1264
|
+
state: currentState,
|
|
1265
|
+
event,
|
|
1266
|
+
payload
|
|
1267
|
+
});
|
|
1268
|
+
if (fallback) {
|
|
1269
|
+
if (!states[fallback]) {
|
|
1270
|
+
throw new Error(`Invalid fallback state "${fallback}" - not defined in states`);
|
|
1271
|
+
}
|
|
1272
|
+
await doTransition(event, fallback, payload, false);
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
if (transition.actions) {
|
|
1278
|
+
const actions = Array.isArray(transition.actions) ? transition.actions : [transition.actions];
|
|
1279
|
+
for (const action of actions) {
|
|
1280
|
+
await action(context, event, payload);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
await doTransition(event, transition.target, payload, false);
|
|
1284
|
+
return true;
|
|
1285
|
+
},
|
|
1286
|
+
can(event) {
|
|
1287
|
+
return getTransition(event) !== null;
|
|
1288
|
+
},
|
|
1289
|
+
transitions() {
|
|
1290
|
+
const stateNode = states[currentState];
|
|
1291
|
+
if (!stateNode?.on) return [];
|
|
1292
|
+
return Object.keys(stateNode.on);
|
|
1293
|
+
},
|
|
1294
|
+
toJSON() {
|
|
1295
|
+
return {
|
|
1296
|
+
state: currentState,
|
|
1297
|
+
context: structuredClone(context),
|
|
1298
|
+
history: trackHistory ? [...history] : void 0
|
|
1299
|
+
};
|
|
1300
|
+
},
|
|
1301
|
+
restore(snapshot2) {
|
|
1302
|
+
if (!states[snapshot2.state]) {
|
|
1303
|
+
throw new Error(`Invalid snapshot state "${snapshot2.state}" - not defined in states`);
|
|
1304
|
+
}
|
|
1305
|
+
currentState = snapshot2.state;
|
|
1306
|
+
context = snapshot2.context;
|
|
1307
|
+
if (snapshot2.history) {
|
|
1308
|
+
history = [...snapshot2.history];
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
destroy() {
|
|
1312
|
+
for (const cleanup of cleanupFns) {
|
|
1313
|
+
cleanup();
|
|
1314
|
+
}
|
|
1315
|
+
cleanupFns.length = 0;
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
if (!snapshot) {
|
|
1319
|
+
setTimeout(async () => {
|
|
1320
|
+
const initialNode = states[initial];
|
|
1321
|
+
await emitFSM(`enter:${initial}`, {
|
|
1322
|
+
state: initial
|
|
1323
|
+
});
|
|
1324
|
+
if (initialNode?.entry) {
|
|
1325
|
+
await initialNode.entry(context, "__INIT__", void 0);
|
|
1326
|
+
}
|
|
1327
|
+
}, 0);
|
|
1328
|
+
}
|
|
1329
|
+
return machine;
|
|
1330
|
+
}
|
|
1331
|
+
function defineMachine(config) {
|
|
1332
|
+
return config;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/fsm/create-machine.ts
|
|
1336
|
+
function createMachine(config) {
|
|
1337
|
+
const {
|
|
1338
|
+
id,
|
|
1339
|
+
initial,
|
|
1340
|
+
context: initialContext,
|
|
1341
|
+
states,
|
|
1342
|
+
on: eventHandlers = {},
|
|
1343
|
+
helpers: customHelpers = {}
|
|
1344
|
+
} = config;
|
|
1345
|
+
const kernel = new Kernel();
|
|
1346
|
+
const activeTimers = /* @__PURE__ */ new Map();
|
|
1347
|
+
const machineConfig = {
|
|
1348
|
+
prefix: id,
|
|
1349
|
+
initial,
|
|
1350
|
+
context: initialContext,
|
|
1351
|
+
states: {},
|
|
1352
|
+
trackHistory: true
|
|
1353
|
+
};
|
|
1354
|
+
for (const [stateName, stateDef] of Object.entries(states)) {
|
|
1355
|
+
machineConfig.states[stateName] = {
|
|
1356
|
+
on: stateDef.on ? { ...stateDef.on } : void 0
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
const baseMachine = useMachine(kernel, machineConfig);
|
|
1360
|
+
const createHelpers = () => ({
|
|
1361
|
+
// Built-ins
|
|
1362
|
+
set: (partial) => baseMachine.setContext(partial),
|
|
1363
|
+
send: (event, payload) => baseMachine.send(event, payload),
|
|
1364
|
+
log: console.log,
|
|
1365
|
+
// Custom helpers (can override built-ins)
|
|
1366
|
+
...customHelpers
|
|
1367
|
+
});
|
|
1368
|
+
kernel.on(`${id}:enter:*`, async (e) => {
|
|
1369
|
+
const stateName = e.data?.state;
|
|
1370
|
+
if (!stateName) return;
|
|
1371
|
+
activeTimers.forEach((timer, key) => {
|
|
1372
|
+
if (!key.startsWith(stateName + ":")) {
|
|
1373
|
+
clearTimeout(timer);
|
|
1374
|
+
activeTimers.delete(key);
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
const stateConfig = states[stateName];
|
|
1378
|
+
if (!stateConfig) return;
|
|
1379
|
+
if (stateConfig.entry) {
|
|
1380
|
+
await stateConfig.entry(baseMachine.getContext(), createHelpers());
|
|
1381
|
+
}
|
|
1382
|
+
if (stateConfig.after) {
|
|
1383
|
+
const timerId = setTimeout(() => {
|
|
1384
|
+
baseMachine.send(stateConfig.after.send);
|
|
1385
|
+
activeTimers.delete(stateName + ":timer");
|
|
1386
|
+
}, stateConfig.after.delay);
|
|
1387
|
+
activeTimers.set(stateName + ":timer", timerId);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
kernel.on(`${id}:exit:*`, async (e) => {
|
|
1391
|
+
const stateName = e.data?.state;
|
|
1392
|
+
if (!stateName) return;
|
|
1393
|
+
const timerId = activeTimers.get(stateName + ":timer");
|
|
1394
|
+
if (timerId) {
|
|
1395
|
+
clearTimeout(timerId);
|
|
1396
|
+
activeTimers.delete(stateName + ":timer");
|
|
1397
|
+
}
|
|
1398
|
+
const stateConfig = states[stateName];
|
|
1399
|
+
if (!stateConfig) return;
|
|
1400
|
+
if (stateConfig.exit) {
|
|
1401
|
+
await stateConfig.exit(baseMachine.getContext(), createHelpers());
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
kernel.on(`${id}:transition`, async (e) => {
|
|
1405
|
+
const event = e.data?.event;
|
|
1406
|
+
if (!event) return;
|
|
1407
|
+
const handler = eventHandlers[event];
|
|
1408
|
+
if (handler) {
|
|
1409
|
+
await handler(baseMachine.getContext(), createHelpers());
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
const machine = {
|
|
1413
|
+
...baseMachine,
|
|
1414
|
+
id,
|
|
1415
|
+
get state() {
|
|
1416
|
+
return baseMachine.getState();
|
|
1417
|
+
},
|
|
1418
|
+
get context() {
|
|
1419
|
+
return baseMachine.getContext();
|
|
1420
|
+
},
|
|
1421
|
+
destroy() {
|
|
1422
|
+
activeTimers.forEach((timer) => clearTimeout(timer));
|
|
1423
|
+
activeTimers.clear();
|
|
1424
|
+
baseMachine.destroy();
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
return machine;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export { createMachine, defineMachine, useMachine };
|
|
1431
|
+
//# sourceMappingURL=fsm.js.map
|
|
1432
|
+
//# sourceMappingURL=fsm.js.map
|