@almadar/ui 5.157.0 → 5.158.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.
@@ -145,7 +145,7 @@ interface SendEventResult {
145
145
  }
146
146
  interface ServerBridgeContextValue {
147
147
  connected: boolean;
148
- sendEvent: (orbitalName: string, event: string, payload?: EventPayload) => Promise<SendEventResult>;
148
+ sendEvent: (orbitalName: string, event: string, payload?: EventPayload, tick?: string, sourceTrait?: string) => Promise<SendEventResult>;
149
149
  }
150
150
  /**
151
151
  * Transport adapter for ServerBridgeProvider. Decouples the bridge's
@@ -168,8 +168,12 @@ interface ServerBridgeTransport {
168
168
  * `clientId` (Almadar_Live_Push.md) is the per-tab id stamped on every
169
169
  * dispatch so the server's push broadcast can exclude the origin tab
170
170
  * (it already got this cascade in-response).
171
+ *
172
+ * `tick`/`sourceTrait` (T6, docs/Almadar_Tick_Loop.md §3a) mark a
173
+ * tick-originated latest-state broadcast: the server coalesces these
174
+ * newest-per-key and relays them to other tabs at snapshot rate.
171
175
  */
172
- sendEvent: (orbitalName: string, event: string, payload?: EventPayload, clientId?: string) => Promise<OrbitalEventResponse>;
176
+ sendEvent: (orbitalName: string, event: string, payload?: EventPayload, clientId?: string, tick?: string, sourceTrait?: string) => Promise<OrbitalEventResponse>;
173
177
  }
174
178
  /**
175
179
  * Access the server bridge. Returns a no-op stub when outside the provider.
@@ -145,7 +145,7 @@ interface SendEventResult {
145
145
  }
146
146
  interface ServerBridgeContextValue {
147
147
  connected: boolean;
148
- sendEvent: (orbitalName: string, event: string, payload?: EventPayload) => Promise<SendEventResult>;
148
+ sendEvent: (orbitalName: string, event: string, payload?: EventPayload, tick?: string, sourceTrait?: string) => Promise<SendEventResult>;
149
149
  }
150
150
  /**
151
151
  * Transport adapter for ServerBridgeProvider. Decouples the bridge's
@@ -168,8 +168,12 @@ interface ServerBridgeTransport {
168
168
  * `clientId` (Almadar_Live_Push.md) is the per-tab id stamped on every
169
169
  * dispatch so the server's push broadcast can exclude the origin tab
170
170
  * (it already got this cascade in-response).
171
+ *
172
+ * `tick`/`sourceTrait` (T6, docs/Almadar_Tick_Loop.md §3a) mark a
173
+ * tick-originated latest-state broadcast: the server coalesces these
174
+ * newest-per-key and relays them to other tabs at snapshot rate.
171
175
  */
172
- sendEvent: (orbitalName: string, event: string, payload?: EventPayload, clientId?: string) => Promise<OrbitalEventResponse>;
176
+ sendEvent: (orbitalName: string, event: string, payload?: EventPayload, clientId?: string, tick?: string, sourceTrait?: string) => Promise<OrbitalEventResponse>;
173
177
  }
