@almadar/runtime 6.70.0 → 6.72.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,14 +1,14 @@
1
- import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-PAEMYGKV.js';
2
- export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-PAEMYGKV.js';
1
+ import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor, LIFECYCLE_EVENTS } from './chunk-A474B57C.js';
2
+ export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-A474B57C.js';
3
3
  import { isValidCronExpression } from './chunk-OU3ITB5S.js';
4
- import { createContextFromBindings, checkMutationAccess, resolveCallSitePayloadCaptures, applyRowAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
4
+ import { createContextFromBindings, checkMutationAccess, applyRowAccess, accessDeniedMessage } from './chunk-ZWAOJT6R.js';
5
5
  import './chunk-T4VDAB4C.js';
6
6
  import './chunk-SCRAHWOC.js';
7
7
  import './chunk-MLKGABMK.js';
8
8
  import { createLogger } from '@almadar/logger';
9
9
  import * as nodeModule from 'module';
10
10
  import { evaluateListenPayloadExpr, evaluateGuard, evaluate } from '@almadar/evaluator';
11
- import { DEFAULT_VIEWER, buildResolvedTraitConfigs, isInlineTrait, isEntityCall, applyListenPayloadMapping, personaFromIdentityRow, normalizeUserContext, isRuntimeEntity, isPageReference } from '@almadar/core';
11
+ import { DEFAULT_VIEWER, buildResolvedTraitConfigs, collectCallsiteCaptureChildren, isInlineTrait, isEntityCall, applyListenPayloadMapping, personaFromIdentityRow, normalizeUserContext, isRuntimeEntity, isPageReference } from '@almadar/core';
12
12
  import { ownerFieldsFromSchema, identityEntityName, entityAccessPoliciesByStoreKey, entityAccessPolicies } from '@almadar/core/mock';
13
13
  import { getPatternFieldsContract } from '@almadar/core/patterns';
14
14
 
@@ -115,6 +115,10 @@ var OrbitalServerRuntime = class {
115
115
  orbitals = /* @__PURE__ */ new Map();
116
116
  eventBus;
117
117
  config;
118
+ /** The bound persistence adapter (mock/in-memory/consumer-supplied). Public
119
+ * so a test can inspect committed rows honestly — `(runtime as any)
120
+ * .persistence` was the alternative, and that cast is what this field
121
+ * visibility replaces. */
118
122
  persistence;
119
123
  listenerCleanups = [];
120
124
  tickBindings = [];
@@ -156,6 +160,15 @@ var OrbitalServerRuntime = class {
156
160
  * `declaredDefaults` and ahead of `callSiteOverride`.
157
161
  */
158
162
  resolvedTraitConfigs = {};
163
+ /**
164
+ * Referrer trait name → the DIRECT children (via `@trait.X`) that need
165
+ * their lifecycle transition re-run under the referrer's `callsitePayload`
166
+ * whenever the referrer's own transition fires — computed once per
167
+ * `register()` via `@almadar/core`'s `collectCallsiteCaptureChildren`
168
+ * (merged across every orbital in the schema, same flattening
169
+ * `resolvedTraitConfigs` uses). See `rerenderCallsiteCaptureChildren`.
170
+ */
171
+ callsiteCaptureChildrenByTrait = /* @__PURE__ */ new Map();
159
172
  constructor(config = {}) {
160
173
  this.config = {
161
174
  mode: "mock",
@@ -356,6 +369,7 @@ var OrbitalServerRuntime = class {
356
369
  this.setupTicks();
357
370
  this.resolvedSchema = schema;
358
371
  this.resolvedTraitConfigs = buildResolvedTraitConfigs(schema);
372
+ this.callsiteCaptureChildrenByTrait = this.buildCallsiteCaptureChildrenByTrait(schema);
359
373
  this.installOwnerGate();
360
374
  }
361
375
  /**
@@ -375,6 +389,22 @@ var OrbitalServerRuntime = class {
375
389
  columns: derived
376
390
  });
377
391
  }
392
+ /**
393
+ * Merge `collectCallsiteCaptureChildren` across every orbital in the
394
+ * schema into ONE flat referrer-trait-name → children map — same
395
+ * flattening `resolvedTraitConfigs` uses, safe because trait names are
396
+ * unique within one running schema (the compose/resolve pipeline already
397
+ * relies on that for `configByTrait` and `resolvedTraitConfigs`).
398
+ */
399
+ buildCallsiteCaptureChildrenByTrait(schema) {
400
+ const merged = /* @__PURE__ */ new Map();
401
+ for (const orbital of schema.orbitals) {
402
+ for (const [referrer, children] of collectCallsiteCaptureChildren(orbital)) {
403
+ merged.set(referrer, children);
404
+ }
405
+ }
406
+ return merged;
407
+ }
378
408
  /**
379
409
  * Register an OrbitalSchema synchronously (for backward compatibility).
380
410
  * Note: This version doesn't wait for instance seeding to complete.
@@ -393,6 +423,7 @@ var OrbitalServerRuntime = class {
393
423
  this.setupTicks();
394
424
  this.resolvedSchema = schema;
395
425
  this.resolvedTraitConfigs = buildResolvedTraitConfigs(schema);
426
+ this.callsiteCaptureChildrenByTrait = this.buildCallsiteCaptureChildrenByTrait(schema);
396
427
  this.installOwnerGate();
397
428
  }
398
429
  /**
@@ -760,6 +791,40 @@ var OrbitalServerRuntime = class {
760
791
  });
761
792
  });
762
793
  }
794
+ /**
795
+ * The id of the SOURCE event a `listens {}` entry names, derived from the
796
+ * emitting trait's own declared `emits[]` contract rather than trusted
797
+ * from `listener.eventId` alone.
798
+ *
799
+ * `TraitEventListener.eventId` is "optional until the Phase-7 flip" (see
800
+ * `@almadar/core`'s `trait.ts`) — the compose/resolve pipeline stamps a
801
+ * V4 ledger id onto every `emits[]` entry (the emitter's own contract)
802
+ * well before it stamps the matching id onto every `listens[]` entry that
803
+ * names that event (source-qualified `Trait.EVENT -> X` listens compiled
804
+ * from `uses`-resolved atoms carry `triggersId` for their OWN triggered
805
+ * event but no `eventId` for the event they're listening to). The emit
806
+ * handler (`executeEffects`'s `emit` closure below) always looks up and
807
+ * stamps `emittingTrait.emits[].eventId` when present, so during that
808
+ * transitional window the emit side routes under an id-qualified bus key
809
+ * while the listen side — reading only its own possibly-absent
810
+ * `eventId` — subscribes under the bare name, and the two never meet.
811
+ *
812
+ * Fix: derive the SAME id the emitter will stamp by resolving the
813
+ * listener's declared `source` (trait/orbital, name or id) to the actual
814
+ * registered trait and reading ITS `emits[]` contract for `listener.event`
815
+ * — the single canonical place an event's id lives. `kind: "any"` sources
816
+ * are left alone (no single emitter to resolve against; bare-name routing
817
+ * is already the correct, safe default there).
818
+ */
819
+ resolveSourceEmitEventId(source, event, listenerOrbital) {
820
+ if (!source || source.kind === "any") return void 0;
821
+ const ownerOrbitalName = source.kind === "orbital" ? source.orbital : listenerOrbital;
822
+ const owner = this.orbitals.get(ownerOrbitalName);
823
+ if (!owner) return void 0;
824
+ const sourceTrait = source.traitId !== void 0 ? owner.traits.find((t) => t.id === source.traitId) : owner.traits.find((t) => t.name === source.trait);
825
+ if (!sourceTrait) return void 0;
826
+ return sourceTrait.emits?.find((e) => e.event === event)?.eventId;
827
+ }
763
828
  /**
764
829
  * Set up event listeners for cross-orbital communication
765
830
  */
@@ -773,7 +838,8 @@ var OrbitalServerRuntime = class {
773
838
  if (!trait.listens) continue;
774
839
  for (const listener of trait.listens) {
775
840
  const { bareEvent, matcher } = parseListenSource(listener, orbitalName);
776
- const routeKey = eventRouteKey(bareEvent, listener.eventId);
841
+ const effectiveEventId = listener.eventId ?? this.resolveSourceEmitEventId(listener.source, bareEvent, orbitalName);
842
+ const routeKey = eventRouteKey(bareEvent, effectiveEventId);
777
843
  const cleanup = this.eventBus.on(routeKey, async (event) => {
778
844
  if (!matcher(event.source)) return;
779
845
  if (event.source?.originClientId !== void 0) {
@@ -1337,6 +1403,21 @@ var OrbitalServerRuntime = class {
1337
1403
  onPush,
1338
1404
  clientId
1339
1405
  );
1406
+ await this.rerenderCallsiteCaptureChildren(
1407
+ registered,
1408
+ traitName,
1409
+ cleanPayload ?? {},
1410
+ entityData,
1411
+ entityId,
1412
+ emittedEvents,
1413
+ fetchedData,
1414
+ clientEffects,
1415
+ effectResults,
1416
+ viewer,
1417
+ clientEffectsByTrait,
1418
+ onPush,
1419
+ clientId
1420
+ );
1340
1421
  }
1341
1422
  }
1342
1423
  const states = {};
@@ -1382,7 +1463,7 @@ var OrbitalServerRuntime = class {
1382
1463
  /**
1383
1464
  * Execute effects from a transition
1384
1465
  */
1385
- async executeEffects(registered, traitName, effects, payload, entityData, entityId, emittedEvents, fetchedData, clientEffects, effectResults, user, clientEffectsByTrait, onPush, originClientId) {
1466
+ async executeEffects(registered, traitName, effects, payload, entityData, entityId, emittedEvents, fetchedData, clientEffects, effectResults, user, clientEffectsByTrait, onPush, originClientId, callsitePayload) {
1386
1467
  const entityType = registered.entity.name;
1387
1468
  for (const eff of effects) {
1388
1469
  if (Array.isArray(eff) && eff[0] === "fetch") {
@@ -1498,7 +1579,7 @@ var OrbitalServerRuntime = class {
1498
1579
  case "create": {
1499
1580
  const createData = opRest[0] || {};
1500
1581
  const { id: newId } = await this.persistence.create(opEntityType, createData);
1501
- batchResults.push({ action: "create", entityType: opEntityType, id: newId, ...createData });
1582
+ batchResults.push({ ...createData, action: "create", entityType: opEntityType, id: newId });
1502
1583
  completed.push({ action: "create", entityType: opEntityType, id: newId });
1503
1584
  break;
1504
1585
  }
@@ -1507,7 +1588,7 @@ var OrbitalServerRuntime = class {
1507
1588
  const updateData = opRest[1] || {};
1508
1589
  await this.persistence.update(opEntityType, updateId, updateData);
1509
1590
  const updated = await this.persistence.getById(opEntityType, updateId);
1510
- batchResults.push({ action: "update", entityType: opEntityType, id: updateId, ...updated || updateData });
1591
+ batchResults.push({ ...updated || updateData, action: "update", entityType: opEntityType, id: updateId });
1511
1592
  completed.push({ action: "update", entityType: opEntityType, id: updateId });
1512
1593
  break;
1513
1594
  }
@@ -1546,6 +1627,7 @@ var OrbitalServerRuntime = class {
1546
1627
  const type = targetEntityType || entityType;
1547
1628
  let resultData;
1548
1629
  const sizeBefore = (await this.persistence.list(type)).length;
1630
+ let deniedReason;
1549
1631
  try {
1550
1632
  if (action === "create" || action === "update") {
1551
1633
  this.validateRelationCardinality(type, data || {});
@@ -1555,10 +1637,11 @@ var OrbitalServerRuntime = class {
1555
1637
  switch (action) {
1556
1638
  case "create": {
1557
1639
  if (!checkMutationAccess(data || {}, mutationPolicy, accessBindings)) {
1640
+ deniedReason = "access-denied";
1558
1641
  throw new Error(accessDeniedMessage("create", type));
1559
1642
  }
1560
1643
  const { id } = await this.persistence.create(type, data || {});
1561
- resultData = { id, ...data || {} };
1644
+ resultData = { ...data || {}, id };
1562
1645
  break;
1563
1646
  }
1564
1647
  case "update":
@@ -1567,15 +1650,17 @@ var OrbitalServerRuntime = class {
1567
1650
  if (mutationPolicy !== void 0) {
1568
1651
  const existing = await this.persistence.getById(type, updateId);
1569
1652
  if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings)) {
1653
+ deniedReason = "access-denied";
1570
1654
  throw new Error(accessDeniedMessage("update", type));
1571
1655
  }
1572
1656
  }
1573
1657
  await this.persistence.update(type, updateId, data || {});
1574
1658
  const updated = await this.persistence.getById(type, updateId);
1575
- resultData = updated || { id: updateId, ...data || {} };
1659
+ resultData = updated || { ...data || {}, id: updateId };
1576
1660
  } else {
1577
1661
  effectLog.error("persist:no-row-key", { action, entityType: type });
1578
1662
  if (NO_ROW_KEY_IS_FATAL) {
1663
+ deniedReason = "no-row-key";
1579
1664
  throw new Error(
1580
1665
  `persist ${action} ${type} resolved no row key \u2014 the id was neither on the row being written nor on the request. Bind it before the write, e.g. (set @entity.id ?row.id) on the transition that selects it.`
1581
1666
  );
@@ -1590,6 +1675,7 @@ var OrbitalServerRuntime = class {
1590
1675
  if (mutationPolicy !== void 0) {
1591
1676
  const existing = await this.persistence.getById(type, deleteId);
1592
1677
  if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings)) {
1678
+ deniedReason = "access-denied";
1593
1679
  throw new Error(accessDeniedMessage("delete", type));
1594
1680
  }
1595
1681
  }
@@ -1599,6 +1685,7 @@ var OrbitalServerRuntime = class {
1599
1685
  } else {
1600
1686
  effectLog.error("persist:no-row-key", { action, entityType: type });
1601
1687
  if (NO_ROW_KEY_IS_FATAL) {
1688
+ deniedReason = "no-row-key";
1602
1689
  throw new Error(
1603
1690
  `persist ${action} ${type} resolved no row key \u2014 the id was neither on the row being written nor on the request. Bind it before the write, e.g. (set @entity.id ?row.id) on the transition that selects it.`
1604
1691
  );
@@ -1607,6 +1694,17 @@ var OrbitalServerRuntime = class {
1607
1694
  break;
1608
1695
  }
1609
1696
  }
1697
+ if (resultData === void 0 && (action === "update" || action === "delete")) {
1698
+ effectResults.push({
1699
+ effect: "persist",
1700
+ action,
1701
+ entityType: type,
1702
+ success: false,
1703
+ denied: true,
1704
+ error: `persist ${action} ${type} resolved no row key`
1705
+ });
1706
+ return void 0;
1707
+ }
1610
1708
  const sizeAfter = (await this.persistence.list(type)).length;
1611
1709
  effectLog.debug("persist:store-mutate", {
1612
1710
  action,
@@ -1635,6 +1733,7 @@ var OrbitalServerRuntime = class {
1635
1733
  action,
1636
1734
  entityType: type,
1637
1735
  success: false,
1736
+ ...deniedReason !== void 0 ? { denied: true } : {},
1638
1737
  error: err instanceof Error ? err.message : String(err)
1639
1738
  });
1640
1739
  }
@@ -1929,17 +2028,17 @@ var OrbitalServerRuntime = class {
1929
2028
  state: state?.currentState || "unknown",
1930
2029
  user
1931
2030
  };
2031
+ if (callsitePayload) {
2032
+ bindings.callsitePayload = callsitePayload;
2033
+ }
1932
2034
  const traitDef = registered.traits.find((t) => t.name === traitName);
1933
2035
  const declaredDefaults = collectDeclaredConfigDefaults(traitDef);
1934
2036
  const resolvedDefaults = this.resolvedTraitConfigs[traitName];
1935
2037
  const callSiteOverrideRaw = registered.configByTrait.get(traitName);
1936
- const callSiteOverride = callSiteOverrideRaw ? resolveCallSitePayloadCaptures(
1937
- Object.fromEntries(
1938
- Object.entries(callSiteOverrideRaw).filter(
1939
- ([, v]) => !(typeof v === "string" && v.startsWith("@config."))
1940
- )
1941
- ),
1942
- payload
2038
+ const callSiteOverride = callSiteOverrideRaw ? Object.fromEntries(
2039
+ Object.entries(callSiteOverrideRaw).filter(
2040
+ ([, v]) => !(typeof v === "string" && v.startsWith("@config."))
2041
+ )
1943
2042
  ) : void 0;
1944
2043
  if (declaredDefaults || resolvedDefaults || callSiteOverride) {
1945
2044
  bindings.config = {
@@ -2022,6 +2121,85 @@ var OrbitalServerRuntime = class {
2022
2121
  });
2023
2122
  await executor.executeAll(effects);
2024
2123
  }
2124
+ /**
2125
+ * Re-run a JSX-hoisted inline child trait's (`@trait.X`) lifecycle
2126
+ * transition under `callsitePayload` — the payload of the transition that
2127
+ * just composed it — so its `@callsitePayload.<field>` captures reflect
2128
+ * the composing event instead of staying frozen at whatever the child
2129
+ * captured at its own mount-time INIT (a child renders once at mount and
2130
+ * never again on its own).
2131
+ *
2132
+ * `this.callsiteCaptureChildrenByTrait` (built at `register()` via
2133
+ * `@almadar/core`'s `collectCallsiteCaptureChildren`) gives `traitName`'s
2134
+ * DIRECT children that need this — either because the child itself
2135
+ * captures, or because it is a pass-through to a capturing descendant.
2136
+ * The child's lifecycle event (INIT/LOAD/$MOUNT) is re-dispatched
2137
+ * TARGETED at just that trait, from its CURRENT state (the same
2138
+ * guard-aware `sendEvent`/`canHandleEvent` lookup a mount-time INIT
2139
+ * uses), then its effects run through the SAME `executeEffects` used
2140
+ * everywhere else, with `payload: {}` (a lifecycle event carries none)
2141
+ * and `callsitePayload` set so `@callsitePayload.*` resolves — pushing
2142
+ * into the SAME `clientEffects`/`clientEffectsByTrait`/`effectResults`
2143
+ * so the child's refreshed frame reaches the sidecar under its own trait
2144
+ * name. Recurses into the child's own entry in the same map (still under
2145
+ * the SAME `callsitePayload` — the capture resolves up the embed chain to
2146
+ * the nearest transition that actually has one) for grandchildren;
2147
+ * `visited` guards against a malformed embed graph cycling on itself.
2148
+ * Never goes through `processOrbitalEvent` (that would be re-entrant) —
2149
+ * calls this internal executor directly, exactly like every other
2150
+ * transition's effects.
2151
+ */
2152
+ async rerenderCallsiteCaptureChildren(registered, traitName, callsitePayload, entityData, entityId, emittedEvents, fetchedData, clientEffects, effectResults, user, clientEffectsByTrait, onPush, originClientId, visited = /* @__PURE__ */ new Set()) {
2153
+ const children = this.callsiteCaptureChildrenByTrait.get(traitName);
2154
+ if (!children || children.size === 0) return;
2155
+ for (const childName of children) {
2156
+ if (visited.has(childName)) continue;
2157
+ visited.add(childName);
2158
+ const lifecycleEvent = LIFECYCLE_EVENTS.find((evt) => registered.manager.canHandleEvent(childName, evt));
2159
+ if (lifecycleEvent === void 0) continue;
2160
+ const [entry] = registered.manager.sendEvent(lifecycleEvent, {}, entityData, void 0, void 0, childName, user);
2161
+ if (!entry || !entry.result.executed) continue;
2162
+ xOrbitalLog.debug("callsite-capture-child:rerender", () => ({
2163
+ referrer: traitName,
2164
+ child: childName,
2165
+ lifecycleEvent,
2166
+ callsitePayload: JSON.stringify(callsitePayload)
2167
+ }));
2168
+ await this.executeEffects(
2169
+ registered,
2170
+ childName,
2171
+ entry.result.effects,
2172
+ {},
2173
+ entityData,
2174
+ entityId,
2175
+ emittedEvents,
2176
+ fetchedData,
2177
+ clientEffects,
2178
+ effectResults,
2179
+ user,
2180
+ clientEffectsByTrait,
2181
+ onPush,
2182
+ originClientId,
2183
+ callsitePayload
2184
+ );
2185
+ await this.rerenderCallsiteCaptureChildren(
2186
+ registered,
2187
+ childName,
2188
+ callsitePayload,
2189
+ entityData,
2190
+ entityId,
2191
+ emittedEvents,
2192
+ fetchedData,
2193
+ clientEffects,
2194
+ effectResults,
2195
+ user,
2196
+ clientEffectsByTrait,
2197
+ onPush,
2198
+ originClientId,
2199
+ visited
2200
+ );
2201
+ }
2202
+ }
2025
2203
  // ==========================================================================
2026
2204
  // Relation Population
2027
2205
  // ==========================================================================
@@ -2281,6 +2459,7 @@ var OrbitalServerRuntime = class {
2281
2459
  * Routes:
2282
2460
  * - GET / - List registered orbitals
2283
2461
  * - GET /:orbital - Get orbital info and current states
2462
+ * - GET /:orbital/entities/:entityType - Full mock-store row set (verification/tooling only)
2284
2463
  * - POST /:orbital/events - Send event to orbital (includes data from `fetch` effects)
2285
2464
  */
2286
2465
  router() {
@@ -2326,6 +2505,17 @@ var OrbitalServerRuntime = class {
2326
2505
  }
2327
2506
  });
2328
2507
  });
2508
+ router.get("/:orbital/entities/:entityType", (req, res, next) => {
2509
+ const orbitalName = req.params.orbital;
2510
+ const entityType = req.params.entityType;
2511
+ if (!this.orbitals.has(orbitalName)) {
2512
+ res.status(404).json({ success: false, error: "Orbital not found" });
2513
+ return;
2514
+ }
2515
+ this.persistence.list(entityType).then((rows) => {
2516
+ res.json({ success: true, entityType, rows });
2517
+ }).catch(next);
2518
+ });
2329
2519
  router.post(
2330
2520
  "/:orbital/events",
2331
2521
  async (req, res, next) => {
@@ -1,4 +1,4 @@
1
- import { I as IEventBus } from './types-BaD_ox7e.js';
1
+ import { I as IEventBus } from './types-CWaIuEQn.js';
2
2
  import { EventPayload } from '@almadar/core';
3
3
 
4
4
  /**