@almadar/ui 5.50.0 → 5.52.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.
@@ -57351,7 +57351,7 @@ function convertFnFormLambdasInProps(props) {
57351
57351
  }
57352
57352
  var log10 = logger.createLogger("almadar:ui:effects:client-handlers");
57353
57353
  function createClientEffectHandlers(options) {
57354
- const { eventBus, slotSetter, navigate, notify, callService } = options;
57354
+ const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
57355
57355
  return {
57356
57356
  emit: (event, payload) => {
57357
57357
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
@@ -57360,8 +57360,12 @@ function createClientEffectHandlers(options) {
57360
57360
  persist: async () => {
57361
57361
  log10.warn("persist is server-side only, ignored on client");
57362
57362
  },
57363
- set: () => {
57364
- log10.warn("set is server-side only, ignored on client");
57363
+ set: (_entityId, field, value) => {
57364
+ if (!liveEntity) {
57365
+ log10.warn("set is server-side only, ignored on client (no live entity)");
57366
+ return;
57367
+ }
57368
+ liveEntity[field] = value;
57365
57369
  },
57366
57370
  callService: async (service, action, params) => {
57367
57371
  if (callService) return callService(service, action, params);
@@ -57426,6 +57430,7 @@ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
57426
57430
  "do",
57427
57431
  "when"
57428
57432
  ]);
57433
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
57429
57434
  function toTraitDefinition(binding) {
57430
57435
  return {
57431
57436
  name: binding.trait.name,
@@ -57458,6 +57463,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57458
57463
  });
57459
57464
  const eventQueueRef = React90.useRef([]);
57460
57465
  const processingRef = React90.useRef(false);
57466
+ const initedTraitsRef = React90.useRef(/* @__PURE__ */ new Set());
57461
57467
  const traitBindingsRef = React90.useRef(traitBindings);
57462
57468
  const managerRef = React90.useRef(manager);
57463
57469
  const uiSlotsRef = React90.useRef(uiSlots);
@@ -57627,6 +57633,11 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57627
57633
  const traitName = binding.trait.name;
57628
57634
  const linkedEntity = binding.linkedEntity || "";
57629
57635
  const entityId = payload?.entityId;
57636
+ let liveEntity = traitFieldStatesRef.current.get(traitName);
57637
+ if (!liveEntity) {
57638
+ liveEntity = {};
57639
+ traitFieldStatesRef.current.set(traitName, liveEntity);
57640
+ }
57630
57641
  const effects = syncOnly ? params.effects.filter(
57631
57642
  (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
57632
57643
  ) : params.effects;
@@ -57649,7 +57660,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57649
57660
  },
57650
57661
  navigate: optionsRef.current?.navigate,
57651
57662
  notify: optionsRef.current?.notify,
57652
- callService: optionsRef.current?.callService
57663
+ callService: optionsRef.current?.callService,
57664
+ // The canonical client `set` writes `(set @entity.X)` straight into
57665
+ // the trait's one live store — the same object bound below. This is
57666
+ // what makes a [runtime] entity's set (from ticks AND events) reach
57667
+ // the render-ui + next tick + guards.
57668
+ liveEntity
57653
57669
  });
57654
57670
  const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
57655
57671
  let handlers = clientHandlers;
@@ -57660,7 +57676,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57660
57676
  // uses below. `@payload.*` resolves from `payload` separately,
57661
57677
  // so dropping the prior `payload as EntityRow` cast loses
57662
57678
  // nothing — it just stops mislabelling the payload as an entity.
57663
- entity: traitFieldStatesRef.current.get(traitName) ?? {},
57679
+ entity: liveEntity,
57664
57680
  payload: payload || {},
57665
57681
  state: previousState
57666
57682
  };
@@ -57700,24 +57716,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57700
57716
  handlers = {
57701
57717
  ...handlers,
57702
57718
  set: async (targetId, field, value) => {
57703
- let fieldState = traitFieldStatesRef.current.get(traitName);
57704
- if (!fieldState) {
57705
- fieldState = {};
57706
- traitFieldStatesRef.current.set(traitName, fieldState);
57707
- }
57708
- fieldState[field] = value;
57719
+ if (baseSet) await baseSet(targetId, field, value);
57709
57720
  log15.debug("set:write", {
57710
57721
  traitName,
57711
57722
  field,
57712
57723
  value: JSON.stringify(value),
57713
57724
  transition: `${previousState}->${newState}`
57714
57725
  });
57715
- if (baseSet) await baseSet(targetId, field, value);
57716
57726
  }
57717
57727
  };
57718
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
57719
57728
  const bindingCtx = {
57720
- entity: entityForBinding,
57729
+ entity: liveEntity,
57721
57730
  payload: payload || {},
57722
57731
  state: previousState
57723
57732
  };
@@ -58070,7 +58079,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
58070
58079
  if (!orbitalName) continue;
58071
58080
  for (const transition of binding.trait.transitions) {
58072
58081
  const eventKey = transition.event;
58073
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
58082
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
58074
58083
  continue;
58075
58084
  }
58076
58085
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -58125,6 +58134,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
58125
58134
  crossTraitLog.debug("cleanup:done", {});
58126
58135
  };