174
178
  /**
175
179
  * Access the server bridge. Returns a no-op stub when outside the provider.
@@ -4664,11 +4664,32 @@ function isPlainObject(value) {
4664
4664
  if (typeof value === "function") return false;
4665
4665
  return true;
4666
4666
  }
4667
+ function subtreeHasMarker(value) {
4668
+ const cached = markerPresenceCache.get(value);
4669
+ if (cached !== void 0) return cached;
4670
+ let found = false;
4671
+ const children = Array.isArray(value) ? value : Object.values(value);
4672
+ for (const child of children) {
4673
+ if (core.isRenderBindingMarker(child)) {
4674
+ found = true;
4675
+ break;
4676
+ }
4677
+ if (Array.isArray(child) || isPlainObject(child)) {
4678
+ if (subtreeHasMarker(child)) {
4679
+ found = true;
4680
+ break;
4681
+ }
4682
+ }
4683
+ }
4684
+ markerPresenceCache.set(value, found);
4685
+ return found;
4686
+ }
4667
4687
  function walkValue(value, scopeTrait, entity, config, state) {
4668
4688
  if (core.isRenderBindingMarker(value)) {
4669
4689
  return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
4670
4690
  }
4671
4691
  if (Array.isArray(value)) {
4692
+ if (!subtreeHasMarker(value)) return { resolved: value, changed: false };
4672
4693
  const out = [];
4673
4694
  let changed = false;
4674
4695
  for (const item of value) {
@@ -4686,6 +4707,7 @@ function walkValue(value, scopeTrait, entity, config, state) {
4686
4707
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4687
4708
  }
4688
4709
  if (isPlainObject(value)) {
4710
+ if (!subtreeHasMarker(value)) return { resolved: value, changed: false };
4689
4711
  const sourceTrait = value._sourceTrait;
4690
4712
  if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
4691
4713
  return { resolved: value, changed: false };
@@ -4711,9 +4733,11 @@ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
4711
4733
  }
4712
4734
  return changed ? out : props;
4713
4735
  }
4736
+ var markerPresenceCache;
4714
4737
  var init_resolve_render_bindings = __esm({
4715
4738
  "lib/resolve-render-bindings.ts"() {
4716
4739
  "use client";
4740
+ markerPresenceCache = /* @__PURE__ */ new WeakMap();
4717
4741
  }
4718
4742
  });
4719
4743
  function getCurrentIconFamily() {
@@ -29707,7 +29731,7 @@ function fileIcon(name) {
29707
29731
  return "file";
29708
29732
  }
29709
29733
  }
29710
- var TreeNodeItem, FileTree;
29734
+ var TreeNodeItem, FlatTreeNodeItem, FileTree;
29711
29735
  var init_FileTree = __esm({
29712
29736
  "components/core/molecules/FileTree.tsx"() {
29713
29737
  "use client";
@@ -29792,14 +29816,101 @@ var init_FileTree = __esm({
29792
29816
  )) })
29793
29817
  ] });
29794
29818
  };
29819
+ FlatTreeNodeItem = ({
29820
+ item,
29821
+ depth,
29822
+ indent,
29823
+ childrenByParent,
29824
+ onNodeSelect,
29825
+ defaultExpanded = false
29826
+ }) => {
29827
+ const [expanded, setExpanded] = React94.useState(defaultExpanded || depth < 1);
29828
+ const children = childrenByParent.get(item.id);
29829
+ const hasChildren = !!children && children.length > 0;
29830
+ const handleClick = React94.useCallback(() => {
29831
+ if (hasChildren) setExpanded((prev) => !prev);
29832
+ onNodeSelect?.(item.id);
29833
+ }, [hasChildren, item.id, onNodeSelect]);
29834
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
29835
+ /* @__PURE__ */ jsxRuntime.jsxs(
29836
+ Box,
29837
+ {
29838
+ className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
29839
+ style: { paddingLeft: depth * indent + 8 },
29840
+ onClick: handleClick,
29841
+ role: "treeitem",
29842
+ "aria-expanded": hasChildren ? expanded : void 0,
29843
+ children: [
29844
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
29845
+ Icon,
29846
+ {
29847
+ name: expanded ? "chevron-down" : "chevron-right",
29848
+ size: "xs",
29849
+ className: "text-[var(--color-muted-foreground)] flex-shrink-0"
29850
+ }
29851
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Box, { style: { width: 12, flexShrink: 0 } }),
29852
+ /* @__PURE__ */ jsxRuntime.jsx(
29853
+ Icon,
29854
+ {
29855
+ name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
29856
+ size: "xs",
29857
+ className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
29858
+ }
29859
+ ),
29860
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
29861
+ ]
29862
+ }
29863
+ ),
29864
+ hasChildren && expanded && /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "group", children: children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
29865
+ FlatTreeNodeItem,
29866
+ {
29867
+ item: child,
29868
+ depth: depth + 1,
29869
+ indent,
29870
+ childrenByParent,
29871
+ onNodeSelect
29872
+ },
29873
+ child.id
29874
+ )) })
29875
+ ] });
29876
+ };
29795
29877
  FileTree = ({
29796
29878
  tree,
29879
+ items,
29797
29880
  selectedPath,
29798
29881
  onFileSelect,
29882
+ onNodeSelect,
29799
29883
  className,
29800
29884
  indent = 16
29801
29885
  }) => {
29802
- if (tree.length === 0) return null;
29886
+ if (items) {
29887
+ if (items.length === 0) return null;
29888
+ const ids = new Set(items.map((node) => node.id));
29889
+ const childrenByParent = /* @__PURE__ */ new Map();
29890
+ const roots = [];
29891
+ for (const item of items) {
29892
+ if (item.parentId && ids.has(item.parentId)) {
29893
+ const siblings = childrenByParent.get(item.parentId);
29894
+ if (siblings) siblings.push(item);
29895
+ else childrenByParent.set(item.parentId, [item]);
29896
+ } else {
29897
+ roots.push(item);
29898
+ }
29899
+ }
29900
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
29901
+ FlatTreeNodeItem,
29902
+ {
29903
+ item,
29904
+ depth: 0,
29905
+ indent,
29906
+ childrenByParent,
29907
+ onNodeSelect,
29908
+ defaultExpanded: true
29909
+ },
29910
+ item.id
29911
+ )) });
29912
+ }
29913
+ if (!tree || tree.length === 0) return null;
29803
29914
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
29804
29915
  TreeNodeItem,
