@almadar/runtime 6.9.3 → 6.9.5

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.
@@ -1,4 +1,1870 @@
1
- export { InMemoryPersistence, OrbitalServerRuntime, collectDeclaredConfigDefaults, createOrbitalServerRuntime } from './chunk-VUXJJPIQ.js';
1
+ import { EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, StateMachineManager, createContextFromBindings, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, EffectExecutor } from './chunk-R6Y4IJ7I.js';
2
+ export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-R6Y4IJ7I.js';
2
3
  import './chunk-PZ5AY32C.js';
4
+ import { createLogger } from '@almadar/logger';
5
+ import * as nodeModule from 'module';
6
+ import { evaluateGuard, evaluate } from '@almadar/evaluator';
7
+ import { isInlineTrait, isEntityCall } from '@almadar/core';
8
+
9
+ var _resolvedNodeRequire = null;
10
+ function nodeRequire(modulePath) {
11
+ if (!_resolvedNodeRequire) {
12
+ const evalRequire = (0, eval)('typeof require !== "undefined" ? require : null');
13
+ if (evalRequire) {
14
+ _resolvedNodeRequire = evalRequire;
15
+ } else {
16
+ const createReq = nodeModule.createRequire;
17
+ if (typeof createReq !== "function") {
18
+ throw new Error(
19
+ "[OrbitalServerRuntime] No synchronous require available. This branch is Node-only \u2014 invoking it from a browser indicates an isNodeEnv() guard regression upstream."
20
+ );
21
+ }
22
+ _resolvedNodeRequire = createReq(import.meta.url);
23
+ }
24
+ }
25
+ return _resolvedNodeRequire(modulePath);
26
+ }
27
+ var _nodeRequireExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
28
+ var effectLog = createLogger("almadar:runtime:effects");
29
+ var busLog = createLogger("almadar:runtime:bus");
30
+ var renderLog = createLogger("almadar:runtime:render-ui");
31
+ var xOrbitalLog = createLogger("almadar:runtime:cross-orbital");
32
+ var persistLog = createLogger("almadar:runtime:persist");
33
+ var registerLog = createLogger("almadar:runtime:register");
34
+ var dynamicLog = createLogger("almadar:runtime:dynamic");
35
+ function isNodeEnv() {
36
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
37
+ }
38
+ function needsPreprocessing(schema) {
39
+ for (const orbital of schema.orbitals) {
40
+ const uses = orbital.uses;
41
+ if (Array.isArray(uses) && uses.length > 0) {
42
+ return true;
43
+ }
44
+ const traits = orbital.traits ?? [];
45
+ for (const t of traits) {
46
+ if (!t || typeof t !== "object") continue;
47
+ const obj = t;
48
+ if (typeof obj.ref === "string" && obj.ref.includes(".") && !obj.stateMachine) {
49
+ return true;
50
+ }
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+ var OrbitalServerRuntime = class {
56
+ orbitals = /* @__PURE__ */ new Map();
57
+ eventBus;
58
+ config;
59
+ persistence;
60
+ listenerCleanups = [];
61
+ tickBindings = [];
62
+ loader = null;
63
+ preprocessedCache = /* @__PURE__ */ new Map();
64
+ entitySharingMap = {};
65
+ eventNamespaceMap = {};
66
+ osHandlers = null;
67
+ osHandlersPromise = null;
68
+ localPersistence = null;
69
+ resolvedSchema = null;
70
+ constructor(config = {}) {
71
+ this.config = {
72
+ mode: "mock",
73
+ // Default to mock mode for preview
74
+ autoPreprocess: false,
75
+ namespaceEvents: true,
76
+ ...config
77
+ };
78
+ this.eventBus = new EventBus();
79
+ if (config.loaderConfig?.loader) {
80
+ this.loader = config.loaderConfig.loader;
81
+ } else if (config.loaderConfig?.stdLibPath) {
82
+ this.loader = createUnifiedLoader({
83
+ basePath: config.loaderConfig.basePath,
84
+ stdLibPath: config.loaderConfig.stdLibPath,
85
+ scopedPaths: config.loaderConfig.scopedPaths
86
+ });
87
+ }
88
+ if (this.config.mode === "mock" && !config.persistence) {
89
+ this.persistence = new MockPersistenceAdapter({
90
+ seed: config.mockSeed,
91
+ defaultSeedCount: config.mockSeedCount ?? 6,
92
+ debug: config.debug
93
+ });
94
+ if (config.debug) {
95
+ persistLog.debug("mock:init", { adapter: "MockPersistenceAdapter" });
96
+ }
97
+ } else {
98
+ this.persistence = config.persistence || new InMemoryPersistence();
99
+ }
100
+ if (config.localStorageRoot && isNodeEnv()) {
101
+ const { LocalPersistenceAdapter } = nodeRequire(`./LocalPersistenceAdapter${_nodeRequireExt}`);
102
+ this.localPersistence = new LocalPersistenceAdapter(config.localStorageRoot);
103
+ }
104
+ this.osHandlers = { handlers: {}, cleanup: () => {
105
+ } };
106
+ }
107
+ /**
108
+ * Lazily wire the OS-level effect handlers (fs/net/child_process), merging
109
+ * them UNDER any user-provided handlers. Deferred out of the constructor
110
+ * because `./createOsHandlers.js` is ESM and `require()`-ing it from a
111
+ * `type: module` package throws ERR_REQUIRE_ESM — so it is dynamic-import()ed
112
+ * here on the first event. Node-only, idempotent (single shared load), and a
113
+ * no-op in the browser (the import never runs behind the isNodeEnv guard).
114
+ */
115
+ async ensureOsHandlers() {
116
+ if (!isNodeEnv()) return;
117
+ if (!this.osHandlersPromise) {
118
+ this.osHandlersPromise = (async () => {
119
+ const { createOsHandlers } = await import('./createOsHandlers.js');
120
+ this.osHandlers = createOsHandlers({
121
+ emitEvent: (type, payload) => this.eventBus.emit(type, payload)
122
+ });
123
+ this.config.effectHandlers = {
124
+ ...this.osHandlers.handlers,
125
+ ...this.config.effectHandlers
126
+ };
127
+ })();
128
+ }
129
+ return this.osHandlersPromise;
130
+ }
131
+ /**
132
+ * Lazily construct a default loader when the caller didn't provide one
133
+ * but `register()` needs to preprocess. Looks for `@almadar/std` in the
134
+ * nearest `node_modules` so cross-orbital `std/behaviors/<name>` imports
135
+ * resolve to the tiered registry on disk.
136
+ *
137
+ * Node only — browsers should receive already-preprocessed schemas from
138
+ * their server.
139
+ */
140
+ async ensureLoader() {
141
+ if (this.loader) return;
142
+ if (typeof process === "undefined" || !process.versions?.node) {
143
+ return;
144
+ }
145
+ try {
146
+ const [{ fileURLToPath }, path, fs] = await Promise.all([
147
+ import('url'),
148
+ import('path'),
149
+ import('fs')
150
+ ]);
151
+ const mainEntryUrl = import.meta.resolve("@almadar/std");
152
+ const mainEntry = fileURLToPath(mainEntryUrl);
153
+ let stdLibPath = path.dirname(mainEntry);
154
+ while (stdLibPath !== path.dirname(stdLibPath)) {
155
+ if (fs.existsSync(path.join(stdLibPath, "package.json"))) {
156
+ const pkg = JSON.parse(
157
+ fs.readFileSync(path.join(stdLibPath, "package.json"), "utf-8")
158
+ );
159
+ if (pkg.name === "@almadar/std") break;
160
+ }
161
+ stdLibPath = path.dirname(stdLibPath);
162
+ }
163
+ const basePath = this.config.loaderConfig?.basePath ?? process.cwd();
164
+ this.loader = createUnifiedLoader({
165
+ basePath,
166
+ stdLibPath,
167
+ scopedPaths: this.config.loaderConfig?.scopedPaths
168
+ });
169
+ if (this.config.debug) {
170
+ registerLog.debug("loader:constructed", { basePath, stdLibPath });
171
+ }
172
+ } catch (err) {
173
+ if (this.config.debug) {
174
+ registerLog.warn("loader:construct-failed", { error: err instanceof Error ? err : String(err) });
175
+ }
176
+ }
177
+ }
178
+ // ==========================================================================
179
+ // Schema Registration
180
+ // ==========================================================================
181
+ /**
182
+ * Register an OrbitalSchema for execution.
183
+ *
184
+ * Auto-preprocesses the schema when it contains `uses` declarations or
185
+ * unresolved cross-orbital trait references (e.g. a trait with
186
+ * `ref: "Modal.traits.ModalRecordModal"` and no inline `stateMachine`).
187
+ * Without preprocessing, those refs arrive empty at the state machine and
188
+ * button clicks silently do nothing — see Phase 9.5.H.
189
+ *
190
+ * Preprocessing needs a loader. If `loaderConfig` is set, that loader is
191
+ * used. Otherwise, a default loader is constructed that points at
192
+ * `<cwd>` (for `basePath`) and the nearest `node_modules/@almadar/std` (for
193
+ * `stdLibPath`), which matches how every caller in this monorepo has the
194
+ * std registry on disk.
195
+ */
196
+ async register(schema) {
197
+ if (this.config.debug) {
198
+ registerLog.debug("register:schema", { name: schema.name });
199
+ }
200
+ if (needsPreprocessing(schema)) {
201
+ await this.ensureLoader();
202
+ if (this.loader) {
203
+ if (this.config.debug) {
204
+ registerLog.debug("register:auto-preprocessing", { name: schema.name });
205
+ }
206
+ const result = await preprocessSchema(schema, {
207
+ basePath: this.config.loaderConfig?.basePath || process.cwd(),
208
+ stdLibPath: this.config.loaderConfig?.stdLibPath,
209
+ scopedPaths: this.config.loaderConfig?.scopedPaths,
210
+ loader: this.loader,
211
+ namespaceEvents: this.config.namespaceEvents
212
+ });
213
+ if (!result.success) {
214
+ throw new Error(
215
+ `Schema preprocessing failed: ${result.errors.join("; ")}`
216
+ );
217
+ }
218
+ schema = result.data.schema;
219
+ this.entitySharingMap = {
220
+ ...this.entitySharingMap,
221
+ ...result.data.entitySharing
222
+ };
223
+ this.eventNamespaceMap = {
224
+ ...this.eventNamespaceMap,
225
+ ...result.data.eventNamespaces
226
+ };
227
+ } else if (this.config.debug) {
228
+ registerLog.warn("register:no-loader", { name: schema.name });
229
+ }
230
+ }
231
+ for (const orbital of schema.orbitals) {
232
+ await this.registerOrbitalAsync(orbital);
233
+ }
234
+ this.setupEventListeners();
235
+ this.setupTicks();
236
+ this.resolvedSchema = schema;
237
+ }
238
+ /**
239
+ * Register an OrbitalSchema synchronously (for backward compatibility).
240
+ * Note: This version doesn't wait for instance seeding to complete.
241
+ * Use async register() for guaranteed instance seeding.
242
+ */
243
+ registerSync(schema) {
244
+ if (this.config.debug) {
245
+ registerLog.debug("register:schema-sync", { name: schema.name });
246
+ }
247
+ for (const orbital of schema.orbitals) {
248
+ this.registerOrbital(orbital);
249
+ }
250
+ this.setupEventListeners();
251
+ this.setupTicks();
252
+ this.resolvedSchema = schema;
253
+ }
254
+ /**
255
+ * Returns the schema that this runtime is currently executing, post-
256
+ * preprocessing. Safe to expose from an HTTP `/api/schema` endpoint — every
257
+ * cross-orbital trait ref will have an inline `stateMachine` already, which
258
+ * is what the browser's `schema-to-ir` resolver needs to wire button clicks
259
+ * back to state transitions.
260
+ *
261
+ * Returns `null` if `register()` hasn't run yet.
262
+ */
263
+ getResolvedSchema() {
264
+ return this.resolvedSchema;
265
+ }
266
+ /**
267
+ * One-call entry point: read an `.orb` file from disk, parse it, preprocess
268
+ * cross-orbital imports, and register the result. Callers never touch raw
269
+ * `.orb` bytes — `register()` handles preprocessing internally.
270
+ *
271
+ * Node only. Browsers must receive already-resolved schemas from their
272
+ * server (see `getResolvedSchema()`).
273
+ */
274
+ async registerFromFile(path) {
275
+ if (typeof process === "undefined" || !process.versions?.node) {
276
+ throw new Error(
277
+ "registerFromFile is Node-only. Browsers should receive resolved schemas from their server."
278
+ );
279
+ }
280
+ const { readFile } = await import('fs/promises');
281
+ const raw = await readFile(path, "utf-8");
282
+ let schema;
283
+ try {
284
+ schema = JSON.parse(raw);
285
+ } catch (err) {
286
+ const msg = err instanceof Error ? err.message : String(err);
287
+ throw new Error(`registerFromFile: ${path} is not valid JSON: ${msg}`);
288
+ }
289
+ await this.register(schema);
290
+ }
291
+ /**
292
+ * Register an OrbitalSchema with preprocessing to resolve `uses` imports.
293
+ *
294
+ * This method:
295
+ * 1. Loads all external orbitals referenced in `uses` declarations
296
+ * 2. Expands entity/trait/page references to inline definitions
297
+ * 3. Builds entity sharing and event namespace maps
298
+ * 4. Caches the preprocessed result
299
+ * 5. Registers the resolved schema
300
+ *
301
+ * @param schema - Schema with potential `uses` declarations
302
+ * @param options - Optional preprocessing options
303
+ * @returns Preprocessing result with entity sharing info
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const runtime = new OrbitalServerRuntime({
308
+ * loaderConfig: {
309
+ * basePath: '/schemas',
310
+ * stdLibPath: '/std',
311
+ * },
312
+ * });
313
+ *
314
+ * const result = await runtime.registerWithPreprocess(schema);
315
+ * if (result.success) {
316
+ * console.log('Registered with', Object.keys(result.entitySharing).length, 'orbitals');
317
+ * }
318
+ * ```
319
+ */
320
+ async registerWithPreprocess(schema, options) {
321
+ if (!this.loader && !this.config.loaderConfig) {
322
+ return {
323
+ success: false,
324
+ errors: ["Loader not configured. Set loaderConfig in OrbitalServerRuntimeConfig."]
325
+ };
326
+ }
327
+ if (!this.loader && this.config.loaderConfig) {
328
+ this.loader = this.config.loaderConfig.loader ?? createUnifiedLoader({
329
+ basePath: this.config.loaderConfig.basePath,
330
+ stdLibPath: this.config.loaderConfig.stdLibPath,
331
+ scopedPaths: this.config.loaderConfig.scopedPaths
332
+ });
333
+ }
334
+ const cacheKey = `${schema.name}:${schema.version || "1.0.0"}`;
335
+ const cached = this.preprocessedCache.get(cacheKey);
336
+ if (cached) {
337
+ if (this.config.debug) {
338
+ registerLog.debug("preprocess:cache-hit", { name: schema.name });
339
+ }
340
+ this.register(cached.schema);
341
+ this.entitySharingMap = { ...this.entitySharingMap, ...cached.entitySharing };
342
+ this.eventNamespaceMap = { ...this.eventNamespaceMap, ...cached.eventNamespaces };
343
+ return {
344
+ success: true,
345
+ entitySharing: cached.entitySharing,
346
+ eventNamespaces: cached.eventNamespaces,
347
+ warnings: cached.warnings
348
+ };
349
+ }
350
+ if (this.config.debug) {
351
+ registerLog.debug("preprocess:start", { name: schema.name });
352
+ }
353
+ const result = await preprocessSchema(schema, {
354
+ basePath: this.config.loaderConfig?.basePath || ".",
355
+ stdLibPath: this.config.loaderConfig?.stdLibPath,
356
+ scopedPaths: this.config.loaderConfig?.scopedPaths,
357
+ loader: this.loader,
358
+ namespaceEvents: this.config.namespaceEvents
359
+ });
360
+ if (!result.success) {
361
+ return {
362
+ success: false,
363
+ errors: result.errors
364
+ };
365
+ }
366
+ this.preprocessedCache.set(cacheKey, result.data);
367
+ this.entitySharingMap = { ...this.entitySharingMap, ...result.data.entitySharing };
368
+ this.eventNamespaceMap = { ...this.eventNamespaceMap, ...result.data.eventNamespaces };
369
+ this.register(result.data.schema);
370
+ return {
371
+ success: true,
372
+ entitySharing: result.data.entitySharing,
373
+ eventNamespaces: result.data.eventNamespaces,
374
+ warnings: result.data.warnings
375
+ };
376
+ }
377
+ /**
378
+ * Get entity sharing information for registered orbitals.
379
+ * Useful for determining entity isolation and collection names.
380
+ */
381
+ getEntitySharing() {
382
+ return { ...this.entitySharingMap };
383
+ }
384
+ /**
385
+ * Get event namespace mapping for registered orbitals.
386
+ * Useful for debugging cross-orbital event routing.
387
+ */
388
+ getEventNamespaces() {
389
+ return { ...this.eventNamespaceMap };
390
+ }
391
+ /**
392
+ * Clear the preprocessing cache.
393
+ */
394
+ clearPreprocessCache() {
395
+ this.preprocessedCache.clear();
396
+ }
397
+ /**
398
+ * Register a single orbital
399
+ */
400
+ async registerOrbitalAsync(orbital) {
401
+ const configByTrait = /* @__PURE__ */ new Map();
402
+ const unwrapped = (orbital.traits || []).map((t) => {
403
+ if (t && typeof t === "object" && "ref" in t && "_resolved" in t) {
404
+ const wrapper = t;
405
+ const inner = wrapper._resolved;
406
+ if (wrapper.config && inner?.name) {
407
+ configByTrait.set(inner.name, wrapper.config);
408
+ }
409
+ return inner;
410
+ }
411
+ return t;
412
+ });
413
+ const inlineTraits = unwrapped.filter(isInlineTrait);
414
+ const traitDefs = inlineTraits.map((t) => {
415
+ const sm = t.stateMachine;
416
+ const states = sm?.states || [];
417
+ const transitions = sm?.transitions || [];
418
+ return {
419
+ name: t.name,
420
+ states,
421
+ transitions,
422
+ listens: t.listens
423
+ };
424
+ });
425
+ const manager = new StateMachineManager(traitDefs, {
426
+ contextExtensions: this.config.contextExtensions
427
+ });
428
+ for (const [traitName, traitConfig] of configByTrait) {
429
+ manager.setTraitConfig(traitName, traitConfig);
430
+ }
431
+ const entityRef = orbital.entity;
432
+ let entity;
433
+ if (typeof entityRef === "string") {
434
+ entity = { name: entityRef, fields: [] };
435
+ } else if (isEntityCall(entityRef)) {
436
+ const fallbackName = entityRef.name ?? entityRef.extends.replace(/\.entity$/, "");
437
+ entity = {
438
+ name: fallbackName,
439
+ fields: entityRef.fields ?? [],
440
+ ...entityRef.persistence ? { persistence: entityRef.persistence } : {},
441
+ ...entityRef.collection ? { collection: entityRef.collection } : {}
442
+ };
443
+ } else {
444
+ entity = entityRef;
445
+ }
446
+ this.orbitals.set(orbital.name, {
447
+ schema: orbital,
448
+ entity,
449
+ traits: inlineTraits,
450
+ configByTrait,
451
+ manager,
452
+ entityData: /* @__PURE__ */ new Map(),
453
+ traitFieldStates: /* @__PURE__ */ new Map()
454
+ });
455
+ if (entity?.name && entity.instances && Array.isArray(entity.instances)) {
456
+ const instances = entity.instances;
457
+ if (instances.length > 0) {
458
+ persistLog.debug("seed:start", { entity: entity.name, count: instances.length });
459
+ const results = await Promise.all(
460
+ instances.map(async (instance) => {
461
+ try {
462
+ const result = await this.persistence.create(entity.name, instance);
463
+ persistLog.debug("seed:instance", { entity: entity.name, id: instance.id ?? "no-id" });
464
+ return result;
465
+ } catch (err) {
466
+ persistLog.error("seed:instance-error", {
467
+ entity: entity.name,
468
+ id: instance.id,
469
+ error: err instanceof Error ? err : String(err)
470
+ });
471
+ return null;
472
+ }
473
+ })
474
+ );
475
+ const successCount = results.filter((r) => r !== null).length;
476
+ persistLog.debug("seed:done", { entity: entity.name, success: successCount, total: instances.length });
477
+ }
478
+ } else if (this.config.mode === "mock" && this.persistence instanceof MockPersistenceAdapter) {
479
+ if (this.config.debug) {
480
+ persistLog.debug("mock:generate", { entity: entity?.name });
481
+ }
482
+ if (entity?.name && entity.fields) {
483
+ const fields = entity.fields.filter(
484
+ (f) => typeof f.name === "string" && f.name.length > 0
485
+ );
486
+ this.persistence.registerEntity({ name: entity.name, fields });
487
+ if (this.config.debug) {
488
+ persistLog.debug("mock:seeded", { entity: entity.name, count: this.persistence.count(entity.name) });
489
+ }
490
+ }
491
+ }
492
+ const auxiliaryEntities = orbital.auxiliaryEntities;
493
+ if (auxiliaryEntities !== void 0 && auxiliaryEntities.length > 0 && this.config.mode === "mock" && this.persistence instanceof MockPersistenceAdapter) {
494
+ for (const auxRef of auxiliaryEntities) {
495
+ if (typeof auxRef === "string" || isEntityCall(auxRef)) continue;
496
+ const auxEntity = auxRef;
497
+ if (!auxEntity.name || !auxEntity.fields) continue;
498
+ const auxFields = auxEntity.fields.filter(
499
+ (f) => typeof f.name === "string" && f.name.length > 0
500
+ );
501
+ this.persistence.registerEntity({ name: auxEntity.name, fields: auxFields });
502
+ if (this.config.debug) {
503
+ persistLog.debug("mock:seeded-auxiliary", {
504
+ entity: auxEntity.name,
505
+ count: this.persistence.count(auxEntity.name)
506
+ });
507
+ }
508
+ }
509
+ }
510
+ if (this.config.debug) {
511
+ registerLog.debug("register:orbital", {
512
+ name: orbital.name,
513
+ traitCount: (orbital.traits || []).length
514
+ });
515
+ }
516
+ }
517
+ /**
518
+ * Register a single orbital (sync wrapper for backward compatibility)
519
+ */
520
+ registerOrbital(orbital) {
521
+ this.registerOrbitalAsync(orbital).catch((err) => {
522
+ registerLog.error("register:failed", {
523
+ name: orbital.name,
524
+ error: err instanceof Error ? err : String(err)
525
+ });
526
+ });
527
+ }
528
+ /**
529
+ * Set up event listeners for cross-orbital communication
530
+ */
531
+ setupEventListeners() {
532
+ for (const cleanup of this.listenerCleanups) {
533
+ cleanup();
534
+ }
535
+ this.listenerCleanups = [];
536
+ for (const [orbitalName, registered] of this.orbitals) {
537
+ for (const trait of registered.traits) {
538
+ if (!trait.listens) continue;
539
+ for (const listener of trait.listens) {
540
+ const { bareEvent, matcher } = parseListenSource(listener, orbitalName);
541
+ const cleanup = this.eventBus.on(bareEvent, async (event) => {
542
+ if (!matcher(event.source)) return;
543
+ if (this.config.debug) {
544
+ xOrbitalLog.debug("listen:received", () => ({
545
+ receiverOrbital: orbitalName,
546
+ receiverTrait: trait.name,
547
+ event: listener.event,
548
+ sourceOrbital: event.source?.orbital ?? "?",
549
+ sourceTrait: event.source?.trait ?? "?"
550
+ }));
551
+ }
552
+ let mappedPayload = event.payload;
553
+ if (listener.payloadMapping && event.payload) {
554
+ mappedPayload = {};
555
+ for (const [key, expr] of Object.entries(
556
+ listener.payloadMapping
557
+ )) {
558
+ if (typeof expr === "string" && expr.startsWith("@payload.")) {
559
+ const field = expr.slice("@payload.".length);
560
+ mappedPayload[key] = event.payload[field];
561
+ } else {
562
+ mappedPayload[key] = expr;
563
+ }
564
+ }
565
+ }
566
+ const raw = event.payload;
567
+ const mapped = mappedPayload;
568
+ const pickId = (field) => mapped?.[field] ?? raw?.[field];
569
+ const forwardedEntityId = pickId("entityId") ?? pickId("orbitalName");
570
+ await this.processOrbitalEvent(orbitalName, {
571
+ event: listener.triggers,
572
+ payload: mappedPayload,
573
+ entityId: forwardedEntityId
574
+ });
575
+ });
576
+ this.listenerCleanups.push(cleanup);
577
+ }
578
+ }
579
+ }
580
+ }
581
+ /**
582
+ * Set up scheduled ticks for all traits
583
+ */
584
+ setupTicks() {
585
+ this.cleanupTicks();
586
+ for (const [orbitalName, registered] of this.orbitals) {
587
+ for (const trait of registered.traits || []) {
588
+ if (!trait.ticks || trait.ticks.length === 0) continue;
589
+ for (const tick of trait.ticks) {
590
+ this.registerTick(orbitalName, trait.name, tick, registered);
591
+ }
592
+ }
593
+ }
594
+ if (this.config.debug && this.tickBindings.length > 0) {
595
+ registerLog.debug("register:ticks", { count: this.tickBindings.length });
596
+ }
597
+ }
598
+ /**
599
+ * Register a single tick
600
+ */
601
+ registerTick(orbitalName, traitName, tick, registered) {
602
+ let intervalMs;
603
+ if (typeof tick.interval === "number") {
604
+ intervalMs = tick.interval;
605
+ } else if (typeof tick.interval === "string") {
606
+ intervalMs = this.parseIntervalString(tick.interval);
607
+ } else {
608
+ intervalMs = 1e3;
609
+ }
610
+ if (this.config.debug) {
611
+ registerLog.debug("register:tick", {
612
+ orbital: orbitalName,
613
+ trait: traitName,
614
+ tick: tick.name,
615
+ intervalMs
616
+ });
617
+ }
618
+ const timerId = setInterval(async () => {
619
+ await this.executeTick(orbitalName, traitName, tick, registered);
620
+ }, intervalMs);
621
+ this.tickBindings.push({
622
+ orbitalName,
623
+ traitName,
624
+ tick,
625
+ timerId
626
+ });
627
+ }
628
+ /**
629
+ * Parse interval string to milliseconds
630
+ * Supports: '5s', '1m', '1h', '30000' (ms)
631
+ */
632
+ parseIntervalString(interval) {
633
+ const match = interval.match(/^(\d+)(ms|s|m|h)?$/);
634
+ if (!match) {
635
+ registerLog.warn("register:tick-invalid-interval", { interval, defaultMs: 1e3 });
636
+ return 1e3;
637
+ }
638
+ const value = parseInt(match[1], 10);
639
+ const unit = match[2] || "ms";
640
+ switch (unit) {
641
+ case "ms":
642
+ return value;
643
+ case "s":
644
+ return value * 1e3;
645
+ case "m":
646
+ return value * 60 * 1e3;
647
+ case "h":
648
+ return value * 60 * 60 * 1e3;
649
+ default:
650
+ return value;
651
+ }
652
+ }
653
+ /**
654
+ * Execute a tick for all applicable entities
655
+ */
656
+ async executeTick(orbitalName, traitName, tick, registered) {
657
+ const entityType = registered.entity.name;
658
+ const emittedEvents = [];
659
+ try {
660
+ let entities = await this.persistence.list(entityType);
661
+ if (tick.appliesTo && tick.appliesTo.length > 0) {
662
+ const appliesToSet = new Set(tick.appliesTo);
663
+ entities = entities.filter((e) => appliesToSet.has(e.id));
664
+ }
665
+ if (this.config.debug && entities.length > 0) {
666
+ effectLog.debug("tick:processing", () => ({
667
+ orbital: orbitalName,
668
+ trait: traitName,
669
+ tick: tick.name,
670
+ entityCount: entities.length
671
+ }));
672
+ }
673
+ for (const entity of entities) {
674
+ if (tick.guard) {
675
+ try {
676
+ const ctx = createContextFromBindings({
677
+ entity,
678
+ payload: {},
679
+ state: registered.manager.getState(traitName)?.currentState || "unknown"
680
+ }, false, this.config.contextExtensions);
681
+ const guardPasses = evaluateGuard(
682
+ tick.guard,
683
+ ctx
684
+ );
685
+ if (!guardPasses) {
686
+ if (this.config.debug) {
687
+ effectLog.debug("tick:guard-failed", () => ({
688
+ tick: tick.name,
689
+ entityId: typeof entity.id === "string" ? entity.id : void 0
690
+ }));
691
+ }
692
+ continue;
693
+ }
694
+ } catch (error) {
695
+ effectLog.error("tick:guard-error", {
696
+ tick: tick.name,
697
+ entityId: typeof entity.id === "string" ? entity.id : void 0,
698
+ error: error instanceof Error ? error : String(error)
699
+ });
700
+ continue;
701
+ }
702
+ }
703
+ if (tick.effects && tick.effects.length > 0) {
704
+ const fetchedData = {};
705
+ const clientEffects = [];
706
+ const tickEffectResults = [];
707
+ await this.executeEffects(
708
+ registered,
709
+ traitName,
710
+ tick.effects,
711
+ {},
712
+ // No payload for ticks
713
+ entity,
714
+ entity.id,
715
+ emittedEvents,
716
+ fetchedData,
717
+ clientEffects,
718
+ tickEffectResults
719
+ );
720
+ if (this.config.debug) {
721
+ effectLog.debug("tick:effects-executed", () => ({
722
+ tick: tick.name,
723
+ entityId: typeof entity.id === "string" ? entity.id : void 0
724
+ }));
725
+ }
726
+ }
727
+ }
728
+ } catch (error) {
729
+ effectLog.error("tick:execute-error", {
730
+ tick: tick.name,
731
+ error: error instanceof Error ? error : String(error)
732
+ });
733
+ }
734
+ }
735
+ /**
736
+ * Clean up all active ticks
737
+ */
738
+ cleanupTicks() {
739
+ for (const binding of this.tickBindings) {
740
+ clearInterval(binding.timerId);
741
+ }
742
+ this.tickBindings = [];
743
+ }
744
+ /**
745
+ * Unregister all orbitals and clean up
746
+ */
747
+ unregisterAll() {
748
+ this.cleanupTicks();
749
+ for (const cleanup of this.listenerCleanups) {
750
+ cleanup();
751
+ }
752
+ this.listenerCleanups = [];
753
+ this.orbitals.clear();
754
+ this.eventBus.clear();
755
+ if (this.persistence instanceof MockPersistenceAdapter) {
756
+ this.persistence.clearAll();
757
+ }
758
+ if (this.osHandlers) {
759
+ this.osHandlers.cleanup();
760
+ this.osHandlers = null;
761
+ }
762
+ }
763
+ /**
764
+ * Reset the mock persistence store to a clean-slate re-seed without
765
+ * unregistering orbitals. Exposed for verifier tools that want to
766
+ * start each test with deterministic seeded rows, not the residue of
767
+ * the previous walk's persist-creates. No-op when the persistence
768
+ * layer is not MockPersistenceAdapter.
769
+ */
770
+ resetMockPersistence() {
771
+ if (!(this.persistence instanceof MockPersistenceAdapter)) return;
772
+ busLog.debug("mock:reset:enter", {
773
+ orbitalCount: this.orbitals.size,
774
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
775
+ });
776
+ this.persistence.clearAll();
777
+ for (const registered of this.orbitals.values()) {
778
+ const entity = registered.entity;
779
+ if (entity?.name && entity.fields) {
780
+ const fields = entity.fields.filter(
781
+ (f) => typeof f.name === "string" && f.name.length > 0
782
+ );
783
+ this.persistence.registerEntity({ name: entity.name, fields });
784
+ }
785
+ }
786
+ }
787
+ // ==========================================================================
788
+ // Event Processing
789
+ // ==========================================================================
790
+ /**
791
+ * Process an event for an orbital
792
+ */
793
+ async processOrbitalEvent(orbitalName, request) {
794
+ await this.ensureOsHandlers();
795
+ const registered = this.orbitals.get(orbitalName);
796
+ if (!registered) {
797
+ return {
798
+ success: false,
799
+ transitioned: false,
800
+ states: {},
801
+ emittedEvents: [],
802
+ error: `Orbital not found: ${orbitalName}`
803
+ };
804
+ }
805
+ const payloadRow = request.payload?.["row"];
806
+ const payloadRowAsPayload = payloadRow !== null && typeof payloadRow === "object" && !Array.isArray(payloadRow) ? payloadRow : void 0;
807
+ const payloadRowId = payloadRowAsPayload?.["id"];
808
+ renderLog.debug("processOrbitalEvent:enter", {
809
+ orbital: orbitalName,
810
+ event: request.event,
811
+ hasPayloadRow: payloadRowAsPayload !== void 0,
812
+ payloadRowId: typeof payloadRowId === "string" || typeof payloadRowId === "number" ? payloadRowId : void 0,
813
+ entityId: request.entityId
814
+ });
815
+ busLog.debug("bus:incoming", () => ({
816
+ orbital: orbitalName,
817
+ event: request.event,
818
+ payload: JSON.stringify(request.payload ?? null),
819
+ entityId: request.entityId,
820
+ traitStates: JSON.stringify(
821
+ Array.from(registered.manager.getAllStates().entries()).map(([traitName, state]) => ({
822
+ traitName,
823
+ currentState: state.currentState
824
+ }))
825
+ )
826
+ }));
827
+ xOrbitalLog.info("processOrbitalEvent:enter", () => ({
828
+ orbital: orbitalName,
829
+ event: request.event,
830
+ traitsInOrbital: registered.traits.map((t) => t.name).join(","),
831
+ payloadActiveTraits: JSON.stringify(
832
+ request.payload?.["_activeTraits"] ?? null
833
+ )
834
+ }));
835
+ const { event, payload, entityId, user } = request;
836
+ const validationFailures = [];
837
+ for (const trait of registered.traits) {
838
+ const eventSchema = trait.stateMachine?.events?.find((e) => e.key === event);
839
+ if (eventSchema?.payloadSchema && eventSchema.payloadSchema.length > 0) {
840
+ validationFailures.push(
841
+ ...validateEventPayload(event, payload, eventSchema.payloadSchema)
842
+ );
843
+ }
844
+ }
845
+ if (validationFailures.length > 0) {
846
+ return {
847
+ success: false,
848
+ transitioned: false,
849
+ states: {},
850
+ emittedEvents: [],
851
+ error: formatPayloadValidationError(validationFailures)
852
+ };
853
+ }
854
+ const emittedEvents = [];
855
+ const fetchedData = {};
856
+ const clientEffects = [];
857
+ const clientEffectsByTrait = [];
858
+ const effectResults = [];
859
+ const activeTraits = payload?._activeTraits;
860
+ const cleanPayload = payload ? { ...payload } : void 0;
861
+ if (cleanPayload) {
862
+ delete cleanPayload._activeTraits;
863
+ }
864
+ let entityData = {};
865
+ if (entityId) {
866
+ const stored = await this.persistence.getById(
867
+ registered.entity.name,
868
+ entityId
869
+ );
870
+ if (stored) {
871
+ entityData = stored;
872
+ }
873
+ }
874
+ const entityByTrait = {};
875
+ for (const [name, fields] of registered.traitFieldStates) {
876
+ if (fields && Object.keys(fields).length > 0) {
877
+ entityByTrait[name] = fields;
878
+ }
879
+ }
880
+ const results = registered.manager.sendEvent(
881
+ event,
882
+ cleanPayload,
883
+ entityData,
884
+ entityByTrait
885
+ );
886
+ const filteredResults = activeTraits && activeTraits.length > 0 ? results.filter(({ traitName }) => activeTraits.includes(traitName)) : results;
887
+ if (this.config.debug && activeTraits) {
888
+ busLog.debug("dispatch:filter-traits", () => ({
889
+ total: results.length,
890
+ active: filteredResults.length,
891
+ activeTraits: activeTraits.join(",")
892
+ }));
893
+ }
894
+ for (const { traitName, result } of filteredResults) {
895
+ if (result.effects.length > 0) {
896
+ await this.executeEffects(
897
+ registered,
898
+ traitName,
899
+ result.effects,
900
+ cleanPayload,
901
+ entityData,
902
+ entityId,
903
+ emittedEvents,
904
+ fetchedData,
905
+ clientEffects,
906
+ effectResults,
907
+ user,
908
+ clientEffectsByTrait
909
+ );
910
+ }
911
+ }
912
+ const states = {};
913
+ for (const [name, state] of registered.manager.getAllStates()) {
914
+ states[name] = state.currentState;
915
+ }
916
+ const response = {
917
+ success: true,
918
+ transitioned: results.length > 0,
919
+ states,
920
+ emittedEvents
921
+ };
922
+ if (clientEffects.length > 0) {
923
+ response.clientEffects = clientEffects;
924
+ }
925
+ if (clientEffectsByTrait.length > 0) {
926
+ response.clientEffectsByTrait = clientEffectsByTrait;
927
+ }
928
+ if (effectResults.length > 0) {
929
+ response.effectResults = effectResults;
930
+ }
931
+ return response;
932
+ }
933
+ /**
934
+ * Execute effects from a transition
935
+ */
936
+ async executeEffects(registered, traitName, effects, payload, entityData, entityId, emittedEvents, fetchedData, clientEffects, effectResults, user, clientEffectsByTrait) {
937
+ const entityType = registered.entity.name;
938
+ const pushClientEffect = (effect) => {
939
+ clientEffects.push(effect);
940
+ clientEffectsByTrait?.push({ traitName, effect });
941
+ };
942
+ let bindingsRef = null;
943
+ let contextRef = null;
944
+ const handlers = {
945
+ emit: (event, eventPayload, source) => {
946
+ if (this.config.debug) {
947
+ busLog.debug("emit:dispatch", () => ({
948
+ event,
949
+ payloadJson: JSON.stringify(eventPayload ?? null),
950
+ sourceOrbital: source?.orbital,
951
+ sourceTrait: source?.trait
952
+ }));
953
+ }
954
+ const stamp = source ?? {
955
+ orbital: registered.schema.name,
956
+ trait: traitName
957
+ };
958
+ this.eventBus.emit(event, eventPayload, stamp);
959
+ emittedEvents.push({ event, payload: eventPayload, source: stamp });
960
+ effectLog.debug("emit:push", {
961
+ event,
962
+ cumulativeEmittedCount: emittedEvents.length,
963
+ sourceTrait: stamp.trait,
964
+ sourceOrbital: stamp.orbital
965
+ });
966
+ xOrbitalLog.info("emit:server", {
967
+ event,
968
+ sourceOrbital: stamp.orbital,
969
+ sourceTrait: stamp.trait,
970
+ dispatchOrbital: registered.schema.name
971
+ });
972
+ },
973
+ set: async (targetId, field, value) => {
974
+ let fieldState = registered.traitFieldStates.get(traitName);
975
+ if (!fieldState) {
976
+ fieldState = {};
977
+ registered.traitFieldStates.set(traitName, fieldState);
978
+ }
979
+ fieldState[field] = value;
980
+ effectResults.push({
981
+ effect: "set",
982
+ entityType,
983
+ data: { id: targetId || entityId || "", field, value },
984
+ success: true
985
+ });
986
+ },
987
+ persist: async (action, targetEntityType, data) => {
988
+ if (action === "batch") {
989
+ const operations = data?.operations;
990
+ if (!Array.isArray(operations) || operations.length === 0) {
991
+ effectResults.push({
992
+ effect: "persist",
993
+ action: "batch",
994
+ success: false,
995
+ error: "Batch requires a non-empty operations array"
996
+ });
997
+ return;
998
+ }
999
+ const batchResults = [];
1000
+ const completed = [];
1001
+ let batchFailed = false;
1002
+ let batchError = "";
1003
+ for (const op of operations) {
1004
+ if (!Array.isArray(op) || op.length < 2) {
1005
+ batchFailed = true;
1006
+ batchError = `Invalid batch operation format: ${JSON.stringify(op)}`;
1007
+ break;
1008
+ }
1009
+ const [opAction, opEntityType, ...opRest] = op;
1010
+ try {
1011
+ switch (opAction) {
1012
+ case "create": {
1013
+ const createData = opRest[0] || {};
1014
+ const { id: newId } = await this.persistence.create(opEntityType, createData);
1015
+ batchResults.push({ action: "create", entityType: opEntityType, id: newId, ...createData });
1016
+ completed.push({ action: "create", entityType: opEntityType, id: newId });
1017
+ break;
1018
+ }
1019
+ case "update": {
1020
+ const updateId = opRest[0];
1021
+ const updateData = opRest[1] || {};
1022
+ await this.persistence.update(opEntityType, updateId, updateData);
1023
+ const updated = await this.persistence.getById(opEntityType, updateId);
1024
+ batchResults.push({ action: "update", entityType: opEntityType, id: updateId, ...updated || updateData });
1025
+ completed.push({ action: "update", entityType: opEntityType, id: updateId });
1026
+ break;
1027
+ }
1028
+ case "delete": {
1029
+ const deleteId = opRest[0];
1030
+ await this.persistence.delete(opEntityType, deleteId);
1031
+ batchResults.push({ action: "delete", entityType: opEntityType, id: deleteId, deleted: true });
1032
+ completed.push({ action: "delete", entityType: opEntityType, id: deleteId });
1033
+ break;
1034
+ }
1035
+ default:
1036
+ batchFailed = true;
1037
+ batchError = `Unknown batch operation action: ${opAction}`;
1038
+ break;
1039
+ }
1040
+ } catch (err) {
1041
+ batchFailed = true;
1042
+ batchError = `Batch operation [${opAction}, ${opEntityType}] failed: ${err instanceof Error ? err.message : String(err)}`;
1043
+ break;
1044
+ }
1045
+ if (batchFailed) break;
1046
+ }
1047
+ effectResults.push({
1048
+ effect: "persist",
1049
+ action: "batch",
1050
+ data: {
1051
+ operations: batchResults,
1052
+ completedCount: completed.length,
1053
+ totalCount: operations.length
1054
+ },
1055
+ success: !batchFailed,
1056
+ ...batchFailed ? { error: batchError } : {}
1057
+ });
1058
+ return;
1059
+ }
1060
+ const type = targetEntityType || entityType;
1061
+ let resultData;
1062
+ const sizeBefore = (await this.persistence.list(type)).length;
1063
+ try {
1064
+ if (action === "create" || action === "update") {
1065
+ this.validateRelationCardinality(type, data || {});
1066
+ }
1067
+ switch (action) {
1068
+ case "create": {
1069
+ const { id } = await this.persistence.create(type, data || {});
1070
+ resultData = { id, ...data || {} };
1071
+ break;
1072
+ }
1073
+ case "update":
1074
+ if (data?.id || entityId) {
1075
+ const updateId = data?.id || entityId;
1076
+ await this.persistence.update(type, updateId, data || {});
1077
+ const updated = await this.persistence.getById(type, updateId);
1078
+ resultData = updated || { id: updateId, ...data || {} };
1079
+ }
1080
+ break;
1081
+ case "delete": {
1082
+ const directId = typeof data === "string" ? data : void 0;
1083
+ const nestedId = typeof data === "object" && data !== null ? data.id : void 0;
1084
+ const deleteId = directId ?? nestedId ?? entityId;
1085
+ if (deleteId) {
1086
+ await this.enforceOnDeleteRules(type, deleteId);
1087
+ await this.persistence.delete(type, deleteId);
1088
+ resultData = { id: deleteId, deleted: true };
1089
+ }
1090
+ break;
1091
+ }
1092
+ }
1093
+ const sizeAfter = (await this.persistence.list(type)).length;
1094
+ effectLog.debug("persist:store-mutate", {
1095
+ action,
1096
+ entityType: type,
1097
+ resultId: resultData?.id,
1098
+ sizeBefore,
1099
+ sizeAfter,
1100
+ delta: sizeAfter - sizeBefore
1101
+ });
1102
+ effectResults.push({
1103
+ effect: "persist",
1104
+ action,
1105
+ entityType: type,
1106
+ data: resultData,
1107
+ success: true
1108
+ });
1109
+ } catch (err) {
1110
+ effectLog.error("persist:store-mutate-error", {
1111
+ action,
1112
+ entityType: type,
1113
+ error: err instanceof Error ? err.message : String(err)
1114
+ });
1115
+ effectResults.push({
1116
+ effect: "persist",
1117
+ action,
1118
+ entityType: type,
1119
+ success: false,
1120
+ error: err instanceof Error ? err.message : String(err)
1121
+ });
1122
+ }
1123
+ },
1124
+ callService: async (service, action, params) => {
1125
+ try {
1126
+ let result = null;
1127
+ if (this.config.effectHandlers?.callService) {
1128
+ result = await this.config.effectHandlers.callService(
1129
+ service,
1130
+ action,
1131
+ params
1132
+ );
1133
+ } else if (this.config.mode === "mock") {
1134
+ const mockId = `mock_${service}_${action}_${Math.random().toString(36).slice(2, 10)}`;
1135
+ const paramsEcho = {};
1136
+ if (params) {
1137
+ for (const [k, v] of Object.entries(params)) {
1138
+ if (v !== void 0 && (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null || v instanceof Date)) {
1139
+ paramsEcho[k] = v;
1140
+ }
1141
+ }
1142
+ }
1143
+ result = {
1144
+ id: mockId,
1145
+ clientSecret: `secret_${mockId}`,
1146
+ success: true,
1147
+ status: "succeeded",
1148
+ ...paramsEcho
1149
+ };
1150
+ } else {
1151
+ effectLog.warn("call-service:not-configured", { service, action });
1152
+ }
1153
+ effectResults.push({
1154
+ effect: "call-service",
1155
+ action: `${service}.${action}`,
1156
+ data: result,
1157
+ success: true
1158
+ });
1159
+ return result;
1160
+ } catch (err) {
1161
+ effectResults.push({
1162
+ effect: "call-service",
1163
+ action: `${service}.${action}`,
1164
+ success: false,
1165
+ error: err instanceof Error ? err.message : String(err)
1166
+ });
1167
+ return null;
1168
+ }
1169
+ },
1170
+ fetch: async (fetchEntityType, options) => {
1171
+ try {
1172
+ xOrbitalLog.info("fetch:enter", () => ({
1173
+ entityType: fetchEntityType,
1174
+ hasOptions: options !== void 0 && options !== null,
1175
+ optionsKeys: options ? Object.keys(options).join(",") : "",
1176
+ filterType: typeof options?.filter,
1177
+ filterIsArray: Array.isArray(options?.filter),
1178
+ filterJson: JSON.stringify(options?.filter ?? null).slice(0, 300),
1179
+ payloadJson: JSON.stringify(bindingsRef?.payload ?? null).slice(0, 300)
1180
+ }));
1181
+ let result = null;
1182
+ let total = 0;
1183
+ if (options?.id) {
1184
+ const entity = await this.persistence.getById(fetchEntityType, options.id);
1185
+ if (entity) {
1186
+ if (options?.include && options.include.length > 0) {
1187
+ await this.populateRelations([entity], fetchEntityType, options.include);
1188
+ }
1189
+ fetchedData[fetchEntityType] = [entity];
1190
+ result = entity;
1191
+ total = 1;
1192
+ }
1193
+ } else {
1194
+ let entities = await this.persistence.list(fetchEntityType);
1195
+ if (options?.filter !== void 0 && options.filter !== null) {
1196
+ const predicate = options.filter;
1197
+ entities = entities.filter((entity) => {
1198
+ const ctx = createContextFromBindings(
1199
+ { entity, payload: bindingsRef?.payload, current: entity },
1200
+ false
1201
+ );
1202
+ try {
1203
+ return Boolean(evaluate(predicate, ctx));
1204
+ } catch (err) {
1205
+ effectLog.error("fetch:filter-eval-error", {
1206
+ entityType: fetchEntityType,
1207
+ error: err instanceof Error ? err : String(err)
1208
+ });
1209
+ return false;
1210
+ }
1211
+ });
1212
+ }
1213
+ total = entities.length;
1214
+ if (options?.offset && options.offset > 0) {
1215
+ entities = entities.slice(options.offset);
1216
+ }
1217
+ if (options?.limit && options.limit > 0) {
1218
+ entities = entities.slice(0, options.limit);
1219
+ }
1220
+ if (options?.include && options.include.length > 0) {
1221
+ await this.populateRelations(entities, fetchEntityType, options.include);
1222
+ }
1223
+ fetchedData[fetchEntityType] = entities;
1224
+ result = entities;
1225
+ }
1226
+ return result === null ? null : { rows: result, total };
1227
+ } catch (error) {
1228
+ effectLog.error("fetch:error", {
1229
+ entityType: fetchEntityType,
1230
+ error: error instanceof Error ? error : String(error)
1231
+ });
1232
+ return null;
1233
+ }
1234
+ },
1235
+ // Resource operators: ref, deref, swap, watch, atomic
1236
+ ref: async (refEntityType, options) => {
1237
+ try {
1238
+ return await handlers.fetch(refEntityType, options);
1239
+ } catch (error) {
1240
+ effectLog.error("ref:error", {
1241
+ entityType: refEntityType,
1242
+ error: error instanceof Error ? error : String(error)
1243
+ });
1244
+ return null;
1245
+ }
1246
+ },
1247
+ deref: async (derefEntityType, options) => {
1248
+ try {
1249
+ let result = null;
1250
+ let total = 0;
1251
+ if (options?.id) {
1252
+ const entity = await this.persistence.getById(derefEntityType, options.id);
1253
+ if (entity) {
1254
+ fetchedData[derefEntityType] = [entity];
1255
+ result = entity;
1256
+ total = 1;
1257
+ }
1258
+ } else {
1259
+ const entities = await this.persistence.list(derefEntityType);
1260
+ fetchedData[derefEntityType] = entities;
1261
+ result = entities;
1262
+ total = entities.length;
1263
+ }
1264
+ effectResults.push({
1265
+ effect: "deref",
1266
+ entityType: derefEntityType,
1267
+ success: true
1268
+ });
1269
+ return result === null ? null : { rows: result, total };
1270
+ } catch (error) {
1271
+ effectResults.push({
1272
+ effect: "deref",
1273
+ entityType: derefEntityType,
1274
+ success: false,
1275
+ error: error instanceof Error ? error.message : String(error)
1276
+ });
1277
+ return null;
1278
+ }
1279
+ },
1280
+ swap: async (swapEntityType, swapEntityId, transform) => {
1281
+ try {
1282
+ const current = await this.persistence.getById(swapEntityType, swapEntityId);
1283
+ if (!current) {
1284
+ effectResults.push({
1285
+ effect: "swap",
1286
+ entityType: swapEntityType,
1287
+ success: false,
1288
+ error: `Entity ${swapEntityType}/${swapEntityId} not found`
1289
+ });
1290
+ return null;
1291
+ }
1292
+ const ctx = createContextFromBindings({
1293
+ current,
1294
+ entity: entityData,
1295
+ payload
1296
+ }, false, this.config.contextExtensions);
1297
+ let newData;
1298
+ if (Array.isArray(transform)) {
1299
+ const result = evaluate(
1300
+ transform,
1301
+ ctx
1302
+ );
1303
+ if (result && typeof result === "object" && !Array.isArray(result)) {
1304
+ newData = result;
1305
+ } else {
1306
+ newData = current;
1307
+ }
1308
+ } else if (typeof transform === "object" && transform !== null) {
1309
+ newData = { ...current, ...transform };
1310
+ } else {
1311
+ effectResults.push({
1312
+ effect: "swap",
1313
+ entityType: swapEntityType,
1314
+ success: false,
1315
+ error: "swap! transform must be an S-expression or object"
1316
+ });
1317
+ return null;
1318
+ }
1319
+ await this.persistence.update(swapEntityType, swapEntityId, newData);
1320
+ effectResults.push({
1321
+ effect: "swap",
1322
+ entityType: swapEntityType,
1323
+ data: { id: swapEntityId, ...newData },
1324
+ success: true
1325
+ });
1326
+ return newData;
1327
+ } catch (error) {
1328
+ effectResults.push({
1329
+ effect: "swap",
1330
+ entityType: swapEntityType,
1331
+ success: false,
1332
+ error: error instanceof Error ? error.message : String(error)
1333
+ });
1334
+ return null;
1335
+ }
1336
+ },
1337
+ watch: (_watchEntityType, _watchOptions) => {
1338
+ if (this.config.debug) {
1339
+ effectLog.debug("watch:noop-server", { entityType: _watchEntityType });
1340
+ }
1341
+ },
1342
+ atomic: async (atomicEffects) => {
1343
+ let atomicFailed = false;
1344
+ let atomicError = "";
1345
+ const atomicExecutor = new EffectExecutor({
1346
+ handlers,
1347
+ bindings: bindingsRef ?? {},
1348
+ context: contextRef ?? { traitName, orbitalName: registered.schema.name, state: "unknown", transition: "unknown" },
1349
+ debug: this.config.debug,
1350
+ contextExtensions: this.config.contextExtensions
1351
+ });
1352
+ for (const innerEffect of atomicEffects) {
1353
+ if (atomicFailed) break;
1354
+ try {
1355
+ await atomicExecutor.execute(innerEffect);
1356
+ } catch (err) {
1357
+ atomicFailed = true;
1358
+ atomicError = err instanceof Error ? err.message : String(err);
1359
+ }
1360
+ }
1361
+ if (atomicFailed) {
1362
+ effectResults.push({
1363
+ effect: "atomic",
1364
+ success: false,
1365
+ error: `Atomic block failed: ${atomicError}`
1366
+ });
1367
+ } else {
1368
+ effectResults.push({
1369
+ effect: "atomic",
1370
+ success: true,
1371
+ data: { innerCount: atomicEffects.length }
1372
+ });
1373
+ }
1374
+ },
1375
+ // Client-side effects - collect for forwarding to client
1376
+ renderUI: (slot, pattern, props, priority) => {
1377
+ const patternNode = pattern !== null && typeof pattern === "object" && !Array.isArray(pattern) ? pattern : null;
1378
+ const patternEntity = patternNode?.entity;
1379
+ const entityRow = patternEntity !== null && typeof patternEntity === "object" && !Array.isArray(patternEntity) ? patternEntity : null;
1380
+ const patternTypeRaw = patternNode?.["type"];
1381
+ renderLog.debug("renderUI:push", {
1382
+ trait: traitName,
1383
+ slot,
1384
+ patternType: typeof patternTypeRaw === "string" ? patternTypeRaw : void 0,
1385
+ entityRowId: typeof entityRow?.id === "string" ? entityRow.id : void 0,
1386
+ entityIsObject: entityRow !== null
1387
+ });
1388
+ pushClientEffect(["render-ui", slot, pattern, props, priority]);
1389
+ },
1390
+ navigate: (path, params) => {
1391
+ pushClientEffect(["navigate", path, params]);
1392
+ },
1393
+ notify: (message, type) => {
1394
+ if (this.config.debug) {
1395
+ effectLog.info("notify", { type, message });
1396
+ }
1397
+ pushClientEffect(["notify", message, { type }]);
1398
+ },
1399
+ log: (message, level) => {
1400
+ if (level === "error") {
1401
+ dynamicLog.error(message);
1402
+ } else if (level === "warn") {
1403
+ dynamicLog.warn(message);
1404
+ } else {
1405
+ dynamicLog.debug(message);
1406
+ }
1407
+ },
1408
+ // Allow custom handlers to override
1409
+ ...this.config.effectHandlers
1410
+ };
1411
+ const state = registered.manager.getState(traitName);
1412
+ const bindings = {
1413
+ entity: entityData,
1414
+ payload,
1415
+ state: state?.currentState || "unknown",
1416
+ user
1417
+ // @user bindings from Firebase auth
1418
+ };
1419
+ const traitDef = registered.traits.find((t) => t.name === traitName);
1420
+ const declaredDefaults = collectDeclaredConfigDefaults(traitDef);
1421
+ const callSiteOverride = registered.configByTrait.get(traitName);
1422
+ if (declaredDefaults || callSiteOverride) {
1423
+ bindings.config = { ...declaredDefaults ?? {}, ...callSiteOverride ?? {} };
1424
+ }
1425
+ const traitFieldState = registered.traitFieldStates.get(traitName);
1426
+ if (traitFieldState) {
1427
+ bindings.entity = traitFieldState;
1428
+ }
1429
+ if (entityType) {
1430
+ bindings[entityType] = bindings.entity ?? entityData;
1431
+ }
1432
+ bindingsRef = bindings;
1433
+ const context = {
1434
+ traitName,
1435
+ orbitalName: registered.schema.name,
1436
+ state: state?.currentState || "unknown",
1437
+ transition: "unknown",
1438
+ entityId
1439
+ };
1440
+ contextRef = context;
1441
+ const executor = new EffectExecutor({
1442
+ handlers,
1443
+ bindings,
1444
+ context,
1445
+ debug: this.config.debug,
1446
+ contextExtensions: this.config.contextExtensions
1447
+ });
1448
+ await executor.executeAll(effects);
1449
+ }
1450
+ // ==========================================================================
1451
+ // Relation Population
1452
+ // ==========================================================================
1453
+ /**
1454
+ * Populate relation fields on entities
1455
+ *
1456
+ * For each field in `include`, find the relation field configuration and
1457
+ * fetch the related entity, attaching it to the parent entity.
1458
+ *
1459
+ * @param entities - Entities to populate
1460
+ * @param entityType - Entity type name
1461
+ * @param include - Relation field names to populate
1462
+ */
1463
+ /**
1464
+ * Validate that relation field values match their declared cardinality.
1465
+ * Called before create/update to ensure data integrity.
1466
+ */
1467
+ validateRelationCardinality(entityType, data) {
1468
+ for (const [, registered] of this.orbitals) {
1469
+ if (registered.entity.name !== entityType) continue;
1470
+ for (const field of registered.entity.fields ?? []) {
1471
+ if (field.type !== "relation") continue;
1472
+ if (field.name === void 0) continue;
1473
+ const fieldName = field.name;
1474
+ const value = data[fieldName];
1475
+ if (value === void 0 || value === null) continue;
1476
+ const cardinality = field.relation?.cardinality || "one";
1477
+ if (cardinality === "one" || cardinality === "many-to-one") {
1478
+ if (Array.isArray(value)) {
1479
+ throw new Error(
1480
+ `Cardinality violation: ${entityType}.${fieldName} has cardinality '${cardinality}' but received an array. Expected a single string ID.`
1481
+ );
1482
+ }
1483
+ } else if (cardinality === "many" || cardinality === "many-to-many" || cardinality === "one-to-many") {
1484
+ if (typeof value === "string") {
1485
+ data[fieldName] = [value];
1486
+ } else if (Array.isArray(value)) {
1487
+ const nonStrings = value.filter((v) => typeof v !== "string");
1488
+ if (nonStrings.length > 0) {
1489
+ throw new Error(
1490
+ `Cardinality violation: ${entityType}.${fieldName} has cardinality '${cardinality}' but array contains non-string values.`
1491
+ );
1492
+ }
1493
+ }
1494
+ }
1495
+ }
1496
+ break;
1497
+ }
1498
+ }
1499
+ /**
1500
+ * Enforce onDelete rules for relation fields pointing to the entity being deleted.
1501
+ * Scans all registered entities for relation fields targeting the given entity type,
1502
+ * finds records referencing the ID being deleted, and applies cascade/nullify/restrict.
1503
+ */
1504
+ async enforceOnDeleteRules(entityType, deletedId) {
1505
+ for (const [, registered] of this.orbitals) {
1506
+ const entity = registered.entity;
1507
+ const fields = entity.fields ?? [];
1508
+ for (const field of fields) {
1509
+ if (field.type !== "relation") continue;
1510
+ if (field.relation?.entity !== entityType) continue;
1511
+ if (field.name === void 0) continue;
1512
+ const fieldName = field.name;
1513
+ const onDelete = field.relation.onDelete || "restrict";
1514
+ const referringEntityType = entity.name;
1515
+ const allRecords = await this.persistence.list(referringEntityType);
1516
+ const affectedRecords = allRecords.filter((record) => {
1517
+ const fkValue = record[fieldName];
1518
+ if (typeof fkValue === "string") return fkValue === deletedId;
1519
+ if (Array.isArray(fkValue)) return fkValue.includes(deletedId);
1520
+ return false;
1521
+ });
1522
+ if (affectedRecords.length === 0) continue;
1523
+ switch (onDelete) {
1524
+ case "restrict":
1525
+ throw new Error(
1526
+ `Cannot delete ${entityType} ${deletedId}: ${affectedRecords.length} ${referringEntityType} record(s) reference it via ${field.name}. Rule: restrict.`
1527
+ );
1528
+ case "cascade":
1529
+ for (const record of affectedRecords) {
1530
+ const recordId = record.id;
1531
+ if (recordId) {
1532
+ await this.persistence.delete(referringEntityType, recordId);
1533
+ }
1534
+ }
1535
+ if (this.config.debug) {
1536
+ persistLog.debug("cascade-delete", {
1537
+ count: affectedRecords.length,
1538
+ entityType: referringEntityType
1539
+ });
1540
+ }
1541
+ break;
1542
+ case "nullify":
1543
+ for (const record of affectedRecords) {
1544
+ const recordId = record.id;
1545
+ if (recordId && field.name !== void 0) {
1546
+ const fieldName2 = field.name;
1547
+ const update = {};
1548
+ const fkValue = record[fieldName2];
1549
+ if (Array.isArray(fkValue)) {
1550
+ update[fieldName2] = fkValue.filter((id) => id !== deletedId);
1551
+ } else {
1552
+ update[fieldName2] = null;
1553
+ }
1554
+ await this.persistence.update(referringEntityType, recordId, update);
1555
+ }
1556
+ }
1557
+ if (this.config.debug) {
1558
+ persistLog.debug("nullify", {
1559
+ field: field.name,
1560
+ count: affectedRecords.length,
1561
+ entityType: referringEntityType
1562
+ });
1563
+ }
1564
+ break;
1565
+ }
1566
+ }
1567
+ }
1568
+ }
1569
+ async populateRelations(entities, entityType, include, depth = 0, visited = /* @__PURE__ */ new Set()) {
1570
+ const maxDepth = 2;
1571
+ if (depth >= maxDepth || visited.has(entityType)) {
1572
+ if (this.config.debug) {
1573
+ persistLog.debug("populate:skip", {
1574
+ entityType,
1575
+ depth,
1576
+ visited: visited.has(entityType)
1577
+ });
1578
+ }
1579
+ return;
1580
+ }
1581
+ visited.add(entityType);
1582
+ let entityFields;
1583
+ for (const [, registered] of this.orbitals) {
1584
+ if (registered.entity.name === entityType) {
1585
+ entityFields = registered.entity.fields.filter(
1586
+ (f) => typeof f.name === "string" && f.name.length > 0
1587
+ );
1588
+ break;
1589
+ }
1590
+ }
1591
+ if (!entityFields) {
1592
+ if (this.config.debug) {
1593
+ persistLog.warn("populate:no-entity-def", { entityType });
1594
+ }
1595
+ return;
1596
+ }
1597
+ for (const includeField of include) {
1598
+ const relationField = entityFields.find((f) => {
1599
+ if (f.type !== "relation") return false;
1600
+ return f.name === includeField || f.name === `${includeField}Id` || f.name.replace(/Id$/, "") === includeField;
1601
+ });
1602
+ if (!relationField?.relation?.entity) {
1603
+ if (this.config.debug) {
1604
+ persistLog.warn("populate:no-relation-field", { includeField, entityType });
1605
+ }
1606
+ continue;
1607
+ }
1608
+ const foreignKeyField = relationField.name;
1609
+ const relatedEntityType = relationField.relation.entity;
1610
+ const cardinality = relationField.relation.cardinality || "one";
1611
+ const foreignKeyIds = /* @__PURE__ */ new Set();
1612
+ for (const entity of entities) {
1613
+ const fkValue = entity[foreignKeyField];
1614
+ if (fkValue && typeof fkValue === "string") {
1615
+ foreignKeyIds.add(fkValue);
1616
+ } else if (Array.isArray(fkValue)) {
1617
+ for (const id of fkValue) {
1618
+ if (id && typeof id === "string") {
1619
+ foreignKeyIds.add(id);
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+ if (foreignKeyIds.size === 0) continue;
1625
+ const relatedEntities = /* @__PURE__ */ new Map();
1626
+ for (const fkId of foreignKeyIds) {
1627
+ try {
1628
+ const related = await this.persistence.getById(relatedEntityType, fkId);
1629
+ if (related) {
1630
+ relatedEntities.set(fkId, related);
1631
+ }
1632
+ } catch (error) {
1633
+ if (this.config.debug) {
1634
+ persistLog.error("populate:fetch-related-error", {
1635
+ entityType: relatedEntityType,
1636
+ error: error instanceof Error ? error : String(error)
1637
+ });
1638
+ }
1639
+ }
1640
+ }
1641
+ const populatedFieldName = includeField.endsWith("Id") ? includeField.slice(0, -2) : includeField;
1642
+ const isSelfRef = relatedEntityType === entityType;
1643
+ const hydrateClone = (id) => {
1644
+ const related = relatedEntities.get(id);
1645
+ if (!related) return void 0;
1646
+ const copy = { ...related };
1647
+ if (isSelfRef) copy[foreignKeyField] = [];
1648
+ return copy;
1649
+ };
1650
+ for (const entity of entities) {
1651
+ const fkValue = entity[foreignKeyField];
1652
+ if (cardinality === "one" || cardinality === "many-to-one") {
1653
+ if (typeof fkValue === "string" && relatedEntities.has(fkValue)) {
1654
+ Object.defineProperty(entity, populatedFieldName, {
1655
+ value: hydrateClone(fkValue),
1656
+ writable: true,
1657
+ enumerable: true,
1658
+ configurable: true
1659
+ });
1660
+ }
1661
+ } else {
1662
+ if (Array.isArray(fkValue)) {
1663
+ const fkIds = fkValue.filter((id) => typeof id === "string");
1664
+ Object.defineProperty(entity, populatedFieldName, {
1665
+ value: fkIds.map(hydrateClone).filter(Boolean),
1666
+ writable: true,
1667
+ enumerable: true,
1668
+ configurable: true
1669
+ });
1670
+ } else if (typeof fkValue === "string" && relatedEntities.has(fkValue)) {
1671
+ Object.defineProperty(entity, populatedFieldName, {
1672
+ value: [hydrateClone(fkValue)],
1673
+ writable: true,
1674
+ enumerable: true,
1675
+ configurable: true
1676
+ });
1677
+ }
1678
+ }
1679
+ }
1680
+ if (this.config.debug) {
1681
+ persistLog.debug("populate:done", {
1682
+ field: populatedFieldName,
1683
+ count: entities.length,
1684
+ entityType
1685
+ });
1686
+ }
1687
+ }
1688
+ }
1689
+ // ==========================================================================
1690
+ // Express Router
1691
+ // ==========================================================================
1692
+ /**
1693
+ * Create Express router for orbital API endpoints
1694
+ *
1695
+ * All data access goes through trait events with guards.
1696
+ * No direct CRUD routes - use events with `fetch` effects.
1697
+ *
1698
+ * Routes:
1699
+ * - GET / - List registered orbitals
1700
+ * - GET /:orbital - Get orbital info and current states
1701
+ * - POST /:orbital/events - Send event to orbital (includes data from `fetch` effects)
1702
+ */
1703
+ router() {
1704
+ if (!isNodeEnv()) {
1705
+ throw new Error(
1706
+ "OrbitalServerRuntime.router() is Node-only (uses Express). For in-browser use, mount <BrowserPlayground> from @almadar/ui instead."
1707
+ );
1708
+ }
1709
+ const { Router } = nodeRequire("express");
1710
+ const router = Router();
1711
+ router.get("/", (_req, res) => {
1712
+ const orbitals = Array.from(this.orbitals.entries()).map(
1713
+ ([name, reg]) => ({
1714
+ name,
1715
+ entity: reg.entity?.name,
1716
+ traits: (reg.traits || []).map((t) => t.name)
1717
+ })
1718
+ );
1719
+ res.json({ success: true, orbitals });
1720
+ });
1721
+ router.get("/:orbital", (req, res) => {
1722
+ const orbitalName = req.params.orbital;
1723
+ const registered = this.orbitals.get(orbitalName);
1724
+ if (!registered) {
1725
+ res.status(404).json({ success: false, error: "Orbital not found" });
1726
+ return;
1727
+ }
1728
+ const states = {};
1729
+ for (const [name, state] of registered.manager.getAllStates()) {
1730
+ states[name] = state.currentState;
1731
+ }
1732
+ res.json({
1733
+ success: true,
1734
+ orbital: {
1735
+ name: orbitalName,
1736
+ entity: registered.entity,
1737
+ traits: registered.traits.map((t) => ({
1738
+ name: t.name,
1739
+ currentState: states[t.name],
1740
+ states: (t.stateMachine?.states || []).map((s) => s.name),
1741
+ events: [...new Set((t.stateMachine?.transitions || []).map((tr) => tr.event))]
1742
+ }))
1743
+ }
1744
+ });
1745
+ });
1746
+ router.post(
1747
+ "/:orbital/events",
1748
+ async (req, res, next) => {
1749
+ try {
1750
+ const orbitalName = req.params.orbital;
1751
+ const firebaseUser = req.firebaseUser;
1752
+ const user = firebaseUser ? {
1753
+ ...firebaseUser,
1754
+ displayName: firebaseUser.name ?? firebaseUser.displayName
1755
+ } : void 0;
1756
+ const result = await this.processOrbitalEvent(orbitalName, {
1757
+ ...req.body,
1758
+ user
1759
+ });
1760
+ res.json(result);
1761
+ } catch (error) {
1762
+ next(error);
1763
+ }
1764
+ }
1765
+ );
1766
+ return router;
1767
+ }
1768
+ // ==========================================================================
1769
+ // Direct API (for programmatic use)
1770
+ // ==========================================================================
1771
+ /**
1772
+ * Get the event bus for manual event emission
1773
+ */
1774
+ getEventBus() {
1775
+ return this.eventBus;
1776
+ }
1777
+ /**
1778
+ * Get state for a specific orbital/trait
1779
+ */
1780
+ getState(orbitalName, traitName) {
1781
+ const registered = this.orbitals.get(orbitalName);
1782
+ if (!registered) return void 0;
1783
+ if (traitName) {
1784
+ return registered.manager.getState(traitName);
1785
+ }
1786
+ const states = {};
1787
+ for (const [name, state] of registered.manager.getAllStates()) {
1788
+ states[name] = state;
1789
+ }
1790
+ return states;
1791
+ }
1792
+ /**
1793
+ * List registered orbitals
1794
+ */
1795
+ listOrbitals() {
1796
+ return Array.from(this.orbitals.keys());
1797
+ }
1798
+ /**
1799
+ * Check if an orbital is registered
1800
+ */
1801
+ hasOrbital(name) {
1802
+ return this.orbitals.has(name);
1803
+ }
1804
+ /**
1805
+ * Get information about active ticks
1806
+ */
1807
+ getActiveTicks() {
1808
+ return this.tickBindings.map((binding) => ({
1809
+ orbital: binding.orbitalName,
1810
+ trait: binding.traitName,
1811
+ tick: binding.tick.name,
1812
+ interval: binding.tick.interval,
1813
+ hasGuard: !!binding.tick.guard
1814
+ }));
1815
+ }
1816
+ };
1817
+ function createOrbitalServerRuntime(config) {
1818
+ return new OrbitalServerRuntime(config);
1819
+ }
1820
+ function parseListenSource(listener, listenerOrbital) {
1821
+ const explicit = listener.source;
1822
+ if (explicit && typeof explicit === "object") {
1823
+ return {
1824
+ bareEvent: listener.event,
1825
+ matcher: buildMatcher(explicit, listenerOrbital)
1826
+ };
1827
+ }
1828
+ const key = listener.event;
1829
+ const parts = key.split(".");
1830
+ if (parts.length === 1) {
1831
+ return { bareEvent: key, matcher: () => true };
1832
+ }
1833
+ if (parts.length === 2) {
1834
+ const [sourceOrStar, eventName] = parts;
1835
+ if (sourceOrStar === "*") {
1836
+ return { bareEvent: eventName, matcher: () => true };
1837
+ }
1838
+ return {
1839
+ bareEvent: eventName,
1840
+ matcher: buildMatcher(
1841
+ { kind: "trait", trait: sourceOrStar },
1842
+ listenerOrbital
1843
+ )
1844
+ };
1845
+ }
1846
+ if (parts.length >= 3) {
1847
+ const eventName = parts[parts.length - 1];
1848
+ const trait = parts[parts.length - 2];
1849
+ const orbital = parts.slice(0, parts.length - 2).join(".");
1850
+ return {
1851
+ bareEvent: eventName,
1852
+ matcher: buildMatcher({ kind: "orbital", orbital, trait }, listenerOrbital)
1853
+ };
1854
+ }
1855
+ return { bareEvent: key, matcher: () => true };
1856
+ }
1857
+ function buildMatcher(src, listenerOrbital) {
1858
+ if (src.kind === "any") return () => true;
1859
+ if (src.kind === "trait") {
1860
+ const wantedTrait2 = src.trait;
1861
+ return (source) => !!source && source.orbital === listenerOrbital && source.trait === wantedTrait2;
1862
+ }
1863
+ const wantedOrbital = src.orbital;
1864
+ const wantedTrait = src.trait;
1865
+ return (source) => !!source && source.orbital === wantedOrbital && source.trait === wantedTrait;
1866
+ }
1867
+
1868
+ export { OrbitalServerRuntime, createOrbitalServerRuntime };
3
1869
  //# sourceMappingURL=OrbitalServerRuntime.js.map
4
1870
  //# sourceMappingURL=OrbitalServerRuntime.js.map