58127
58136
  }, [traitBindings, eventBus, enqueueAndDrain]);
58137
+ React90.useEffect(() => {
58138
+ const mgr = managerRef.current;
58139
+ const inited = initedTraitsRef.current;
58140
+ for (const binding of traitBindings) {
58141
+ const traitName = binding.trait.name;
58142
+ if (inited.has(traitName)) continue;
58143
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
58144
+ (evt) => mgr.canHandleEvent(traitName, evt)
58145
+ );
58146
+ if (lifecycleEvent === void 0) continue;
58147
+ inited.add(traitName);
58148
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
58149
+ enqueueAndDrain(lifecycleEvent, {});
58150
+ }
58151
+ return () => {
58152
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
58153
+ };
58154
+ }, [traitBindings, enqueueAndDrain]);
58128
58155
  return {
58129
58156
  traitStates,
58130
58157
  sendEvent,
package/dist/avl/index.js CHANGED
@@ -57304,7 +57304,7 @@ function convertFnFormLambdasInProps(props) {
57304
57304
  }
57305
57305
  var log10 = createLogger("almadar:ui:effects:client-handlers");
57306
57306
  function createClientEffectHandlers(options) {
57307
- const { eventBus, slotSetter, navigate, notify, callService } = options;
57307
+ const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
57308
57308
  return {
57309
57309
  emit: (event, payload) => {
57310
57310
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
@@ -57313,8 +57313,12 @@ function createClientEffectHandlers(options) {
57313
57313
  persist: async () => {
57314
57314
  log10.warn("persist is server-side only, ignored on client");
57315
57315
  },
57316
- set: () => {
57317
- log10.warn("set is server-side only, ignored on client");
57316
+ set: (_entityId, field, value) => {
57317
+ if (!liveEntity) {
57318
+ log10.warn("set is server-side only, ignored on client (no live entity)");
57319
+ return;
57320
+ }
57321
+ liveEntity[field] = value;
57318
57322
  },
57319
57323
  callService: async (service, action, params) => {
57320
57324
  if (callService) return callService(service, action, params);
@@ -57379,6 +57383,7 @@ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
57379
57383
  "do",
57380
57384
  "when"
57381
57385
  ]);
57386
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
57382
57387
  function toTraitDefinition(binding) {
57383
57388
  return {
57384
57389
  name: binding.trait.name,
@@ -57411,6 +57416,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57411
57416
  });
57412
57417
  const eventQueueRef = useRef([]);
57413
57418
  const processingRef = useRef(false);
57419
+ const initedTraitsRef = useRef(/* @__PURE__ */ new Set());
57414
57420
  const traitBindingsRef = useRef(traitBindings);
57415
57421
  const managerRef = useRef(manager);
57416
57422
  const uiSlotsRef = useRef(uiSlots);
@@ -57580,6 +57586,11 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57580
57586
  const traitName = binding.trait.name;
57581
57587
  const linkedEntity = binding.linkedEntity || "";
57582
57588
  const entityId = payload?.entityId;
57589
+ let liveEntity = traitFieldStatesRef.current.get(traitName);
57590
+ if (!liveEntity) {
57591
+ liveEntity = {};
57592
+ traitFieldStatesRef.current.set(traitName, liveEntity);
57593
+ }
57583
57594
  const effects = syncOnly ? params.effects.filter(
57584
57595
  (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
57585
57596
  ) : params.effects;
@@ -57602,7 +57613,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57602
57613
  },
57603
57614
  navigate: optionsRef.current?.navigate,
57604
57615
  notify: optionsRef.current?.notify,
57605
- callService: optionsRef.current?.callService
57616
+ callService: optionsRef.current?.callService,
57617
+ // The canonical client `set` writes `(set @entity.X)` straight into
57618
+ // the trait's one live store — the same object bound below. This is
57619
+ // what makes a [runtime] entity's set (from ticks AND events) reach
57620
+ // the render-ui + next tick + guards.
57621
+ liveEntity
57606
57622
  });
57607
57623
  const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
57608
57624
  let handlers = clientHandlers;
@@ -57613,7 +57629,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57613
57629
  // uses below. `@payload.*` resolves from `payload` separately,
57614
57630
  // so dropping the prior `payload as EntityRow` cast loses
57615
57631
  // nothing — it just stops mislabelling the payload as an entity.
57616
- entity: traitFieldStatesRef.current.get(traitName) ?? {},
57632
+ entity: liveEntity,
57617
57633
  payload: payload || {},
57618
57634
  state: previousState
57619
57635
  };
@@ -57653,24 +57669,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57653
57669
  handlers = {
57654
57670
  ...handlers,
57655
57671
  set: async (targetId, field, value) => {
57656
- let fieldState = traitFieldStatesRef.current.get(traitName);
57657
- if (!fieldState) {
57658
- fieldState = {};
57659
- traitFieldStatesRef.current.set(traitName, fieldState);
57660
- }
57661
- fieldState[field] = value;
57672
+ if (baseSet) await baseSet(targetId, field, value);
57662
57673
  log15.debug("set:write", {
57663
57674
  traitName,
57664
57675
  field,
57665
57676
  value: JSON.stringify(value),
57666
57677
  transition: `${previousState}->${newState}`
57667
57678
  });
57668
- if (baseSet) await baseSet(targetId, field, value);
57669
57679
  }
57670
57680
  };
57671
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
57672
57681
  const bindingCtx = {
57673
- entity: entityForBinding,
57682
+ entity: liveEntity,
57674
57683
  payload: payload || {},
57675
57684
  state: previousState
57676
57685
  };
@@ -58023,7 +58032,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
58023
58032
  if (!orbitalName) continue;
58024
58033
  for (const transition of binding.trait.transitions) {
58025
58034
  const eventKey = transition.event;
58026
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
58035
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
58027
58036
  continue;
58028
58037
  }
58029
58038
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -58078,6 +58087,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
58078
58087
  crossTraitLog.debug("cleanup:done", {});
58079
58088
  };
58080
58089
  }, [traitBindings, eventBus, enqueueAndDrain]);