29805
29916
  {
@@ -31194,7 +31305,7 @@ var init_debug = __esm({
31194
31305
  logger.createLogger("almadar:ui:debug:game-state");
31195
31306
  }
31196
31307
  });
31197
- var isRelationsDebugEnabled, RelationSelect;
31308
+ var isRelationsDebugEnabled, MANY_CARDINALITIES, RelationSelect;
31198
31309
  var init_RelationSelect = __esm({
31199
31310
  "components/core/molecules/RelationSelect.tsx"() {
31200
31311
  "use client";
@@ -31208,6 +31319,11 @@ var init_RelationSelect = __esm({
31208
31319
  init_Typography();
31209
31320
  init_debug();
31210
31321
  isRelationsDebugEnabled = () => isDebugEnabled();
31322
+ MANY_CARDINALITIES = [
31323
+ "many",
31324
+ "one-to-many",
31325
+ "many-to-many"
31326
+ ];
31211
31327
  RelationSelect = ({
31212
31328
  value,
31213
31329
  onChange,
@@ -32872,17 +32988,21 @@ var init_MathCanvas = __esm({
32872
32988
  error
32873
32989
  }) => {
32874
32990
  const eventBus = useEventBus();
32991
+ const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
32992
+ const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
32993
+ const stableKeyMap = React94.useMemo(() => keyMap, [keyMapKey]);
32994
+ const stableKeyUpMap = React94.useMemo(() => keyUpMap, [keyUpMapKey]);
32875
32995
  React94.useEffect(() => {
32876
- if (!keyMap && !keyUpMap) return;
32996
+ if (!stableKeyMap && !stableKeyUpMap) return;
32877
32997
  const onDown = (e) => {
32878
- const ev = keyMap?.[e.code];
32998
+ const ev = stableKeyMap?.[e.code];
32879
32999
  if (ev) {
32880
33000
  eventBus.emit(`UI:${ev}`, {});
32881
33001
  e.preventDefault();
32882
33002
  }
32883
33003
  };
32884
33004
  const onUp = (e) => {
32885
- const ev = keyUpMap?.[e.code];
33005
+ const ev = stableKeyUpMap?.[e.code];
32886
33006
  if (ev) eventBus.emit(`UI:${ev}`, {});
32887
33007
  };
32888
33008
  window.addEventListener("keydown", onDown);
@@ -32891,7 +33011,7 @@ var init_MathCanvas = __esm({
32891
33011
  window.removeEventListener("keydown", onDown);
32892
33012
  window.removeEventListener("keyup", onUp);
32893
33013
  };
32894
- }, [keyMap, keyUpMap, eventBus]);
33014
+ }, [stableKeyMap, stableKeyUpMap, eventBus]);
32895
33015
  const derivedShapes = React94.useMemo(() => {
32896
33016
  const out = [];
32897
33017
  const margin = 24;
@@ -44919,6 +45039,9 @@ function determineInputType(field) {
44919
45039
  if (field.type === "relation" || field.relation) {
44920
45040
  return "relation";
44921
45041
  }
45042
+ if (field.type === "array") {
45043
+ return "array";
45044
+ }
44922
45045
  if (field.type === "enum" || field.values || getEnumOptions(field).length > 0) {
44923
45046
  return "select";
44924
45047
  }
@@ -44994,6 +45117,7 @@ var init_Form = __esm({
44994
45117
  init_Typography();
44995
45118
  init_Icon();
44996
45119
  init_RelationSelect();
45120
+ init_TagInput();
44997
45121
  init_UploadDropZone();
44998
45122
  init_Alert();
44999
45123
  init_useEventBus();
@@ -45073,7 +45197,7 @@ var init_Form = __esm({
45073
45197
  values: "values" in f3 ? f3.values : void 0,
45074
45198
  min: f3.min,
45075
45199
  max: f3.max,
45076
- relation: "relation" in f3 ? { entity: f3.relation.entity } : void 0
45200
+ relation: "relation" in f3 ? { entity: f3.relation.entity, cardinality: f3.relation.cardinality } : void 0
45077
45201
  })
45078
45202
  );
45079
45203
  }, [entity, fields]);
@@ -45308,7 +45432,7 @@ var init_Form = __esm({
45308
45432
  values: "values" in entityField ? entityField.values : void 0,
45309
45433
  min: entityField.min,
45310
45434
  max: entityField.max,
45311
- relation: "relation" in entityField ? { entity: entityField.relation.entity } : void 0
45435
+ relation: "relation" in entityField ? { entity: entityField.relation.entity, cardinality: entityField.relation.cardinality } : void 0
45312
45436
  };
45313
45437
  }
45314
45438
  return { name: field, type: "string" };
@@ -45420,6 +45544,22 @@ var init_Form = __esm({
45420
45544
  case "relation": {
45421
45545
  const relationOptions = relationsData[fieldName] || [];
45422
45546
  const relationLoading = relationsLoading[fieldName] || false;
45547
+ if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
45548
+ const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
45549
+ return /* @__PURE__ */ jsxRuntime.jsx(
45550
+ Select,
45551
+ {
45552
+ ...commonProps,
45553
+ multiple: true,
45554
+ searchable: true,
45555
+ clearable: true,
45556
+ options: [...relationOptions],
45557
+ value: selectedValues,
45558
+ onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
45559
+ placeholder: field.placeholder || `Select ${label}...`
45560
+ }
45561
+ );
45562
+ }
45423
45563
  return /* @__PURE__ */ jsxRuntime.jsx(
45424
45564
  RelationSelect,
45425
45565
  {
@@ -45434,6 +45574,18 @@ var init_Form = __esm({
45434
45574
  }
45435
45575
  );
45436
45576
  }
45577
+ case "array": {
45578
+ const arrayValue = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : currentValue != null && currentValue !== "" ? [String(currentValue)] : [];
45579
+ return /* @__PURE__ */ jsxRuntime.jsx(
45580
+ TagInput,
45581
+ {
45582
+ placeholder: field.placeholder,
45583
+ disabled: isLoading,
45584
+ value: arrayValue,
45585
+ onChange: (next) => handleChange(fieldName, [...next])
45586
+ }
45587
+ );
45588
+ }
45437
45589
  case "number":
45438
45590
  return /* @__PURE__ */ jsxRuntime.jsx(
45439
45591
  Input,
@@ -51002,7 +51154,10 @@ function enrichFormFields(fields, entityDef) {
51002
51154
  enriched.values = entityField.enumValues;
51003
51155
  }
51004
51156
  if (entityField.relation) {
51005
- enriched.relation = entityField.relation.entity;
51157
+ enriched.relation = {
51158
+ entity: entityField.relation.entity,
51159
+ cardinality: entityField.relation.cardinality
51160
+ };
51006
51161
  }
51007
51162
  return enriched;
51008
51163
  }
@@ -51030,7 +51185,10 @@ function enrichFormFields(fields, entityDef) {
51030
51185
  }
51031
51186
  }
51032
51187
  if (!obj.relation && entityField.relation) {
51033
- enriched.relation = entityField.relation.entity;
51188
+ enriched.relation = {
51189
+ entity: entityField.relation.entity,
51190
+ cardinality: entityField.relation.cardinality
51191
+ };
51034
51192
  }
51035
51193
  return enriched;
51036
51194
  }
@@ -51045,7 +51203,12 @@ function enrichDetailFields(fields, entityDef) {
51045
51203
  const meta = { type: entityField.type };
51046
51204
  const values = entityField.values ?? entityField.enumValues;
51047
51205
  if (values && values.length > 0) meta.values = values;
51048
- if (entityField.relation) meta.relation = entityField.relation.entity;
51206
+ if (entityField.relation) {
51207
+ meta.relation = {
51208
+ entity: entityField.relation.entity,
51209
+ cardinality: entityField.relation.cardinality
51210
+ };
51211
+ }
51049
51212
  return meta;
51050
51213
  };
51051
51214
  return fields.map((field) => {
@@ -51599,6 +51762,32 @@ function isPlainConfigObject(value) {
51599
51762
  const proto = Object.getPrototypeOf(value);
51600
51763
  return proto === Object.prototype || proto === null;
51601
51764
  }
51765
+ function subtreeHasTraitRef(value) {
51766
+ const cached = traitRefPresenceCache.get(value);
51767
+ if (cached !== void 0) return cached;
51768
+ let found = false;
51769
+ const children = Array.isArray(value) ? value : Object.values(value);
51770
+ for (const child of children) {
51771
+ if (typeof child === "string" && TRAIT_BINDING_RE.test(child)) {
51772
+ found = true;
51773
+ break;
51774
+ }
51775
+ if (core.isRenderBindingMarker(child)) continue;
51776
+ if (Array.isArray(child)) {
51777
+ if (subtreeHasTraitRef(child)) {
51778
+ found = true;
51779
+ break;
51780
+ }
51781
+ } else if (child !== null && typeof child === "object" && isPlainConfigObject(child)) {
51782
+ if (subtreeHasTraitRef(child)) {
51783
+ found = true;
51784
+ break;
51785
+ }
51786
+ }
51787
+ }
51788
+ traitRefPresenceCache.set(value, found);
51789
+ return found;
51790
+ }
51602
51791
  function substituteTraitRefsDeep(value, pathKey) {
51603
51792
  if (core.isRenderBindingMarker(value)) return value;
51604
51793
  if (typeof value === "string") {
@@ -51613,11 +51802,13 @@ function substituteTraitRefsDeep(value, pathKey) {
51613
51802
  return value;
51614
51803
  }
51615
51804
  if (Array.isArray(value)) {
51805
+ if (!subtreeHasTraitRef(value)) return value;
51616
51806
  return value.map(
51617
51807
  (item, i) => substituteTraitRefsDeep(item, `${pathKey}[${i}]`)
51618
51808
  );
51619
51809
  }
51620
51810
  if (typeof value === "object" && isPlainConfigObject(value)) {
51811
+ if (!subtreeHasTraitRef(value)) return value;
51621
51812
  const out = {};
51622
51813
  for (const [k, v] of Object.entries(value)) {
51623
51814
  out[k] = substituteTraitRefsDeep(v, `${pathKey}.${k}`);
@@ -51906,7 +52097,7 @@ function UISlotRenderer({
51906
52097
  }
51907
52098
  return wrapped;
51908
52099
  }
51909
- var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN;
52100
+ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN, traitRefPresenceCache;
51910
52101
  var init_UISlotRenderer = __esm({
51911
52102
  "components/core/organisms/UISlotRenderer.tsx"() {
51912
52103
  "use client";
@@ -51980,6 +52171,7 @@ var init_UISlotRenderer = __esm({
51980
52171
  "alert",
51981
52172
  "dialog"
51982
52173
  ]);
52174
+ traitRefPresenceCache = /* @__PURE__ */ new WeakMap();
51983
52175
  UISlotRenderer.displayName = "UISlotRenderer";
51984
52176
  }
51985
52177
  });
@@ -55654,6 +55846,20 @@ function createClientEffectHandlers(options) {
55654
55846
  };
55655
55847
  }
55656
55848
 
55849
+ // lib/event-queue-coalesce.ts
55850
+ function enqueueEvent(queue, entry) {
55851
+ if (entry.tick !== void 0) {
55852
+ const pending = queue.find(
55853
+ (e) => e.tick !== void 0 && e.eventKey === entry.eventKey && e.targetTrait === entry.targetTrait
55854
+ );
55855
+ if (pending !== void 0) {
55856
+ pending.payload = entry.payload;
55857
+ return;
55858
+ }
55859
+ }
55860
+ queue.push(entry);
55861
+ }
55862
+
55657
55863
  // hooks/useTraitStateMachine.ts
55658
55864
  init_traitRegistry();
55659
55865
  init_verificationRegistry();
@@ -56228,12 +56434,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56228
56434
  entityId
56229
56435
  };
56230
56436
  const emittedDuringExec = [];
56437
+ const tickName = flushEvent.startsWith("tick:") ? flushEvent.slice(5) : void 0;
56231
56438
  const baseEmit = handlers.emit;
56232
56439
  const trackingHandlers = {
56233
56440
  ...handlers,
56234
56441
  emit: (event, eventPayload, source) => {
56235
56442
  emittedDuringExec.push(event);
56236
- baseEmit(event, eventPayload, source);
56443
+ baseEmit(event, eventPayload, tickName !== void 0 ? { ...source, tick: tickName } : source);
56237
56444
  }
56238
56445
  };
56239
56446
  if (traitName === "Hero") {
@@ -56392,7 +56599,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56392
56599
  }
56393
56600
  return () => scheduler.stopAll();
56394
56601
  }, [traitBindings, runTickEffects, sharedGroups, sharedEntityStore, emitFromSharedWriter]);
56395
- const processEventQueued = React94.useCallback(async (eventKey, payload, targetTrait) => {
56602
+ const processEventQueued = React94.useCallback(async (eventKey, payload, targetTrait, tick, sourceTrait) => {
56396
56603
  const normalizedEvent = normalizeEventKey(eventKey);
56397
56604
  const bindings = traitBindingsRef.current;
56398
56605
  const currentManager = managerRef.current;
@@ -56581,7 +56788,11 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56581
56788
  if (orbital) dispatchedOrbitals.add(orbital);
56582
56789
  }
56583
56790
  const relayPayload = targetTrait !== void 0 ? { ...payload ?? {}, _targetTrait: targetTrait } : payload;
56584
- await onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals);
56791
+ if (tick !== void 0) {
56792
+ void onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals, tick, sourceTrait);
56793
+ } else {
56794
+ await onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals);
56795
+ }
56585
56796
  }
56586
56797
  }, [entities, eventBus, sharedEntityStore]);
56587
56798
  const drainEventQueue = React94.useCallback(async () => {
@@ -56590,14 +56801,14 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56590
56801
  try {
56591
56802
  while (eventQueueRef.current.length > 0) {
56592
56803
  const entry = eventQueueRef.current.shift();
56593
- await processEventQueued(entry.eventKey, entry.payload, entry.targetTrait);
56804
+ await processEventQueued(entry.eventKey, entry.payload, entry.targetTrait, entry.tick, entry.sourceTrait);
56594
56805
  }
56595
56806
  } finally {
56596
56807
  processingRef.current = false;
56597
56808
  }
56598
56809
  }, [processEventQueued]);
56599
- const enqueueAndDrain = React94.useCallback((eventKey, payload, targetTrait) => {
56600
- eventQueueRef.current.push({ eventKey, payload, targetTrait });
56810
+ const enqueueAndDrain = React94.useCallback((eventKey, payload, targetTrait, tick, sourceTrait) => {
56811
+ enqueueEvent(eventQueueRef.current, { eventKey, payload, targetTrait, tick, sourceTrait });
56601
56812
  void drainEventQueue();
56602
56813
  }, [drainEventQueue]);
56603
56814
  React94.useCallback((eventKey, payload) => {
@@ -56650,7 +56861,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56650
56861
  crossTraitLog.debug("self:fire-server-cascade", { traitName, busKey: selfBusKey, eventKey });
56651
56862
  }
56652
56863
  crossTraitLog.debug("self:fire", { traitName, busKey: selfBusKey, eventKey });
56653
- enqueueAndDrain(eventKey, event.payload, traitName);
56864
+ enqueueAndDrain(eventKey, event.payload, traitName, event.source?.tick, event.source?.trait);
56654
56865
  });
56655
56866
  unsubscribes.push(() => {
56656
56867
  crossTraitLog.debug("self:unsubscribe", { traitName, busKey: selfBusKey, eventKey });
@@ -56668,7 +56879,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56668
56879
  const bareKey = `UI:${eventKey}`;
56669
56880
  const unsub = eventBus.on(bareKey, (event) => {
56670
56881
  crossTraitLog.debug("bare-cascade:fire", { bareKey, eventKey });
56671
- enqueueAndDrain(eventKey, event.payload);
56882
+ enqueueAndDrain(eventKey, event.payload, void 0, event.source?.tick, event.source?.trait);
56672
56883
  });
56673
56884
  unsubscribes.push(() => {
56674
56885
  crossTraitLog.debug("bare-cascade:unsubscribe", { bareKey, eventKey });
@@ -56702,7 +56913,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56702
56913
  enqueueAndDrain(
56703
56914
  listen.triggers,
56704
56915
  core.applyListenPayloadMapping(listen.payloadMapping, event.payload, evaluator.evaluateListenPayloadExpr),
56705
- binding.trait.name
56916
+ binding.trait.name,
56917
+ event.source?.tick,
56918
+ event.source?.trait
56706
56919
  );
56707
56920
  });
56708
56921
  unsubscribes.push(() => {
@@ -56894,7 +57107,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
56894
57107
  [serverActiveTraits]
56895
57108
  );
56896
57109
  const uiSlots = context.useUISlots();
56897
- const onEventProcessed = React94.useCallback(async (event, payload, dispatchedOrbitals) => {
57110
+ const onEventProcessed = React94.useCallback(async (event, payload, dispatchedOrbitals, tick, sourceTrait) => {
56898
57111
  if (!bridge.connected || !orbitalNames?.length) return;
56899
57112
  const targets = dispatchedOrbitals && dispatchedOrbitals.size > 0 ? orbitalNames.filter((n) => dispatchedOrbitals.has(n)) : orbitalNames;
56900
57113
  xOrbitalLog.debug("TraitInitializer:fanout", () => ({
@@ -56904,6 +57117,10 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
56904
57117
  dispatchedOrbitalsSize: dispatchedOrbitals?.size ?? 0
56905
57118
  }));
56906
57119
  for (const name of targets) {
57120
+ if (tick !== void 0) {
57121
+ void bridge.sendEvent(name, event, withActiveTraits(payload), tick, sourceTrait);
57122
+ continue;
57123
+ }
56907
57124
  const { effects, meta } = await bridge.sendEvent(name, event, withActiveTraits(payload));
56908
57125
  recordServerResponse(name, event, meta);
56909
57126
  applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
@@ -57379,11 +57596,13 @@ function BrowserPlayground({
57379
57596
  unregister: async () => {
57380
57597
  runtime.unregisterAll();
57381
57598
  },
57382
- sendEvent: async (orbitalName, event, payload) => {
57599
+ sendEvent: async (orbitalName, event, payload, _clientId, tick, sourceTrait) => {
57383
57600
  await registrationReady;
57384
57601
  return runtime.processOrbitalEvent(orbitalName, {
57385
57602
  event,
57386
- payload
57603
+ payload,
57604
+ tick,
57605
+ sourceTrait
57387
57606
  // @almadar/runtime OrbitalEventResponse.clientEffects uses a wider ClientEffectTuple than
57388
57607
  // ServerBridge's local definition — cast at this boundary (upstream fix queued).
57389
57608
  });