@almadar/ui 5.101.0 → 5.103.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.
@@ -16,7 +16,9 @@
16
16
  *
17
17
  * @packageDocumentation
18
18
  */
19
- import type { EventPayload, ServiceParams } from '@almadar/core';
19
+ import { type MutableRefObject } from 'react';
20
+ import { type SharedEntityWriter } from './index';
21
+ import type { ResolvedTraitTick, EventPayload, SExpr, ServiceParams, ResolvedTrait } from '@almadar/core';
20
22
  import { type TraitState } from '@almadar/runtime';
21
23
  import type { ResolvedTraitBinding } from '../types/runtime-types';
22
24
  import type { useUISlots } from '../providers/UISlotContext';
@@ -31,6 +33,35 @@ export interface TraitStateMachineResult {
31
33
  /** Check if a trait can handle an event from its current state */
32
34
  canHandleEvent: (traitName: string, eventKey: string) => boolean;
33
35
  }
36
+ /** Effect head-operator that writes a field onto a shared entity. */
37
+ export declare const SHARED_ENTITY_WRITE_OPS: ReadonlySet<string>;
38
+ /** Effect head-operators that paint a shared entity's merged state. */
39
+ export declare const SHARED_ENTITY_RENDER_OPS: ReadonlySet<string>;
40
+ /** Every tick + transition effect a trait declares, flattened for a structural scan. */
41
+ export declare function collectAllTraitEffects(trait: ResolvedTrait): SExpr[];
42
+ /**
43
+ * Does ANY effect in this list call one of `ops` anywhere in its (possibly
44
+ * `do`/`if`/`when`/`let`-nested) tree? Reuses `@almadar/core`'s canonical
45
+ * `walkSExpr` rather than a bespoke recursive walk, and looks only at the
46
+ * effect's head operator — a structural presence check, no name-matching.
47
+ */
48
+ export declare function effectsCallOp(effects: SExpr[], ops: ReadonlySet<string>): boolean;
49
+ /**
50
+ * Build one writer trait's tick into a synchronous `SharedEntityWriter`
51
+ * (`(scratch) => writes[]`) so `runTickFrame` can fold several writer ticks
52
+ * into one merged commit, in binding order.
53
+ *
54
+ * `runTickFrame`'s writer contract is synchronous, but `@almadar/runtime`'s
55
+ * `EffectExecutor` is inherently async (`executeAll`'s `for...of await`
56
+ * sequencing only guarantees the FIRST top-level effect lands before a
57
+ * fire-and-forget caller reads the result back — everything after the first
58
+ * `await` boundary would be silently dropped). `@almadar/evaluator`'s
59
+ * `executeEffects` is fully synchronous end-to-end (its `set`/`do`/`if`/
60
+ * `when`/`let` dispatch never awaits), so it — not `EffectExecutor` — is the
61
+ * correct tool here; `ctx.mutateEntity` captures each write instead of
62
+ * applying it through a handler.
63
+ */
64
+ export declare function createSharedEntityWriter(binding: ResolvedTraitBinding, tick: ResolvedTraitTick, traitStatesRef: MutableRefObject<Map<string, TraitState>>, emit: (event: string, payload?: EventPayload) => void): SharedEntityWriter;
34
65
  export interface UseTraitStateMachineOptions {
35
66
  /**
36
67
  * Callback invoked after each event is processed (for server forwarding).
@@ -7730,7 +7730,7 @@ function recordTransition(trace) {
7730
7730
  ...trace,
7731
7731
  id: `t-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
7732
7732
  };
7733
- log3.debug("transition:recorded", { trait: trace.traitName, from: trace.from, to: trace.to, event: trace.event, effectCount: trace.effects.length });
7733
+ log4.debug("transition:recorded", { trait: trace.traitName, from: trace.from, to: trace.to, event: trace.event, effectCount: trace.effects.length });
7734
7734
  getState().transitions.push(entry);
7735
7735
  if (getState().transitions.length > MAX_TRANSITIONS) {
7736
7736
  getState().transitions.shift();
@@ -7832,7 +7832,7 @@ function getTraitSnapshots() {
7832
7832
  try {
7833
7833
  snapshots.push(getter());
7834
7834
  } catch (err) {
7835
- log3.error("traitSnapshot getter failed", { trait: traitName, err: String(err) });
7835
+ log4.error("traitSnapshot getter failed", { trait: traitName, err: String(err) });
7836
7836
  }
7837
7837
  }
7838
7838
  return snapshots;
@@ -7931,10 +7931,10 @@ function updateAssetStatus(url, status) {
7931
7931
  window.__orbitalVerification.assetStatus[url] = status;
7932
7932
  }
7933
7933
  }
7934
- var log3, MAX_TRANSITIONS;
7934
+ var log4, MAX_TRANSITIONS;
7935
7935
  var init_verificationRegistry = __esm({
7936
7936
  "lib/verificationRegistry.ts"() {
7937
- log3 = logger.createLogger("almadar:bridge");
7937
+ log4 = logger.createLogger("almadar:bridge");
7938
7938
  MAX_TRANSITIONS = 500;
7939
7939
  exposeOnWindow();
7940
7940
  }
@@ -17542,7 +17542,7 @@ var init_avl_elk_layout = __esm({
17542
17542
  elk = new ELK__default.default();
17543
17543
  }
17544
17544
  });
17545
- var log4, SWIM_GUTTER, CENTER_W, BehaviorView;
17545
+ var log5, SWIM_GUTTER, CENTER_W, BehaviorView;
17546
17546
  var init_BehaviorView = __esm({
17547
17547
  "components/avl/molecules/BehaviorView.tsx"() {
17548
17548
  "use client";
@@ -17551,7 +17551,7 @@ var init_BehaviorView = __esm({
17551
17551
  init_AvlSwimLane();
17552
17552
  init_avl_atom_types();
17553
17553
  init_avl_elk_layout();
17554
- log4 = logger.createLogger("almadar:ui:avl:behavior-view");
17554
+ log5 = logger.createLogger("almadar:ui:avl:behavior-view");
17555
17555
  SWIM_GUTTER = 120;
17556
17556
  CENTER_W = 360;
17557
17557
  BehaviorView = ({ data }) => {
@@ -17562,7 +17562,7 @@ var init_BehaviorView = __esm({
17562
17562
  const dataKey = React96.useMemo(() => JSON.stringify(traitData), [traitData]);
17563
17563
  React96.useEffect(() => {
17564
17564
  if (!traitData) return;
17565
- computeTraitLayout(traitData).then(setLayout).catch((err) => log4.error("compute-trait-layout-failed", { error: err instanceof Error ? err : String(err) }));
17565
+ computeTraitLayout(traitData).then(setLayout).catch((err) => log5.error("compute-trait-layout-failed", { error: err instanceof Error ? err : String(err) }));
17566
17566
  }, [dataKey]);
17567
17567
  if (!traitData) {
17568
17568
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4 text-center text-[var(--color-muted-foreground)] text-sm", children: t("avl.noTraitData") });
@@ -18114,7 +18114,7 @@ function generateDiff(oldVal, newVal) {
18114
18114
  }
18115
18115
  return diff;
18116
18116
  }
18117
- var orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log5, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, CodeBlock;
18117
+ var orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log6, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, CodeBlock;
18118
18118
  var init_CodeBlock = __esm({
18119
18119
  "components/core/molecules/markdown/CodeBlock.tsx"() {
18120
18120
  init_cn();
@@ -18197,7 +18197,7 @@ var init_CodeBlock = __esm({
18197
18197
  "lolo-op-async": { color: syntax.ORB_COLORS.dark.async }
18198
18198
  };
18199
18199
  loloStyle = { ...dark__default.default, ...loloStyleOverrides };
18200
- log5 = logger.createLogger("almadar:ui:markdown-code");
18200
+ log6 = logger.createLogger("almadar:ui:markdown-code");
18201
18201
  CODE_LANGUAGES = [
18202
18202
  "text",
18203
18203
  "json",
@@ -18465,7 +18465,7 @@ var init_CodeBlock = __esm({
18465
18465
  eventBus.emit("UI:COPY_CODE", { language: activeLanguage, success: true });
18466
18466
  setTimeout(() => setCopied(false), 2e3);
18467
18467
  } catch (err) {
18468
- log5.error("Failed to copy code", { error: err instanceof Error ? err : String(err) });
18468
+ log6.error("Failed to copy code", { error: err instanceof Error ? err : String(err) });
18469
18469
  eventBus.emit("UI:COPY_CODE", { language: activeLanguage, success: false });
18470
18470
  }
18471
18471
  };
@@ -27330,14 +27330,14 @@ function useSafeEventBus3() {
27330
27330
  } };
27331
27331
  }
27332
27332
  }
27333
- var log6, lookStyles5, ButtonGroup;
27333
+ var log7, lookStyles5, ButtonGroup;
27334
27334
  var init_ButtonGroup = __esm({
27335
27335
  "components/core/molecules/ButtonGroup.tsx"() {
27336
27336
  "use client";
27337
27337
  init_cn();
27338
27338
  init_atoms();
27339
27339
  init_useEventBus();
27340
- log6 = logger.createLogger("almadar:ui:button-group");
27340
+ log7 = logger.createLogger("almadar:ui:button-group");
27341
27341
  lookStyles5 = {
27342
27342
  "right-aligned-buttons": "",
27343
27343
  "floating-bar": "fixed bottom-section left-1/2 -translate-x-1/2 shadow-elevation-toast bg-card p-card-sm rounded-container",
@@ -27418,7 +27418,7 @@ var init_ButtonGroup = __esm({
27418
27418
  {
27419
27419
  variant: "ghost",
27420
27420
  onClick: () => {
27421
- log6.debug("Filter clicked", { field: filter.field });
27421
+ log7.debug("Filter clicked", { field: filter.field });
27422
27422
  },
27423
27423
  children: filter.label
27424
27424
  },
@@ -28327,9 +28327,9 @@ function debug(...args) {
28327
28327
  const [first, ...rest] = args;
28328
28328
  const message = typeof first === "string" ? first : "<debug>";
28329
28329
  if (rest.length === 0 && typeof first === "string") {
28330
- log7.debug(message);
28330
+ log8.debug(message);
28331
28331
  } else {
28332
- log7.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
28332
+ log8.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
28333
28333
  }
28334
28334
  }
28335
28335
  function debugGroup(label) {
@@ -28357,11 +28357,11 @@ function toLogMetaValue(v) {
28357
28357
  }
28358
28358
  return String(v);
28359
28359
  }
28360
- var NAMESPACE, log7;
28360
+ var NAMESPACE, log8;
28361
28361
  var init_debug = __esm({
28362
28362
  "lib/debug.ts"() {
28363
28363
  NAMESPACE = "almadar:ui:debug";
28364
- log7 = logger.createLogger(NAMESPACE);
28364
+ log8 = logger.createLogger(NAMESPACE);
28365
28365
  logger.createLogger("almadar:ui:debug:input");
28366
28366
  logger.createLogger("almadar:ui:debug:collision");
28367
28367
  logger.createLogger("almadar:ui:debug:physics");
@@ -30519,12 +30519,12 @@ var init_MapView = __esm({
30519
30519
  shadowSize: [41, 41]
30520
30520
  });
30521
30521
  L.Marker.prototype.options.icon = defaultIcon;
30522
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback98, useState: useState97 } = React96__namespace.default;
30522
+ const { useEffect: useEffect67, useRef: useRef66, useCallback: useCallback98, useState: useState97 } = React96__namespace.default;
30523
30523
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
30524
30524
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
30525
30525
  function MapUpdater({ centerLat, centerLng, zoom }) {
30526
30526
  const map = useMap();
30527
- const prevRef = useRef65({ centerLat, centerLng, zoom });
30527
+ const prevRef = useRef66({ centerLat, centerLng, zoom });
30528
30528
  useEffect67(() => {
30529
30529
  const prev = prevRef.current;
30530
30530
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -43786,7 +43786,7 @@ function getAllEvents(traits2) {
43786
43786
  function EventDispatcherTab({ traits: traits2, schema }) {
43787
43787
  const eventBus = useEventBus();
43788
43788
  const { t } = hooks.useTranslate();
43789
- const [log9, setLog] = React96__namespace.useState([]);
43789
+ const [log10, setLog] = React96__namespace.useState([]);
43790
43790
  const prevStatesRef = React96__namespace.useRef(/* @__PURE__ */ new Map());