58090
+ useEffect(() => {
58091
+ const mgr = managerRef.current;
58092
+ const inited = initedTraitsRef.current;
58093
+ for (const binding of traitBindings) {
58094
+ const traitName = binding.trait.name;
58095
+ if (inited.has(traitName)) continue;
58096
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
58097
+ (evt) => mgr.canHandleEvent(traitName, evt)
58098
+ );
58099
+ if (lifecycleEvent === void 0) continue;
58100
+ inited.add(traitName);
58101
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
58102
+ enqueueAndDrain(lifecycleEvent, {});
58103
+ }
58104
+ return () => {
58105
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
58106
+ };
58107
+ }, [traitBindings, enqueueAndDrain]);
58081
58108
  return {
58082
58109
  traitStates,
58083
58110
  sendEvent,
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
- import type { EventPayload, ServiceParams } from '@almadar/core';
8
+ import type { EventPayload, EntityRow, ServiceParams } from '@almadar/core';
9
9
  import type { EffectHandlers } from '@almadar/runtime';
10
10
  export interface ClientEventBus {
11
11
  emit: (type: string, payload?: EventPayload) => void;
@@ -19,6 +19,17 @@ export interface CreateClientEffectHandlersOptions {
19
19
  slotSetter: SlotSetter;
20
20
  navigate?: (path: string, params?: Record<string, unknown>) => void;
21
21
  notify?: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void;
22
+ /**
23
+ * Live client-entity write target for `[runtime]` entities. `(set
24
+ * @entity.<field> value)` runs entirely in the browser for in-memory
25
+ * entities (game boards, wizards): there is no server row to persist to,
26
+ * so the canonical client `set` must mutate THIS object — the same object
27
+ * `EffectExecutor` reads `@entity.*` from for the current `executeAll` and
28
+ * the next tick seeds from. When omitted, `set` is a no-op (bridge mode:
29
+ * the server owns persistence). One store, read live by render-ui, guards,
30
+ * and ticks — no guard-vs-render split.
31
+ */
32
+ liveEntity?: EntityRow;
22
33
  /**
23
34
  * Optional consumer-supplied call-service handler. When set, it runs
24
35
  * instead of the default mock fallback — use to wire the playground
@@ -51279,7 +51279,7 @@ var init_UISlotRenderer = __esm({
51279
51279
  });
51280
51280
  var log = logger.createLogger("almadar:ui:effects:client-handlers");
51281
51281
  function createClientEffectHandlers(options) {
51282
- const { eventBus, slotSetter, navigate, notify, callService } = options;
51282
+ const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
51283
51283
  return {
51284
51284
  emit: (event, payload) => {
51285
51285
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
@@ -51288,8 +51288,12 @@ function createClientEffectHandlers(options) {
51288
51288
  persist: async () => {
51289
51289
  log.warn("persist is server-side only, ignored on client");
51290
51290
  },
51291
- set: () => {
51292
- log.warn("set is server-side only, ignored on client");
51291
+ set: (_entityId, field, value) => {
51292
+ if (!liveEntity) {
51293
+ log.warn("set is server-side only, ignored on client (no live entity)");
51294
+ return;
51295
+ }
51296
+ liveEntity[field] = value;
51293
51297
  },
51294
51298
  callService: async (service, action, params) => {
51295
51299
  if (callService) return callService(service, action, params);
@@ -51480,6 +51484,7 @@ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
51480
51484
  "do",
51481
51485
  "when"
51482
51486
  ]);
51487
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
51483
51488
  function toTraitDefinition(binding) {
51484
51489
  return {
51485
51490
  name: binding.trait.name,
@@ -51512,6 +51517,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51512
51517
  });
51513
51518
  const eventQueueRef = React81.useRef([]);
51514
51519
  const processingRef = React81.useRef(false);
51520
+ const initedTraitsRef = React81.useRef(/* @__PURE__ */ new Set());
51515
51521
  const traitBindingsRef = React81.useRef(traitBindings);
51516
51522
  const managerRef = React81.useRef(manager);
51517
51523
  const uiSlotsRef = React81.useRef(uiSlots);
@@ -51681,6 +51687,11 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51681
51687
  const traitName = binding.trait.name;
51682
51688
  const linkedEntity = binding.linkedEntity || "";
51683
51689
  const entityId = payload?.entityId;
51690
+ let liveEntity = traitFieldStatesRef.current.get(traitName);
51691
+ if (!liveEntity) {
51692
+ liveEntity = {};
51693
+ traitFieldStatesRef.current.set(traitName, liveEntity);
51694
+ }
51684
51695
  const effects = syncOnly ? params.effects.filter(
51685
51696
  (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
51686
51697
  ) : params.effects;
@@ -51703,7 +51714,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51703
51714
  },
51704
51715
  navigate: optionsRef.current?.navigate,
51705
51716
  notify: optionsRef.current?.notify,
51706
- callService: optionsRef.current?.callService
51717
+ callService: optionsRef.current?.callService,
51718
+ // The canonical client `set` writes `(set @entity.X)` straight into
51719
+ // the trait's one live store — the same object bound below. This is
51720
+ // what makes a [runtime] entity's set (from ticks AND events) reach
51721
+ // the render-ui + next tick + guards.
51722
+ liveEntity
51707
51723
  });
