@mantaq/core 0.0.1
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 +21 -0
- package/README.md +657 -0
- package/dist/actor-internal-CfZDmHJ3.d.mts +89 -0
- package/dist/index.d.mts +151 -0
- package/dist/index.mjs +557 -0
- package/dist/internal.d.mts +26 -0
- package/dist/internal.mjs +31 -0
- package/package.json +45 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { registerActor, setOutputHandler } from "./internal.mjs";
|
|
2
|
+
//#region src/state.ts
|
|
3
|
+
function state(id) {
|
|
4
|
+
return () => new StateRef(id, false);
|
|
5
|
+
}
|
|
6
|
+
var StateRef = class StateRef {
|
|
7
|
+
name;
|
|
8
|
+
isFinal;
|
|
9
|
+
/** @internal */ _regions;
|
|
10
|
+
constructor(name, isFinal) {
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.isFinal = isFinal;
|
|
13
|
+
}
|
|
14
|
+
regions(options) {
|
|
15
|
+
this._regions = options;
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
final() {
|
|
19
|
+
const next = new StateRef(this.name, true);
|
|
20
|
+
next._regions = this._regions;
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
create(payload) {
|
|
24
|
+
return {
|
|
25
|
+
state: this,
|
|
26
|
+
payload
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/real-clock.ts
|
|
32
|
+
var RealClock = class {
|
|
33
|
+
#start = Date.now();
|
|
34
|
+
now() {
|
|
35
|
+
return Date.now() - this.#start;
|
|
36
|
+
}
|
|
37
|
+
setTimeout(ms, cb, options) {
|
|
38
|
+
if (options?.signal?.aborted) return -1;
|
|
39
|
+
let onAbort;
|
|
40
|
+
const id = Number(globalThis.setTimeout(() => {
|
|
41
|
+
if (onAbort && options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
42
|
+
cb();
|
|
43
|
+
}, ms));
|
|
44
|
+
if (options?.signal) {
|
|
45
|
+
onAbort = () => globalThis.clearTimeout(id);
|
|
46
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
47
|
+
}
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
clearTimeout(id) {
|
|
51
|
+
globalThis.clearTimeout(id);
|
|
52
|
+
}
|
|
53
|
+
setInterval(ms, cb, options) {
|
|
54
|
+
const id = Number(globalThis.setInterval(cb, ms));
|
|
55
|
+
if (options?.signal) {
|
|
56
|
+
const onAbort = () => globalThis.clearInterval(id);
|
|
57
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
}
|
|
59
|
+
return id;
|
|
60
|
+
}
|
|
61
|
+
clearInterval(id) {
|
|
62
|
+
globalThis.clearInterval(id);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/abort-tracker.ts
|
|
67
|
+
function trackAbort(signal, id, map) {
|
|
68
|
+
const onAbort = signal ? () => {
|
|
69
|
+
map.delete(id);
|
|
70
|
+
} : void 0;
|
|
71
|
+
if (signal && onAbort) signal.addEventListener("abort", onAbort, { once: true });
|
|
72
|
+
return onAbort;
|
|
73
|
+
}
|
|
74
|
+
function clearAbort(timer) {
|
|
75
|
+
if (timer.signal && timer.onAbort) timer.signal.removeEventListener("abort", timer.onAbort);
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/virtual-clock.ts
|
|
79
|
+
var VirtualClock = class {
|
|
80
|
+
#now = 0;
|
|
81
|
+
#timers = /* @__PURE__ */ new Map();
|
|
82
|
+
#intervals = /* @__PURE__ */ new Map();
|
|
83
|
+
#nextId = 1;
|
|
84
|
+
#drain = null;
|
|
85
|
+
now() {
|
|
86
|
+
return this.#now;
|
|
87
|
+
}
|
|
88
|
+
setTimeout(ms, cb, options) {
|
|
89
|
+
const signal = options?.signal;
|
|
90
|
+
if (signal?.aborted) return -1;
|
|
91
|
+
const id = this.#nextId++;
|
|
92
|
+
const onAbort = trackAbort(signal, id, this.#timers);
|
|
93
|
+
this.#timers.set(id, {
|
|
94
|
+
deadline: this.#now + ms,
|
|
95
|
+
cb,
|
|
96
|
+
signal,
|
|
97
|
+
onAbort,
|
|
98
|
+
eventName: options?.eventName
|
|
99
|
+
});
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
clearTimeout(id) {
|
|
103
|
+
const timer = this.#timers.get(id);
|
|
104
|
+
if (timer) {
|
|
105
|
+
clearAbort(timer);
|
|
106
|
+
this.#timers.delete(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
setInterval(ms, cb, options) {
|
|
110
|
+
const signal = options?.signal;
|
|
111
|
+
if (signal?.aborted) return -1;
|
|
112
|
+
const id = this.#nextId++;
|
|
113
|
+
const onAbort = trackAbort(signal, id, this.#intervals);
|
|
114
|
+
this.#intervals.set(id, {
|
|
115
|
+
ms,
|
|
116
|
+
next: this.#now + ms,
|
|
117
|
+
cb,
|
|
118
|
+
signal,
|
|
119
|
+
onAbort
|
|
120
|
+
});
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
clearInterval(id) {
|
|
124
|
+
const interval = this.#intervals.get(id);
|
|
125
|
+
if (interval) {
|
|
126
|
+
clearAbort(interval);
|
|
127
|
+
this.#intervals.delete(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
#findEarliestDeadline(target) {
|
|
131
|
+
let earliest = target;
|
|
132
|
+
let found = false;
|
|
133
|
+
for (const t of this.#timers.values()) if (t.deadline <= target && t.deadline <= earliest) {
|
|
134
|
+
earliest = t.deadline;
|
|
135
|
+
found = true;
|
|
136
|
+
}
|
|
137
|
+
for (const t of this.#intervals.values()) if (t.next <= target && t.next <= earliest) {
|
|
138
|
+
earliest = t.next;
|
|
139
|
+
found = true;
|
|
140
|
+
}
|
|
141
|
+
return found ? earliest : null;
|
|
142
|
+
}
|
|
143
|
+
#fireTimersAt(deadline) {
|
|
144
|
+
const ids = [];
|
|
145
|
+
for (const [id, t] of this.#timers) if (t.deadline === deadline) ids.push(id);
|
|
146
|
+
for (const id of ids) {
|
|
147
|
+
const timer = this.#timers.get(id);
|
|
148
|
+
if (timer) {
|
|
149
|
+
clearAbort(timer);
|
|
150
|
+
this.#timers.delete(id);
|
|
151
|
+
timer.cb();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
#fireIntervalsAt(deadline) {
|
|
156
|
+
const ids = [];
|
|
157
|
+
for (const [id, t] of this.#intervals) if (t.next === deadline) ids.push(id);
|
|
158
|
+
for (const id of ids) {
|
|
159
|
+
const interval = this.#intervals.get(id);
|
|
160
|
+
if (interval) {
|
|
161
|
+
interval.cb();
|
|
162
|
+
if (this.#intervals.has(id)) interval.next = this.#now + interval.ms;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
advance(ms) {
|
|
167
|
+
const target = this.#now + ms;
|
|
168
|
+
while (true) {
|
|
169
|
+
const deadline = this.#findEarliestDeadline(target);
|
|
170
|
+
if (deadline === null) break;
|
|
171
|
+
this.#now = deadline;
|
|
172
|
+
this.#fireTimersAt(deadline);
|
|
173
|
+
this.#fireIntervalsAt(deadline);
|
|
174
|
+
}
|
|
175
|
+
this.#now = target;
|
|
176
|
+
this.#drain?.();
|
|
177
|
+
}
|
|
178
|
+
hasPending() {
|
|
179
|
+
return this.#timers.size > 0 || this.#intervals.size > 0;
|
|
180
|
+
}
|
|
181
|
+
pendingTimers() {
|
|
182
|
+
const result = [];
|
|
183
|
+
for (const [id, t] of this.#timers) result.push({
|
|
184
|
+
id,
|
|
185
|
+
deadline: t.deadline,
|
|
186
|
+
ms: t.deadline - this.#now,
|
|
187
|
+
eventName: t.eventName
|
|
188
|
+
});
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
setDrain(fn) {
|
|
192
|
+
this.#drain = fn;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/queue.ts
|
|
197
|
+
var InternalQueue = class {
|
|
198
|
+
#queue = [];
|
|
199
|
+
#index = 0;
|
|
200
|
+
#processing = false;
|
|
201
|
+
#stopped = false;
|
|
202
|
+
#settledResolvers = [];
|
|
203
|
+
get length() {
|
|
204
|
+
return this.#queue.length - this.#index;
|
|
205
|
+
}
|
|
206
|
+
get isProcessing() {
|
|
207
|
+
return this.#processing;
|
|
208
|
+
}
|
|
209
|
+
push(...events) {
|
|
210
|
+
if (this.#stopped) return;
|
|
211
|
+
this.#queue.push(...events);
|
|
212
|
+
}
|
|
213
|
+
settled() {
|
|
214
|
+
if (this.length === 0 && !this.#processing) return Promise.resolve();
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
this.#settledResolvers.push(resolve);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
process(processEvent) {
|
|
220
|
+
this.#process((event) => {
|
|
221
|
+
processEvent(event);
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
processCancellable(processEvent) {
|
|
226
|
+
this.#process(processEvent);
|
|
227
|
+
}
|
|
228
|
+
#process(processEvent) {
|
|
229
|
+
if (this.#processing) return;
|
|
230
|
+
this.#processing = true;
|
|
231
|
+
try {
|
|
232
|
+
while (this.#index < this.#queue.length) {
|
|
233
|
+
if (this.#stopped) break;
|
|
234
|
+
const event = this.#queue[this.#index++];
|
|
235
|
+
if (!processEvent(event)) {
|
|
236
|
+
this.#stopped = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
this.#queue.length = 0;
|
|
242
|
+
this.#index = 0;
|
|
243
|
+
this.#processing = false;
|
|
244
|
+
this.#stopped = false;
|
|
245
|
+
if (this.#settledResolvers.length > 0) {
|
|
246
|
+
const resolvers = this.#settledResolvers.splice(0);
|
|
247
|
+
for (const resolve of resolvers) resolve();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/subscribers.ts
|
|
254
|
+
var Subscribers = class {
|
|
255
|
+
change = /* @__PURE__ */ new Set();
|
|
256
|
+
done = /* @__PURE__ */ new Set();
|
|
257
|
+
addChange(fn) {
|
|
258
|
+
this.change.add(fn);
|
|
259
|
+
return () => this.change.delete(fn);
|
|
260
|
+
}
|
|
261
|
+
addDone(fn) {
|
|
262
|
+
this.done.add(fn);
|
|
263
|
+
return () => this.done.delete(fn);
|
|
264
|
+
}
|
|
265
|
+
emitChange(snapshot) {
|
|
266
|
+
for (const fn of this.change) fn(snapshot);
|
|
267
|
+
}
|
|
268
|
+
emitDone() {
|
|
269
|
+
for (const fn of this.done) fn();
|
|
270
|
+
}
|
|
271
|
+
clear() {
|
|
272
|
+
this.change.clear();
|
|
273
|
+
this.done.clear();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/snapshot.ts
|
|
278
|
+
function buildSnapshot(s, regions) {
|
|
279
|
+
const path = [s.name];
|
|
280
|
+
const regionSnapshots = {};
|
|
281
|
+
for (const [regionName, child] of Object.entries(regions)) regionSnapshots[regionName] = child.snapshot();
|
|
282
|
+
const snap = {
|
|
283
|
+
path,
|
|
284
|
+
regions: regionSnapshots
|
|
285
|
+
};
|
|
286
|
+
if (s.isFinal) snap.done = true;
|
|
287
|
+
return snap;
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/effects.ts
|
|
291
|
+
function runEffects(options) {
|
|
292
|
+
if (options.state.isFinal) return null;
|
|
293
|
+
const list = options.effects[options.state.name];
|
|
294
|
+
if (!list || list.length === 0) return null;
|
|
295
|
+
const abort = new AbortController();
|
|
296
|
+
for (const effectFn of list) effectFn({
|
|
297
|
+
signal: abort.signal,
|
|
298
|
+
state: {
|
|
299
|
+
name: options.state.name,
|
|
300
|
+
payload: options.statePayload
|
|
301
|
+
},
|
|
302
|
+
event: options.event,
|
|
303
|
+
context: options.context,
|
|
304
|
+
emit: options.emit,
|
|
305
|
+
clock: options.clock
|
|
306
|
+
});
|
|
307
|
+
return abort;
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/dispatch.ts
|
|
311
|
+
function parseTarget(step) {
|
|
312
|
+
if (!step.state) return void 0;
|
|
313
|
+
if (step.state instanceof StateRef) return {
|
|
314
|
+
state: step.state,
|
|
315
|
+
payload: step.payload
|
|
316
|
+
};
|
|
317
|
+
return {
|
|
318
|
+
state: step.state.state,
|
|
319
|
+
payload: step.state.payload
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/builder.ts
|
|
324
|
+
var ActorBuilder = class {
|
|
325
|
+
#transitions = {};
|
|
326
|
+
#effects = {};
|
|
327
|
+
on(stateRef, eventRef, fn) {
|
|
328
|
+
const sName = stateRef.name;
|
|
329
|
+
const eId = eventRef.id;
|
|
330
|
+
(this.#transitions[sName] ??= {})[eId] = fn;
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
onAny(eventRef, fn) {
|
|
334
|
+
const eId = eventRef.id;
|
|
335
|
+
(this.#transitions["Any"] ??= {})[eId] = fn;
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
effect(stateRef, fn) {
|
|
339
|
+
(this.#effects[stateRef.name] ??= []).push(fn);
|
|
340
|
+
return this;
|
|
341
|
+
}
|
|
342
|
+
/** @internal */ build() {
|
|
343
|
+
return {
|
|
344
|
+
transitions: this.#transitions,
|
|
345
|
+
effects: this.#effects
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/actor.ts
|
|
351
|
+
function pickState(initial) {
|
|
352
|
+
return initial instanceof StateRef ? initial : initial.state;
|
|
353
|
+
}
|
|
354
|
+
function resolveInitial(initial) {
|
|
355
|
+
return pickState(initial);
|
|
356
|
+
}
|
|
357
|
+
var Actor = class {
|
|
358
|
+
state;
|
|
359
|
+
clock;
|
|
360
|
+
#context;
|
|
361
|
+
#options;
|
|
362
|
+
#regions = {};
|
|
363
|
+
#children = /* @__PURE__ */ new Map();
|
|
364
|
+
#queue = new InternalQueue();
|
|
365
|
+
#subs = new Subscribers();
|
|
366
|
+
#effectAbort = null;
|
|
367
|
+
#outputHandler = null;
|
|
368
|
+
#internalIds;
|
|
369
|
+
#inputIds;
|
|
370
|
+
#internalBudget;
|
|
371
|
+
#draining = false;
|
|
372
|
+
get context() {
|
|
373
|
+
return this.#context;
|
|
374
|
+
}
|
|
375
|
+
get regions() {
|
|
376
|
+
return this.#regions;
|
|
377
|
+
}
|
|
378
|
+
get options() {
|
|
379
|
+
return this.#options;
|
|
380
|
+
}
|
|
381
|
+
constructor(options) {
|
|
382
|
+
registerActor(this, {
|
|
383
|
+
children: this.#children,
|
|
384
|
+
getOutputHandler: () => this.#outputHandler,
|
|
385
|
+
setOutputHandler: (fn) => {
|
|
386
|
+
this.#outputHandler = fn;
|
|
387
|
+
},
|
|
388
|
+
pushInternal: (event) => this.#pushEvent(event),
|
|
389
|
+
drainInternal: () => this.#drainInternal(),
|
|
390
|
+
abortEffects: () => this.#abortEffects()
|
|
391
|
+
});
|
|
392
|
+
const builder = new ActorBuilder();
|
|
393
|
+
options.setup(builder);
|
|
394
|
+
const built = builder.build();
|
|
395
|
+
this.#options = {
|
|
396
|
+
...options,
|
|
397
|
+
transitions: built.transitions,
|
|
398
|
+
effects: built.effects
|
|
399
|
+
};
|
|
400
|
+
this.#internalIds = new Set((options.internal ?? []).map((e) => e.id));
|
|
401
|
+
this.#inputIds = new Set(options.inputs.map((e) => e.id));
|
|
402
|
+
this.#internalBudget = options.internalBudget ?? 1e4;
|
|
403
|
+
this.clock = options.clock ?? new RealClock();
|
|
404
|
+
this.clock.setDrain?.(() => this.#drainInternal());
|
|
405
|
+
const initState = resolveInitial(options.initial);
|
|
406
|
+
const stateNames = new Set(options.states.map((s) => s.name));
|
|
407
|
+
if (!stateNames.has(initState.name)) console.warn(`[Actor] initial state "${initState.name}" not found in declared states [${[...stateNames].join(", ")}]`);
|
|
408
|
+
this.state = initState;
|
|
409
|
+
this.#context = options.context ?? {};
|
|
410
|
+
if (options.regions) for (const [key, child] of Object.entries(options.regions)) {
|
|
411
|
+
this.#regions[key] = child;
|
|
412
|
+
const result = setOutputHandler(child, (event) => {
|
|
413
|
+
this.#queue.push(event);
|
|
414
|
+
this.#drainInternal();
|
|
415
|
+
});
|
|
416
|
+
if (result[0] !== void 0) console.error(result[0].message);
|
|
417
|
+
this.#children.set(key, child);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
on(event, fn) {
|
|
421
|
+
if (event === "change") {
|
|
422
|
+
const cb = fn;
|
|
423
|
+
this.#subs.change.add(cb);
|
|
424
|
+
cb(this.snapshot());
|
|
425
|
+
return () => this.#subs.change.delete(cb);
|
|
426
|
+
}
|
|
427
|
+
const cb = fn;
|
|
428
|
+
this.#subs.done.add(cb);
|
|
429
|
+
return () => this.#subs.done.delete(cb);
|
|
430
|
+
}
|
|
431
|
+
settled() {
|
|
432
|
+
return this.#queue.settled();
|
|
433
|
+
}
|
|
434
|
+
send(event) {
|
|
435
|
+
this.#dispatch(event);
|
|
436
|
+
}
|
|
437
|
+
#dispatch(event) {
|
|
438
|
+
if (this.state.isFinal) return;
|
|
439
|
+
const transitions = this.#options.transitions;
|
|
440
|
+
const stateTransition = transitions[this.state.name]?.[event.id];
|
|
441
|
+
const anyTransition = transitions["Any"]?.[event.id];
|
|
442
|
+
const transitionApplied = stateTransition ? this.#applyStateStep(event, stateTransition) : false;
|
|
443
|
+
if ((anyTransition ? this.#applyAnyStep(event, anyTransition, !transitionApplied) : false) || this.#queue.length > 0) this.#drainInternal();
|
|
444
|
+
else if (!stateTransition && !anyTransition) console.warn(`[Actor] no transition for event "${event.id}" in state "${this.state.name}". Event dropped.`);
|
|
445
|
+
}
|
|
446
|
+
#applyStateStep(event, transition) {
|
|
447
|
+
const step = transition(event, {
|
|
448
|
+
context: this.#context,
|
|
449
|
+
actor: this
|
|
450
|
+
});
|
|
451
|
+
if (step.emit) this.#queue.push(...step.emit);
|
|
452
|
+
if (step.state) {
|
|
453
|
+
this.#applyTransition(event, step);
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
#applyAnyStep(event, transition, allowTransition) {
|
|
459
|
+
const step = transition(event, {
|
|
460
|
+
context: this.#context,
|
|
461
|
+
actor: this
|
|
462
|
+
});
|
|
463
|
+
if (step.state && allowTransition) this.#applyTransition(event, step);
|
|
464
|
+
if (step.emit) {
|
|
465
|
+
this.#queue.push(...step.emit);
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
snapshot() {
|
|
471
|
+
return buildSnapshot(this.state, this.#regions);
|
|
472
|
+
}
|
|
473
|
+
#pushEvent(event) {
|
|
474
|
+
this.#queue.push(event);
|
|
475
|
+
}
|
|
476
|
+
#abortEffects() {
|
|
477
|
+
this.#effectAbort?.abort();
|
|
478
|
+
this.#subs.clear();
|
|
479
|
+
}
|
|
480
|
+
#applyTransition(event, step) {
|
|
481
|
+
if (!step.state) return;
|
|
482
|
+
this.#effectAbort?.abort();
|
|
483
|
+
const resolved = parseTarget(step);
|
|
484
|
+
if (!resolved) return;
|
|
485
|
+
this.state = resolved.state;
|
|
486
|
+
this.#runEffects(event, resolved.payload);
|
|
487
|
+
this.#subs.emitChange(this.snapshot());
|
|
488
|
+
if (this.state.isFinal) this.#subs.emitDone();
|
|
489
|
+
}
|
|
490
|
+
#runEffects(event, statePayload) {
|
|
491
|
+
const list = this.#options.effects[this.state.name];
|
|
492
|
+
if (!list || list.length === 0) {
|
|
493
|
+
this.#effectAbort = null;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const abort = runEffects({
|
|
497
|
+
effects: { [this.state.name]: list },
|
|
498
|
+
state: this.state,
|
|
499
|
+
statePayload,
|
|
500
|
+
event,
|
|
501
|
+
context: this.#context,
|
|
502
|
+
emit: (e) => {
|
|
503
|
+
this.#queue.push(e);
|
|
504
|
+
this.#drainInternal();
|
|
505
|
+
},
|
|
506
|
+
clock: this.clock
|
|
507
|
+
});
|
|
508
|
+
this.#effectAbort = abort;
|
|
509
|
+
}
|
|
510
|
+
#drainInternal() {
|
|
511
|
+
if (this.#draining) return;
|
|
512
|
+
this.#draining = true;
|
|
513
|
+
let budgetExceeded = false;
|
|
514
|
+
try {
|
|
515
|
+
const budget = this.#internalBudget;
|
|
516
|
+
let count = 0;
|
|
517
|
+
this.#queue.processCancellable((event) => {
|
|
518
|
+
if (count >= budget) {
|
|
519
|
+
budgetExceeded = true;
|
|
520
|
+
this.#abortEffects();
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
count++;
|
|
524
|
+
if (this.#internalIds.has(event.id)) this.#dispatch(event);
|
|
525
|
+
else if (this.#inputIds.has(event.id)) this.#dispatch(event);
|
|
526
|
+
else if (this.#outputHandler) this.#outputHandler(event);
|
|
527
|
+
return true;
|
|
528
|
+
});
|
|
529
|
+
} finally {
|
|
530
|
+
this.#draining = false;
|
|
531
|
+
}
|
|
532
|
+
if (budgetExceeded) console.warn(`[Actor] internal event budget (${this.#internalBudget}) exceeded — possible emit loop. Actor halting.`);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/event.ts
|
|
537
|
+
function event(id) {
|
|
538
|
+
return () => new EventRef(id);
|
|
539
|
+
}
|
|
540
|
+
var EventRef = class {
|
|
541
|
+
id;
|
|
542
|
+
constructor(id) {
|
|
543
|
+
this.id = id;
|
|
544
|
+
}
|
|
545
|
+
is(anyEvent) {
|
|
546
|
+
return !!anyEvent && typeof anyEvent === "object" && "id" in anyEvent && anyEvent.id === this.id;
|
|
547
|
+
}
|
|
548
|
+
create(payload) {
|
|
549
|
+
if (payload === void 0) return { id: this.id };
|
|
550
|
+
return {
|
|
551
|
+
...payload,
|
|
552
|
+
id: this.id
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
//#endregion
|
|
557
|
+
export { Actor, EventRef, RealClock, StateRef, VirtualClock, event, state };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { s as InternalEvent, t as AnyActor } from "./actor-internal-CfZDmHJ3.mjs";
|
|
2
|
+
import { Either } from "@mantaq/utils";
|
|
3
|
+
//#region src/internal-registry.d.ts
|
|
4
|
+
interface ActorInternal {
|
|
5
|
+
children: Map<string, AnyActor>;
|
|
6
|
+
getOutputHandler(): ((event: InternalEvent) => void) | null;
|
|
7
|
+
setOutputHandler(fn: ((event: InternalEvent) => void) | null): void;
|
|
8
|
+
pushInternal(event: InternalEvent): void;
|
|
9
|
+
drainInternal(): void;
|
|
10
|
+
abortEffects(): void;
|
|
11
|
+
}
|
|
12
|
+
interface RegistryError {
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
declare global {
|
|
16
|
+
var __mantaqCoreInternalRegistry: WeakMap<object, ActorInternal> | undefined;
|
|
17
|
+
}
|
|
18
|
+
declare function registerActor(actor: object, internal: ActorInternal): void;
|
|
19
|
+
declare function getChildren(actor: object): Either<RegistryError, Map<string, AnyActor>>;
|
|
20
|
+
declare function getOutputHandler(actor: object): Either<RegistryError, ((event: InternalEvent) => void) | null>;
|
|
21
|
+
declare function setOutputHandler(actor: object, fn: ((event: InternalEvent) => void) | null): Either<RegistryError, void>;
|
|
22
|
+
declare function pushInternal(actor: object, event: InternalEvent): Either<RegistryError, void>;
|
|
23
|
+
declare function drainInternal(actor: object): Either<RegistryError, void>;
|
|
24
|
+
declare function abortEffects(actor: object): Either<RegistryError, void>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { ActorInternal, RegistryError, abortEffects, drainInternal, getChildren, getOutputHandler, pushInternal, registerActor, setOutputHandler };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Either } from "@mantaq/utils";
|
|
2
|
+
//#region src/internal-registry.ts
|
|
3
|
+
const registry = globalThis.__mantaqCoreInternalRegistry ??= /* @__PURE__ */ new WeakMap();
|
|
4
|
+
const UNREGISTERED = { message: "[mantaq] actor is not registered with the internal registry" };
|
|
5
|
+
function withInternal(actor, fn) {
|
|
6
|
+
const internal = registry.get(actor);
|
|
7
|
+
return internal === void 0 ? Either.left(UNREGISTERED) : [void 0, fn(internal)];
|
|
8
|
+
}
|
|
9
|
+
function registerActor(actor, internal) {
|
|
10
|
+
registry.set(actor, internal);
|
|
11
|
+
}
|
|
12
|
+
function getChildren(actor) {
|
|
13
|
+
return withInternal(actor, (internal) => internal.children);
|
|
14
|
+
}
|
|
15
|
+
function getOutputHandler(actor) {
|
|
16
|
+
return withInternal(actor, (internal) => internal.getOutputHandler());
|
|
17
|
+
}
|
|
18
|
+
function setOutputHandler(actor, fn) {
|
|
19
|
+
return withInternal(actor, (internal) => internal.setOutputHandler(fn));
|
|
20
|
+
}
|
|
21
|
+
function pushInternal(actor, event) {
|
|
22
|
+
return withInternal(actor, (internal) => internal.pushInternal(event));
|
|
23
|
+
}
|
|
24
|
+
function drainInternal(actor) {
|
|
25
|
+
return withInternal(actor, (internal) => internal.drainInternal());
|
|
26
|
+
}
|
|
27
|
+
function abortEffects(actor) {
|
|
28
|
+
return withInternal(actor, (internal) => internal.abortEffects());
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { abortEffects, drainInternal, getChildren, getOutputHandler, pushInternal, registerActor, setOutputHandler };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mantaq/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "State machine runtime with actor model, event system, state hierarchy, effects, and virtual clock.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/AndersCan/mantaq",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.mjs",
|
|
17
|
+
"./internal": "./dist/internal.mjs",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@mantaq/utils": "0.0.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@stryker-mutator/core": "^9",
|
|
28
|
+
"@stryker-mutator/vitest-runner": "^9",
|
|
29
|
+
"@types/node": "^26.2.0",
|
|
30
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
31
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
32
|
+
"typescript": "^6.0.3",
|
|
33
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
|
34
|
+
"vite-plus": "0.2.8",
|
|
35
|
+
"vitest": "4.1.10",
|
|
36
|
+
"@mantaq/pbt": "0.0.1"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "vp pack",
|
|
40
|
+
"dev": "vp pack --watch",
|
|
41
|
+
"test": "vp test",
|
|
42
|
+
"check": "vp check",
|
|
43
|
+
"coverage": "vp test run --coverage"
|
|
44
|
+
}
|
|
45
|
+
}
|