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