51708
51724
  const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
51709
51725
  let handlers = clientHandlers;
@@ -51714,7 +51730,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51714
51730
  // uses below. `@payload.*` resolves from `payload` separately,
51715
51731
  // so dropping the prior `payload as EntityRow` cast loses
51716
51732
  // nothing — it just stops mislabelling the payload as an entity.
51717
- entity: traitFieldStatesRef.current.get(traitName) ?? {},
51733
+ entity: liveEntity,
51718
51734
  payload: payload || {},
51719
51735
  state: previousState
51720
51736
  };
@@ -51754,24 +51770,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51754
51770
  handlers = {
51755
51771
  ...handlers,
51756
51772
  set: async (targetId, field, value) => {
51757
- let fieldState = traitFieldStatesRef.current.get(traitName);
51758
- if (!fieldState) {
51759
- fieldState = {};
51760
- traitFieldStatesRef.current.set(traitName, fieldState);
51761
- }
51762
- fieldState[field] = value;
51773
+ if (baseSet) await baseSet(targetId, field, value);
51763
51774
  log14.debug("set:write", {
51764
51775
  traitName,
51765
51776
  field,
51766
51777
  value: JSON.stringify(value),
51767
51778
  transition: `${previousState}->${newState}`
51768
51779
  });
51769
- if (baseSet) await baseSet(targetId, field, value);
51770
51780
  }
51771
51781
  };
