@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/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
+ }