43791
43791
  React96__namespace.useEffect(() => {
43792
43792
  for (const trait of traits2) {
@@ -43850,9 +43850,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
43850
43850
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-gray-500 mb-1", children: t("debug.otherEvents") }),
43851
43851
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: unavailableEvents.map((event) => /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", size: "sm", className: "opacity-50", children: event }, event)) })
43852
43852
  ] }),
43853
- log9.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
43853
+ log10.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
43854
43854
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-gray-500 mb-1", children: t("debug.recentTransitions") }),
43855
- /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log9.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
43855
+ /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log10.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
43856
43856
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-purple-400", children: entry.traitName }),
43857
43857
  " ",
43858
43858
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500", children: entry.from }),
@@ -47562,7 +47562,67 @@ var init_UISlotRenderer = __esm({
47562
47562
 
47563
47563
  // hooks/index.ts
47564
47564
  init_useEventBus();
47565
- var log2 = logger.createLogger("almadar:ui:effects:client-handlers");
47565
+ var log2 = logger.createLogger("almadar:ui:shared-entity-store");
47566
+ var EMPTY_ENTITY_STATE = {};
47567
+ function createSharedEntityStore() {
47568
+ const states = /* @__PURE__ */ new Map();
47569
+ const subscribers2 = /* @__PURE__ */ new Map();
47570
+ const getSnapshot3 = (entityId) => states.get(entityId) ?? EMPTY_ENTITY_STATE;
47571
+ const subscribe2 = (entityId, callback) => {
47572
+ let set = subscribers2.get(entityId);
47573
+ if (!set) {
47574
+ set = /* @__PURE__ */ new Set();
47575
+ subscribers2.set(entityId, set);
47576
+ }
47577
+ set.add(callback);
47578
+ return () => {
47579
+ const current = subscribers2.get(entityId);
47580
+ if (!current) return;
47581
+ current.delete(callback);
47582
+ if (current.size === 0) {
47583
+ subscribers2.delete(entityId);
47584
+ }
47585
+ };
47586
+ };
47587
+ const commit = (entityId, nextState) => {
47588
+ states.set(entityId, nextState);
47589
+ const set = subscribers2.get(entityId);
47590
+ if (!set) return;
47591
+ set.forEach((callback) => {
47592
+ try {
47593
+ callback();
47594
+ } catch (error) {
47595
+ log2.error("Shared entity subscriber error", {
47596
+ entityId,
47597
+ error: error instanceof Error ? error : String(error)
47598
+ });
47599
+ }
47600
+ });
47601
+ };
47602
+ const seed = (entityId, initialState) => {
47603
+ if (!states.has(entityId)) {
47604
+ states.set(entityId, initialState);
47605
+ }
47606
+ };
47607
+ return { getSnapshot: getSnapshot3, subscribe: subscribe2, commit, seed };
47608
+ }
47609
+ function useSharedEntityStore() {
47610
+ const storeRef = React96.useRef(null);
47611
+ if (storeRef.current === null) {
47612
+ storeRef.current = createSharedEntityStore();
47613
+ }
47614
+ return storeRef.current;
47615
+ }
47616
+ function runTickFrame(entityId, orderedWriters, store) {
47617
+ let scratch = store.getSnapshot(entityId);
47618
+ for (const writer of orderedWriters) {
47619
+ const writes = writer(scratch);
47620
+ scratch = core.mergeEntityFrame(scratch, writes);
47621
+ }
47622
+ store.commit(entityId, scratch);
47623
+ return scratch;
47624
+ }
47625
+ var log3 = logger.createLogger("almadar:ui:effects:client-handlers");
47566
47626
  function createClientEffectHandlers(options) {
47567
47627
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
47568
47628
  return {
@@ -47571,12 +47631,12 @@ function createClientEffectHandlers(options) {
47571
47631
  eventBus.emit(prefixedEvent, payload);
47572
47632
  },
47573
47633
  persist: async () => {
47574
- log2.warn("persist is server-side only, ignored on client");
47634
+ log3.warn("persist is server-side only, ignored on client");
47575
47635
  },
47576
47636
  // @almadar/runtime EffectHandlers.set types value:unknown — should be FieldValue (upstream fix queued)
47577
47637
  set: ((_entityId, field, value) => {
47578
47638
  if (!liveEntity) {
47579
- log2.warn("set is server-side only, ignored on client (no live entity)");
47639
+ log3.warn("set is server-side only, ignored on client (no live entity)");
47580
47640
  return;
47581
47641
  }
47582
47642
  liveEntity[field] = value;
@@ -47608,10 +47668,10 @@ function createClientEffectHandlers(options) {
47608
47668
  slotSetter.addPattern(slot, pattern, props);
47609
47669
  },
47610
47670
  navigate: navigate ?? ((path) => {
47611
- log2.warn("No navigate handler, ignoring", { path });
47671
+ log3.warn("No navigate handler, ignoring", { path });
47612
47672
  }),
47613
47673
  notify: notify ?? ((msg, type) => {
47614
- log2.debug("notify", { type, message: msg });
47674
+ log3.debug("notify", { type, message: msg });
47615
47675
  })
47616
47676
  };
47617
47677
  }
@@ -47782,11 +47842,101 @@ function toTraitDefinition(binding) {
47782
47842
  function normalizeEventKey(eventKey) {
47783
47843
  return eventKey.startsWith("UI:") ? eventKey.slice(3) : eventKey;
47784
47844
  }
47845
+ var SHARED_ENTITY_WRITE_OPS = /* @__PURE__ */ new Set(["set"]);
47846
+ var SHARED_ENTITY_RENDER_OPS = /* @__PURE__ */ new Set(["render-ui", "render"]);
47847
+ function collectAllTraitEffects(trait) {
47848
+ return [
47849
+ ...trait.ticks.flatMap((t) => t.effects),
47850
+ ...trait.transitions.flatMap((t) => t.effects)
47851
+ ];
47852
+ }
47853
+ function effectsCallOp(effects, ops) {
47854
+ for (const effect of effects) {
47855
+ let found = false;
47856
+ core.walkSExpr(effect, (node) => {
47857
+ if (!found && Array.isArray(node) && typeof node[0] === "string" && ops.has(node[0])) {
47858
+ found = true;
47859
+ }
47860
+ });
47861
+ if (found) return true;
47862
+ }
47863
+ return false;
47864
+ }
47865
+ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
47866
+ return (scratch) => {
47867
+ const traitName = binding.trait.name;
47868
+ const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
47869
+ if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return [];
47870
+ const scratchEntity = { ...scratch };
47871
+ const writes = [];
47872
+ const ctx = evaluator.createMinimalContext(scratchEntity, {}, currentState);
47873
+ const declaredDefaults = runtime.collectDeclaredConfigDefaults(binding.trait);
47874
+ const callSiteConfig = binding.config;
47875
+ if (declaredDefaults || callSiteConfig) {
47876
+ ctx.config = { ...declaredDefaults ?? {}, ...callSiteConfig ?? {} };
47877
+ }
47878
+ ctx.mutateEntity = (changes) => {
47879
+ for (const [field, value] of Object.entries(changes)) {
47880
+ const fieldValue = value;
47881
+ scratchEntity[field] = fieldValue;
47882
+ writes.push({ field, value: fieldValue });
47883
+ }
47884
+ };
47885
+ ctx.emit = (event, payload) => {
47886
+ emit(event, payload);
47887
+ };
47888
+ if (tick.guard !== void 0 && !evaluator.evaluateGuard(tick.guard, ctx)) {
47889
+ tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
47890
+ return [];
47891
+ }
47892
+ evaluator.executeEffects(tick.effects, ctx);
47893
+ return writes;
47894
+ };
47895
+ }
47785
47896
  function useTraitStateMachine(traitBindings, uiSlots, options) {
47786
47897
  const eventBus = useEventBus();
47787
47898
  const { entities } = providers.useEntitySchema();
47788
47899
  const traitConfigsByName = options?.traitConfigsByName;
47789
47900
  const orbitalsByTrait = options?.orbitalsByTrait;
47901
+ const sharedEntityStore = useSharedEntityStore();
47902
+ const sharedGroups = React96.useMemo(() => {
47903
+ const groups = /* @__PURE__ */ new Map();
47904
+ for (const binding of traitBindings) {
47905
+ const linkedEntityName = binding.linkedEntity ?? binding.trait.linkedEntity;
47906
+ if (!linkedEntityName) continue;
47907
+ const entityDef = entities.get(linkedEntityName);
47908
+ if (!entityDef?.shared) continue;
47909
+ const orbitalName = orbitalsByTrait?.[binding.trait.name] ?? "";
47910
+ const storeKey = `${orbitalName}::${linkedEntityName}`;
47911
+ let group = groups.get(storeKey);
47912
+ if (!group) {
47913
+ let defaults;
47914
+ for (const field of entityDef.fields) {
47915
+ if (field.default !== void 0) {
47916
+ (defaults ?? (defaults = {}))[field.name] = field.default;
47917
+ }
47918
+ }
47919
+ group = { storeKey, entityName: linkedEntityName, writerBindings: [], renderBindings: [], defaults };
47920
+ groups.set(storeKey, group);
47921
+ }
47922
+ const allEffects = collectAllTraitEffects(binding.trait);
47923
+ if (effectsCallOp(allEffects, SHARED_ENTITY_WRITE_OPS)) group.writerBindings.push(binding);
47924
+ if (effectsCallOp(allEffects, SHARED_ENTITY_RENDER_OPS)) group.renderBindings.push(binding);
47925
+ }
47926
+ return groups;
47927
+ }, [traitBindings, entities, orbitalsByTrait]);
47928
+ for (const group of sharedGroups.values()) {
47929
+ if (group.defaults) sharedEntityStore.seed(group.storeKey, group.defaults);
47930
+ }
47931
+ const sharedKeyByTraitNameRef = React96.useRef(/* @__PURE__ */ new Map());
47932
+ React96.useEffect(() => {
47933
+ const map = /* @__PURE__ */ new Map();
47934
+ for (const group of sharedGroups.values()) {
47935
+ for (const binding of group.writerBindings) map.set(binding.trait.name, group.storeKey);
47936
+ for (const binding of group.renderBindings) map.set(binding.trait.name, group.storeKey);
47937
+ }
47938
+ sharedKeyByTraitNameRef.current = map;
47939
+ }, [sharedGroups]);
47790
47940
  const manager = React96.useMemo(() => {
47791
47941
  const traitDefs = traitBindings.map(toTraitDefinition);
47792
47942
  const m = new runtime.StateMachineManager(traitDefs);
@@ -47969,14 +48119,21 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47969
48119
  };
47970
48120
  }, [traitBindings]);
47971
48121
  const executeTransitionEffects = React96.useCallback(async (params) => {
47972
- const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log9 } = params;
48122
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log10 } = params;
47973
48123
  const traitName = binding.trait.name;
47974
48124
  const linkedEntity = binding.linkedEntity || "";
47975
48125
  const entityId = payload?.entityId;
47976
- let liveEntity = traitFieldStatesRef.current.get(traitName);
47977
- if (!liveEntity) {
47978
- liveEntity = {};
47979
- traitFieldStatesRef.current.set(traitName, liveEntity);
48126
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
48127
+ let liveEntity;
48128
+ if (sharedKey !== void 0) {
48129
+ liveEntity = { ...sharedEntityStore.getSnapshot(sharedKey) };
48130
+ } else {
48131
+ let existing = traitFieldStatesRef.current.get(traitName);
48132
+ if (!existing) {
48133
+ existing = {};
48134
+ traitFieldStatesRef.current.set(traitName, existing);
48135
+ }
48136
+ liveEntity = existing;
47980
48137
  }
47981
48138
  const effects = syncOnly ? params.effects.filter(
47982
48139
  (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
@@ -48058,7 +48215,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48058
48215
  ...handlers,
48059
48216
  set: async (targetId, field, value) => {
48060
48217
  if (baseSet) await baseSet(targetId, field, value);
48061
- log9.debug("set:write", {
48218
+ log10.debug("set:write", {
48062
48219
  traitName,
48063
48220
  field,
48064
48221
  value: JSON.stringify(value),
@@ -48098,17 +48255,20 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48098
48255
  const executor = new runtime.EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
48099
48256
  try {
48100
48257
  await executor.executeAll(effects);
48101
- log9.debug("effects:executed", () => ({
48258
+ if (sharedKey !== void 0) {
48259
+ sharedEntityStore.commit(sharedKey, liveEntity);
48260
+ }
48261
+ log10.debug("effects:executed", () => ({
48102
48262
  traitName,
48103
48263
  transition: `${previousState}->${newState}`,
48104
48264
  event: flushEvent,
48105
48265
  effectCount: effects.length,
48106
48266
  emitted: emittedDuringExec.join(","),
48107
- entityAfter: JSON.stringify(traitFieldStatesRef.current.get(traitName) ?? {}),
48267
+ entityAfter: JSON.stringify(liveEntity ?? {}),
48108
48268
  slotsTouched: Array.from(pendingSlots.keys()).join(",")
48109
48269
  }));
48110
48270
  for (const [slot, patterns] of pendingSlots) {
48111
- log9.debug("flush:slot", {
48271
+ log10.debug("flush:slot", {
48112
48272
  traitName,
48113
48273
  slot,
48114
48274
  patternCount: patterns.length,
@@ -48123,7 +48283,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48123
48283
  });
48124
48284
  }
48125
48285
  } catch (error) {
48126
- log9.error("effects:error", {
48286
+ log10.error("effects:error", {
48127
48287
  traitName,
48128
48288
  transition: `${previousState}->${newState}`,
48129
48289
  event: flushEvent,
@@ -48132,14 +48292,16 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48132
48292
  });
48133
48293
  }
48134
48294
  return emittedDuringExec;
48135
- }, [eventBus, flushSlot]);
48295
+ }, [eventBus, flushSlot, sharedEntityStore]);
48136
48296
  const runTickEffects = React96.useCallback((tick, binding) => {
48137
48297
  const traitName = binding.trait.name;
48138
48298
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
48139
48299
  if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
48140
48300
  if (tick.guard !== void 0) {
48301
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
48302
+ const entity = sharedKey !== void 0 ? sharedEntityStore.getSnapshot(sharedKey) : traitFieldStatesRef.current.get(traitName) ?? {};
48141
48303
  const guardCtx = {
48142
- entity: traitFieldStatesRef.current.get(traitName) ?? {},
48304
+ entity,
48143
48305
  payload: {},
48144
48306
  state: currentState
48145
48307
  };
@@ -48164,10 +48326,46 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48164
48326
  syncOnly: true,
48165
48327
  log: tickLog
48166
48328
  });
48167
- }, [executeTransitionEffects]);
48329
+ }, [executeTransitionEffects, sharedEntityStore]);
48330
+ const emitFromSharedWriter = React96.useCallback((event, payload) => {
48331
+ const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
48332
+ eventBus.emit(prefixedEvent, payload);
48333
+ }, [eventBus]);
48168
48334
  React96.useEffect(() => {
48169
48335
  const scheduler = runtime.createTickScheduler();
48336
+ const writerTraitNames = /* @__PURE__ */ new Set();
48337
+ for (const group of sharedGroups.values()) {
48338
+ const ticksByInterval = /* @__PURE__ */ new Map();
48339
+ for (const binding of group.writerBindings) {
48340
+ writerTraitNames.add(binding.trait.name);
48341
+ for (const tick of binding.trait.ticks ?? []) {
48342
+ const intervalKey = String(tick.interval);
48343
+ const entries = ticksByInterval.get(intervalKey) ?? [];
48344
+ entries.push({ binding, tick });
48345
+ ticksByInterval.set(intervalKey, entries);
48346
+ }
48347
+ }
48348
+ for (const entries of ticksByInterval.values()) {
48349
+ const interval = entries[0].tick.interval;
48350
+ const onDue = () => {
48351
+ const writers = entries.map(
48352
+ ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
48353
+ );
48354
+ runTickFrame(group.storeKey, writers, sharedEntityStore);
48355
+ };
48356
+ if (interval === "frame") {
48357
+ scheduler.add(0, onDue);
48358
+ } else if (typeof interval === "number") {
48359
+ scheduler.add(interval, onDue);
48360
+ } else if (runtime.isValidCronExpression(interval)) {
48361
+ scheduler.addCron(interval, onDue);
48362
+ } else {
48363
+ scheduler.add(runtime.parseDurationString(interval), onDue);
48364
+ }
48365
+ }
48366
+ }
48170
48367
  for (const binding of traitBindings) {
48368
+ if (writerTraitNames.has(binding.trait.name)) continue;
48171
48369
  for (const tick of binding.trait.ticks ?? []) {
48172
48370
  if (tick.interval === "frame") {
48173
48371
  scheduler.add(0, () => runTickEffects(tick, binding));
@@ -48181,7 +48379,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48181
48379
  }
48182
48380
  }
48183
48381
  return () => scheduler.stopAll();
48184
- }, [traitBindings, runTickEffects]);
48382
+ }, [traitBindings, runTickEffects, sharedGroups, sharedEntityStore, emitFromSharedWriter]);
48185
48383
  const processEventQueued = React96.useCallback(async (eventKey, payload) => {
48186
48384
  const normalizedEvent = normalizeEventKey(eventKey);
48187
48385
  const bindings = traitBindingsRef.current;
@@ -48701,7 +48899,7 @@ function buildOrbitalsByTrait(schema, resolvedPages = []) {
48701
48899
  // runtime/OrbPreview.tsx
48702
48900
  init_verificationRegistry();
48703
48901
  var PERF_NAMESPACE = "almadar:perf:canvas";
48704
- var log8 = logger.createLogger(PERF_NAMESPACE);
48902
+ var log9 = logger.createLogger(PERF_NAMESPACE);
48705
48903
  var RING_SIZE = 50;
48706
48904
  var ring = [];
48707
48905
  var writeIdx = 0;
@@ -48756,7 +48954,7 @@ function perfEnd(name, startToken, detail) {
48756
48954
  }
48757
48955
  }
48758
48956
  push({ name, durationMs, ts: endTs, detail });
48759
- log8.debug(name, () => ({ durationMs, ...detail ?? {} }));
48957
+ log9.debug(name, () => ({ durationMs, ...detail ?? {} }));
48760
48958
  }
48761
48959
  function perfTime(name, fn, detail) {
48762
48960
  const t = perfStart(name);
@@ -48774,7 +48972,7 @@ var profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, com
48774
48972
  ts: commitTime,
48775
48973
  detail: { baseDuration }
48776
48974
  });
48777
- log8.debug(`profiler:${id}:${phase}`, () => ({ actualDuration, baseDuration }));
48975
+ log9.debug(`profiler:${id}:${phase}`, () => ({ actualDuration, baseDuration }));
48778
48976
  };
48779
48977
  function getPerfSnapshot() {
48780
48978
  if (ring.length < RING_SIZE) return ring.slice();