51772
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
51773
51782
  const bindingCtx = {
51774
- entity: entityForBinding,
51783
+ entity: liveEntity,
51775
51784
  payload: payload || {},
51776
51785
  state: previousState
51777
51786
  };
@@ -52124,7 +52133,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52124
52133
  if (!orbitalName) continue;
52125
52134
  for (const transition of binding.trait.transitions) {
52126
52135
  const eventKey = transition.event;
52127
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
52136
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
52128
52137
  continue;
52129
52138
  }
52130
52139
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -52179,6 +52188,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52179
52188
  crossTraitLog.debug("cleanup:done", {});
52180
52189
  };
52181
52190
  }, [traitBindings, eventBus, enqueueAndDrain]);
52191
+ React81.useEffect(() => {
52192
+ const mgr = managerRef.current;
52193
+ const inited = initedTraitsRef.current;
52194
+ for (const binding of traitBindings) {
52195
+ const traitName = binding.trait.name;
52196
+ if (inited.has(traitName)) continue;
52197
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
52198
+ (evt) => mgr.canHandleEvent(traitName, evt)
52199
+ );
52200
+ if (lifecycleEvent === void 0) continue;
52201
+ inited.add(traitName);
52202
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
52203
+ enqueueAndDrain(lifecycleEvent, {});
52204
+ }
52205
+ return () => {
52206
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
52207
+ };
52208
+ }, [traitBindings, enqueueAndDrain]);
52182
52209
  return {
52183
52210
  traitStates,
52184
52211
  sendEvent,
@@ -51232,7 +51232,7 @@ var init_UISlotRenderer = __esm({
51232
51232
  });
51233
51233
  var log = createLogger("almadar:ui:effects:client-handlers");
51234
51234
  function createClientEffectHandlers(options) {
51235
- const { eventBus, slotSetter, navigate, notify, callService } = options;
51235
+ const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
51236
51236
  return {
51237
51237
  emit: (event, payload) => {
51238
51238
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
@@ -51241,8 +51241,12 @@ function createClientEffectHandlers(options) {
51241
51241
  persist: async () => {
51242
51242
  log.warn("persist is server-side only, ignored on client");
51243
51243
  },
51244
- set: () => {
51245
- log.warn("set is server-side only, ignored on client");
51244
+ set: (_entityId, field, value) => {
51245
+ if (!liveEntity) {
51246
+ log.warn("set is server-side only, ignored on client (no live entity)");
51247
+ return;
51248
+ }
51249
+ liveEntity[field] = value;
51246
51250
  },
51247
51251
  callService: async (service, action, params) => {
51248
51252
  if (callService) return callService(service, action, params);
@@ -51433,6 +51437,7 @@ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
51433
51437
  "do",
51434
51438
  "when"
51435
51439
  ]);
51440
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
51436
51441
  function toTraitDefinition(binding) {
51437
51442
  return {
51438
51443
  name: binding.trait.name,
@@ -51465,6 +51470,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51465
51470
  });
51466
51471
  const eventQueueRef = useRef([]);
51467
51472
  const processingRef = useRef(false);
51473
+ const initedTraitsRef = useRef(/* @__PURE__ */ new Set());
51468
51474
  const traitBindingsRef = useRef(traitBindings);
51469
51475
  const managerRef = useRef(manager);
51470
51476
  const uiSlotsRef = useRef(uiSlots);
@@ -51634,6 +51640,11 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51634
51640
  const traitName = binding.trait.name;
51635
51641
  const linkedEntity = binding.linkedEntity || "";
51636
51642
  const entityId = payload?.entityId;
51643
+ let liveEntity = traitFieldStatesRef.current.get(traitName);
51644
+ if (!liveEntity) {
51645
+ liveEntity = {};
51646
+ traitFieldStatesRef.current.set(traitName, liveEntity);
51647
+ }
51637
51648
  const effects = syncOnly ? params.effects.filter(
51638
51649
  (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
51639
51650
  ) : params.effects;
@@ -51656,7 +51667,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51656
51667
  },
51657
51668
  navigate: optionsRef.current?.navigate,
51658
51669
  notify: optionsRef.current?.notify,
51659
- callService: optionsRef.current?.callService
51670
+ callService: optionsRef.current?.callService,
51671
+ // The canonical client `set` writes `(set @entity.X)` straight into
51672
+ // the trait's one live store — the same object bound below. This is
51673
+ // what makes a [runtime] entity's set (from ticks AND events) reach
51674
+ // the render-ui + next tick + guards.
51675
+ liveEntity
51660
51676
  });
51661
51677
  const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
51662
51678
  let handlers = clientHandlers;
@@ -51667,7 +51683,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51667
51683
  // uses below. `@payload.*` resolves from `payload` separately,
51668
51684
  // so dropping the prior `payload as EntityRow` cast loses
51669
51685
  // nothing — it just stops mislabelling the payload as an entity.
51670
- entity: traitFieldStatesRef.current.get(traitName) ?? {},
51686
+ entity: liveEntity,
51671
51687
  payload: payload || {},
51672
51688
  state: previousState
51673
51689
  };
@@ -51707,24 +51723,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51707
51723
  handlers = {
51708
51724
  ...handlers,
51709
51725
  set: async (targetId, field, value) => {
51710
- let fieldState = traitFieldStatesRef.current.get(traitName);
51711
- if (!fieldState) {
51712
- fieldState = {};
51713
- traitFieldStatesRef.current.set(traitName, fieldState);
51714
- }
51715
- fieldState[field] = value;
51726
+ if (baseSet) await baseSet(targetId, field, value);
51716
51727
  log14.debug("set:write", {
51717
51728
  traitName,
51718
51729
  field,
51719
51730
  value: JSON.stringify(value),
51720
51731
  transition: `${previousState}->${newState}`
51721
51732
  });
51722
- if (baseSet) await baseSet(targetId, field, value);
51723
51733
  }
51724
51734
  };
51725
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
51726
51735
  const bindingCtx = {
51727
- entity: entityForBinding,
51736
+ entity: liveEntity,
51728
51737
  payload: payload || {},
51729
51738
  state: previousState
51730
51739
  };
@@ -52077,7 +52086,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52077
52086
  if (!orbitalName) continue;
52078
52087
  for (const transition of binding.trait.transitions) {
52079
52088
  const eventKey = transition.event;
52080
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
52089
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
52081
52090
  continue;
52082
52091
  }
52083
52092
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -52132,6 +52141,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52132
52141
  crossTraitLog.debug("cleanup:done", {});
52133
52142
  };
52134
52143
  }, [traitBindings, eventBus, enqueueAndDrain]);
52144
+ useEffect(() => {
52145
+ const mgr = managerRef.current;
52146
+ const inited = initedTraitsRef.current;
52147
+ for (const binding of traitBindings) {
52148
+ const traitName = binding.trait.name;
52149
+ if (inited.has(traitName)) continue;
52150
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
52151
+ (evt) => mgr.canHandleEvent(traitName, evt)
52152
+ );
52153
+ if (lifecycleEvent === void 0) continue;
52154
+ inited.add(traitName);
52155
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
52156
+ enqueueAndDrain(lifecycleEvent, {});
52157
+ }
52158
+ return () => {
52159
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
52160
+ };
52161
+ }, [traitBindings, enqueueAndDrain]);
52135
52162
  return {
52136
52163
  traitStates,
52137
52164
  sendEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "5.50.0",
3
+ "version": "5.52.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "sideEffects": [