@jay-framework/stack-client-runtime 0.10.0 → 0.12.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.
package/dist/index.cjs CHANGED
@@ -7,7 +7,8 @@ var __publicField = (obj, key, value) => {
7
7
  };
8
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
9
  const component = require("@jay-framework/component");
10
- function deepMergeViewStates(base, overlay, trackByMap, path = "") {
10
+ const runtime = require("@jay-framework/runtime");
11
+ function deepMergeViewStates$1(base, overlay, trackByMap, path = "") {
11
12
  if (!base && !overlay)
12
13
  return {};
13
14
  if (!base)
@@ -27,7 +28,7 @@ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
27
28
  } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
28
29
  const trackByField = trackByMap[currentPath];
29
30
  if (trackByField) {
30
- result[key] = mergeArraysByTrackBy(
31
+ result[key] = mergeArraysByTrackBy$1(
31
32
  baseValue,
32
33
  overlayValue,
33
34
  trackByField,
@@ -38,14 +39,14 @@ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
38
39
  result[key] = overlayValue;
39
40
  }
40
41
  } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
41
- result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
42
+ result[key] = deepMergeViewStates$1(baseValue, overlayValue, trackByMap, currentPath);
42
43
  } else {
43
44
  result[key] = overlayValue;
44
45
  }
45
46
  }
46
47
  return result;
47
48
  }
48
- function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
49
+ function mergeArraysByTrackBy$1(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
49
50
  const baseByKey = /* @__PURE__ */ new Map();
50
51
  for (const item of baseArray) {
51
52
  const key = item[trackByField];
@@ -72,12 +73,29 @@ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap,
72
73
  }
73
74
  const overlayItem = overlayByKey.get(key);
74
75
  if (overlayItem) {
75
- return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
76
+ return deepMergeViewStates$1(baseItem, overlayItem, trackByMap, arrayPath);
76
77
  } else {
77
78
  return baseItem;
78
79
  }
79
80
  });
80
81
  }
