@almadar/shell-node 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1326 +0,0 @@
1
- import { installNodeHostPorts } from './chunk-23YAOI6P.js';
2
- import { nativeMemoryOf } from './chunk-UBZT5LDW.js';
3
- import { createTickScheduler, EventBus, parseOrbitalTraits, bindDeclaredCollections, createHttpPorts, defaultCallService, kernelOsCompletions, stampEmitSource, eventRouteKey, kernelOsGrants, KernelEngine, StdMockSeed, declaredSeedConfig, seedMockStore, seedBrowserStore, isValidCronExpression, parseDurationString, restampMockOwner, dispatchDeclaredInput, dispatchInbound, createHttpSecrets, evaluateOnKernel, InFlightCalls } from '@almadar/shell-runtime';
4
- import { ANONYMOUS_USER, installKernelEvaluator, isEntityCall, inlineTraitRefs, tickEventKey, entityOwnerOrbital, personaFromIdentityRow, externalInputsOf, applyRowAccess, readableEntitiesOf, inboundOnlyTrait, inboundOnlyMessage, UNMOUNT_EVENT, LIFECYCLE_EVENTS, normalizeUserContext } from '@almadar/core';
5
- import { createLogger } from '@almadar/logger';
6
- import { identityEntityName, entityAccessPolicies } from '@almadar/core/mock';
7
-
8
- var effectLog = createLogger("almadar:runtime:effects");
9
- var busLog = createLogger("almadar:runtime:bus");
10
- var renderLog = createLogger("almadar:runtime:render-ui");
11
- var xOrbitalLog = createLogger("almadar:runtime:cross-orbital");
12
- createLogger("almadar:runtime:persist");
13
- var STD_MOCK_SEED = "std/behaviors/std-mock-seed";
14
- var registerLog = createLogger("almadar:runtime:register");
15
- function isNodeEnv() {
16
- return typeof process !== "undefined" && Boolean(process.versions?.node);
17
- }
18
- var UnresolvedProgramError = class extends Error {
19
- constructor(schemaName) {
20
- super(`program "${schemaName}" has unresolved imports and this host cannot run orb: register its resolved form (orb resolve)`);
21
- this.name = "UnresolvedProgramError";
22
- }
23
- };
24
- function needsResolving(schema) {
25
- for (const orbital of schema.orbitals) {
26
- for (const t of orbital.traits ?? []) {
27
- if (typeof t === "object" && "ref" in t && t.ref.includes(".") && t._resolved === void 0) {
28
- return true;
29
- }
30
- }
31
- }
32
- return false;
33
- }
34
- async function readSidecarCatalogs(readFile, orbPath, locales) {
35
- const messages = {};
36
- const stem = orbPath.replace(/\.orb$/, "");
37
- for (const locale of locales) {
38
- const catalogPath = `${stem}.${locale}.json`;
39
- let catalogRaw;
40
- try {
41
- catalogRaw = await readFile(catalogPath, "utf-8");
42
- } catch (err) {
43
- const msg = err instanceof Error ? err.message : String(err);
44
- throw new Error(`registerFromFile: ${orbPath} declares locale "${locale}" but its catalog ${catalogPath} could not be read: ${msg}`);
45
- }
46
- messages[locale] = JSON.parse(catalogRaw);
47
- }
48
- return messages;
49
- }
50
- var OrbitalServerRuntime = class {
51
- orbitals = /* @__PURE__ */ new Map();
52
- eventBus;
53
- config;
54
- /** The row store a host-supplied `persistence` replaces: the kernel's own `MemoryStore`, opened by the first `register()`. */
55
- ownedStore;
56
- /** The owner cells the mock seed stamped, kept for a persona restamp. */
57
- mockJournal;
58
- /** The std seed program resolved from the installed packages, once per runtime. */
59
- seedProgram;
60
- /** Store writes (seed, reseed, restamp) run one after another. */
61
- storeOps = Promise.resolve();
62
- /**
63
- * The bound persistence adapter (host-supplied, else the kernel's own memory store). Public so a test can
64
- * inspect committed rows honestly. A runtime that owns its store has one from the first `register()`.
65
- */
66
- get persistence() {
67
- const store = this.config.persistence ?? this.ownedStore;
68
- if (store === void 0) throw new Error("OrbitalServerRuntime: the runtime opens its store when a program is registered; none is registered yet");
69
- return store;
70
- }
71
- /** Whether this runtime seeds generated mock rows: mock mode over the store it owns. */
72
- seedsMock() {
73
- return this.config.mode === "mock" && this.config.persistence === void 0;
74
- }
75
- tickBindings = [];
76
- /** The kernel running the registered program; null before a program is registered. */
77
- kernelEngine = null;
78
- /** The `os` handles of the program the kernel runs; closed when that program is replaced, reset or dropped. */
79
- kernelOs;
80
- /** `orbital::trait` → the clients that have it mounted (Runtime Spec Clause 8.3). */
81
- mountedBy = /* @__PURE__ */ new Map();
82
- // One coalesced clock for every tick this runtime registers — replaces
83
- // "one setInterval per tick" with a single accumulator loop so ticks due
84
- // in the same pass fire together instead of on independent timers.
85
- tickScheduler = createTickScheduler();
86
- transitionObservers = /* @__PURE__ */ new Set();
87
- transitionFanout = {
88
- onTransition: (trace) => {
89
- for (const o of this.transitionObservers) o.onTransition(trace);
90
- },
91
- onRollback: (trace) => {
92
- for (const o of this.transitionObservers) o.onRollback?.(trace);
93
- }
94
- };
95
- programHandlersPromise = null;
96
- resolvedSchema = null;
97
- /** The program's declared `locales`, in declared order; the first is the default. */
98
- appLocales = [];
99
- /** Merged message catalogs of every registered program (keys are behavior-qualified). */
100
- messages = {};
101
- /** Wired by the hosting server (e.g. the playground SSE endpoint) via `setLiveBroadcastSink`. */
102
- liveSubscribers = /* @__PURE__ */ new Set();
103
- liveBroadcastSink = null;
104
- hostDispatchSink = null;
105
- /**
106
- * Tick-broadcast relay (T6): newest pending item per
107
- * (originClientId, orbital, event), flushed to the live-broadcast sink on
108
- * the `tickRelayIntervalMs` cadence — other tabs get the latest position
109
- * at snapshot rate, never 1:1 with emissions.
110
- */
111
- tickRelayPending = /* @__PURE__ */ new Map();
112
- tickRelayTimer = null;
113
- constructor(config) {
114
- installNodeHostPorts();
115
- this.config = {
116
- ...config,
117
- // `@user` is never Null: a host that named no viewer and has no sign-in serves the unauthenticated viewer.
118
- defaultUser: config.defaultUser ?? ANONYMOUS_USER
119
- };
120
- this.viewerNamedByHost = config.defaultUser !== void 0;
121
- this.eventBus = new EventBus();
122
- }
123
- /**
124
- * Lazily wire the program host handlers (behavior/* and program/* effects),
125
- * merging them UNDER any user-provided handlers. Only when `programHost`
126
- * is configured; otherwise the effects fail explicitly.
127
- */
128
- async ensureProgramHandlers() {
129
- if (!isNodeEnv()) return;
130
- const host = this.config.programHost;
131
- if (host === void 0) return;
132
- if (!this.programHandlersPromise) {
133
- this.programHandlersPromise = (async () => {
134
- const { createProgramHandlers } = await import('./createProgramHandlers.js');
135
- this.config.effectHandlers = {
136
- ...createProgramHandlers(host),
137
- ...this.config.effectHandlers
138
- };
139
- })();
140
- }
141
- return this.programHandlersPromise;
142
- }
143
- // ==========================================================================
144
- // Schema Registration
145
- // ==========================================================================
146
- /**
147
- * Register an OrbitalSchema for execution.
148
- *
149
- * Auto-preprocesses the schema when it contains `uses` declarations or
150
- * unresolved cross-orbital trait references (e.g. a trait with
151
- * `ref: "Modal.traits.ModalRecordModal"` and no inline `stateMachine`).
152
- * Without preprocessing, those refs arrive empty at the state machine and
153
- * button clicks silently do nothing — see Phase 9.5.H.
154
- *
155
- * Preprocessing needs a loader. If `loaderConfig` is set, that loader is
156
- * used. Otherwise, a default loader is constructed that points at
157
- * `<cwd>` (for `basePath`) and the nearest `node_modules/@almadar/std` (for
158
- * `stdLibPath`), which matches how every caller in this monorepo has the
159
- * std registry on disk.
160
- */
161
- /**
162
- * The in-flight (or last-completed) app mutation — `register()` chains its
163
- * body onto this, and `unregisterAll()` stamps a marker after clearing.
164
- * `processOrbitalEvent` consults it when the requested orbital is missing:
165
- * during a catalog-style app swap the new schema's registration is already
166
- * in flight while a client's first events arrive, and answering those with
167
- * an immediate `Orbital not found` 404 (the pre-fix behavior) races the
168
- * swap — big multi-orbital organisms lost their mount INITs intermittently
169
- * while single-orbital ones almost always won. Awaiting the epoch and
170
- * re-reading the map turns the race into a brief wait. `BrowserPlayground`
171
- * gated dispatches on registration ad hoc for the same race; the epoch is
172
- * the canonical, host-wide fix.
173
- */
174
- registrationEpoch = null;
175
- /**
176
- * Reentrancy guard for `withRegistrationMutation`. While a bracket is
177
- * open, `register()`/`unregisterAll()` must NOT chain/stamp the epoch:
178
- * the callback's sequential awaits already order them, and chaining a
179
- * `register()` awaited by the callback onto the bracket's gate would
180
- * deadlock (callback → register → epoch → gate → callback's finally).
181
- */
182
- registrationMutationDepth = 0;
183
- async register(schema, options = {}) {
184
- if (this.registrationMutationDepth > 0) {
185
- await this.doRegister(schema, options);
186
- return;
187
- }
188
- const previous = this.registrationEpoch ?? Promise.resolve();
189
- const epoch = previous.then(
190
- () => this.doRegister(schema, options),
191
- () => this.doRegister(schema, options)
192
- );
193
- this.registrationEpoch = epoch;
194
- await epoch;
195
- }
196
- async doRegister(schema, options) {
197
- const kernel = await this.kernelModule();
198
- installKernelEvaluator(kernel);
199
- this.openOwnedStore(kernel);
200
- if (this.config.debug) {
201
- registerLog.debug("register:schema", { name: schema.name });
202
- }
203
- this.appLocales = schema.locales ?? [];
204
- for (const [locale, catalog] of Object.entries(options.messages ?? {})) {
205
- this.messages[locale] = { ...this.messages[locale], ...catalog };
206
- }
207
- if (needsResolving(schema)) schema = await this.resolveWithOrb(schema);
208
- const lazyOrbitals = (options.lazy ?? []).flatMap((lazy) => lazy.orbitals);
209
- const executed = lazyOrbitals.length > 0 ? { ...schema, orbitals: [...schema.orbitals, ...lazyOrbitals] } : schema;
210
- for (const orbital of executed.orbitals) {
211
- await this.registerOrbitalAsync(orbital);
212
- }
213
- this.setupTicks();
214
- await this.openKernel(executed);
215
- this.resolvedSchema = schema;
216
- await this.flushPendingOwnerRestamp();
217
- }
218
- /** The kernel's own row store, opened once in the register phase when the host supplied none. */
219
- openOwnedStore(kernel) {
220
- if (this.config.persistence !== void 0 || this.ownedStore !== void 0) return;
221
- this.ownedStore = nativeMemoryOf(kernel);
222
- }
223
- /**
224
- * Returns the schema that this runtime is currently executing, post-
225
- * preprocessing. Safe to expose from an HTTP `/api/schema` endpoint — every
226
- * cross-orbital trait ref will have an inline `stateMachine` already, which
227
- * is what the browser's `schema-to-ir` resolver needs to wire button clicks
228
- * back to state transitions.
229
- *
230
- * Returns `null` if `register()` hasn't run yet.
231
- */
232
- /** The registered program's name, or null when nothing is registered (none yet, or after `unregisterAll`). */
233
- registeredSchemaName() {
234
- return this.orbitals.size > 0 ? this.resolvedSchema?.name ?? null : null;
235
- }
236
- getResolvedSchema() {
237
- return this.resolvedSchema;
238
- }
239
- /** The registered program's message catalogs (locale → qualified key → message), for a host's client. */
240
- getMessages() {
241
- return this.messages;
242
- }
243
- /**
244
- * One-call entry point: read an `.orb` file from disk, parse it, preprocess
245
- * cross-orbital imports, and register the result. Callers never touch raw
246
- * `.orb` bytes — `register()` handles preprocessing internally.
247
- *
248
- * Node only. Browsers must receive already-resolved schemas from their
249
- * server (see `getResolvedSchema()`).
250
- */
251
- async registerFromFile(path) {
252
- if (typeof process === "undefined" || !process.versions?.node) {
253
- throw new Error(
254
- "registerFromFile is Node-only. Browsers should receive resolved schemas from their server."
255
- );
256
- }
257
- const { readFile } = await import('fs/promises');
258
- const raw = await readFile(path, "utf-8");
259
- let schema;
260
- try {
261
- schema = JSON.parse(raw);
262
- } catch (err) {
263
- const msg = err instanceof Error ? err.message : String(err);
264
- throw new Error(`registerFromFile: ${path} is not valid JSON: ${msg}`);
265
- }
266
- const locales = schema.locales ?? [];
267
- const messages = await readSidecarCatalogs(readFile, path, locales);
268
- const { dirname, join } = await import('path');
269
- const lazy = [];
270
- for (const page of schema.lazyPages ?? []) {
271
- const lazyPath = join(dirname(path), page.orbRef);
272
- let lazyRaw;
273
- try {
274
- lazyRaw = await readFile(lazyPath, "utf-8");
275
- } catch (err) {
276
- const msg = err instanceof Error ? err.message : String(err);
277
- throw new Error(`registerFromFile: ${path} declares lazy page ${page.path} but its behavior ${page.orbRef} could not be read: ${msg}`);
278
- }
279
- const lazySchema = JSON.parse(lazyRaw);
280
- if (!lazySchema.orbitals.some((o) => o.name === page.orbital)) {
281
- throw new Error(`registerFromFile: lazy page ${page.path} expects orbital "${page.orbital}" in ${page.orbRef}, found: ${lazySchema.orbitals.map((o) => o.name).join(", ")}`);
282
- }
283
- lazy.push(lazySchema);
284
- for (const [locale, catalog] of Object.entries(await readSidecarCatalogs(readFile, lazyPath, locales))) {
285
- messages[locale] = { ...messages[locale], ...catalog };
286
- }
287
- }
288
- await this.register(schema, { messages, lazy });
289
- }
290
- /** A program with imports resolved by `orb` from the configured base path (Node only: the one resolver is the compiler). */
291
- async resolveWithOrb(schema) {
292
- if (!isNodeEnv()) throw new UnresolvedProgramError(schema.name);
293
- const { programResolveSchema } = await import('./program-35OTIG6Y.js');
294
- const cwd = this.programRoot();
295
- if (this.config.debug) registerLog.debug("register:resolve-with-orb", { name: schema.name, cwd });
296
- return programResolveSchema(schema, { cwd, ...this.config.programHost?.orbBin !== void 0 ? { orbBin: this.config.programHost.orbBin } : {} });
297
- }
298
- /**
299
- * Register a single orbital
300
- */
301
- async registerOrbitalAsync(orbital) {
302
- const { inlineTraits, configByTrait, entity } = parseOrbitalTraits(orbital);
303
- this.orbitals.set(orbital.name, {
304
- schema: orbital,
305
- entity,
306
- traits: inlineTraits,
307
- configByTrait
308
- });
309
- const auxiliary = (orbital.auxiliaryEntities ?? []).filter((ref) => typeof ref !== "string" && !isEntityCall(ref));
310
- bindDeclaredCollections(this.persistence, [entity, ...auxiliary]);
311
- if (this.config.debug) {
312
- registerLog.debug("register:orbital", {
313
- name: orbital.name,
314
- traitCount: (orbital.traits || []).length
315
- });
316
- }
317
- }
318
- /** The adapters the kernel's I/O is answered with: this runtime's persistence, services and program host. */
319
- httpPorts(egress = this.resolvedSchema?.egress ?? []) {
320
- const options = this.config.http;
321
- return options === void 0 ? void 0 : createHttpPorts({ ...options, egress });
322
- }
323
- kernelPorts(egress) {
324
- const http = this.httpPorts(egress);
325
- return {
326
- ...http !== void 0 ? { http } : {},
327
- ...this.config.clock !== void 0 ? { now: this.config.clock } : {},
328
- ...this.kernelOs !== void 0 ? { os: this.kernelOs } : {},
329
- ...this.kernelOs !== void 0 && this.config.kernel !== void 0 ? { osCompletions: kernelOsCompletions(this.config.kernel) } : {},
330
- persistence: this.persistence,
331
- inFlightCalls: this.inFlightCalls,
332
- callService: (service, action, params, context) => {
333
- const provider = this.config.effectHandlers?.callService;
334
- if (provider !== void 0) return provider(service, action, params, context);
335
- return defaultCallService(service, action, params, context);
336
- },
337
- programEffect: () => this.config.effectHandlers?.programEffect,
338
- substrateStream: (op, args) => {
339
- if (op !== "program/live") return void 0;
340
- const first = args[0];
341
- const options = first !== null && typeof first === "object" && !Array.isArray(first) ? first : {};
342
- const rawViewer = options["viewer"];
343
- const viewer = rawViewer !== null && typeof rawViewer === "object" && !Array.isArray(rawViewer) ? rawViewer : null;
344
- const clientId = typeof options["clientId"] === "string" ? options["clientId"] : void 0;
345
- return {
346
- subscribe: (message) => {
347
- const receive = (item) => {
348
- if (this.requireKernelEngine().liveItemDelivered(item, viewer, clientId)) message(JSON.parse(JSON.stringify({ event: item.event, ...item.payload !== void 0 ? { payload: item.payload } : {}, source: item.source })));
349
- };
350
- this.liveSubscribers.add(receive);
351
- return () => {
352
- this.liveSubscribers.delete(receive);
353
- };
354
- }
355
- };
356
- },
357
- servicePorts: (caller, user, message, assertReentry) => this.servicePorts(caller, user, message, assertReentry),
358
- deliverLive: (site, payload, originClientId) => this.deliverLiveMessage(site, payload, originClientId),
359
- deferCallOutcome: () => (call) => this.runDeferredOutcome(call.orbital, {
360
- event: call.event,
361
- ...call.payload !== void 0 ? { payload: call.payload } : {},
362
- targetTrait: call.traitName,
363
- ...call.entityId !== void 0 ? { entityId: call.entityId } : {},
364
- ...call.originClientId !== void 0 ? { clientId: call.originClientId } : {},
365
- ...call.user !== void 0 ? { user: call.user } : {},
366
- ...call.locale !== void 0 ? { locale: call.locale } : {}
367
- })
368
- };
369
- }
370
- /** One live message of a running `call-service`, stamped as its calling trait, to the requesting tab only. */
371
- deliverLiveMessage(site, payload, originClientId) {
372
- const registered = this.orbitals.get(site.orbital);
373
- const callingTrait = registered?.traits.find((t) => t.name === site.trait);
374
- const stamp = stampEmitSource(void 0, {
375
- orbitalName: site.orbital,
376
- traitName: site.trait,
377
- orbitalId: registered?.schema.id,
378
- traitId: callingTrait?.id,
379
- emitContractEventId: callingTrait?.emits?.find((e) => e.event === site.event)?.eventId,
380
- originClientId
381
- });
382
- this.eventBus.emit(site.event, payload, stamp, eventRouteKey(site.event, stamp.eventId));
383
- this.publishLiveItem({ event: site.event, payload, source: stamp, originClientId, target: "origin" });
384
- }
385
- /** The project root this runtime hands `orb` and the kernel's language data: the program host's cwd, else the loader base path. */
386
- programRoot() {
387
- return this.config.programHost?.cwd ?? this.config.loaderConfig?.basePath ?? process.cwd();
388
- }
389
- /** The kernel module this runtime runs on: the one the host handed in, else the installed `@almadar/kernel`. */
390
- async kernelModule() {
391
- if (this.config.kernel !== void 0) return this.config.kernel;
392
- const { loadInstalledKernel } = await import('./kernel-loader-24KWM5C7.js');
393
- this.config.kernel = await loadInstalledKernel(this.programRoot());
394
- return this.config.kernel;
395
- }
396
- /** (Re)open the kernel on the program just registered. */
397
- async openKernel(registered) {
398
- const kernel = await this.kernelModule();
399
- const schema = { ...registered, orbitals: inlineTraitRefs(registered.orbitals) };
400
- for (const orbital of schema.orbitals) {
401
- for (const trait of orbital.traits ?? []) {
402
- if (typeof trait === "object" && "ref" in trait && !("stateMachine" in trait)) {
403
- throw new Error(`OrbitalServerRuntime: the kernel takes an orb-resolved program, but trait "${trait.ref}" of orbital "${orbital.name}" is still a reference`);
404
- }
405
- }
406
- }
407
- this.inFlightCalls.cancelAll();
408
- this.kernelEngine?.dispose();
409
- this.kernelOs?.close();
410
- this.kernelOs = this.config.osHost?.(kernelOsGrants(kernel, () => this.requireKernelEngine()));
411
- this.kernelEngine = KernelEngine.open(kernel, schema, this.kernelPorts(registered.egress ?? []));
412
- this.kernelEngine.setMessages(this.appLocales, this.messages);
413
- if (this.managedProgram !== void 0) this.kernelEngine.setManagedProgram(this.managedProgram);
414
- await this.seedStores(registered);
415
- this.seedKernelRuntimeMock();
416
- this.syncTickMount();
417
- await this.mountBootEndpoints();
418
- }
419
- /** Run the kernel's boot mounts for the server side (EXEC-ENDPOINT-3): the host sends what the kernel lists and chooses nothing. */
420
- async mountBootEndpoints() {
421
- const owners = /* @__PURE__ */ new Map();
422
- for (const registered of this.orbitals.values()) {
423
- for (const trait of registered.traits) owners.set(trait.name, registered);
424
- }
425
- const batches = /* @__PURE__ */ new Map();
426
- for (const seed of this.requireKernelEngine().bootMounts("server")) {
427
- const owner = owners.get(seed.trait);
428
- if (owner === void 0) continue;
429
- batches.set(owner, [...batches.get(owner) ?? [], seed]);
430
- }
431
- for (const [registered, mount] of batches) {
432
- const first = mount[0];
433
- if (first === void 0) continue;
434
- const response = await this.enqueueEvent(
435
- (hold) => this.evaluateForOrbital(registered, { event: first.event, mount, payload: {} }, void 0, hold)
436
- );
437
- if (!response.success) {
438
- throw new Error(`OrbitalServerRuntime: the boot mount of orbital "${registered.schema.name}" failed: ${response.error ?? "the kernel refused it"}`);
439
- }
440
- }
441
- }
442
- /** The program whose rows the store holds, once seeded; absent before the first seed and after `unregisterAll`. */
443
- seededProgram;
444
- /** The std mock-seed program, resolved by `orb` from the installed packages. */
445
- async resolveSeedProgram() {
446
- if (this.seedProgram !== void 0) return this.seedProgram;
447
- const { behaviorResolve } = await import('./program-35OTIG6Y.js');
448
- const resolved = await behaviorResolve(
449
- { behavior: STD_MOCK_SEED },
450
- { cwd: this.programRoot(), ...this.config.programHost?.orbBin !== void 0 ? { orbBin: this.config.programHost.orbBin } : {} }
451
- );
452
- if (!("orbitals" in resolved)) throw new Error(`OrbitalServerRuntime: the mock seed program ${STD_MOCK_SEED} did not resolve: ${resolved.error}`);
453
- this.seedProgram = resolved;
454
- return resolved;
455
- }
456
- /** Run `use` with the seed program forked off the kernel just opened; the fork is released after. */
457
- async withSeeder(user, use) {
458
- const program = await this.resolveSeedProgram();
459
- const seedEngine = this.requireKernelEngine().forkProgram(program);
460
- try {
461
- return await use({
462
- seeder: new StdMockSeed(seedEngine, declaredSeedConfig(program, this.resolvedSchema ?? void 0), user),
463
- persistence: this.persistence,
464
- nowMs: this.config.clock?.() ?? Date.now()
465
- });
466
- } finally {
467
- seedEngine.dispose();
468
- }
469
- }
470
- /** Store writes (seed, reseed, restamp) run one after another, so a persona switch never interleaves a reseed. */
471
- enqueueStoreOp(op) {
472
- const run = this.storeOps.then(op, op);
473
- this.storeOps = run.catch(() => void 0);
474
- return run;
475
- }
476
- /**
477
- * Fill the store from the program, in the register phase: a mock runtime seeds every entity for the viewer, any
478
- * other runtime seeds the rows the program writes out and its browser-stored entities.
479
- */
480
- async seedStores(program) {
481
- if (this.seedsMock()) {
482
- const named = this.viewerNamedByHost ? this.config.defaultUser : void 0;
483
- const seeded = await this.withSeeder(named, (host) => seedMockStore(host, program, {
484
- namedByHost: this.viewerNamedByHost,
485
- current: () => this.config.defaultUser,
486
- seedIdentity: true
487
- }));
488
- this.mockJournal = seeded.journal;
489
- if (!this.viewerNamedByHost) this.config.defaultUser = seeded.viewer;
490
- this.seededProgram = program;
491
- return;
492
- }
493
- const local = [];
494
- for (const registered of this.orbitals.values()) {
495
- if (registered.entity.browser === true) local.push(registered.entity);
496
- for (const aux of registered.schema.auxiliaryEntities ?? []) {
497
- if (typeof aux !== "string" && !isEntityCall(aux) && aux.browser === true) local.push(aux);
498
- }
499
- }
500
- if (local.length === 0) return;
501
- await this.withSeeder(void 0, (host) => seedBrowserStore(host.persistence, local, void 0, host));
502
- }
503
- managedProgram;
504
- /**
505
- * Name the program whose declared credentials this runtime's credential effects manage beside its own: an
506
- * integrations console serving the app a host shows saves and tests that app's keys. Naming another replaces it.
507
- */
508
- manageCredentialsOf(program) {
509
- this.managedProgram = program;
510
- this.kernelEngine?.setManagedProgram(program);
511
- }
512
- /** A mock store seeds every entity; the `[runtime]` rows the kernel holds itself are seeded in the kernel. */
513
- seedKernelRuntimeMock() {
514
- if (!this.seedsMock()) return;
515
- this.kernelEngine?.seedRuntimeMock(this.config.defaultUser?.id);
516
- }
517
- requireKernelEngine() {
518
- if (this.kernelEngine === null) throw new Error("OrbitalServerRuntime: an os ask arrived with no kernel program registered");
519
- return this.kernelEngine;
520
- }
521
- /**
522
- * Set up scheduled ticks for all traits
523
- */
524
- setupTicks() {
525
- this.cleanupTicks();
526
- for (const [orbitalName, registered] of this.orbitals) {
527
- for (const trait of registered.traits || []) {
528
- if (!trait.ticks || trait.ticks.length === 0) continue;
529
- for (const tick of trait.ticks) {
530
- this.registerTick(orbitalName, trait.name, tick);
531
- }
532
- }
533
- }
534
- if (this.config.debug && this.tickBindings.length > 0) {
535
- registerLog.debug("register:ticks", { count: this.tickBindings.length });
536
- }
537
- }
538
- /**
539
- * Register a single tick
540
- */
541
- registerTick(orbitalName, traitName, tick) {
542
- if (typeof tick.interval === "string" && isValidCronExpression(tick.interval)) {
543
- if (this.config.debug) {
544
- registerLog.debug("register:tick-cron", {
545
- orbital: orbitalName,
546
- trait: traitName,
547
- tick: tick.name,
548
- expression: tick.interval
549
- });
550
- }
551
- const handle2 = this.tickScheduler.addCron(tick.interval, () => {
552
- void this.executeTick(orbitalName, traitName, tick);
553
- });
554
- this.tickBindings.push({ orbitalName, traitName, tick, handle: handle2 });
555
- return;
556
- }
557
- let intervalMs;
558
- if (typeof tick.interval === "number") {
559
- intervalMs = tick.interval;
560
- } else if (typeof tick.interval === "string") {
561
- intervalMs = parseDurationString(tick.interval);
562
- } else {
563
- intervalMs = 1e3;
564
- }
565
- if (this.config.debug) {
566
- registerLog.debug("register:tick", {
567
- orbital: orbitalName,
568
- trait: traitName,
569
- tick: tick.name,
570
- intervalMs
571
- });
572
- }
573
- const handle = this.tickScheduler.add(intervalMs, () => {
574
- void this.executeTick(orbitalName, traitName, tick);
575
- });
576
- this.tickBindings.push({
577
- orbitalName,
578
- traitName,
579
- tick,
580
- handle
581
- });
582
- }
583
- /** A tick: the host owns the timer, the kernel decides whether and over which rows it fires. */
584
- async executeTick(orbitalName, traitName, tick) {
585
- const response = await this.processOrbitalEvent(orbitalName, {
586
- event: tickEventKey(tick.name),
587
- runTick: { trait: traitName, tick: tick.name },
588
- targetTrait: traitName
589
- });
590
- if (!response.success) {
591
- effectLog.error("tick:execute-error", { tick: tick.name, error: response.error ?? "the kernel refused the tick" });
592
- }
593
- }
594
- /** Tell the kernel which traits some client has mounted, so it can pause the other traits' ticks. */
595
- syncTickMount() {
596
- if (this.kernelEngine === null) return;
597
- const traits = /* @__PURE__ */ new Set();
598
- for (const key of this.mountedBy.keys()) traits.add(key.split("::")[1] ?? "");
599
- this.kernelEngine.setTickMount([...traits]);
600
- }
601
- /**
602
- * Clean up all active ticks
603
- */
604
- cleanupTicks() {
605
- for (const binding of this.tickBindings) {
606
- binding.handle.stop();
607
- }
608
- this.tickBindings = [];
609
- }
610
- /**
611
- * Unregister all orbitals and clean up
612
- */
613
- /** A client mounts a trait by dispatching its lifecycle event and releases it with `$UNMOUNT`. */
614
- recordMount(orbitalName, request, mounted) {
615
- const client = request.clientId;
616
- if (client === void 0) return;
617
- const traits = request.mount !== void 0 ? request.mount.map((m) => m.trait) : request.targetTrait !== void 0 ? [request.targetTrait] : (request.traits ?? []).map((t) => t.trait);
618
- for (const traitName of traits) {
619
- const key = `${orbitalName}::${traitName}`;
620
- const clients = this.mountedBy.get(key) ?? /* @__PURE__ */ new Set();
621
- if (mounted) clients.add(client);
622
- else clients.delete(client);
623
- if (clients.size > 0) this.mountedBy.set(key, clients);
624
- else this.mountedBy.delete(key);
625
- }
626
- this.syncTickMount();
627
- }
628
- /** Drop every mount a disconnected client held, so its traits' mount-scoped ticks pause. */
629
- releaseClient(clientId) {
630
- for (const [key, clients] of this.mountedBy) {
631
- clients.delete(clientId);
632
- if (clients.size === 0) this.mountedBy.delete(key);
633
- }
634
- this.syncTickMount();
635
- }
636
- /** True while at least one client has `traitName` mounted. */
637
- isTraitMounted(orbitalName, traitName) {
638
- return (this.mountedBy.get(`${orbitalName}::${traitName}`)?.size ?? 0) > 0;
639
- }
640
- unregisterAll() {
641
- this.cleanupTicks();
642
- this.mountedBy.clear();
643
- this.orbitals.clear();
644
- this.inFlightCalls.cancelAll();
645
- this.kernelEngine?.dispose();
646
- this.kernelEngine = null;
647
- this.kernelOs?.close();
648
- this.kernelOs = void 0;
649
- this.eventBus.clear();
650
- this.messages = {};
651
- this.appLocales = [];
652
- if (this.seedsMock()) this.ownedStore?.clear();
653
- this.mockJournal = void 0;
654
- this.seededProgram = void 0;
655
- this.programHandlersPromise = null;
656
- if (this.registrationMutationDepth === 0) {
657
- this.registrationEpoch = (this.registrationEpoch ?? Promise.resolve()).then(() => void 0, () => void 0);
658
- }
659
- }
660
- /**
661
- * Bracket a multi-step app mutation (the classic swap is
662
- * `unregisterAll()` → resolve next schema → `register()`) as ONE
663
- * registration epoch. `register()` alone chains only its own body onto
664
- * the epoch, so between `unregisterAll()` and the `register()` call the
665
- * runtime's map is empty while the epoch marker has already resolved —
666
- * any `processOrbitalEvent` that lands in that gap (the resolve of a big
667
- * organism takes real time) 404s `Orbital not found` even though the
668
- * requested orbital is milliseconds away from landing. Inside this
669
- * bracket the epoch stays pending for the WHOLE callback, so mid-swap
670
- * dispatches await the completed swap and then resolve against the new
671
- * app. A failed callback still releases the epoch (chained on both
672
- * branches), so a failed swap falls through to the honest 404 instead of
673
- * wedging the queue. Callbacks SERIALIZE: each bracket awaits the previous
674
- * epoch before running `fn`, so two hosts swapping at once (a catalog
675
- * select racing a preview remount's `/register`) cannot interleave their
676
- * unregister/register sequences into a mixed registry. A bracket opened
677
- * from INSIDE another bracket's callback runs `fn` directly — detected
678
- * via the AsyncLocalStorage context below (a depth counter alone can't
679
- * tell a true nest from a concurrent caller) — because the outer
680
- * callback's awaits already serialize it and queueing on the outer gate
681
- * would deadlock. Deadlock-free otherwise: `doRegister` never dispatches
682
- * events, so awaiting dispatches never block registration progress.
683
- */
684
- /**
685
- * Reentrancy context for `withRegistrationMutation`. A depth counter alone
686
- * cannot distinguish "called from inside a bracket callback" (must run
687
- * directly — queueing on the outer gate would deadlock) from "called
688
- * concurrently while another bracket is in flight" (must queue — running
689
- * directly would interleave two swaps). The ALS marks the callback's async
690
- * context, which separates the two exactly.
691
- */
692
- bracketAls = null;
693
- ensureBracketAls() {
694
- this.bracketAls ??= import('async_hooks').then(
695
- ({ AsyncLocalStorage }) => new AsyncLocalStorage()
696
- );
697
- return this.bracketAls;
698
- }
699
- async withRegistrationMutation(fn) {
700
- const previous = this.registrationEpoch ?? Promise.resolve();
701
- let release = () => void 0;
702
- const gate = new Promise((resolve) => {
703
- release = resolve;
704
- });
705
- this.registrationEpoch = previous.then(() => gate, () => gate);
706
- this.registrationMutationDepth++;
707
- try {
708
- const als = await this.ensureBracketAls();
709
- if (als.getStore() !== void 0) {
710
- release();
711
- return await fn();
712
- }
713
- await previous.catch(() => void 0);
714
- const nestingToken = /* @__PURE__ */ Symbol("withRegistrationMutation");
715
- return await als.run(nestingToken, fn);
716
- } finally {
717
- this.registrationMutationDepth--;
718
- release();
719
- }
720
- }
721
- /**
722
- * Reset the mock persistence store to a clean-slate re-seed without
723
- * unregistering orbitals. Exposed for verifier tools that want to
724
- * start each test with deterministic seeded rows, not the residue of
725
- * the previous walk's persist-creates. No-op when the runtime does not
726
- * seed a mock store of its own.
727
- */
728
- resetMockPersistence() {
729
- const store = this.ownedStore;
730
- if (!this.seedsMock() || store === void 0) return Promise.resolve();
731
- busLog.debug("mock:reset:enter", {
732
- orbitalCount: this.orbitals.size,
733
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
734
- });
735
- return this.enqueueStoreOp(async () => {
736
- store.clear();
737
- if (this.seededProgram !== void 0) await this.seedStores(this.seededProgram);
738
- });
739
- }
740
- /**
741
- * Return every registered orbital's circuit to boot: trait states to their
742
- * initial states, frames and row cache emptied, and a fresh mount (no trait
743
- * awaiting INIT, nothing held). The hermetic-reset twin of
744
- * `resetMockPersistence`: a reset walk must not inherit the previous walk's
745
- * circuit any more than its rows.
746
- */
747
- resetCircuitState() {
748
- this.inFlightCalls.cancelAll();
749
- this.kernelEngine?.reset();
750
- this.seedKernelRuntimeMock();
751
- this.kernelOs?.close();
752
- }
753
- /**
754
- * Wire a live-broadcast sink. Called only from a `persist` effect's
755
- * `emit:{success}` envelope firing site (batch and single-op) — see
756
- * `docs/Almadar_Live_Push.md`. The hosting server (e.g. the playground's
757
- * `/api/events` SSE endpoint) uses this to fan persist mutations out to
758
- * every OTHER connected client; a fresh call replaces the previous sink.
759
- */
760
- setLiveBroadcastSink(sink) {
761
- this.liveBroadcastSink = sink;
762
- }
763
- publishLiveItem(item) {
764
- this.liveBroadcastSink?.(item);
765
- for (const receive of this.liveSubscribers) receive(item);
766
- }
767
- /** Wire where a host-run dispatch's result goes (see `HostDispatchItem`); a fresh call replaces the previous sink. */
768
- setHostDispatchSink(sink) {
769
- this.hostDispatchSink = sink;
770
- }
771
- /** Run a follow-on step's call outcome as its own dispatch, and hand its result to the host-dispatch sink. */
772
- runDeferredOutcome(orbitalName, request) {
773
- void this.processOrbitalEvent(orbitalName, request).then(
774
- (response) => this.hostDispatchSink?.({ orbitalName, request, response, ...request.clientId !== void 0 ? { originClientId: request.clientId } : {} }),
775
- (error) => effectLog.error("deferred-outcome:failed", { orbitalName, event: request.event, trait: request.targetTrait, error: error.message })
776
- );
777
- }
778
- /**
779
- * Switch the viewer a dev host presents the app as — see
780
- * `OrbitalServerRuntimeConfig.defaultUser`. Pass `undefined` for an
781
- * unauthenticated viewer. Takes effect on the next event; an authenticated
782
- * request still overrides it.
783
- *
784
- * Mock mode stamps the seeded owner columns for whichever id was `defaultUser`
785
- * at register time. Switching to a different id re-points those cells with
786
- * `restampMockOwner` instead of re-seeding, so an ownership-scoped view for
787
- * the new viewer has rows. The returned promise settles when the restamp has
788
- * landed (immediately when there is nothing seeded yet; register flushes it).
789
- */
790
- setDefaultUser(user) {
791
- const previousId = this.config.defaultUser?.id;
792
- this.config.defaultUser = user;
793
- this.viewerNamedByHost = true;
794
- if (this.seedsMock() && user?.id !== void 0 && user.id !== previousId) return this.restampOwnerGated(user);
795
- return Promise.resolve();
796
- }
797
- /** Whether the host named the viewer (constructor or `setDefaultUser`). If not, the viewer is
798
- * the app's first declared persona, resolved as soon as its roster is seeded. */
799
- viewerNamedByHost = false;
800
- /** setDefaultUser can land before register() finishes resolving the schema
801
- * (a dev host pins a persona right after boot). An ungated restamp there
802
- * would hand a read-only persona authorship of drafts, so the restamp is
803
- * DEFERRED until the schema exists — register()/registerSync() flush it. */
804
- pendingOwnerRestamp = false;
805
- /**
806
- * Re-point the seeded owner cells at `user` (a persona switch never reseeds). The seed program's policy gates decide
807
- * per row whether the persona could have created it. Deferred when the program is not seeded yet.
808
- */
809
- restampOwnerGated(user) {
810
- if (this.seededProgram === void 0) {
811
- this.pendingOwnerRestamp = true;
812
- return Promise.resolve();
813
- }
814
- return this.enqueueStoreOp(async () => {
815
- const program = this.seededProgram;
816
- const journal = this.mockJournal;
817
- if (program === void 0 || journal === void 0) return;
818
- this.mockJournal = await this.withSeeder(user, (host) => restampMockOwner(host, program, journal, user));
819
- });
820
- }
821
- /** Flush a persona switch that arrived before the schema was resolved. */
822
- async flushPendingOwnerRestamp() {
823
- if (!this.pendingOwnerRestamp) return;
824
- this.pendingOwnerRestamp = false;
825
- const user = this.config.defaultUser;
826
- if (user?.id !== void 0) await this.restampOwnerGated(user);
827
- }
828
- /** The viewer a dev host is currently presenting the app as. */
829
- getDefaultUser() {
830
- return this.config.defaultUser;
831
- }
832
- /**
833
- * Queue a tick-stamped dispatch for coalesced snapshot relay (T6). Newest
834
- * per (originClientId, orbital, event) wins between flushes — the standard
835
- * netcode position-broadcast discipline.
836
- */
837
- queueTickRelay(orbitalName, request) {
838
- const key = `${request.clientId ?? ""}:${orbitalName}:${request.event}`;
839
- this.tickRelayPending.set(key, {
840
- event: request.event,
841
- payload: request.payload,
842
- source: { orbital: orbitalName, trait: request.sourceTrait, tick: request.tick },
843
- originClientId: request.clientId,
844
- target: "peers"
845
- });
846
- if (this.tickRelayTimer === null) {
847
- const timer = setInterval(() => this.flushTickRelay(), this.config.tickRelayIntervalMs ?? 50);
848
- if (typeof timer === "object" && typeof timer.unref === "function") timer.unref();
849
- this.tickRelayTimer = timer;
850
- }
851
- }
852
- flushTickRelay() {
853
- if (this.tickRelayPending.size === 0) {
854
- if (this.tickRelayTimer !== null) {
855
- clearInterval(this.tickRelayTimer);
856
- this.tickRelayTimer = null;
857
- }
858
- return;
859
- }
860
- const items = Array.from(this.tickRelayPending.values());
861
- this.tickRelayPending.clear();
862
- for (const item of items) this.publishLiveItem(item);
863
- }
864
- /**
865
- * The registered app's declared persona roster: the live rows of its
866
- * `[identity]` entity, mapped onto viewers (`Almadar_LOLO_Identity.md` §4.3).
867
- * Live-store rows rather than a re-derivation, so the roster carries the ids
868
- * ownership scoping actually compares `@user.id` against. Empty when no
869
- * schema is registered or the app declares no `[identity]` entity — there is
870
- * no global fallback roster by design.
871
- */
872
- /**
873
- * `reference` resolved for `user` against the store as it is now: the rows that still match its query and the
874
- * viewer's `@read`, in the reference's order, `total` re-counted. The kernel resolves through its server
875
- * resolve flight. A client reaches it with
876
- * `POST /:orbital/../resolve-rows` as the signed-in viewer.
877
- */
878
- async resolveRows(reference, user) {
879
- const schema = this.resolvedSchema;
880
- const owner = schema === null ? void 0 : entityOwnerOrbital(schema, reference.entityType);
881
- if (schema === null || owner === void 0) throw new Error(`${reference.entityType}: no registered orbital declares this entity`);
882
- return this.requireKernelEngine().resolveRows(reference, { orbital: owner.name, user });
883
- }
884
- async getIdentityRoster() {
885
- const schema = this.resolvedSchema;
886
- if (!schema) return [];
887
- const entityName = identityEntityName(schema);
888
- if (!entityName) return [];
889
- const rows = await this.persistence.list(entityName);
890
- return rows.map((row) => personaFromIdentityRow(row)).filter((p) => p !== void 0);
891
- }
892
- // ==========================================================================
893
- // Event Processing
894
- // ==========================================================================
895
- /**
896
- * Process an event for an orbital — the stateful host's thin shell over
897
- * the unified `evaluateOrbitalEvent` composition (design:
898
- * docs/Almadar_Runtime_Stateless_Stateful_PLAN.md §4.1). This method owns
899
- * ONLY session concerns: orbital lookup, viewer normalization, the lazy
900
- * OS/substrate handler wiring, the per-request relay mask
901
- * (G-RUNTIME-031 — the client's mounted set as DATA, replacing the
902
- * deleted blanket `originClientId` skip), the tick snapshot relay, and
903
- * the cross-orbital listens relay (a listener registered under a
904
- * DIFFERENT orbital than the emitter — the per-orbital composition's
905
- * in-band fan-out covers the rest). Target resolution, payload
906
- * validation, the cascade, the fan-out, structured rejections, and
907
- * response assembly are the composition's, identical to the stateless
908
- * path.
909
- *
910
- * Serialization: the whole evaluation runs on ONE runtime-wide async
911
- * queue. The composition commits manager state directly (no per-trait
912
- * actor queue), so concurrent requests against the long-lived session
913
- * manager would otherwise both evaluate from the same held state and
914
- * lose a transition. Coarser than the old per-(trait, entityId) actor
915
- * queues — a deliberate W4 tradeoff, recorded in the plan doc.
916
- *
917
- * @param onPush - Optional incremental-push callback. Called immediately
918
- * when an event is emitted or a client effect fires, before the promise
919
- * resolves. Used by the SSE streaming path to write items to the
920
- * response as they arrive rather than buffering and flushing at the end.
921
- */
922
- /**
923
- * Read an orbital by name, awaiting an in-flight registration mutation
924
- * first when the lookup misses (G-RUNTIME-038): mid-swap lookups 404ed
925
- * against the emptied map even though the requested orbital was
926
- * milliseconds from landing. A failed mutation falls through to the miss.
927
- */
928
- async resolveRegisteredOrbital(orbitalName) {
929
- let registered = this.orbitals.get(orbitalName);
930
- if (!registered && this.registrationEpoch !== null) {
931
- await this.registrationEpoch.catch(() => void 0);
932
- registered = this.orbitals.get(orbitalName);
933
- }
934
- return registered;
935
- }
936
- /** The registered orbitals as `GET /` lists them. */
937
- listOrbitalSummaries() {
938
- return Array.from(this.orbitals.entries()).map(([name, reg]) => ({
939
- name,
940
- entity: reg.entity?.name,
941
- traits: (reg.traits || []).map((t) => t.name)
942
- }));
943
- }
944
- /** One orbital as `GET /:orbital` describes it, awaiting an in-flight swap first (G-RUNTIME-038); `undefined` when it is not registered. */
945
- async describeOrbital(orbitalName) {
946
- const registered = await this.resolveRegisteredOrbital(orbitalName);
947
- if (!registered) return void 0;
948
- const states = {};
949
- for (const trait of registered.traits) {
950
- const currentState = this.kernelEngine?.stateOf(trait.name) ?? "";
951
- if (currentState !== "") states[trait.name] = currentState;
952
- }
953
- return {
954
- name: orbitalName,
955
- entity: registered.entity,
956
- traits: registered.traits.map((t) => ({
957
- name: t.name,
958
- currentState: states[t.name],
959
- states: (t.stateMachine?.states || []).map((s) => s.name),
960
- events: [...new Set((t.stateMachine?.transitions || []).map((tr) => tr.event))]
961
- }))
962
- };
963
- }
964
- /** An entity's full row set as `GET /:orbital/entities/:entityType` answers it (verification and tooling only); `undefined` when the orbital is not registered. */
965
- async entityRows(orbitalName, entityType) {
966
- const registered = await this.resolveRegisteredOrbital(orbitalName);
967
- if (!registered) return void 0;
968
- return this.persistence.list(entityType);
969
- }
970
- /** The program's declared external inputs: what an outside client may send which trait. */
971
- listExternalInputs() {
972
- return this.resolvedSchema ? externalInputsOf(this.resolvedSchema) : [];
973
- }
974
- /**
975
- * The outside-client input channel: an API caller, an agent acting as the
976
- * user, an MCP client. Only a declared external input of the target trait is
977
- * dispatched — through `processOrbitalEvent`, so guards, entity policies and
978
- * cascades apply exactly as for a click. Anything else is refused with
979
- * `not-an-external-input`. The app's own UI keeps using `processOrbitalEvent`.
980
- */
981
- /**
982
- * The running app lent to one `call-service` provider as the caller: the
983
- * declared inputs, the input channel as `user`, and the rows `user` may read
984
- * (the entity's `@read` policy, the same rule a `fetch` effect applies).
985
- */
986
- servicePorts(caller, user, message = () => {
987
- }, assertReentry) {
988
- return {
989
- caller,
990
- message,
991
- inputs: () => this.listExternalInputs(),
992
- readableEntities: () => this.resolvedSchema ? readableEntitiesOf(this.resolvedSchema) : [],
993
- dispatchInput: (orbital, request) => {
994
- assertReentry?.();
995
- return this.dispatchExternalInput(orbital, { ...request, ...user !== void 0 ? { user } : {} });
996
- },
997
- read: async (entity) => {
998
- const readPolicy = this.resolvedSchema ? entityAccessPolicies(this.resolvedSchema, entity)?.read : void 0;
999
- return applyRowAccess(await this.persistence.list(entity), readPolicy, void 0, { user });
1000
- },
1001
- supportsDispatchStream: true,
1002
- dispatchRequest: (orbital, request, input, stream) => {
1003
- assertReentry?.();
1004
- return this.dispatchEventRequest(orbital, request, input, user, stream);
1005
- },
1006
- deliverInbound: (name, delivery) => this.deliverInbound(name, delivery)
1007
- };
1008
- }
1009
- /**
1010
- * A `program/dispatch` event request, as the events route (or with `input`, the inputs route) runs it: the
1011
- * request's own `user` is its viewer, else the caller's. An unknown orbital is `ORBITAL_NOT_FOUND`, an undeclared
1012
- * locale `LOCALE_NOT_DECLARED`; an inbound-only event and an undeclared input are the routes' refusals.
1013
- */
1014
- async dispatchEventRequest(orbital, request, input, caller, stream = false) {
1015
- const streamItems = [];
1016
- const onPush = stream ? (item) => {
1017
- streamItems.push({ ...item, timestamp: item.timestamp ?? Date.now() });
1018
- } : void 0;
1019
- const answer = (response) => ({
1020
- ok: true,
1021
- response,
1022
- ...stream ? { streamItems: [...streamItems, { type: "complete", data: response, timestamp: Date.now() }] } : {}
1023
- });
1024
- if (await this.describeOrbital(orbital) === void 0) {
1025
- return { ok: false, error: `Orbital not found: ${orbital}`, code: "ORBITAL_NOT_FOUND", status: 404 };
1026
- }
1027
- const user = request.user ?? caller;
1028
- if (input) {
1029
- if (request.targetTrait === void 0) {
1030
- return { ok: false, error: "program/dispatch: an input request names its trait", code: "NO_TRAIT", status: 400 };
1031
- }
1032
- const response = await this.dispatchExternalInput(orbital, {
1033
- targetTrait: request.targetTrait,
1034
- event: request.event,
1035
- ...request.payload !== void 0 ? { payload: request.payload } : {},
1036
- ...request.entityId !== void 0 ? { entityId: request.entityId } : {},
1037
- ...user !== void 0 ? { user } : {}
1038
- }, onPush);
1039
- return answer(response);
1040
- }
1041
- const refusal = this.refuseInboundOnly(orbital, request);
1042
- if (refusal !== void 0) return answer(refusal);
1043
- if (request.locale !== void 0 && this.appLocales.length > 0 && !this.appLocales.includes(request.locale)) {
1044
- const error = `locale "${request.locale}" is not one of the program's declared locales [${this.appLocales.join(", ")}]`;
1045
- return { ok: false, error, code: "LOCALE_NOT_DECLARED", status: 400 };
1046
- }
1047
- return answer(await this.processOrbitalEvent(orbital, user !== void 0 ? { ...request, user } : request, onPush));
1048
- }
1049
- async dispatchExternalInput(orbitalName, request, onPush) {
1050
- return dispatchDeclaredInput(this.resolvedSchema, orbitalName, request, (orbital, req) => this.processOrbitalEvent(orbital, req, onPush));
1051
- }
1052
- /**
1053
- * An inbound delivery (`/api/hooks/<name>`): the runtime decides whether the name is served, runs
1054
- * the handler through `processOrbitalEvent` and answers; the host only carries the bytes.
1055
- */
1056
- /**
1057
- * An event that is an inbound hook's is delivered only by `deliverInbound`: the app's own event
1058
- * route refuses it, as the input channel refuses an event that is no declared input.
1059
- */
1060
- refuseInboundOnly(orbitalName, request) {
1061
- const trait = this.resolvedSchema ? inboundOnlyTrait(this.resolvedSchema, orbitalName, request.targetTrait, request.event) : void 0;
1062
- if (trait === void 0) return void 0;
1063
- return {
1064
- success: false,
1065
- transitioned: false,
1066
- states: {},
1067
- emittedEvents: [],
1068
- error: inboundOnlyMessage(trait, request.event),
1069
- rejections: [{ code: "inbound-only-event", trait, event: request.event }]
1070
- };
1071
- }
1072
- async deliverInbound(name, delivery, onPush) {
1073
- return dispatchInbound(this.resolvedSchema, name, delivery, (orbital, req) => this.processOrbitalEvent(orbital, req, onPush), (credential) => (this.httpPorts()?.secrets ?? createHttpSecrets()).get(credential, void 0));
1074
- }
1075
- async processOrbitalEvent(orbitalName, request, onPush) {
1076
- try {
1077
- await this.ensureProgramHandlers();
1078
- const registered = await this.resolveRegisteredOrbital(orbitalName);
1079
- if (!registered) {
1080
- return {
1081
- success: false,
1082
- transitioned: false,
1083
- states: {},
1084
- emittedEvents: [],
1085
- error: `Orbital not found: ${orbitalName}`
1086
- };
1087
- }
1088
- if (request.event === UNMOUNT_EVENT) {
1089
- this.recordMount(orbitalName, request, false);
1090
- return { success: true, transitioned: false, states: {}, emittedEvents: [] };
1091
- }
1092
- if (request.mount !== void 0 || LIFECYCLE_EVENTS.includes(request.event)) {
1093
- this.recordMount(orbitalName, request, true);
1094
- }
1095
- const response = await this.enqueueEvent(
1096
- (queueHold) => this.evaluateForOrbital(registered, request, onPush, queueHold)
1097
- );
1098
- if (request.tick !== void 0 && this.liveBroadcastSink !== null) {
1099
- this.queueTickRelay(orbitalName, request);
1100
- }
1101
- return response;
1102
- } catch (error) {
1103
- const message = error instanceof Error ? error.message : String(error);
1104
- xOrbitalLog.error("processOrbitalEvent:error", {
1105
- orbital: orbitalName,
1106
- event: request.event,
1107
- entityId: request.entityId,
1108
- error: message
1109
- });
1110
- return {
1111
- success: false,
1112
- transitioned: false,
1113
- states: {},
1114
- emittedEvents: [],
1115
- error: message
1116
- };
1117
- }
1118
- }
1119
- /**
1120
- * The enqueued critical section behind `processOrbitalEvent`: evaluate
1121
- * one request through the composition, then relay its emits to
1122
- * cross-orbital listeners. MUST only run holding the event queue.
1123
- */
1124
- async evaluateForOrbital(registered, request, onPush, queueHold) {
1125
- const orbitalName = registered.schema.name;
1126
- const payloadRow = request.payload?.["row"];
1127
- const payloadRowAsPayload = payloadRow !== null && typeof payloadRow === "object" && !Array.isArray(payloadRow) ? payloadRow : void 0;
1128
- const payloadRowId = payloadRowAsPayload?.["id"];
1129
- renderLog.debug("processOrbitalEvent:enter", {
1130
- orbital: orbitalName,
1131
- event: request.event,
1132
- hasPayloadRow: payloadRowAsPayload !== void 0,
1133
- payloadRowId: typeof payloadRowId === "string" || typeof payloadRowId === "number" ? payloadRowId : void 0,
1134
- entityId: request.entityId
1135
- });
1136
- busLog.debug("bus:incoming", () => ({
1137
- orbital: orbitalName,
1138
- event: request.event,
1139
- payload: JSON.stringify(request.payload ?? null),
1140
- entityId: request.entityId,
1141
- traitStates: JSON.stringify(registered.traits.map((trait) => ({ traitName: trait.name, currentState: this.kernelEngine?.stateOf(trait.name) ?? "" })))
1142
- }));
1143
- xOrbitalLog.debug("processOrbitalEvent:enter", () => ({
1144
- orbital: orbitalName,
1145
- event: request.event,
1146
- traitsInOrbital: registered.traits.map((t) => t.name).join(","),
1147
- payloadActiveTraits: JSON.stringify(
1148
- request.payload?.["_activeTraits"] ?? null
1149
- )
1150
- }));
1151
- const viewer = normalizeUserContext(request.user) ?? this.config.defaultUser;
1152
- if (request.locale !== void 0 && this.appLocales.length > 0 && !this.appLocales.includes(request.locale)) {
1153
- throw new Error(`locale "${request.locale}" is not one of the program's declared locales [${this.appLocales.join(", ")}]`);
1154
- }
1155
- const isDelegatedLeg = (request.targetTrait !== void 0 || request.mount !== void 0) && (request.traits !== void 0 || request.entityByTrait !== void 0);
1156
- let serverRequest = request;
1157
- if (!isDelegatedLeg && request.targetTrait === void 0) {
1158
- const sidecarTarget = request.payload?.["_targetTrait"];
1159
- if (sidecarTarget !== void 0 || request.traits !== void 0 || request.entityByTrait !== void 0) {
1160
- serverRequest = { ...request, ...sidecarTarget !== void 0 ? { targetTrait: sidecarTarget } : {} };
1161
- delete serverRequest.traits;
1162
- delete serverRequest.entityByTrait;
1163
- }
1164
- }
1165
- const response = await evaluateOnKernel(
1166
- { engine: this.requireKernelEngine(), orbital: registered.schema.name, ...onPush !== void 0 ? { onStream: (item) => {
1167
- if (item.type === "event" || item.type === "effect") onPush(item);
1168
- } } : {}, outsideEventQueue: (work) => this.outsideEventQueue(queueHold, work) },
1169
- serverRequest,
1170
- viewer,
1171
- request.locale
1172
- );
1173
- this.publishKernelResponse(response, void 0, request.clientId);
1174
- return response;
1175
- }
1176
- /**
1177
- * Feed the host sinks from a kernel response: the kernel answers a dispatch whole, so each emit reaches the
1178
- * event bus (the public firehose) and `onPush` once the dispatch is done, in the order the response carries them.
1179
- */
1180
- publishKernelResponse(response, onPush, originClientId) {
1181
- for (const hop of response.transitions ?? []) {
1182
- this.transitionFanout.onTransition({ traitName: hop.trait, from: hop.from, to: hop.to, event: hop.event, ...hop.firedArm !== void 0 ? { firedArm: hop.firedArm } : {}, ...hop.guardResult !== void 0 ? { guardResult: hop.guardResult } : {}, effects: hop.effects });
1183
- }
1184
- for (const emitted of response.emittedEvents) {
1185
- this.eventBus.emit(emitted.event, emitted.payload, emitted.source, eventRouteKey(emitted.event, emitted.source?.eventId));
1186
- if (emitted.source?.persistSuccess === true) {
1187
- const { persistSuccess: _mark, ...source } = emitted.source;
1188
- this.publishLiveItem({ event: emitted.event, ...emitted.payload !== void 0 ? { payload: emitted.payload } : {}, source, ...originClientId !== void 0 ? { originClientId } : {}, target: "peers" });
1189
- }
1190
- onPush?.({ type: "event", data: emitted });
1191
- }
1192
- for (const effect of response.clientEffects ?? []) onPush?.({ type: "effect", data: effect });
1193
- }
1194
- /**
1195
- * The runtime-wide event queue — ONE async serialization point for every
1196
- * composition run (requests, cross-orbital relays, external emits, tick
1197
- * relays). The composition commits manager state directly rather than
1198
- * through the manager's per-(trait, entityId) actor queues, so without
1199
- * this, two concurrent requests against the session manager could both
1200
- * evaluate from the same held state and lose a transition. `run` sees
1201
- * the caller's result (or throw); the queue itself never wedges on a
1202
- * failure.
1203
- */
1204
- eventQueue = Promise.resolve();
1205
- /** This app's in-flight keyed `call-service`s, shared by every transition it runs (`cancel-call`). */
1206
- inFlightCalls = new InFlightCalls();
1207
- /** Wait for the queue, then hold it until the returned release is called. FIFO. */
1208
- acquireEventQueue() {
1209
- const previous = this.eventQueue;
1210
- let release = () => void 0;
1211
- const held = new Promise((resolve) => {
1212
- release = resolve;
1213
- });
1214
- this.eventQueue = previous.then(() => held);
1215
- return previous.then(() => release);
1216
- }
1217
- /**
1218
- * Hold the queue for `run`, handing it the hold so the work it reaches can
1219
- * release it (`outsideEventQueue`). The hold is passed explicitly rather than
1220
- * carried in async context: this runtime also runs in the browser (the orb
1221
- * website), which has no AsyncLocalStorage.
1222
- */
1223
- async enqueueEvent(run) {
1224
- const hold = { held: true, release: await this.acquireEventQueue() };
1225
- try {
1226
- return await run(hold);
1227
- } finally {
1228
- if (hold.held) hold.release();
1229
- }
1230
- }
1231
- /**
1232
- * Run `work` with this context's queue hold released, then re-acquire it (in
1233
- * FIFO order behind whatever queued meanwhile). Dispatches that land while
1234
- * released run as their own dispatches; their commits and the held run's
1235
- * later commit follow server-arrival last-write-wins. Outside a hold, `work`
1236
- * just runs.
1237
- */
1238
- async outsideEventQueue(hold, work) {
1239
- if (hold === void 0 || !hold.held) return work();
1240
- hold.held = false;
1241
- hold.release();
1242
- try {
1243
- return await work();
1244
- } finally {
1245
- hold.release = await this.acquireEventQueue();
1246
- hold.held = true;
1247
- }
1248
- }
1249
- // ==========================================================================
1250
- // Direct API (for programmatic use)
1251
- // ==========================================================================
1252
- /**
1253
- * Get the event bus for manual event emission
1254
- */
1255
- getEventBus() {
1256
- return this.eventBus;
1257
- }
1258
- /** Observe every executed hop (listener deliveries included) on every orbital; returns the unsubscribe. */
1259
- observeTransitions(observer) {
1260
- this.transitionObservers.add(observer);
1261
- return () => {
1262
- this.transitionObservers.delete(observer);
1263
- };
1264
- }
1265
- /**
1266
- * Get state for a specific orbital/trait
1267
- */
1268
- getState(orbitalName, traitName) {
1269
- const registered = this.orbitals.get(orbitalName);
1270
- if (!registered) return void 0;
1271
- const kernel = this.kernelEngine;
1272
- if (kernel === null) return void 0;
1273
- const stateOf = (name) => {
1274
- const currentState = kernel.stateOf(name);
1275
- return currentState === "" ? void 0 : { traitName: name, currentState, previousState: null, lastEvent: null, context: {} };
1276
- };
1277
- if (traitName) return stateOf(traitName);
1278
- const states = {};
1279
- for (const trait of registered.traits) {
1280
- const state = stateOf(trait.name);
1281
- if (state !== void 0) states[trait.name] = state;
1282
- }
1283
- return states;
1284
- }
1285
- /**
1286
- * List registered orbitals
1287
- */
1288
- listOrbitals() {
1289
- return Array.from(this.orbitals.keys());
1290
- }
1291
- /**
1292
- * Check if an orbital is registered
1293
- */
1294
- hasOrbital(name) {
1295
- return this.orbitals.has(name);
1296
- }
1297
- /**
1298
- * Get information about active ticks
1299
- */
1300
- getActiveTicks() {
1301
- return this.tickBindings.map((binding) => ({
1302
- orbital: binding.orbitalName,
1303
- trait: binding.traitName,
1304
- tick: binding.tick.name,
1305
- interval: binding.tick.interval,
1306
- hasGuard: !!binding.tick.guard
1307
- }));
1308
- }
1309
- /** Halt every registered tick's shared clock. Delegates to `TickScheduler.pause`. */
1310
- pauseTicks() {
1311
- this.tickScheduler.pause();
1312
- }
1313
- /** Resume ticks halted by `pauseTicks()`. Delegates to `TickScheduler.resume`. */
1314
- resumeTicks() {
1315
- this.tickScheduler.resume();
1316
- }
1317
- /** True while ticks are paused. Delegates to `TickScheduler.isPaused`. */
1318
- areTicksPaused() {
1319
- return this.tickScheduler.isPaused;
1320
- }
1321
- };
1322
- function createOrbitalServerRuntime(config) {
1323
- return new OrbitalServerRuntime(config);
1324
- }
1325
-
1326
- export { OrbitalServerRuntime, UnresolvedProgramError, createOrbitalServerRuntime };