@almadar/runtime 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -72
- package/README.md +25 -0
- package/dist/{OrbitalServerRuntime-Bp1YgmI1.d.ts → OrbitalServerRuntime-CNyOL8XD.d.ts} +13 -2
- package/dist/OrbitalServerRuntime.d.ts +2 -2
- package/dist/OrbitalServerRuntime.js +73 -1
- package/dist/OrbitalServerRuntime.js.map +1 -1
- package/dist/ServerBridge.d.ts +1 -1
- package/dist/{chunk-QBQSJJF6.js → chunk-EOMQVFFE.js} +247 -32
- package/dist/chunk-EOMQVFFE.js.map +1 -0
- package/dist/index.d.ts +127 -6
- package/dist/index.js +79 -2
- package/dist/index.js.map +1 -1
- package/dist/{types-LiBPiu-u.d.ts → types-9hbY6RWC.d.ts} +87 -3
- package/package.json +6 -5
- package/dist/chunk-QBQSJJF6.js.map +0 -1
|
@@ -6,13 +6,28 @@ import { OrbitalSchemaSchema, isEntityReference, parseEntityRef, parseImportedTr
|
|
|
6
6
|
var EventBus = class {
|
|
7
7
|
listeners = /* @__PURE__ */ new Map();
|
|
8
8
|
debug;
|
|
9
|
+
/** Maximum recursion depth before circuit breaker activates (RCG-05) */
|
|
10
|
+
maxDepth;
|
|
11
|
+
/** Current emission depth for circular loop detection */
|
|
12
|
+
depth = 0;
|
|
9
13
|
constructor(options = {}) {
|
|
10
14
|
this.debug = options.debug ?? false;
|
|
15
|
+
this.maxDepth = options.maxDepth ?? 10;
|
|
11
16
|
}
|
|
12
17
|
/**
|
|
13
|
-
* Emit an event to all registered listeners
|
|
18
|
+
* Emit an event to all registered listeners.
|
|
19
|
+
*
|
|
20
|
+
* Includes circuit breaker (RCG-05): if emit is called recursively
|
|
21
|
+
* beyond `maxDepth`, the event is dropped and an error is logged.
|
|
22
|
+
* This prevents infinite loops from circular emit/listen chains.
|
|
14
23
|
*/
|
|
15
24
|
emit(type, payload, source) {
|
|
25
|
+
if (this.depth >= this.maxDepth) {
|
|
26
|
+
console.error(
|
|
27
|
+
`[EventBus] Circular event loop detected: "${type}" at depth ${this.depth}. Event dropped to prevent infinite recursion. Increase maxDepth (currently ${this.maxDepth}) if this is intentional.`
|
|
28
|
+
);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
16
31
|
const event = {
|
|
17
32
|
type,
|
|
18
33
|
payload,
|
|
@@ -23,32 +38,37 @@ var EventBus = class {
|
|
|
23
38
|
const listenerCount = listeners?.size ?? 0;
|
|
24
39
|
if (this.debug) {
|
|
25
40
|
if (listenerCount > 0) {
|
|
26
|
-
console.log(`[EventBus] Emit: ${type} \u2192 ${listenerCount} listener(s)`, payload);
|
|
41
|
+
console.log(`[EventBus] Emit: ${type} \u2192 ${listenerCount} listener(s) (depth: ${this.depth})`, payload);
|
|
27
42
|
} else {
|
|
28
43
|
console.warn(`[EventBus] Emit: ${type} (NO LISTENERS)`, payload);
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error(`[EventBus] Error in listener for '${type}':`, error);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (type !== "*") {
|
|
42
|
-
const wildcardListeners = this.listeners.get("*");
|
|
43
|
-
if (wildcardListeners) {
|
|
44
|
-
for (const listener of Array.from(wildcardListeners)) {
|
|
46
|
+
this.depth++;
|
|
47
|
+
try {
|
|
48
|
+
if (listeners) {
|
|
49
|
+
const listenersCopy = Array.from(listeners);
|
|
50
|
+
for (const listener of listenersCopy) {
|
|
45
51
|
try {
|
|
46
52
|
listener(event);
|
|
47
53
|
} catch (error) {
|
|
48
|
-
console.error(`[EventBus] Error in
|
|
54
|
+
console.error(`[EventBus] Error in listener for '${type}':`, error);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
}
|
|
58
|
+
if (type !== "*") {
|
|
59
|
+
const wildcardListeners = this.listeners.get("*");
|
|
60
|
+
if (wildcardListeners) {
|
|
61
|
+
for (const listener of Array.from(wildcardListeners)) {
|
|
62
|
+
try {
|
|
63
|
+
listener(event);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(`[EventBus] Error in wildcard listener:`, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
this.depth--;
|
|
52
72
|
}
|
|
53
73
|
}
|
|
54
74
|
/**
|
|
@@ -251,12 +271,16 @@ function extractBindings(value) {
|
|
|
251
271
|
collect(value);
|
|
252
272
|
return [...new Set(bindings)];
|
|
253
273
|
}
|
|
254
|
-
function createContextFromBindings(bindings) {
|
|
255
|
-
|
|
274
|
+
function createContextFromBindings(bindings, strictBindings) {
|
|
275
|
+
const ctx = createMinimalContext(
|
|
256
276
|
bindings.entity || {},
|
|
257
277
|
bindings.payload || {},
|
|
258
278
|
bindings.state || "idle"
|
|
259
279
|
);
|
|
280
|
+
if (strictBindings) {
|
|
281
|
+
ctx.strictBindings = true;
|
|
282
|
+
}
|
|
283
|
+
return ctx;
|
|
260
284
|
}
|
|
261
285
|
function findInitialState(trait) {
|
|
262
286
|
if (!trait.states || trait.states.length === 0) {
|
|
@@ -292,7 +316,15 @@ function normalizeEventKey(eventKey) {
|
|
|
292
316
|
return eventKey.startsWith("UI:") ? eventKey.slice(3) : eventKey;
|
|
293
317
|
}
|
|
294
318
|
function processEvent(options) {
|
|
295
|
-
const {
|
|
319
|
+
const {
|
|
320
|
+
traitState,
|
|
321
|
+
trait,
|
|
322
|
+
eventKey,
|
|
323
|
+
payload,
|
|
324
|
+
entityData,
|
|
325
|
+
guardMode = "permissive",
|
|
326
|
+
strictBindings = false
|
|
327
|
+
} = options;
|
|
296
328
|
const normalizedEvent = normalizeEventKey(eventKey);
|
|
297
329
|
const transition = findTransition(trait, traitState.currentState, normalizedEvent);
|
|
298
330
|
if (!transition) {
|
|
@@ -308,7 +340,7 @@ function processEvent(options) {
|
|
|
308
340
|
entity: entityData,
|
|
309
341
|
payload,
|
|
310
342
|
state: traitState.currentState
|
|
311
|
-
});
|
|
343
|
+
}, strictBindings);
|
|
312
344
|
try {
|
|
313
345
|
const guardPasses = evaluateGuard(
|
|
314
346
|
transition.guard,
|
|
@@ -329,6 +361,24 @@ function processEvent(options) {
|
|
|
329
361
|
};
|
|
330
362
|
}
|
|
331
363
|
} catch (error) {
|
|
364
|
+
if (guardMode === "strict") {
|
|
365
|
+
console.error(
|
|
366
|
+
`[StateMachineCore] Guard error blocks transition ${traitState.currentState}\u2192${transition.to} (${normalizedEvent}):`,
|
|
367
|
+
error
|
|
368
|
+
);
|
|
369
|
+
return {
|
|
370
|
+
executed: false,
|
|
371
|
+
newState: traitState.currentState,
|
|
372
|
+
previousState: traitState.currentState,
|
|
373
|
+
effects: [],
|
|
374
|
+
transition: {
|
|
375
|
+
from: traitState.currentState,
|
|
376
|
+
to: transition.to,
|
|
377
|
+
event: normalizedEvent
|
|
378
|
+
},
|
|
379
|
+
guardResult: false
|
|
380
|
+
};
|
|
381
|
+
}
|
|
332
382
|
console.error("[StateMachineCore] Guard evaluation error:", error);
|
|
333
383
|
}
|
|
334
384
|
}
|
|
@@ -348,11 +398,23 @@ function processEvent(options) {
|
|
|
348
398
|
var StateMachineManager = class {
|
|
349
399
|
traits = /* @__PURE__ */ new Map();
|
|
350
400
|
states = /* @__PURE__ */ new Map();
|
|
351
|
-
|
|
401
|
+
config;
|
|
402
|
+
observer;
|
|
403
|
+
constructor(traits = [], config = {}, observer) {
|
|
404
|
+
this.config = config;
|
|
405
|
+
this.observer = observer;
|
|
352
406
|
for (const trait of traits) {
|
|
353
407
|
this.addTrait(trait);
|
|
354
408
|
}
|
|
355
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Set the transition observer for runtime verification.
|
|
412
|
+
* Wire this to `verificationRegistry.recordTransition()` to enable
|
|
413
|
+
* automatic verification tracking.
|
|
414
|
+
*/
|
|
415
|
+
setObserver(observer) {
|
|
416
|
+
this.observer = observer;
|
|
417
|
+
}
|
|
356
418
|
/**
|
|
357
419
|
* Add a trait to the manager.
|
|
358
420
|
*/
|
|
@@ -403,7 +465,9 @@ var StateMachineManager = class {
|
|
|
403
465
|
trait,
|
|
404
466
|
eventKey,
|
|
405
467
|
payload,
|
|
406
|
-
entityData
|
|
468
|
+
entityData,
|
|
469
|
+
guardMode: this.config.guardMode,
|
|
470
|
+
strictBindings: this.config.strictBindings
|
|
407
471
|
});
|
|
408
472
|
if (result.executed) {
|
|
409
473
|
this.states.set(traitName, {
|
|
@@ -414,6 +478,17 @@ var StateMachineManager = class {
|
|
|
414
478
|
context: { ...traitState.context, ...payload }
|
|
415
479
|
});
|
|
416
480
|
results.push({ traitName, result });
|
|
481
|
+
if (this.observer && result.transition) {
|
|
482
|
+
this.observer.onTransition({
|
|
483
|
+
traitName,
|
|
484
|
+
from: result.transition.from,
|
|
485
|
+
to: result.transition.to,
|
|
486
|
+
event: result.transition.event,
|
|
487
|
+
guardResult: result.guardResult,
|
|
488
|
+
// Effects will be traced when executed — placeholder here
|
|
489
|
+
effects: []
|
|
490
|
+
});
|
|
491
|
+
}
|
|
417
492
|
}
|
|
418
493
|
}
|
|
419
494
|
return results;
|
|
@@ -437,6 +512,27 @@ var StateMachineManager = class {
|
|
|
437
512
|
}
|
|
438
513
|
};
|
|
439
514
|
|
|
515
|
+
// src/types.ts
|
|
516
|
+
var HANDLER_MANIFEST = {
|
|
517
|
+
client: ["render-ui", "render", "navigate", "notify", "emit", "set", "log"],
|
|
518
|
+
server: ["persist", "fetch", "call-service", "emit", "set", "spawn", "despawn", "log"],
|
|
519
|
+
test: [
|
|
520
|
+
"render-ui",
|
|
521
|
+
"render",
|
|
522
|
+
"navigate",
|
|
523
|
+
"notify",
|
|
524
|
+
"emit",
|
|
525
|
+
"set",
|
|
526
|
+
"persist",
|
|
527
|
+
"fetch",
|
|
528
|
+
"call-service",
|
|
529
|
+
"spawn",
|
|
530
|
+
"despawn",
|
|
531
|
+
"log"
|
|
532
|
+
],
|
|
533
|
+
ssr: ["render-ui", "render", "fetch", "emit", "set", "log"]
|
|
534
|
+
};
|
|
535
|
+
|
|
440
536
|
// src/EffectExecutor.ts
|
|
441
537
|
function parseEffect(effect) {
|
|
442
538
|
if (!Array.isArray(effect) || effect.length === 0) {
|
|
@@ -448,8 +544,8 @@ function parseEffect(effect) {
|
|
|
448
544
|
}
|
|
449
545
|
return { operator, args };
|
|
450
546
|
}
|
|
451
|
-
function resolveArgs(args, bindings) {
|
|
452
|
-
const ctx = createContextFromBindings(bindings);
|
|
547
|
+
function resolveArgs(args, bindings, strictBindings) {
|
|
548
|
+
const ctx = createContextFromBindings(bindings, strictBindings);
|
|
453
549
|
return args.map((arg) => interpolateValue(arg, ctx));
|
|
454
550
|
}
|
|
455
551
|
var EffectExecutor = class {
|
|
@@ -457,11 +553,77 @@ var EffectExecutor = class {
|
|
|
457
553
|
bindings;
|
|
458
554
|
context;
|
|
459
555
|
debug;
|
|
556
|
+
strictBindings;
|
|
460
557
|
constructor(options) {
|
|
461
558
|
this.handlers = options.handlers;
|
|
462
559
|
this.bindings = options.bindings;
|
|
463
560
|
this.context = options.context;
|
|
464
561
|
this.debug = options.debug ?? false;
|
|
562
|
+
this.strictBindings = options.strictBindings ?? false;
|
|
563
|
+
}
|
|
564
|
+
// ==========================================================================
|
|
565
|
+
// Handler Manifest Validation (RCG-03)
|
|
566
|
+
// ==========================================================================
|
|
567
|
+
/**
|
|
568
|
+
* Validate that all effect types used in a schema have handlers registered.
|
|
569
|
+
* Call this at runtime startup to catch missing handler setup immediately.
|
|
570
|
+
*
|
|
571
|
+
* @param usedEffectTypes - Effect operator names used in the loaded schemas
|
|
572
|
+
* @param environment - Execution environment for context-aware error messages
|
|
573
|
+
* @returns Array of missing handler errors (empty if all handlers are available)
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* ```ts
|
|
577
|
+
* const missing = EffectExecutor.validateHandlers(
|
|
578
|
+
* ['persist', 'render-ui', 'fetch'],
|
|
579
|
+
* executor.getRegisteredHandlers(),
|
|
580
|
+
* 'client'
|
|
581
|
+
* );
|
|
582
|
+
* if (missing.length > 0) {
|
|
583
|
+
* console.error('Missing handlers:', missing);
|
|
584
|
+
* }
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
static validateHandlers(usedEffectTypes, registeredHandlers, environment) {
|
|
588
|
+
const errors = [];
|
|
589
|
+
const expectedHandlers = environment ? HANDLER_MANIFEST[environment] : void 0;
|
|
590
|
+
for (const effectType of usedEffectTypes) {
|
|
591
|
+
if (!registeredHandlers.includes(effectType)) {
|
|
592
|
+
let message = `Effect "${effectType}" is used in schema but no handler is registered.`;
|
|
593
|
+
if (expectedHandlers && !expectedHandlers.includes(effectType)) {
|
|
594
|
+
message += ` Effect "${effectType}" is not expected in "${environment}" environment.`;
|
|
595
|
+
}
|
|
596
|
+
errors.push(message);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return errors;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Get list of effect operators that have handlers registered.
|
|
603
|
+
*/
|
|
604
|
+
getRegisteredHandlers() {
|
|
605
|
+
const registered = [];
|
|
606
|
+
const handlerMap = {
|
|
607
|
+
"emit": this.handlers.emit,
|
|
608
|
+
"persist": this.handlers.persist,
|
|
609
|
+
"set": this.handlers.set,
|
|
610
|
+
"call-service": this.handlers.callService,
|
|
611
|
+
"fetch": this.handlers.fetch,
|
|
612
|
+
"spawn": this.handlers.spawn,
|
|
613
|
+
"despawn": this.handlers.despawn,
|
|
614
|
+
"render-ui": this.handlers.renderUI,
|
|
615
|
+
"render": this.handlers.renderUI,
|
|
616
|
+
"navigate": this.handlers.navigate,
|
|
617
|
+
"notify": this.handlers.notify,
|
|
618
|
+
"log": this.handlers.log
|
|
619
|
+
};
|
|
620
|
+
for (const [name, handler] of Object.entries(handlerMap)) {
|
|
621
|
+
if (handler) {
|
|
622
|
+
registered.push(name);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
registered.push("do", "when");
|
|
626
|
+
return registered;
|
|
465
627
|
}
|
|
466
628
|
/**
|
|
467
629
|
* Execute a single effect.
|
|
@@ -476,7 +638,7 @@ var EffectExecutor = class {
|
|
|
476
638
|
}
|
|
477
639
|
const { operator, args } = parsed;
|
|
478
640
|
const isCompound = operator === "do" || operator === "when";
|
|
479
|
-
const resolvedArgs = isCompound ? args : resolveArgs(args, this.bindings);
|
|
641
|
+
const resolvedArgs = isCompound ? args : resolveArgs(args, this.bindings, this.strictBindings);
|
|
480
642
|
if (this.debug) {
|
|
481
643
|
console.log("[EffectExecutor] Executing:", operator, resolvedArgs);
|
|
482
644
|
}
|
|
@@ -502,6 +664,54 @@ var EffectExecutor = class {
|
|
|
502
664
|
await Promise.all(effects.map((effect) => this.execute(effect)));
|
|
503
665
|
}
|
|
504
666
|
// ==========================================================================
|
|
667
|
+
// Effect Execution with Results (RCG-04)
|
|
668
|
+
// ==========================================================================
|
|
669
|
+
/**
|
|
670
|
+
* Execute effects and return detailed results for each.
|
|
671
|
+
* Enables compensating transitions by reporting which effects failed.
|
|
672
|
+
*
|
|
673
|
+
* Unlike `executeAll`, this method does NOT throw on effect errors.
|
|
674
|
+
* Instead, it captures errors in the returned `EffectResult[]` array.
|
|
675
|
+
*/
|
|
676
|
+
async executeWithResults(effects) {
|
|
677
|
+
const results = [];
|
|
678
|
+
for (const effect of effects) {
|
|
679
|
+
const parsed = parseEffect(effect);
|
|
680
|
+
if (!parsed) {
|
|
681
|
+
results.push({
|
|
682
|
+
type: "unknown",
|
|
683
|
+
args: [],
|
|
684
|
+
status: "skipped",
|
|
685
|
+
error: "Invalid effect format"
|
|
686
|
+
});
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const start = Date.now();
|
|
690
|
+
const { operator, args: rawArgs } = parsed;
|
|
691
|
+
const isCompound = operator === "do" || operator === "when";
|
|
692
|
+
const resolvedArgs = isCompound ? rawArgs : resolveArgs(rawArgs, this.bindings, this.strictBindings);
|
|
693
|
+
try {
|
|
694
|
+
await this.dispatch(operator, resolvedArgs);
|
|
695
|
+
results.push({
|
|
696
|
+
type: operator,
|
|
697
|
+
args: resolvedArgs,
|
|
698
|
+
status: "executed",
|
|
699
|
+
durationMs: Date.now() - start
|
|
700
|
+
});
|
|
701
|
+
} catch (error) {
|
|
702
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
703
|
+
results.push({
|
|
704
|
+
type: operator,
|
|
705
|
+
args: resolvedArgs,
|
|
706
|
+
status: "failed",
|
|
707
|
+
error: errorMessage,
|
|
708
|
+
durationMs: Date.now() - start
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return results;
|
|
713
|
+
}
|
|
714
|
+
// ==========================================================================
|
|
505
715
|
// Effect Dispatch
|
|
506
716
|
// ==========================================================================
|
|
507
717
|
async dispatch(operator, args) {
|
|
@@ -520,9 +730,14 @@ var EffectExecutor = class {
|
|
|
520
730
|
}
|
|
521
731
|
case "persist": {
|
|
522
732
|
const action = args[0];
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
733
|
+
if (action === "batch") {
|
|
734
|
+
const operations = args[1];
|
|
735
|
+
await this.handlers.persist("batch", "", { operations });
|
|
736
|
+
} else {
|
|
737
|
+
const entityType = args[1];
|
|
738
|
+
const data = args[2];
|
|
739
|
+
await this.handlers.persist(action, entityType, data);
|
|
740
|
+
}
|
|
526
741
|
break;
|
|
527
742
|
}
|
|
528
743
|
case "call-service": {
|
|
@@ -1867,6 +2082,6 @@ function parseNamespacedEvent(eventName) {
|
|
|
1867
2082
|
return { event: eventName };
|
|
1868
2083
|
}
|
|
1869
2084
|
|
|
1870
|
-
export { EffectExecutor, EventBus, StateMachineManager, containsBindings, createContextFromBindings, createInitialTraitState, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isNamespacedEvent, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent };
|
|
1871
|
-
//# sourceMappingURL=chunk-
|
|
1872
|
-
//# sourceMappingURL=chunk-
|
|
2085
|
+
export { EffectExecutor, EventBus, HANDLER_MANIFEST, StateMachineManager, containsBindings, createContextFromBindings, createInitialTraitState, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isNamespacedEvent, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent };
|
|
2086
|
+
//# sourceMappingURL=chunk-EOMQVFFE.js.map
|
|
2087
|
+
//# sourceMappingURL=chunk-EOMQVFFE.js.map
|