@almadar/shell-node 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,844 @@
1
+ import { installNodeHostPorts, createNodeTokenPost } from './chunk-VQVAHUAX.js';
2
+ import { nativeMemoryOf } from './chunk-EEYGL7AB.js';
3
+ import { EventBus, bindDeclaredCollections, createSecretPorts, defaultCallService, kernelEventRequestParser, kernelOsTls, kernelOsCompletions, stampEmitSource, eventRouteKey, kernelOsGrants, KernelEngine, dispatchDeclaredInput, dispatchInbound, createSecretStore, evaluateOnKernel, InFlightCalls } from '@almadar/shell-runtime';
4
+ import { installKernelEvaluator, orbitalInlineEntities, asEventId, asTraitId, inlineTraitRefs, tickEventKey, toPayloadValue } from '@almadar/core';
5
+ import { createLogger } from '@almadar/logger';
6
+
7
+ var effectLog = createLogger("almadar:runtime:effects");
8
+ var busLog = createLogger("almadar:runtime:bus");
9
+ var renderLog = createLogger("almadar:runtime:render-ui");
10
+ var xOrbitalLog = createLogger("almadar:runtime:cross-orbital");
11
+ createLogger("almadar:runtime:persist");
12
+ var registerLog = createLogger("almadar:runtime:register");
13
+ var TICK_PASS_MS = 16;
14
+ function isNodeEnv() {
15
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
16
+ }
17
+ var UnresolvedProgramError = class extends Error {
18
+ constructor(schemaName) {
19
+ super(`program "${schemaName}" has unresolved imports and this host cannot run orb: register its resolved form (orb resolve)`);
20
+ this.name = "UnresolvedProgramError";
21
+ }
22
+ };
23
+ function needsResolving(schema) {
24
+ for (const orbital of schema.orbitals) {
25
+ for (const t of orbital.traits ?? []) {
26
+ if (typeof t === "object" && "ref" in t && t.ref.includes(".") && t._resolved === void 0) {
27
+ return true;
28
+ }
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ var OrbitalServerRuntime = class {
34
+ orbitals = /* @__PURE__ */ new Map();
35
+ eventBus;
36
+ config;
37
+ /** The row store a host-supplied `persistence` replaces: the kernel's own `MemoryStore`, opened by the first `register()`. */
38
+ ownedStore;
39
+ /**
40
+ * The bound persistence adapter (host-supplied, else the kernel's own memory store). Public so a test can
41
+ * inspect committed rows honestly. A runtime that owns its store has one from the first `register()`.
42
+ */
43
+ get persistence() {
44
+ const store = this.config.persistence ?? this.ownedStore;
45
+ if (store === void 0) throw new Error("OrbitalServerRuntime: the runtime opens its store when a program is registered; none is registered yet");
46
+ return store;
47
+ }
48
+ /** The kernel running the registered program; null before a program is registered. */
49
+ kernelEngine = null;
50
+ /** What the kernel holds of every trait of the registered program, read once when the kernel opens. */
51
+ programSurface;
52
+ /** The `os` handles of the program the kernel runs; closed when that program is replaced, reset or dropped. */
53
+ kernelOs;
54
+ /** Drives the kernel's one tick clock: every pass asks `KernelEngine.tickPass` which ticks are due. */
55
+ tickTimer = null;
56
+ programHandlersPromise = null;
57
+ resolvedSchema = null;
58
+ /** The folder the program's lazy files sit beside; the pages that name them are on `resolvedSchema.lazyPages`. */
59
+ lazyDir;
60
+ /** Wired by the hosting server (e.g. the playground SSE endpoint) via `setLiveBroadcastSink`. */
61
+ liveSubscribers = /* @__PURE__ */ new Set();
62
+ liveBroadcastSink = null;
63
+ hostDispatchSink = null;
64
+ /** The one-shot timer that flushes the kernel's coalesced tick relay window. */
65
+ tickRelayTimer = null;
66
+ constructor(config) {
67
+ installNodeHostPorts();
68
+ this.config = { ...config };
69
+ this.eventBus = new EventBus();
70
+ }
71
+ /**
72
+ * Lazily wire the program host handlers (behavior/* and program/* effects),
73
+ * merging them UNDER any user-provided handlers. Only when `programHost`
74
+ * is configured; otherwise the effects fail explicitly.
75
+ */
76
+ async ensureProgramHandlers() {
77
+ if (!isNodeEnv()) return;
78
+ const host = this.config.programHost;
79
+ if (host === void 0) return;
80
+ if (!this.programHandlersPromise) {
81
+ this.programHandlersPromise = (async () => {
82
+ const { createProgramHandlers } = await import('./createProgramHandlers.js');
83
+ this.config.effectHandlers = {
84
+ ...createProgramHandlers(host),
85
+ ...this.config.effectHandlers
86
+ };
87
+ })();
88
+ }
89
+ return this.programHandlersPromise;
90
+ }
91
+ // ==========================================================================
92
+ // Schema Registration
93
+ // ==========================================================================
94
+ /**
95
+ * Register an OrbitalSchema for execution.
96
+ *
97
+ * Auto-preprocesses the schema when it contains `uses` declarations or
98
+ * unresolved cross-orbital trait references (e.g. a trait with
99
+ * `ref: "Modal.traits.ModalRecordModal"` and no inline `stateMachine`).
100
+ * Without preprocessing, those refs arrive empty at the state machine and
101
+ * button clicks silently do nothing — see Phase 9.5.H.
102
+ *
103
+ * Preprocessing needs a loader. If `loaderConfig` is set, that loader is
104
+ * used. Otherwise, a default loader is constructed that points at
105
+ * `<cwd>` (for `basePath`) and the nearest `node_modules/@almadar/std` (for
106
+ * `stdLibPath`), which matches how every caller in this monorepo has the
107
+ * std registry on disk.
108
+ */
109
+ /**
110
+ * The in-flight (or last-completed) app mutation — `register()` chains its
111
+ * body onto this, and `unregisterAll()` stamps a marker after clearing.
112
+ * `processOrbitalEvent` consults it when the requested orbital is missing:
113
+ * during a catalog-style app swap the new schema's registration is already
114
+ * in flight while a client's first events arrive, and answering those with
115
+ * an immediate `Orbital not found` 404 (the pre-fix behavior) races the
116
+ * swap — big multi-orbital organisms lost their mount INITs intermittently
117
+ * while single-orbital ones almost always won. Awaiting the epoch and
118
+ * re-reading the map turns the race into a brief wait. `BrowserPlayground`
119
+ * gated dispatches on registration ad hoc for the same race; the epoch is
120
+ * the canonical, host-wide fix.
121
+ */
122
+ registrationEpoch = null;
123
+ /**
124
+ * Reentrancy guard for `withRegistrationMutation`. While a bracket is
125
+ * open, `register()`/`unregisterAll()` must NOT chain/stamp the epoch:
126
+ * the callback's sequential awaits already order them, and chaining a
127
+ * `register()` awaited by the callback onto the bracket's gate would
128
+ * deadlock (callback → register → epoch → gate → callback's finally).
129
+ */
130
+ registrationMutationDepth = 0;
131
+ async register(schema, options = {}) {
132
+ if (this.registrationMutationDepth > 0) {
133
+ await this.doRegister(schema, options);
134
+ return;
135
+ }
136
+ const previous = this.registrationEpoch ?? Promise.resolve();
137
+ const epoch = previous.then(
138
+ () => this.doRegister(schema, options),
139
+ () => this.doRegister(schema, options)
140
+ );
141
+ this.registrationEpoch = epoch;
142
+ await epoch;
143
+ }
144
+ async doRegister(schema, options) {
145
+ const kernel = await this.kernelModule();
146
+ installKernelEvaluator(kernel);
147
+ this.openOwnedStore(kernel);
148
+ if (this.config.debug) {
149
+ registerLog.debug("register:schema", { name: schema.name });
150
+ }
151
+ if (needsResolving(schema)) schema = await this.resolveWithOrb(schema);
152
+ this.lazyDir = options.lazyDir;
153
+ if ((schema.lazyPages ?? []).length > 0 && this.lazyDir === void 0) {
154
+ throw new Error(`OrbitalServerRuntime: program "${schema.name}" lists lazy pages and no folder their files sit beside was given; register it from its file`);
155
+ }
156
+ for (const orbital of schema.orbitals) {
157
+ await this.registerOrbitalAsync(orbital);
158
+ }
159
+ await this.openKernel(schema);
160
+ this.resolvedSchema = schema;
161
+ }
162
+ /** The kernel's own row store, opened once in the register phase when the host supplied none. */
163
+ openOwnedStore(kernel) {
164
+ if (this.config.persistence !== void 0 || this.ownedStore !== void 0) return;
165
+ this.ownedStore = nativeMemoryOf(kernel);
166
+ }
167
+ /**
168
+ * One-call entry point: read an `.orb` file from disk, parse it, preprocess
169
+ * cross-orbital imports, and register the result. Callers never touch raw
170
+ * `.orb` bytes — `register()` handles preprocessing internally.
171
+ *
172
+ * Node only. Browsers must receive already-resolved schemas from their
173
+ * server (see `getResolvedSchema()`).
174
+ */
175
+ async registerFromFile(path) {
176
+ if (typeof process === "undefined" || !process.versions?.node) {
177
+ throw new Error(
178
+ "registerFromFile is Node-only. Browsers should receive resolved schemas from their server."
179
+ );
180
+ }
181
+ const { readFile } = await import('fs/promises');
182
+ const raw = await readFile(path, "utf-8");
183
+ let schema;
184
+ try {
185
+ schema = JSON.parse(raw);
186
+ } catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ throw new Error(`registerFromFile: ${path} is not valid JSON: ${msg}`);
189
+ }
190
+ const { dirname } = await import('path');
191
+ await this.register(schema, { lazyDir: dirname(path) });
192
+ }
193
+ /** A program with imports resolved by `orb` from the configured base path (Node only: the one resolver is the compiler). */
194
+ async resolveWithOrb(schema) {
195
+ if (!isNodeEnv()) throw new UnresolvedProgramError(schema.name);
196
+ const { programResolveSchema } = await import('./program-6DB6LK5B.js');
197
+ const cwd = this.programRoot();
198
+ if (this.config.debug) registerLog.debug("register:resolve-with-orb", { name: schema.name, cwd });
199
+ return programResolveSchema(schema, { cwd, ...this.config.programHost?.orbBin !== void 0 ? { orbBin: this.config.programHost.orbBin } : {} });
200
+ }
201
+ /**
202
+ * Register a single orbital
203
+ */
204
+ async registerOrbitalAsync(orbital) {
205
+ this.orbitals.set(orbital.name, { schema: orbital });
206
+ bindDeclaredCollections(this.persistence, orbitalInlineEntities(orbital));
207
+ if (this.config.debug) {
208
+ registerLog.debug("register:orbital", {
209
+ name: orbital.name,
210
+ traitCount: (orbital.traits || []).length
211
+ });
212
+ }
213
+ }
214
+ /** The adapters the kernel's I/O is answered with: this runtime's persistence, services and program host. */
215
+ secretPorts() {
216
+ const options = this.config.secretPorts;
217
+ if (options === void 0) return void 0;
218
+ const kernel = this.config.kernel;
219
+ if (kernel === void 0 || options.tokenPost !== void 0) return createSecretPorts(options);
220
+ const tokenPost = createNodeTokenPost({ egressAllows: (url) => this.requireKernelEngine().egressAllows(url), isPublicAddress: (ip) => kernel.isPublicAddress(ip) });
221
+ return createSecretPorts({ tokenPost, ...options });
222
+ }
223
+ kernelPorts() {
224
+ const secretPorts = this.secretPorts();
225
+ return {
226
+ ...secretPorts !== void 0 ? { secretPorts } : {},
227
+ ...this.config.clock !== void 0 ? { now: this.config.clock } : {},
228
+ ...this.kernelOs !== void 0 ? { os: this.kernelOs } : {},
229
+ ...this.kernelOs !== void 0 && this.config.kernel !== void 0 ? { osCompletions: kernelOsCompletions(this.config.kernel) } : {},
230
+ ...this.kernelOs !== void 0 ? { osTls: kernelOsTls(() => this.requireKernelEngine()) } : {},
231
+ ...this.config.kernel !== void 0 ? { parseEventRequest: kernelEventRequestParser(this.config.kernel) } : {},
232
+ persistence: this.persistence,
233
+ inFlightCalls: this.inFlightCalls,
234
+ callService: (service, action, params, context) => {
235
+ const provider = this.config.effectHandlers?.callService;
236
+ if (provider !== void 0) return provider(service, action, params, context);
237
+ return defaultCallService(service, action, params, context);
238
+ },
239
+ programEffect: () => this.config.effectHandlers?.programEffect,
240
+ substrateStream: (op, args) => {
241
+ if (op !== "program/live") return void 0;
242
+ const first = args[0];
243
+ const options = first !== null && typeof first === "object" && !Array.isArray(first) ? first : {};
244
+ const clientId = typeof options["clientId"] === "string" ? options["clientId"] : void 0;
245
+ return {
246
+ subscribe: (message) => {
247
+ const engine = this.requireKernelEngine();
248
+ const stream = clientId !== void 0 ? engine.openSessionStream(clientId) : void 0;
249
+ const receive = (item) => {
250
+ if (engine.liveItemDelivered(item, clientId)) message(toPayloadValue({ event: item.event, ...item.payload !== void 0 ? { payload: item.payload } : {}, source: item.source }));
251
+ };
252
+ this.liveSubscribers.add(receive);
253
+ return () => {
254
+ this.liveSubscribers.delete(receive);
255
+ if (stream !== void 0) engine.closeSessionStream(stream);
256
+ };
257
+ }
258
+ };
259
+ },
260
+ servicePorts: (caller, user, message, assertReentry) => this.servicePorts(caller, user, message, assertReentry),
261
+ deliverLive: (site, payload, originClientId) => this.deliverLiveMessage(site, payload, originClientId),
262
+ deferCallOutcome: () => this.hostDispatchSink === null ? void 0 : (call) => this.runDeferredOutcome(call.orbital, {
263
+ event: call.event,
264
+ ...call.payload !== void 0 ? { payload: call.payload } : {},
265
+ targetTrait: call.traitName,
266
+ ...call.entityId !== void 0 ? { entityId: call.entityId } : {},
267
+ ...call.originClientId !== void 0 ? { clientId: call.originClientId } : {},
268
+ ...call.locale !== void 0 ? { locale: call.locale } : {}
269
+ })
270
+ };
271
+ }
272
+ /** One live message of a running `call-service`, stamped as its calling trait, to the requesting tab only. */
273
+ deliverLiveMessage(site, payload, originClientId) {
274
+ const registered = this.orbitals.get(site.orbital);
275
+ const callingTrait = this.traitsOf(site.orbital).find((t) => t.name === site.trait);
276
+ const stamp = stampEmitSource(void 0, {
277
+ orbitalName: site.orbital,
278
+ traitName: site.trait,
279
+ orbitalId: registered?.schema.id,
280
+ traitId: callingTrait?.traitId === void 0 ? void 0 : asTraitId(callingTrait.traitId),
281
+ emitContractEventId: ((eventId) => eventId === void 0 ? void 0 : asEventId(eventId))(callingTrait?.emits.find((e) => e.event === site.event)?.eventId),
282
+ originClientId
283
+ });
284
+ this.eventBus.emit(site.event, payload, stamp, eventRouteKey(site.event, stamp.eventId));
285
+ this.publishLiveItem({ event: site.event, payload, source: stamp, originClientId, target: "origin" });
286
+ }
287
+ /** The traits of one orbital as the kernel holds them. */
288
+ traitsOf(orbital) {
289
+ return (this.programSurface?.traits ?? []).filter((trait) => trait.orbital === orbital);
290
+ }
291
+ /** The project root this runtime hands `orb` and the kernel's language data: the program host's cwd, else the loader base path. */
292
+ programRoot() {
293
+ return this.config.programHost?.cwd ?? this.config.loaderConfig?.basePath ?? process.cwd();
294
+ }
295
+ /** The kernel module this runtime runs on: the one the host handed in, else the installed `@almadar/kernel`. */
296
+ async kernelModule() {
297
+ if (this.config.kernel !== void 0) return this.config.kernel;
298
+ const { loadInstalledKernel } = await import('./kernel-loader-3JR6LJV5.js');
299
+ this.config.kernel = await loadInstalledKernel(this.programRoot());
300
+ return this.config.kernel;
301
+ }
302
+ /** (Re)open the kernel on the program just registered. */
303
+ async openKernel(registered) {
304
+ const kernel = await this.kernelModule();
305
+ const schema = { ...registered, orbitals: inlineTraitRefs(registered.orbitals) };
306
+ for (const orbital of schema.orbitals) {
307
+ for (const trait of orbital.traits ?? []) {
308
+ if (typeof trait === "object" && "ref" in trait && !("stateMachine" in trait)) {
309
+ throw new Error(`OrbitalServerRuntime: the kernel takes an orb-resolved program, but trait "${trait.ref}" of orbital "${orbital.name}" is still a reference`);
310
+ }
311
+ }
312
+ }
313
+ this.inFlightCalls.cancelAll();
314
+ this.kernelEngine?.dispose();
315
+ this.kernelOs?.close();
316
+ this.kernelOs = this.config.osHost?.(kernelOsGrants(kernel, () => this.requireKernelEngine()));
317
+ this.kernelEngine = KernelEngine.open(kernel, schema, this.kernelPorts(), this.lazyDir);
318
+ this.kernelEngine.setStateful();
319
+ this.programSurface = this.kernelEngine.programSurface();
320
+ if (this.config.tickRelayIntervalMs !== void 0) this.kernelEngine.setTickRelayInterval(this.config.tickRelayIntervalMs);
321
+ this.restartTickClock();
322
+ await this.mountBootEndpoints();
323
+ }
324
+ /** Run the kernel's boot mounts for the server side (EXEC-ENDPOINT-3): the host sends what the kernel lists and chooses nothing. */
325
+ async mountBootEndpoints() {
326
+ const owners = /* @__PURE__ */ new Map();
327
+ for (const trait of this.programSurface?.traits ?? []) {
328
+ const registered = this.orbitals.get(trait.orbital);
329
+ if (registered !== void 0) owners.set(trait.name, registered);
330
+ }
331
+ const batches = /* @__PURE__ */ new Map();
332
+ for (const seed of this.requireKernelEngine().bootMounts("server")) {
333
+ const owner = owners.get(seed.trait);
334
+ if (owner === void 0) continue;
335
+ batches.set(owner, [...batches.get(owner) ?? [], seed]);
336
+ }
337
+ for (const [registered, mount] of batches) {
338
+ const first = mount[0];
339
+ if (first === void 0) continue;
340
+ const response = await this.evaluateForOrbital(registered, { event: first.event, mount, payload: {} });
341
+ if (!response.success) {
342
+ throw new Error(`OrbitalServerRuntime: the boot mount of orbital "${registered.schema.name}" failed: ${response.error ?? "the kernel refused it"}`);
343
+ }
344
+ }
345
+ }
346
+ /** What answers the declared contract: the registered program's kernel, else the kernel's own program-less answers, which refuse every input and hook. */
347
+ async contractEngine() {
348
+ return this.kernelEngine ?? KernelEngine.withoutProgram(await this.kernelModule());
349
+ }
350
+ requireKernelEngine() {
351
+ if (this.kernelEngine === null) throw new Error("OrbitalServerRuntime: an os ask arrived with no kernel program registered");
352
+ return this.kernelEngine;
353
+ }
354
+ /** Ask the kernel's clock which ticks are due and start each as a `runTick` request to the orbital it names. */
355
+ tickPass() {
356
+ const engine = this.kernelEngine;
357
+ if (engine === null) return;
358
+ for (const [orbital, ticks] of Object.entries(engine.tickPass(performance.now()))) {
359
+ for (const due of ticks) void this.executeTick(orbital, due.trait, due.tick);
360
+ }
361
+ }
362
+ /** Run the kernel's clock on this host's timer when the program declares a tick. */
363
+ restartTickClock() {
364
+ this.cleanupTicks();
365
+ if (this.kernelEngine?.declaresTicks() !== true) return;
366
+ this.tickTimer = setInterval(() => this.tickPass(), TICK_PASS_MS);
367
+ }
368
+ /** A tick: the host owns the timer, the kernel decides whether and over which rows it fires. */
369
+ async executeTick(orbitalName, traitName, tickName) {
370
+ const response = await this.processOrbitalEvent(orbitalName, {
371
+ event: tickEventKey(tickName),
372
+ runTick: { trait: traitName, tick: tickName },
373
+ targetTrait: traitName
374
+ });
375
+ if (!response.success) {
376
+ effectLog.error("tick:execute-error", { tick: tickName, error: response.error ?? "the kernel refused the tick" });
377
+ }
378
+ }
379
+ /**
380
+ * Stop the host's timers
381
+ */
382
+ cleanupTicks() {
383
+ if (this.tickTimer !== null) clearInterval(this.tickTimer);
384
+ this.tickTimer = null;
385
+ if (this.tickRelayTimer !== null) clearTimeout(this.tickRelayTimer);
386
+ this.tickRelayTimer = null;
387
+ }
388
+ /**
389
+ * Unregister all orbitals and clean up
390
+ */
391
+ unregisterAll() {
392
+ this.cleanupTicks();
393
+ this.orbitals.clear();
394
+ this.inFlightCalls.cancelAll();
395
+ this.kernelEngine?.dispose();
396
+ this.kernelEngine = null;
397
+ this.programSurface = void 0;
398
+ this.kernelOs?.close();
399
+ this.kernelOs = void 0;
400
+ this.eventBus.clear();
401
+ this.programHandlersPromise = null;
402
+ if (this.registrationMutationDepth === 0) {
403
+ this.registrationEpoch = (this.registrationEpoch ?? Promise.resolve()).then(() => void 0, () => void 0);
404
+ }
405
+ }
406
+ /**
407
+ * Bracket a multi-step app mutation (the classic swap is
408
+ * `unregisterAll()` → resolve next schema → `register()`) as ONE
409
+ * registration epoch. `register()` alone chains only its own body onto
410
+ * the epoch, so between `unregisterAll()` and the `register()` call the
411
+ * runtime's map is empty while the epoch marker has already resolved —
412
+ * any `processOrbitalEvent` that lands in that gap (the resolve of a big
413
+ * organism takes real time) 404s `Orbital not found` even though the
414
+ * requested orbital is milliseconds away from landing. Inside this
415
+ * bracket the epoch stays pending for the WHOLE callback, so mid-swap
416
+ * dispatches await the completed swap and then resolve against the new
417
+ * app. A failed callback still releases the epoch (chained on both
418
+ * branches), so a failed swap falls through to the honest 404 instead of
419
+ * wedging the queue. Callbacks SERIALIZE: each bracket awaits the previous
420
+ * epoch before running `fn`, so two hosts swapping at once (a catalog
421
+ * select racing a preview remount's `/register`) cannot interleave their
422
+ * unregister/register sequences into a mixed registry. A bracket opened
423
+ * from INSIDE another bracket's callback runs `fn` directly — detected
424
+ * via the AsyncLocalStorage context below (a depth counter alone can't
425
+ * tell a true nest from a concurrent caller) — because the outer
426
+ * callback's awaits already serialize it and queueing on the outer gate
427
+ * would deadlock. Deadlock-free otherwise: `doRegister` never dispatches
428
+ * events, so awaiting dispatches never block registration progress.
429
+ */
430
+ /**
431
+ * Reentrancy context for `withRegistrationMutation`. A depth counter alone
432
+ * cannot distinguish "called from inside a bracket callback" (must run
433
+ * directly — queueing on the outer gate would deadlock) from "called
434
+ * concurrently while another bracket is in flight" (must queue — running
435
+ * directly would interleave two swaps). The ALS marks the callback's async
436
+ * context, which separates the two exactly.
437
+ */
438
+ bracketAls = null;
439
+ ensureBracketAls() {
440
+ this.bracketAls ??= import('async_hooks').then(
441
+ ({ AsyncLocalStorage }) => new AsyncLocalStorage()
442
+ );
443
+ return this.bracketAls;
444
+ }
445
+ async withRegistrationMutation(fn) {
446
+ const previous = this.registrationEpoch ?? Promise.resolve();
447
+ let release = () => void 0;
448
+ const gate = new Promise((resolve) => {
449
+ release = resolve;
450
+ });
451
+ this.registrationEpoch = previous.then(() => gate, () => gate);
452
+ this.registrationMutationDepth++;
453
+ try {
454
+ const als = await this.ensureBracketAls();
455
+ if (als.getStore() !== void 0) {
456
+ release();
457
+ return await fn();
458
+ }
459
+ await previous.catch(() => void 0);
460
+ const nestingToken = /* @__PURE__ */ Symbol("withRegistrationMutation");
461
+ return await als.run(nestingToken, fn);
462
+ } finally {
463
+ this.registrationMutationDepth--;
464
+ release();
465
+ }
466
+ }
467
+ /**
468
+ * Return every registered orbital's circuit to boot: trait states to their
469
+ * initial states, frames and row cache emptied, and a fresh mount (no trait
470
+ * awaiting INIT, nothing held).
471
+ */
472
+ resetCircuitState() {
473
+ this.inFlightCalls.cancelAll();
474
+ this.kernelEngine?.reset();
475
+ this.kernelOs?.close();
476
+ }
477
+ /**
478
+ * Wire a live-broadcast sink. Called only from a `persist` effect's
479
+ * `emit:{success}` envelope firing site (batch and single-op) — see
480
+ * `docs/Almadar_Live_Push.md`. The hosting server (e.g. the playground's
481
+ * `/api/events` SSE endpoint) uses this to fan persist mutations out to
482
+ * every OTHER connected client; a fresh call replaces the previous sink.
483
+ */
484
+ setLiveBroadcastSink(sink) {
485
+ this.liveBroadcastSink = sink;
486
+ }
487
+ publishLiveItem(item) {
488
+ this.liveBroadcastSink?.(item);
489
+ for (const receive of this.liveSubscribers) receive(item);
490
+ }
491
+ /** Wire where a host-run dispatch's result goes (see `HostDispatchItem`); a fresh call replaces the previous sink. */
492
+ setHostDispatchSink(sink) {
493
+ this.hostDispatchSink = sink;
494
+ }
495
+ /** Run a follow-on step's call outcome as its own dispatch, and hand its result to the host-dispatch sink. */
496
+ runDeferredOutcome(orbitalName, request) {
497
+ void this.processOrbitalEvent(orbitalName, request).then(
498
+ (response) => this.hostDispatchSink?.({ orbitalName, request, response, ...request.clientId !== void 0 ? { originClientId: request.clientId } : {} }),
499
+ (error) => effectLog.error("deferred-outcome:failed", { orbitalName, event: request.event, trait: request.targetTrait, error: error.message })
500
+ );
501
+ }
502
+ /**
503
+ * A tick-stamped dispatch is a latest-state broadcast (T6): the kernel coalesces it with the others of the window
504
+ * (newest per origin client, orbital and event) and says how long the window stays open; this host only runs
505
+ * the timer.
506
+ */
507
+ queueTickRelay(orbitalName, request) {
508
+ const engine = this.requireKernelEngine();
509
+ const window = engine.relayTick(orbitalName, request);
510
+ if (window === void 0) return;
511
+ const timer = setTimeout(() => this.flushTickRelay(), window);
512
+ if (typeof timer === "object" && typeof timer.unref === "function") timer.unref();
513
+ this.tickRelayTimer = timer;
514
+ }
515
+ flushTickRelay() {
516
+ this.tickRelayTimer = null;
517
+ const engine = this.kernelEngine;
518
+ if (engine === null) return;
519
+ for (const item of engine.flushTickRelay()) this.publishLiveItem({ ...item, target: "peers" });
520
+ }
521
+ /**
522
+ * `reference` resolved for the identity bound to `clientId`'s session (none: the anonymous viewer) against the store as it is now: the rows that still match its query and that identity's `@read`, in the reference's order, `total` re-counted. The kernel resolves through its server
523
+ * resolve flight. A client reaches it with
524
+ * `POST /:orbital/../resolve-rows`.
525
+ */
526
+ async resolveRows(reference, clientId) {
527
+ const engine = this.kernelEngine;
528
+ const owner = engine === null ? void 0 : engine.entityOwnerOrbital(reference.entityType);
529
+ if (engine === null || owner === void 0) throw new Error(`${reference.entityType}: no registered orbital declares this entity`);
530
+ return engine.resolveRows(reference, { orbital: owner, ...clientId !== void 0 ? { originClientId: clientId } : {} });
531
+ }
532
+ // ==========================================================================
533
+ // Event Processing
534
+ // ==========================================================================
535
+ /**
536
+ * Process an event for an orbital — the stateful host's thin shell over
537
+ * the unified `evaluateOrbitalEvent` composition (design:
538
+ * docs/Almadar_Runtime_Stateless_Stateful_PLAN.md §4.1). This method owns
539
+ * ONLY session concerns: orbital lookup, viewer normalization, the lazy
540
+ * OS/substrate handler wiring, the per-request relay mask
541
+ * (G-RUNTIME-031 — the client's mounted set as DATA, replacing the
542
+ * deleted blanket `originClientId` skip), the tick snapshot relay, and
543
+ * the cross-orbital listens relay (a listener registered under a
544
+ * DIFFERENT orbital than the emitter — the per-orbital composition's
545
+ * in-band fan-out covers the rest). Target resolution, payload
546
+ * validation, the cascade, the fan-out, structured rejections, and
547
+ * response assembly are the composition's, identical to the stateless
548
+ * path.
549
+ *
550
+ * Serialization: the whole evaluation runs on ONE runtime-wide async
551
+ * queue. The composition commits manager state directly (no per-trait
552
+ * actor queue), so concurrent requests against the long-lived session
553
+ * manager would otherwise both evaluate from the same held state and
554
+ * lose a transition. Coarser than the old per-(trait, entityId) actor
555
+ * queues — a deliberate W4 tradeoff, recorded in the plan doc.
556
+ *
557
+ * @param onPush - Optional incremental-push callback. Called immediately
558
+ * when an event is emitted or a client effect fires, before the promise
559
+ * resolves. Used by the SSE streaming path to write items to the
560
+ * response as they arrive rather than buffering and flushing at the end.
561
+ */
562
+ /**
563
+ * Read an orbital by name, awaiting an in-flight registration mutation
564
+ * first when the lookup misses (G-RUNTIME-038): mid-swap lookups 404ed
565
+ * against the emptied map even though the requested orbital was
566
+ * milliseconds from landing. A failed mutation falls through to the miss.
567
+ */
568
+ async resolveRegisteredOrbital(orbitalName) {
569
+ let registered = this.orbitals.get(orbitalName);
570
+ if (!registered && this.registrationEpoch !== null) {
571
+ await this.registrationEpoch.catch(() => void 0);
572
+ registered = this.orbitals.get(orbitalName);
573
+ }
574
+ if (!registered) {
575
+ await this.loadLazyOrbital(orbitalName);
576
+ registered = this.orbitals.get(orbitalName);
577
+ }
578
+ return registered;
579
+ }
580
+ /** The first request that names a `uses lazy` orbital registers it here; the kernel loads the same file on that request's dispatch (EXEC-LOAD-2). */
581
+ async loadLazyOrbital(orbitalName) {
582
+ const dir = this.lazyDir;
583
+ const pages = (this.resolvedSchema?.lazyPages ?? []).filter((page) => page.orbital === orbitalName);
584
+ const first = pages[0];
585
+ if (dir === void 0 || first === void 0) return;
586
+ const { readFile } = await import('fs/promises');
587
+ const { join } = await import('path');
588
+ const lazyPath = join(dir, first.orbRef);
589
+ let lazySchema;
590
+ try {
591
+ lazySchema = JSON.parse(await readFile(lazyPath, "utf-8"));
592
+ } catch (err) {
593
+ const msg = err instanceof Error ? err.message : String(err);
594
+ throw new Error(`lazy orbital "${orbitalName}": ${first.orbRef} could not be read: ${msg}`);
595
+ }
596
+ if (!lazySchema.orbitals.some((o) => o.name === orbitalName)) {
597
+ throw new Error(`lazy orbital "${orbitalName}" is not in ${first.orbRef}, found: ${lazySchema.orbitals.map((o) => o.name).join(", ")}`);
598
+ }
599
+ for (const orbital of lazySchema.orbitals) {
600
+ await this.registerOrbitalAsync(orbital);
601
+ }
602
+ }
603
+ /** The program's declared external inputs: what an outside client may send which trait. */
604
+ listExternalInputs() {
605
+ return this.kernelEngine?.externalInputs() ?? [];
606
+ }
607
+ /**
608
+ * The outside-client input channel: an API caller, an agent acting as the
609
+ * user, an MCP client. Only a declared external input of the target trait is
610
+ * dispatched — through `processOrbitalEvent`, so guards, entity policies and
611
+ * cascades apply exactly as for a click. Anything else is refused with
612
+ * `not-an-external-input`. The app's own UI keeps using `processOrbitalEvent`.
613
+ */
614
+ /**
615
+ * The running app lent to one `call-service` provider as the caller: the
616
+ * declared inputs, the input channel as `user`, and the rows `user` may read
617
+ * (the entity's `@read` policy, the same rule a `fetch` effect applies).
618
+ */
619
+ servicePorts(caller, user, message = () => {
620
+ }, assertReentry) {
621
+ return {
622
+ caller,
623
+ message,
624
+ inputs: () => this.listExternalInputs(),
625
+ readableEntities: () => this.kernelEngine?.readableEntities() ?? [],
626
+ dispatchInput: (orbital, request) => {
627
+ assertReentry?.();
628
+ return this.dispatchExternalInput(orbital, request);
629
+ },
630
+ read: async (entity) => {
631
+ return this.requireKernelEngine().readRows(entity, await this.persistence.list(entity), user);
632
+ },
633
+ supportsDispatchStream: true,
634
+ dispatchRequest: (orbital, request, input, stream) => {
635
+ assertReentry?.();
636
+ return this.dispatchEventRequest(orbital, request, input, stream);
637
+ },
638
+ deliverInbound: (name, delivery) => this.deliverInbound(name, delivery),
639
+ resolveRows: (reference, clientId) => this.resolveRows(reference, clientId)
640
+ };
641
+ }
642
+ /**
643
+ * A `program/dispatch` event request, as the events route (or with `input`, the inputs route) runs it: the
644
+ * runs as the session's identity, never a viewer the request names. An unknown orbital is `ORBITAL_NOT_FOUND`, an undeclared
645
+ * locale `LOCALE_NOT_DECLARED`; an inbound-only event and an undeclared input are the routes' refusals.
646
+ */
647
+ async dispatchEventRequest(orbital, parsed, input, stream = false) {
648
+ const request = parsed;
649
+ const streamItems = [];
650
+ const onPush = stream ? (item) => {
651
+ streamItems.push({ ...item, timestamp: item.timestamp ?? Date.now() });
652
+ } : void 0;
653
+ const answer = (response2) => ({
654
+ ok: true,
655
+ response: response2,
656
+ ...stream ? { streamItems: [...streamItems, { type: "complete", data: response2, timestamp: Date.now() }] } : {}
657
+ });
658
+ if (await this.resolveRegisteredOrbital(orbital) === void 0) {
659
+ return { ok: false, error: `Orbital not found: ${orbital}`, code: "ORBITAL_NOT_FOUND", status: 404 };
660
+ }
661
+ if (input) {
662
+ if (request.targetTrait === void 0) {
663
+ return { ok: false, error: "program/dispatch: an input request names its trait", code: "NO_TRAIT", status: 400 };
664
+ }
665
+ const response2 = await this.dispatchExternalInput(orbital, {
666
+ targetTrait: request.targetTrait,
667
+ event: request.event,
668
+ ...request.payload !== void 0 ? { payload: request.payload } : {},
669
+ ...request.entityId !== void 0 ? { entityId: request.entityId } : {}
670
+ }, onPush);
671
+ return answer(response2);
672
+ }
673
+ const refusal = this.refuseInboundOnly(orbital, request);
674
+ if (refusal !== void 0) return answer(refusal);
675
+ const response = await this.processOrbitalEvent(orbital, request, onPush);
676
+ if (response.rejections?.[0]?.code === "locale-not-declared") {
677
+ return { ok: false, error: response.error ?? "the locale is not declared", code: "LOCALE_NOT_DECLARED", status: 400 };
678
+ }
679
+ return answer(response);
680
+ }
681
+ async dispatchExternalInput(orbitalName, request, onPush) {
682
+ return dispatchDeclaredInput(await this.contractEngine(), orbitalName, request, (orbital, req) => this.processOrbitalEvent(orbital, req, onPush));
683
+ }
684
+ /**
685
+ * An inbound delivery (`/api/hooks/<name>`): the runtime decides whether the name is served, runs
686
+ * the handler through `processOrbitalEvent` and answers; the host only carries the bytes.
687
+ */
688
+ /**
689
+ * An event that is an inbound hook's is delivered only by `deliverInbound`: the app's own event
690
+ * route refuses it, as the input channel refuses an event that is no declared input.
691
+ */
692
+ refuseInboundOnly(orbitalName, request) {
693
+ const only = this.kernelEngine?.inboundOnly(orbitalName, request.targetTrait, request.event);
694
+ if (only === void 0) return void 0;
695
+ const { trait } = only;
696
+ return {
697
+ success: false,
698
+ transitioned: false,
699
+ states: {},
700
+ emittedEvents: [],
701
+ error: only.message,
702
+ rejections: [{ code: "inbound-only-event", trait, event: request.event }]
703
+ };
704
+ }
705
+ async deliverInbound(name, delivery, onPush) {
706
+ return dispatchInbound(await this.contractEngine(), name, delivery, (orbital, req) => this.processOrbitalEvent(orbital, req, onPush), (credential) => (this.secretPorts()?.secrets ?? createSecretStore()).get(credential, void 0));
707
+ }
708
+ async processOrbitalEvent(orbitalName, request, onPush) {
709
+ try {
710
+ await this.ensureProgramHandlers();
711
+ const registered = await this.resolveRegisteredOrbital(orbitalName);
712
+ if (!registered) {
713
+ return {
714
+ success: false,
715
+ transitioned: false,
716
+ states: {},
717
+ emittedEvents: [],
718
+ error: `Orbital not found: ${orbitalName}`
719
+ };
720
+ }
721
+ const response = await this.evaluateForOrbital(registered, request, onPush);
722
+ if (request.tick !== void 0 && this.liveBroadcastSink !== null) {
723
+ this.queueTickRelay(orbitalName, request);
724
+ }
725
+ return response;
726
+ } catch (error) {
727
+ const message = error instanceof Error ? error.message : String(error);
728
+ xOrbitalLog.error("processOrbitalEvent:error", {
729
+ orbital: orbitalName,
730
+ event: request.event,
731
+ entityId: request.entityId,
732
+ error: message
733
+ });
734
+ return {
735
+ success: false,
736
+ transitioned: false,
737
+ states: {},
738
+ emittedEvents: [],
739
+ error: message
740
+ };
741
+ }
742
+ }
743
+ /**
744
+ * One request evaluated on the kernel, which orders it among every other (EXEC-CONC-2), then its emits relayed
745
+ * to the host sinks.
746
+ */
747
+ async evaluateForOrbital(registered, request, onPush) {
748
+ const orbitalName = registered.schema.name;
749
+ const payloadRow = request.payload?.["row"];
750
+ const payloadRowAsPayload = payloadRow !== null && typeof payloadRow === "object" && !Array.isArray(payloadRow) ? payloadRow : void 0;
751
+ const payloadRowId = payloadRowAsPayload?.["id"];
752
+ renderLog.debug("processOrbitalEvent:enter", {
753
+ orbital: orbitalName,
754
+ event: request.event,
755
+ hasPayloadRow: payloadRowAsPayload !== void 0,
756
+ payloadRowId: typeof payloadRowId === "string" || typeof payloadRowId === "number" ? payloadRowId : void 0,
757
+ entityId: request.entityId
758
+ });
759
+ busLog.debug("bus:incoming", () => ({
760
+ orbital: orbitalName,
761
+ event: request.event,
762
+ payload: JSON.stringify(request.payload ?? null),
763
+ entityId: request.entityId,
764
+ traitStates: JSON.stringify(this.traitsOf(orbitalName).map((trait) => ({ traitName: trait.name, currentState: this.kernelEngine?.stateOf(trait.name) ?? "" })))
765
+ }));
766
+ xOrbitalLog.debug("processOrbitalEvent:enter", () => ({
767
+ orbital: orbitalName,
768
+ event: request.event,
769
+ traitsInOrbital: this.traitsOf(orbitalName).map((t) => t.name).join(","),
770
+ payloadActiveTraits: JSON.stringify(
771
+ request.payload?.["_activeTraits"] ?? null
772
+ )
773
+ }));
774
+ const response = await evaluateOnKernel(
775
+ { engine: this.requireKernelEngine(), orbital: registered.schema.name, ...onPush !== void 0 ? { onStream: (item) => {
776
+ if (item.type === "event" || item.type === "effect") onPush(item);
777
+ } } : {} },
778
+ request,
779
+ request.locale
780
+ );
781
+ this.publishKernelResponse(response, void 0, request.clientId);
782
+ return response;
783
+ }
784
+ /**
785
+ * Feed the host sinks from a kernel response: the kernel answers a dispatch whole, so each emit reaches the
786
+ * event bus (the public firehose) and `onPush` once the dispatch is done, in the order the response carries them.
787
+ */
788
+ publishKernelResponse(response, onPush, originClientId) {
789
+ for (const emitted of response.emittedEvents) {
790
+ this.eventBus.emit(emitted.event, emitted.payload, emitted.source, eventRouteKey(emitted.event, emitted.source?.eventId));
791
+ if (emitted.source?.persistSuccess === true) {
792
+ const { persistSuccess: _mark, ...source } = emitted.source;
793
+ this.publishLiveItem({ event: emitted.event, ...emitted.payload !== void 0 ? { payload: emitted.payload } : {}, source, ...originClientId !== void 0 ? { originClientId } : {}, target: "peers" });
794
+ }
795
+ onPush?.({ type: "event", data: emitted });
796
+ }
797
+ for (const { effect } of response.clientEffectsByTrait ?? []) onPush?.({ type: "effect", data: effect });
798
+ }
799
+ /** This app's in-flight keyed `call-service`s, shared by every transition it runs (`cancel-call`). */
800
+ inFlightCalls = new InFlightCalls();
801
+ // ==========================================================================
802
+ // Direct API (for programmatic use)
803
+ // ==========================================================================
804
+ /**
805
+ * Get the event bus for manual event emission
806
+ */
807
+ getEventBus() {
808
+ return this.eventBus;
809
+ }
810
+ /**
811
+ * Get state for a specific orbital/trait
812
+ */
813
+ getState(orbitalName, traitName) {
814
+ const registered = this.orbitals.get(orbitalName);
815
+ if (!registered) return void 0;
816
+ const kernel = this.kernelEngine;
817
+ if (kernel === null) return void 0;
818
+ const stateOf = (name) => {
819
+ const currentState = kernel.stateOf(name);
820
+ return currentState === "" ? void 0 : { traitName: name, currentState, previousState: null, lastEvent: null, context: {} };
821
+ };
822
+ if (traitName) return stateOf(traitName);
823
+ const states = {};
824
+ for (const trait of this.traitsOf(orbitalName)) {
825
+ const state = stateOf(trait.name);
826
+ if (state !== void 0) states[trait.name] = state;
827
+ }
828
+ return states;
829
+ }
830
+ /**
831
+ * List registered orbitals
832
+ */
833
+ listOrbitals() {
834
+ return Array.from(this.orbitals.keys());
835
+ }
836
+ /**
837
+ * Check if an orbital is registered
838
+ */
839
+ hasOrbital(name) {
840
+ return this.orbitals.has(name);
841
+ }
842
+ };
843
+
844
+ export { OrbitalServerRuntime, UnresolvedProgramError };