82
+ const HEADLESS_INSTANCES = runtime.createJayContext();
83
+ function makeSignals$1(obj) {
84
+ return Object.keys(obj).reduce((signals, key) => {
85
+ signals[key] = component.createSignal(obj[key]);
86
+ return signals;
87
+ }, {});
88
+ }
89
+ function makeHeadlessInstanceComponent(preRender, interactiveConstructor, coordinateKey, pluginContexts = []) {
90
+ const wrappedConstructor = (props, refs, ...pluginResolvedContexts) => {
91
+ const instanceData = runtime.useContext(HEADLESS_INSTANCES);
92
+ const fastVS = instanceData?.viewStates?.[coordinateKey];
93
+ const cf = instanceData?.carryForwards?.[coordinateKey] || {};
94
+ const signalVS = fastVS ? makeSignals$1(fastVS) : void 0;
95
+ return interactiveConstructor(props, refs, signalVS, cf, ...pluginResolvedContexts);
96
+ };
97
+ return component.makeJayComponent(preRender, wrappedConstructor, ...pluginContexts);
98
+ }
81
99
  function makeSignals(obj) {
82
100
  return Object.keys(obj).reduce((signals, key) => {
83
101
  signals[key] = component.createSignal(obj[key]);
@@ -85,39 +103,56 @@ function makeSignals(obj) {
85
103
  }, {});
86
104
  }
87
105
  function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward, parts, trackByMap = {}) {
106
+ const interactiveParts = parts.filter((part) => part.comp !== void 0);
88
107
  const hasFastRendering = defaultViewState !== null && defaultViewState !== void 0;
108
+ const headlessInstanceViewStates = defaultViewState?.__headlessInstances;
109
+ const headlessInstanceCarryForwards = fastCarryForward?.__headlessInstances;
110
+ if (headlessInstanceViewStates)
111
+ delete defaultViewState.__headlessInstances;
112
+ if (headlessInstanceCarryForwards)
113
+ delete fastCarryForward.__headlessInstances;
89
114
  const comp = (props, refs, ...contexts) => {
90
- const instances = parts.map((part) => {
91
- const partRefs = part.key ? refs[part.key] : refs;
92
- let partContexts;
93
- if (hasFastRendering) {
94
- const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
95
- const partFastViewState = partViewState ? makeSignals(partViewState) : void 0;
96
- const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
97
- partContexts = [
98
- partFastViewState,
99
- partCarryForward,
100
- ...contexts.splice(0, part.contextMarkers.length)
101
- ];
102
- } else {
103
- partContexts = [...contexts.splice(0, part.contextMarkers.length)];
115
+ if (headlessInstanceViewStates || headlessInstanceCarryForwards) {
116
+ const componentContext = runtime.useContext(component.COMPONENT_CONTEXT);
117
+ const instancesData = {
118
+ viewStates: headlessInstanceViewStates || {},
119
+ carryForwards: headlessInstanceCarryForwards || {}
120
+ };
121
+ componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
122
+ }
123
+ const instances = interactiveParts.map(
124
+ (part) => {
125
+ const partRefs = part.key ? refs[part.key] : refs;
126
+ let partContexts;
127
+ if (hasFastRendering) {
128
+ const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
129
+ const partFastViewState = partViewState ? makeSignals(partViewState) : void 0;
130
+ const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
131
+ partContexts = [
132
+ partFastViewState,
133
+ partCarryForward,
134
+ ...contexts.splice(0, part.contextMarkers.length)
135
+ ];
136
+ } else {
137
+ partContexts = [...contexts.splice(0, part.contextMarkers.length)];
138
+ }
139
+ return [part.key, part.comp(props, partRefs, ...partContexts)];
104
140
  }
105
- return [part.key, part.comp(props, partRefs, ...partContexts)];
106
- });
141
+ );
107
142
  return {
108
143
  render: () => {
109
144
  let viewState = defaultViewState;
110
145
  instances.forEach(([key, instance]) => {
111
146
  const rendered = component.materializeViewState(instance.render());
112
147
  if (key) {
113
- viewState[key] = deepMergeViewStates(
148
+ viewState[key] = deepMergeViewStates$1(
114
149
  defaultViewState[key],
115
150
  rendered,
116
151
  trackByMap,
117
152
  key
118
153
  );
119
154
  } else {
120
- viewState = deepMergeViewStates(
155
+ viewState = deepMergeViewStates$1(
121
156
  viewState,
122
157
  rendered,
123
158
  trackByMap
@@ -128,7 +163,7 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
128
163
  }
129
164
  };
130
165
  };
131
- const contextMarkers = parts.reduce((cm, part) => {
166
+ const contextMarkers = interactiveParts.reduce((cm, part) => {
132
167
  return [...cm, ...part.contextMarkers];
133
168
  }, []);
134
169
  return component.makeJayComponent(
@@ -230,7 +265,262 @@ function isSimpleObject(obj) {
230
265
  }
231
266
  return true;
232
267
  }
268
+ var __defProp2 = Object.defineProperty;
269
+ var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
270
+ var __publicField2 = (obj, key, value) => {
271
+ __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
272
+ return value;
273
+ };
274
+ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
275
+ if (!base && !overlay)
276
+ return {};
277
+ if (!base)
278
+ return overlay || {};
279
+ if (!overlay)
280
+ return base || {};
281
+ const result = {};
282
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
283
+ for (const key of allKeys) {
284
+ const baseValue = base[key];
285
+ const overlayValue = overlay[key];
286
+ const currentPath = path ? `${path}.${key}` : key;
287
+ if (overlayValue === void 0) {
288
+ result[key] = baseValue;
289
+ } else if (baseValue === void 0) {
290
+ result[key] = overlayValue;
291
+ } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
292
+ const trackByField = trackByMap[currentPath];
293
+ if (trackByField) {
294
+ result[key] = mergeArraysByTrackBy(
295
+ baseValue,
296
+ overlayValue,
297
+ trackByField,
298
+ trackByMap,
299
+ currentPath
300
+ );
301
+ } else {
302
+ result[key] = overlayValue;
303
+ }
304
+ } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
305
+ result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
306
+ } else {
307
+ result[key] = overlayValue;
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
313
+ const baseByKey = /* @__PURE__ */ new Map();
314
+ for (const item of baseArray) {
315
+ const key = item[trackByField];
316
+ if (key !== void 0 && key !== null) {
317
+ if (baseByKey.has(key)) {
318
+ console.warn(
319
+ `Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
320
+ );
321
+ }
322
+ baseByKey.set(key, item);
323
+ }
324
+ }
325
+ const overlayByKey = /* @__PURE__ */ new Map();
326
+ for (const item of overlayArray) {
327
+ const key = item[trackByField];
328
+ if (key !== void 0 && key !== null) {
329
+ overlayByKey.set(key, item);
330
+ }
331
+ }
332
+ return baseArray.map((baseItem) => {
333
+ const key = baseItem[trackByField];
334
+ if (key === void 0 || key === null) {
335
+ return baseItem;
336
+ }
337
+ const overlayItem = overlayByKey.get(key);
338
+ if (overlayItem) {
339
+ return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
340
+ } else {
341
+ return baseItem;
342
+ }
343
+ });
344
+ }
345
+ function collectInteractions(refs) {
346
+ const interactions = [];
347
+ if (!refs)
348
+ return interactions;
349
+ collectInteractionsRecursive(refs, interactions);
350
+ return interactions;
351
+ }
352
+ function collectInteractionsRecursive(refs, interactions) {
353
+ if (!refs)
354
+ return;
355
+ for (const [refName, refImpl] of Object.entries(refs)) {
356
+ if (!refImpl)
357
+ continue;
358
+ if (refImpl.elements && refImpl.elements instanceof Set) {
359
+ for (const elem of refImpl.elements) {
360
+ if (elem.element) {
361
+ interactions.push({
362
+ refName,
363
+ coordinate: elem.coordinate || [refName],
364
+ element: elem.element,
365
+ elementType: getElementType(elem.element),
366
+ supportedEvents: getSupportedEvents(elem.element),
367
+ itemContext: elem.viewState
368
+ });
369
+ }
370
+ }
371
+ } else if (isNestedRefsObject(refImpl)) {
372
+ collectInteractionsRecursive(refImpl, interactions);
373
+ }
374
+ }
375
+ }
376
+ function isNestedRefsObject(obj) {
377
+ if (!obj || typeof obj !== "object")
378
+ return false;
379
+ if (obj.elements instanceof Set)
380
+ return false;
381
+ const proto = Object.getPrototypeOf(obj);
382
+ if (proto !== Object.prototype && proto !== null)
383
+ return false;
384
+ return true;
385
+ }
386
+ function getElementType(element) {
387
+ return element.constructor.name;
388
+ }
389
+ function getSupportedEvents(element) {
390
+ const base = ["click", "focus", "blur"];
391
+ if (element instanceof HTMLInputElement) {
392
+ return [...base, "input", "change"];
393
+ }
394
+ if (element instanceof HTMLButtonElement) {
395
+ return ["click", "focus", "blur"];
396
+ }
397
+ if (element instanceof HTMLSelectElement) {
398
+ return [...base, "change"];
399
+ }
400
+ if (element instanceof HTMLTextAreaElement) {
401
+ return [...base, "input", "change"];
402
+ }
403
+ if (element instanceof HTMLAnchorElement) {
404
+ return ["click"];
405
+ }
406
+ if (element instanceof HTMLFormElement) {
407
+ return ["submit", "reset"];
408
+ }
409
+ return base;
410
+ }
411
+ const VIEW_STATE_CHANGE = "viewStateChange";
412
+ class AutomationAgent {
413
+ constructor(component2, options) {
414
+ __publicField2(this, "stateListeners", /* @__PURE__ */ new Set());
415
+ __publicField2(this, "cachedInteractions", null);
416
+ __publicField2(this, "viewStateHandler", null);
417
+ __publicField2(this, "mergedViewState");
418
+ __publicField2(this, "initialSlowViewState");
419
+ __publicField2(this, "trackByMap");
420
+ this.component = component2;
421
+ if (options) {
422
+ this.initialSlowViewState = options.initialViewState;
423
+ this.trackByMap = options.trackByMap;
424
+ this.mergedViewState = deepMergeViewStates(
425
+ options.initialViewState,
426
+ this.component.viewState || {},
427
+ options.trackByMap
428
+ );
429
+ }
430
+ this.subscribeToUpdates();
431
+ }
432
+ subscribeToUpdates() {
433
+ this.viewStateHandler = () => {
434
+ this.cachedInteractions = null;
435
+ if (this.initialSlowViewState && this.trackByMap) {
436
+ this.mergedViewState = deepMergeViewStates(
437
+ this.initialSlowViewState,
438
+ this.component.viewState || {},
439
+ this.trackByMap
440
+ );
441
+ }
442
+ this.notifyListeners();
443
+ };
444
+ this.component.addEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
445
+ }
446
+ notifyListeners() {
447
+ if (this.stateListeners.size === 0)
448
+ return;
449
+ const state = this.getPageState();
450
+ this.stateListeners.forEach((callback) => callback(state));
451
+ }
452
+ getPageState() {
453
+ if (!this.cachedInteractions) {
454
+ this.cachedInteractions = collectInteractions(this.component.element?.refs);
455
+ }
456
+ return {
457
+ // Use merged state if available (slow+fast), otherwise component's viewState
458
+ viewState: this.mergedViewState || this.component.viewState,
459
+ interactions: this.cachedInteractions,
460
+ customEvents: this.getCustomEvents()
461
+ };
462
+ }
463
+ triggerEvent(eventType, coordinate, eventData) {
464
+ const interaction = this.getInteraction(coordinate);
465
+ if (!interaction) {
466
+ throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
467
+ }
468
+ const event = new Event(eventType, { bubbles: true });
469
+ if (eventData) {
470
+ Object.assign(event, eventData);
471
+ }
472
+ interaction.element.dispatchEvent(event);
473
+ }
474
+ getInteraction(coordinate) {
475
+ const state = this.getPageState();
476
+ return state.interactions.find(
477
+ (i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
478
+ );
479
+ }
480
+ onStateChange(callback) {
481
+ this.stateListeners.add(callback);
482
+ return () => this.stateListeners.delete(callback);
483
+ }
484
+ getCustomEvents() {
485
+ const events = [];
486
+ const component2 = this.component;
487
+ for (const key in component2) {
488
+ if (component2[key]?.emit && typeof component2[key].emit === "function") {
489
+ const name = key.startsWith("on") ? key.slice(2) : key;
490
+ events.push({ name });
491
+ }
492
+ }
493
+ return events;
494
+ }
495
+ onComponentEvent(eventName, callback) {
496
+ const component2 = this.component;
497
+ const handlerKey = eventName.startsWith("on") ? eventName : `on${eventName}`;
498
+ const handler = component2[handlerKey];
499
+ if (!handler || typeof handler !== "function") {
500
+ throw new Error(`Unknown component event: ${eventName}`);
501
+ }
502
+ handler(({ event }) => callback(event));
503
+ return () => handler(void 0);
504
+ }
505
+ dispose() {
506
+ if (this.viewStateHandler) {
507
+ this.component.removeEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
508
+ this.viewStateHandler = null;
509
+ }
510
+ this.stateListeners.clear();
511
+ this.cachedInteractions = null;
512
+ }
513
+ }
514
+ function wrapWithAutomation(component2, options) {
515
+ const agent = new AutomationAgent(component2, options);
516
+ return Object.assign(component2, { automation: agent });
517
+ }
518
+ const AUTOMATION_CONTEXT = runtime.createJayContext();
519
+ exports.AUTOMATION_CONTEXT = AUTOMATION_CONTEXT;
233
520
  exports.ActionError = ActionError;
521
+ exports.HEADLESS_INSTANCES = HEADLESS_INSTANCES;
234
522
  exports.createActionCaller = createActionCaller;
235
523
  exports.makeCompositeJayComponent = makeCompositeJayComponent;
524
+ exports.makeHeadlessInstanceComponent = makeHeadlessInstanceComponent;
236
525
  exports.setActionCallerOptions = setActionCallerOptions;
526
+ exports.wrapWithAutomation = wrapWithAutomation;
package/dist/index.d.ts CHANGED
@@ -1,16 +1,63 @@
1
1
  import * as _jay_framework_component from '@jay-framework/component';
2
2
  import { ComponentConstructor, ContextMarkers, JayComponentCore } from '@jay-framework/component';
3
- import { JayElement, PreRenderElement } from '@jay-framework/runtime';
3
+ import { JayElement, PreRenderElement, ContextMarker } from '@jay-framework/runtime';
4
4
  import { TrackByMap } from '@jay-framework/view-state-merge';
5
+ export { AUTOMATION_CONTEXT, AutomationAPI, AutomationWrappedComponent, Coordinate, Interaction, PageState, wrapWithAutomation } from '@jay-framework/runtime-automation';
5
6
 
6
7
  interface CompositePart {
7
- comp: ComponentConstructor<any, any, any, any, any>;
8
+ /**
9
+ * The interactive component constructor.
10
+ * May be undefined if the component has no interactive phase (only slow/fast phases).
11
+ * See Design Log #72.
12
+ */
13
+ comp?: ComponentConstructor<any, any, any, any, any>;
8
14
  contextMarkers: ContextMarkers<any>;
9
15
  key?: string;
10
16
  }
11
17
 
12
18
  declare function makeCompositeJayComponent<PropsT extends object, ViewState extends object, Refs extends object, JayElementT extends JayElement<ViewState, Refs>, CompCore extends JayComponentCore<PropsT, ViewState>>(preRender: PreRenderElement<ViewState, Refs, JayElementT>, defaultViewState: ViewState, fastCarryForward: object, parts: Array<CompositePart>, trackByMap?: TrackByMap): (props: PropsT) => _jay_framework_component.ConcreteJayComponent<PropsT, ViewState, Refs, CompCore, JayElementT>;
13
19
 
20
+ /**
21
+ * Client-side context and helpers for headless component instances.
22
+ *
23
+ * Provides the mechanism to deliver server-produced fast ViewState and carryForward
24
+ * to headless component instances on the client.
25
+ *
26
+ * Flow:
27
+ * 1. makeCompositeJayComponent extracts __headlessInstances from ViewState/carryForward
28
+ * 2. Registers HEADLESS_INSTANCES context during component construction
29
+ * 3. makeHeadlessInstanceComponent creates instance components that resolve their data
30
+ * from this context by coordinate key
31
+ */
32
+
33
+ /**
34
+ * Data structure for headless instance ViewStates and carryForwards.
35
+ * Keyed by coordinate path (e.g., "product-card:0", "p1/product-card:0").
36
+ */
37
+ interface HeadlessInstancesData {
38
+ viewStates: Record<string, object>;
39
+ carryForwards: Record<string, object>;
40
+ }
41
+ /**
42
+ * Context marker for headless instance data.
43
+ * Provided by makeCompositeJayComponent, consumed by makeHeadlessInstanceComponent.
44
+ */
45
+ declare const HEADLESS_INSTANCES: ContextMarker<HeadlessInstancesData>;
46
+ /**
47
+ * Create a headless instance component that receives its fast ViewState from the
48
+ * HEADLESS_INSTANCES context, matched by coordinate key.
49
+ *
50
+ * This replaces makeJayComponent for headless instances. It wraps the plugin's
51
+ * interactive constructor to inject the instance's fast ViewState signals and
52
+ * carryForward before any plugin-defined context markers.
53
+ *
54
+ * @param preRender - The inline template's render function
55
+ * @param interactiveConstructor - The plugin's interactive constructor
56
+ * @param coordinateKey - The coordinate key for this instance (e.g., "product-card:0")
57
+ * @param pluginContexts - Additional context markers from the plugin (if any)
58
+ */
59
+ declare function makeHeadlessInstanceComponent<PropsT extends object, ViewState extends object, Refs extends object, JayElementT extends JayElement<ViewState, Refs>, CompCore extends JayComponentCore<PropsT, ViewState>>(preRender: PreRenderElement<ViewState, Refs, JayElementT>, interactiveConstructor: ComponentConstructor<PropsT, Refs, ViewState, any, CompCore>, coordinateKey: string, pluginContexts?: ContextMarkers<any>): any;
60
+
14
61
  /**
15
62
  * Client-side action caller for Jay Stack.
16
63
  *
@@ -76,4 +123,4 @@ declare function setActionCallerOptions(options: ActionCallerOptions): void;
76
123
  */
77
124
  declare function createActionCaller<Input, Output>(actionName: string, method?: HttpMethod): (input: Input) => Promise<Output>;
78
125
 
79
- export { type ActionCallerOptions, ActionError, type CompositePart, type HttpMethod, createActionCaller, makeCompositeJayComponent, setActionCallerOptions };
126
+ export { type ActionCallerOptions, ActionError, type CompositePart, HEADLESS_INSTANCES, type HeadlessInstancesData, type HttpMethod, createActionCaller, makeCompositeJayComponent, makeHeadlessInstanceComponent, setActionCallerOptions };
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ var __publicField = (obj, key, value) => {
4
4
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
  return value;
6
6
  };
7
- import { makeJayComponent, materializeViewState, createSignal } from "@jay-framework/component";
8
- function deepMergeViewStates(base, overlay, trackByMap, path = "") {
7
+ import { makeJayComponent, createSignal, COMPONENT_CONTEXT, materializeViewState } from "@jay-framework/component";
8
+ import { createJayContext, useContext } from "@jay-framework/runtime";
9
+ function deepMergeViewStates$1(base, overlay, trackByMap, path = "") {
9
10
  if (!base && !overlay)
10
11
  return {};
11
12
  if (!base)
@@ -25,7 +26,7 @@ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
25
26
  } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
26
27
  const trackByField = trackByMap[currentPath];
27
28
  if (trackByField) {
28
- result[key] = mergeArraysByTrackBy(
29
+ result[key] = mergeArraysByTrackBy$1(
29
30
  baseValue,
30
31
  overlayValue,
31
32
  trackByField,
@@ -36,14 +37,14 @@ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
36
37
  result[key] = overlayValue;
37
38
  }
38
39
  } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
39
- result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
40
+ result[key] = deepMergeViewStates$1(baseValue, overlayValue, trackByMap, currentPath);
40
41
  } else {
41
42
  result[key] = overlayValue;
42
43
  }
43
44
  }
44
45
  return result;
45
46
  }
46
- function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
47
+ function mergeArraysByTrackBy$1(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
47
48
  const baseByKey = /* @__PURE__ */ new Map();
48
49
  for (const item of baseArray) {
49
50
  const key = item[trackByField];
@@ -70,12 +71,29 @@ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap,
70
71
  }
71
72
  const overlayItem = overlayByKey.get(key);
72
73
  if (overlayItem) {
73
- return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
74
+ return deepMergeViewStates$1(baseItem, overlayItem, trackByMap, arrayPath);
74
75
  } else {
75
76
  return baseItem;
76
77
  }
77
78
  });
78
79
  }
80
+ const HEADLESS_INSTANCES = createJayContext();
81
+ function makeSignals$1(obj) {
82
+ return Object.keys(obj).reduce((signals, key) => {
83
+ signals[key] = createSignal(obj[key]);
84
+ return signals;
85
+ }, {});
86
+ }
87
+ function makeHeadlessInstanceComponent(preRender, interactiveConstructor, coordinateKey, pluginContexts = []) {
88
+ const wrappedConstructor = (props, refs, ...pluginResolvedContexts) => {
89
+ const instanceData = useContext(HEADLESS_INSTANCES);
90
+ const fastVS = instanceData?.viewStates?.[coordinateKey];
91
+ const cf = instanceData?.carryForwards?.[coordinateKey] || {};
92
+ const signalVS = fastVS ? makeSignals$1(fastVS) : void 0;
93
+ return interactiveConstructor(props, refs, signalVS, cf, ...pluginResolvedContexts);
94
+ };
95
+ return makeJayComponent(preRender, wrappedConstructor, ...pluginContexts);
96
+ }
79
97
  function makeSignals(obj) {
80
98
  return Object.keys(obj).reduce((signals, key) => {
81
99
  signals[key] = createSignal(obj[key]);
@@ -83,39 +101,56 @@ function makeSignals(obj) {
83
101
  }, {});
84
102
  }
85
103
  function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward, parts, trackByMap = {}) {
104
+ const interactiveParts = parts.filter((part) => part.comp !== void 0);
86
105
  const hasFastRendering = defaultViewState !== null && defaultViewState !== void 0;
106
+ const headlessInstanceViewStates = defaultViewState?.__headlessInstances;
107
+ const headlessInstanceCarryForwards = fastCarryForward?.__headlessInstances;
108
+ if (headlessInstanceViewStates)
109
+ delete defaultViewState.__headlessInstances;
110
+ if (headlessInstanceCarryForwards)
111
+ delete fastCarryForward.__headlessInstances;
87
112
  const comp = (props, refs, ...contexts) => {
88
- const instances = parts.map((part) => {
89
- const partRefs = part.key ? refs[part.key] : refs;
90
- let partContexts;
91
- if (hasFastRendering) {
92
- const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
93
- const partFastViewState = partViewState ? makeSignals(partViewState) : void 0;
94
- const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
95
- partContexts = [
96
- partFastViewState,
97
- partCarryForward,
98
- ...contexts.splice(0, part.contextMarkers.length)
99
- ];
100
- } else {
101
- partContexts = [...contexts.splice(0, part.contextMarkers.length)];
113
+ if (headlessInstanceViewStates || headlessInstanceCarryForwards) {
114
+ const componentContext = useContext(COMPONENT_CONTEXT);
115
+ const instancesData = {
116
+ viewStates: headlessInstanceViewStates || {},
117
+ carryForwards: headlessInstanceCarryForwards || {}
118
+ };
119
+ componentContext.provideContexts.push([HEADLESS_INSTANCES, instancesData]);
120
+ }
121
+ const instances = interactiveParts.map(
122
+ (part) => {
123
+ const partRefs = part.key ? refs[part.key] : refs;
124
+ let partContexts;
125
+ if (hasFastRendering) {
126
+ const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
127
+ const partFastViewState = partViewState ? makeSignals(partViewState) : void 0;
128
+ const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
129
+ partContexts = [
130
+ partFastViewState,
131
+ partCarryForward,
132
+ ...contexts.splice(0, part.contextMarkers.length)
133
+ ];
134
+ } else {
135
+ partContexts = [...contexts.splice(0, part.contextMarkers.length)];
136
+ }
137
+ return [part.key, part.comp(props, partRefs, ...partContexts)];
102
138
  }
103
- return [part.key, part.comp(props, partRefs, ...partContexts)];
104
- });
139
+ );
105
140
  return {
106
141
  render: () => {
107
142
  let viewState = defaultViewState;
108
143
  instances.forEach(([key, instance]) => {
109
144
  const rendered = materializeViewState(instance.render());
110
145
  if (key) {
111
- viewState[key] = deepMergeViewStates(
146
+ viewState[key] = deepMergeViewStates$1(
112
147
  defaultViewState[key],
113
148
  rendered,
114
149
  trackByMap,
115
150
  key
116
151
  );
117
152
  } else {
118
- viewState = deepMergeViewStates(
153
+ viewState = deepMergeViewStates$1(
119
154
  viewState,
120
155
  rendered,
121
156
  trackByMap
@@ -126,7 +161,7 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
126
161
  }
127
162
  };
128
163
  };
129
- const contextMarkers = parts.reduce((cm, part) => {
164
+ const contextMarkers = interactiveParts.reduce((cm, part) => {
130
165
  return [...cm, ...part.contextMarkers];
131
166
  }, []);
132
167
  return makeJayComponent(
@@ -228,9 +263,264 @@ function isSimpleObject(obj) {
228
263
  }
229
264
  return true;
230
265
  }
266
+ var __defProp2 = Object.defineProperty;
267
+ var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
268
+ var __publicField2 = (obj, key, value) => {
269
+ __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
270
+ return value;
271
+ };
272
+ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
273
+ if (!base && !overlay)
274
+ return {};
275
+ if (!base)
276
+ return overlay || {};
277
+ if (!overlay)
278
+ return base || {};
279
+ const result = {};
280
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
281
+ for (const key of allKeys) {
282
+ const baseValue = base[key];
283
+ const overlayValue = overlay[key];
284
+ const currentPath = path ? `${path}.${key}` : key;
285
+ if (overlayValue === void 0) {
286
+ result[key] = baseValue;
287
+ } else if (baseValue === void 0) {
288
+ result[key] = overlayValue;
289
+ } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
290
+ const trackByField = trackByMap[currentPath];
291
+ if (trackByField) {
292
+ result[key] = mergeArraysByTrackBy(
293
+ baseValue,
294
+ overlayValue,
295
+ trackByField,
296
+ trackByMap,
297
+ currentPath
298
+ );
299
+ } else {
300
+ result[key] = overlayValue;
301
+ }
302
+ } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
303
+ result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
304
+ } else {
305
+ result[key] = overlayValue;
306
+ }
307
+ }
308
+ return result;
309
+ }
310
+ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
311
+ const baseByKey = /* @__PURE__ */ new Map();
312
+ for (const item of baseArray) {
313
+ const key = item[trackByField];
314
+ if (key !== void 0 && key !== null) {
315
+ if (baseByKey.has(key)) {
316
+ console.warn(
317
+ `Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
318
+ );
319
+ }
320
+ baseByKey.set(key, item);
321
+ }
322
+ }
323
+ const overlayByKey = /* @__PURE__ */ new Map();
324
+ for (const item of overlayArray) {
325
+ const key = item[trackByField];
326
+ if (key !== void 0 && key !== null) {
327
+ overlayByKey.set(key, item);
328
+ }
329
+ }
330
+ return baseArray.map((baseItem) => {
331
+ const key = baseItem[trackByField];
332
+ if (key === void 0 || key === null) {
333
+ return baseItem;
334
+ }
335
+ const overlayItem = overlayByKey.get(key);
336
+ if (overlayItem) {
337
+ return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
338
+ } else {
339
+ return baseItem;
340
+ }
341
+ });
342
+ }
343
+ function collectInteractions(refs) {
344
+ const interactions = [];
345
+ if (!refs)
346
+ return interactions;
347
+ collectInteractionsRecursive(refs, interactions);
348
+ return interactions;
349
+ }
350
+ function collectInteractionsRecursive(refs, interactions) {
351
+ if (!refs)
352
+ return;
353
+ for (const [refName, refImpl] of Object.entries(refs)) {
354
+ if (!refImpl)
355
+ continue;
356
+ if (refImpl.elements && refImpl.elements instanceof Set) {
357
+ for (const elem of refImpl.elements) {
358
+ if (elem.element) {
359
+ interactions.push({
360
+ refName,
361
+ coordinate: elem.coordinate || [refName],
362
+ element: elem.element,
363
+ elementType: getElementType(elem.element),
364
+ supportedEvents: getSupportedEvents(elem.element),
365
+ itemContext: elem.viewState
366
+ });
367
+ }
368
+ }
369
+ } else if (isNestedRefsObject(refImpl)) {
370
+ collectInteractionsRecursive(refImpl, interactions);
371
+ }
372
+ }
373
+ }
374
+ function isNestedRefsObject(obj) {
375
+ if (!obj || typeof obj !== "object")
376
+ return false;
377
+ if (obj.elements instanceof Set)
378
+ return false;
379
+ const proto = Object.getPrototypeOf(obj);
380
+ if (proto !== Object.prototype && proto !== null)
381
+ return false;
382
+ return true;
383
+ }
384
+ function getElementType(element) {
385
+ return element.constructor.name;
386
+ }
387
+ function getSupportedEvents(element) {
388
+ const base = ["click", "focus", "blur"];
389
+ if (element instanceof HTMLInputElement) {
390
+ return [...base, "input", "change"];
391
+ }
392
+ if (element instanceof HTMLButtonElement) {
393
+ return ["click", "focus", "blur"];
394
+ }
395
+ if (element instanceof HTMLSelectElement) {
396
+ return [...base, "change"];
397
+ }
398
+ if (element instanceof HTMLTextAreaElement) {
399
+ return [...base, "input", "change"];
400
+ }
401
+ if (element instanceof HTMLAnchorElement) {
402
+ return ["click"];
403
+ }
404
+ if (element instanceof HTMLFormElement) {
405
+ return ["submit", "reset"];
406
+ }
407
+ return base;
408
+ }
409
+ const VIEW_STATE_CHANGE = "viewStateChange";
410
+ class AutomationAgent {
411
+ constructor(component, options) {
412
+ __publicField2(this, "stateListeners", /* @__PURE__ */ new Set());
413
+ __publicField2(this, "cachedInteractions", null);
414
+ __publicField2(this, "viewStateHandler", null);
415
+ __publicField2(this, "mergedViewState");
416
+ __publicField2(this, "initialSlowViewState");
417
+ __publicField2(this, "trackByMap");
418
+ this.component = component;
419
+ if (options) {
420
+ this.initialSlowViewState = options.initialViewState;
421
+ this.trackByMap = options.trackByMap;
422
+ this.mergedViewState = deepMergeViewStates(
423
+ options.initialViewState,
424
+ this.component.viewState || {},
425
+ options.trackByMap
426
+ );
427
+ }
428
+ this.subscribeToUpdates();
429
+ }
430
+ subscribeToUpdates() {
431
+ this.viewStateHandler = () => {
432
+ this.cachedInteractions = null;
433
+ if (this.initialSlowViewState && this.trackByMap) {
434
+ this.mergedViewState = deepMergeViewStates(
435
+ this.initialSlowViewState,
436
+ this.component.viewState || {},
437
+ this.trackByMap
438
+ );
439
+ }
440
+ this.notifyListeners();
441
+ };
442
+ this.component.addEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
443
+ }
444
+ notifyListeners() {
445
+ if (this.stateListeners.size === 0)
446
+ return;
447
+ const state = this.getPageState();
448
+ this.stateListeners.forEach((callback) => callback(state));
449
+ }
450
+ getPageState() {
451
+ if (!this.cachedInteractions) {
452
+ this.cachedInteractions = collectInteractions(this.component.element?.refs);
453
+ }
454
+ return {
455
+ // Use merged state if available (slow+fast), otherwise component's viewState
456
+ viewState: this.mergedViewState || this.component.viewState,
457
+ interactions: this.cachedInteractions,
458
+ customEvents: this.getCustomEvents()
459
+ };
460
+ }
461
+ triggerEvent(eventType, coordinate, eventData) {
462
+ const interaction = this.getInteraction(coordinate);
463
+ if (!interaction) {
464
+ throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
465
+ }
466
+ const event = new Event(eventType, { bubbles: true });
467
+ if (eventData) {
468
+ Object.assign(event, eventData);
469
+ }
470
+ interaction.element.dispatchEvent(event);
471
+ }
472
+ getInteraction(coordinate) {
473
+ const state = this.getPageState();
474
+ return state.interactions.find(
475
+ (i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
476
+ );
477
+ }
478
+ onStateChange(callback) {
479
+ this.stateListeners.add(callback);
480
+ return () => this.stateListeners.delete(callback);
481
+ }
482
+ getCustomEvents() {
483
+ const events = [];
484
+ const component = this.component;
485
+ for (const key in component) {
486
+ if (component[key]?.emit && typeof component[key].emit === "function") {
487
+ const name = key.startsWith("on") ? key.slice(2) : key;
488
+ events.push({ name });
489
+ }
490
+ }
491
+ return events;
492
+ }
493
+ onComponentEvent(eventName, callback) {
494
+ const component = this.component;
495
+ const handlerKey = eventName.startsWith("on") ? eventName : `on${eventName}`;
496
+ const handler = component[handlerKey];
497
+ if (!handler || typeof handler !== "function") {
498
+ throw new Error(`Unknown component event: ${eventName}`);
499
+ }
500
+ handler(({ event }) => callback(event));
501
+ return () => handler(void 0);
502
+ }
503
+ dispose() {
504
+ if (this.viewStateHandler) {
505
+ this.component.removeEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
506
+ this.viewStateHandler = null;
507
+ }
508
+ this.stateListeners.clear();
509
+ this.cachedInteractions = null;
510
+ }
511
+ }
512
+ function wrapWithAutomation(component, options) {
513
+ const agent = new AutomationAgent(component, options);
514
+ return Object.assign(component, { automation: agent });
515
+ }
516
+ const AUTOMATION_CONTEXT = createJayContext();
231
517
  export {
518
+ AUTOMATION_CONTEXT,
232
519
  ActionError,
520
+ HEADLESS_INSTANCES,
233
521
  createActionCaller,
234
522
  makeCompositeJayComponent,
235
- setActionCallerOptions
523
+ makeHeadlessInstanceComponent,
524
+ setActionCallerOptions,
525
+ wrapWithAutomation
236
526
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/stack-client-runtime",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -27,14 +27,15 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
30
- "@jay-framework/component": "^0.10.0",
31
- "@jay-framework/fullstack-component": "^0.10.0",
32
- "@jay-framework/runtime": "^0.10.0",
33
- "@jay-framework/view-state-merge": "^0.10.0"
30
+ "@jay-framework/component": "^0.12.0",
31
+ "@jay-framework/fullstack-component": "^0.12.0",
32
+ "@jay-framework/runtime": "^0.12.0",
33
+ "@jay-framework/runtime-automation": "^0.12.0",
34
+ "@jay-framework/view-state-merge": "^0.12.0"
34
35
  },
35
36
  "devDependencies": {
36
- "@jay-framework/dev-environment": "^0.10.0",
37
- "@jay-framework/jay-cli": "^0.10.0",
37
+ "@jay-framework/dev-environment": "^0.12.0",
38
+ "@jay-framework/jay-cli": "^0.12.0",
38
39
  "@types/express": "^5.0.2",
39
40
  "@types/node": "^22.15.21",
40
41
  "nodemon": "^3.0.3",