@jay-framework/stack-client-runtime 0.9.0 → 0.11.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 +468 -25
- package/dist/index.d.ts +75 -3
- package/dist/index.js +469 -26
- package/package.json +8 -6
package/dist/index.cjs
CHANGED
|
@@ -1,44 +1,138 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __publicField = (obj, key, value) => {
|
|
5
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
2
8
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
9
|
const component = require("@jay-framework/component");
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
const runtime = require("@jay-framework/runtime");
|
|
11
|
+
function deepMergeViewStates$1(base, overlay, trackByMap, path = "") {
|
|
12
|
+
if (!base && !overlay)
|
|
13
|
+
return {};
|
|
14
|
+
if (!base)
|
|
15
|
+
return overlay || {};
|
|
16
|
+
if (!overlay)
|
|
17
|
+
return base || {};
|
|
18
|
+
const result = {};
|
|
19
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
|
|
20
|
+
for (const key of allKeys) {
|
|
21
|
+
const baseValue = base[key];
|
|
22
|
+
const overlayValue = overlay[key];
|
|
23
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
24
|
+
if (overlayValue === void 0) {
|
|
25
|
+
result[key] = baseValue;
|
|
26
|
+
} else if (baseValue === void 0) {
|
|
27
|
+
result[key] = overlayValue;
|
|
28
|
+
} else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
29
|
+
const trackByField = trackByMap[currentPath];
|
|
30
|
+
if (trackByField) {
|
|
31
|
+
result[key] = mergeArraysByTrackBy$1(
|
|
32
|
+
baseValue,
|
|
33
|
+
overlayValue,
|
|
34
|
+
trackByField,
|
|
35
|
+
trackByMap,
|
|
36
|
+
currentPath
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
result[key] = overlayValue;
|
|
40
|
+
}
|
|
41
|
+
} else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
|
|
42
|
+
result[key] = deepMergeViewStates$1(baseValue, overlayValue, trackByMap, currentPath);
|
|
43
|
+
} else {
|
|
44
|
+
result[key] = overlayValue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function mergeArraysByTrackBy$1(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
|
|
50
|
+
const baseByKey = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const item of baseArray) {
|
|
52
|
+
const key = item[trackByField];
|
|
53
|
+
if (key !== void 0 && key !== null) {
|
|
54
|
+
if (baseByKey.has(key)) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
baseByKey.set(key, item);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const overlayByKey = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const item of overlayArray) {
|
|
64
|
+
const key = item[trackByField];
|
|
65
|
+
if (key !== void 0 && key !== null) {
|
|
66
|
+
overlayByKey.set(key, item);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return baseArray.map((baseItem) => {
|
|
70
|
+
const key = baseItem[trackByField];
|
|
71
|
+
if (key === void 0 || key === null) {
|
|
72
|
+
return baseItem;
|
|
73
|
+
}
|
|
74
|
+
const overlayItem = overlayByKey.get(key);
|
|
75
|
+
if (overlayItem) {
|
|
76
|
+
return deepMergeViewStates$1(baseItem, overlayItem, trackByMap, arrayPath);
|
|
77
|
+
} else {
|
|
78
|
+
return baseItem;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function makeSignals(obj) {
|
|
83
|
+
return Object.keys(obj).reduce((signals, key) => {
|
|
84
|
+
signals[key] = component.createSignal(obj[key]);
|
|
7
85
|
return signals;
|
|
8
86
|
}, {});
|
|
9
87
|
}
|
|
10
|
-
function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward, parts) {
|
|
88
|
+
function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward, parts, trackByMap = {}) {
|
|
89
|
+
const interactiveParts = parts.filter((part) => part.comp !== void 0);
|
|
90
|
+
const hasFastRendering = defaultViewState !== null && defaultViewState !== void 0;
|
|
11
91
|
const comp = (props, refs, ...contexts) => {
|
|
12
|
-
const instances =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
92
|
+
const instances = interactiveParts.map(
|
|
93
|
+
(part) => {
|
|
94
|
+
const partRefs = part.key ? refs[part.key] : refs;
|
|
95
|
+
let partContexts;
|
|
96
|
+
if (hasFastRendering) {
|
|
97
|
+
const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
|
|
98
|
+
const partFastViewState = partViewState ? makeSignals(partViewState) : void 0;
|
|
99
|
+
const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
|
|
100
|
+
partContexts = [
|
|
101
|
+
partFastViewState,
|
|
102
|
+
partCarryForward,
|
|
103
|
+
...contexts.splice(0, part.contextMarkers.length)
|
|
104
|
+
];
|
|
105
|
+
} else {
|
|
106
|
+
partContexts = [...contexts.splice(0, part.contextMarkers.length)];
|
|
107
|
+
}
|
|
108
|
+
return [part.key, part.comp(props, partRefs, ...partContexts)];
|
|
109
|
+
}
|
|
110
|
+
);
|
|
25
111
|
return {
|
|
26
112
|
render: () => {
|
|
27
113
|
let viewState = defaultViewState;
|
|
28
114
|
instances.forEach(([key, instance]) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
115
|
+
const rendered = component.materializeViewState(instance.render());
|
|
116
|
+
if (key) {
|
|
117
|
+
viewState[key] = deepMergeViewStates$1(
|
|
118
|
+
defaultViewState[key],
|
|
119
|
+
rendered,
|
|
120
|
+
trackByMap,
|
|
121
|
+
key
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
viewState = deepMergeViewStates$1(
|
|
125
|
+
viewState,
|
|
126
|
+
rendered,
|
|
127
|
+
trackByMap
|
|
128
|
+
);
|
|
129
|
+
}
|
|
36
130
|
});
|
|
37
131
|
return viewState;
|
|
38
132
|
}
|
|
39
133
|
};
|
|
40
134
|
};
|
|
41
|
-
const contextMarkers =
|
|
135
|
+
const contextMarkers = interactiveParts.reduce((cm, part) => {
|
|
42
136
|
return [...cm, ...part.contextMarkers];
|
|
43
137
|
}, []);
|
|
44
138
|
return component.makeJayComponent(
|
|
@@ -47,4 +141,353 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
|
|
|
47
141
|
...contextMarkers
|
|
48
142
|
);
|
|
49
143
|
}
|
|
144
|
+
const ACTION_ENDPOINT_BASE = "/_jay/actions";
|
|
145
|
+
class ActionError extends Error {
|
|
146
|
+
constructor(code, message) {
|
|
147
|
+
super(message);
|
|
148
|
+
__publicField(this, "name", "ActionError");
|
|
149
|
+
this.code = code;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let globalOptions = {};
|
|
153
|
+
function setActionCallerOptions(options) {
|
|
154
|
+
globalOptions = { ...globalOptions, ...options };
|
|
155
|
+
}
|
|
156
|
+
function createActionCaller(actionName, method = "POST") {
|
|
157
|
+
return async (input) => {
|
|
158
|
+
const baseUrl = globalOptions.baseUrl ?? "";
|
|
159
|
+
const url = buildActionUrl(baseUrl, actionName, method, input);
|
|
160
|
+
const fetchOptions = {
|
|
161
|
+
method,
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "application/json",
|
|
164
|
+
...globalOptions.headers
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
if (method !== "GET") {
|
|
168
|
+
fetchOptions.body = JSON.stringify(input);
|
|
169
|
+
}
|
|
170
|
+
const timeout = globalOptions.timeout ?? 3e4;
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
173
|
+
fetchOptions.signal = controller.signal;
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch(url, fetchOptions);
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
if (data.success) {
|
|
179
|
+
return data.data;
|
|
180
|
+
} else {
|
|
181
|
+
if (data.error) {
|
|
182
|
+
throw new ActionError(data.error.code, data.error.message);
|
|
183
|
+
}
|
|
184
|
+
throw new ActionError("UNKNOWN_ERROR", "Unknown error occurred");
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
clearTimeout(timeoutId);
|
|
188
|
+
if (error instanceof ActionError) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
192
|
+
throw new ActionError(
|
|
193
|
+
"TIMEOUT",
|
|
194
|
+
`Action '${actionName}' timed out after ${timeout}ms`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (error instanceof TypeError) {
|
|
198
|
+
throw new ActionError(
|
|
199
|
+
"NETWORK_ERROR",
|
|
200
|
+
`Network error calling '${actionName}': ${error.message}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function buildActionUrl(baseUrl, actionName, method, input) {
|
|
208
|
+
const path = `${ACTION_ENDPOINT_BASE}/${actionName}`;
|
|
209
|
+
const fullUrl = `${baseUrl}${path}`;
|
|
210
|
+
if (method === "GET" && input !== void 0 && input !== null) {
|
|
211
|
+
const params = new URLSearchParams();
|
|
212
|
+
if (isSimpleObject(input)) {
|
|
213
|
+
for (const [key, value] of Object.entries(input)) {
|
|
214
|
+
if (value !== void 0) {
|
|
215
|
+
params.append(key, String(value));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
params.append("_input", JSON.stringify(input));
|
|
220
|
+
}
|
|
221
|
+
const queryString = params.toString();
|
|
222
|
+
return queryString ? `${fullUrl}?${queryString}` : fullUrl;
|
|
223
|
+
}
|
|
224
|
+
return fullUrl;
|
|
225
|
+
}
|
|
226
|
+
function isSimpleObject(obj) {
|
|
227
|
+
if (typeof obj !== "object" || obj === null) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
for (const value of Object.values(obj)) {
|
|
231
|
+
if (typeof value === "object" && value !== null) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
var __defProp2 = Object.defineProperty;
|
|
238
|
+
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
239
|
+
var __publicField2 = (obj, key, value) => {
|
|
240
|
+
__defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
241
|
+
return value;
|
|
242
|
+
};
|
|
243
|
+
function deepMergeViewStates(base, overlay, trackByMap, path = "") {
|
|
244
|
+
if (!base && !overlay)
|
|
245
|
+
return {};
|
|
246
|
+
if (!base)
|
|
247
|
+
return overlay || {};
|
|
248
|
+
if (!overlay)
|
|
249
|
+
return base || {};
|
|
250
|
+
const result = {};
|
|
251
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
|
|
252
|
+
for (const key of allKeys) {
|
|
253
|
+
const baseValue = base[key];
|
|
254
|
+
const overlayValue = overlay[key];
|
|
255
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
256
|
+
if (overlayValue === void 0) {
|
|
257
|
+
result[key] = baseValue;
|
|
258
|
+
} else if (baseValue === void 0) {
|
|
259
|
+
result[key] = overlayValue;
|
|
260
|
+
} else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
261
|
+
const trackByField = trackByMap[currentPath];
|
|
262
|
+
if (trackByField) {
|
|
263
|
+
result[key] = mergeArraysByTrackBy(
|
|
264
|
+
baseValue,
|
|
265
|
+
overlayValue,
|
|
266
|
+
trackByField,
|
|
267
|
+
trackByMap,
|
|
268
|
+
currentPath
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
result[key] = overlayValue;
|
|
272
|
+
}
|
|
273
|
+
} else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
|
|
274
|
+
result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
|
|
275
|
+
} else {
|
|
276
|
+
result[key] = overlayValue;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
|
|
282
|
+
const baseByKey = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const item of baseArray) {
|
|
284
|
+
const key = item[trackByField];
|
|
285
|
+
if (key !== void 0 && key !== null) {
|
|
286
|
+
if (baseByKey.has(key)) {
|
|
287
|
+
console.warn(
|
|
288
|
+
`Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
baseByKey.set(key, item);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const overlayByKey = /* @__PURE__ */ new Map();
|
|
295
|
+
for (const item of overlayArray) {
|
|
296
|
+
const key = item[trackByField];
|
|
297
|
+
if (key !== void 0 && key !== null) {
|
|
298
|
+
overlayByKey.set(key, item);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return baseArray.map((baseItem) => {
|
|
302
|
+
const key = baseItem[trackByField];
|
|
303
|
+
if (key === void 0 || key === null) {
|
|
304
|
+
return baseItem;
|
|
305
|
+
}
|
|
306
|
+
const overlayItem = overlayByKey.get(key);
|
|
307
|
+
if (overlayItem) {
|
|
308
|
+
return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
|
|
309
|
+
} else {
|
|
310
|
+
return baseItem;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function collectInteractions(refs) {
|
|
315
|
+
const interactions = [];
|
|
316
|
+
if (!refs)
|
|
317
|
+
return interactions;
|
|
318
|
+
collectInteractionsRecursive(refs, interactions);
|
|
319
|
+
return interactions;
|
|
320
|
+
}
|
|
321
|
+
function collectInteractionsRecursive(refs, interactions) {
|
|
322
|
+
if (!refs)
|
|
323
|
+
return;
|
|
324
|
+
for (const [refName, refImpl] of Object.entries(refs)) {
|
|
325
|
+
if (!refImpl)
|
|
326
|
+
continue;
|
|
327
|
+
if (refImpl.elements && refImpl.elements instanceof Set) {
|
|
328
|
+
for (const elem of refImpl.elements) {
|
|
329
|
+
if (elem.element) {
|
|
330
|
+
interactions.push({
|
|
331
|
+
refName,
|
|
332
|
+
coordinate: elem.coordinate || [refName],
|
|
333
|
+
element: elem.element,
|
|
334
|
+
elementType: getElementType(elem.element),
|
|
335
|
+
supportedEvents: getSupportedEvents(elem.element),
|
|
336
|
+
itemContext: elem.viewState
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} else if (isNestedRefsObject(refImpl)) {
|
|
341
|
+
collectInteractionsRecursive(refImpl, interactions);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isNestedRefsObject(obj) {
|
|
346
|
+
if (!obj || typeof obj !== "object")
|
|
347
|
+
return false;
|
|
348
|
+
if (obj.elements instanceof Set)
|
|
349
|
+
return false;
|
|
350
|
+
const proto = Object.getPrototypeOf(obj);
|
|
351
|
+
if (proto !== Object.prototype && proto !== null)
|
|
352
|
+
return false;
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
function getElementType(element) {
|
|
356
|
+
return element.constructor.name;
|
|
357
|
+
}
|
|
358
|
+
function getSupportedEvents(element) {
|
|
359
|
+
const base = ["click", "focus", "blur"];
|
|
360
|
+
if (element instanceof HTMLInputElement) {
|
|
361
|
+
return [...base, "input", "change"];
|
|
362
|
+
}
|
|
363
|
+
if (element instanceof HTMLButtonElement) {
|
|
364
|
+
return ["click", "focus", "blur"];
|
|
365
|
+
}
|
|
366
|
+
if (element instanceof HTMLSelectElement) {
|
|
367
|
+
return [...base, "change"];
|
|
368
|
+
}
|
|
369
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
370
|
+
return [...base, "input", "change"];
|
|
371
|
+
}
|
|
372
|
+
if (element instanceof HTMLAnchorElement) {
|
|
373
|
+
return ["click"];
|
|
374
|
+
}
|
|
375
|
+
if (element instanceof HTMLFormElement) {
|
|
376
|
+
return ["submit", "reset"];
|
|
377
|
+
}
|
|
378
|
+
return base;
|
|
379
|
+
}
|
|
380
|
+
const VIEW_STATE_CHANGE = "viewStateChange";
|
|
381
|
+
class AutomationAgent {
|
|
382
|
+
constructor(component2, options) {
|
|
383
|
+
__publicField2(this, "stateListeners", /* @__PURE__ */ new Set());
|
|
384
|
+
__publicField2(this, "cachedInteractions", null);
|
|
385
|
+
__publicField2(this, "viewStateHandler", null);
|
|
386
|
+
__publicField2(this, "mergedViewState");
|
|
387
|
+
__publicField2(this, "initialSlowViewState");
|
|
388
|
+
__publicField2(this, "trackByMap");
|
|
389
|
+
this.component = component2;
|
|
390
|
+
if (options) {
|
|
391
|
+
this.initialSlowViewState = options.initialViewState;
|
|
392
|
+
this.trackByMap = options.trackByMap;
|
|
393
|
+
this.mergedViewState = deepMergeViewStates(
|
|
394
|
+
options.initialViewState,
|
|
395
|
+
this.component.viewState || {},
|
|
396
|
+
options.trackByMap
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
this.subscribeToUpdates();
|
|
400
|
+
}
|
|
401
|
+
subscribeToUpdates() {
|
|
402
|
+
this.viewStateHandler = () => {
|
|
403
|
+
this.cachedInteractions = null;
|
|
404
|
+
if (this.initialSlowViewState && this.trackByMap) {
|
|
405
|
+
this.mergedViewState = deepMergeViewStates(
|
|
406
|
+
this.initialSlowViewState,
|
|
407
|
+
this.component.viewState || {},
|
|
408
|
+
this.trackByMap
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
this.notifyListeners();
|
|
412
|
+
};
|
|
413
|
+
this.component.addEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
|
|
414
|
+
}
|
|
415
|
+
notifyListeners() {
|
|
416
|
+
if (this.stateListeners.size === 0)
|
|
417
|
+
return;
|
|
418
|
+
const state = this.getPageState();
|
|
419
|
+
this.stateListeners.forEach((callback) => callback(state));
|
|
420
|
+
}
|
|
421
|
+
getPageState() {
|
|
422
|
+
if (!this.cachedInteractions) {
|
|
423
|
+
this.cachedInteractions = collectInteractions(this.component.element?.refs);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
// Use merged state if available (slow+fast), otherwise component's viewState
|
|
427
|
+
viewState: this.mergedViewState || this.component.viewState,
|
|
428
|
+
interactions: this.cachedInteractions,
|
|
429
|
+
customEvents: this.getCustomEvents()
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
triggerEvent(eventType, coordinate, eventData) {
|
|
433
|
+
const interaction = this.getInteraction(coordinate);
|
|
434
|
+
if (!interaction) {
|
|
435
|
+
throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
|
|
436
|
+
}
|
|
437
|
+
const event = new Event(eventType, { bubbles: true });
|
|
438
|
+
if (eventData) {
|
|
439
|
+
Object.assign(event, eventData);
|
|
440
|
+
}
|
|
441
|
+
interaction.element.dispatchEvent(event);
|
|
442
|
+
}
|
|
443
|
+
getInteraction(coordinate) {
|
|
444
|
+
const state = this.getPageState();
|
|
445
|
+
return state.interactions.find(
|
|
446
|
+
(i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
onStateChange(callback) {
|
|
450
|
+
this.stateListeners.add(callback);
|
|
451
|
+
return () => this.stateListeners.delete(callback);
|
|
452
|
+
}
|
|
453
|
+
getCustomEvents() {
|
|
454
|
+
const events = [];
|
|
455
|
+
const component2 = this.component;
|
|
456
|
+
for (const key in component2) {
|
|
457
|
+
if (component2[key]?.emit && typeof component2[key].emit === "function") {
|
|
458
|
+
const name = key.startsWith("on") ? key.slice(2) : key;
|
|
459
|
+
events.push({ name });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return events;
|
|
463
|
+
}
|
|
464
|
+
onComponentEvent(eventName, callback) {
|
|
465
|
+
const component2 = this.component;
|
|
466
|
+
const handlerKey = eventName.startsWith("on") ? eventName : `on${eventName}`;
|
|
467
|
+
const handler = component2[handlerKey];
|
|
468
|
+
if (!handler || typeof handler !== "function") {
|
|
469
|
+
throw new Error(`Unknown component event: ${eventName}`);
|
|
470
|
+
}
|
|
471
|
+
handler(({ event }) => callback(event));
|
|
472
|
+
return () => handler(void 0);
|
|
473
|
+
}
|
|
474
|
+
dispose() {
|
|
475
|
+
if (this.viewStateHandler) {
|
|
476
|
+
this.component.removeEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
|
|
477
|
+
this.viewStateHandler = null;
|
|
478
|
+
}
|
|
479
|
+
this.stateListeners.clear();
|
|
480
|
+
this.cachedInteractions = null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function wrapWithAutomation(component2, options) {
|
|
484
|
+
const agent = new AutomationAgent(component2, options);
|
|
485
|
+
return Object.assign(component2, { automation: agent });
|
|
486
|
+
}
|
|
487
|
+
const AUTOMATION_CONTEXT = runtime.createJayContext();
|
|
488
|
+
exports.AUTOMATION_CONTEXT = AUTOMATION_CONTEXT;
|
|
489
|
+
exports.ActionError = ActionError;
|
|
490
|
+
exports.createActionCaller = createActionCaller;
|
|
50
491
|
exports.makeCompositeJayComponent = makeCompositeJayComponent;
|
|
492
|
+
exports.setActionCallerOptions = setActionCallerOptions;
|
|
493
|
+
exports.wrapWithAutomation = wrapWithAutomation;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,85 @@
|
|
|
1
1
|
import * as _jay_framework_component from '@jay-framework/component';
|
|
2
2
|
import { ComponentConstructor, ContextMarkers, JayComponentCore } from '@jay-framework/component';
|
|
3
3
|
import { JayElement, PreRenderElement } from '@jay-framework/runtime';
|
|
4
|
+
import { TrackByMap } from '@jay-framework/view-state-merge';
|
|
5
|
+
export { AUTOMATION_CONTEXT, AutomationAPI, AutomationWrappedComponent, Coordinate, Interaction, PageState, wrapWithAutomation } from '@jay-framework/runtime-automation';
|
|
4
6
|
|
|
5
7
|
interface CompositePart {
|
|
6
|
-
|
|
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>;
|
|
7
14
|
contextMarkers: ContextMarkers<any>;
|
|
8
15
|
key?: string;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
|
-
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
|
|
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>;
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Client-side action caller for Jay Stack.
|
|
22
|
+
*
|
|
23
|
+
* This module provides the client-side implementation for calling server actions.
|
|
24
|
+
* It replaces the server-side action handler when the action is imported in client code.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* HTTP method types supported by actions.
|
|
28
|
+
*/
|
|
29
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
30
|
+
/**
|
|
31
|
+
* Error thrown when an action fails on the server.
|
|
32
|
+
* Recreated on the client from the error response.
|
|
33
|
+
*/
|
|
34
|
+
declare class ActionError extends Error {
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly name = "ActionError";
|
|
37
|
+
constructor(code: string, message: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Options for configuring the action caller.
|
|
41
|
+
*/
|
|
42
|
+
interface ActionCallerOptions {
|
|
43
|
+
/** Base URL for action endpoints (default: '') */
|
|
44
|
+
baseUrl?: string;
|
|
45
|
+
/** Custom headers to include with requests */
|
|
46
|
+
headers?: Record<string, string>;
|
|
47
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
48
|
+
timeout?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sets global options for all action callers.
|
|
52
|
+
*
|
|
53
|
+
* @param options - Options to apply globally
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* setActionCallerOptions({
|
|
58
|
+
* baseUrl: 'https://api.example.com',
|
|
59
|
+
* headers: { 'X-Custom-Header': 'value' },
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
declare function setActionCallerOptions(options: ActionCallerOptions): void;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a client-side action caller that makes HTTP requests to the server.
|
|
66
|
+
*
|
|
67
|
+
* This function is used by the build transform to replace server-side action handlers
|
|
68
|
+
* with client-side HTTP callers.
|
|
69
|
+
*
|
|
70
|
+
* @param actionName - The unique action name (matches server registration)
|
|
71
|
+
* @param method - The HTTP method (default: POST)
|
|
72
|
+
* @returns A callable function that makes the HTTP request
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Build transform replaces:
|
|
77
|
+
* import { addToCart } from './actions/cart.actions';
|
|
78
|
+
*
|
|
79
|
+
* // With:
|
|
80
|
+
* const addToCart = createActionCaller<{productId: string}, {cartCount: number}>('cart.addToCart', 'POST');
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
declare function createActionCaller<Input, Output>(actionName: string, method?: HttpMethod): (input: Input) => Promise<Output>;
|
|
84
|
+
|
|
85
|
+
export { type ActionCallerOptions, ActionError, type CompositePart, type HttpMethod, createActionCaller, makeCompositeJayComponent, setActionCallerOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,42 +1,136 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => {
|
|
4
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
1
7
|
import { makeJayComponent, materializeViewState, createSignal } from "@jay-framework/component";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
8
|
+
import { createJayContext } from "@jay-framework/runtime";
|
|
9
|
+
function deepMergeViewStates$1(base, overlay, trackByMap, path = "") {
|
|
10
|
+
if (!base && !overlay)
|
|
11
|
+
return {};
|
|
12
|
+
if (!base)
|
|
13
|
+
return overlay || {};
|
|
14
|
+
if (!overlay)
|
|
15
|
+
return base || {};
|
|
16
|
+
const result = {};
|
|
17
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
|
|
18
|
+
for (const key of allKeys) {
|
|
19
|
+
const baseValue = base[key];
|
|
20
|
+
const overlayValue = overlay[key];
|
|
21
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
22
|
+
if (overlayValue === void 0) {
|
|
23
|
+
result[key] = baseValue;
|
|
24
|
+
} else if (baseValue === void 0) {
|
|
25
|
+
result[key] = overlayValue;
|
|
26
|
+
} else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
27
|
+
const trackByField = trackByMap[currentPath];
|
|
28
|
+
if (trackByField) {
|
|
29
|
+
result[key] = mergeArraysByTrackBy$1(
|
|
30
|
+
baseValue,
|
|
31
|
+
overlayValue,
|
|
32
|
+
trackByField,
|
|
33
|
+
trackByMap,
|
|
34
|
+
currentPath
|
|
35
|
+
);
|
|
36
|
+
} else {
|
|
37
|
+
result[key] = overlayValue;
|
|
38
|
+
}
|
|
39
|
+
} else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
|
|
40
|
+
result[key] = deepMergeViewStates$1(baseValue, overlayValue, trackByMap, currentPath);
|
|
41
|
+
} else {
|
|
42
|
+
result[key] = overlayValue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function mergeArraysByTrackBy$1(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
|
|
48
|
+
const baseByKey = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const item of baseArray) {
|
|
50
|
+
const key = item[trackByField];
|
|
51
|
+
if (key !== void 0 && key !== null) {
|
|
52
|
+
if (baseByKey.has(key)) {
|
|
53
|
+
console.warn(
|
|
54
|
+
`Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
baseByKey.set(key, item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const overlayByKey = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const item of overlayArray) {
|
|
62
|
+
const key = item[trackByField];
|
|
63
|
+
if (key !== void 0 && key !== null) {
|
|
64
|
+
overlayByKey.set(key, item);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return baseArray.map((baseItem) => {
|
|
68
|
+
const key = baseItem[trackByField];
|
|
69
|
+
if (key === void 0 || key === null) {
|
|
70
|
+
return baseItem;
|
|
71
|
+
}
|
|
72
|
+
const overlayItem = overlayByKey.get(key);
|
|
73
|
+
if (overlayItem) {
|
|
74
|
+
return deepMergeViewStates$1(baseItem, overlayItem, trackByMap, arrayPath);
|
|
75
|
+
} else {
|
|
76
|
+
return baseItem;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function makeSignals(obj) {
|
|
81
|
+
return Object.keys(obj).reduce((signals, key) => {
|
|
82
|
+
signals[key] = createSignal(obj[key]);
|
|
5
83
|
return signals;
|
|
6
84
|
}, {});
|
|
7
85
|
}
|
|
8
|
-
function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward, parts) {
|
|
86
|
+
function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward, parts, trackByMap = {}) {
|
|
87
|
+
const interactiveParts = parts.filter((part) => part.comp !== void 0);
|
|
88
|
+
const hasFastRendering = defaultViewState !== null && defaultViewState !== void 0;
|
|
9
89
|
const comp = (props, refs, ...contexts) => {
|
|
10
|
-
const instances =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
90
|
+
const instances = interactiveParts.map(
|
|
91
|
+
(part) => {
|
|
92
|
+
const partRefs = part.key ? refs[part.key] : refs;
|
|
93
|
+
let partContexts;
|
|
94
|
+
if (hasFastRendering) {
|
|
95
|
+
const partViewState = part.key ? defaultViewState?.[part.key] : defaultViewState;
|
|
96
|
+
const partFastViewState = partViewState ? makeSignals(partViewState) : void 0;
|
|
97
|
+
const partCarryForward = part.key ? fastCarryForward?.[part.key] : fastCarryForward;
|
|
98
|
+
partContexts = [
|
|
99
|
+
partFastViewState,
|
|
100
|
+
partCarryForward,
|
|
101
|
+
...contexts.splice(0, part.contextMarkers.length)
|
|
102
|
+
];
|
|
103
|
+
} else {
|
|
104
|
+
partContexts = [...contexts.splice(0, part.contextMarkers.length)];
|
|
105
|
+
}
|
|
106
|
+
return [part.key, part.comp(props, partRefs, ...partContexts)];
|
|
107
|
+
}
|
|
108
|
+
);
|
|
23
109
|
return {
|
|
24
110
|
render: () => {
|
|
25
111
|
let viewState = defaultViewState;
|
|
26
112
|
instances.forEach(([key, instance]) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
113
|
+
const rendered = materializeViewState(instance.render());
|
|
114
|
+
if (key) {
|
|
115
|
+
viewState[key] = deepMergeViewStates$1(
|
|
116
|
+
defaultViewState[key],
|
|
117
|
+
rendered,
|
|
118
|
+
trackByMap,
|
|
119
|
+
key
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
viewState = deepMergeViewStates$1(
|
|
123
|
+
viewState,
|
|
124
|
+
rendered,
|
|
125
|
+
trackByMap
|
|
126
|
+
);
|
|
127
|
+
}
|
|
34
128
|
});
|
|
35
129
|
return viewState;
|
|
36
130
|
}
|
|
37
131
|
};
|
|
38
132
|
};
|
|
39
|
-
const contextMarkers =
|
|
133
|
+
const contextMarkers = interactiveParts.reduce((cm, part) => {
|
|
40
134
|
return [...cm, ...part.contextMarkers];
|
|
41
135
|
}, []);
|
|
42
136
|
return makeJayComponent(
|
|
@@ -45,6 +139,355 @@ function makeCompositeJayComponent(preRender, defaultViewState, fastCarryForward
|
|
|
45
139
|
...contextMarkers
|
|
46
140
|
);
|
|
47
141
|
}
|
|
142
|
+
const ACTION_ENDPOINT_BASE = "/_jay/actions";
|
|
143
|
+
class ActionError extends Error {
|
|
144
|
+
constructor(code, message) {
|
|
145
|
+
super(message);
|
|
146
|
+
__publicField(this, "name", "ActionError");
|
|
147
|
+
this.code = code;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let globalOptions = {};
|
|
151
|
+
function setActionCallerOptions(options) {
|
|
152
|
+
globalOptions = { ...globalOptions, ...options };
|
|
153
|
+
}
|
|
154
|
+
function createActionCaller(actionName, method = "POST") {
|
|
155
|
+
return async (input) => {
|
|
156
|
+
const baseUrl = globalOptions.baseUrl ?? "";
|
|
157
|
+
const url = buildActionUrl(baseUrl, actionName, method, input);
|
|
158
|
+
const fetchOptions = {
|
|
159
|
+
method,
|
|
160
|
+
headers: {
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
...globalOptions.headers
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
if (method !== "GET") {
|
|
166
|
+
fetchOptions.body = JSON.stringify(input);
|
|
167
|
+
}
|
|
168
|
+
const timeout = globalOptions.timeout ?? 3e4;
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
171
|
+
fetchOptions.signal = controller.signal;
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(url, fetchOptions);
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
const data = await response.json();
|
|
176
|
+
if (data.success) {
|
|
177
|
+
return data.data;
|
|
178
|
+
} else {
|
|
179
|
+
if (data.error) {
|
|
180
|
+
throw new ActionError(data.error.code, data.error.message);
|
|
181
|
+
}
|
|
182
|
+
throw new ActionError("UNKNOWN_ERROR", "Unknown error occurred");
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
clearTimeout(timeoutId);
|
|
186
|
+
if (error instanceof ActionError) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
190
|
+
throw new ActionError(
|
|
191
|
+
"TIMEOUT",
|
|
192
|
+
`Action '${actionName}' timed out after ${timeout}ms`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (error instanceof TypeError) {
|
|
196
|
+
throw new ActionError(
|
|
197
|
+
"NETWORK_ERROR",
|
|
198
|
+
`Network error calling '${actionName}': ${error.message}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function buildActionUrl(baseUrl, actionName, method, input) {
|
|
206
|
+
const path = `${ACTION_ENDPOINT_BASE}/${actionName}`;
|
|
207
|
+
const fullUrl = `${baseUrl}${path}`;
|
|
208
|
+
if (method === "GET" && input !== void 0 && input !== null) {
|
|
209
|
+
const params = new URLSearchParams();
|
|
210
|
+
if (isSimpleObject(input)) {
|
|
211
|
+
for (const [key, value] of Object.entries(input)) {
|
|
212
|
+
if (value !== void 0) {
|
|
213
|
+
params.append(key, String(value));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
params.append("_input", JSON.stringify(input));
|
|
218
|
+
}
|
|
219
|
+
const queryString = params.toString();
|
|
220
|
+
return queryString ? `${fullUrl}?${queryString}` : fullUrl;
|
|
221
|
+
}
|
|
222
|
+
return fullUrl;
|
|
223
|
+
}
|
|
224
|
+
function isSimpleObject(obj) {
|
|
225
|
+
if (typeof obj !== "object" || obj === null) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
for (const value of Object.values(obj)) {
|
|
229
|
+
if (typeof value === "object" && value !== null) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
var __defProp2 = Object.defineProperty;
|
|
236
|
+
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
237
|
+
var __publicField2 = (obj, key, value) => {
|
|
238
|
+
__defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
239
|
+
return value;
|
|
240
|
+
};
|
|
241
|
+
function deepMergeViewStates(base, overlay, trackByMap, path = "") {
|
|
242
|
+
if (!base && !overlay)
|
|
243
|
+
return {};
|
|
244
|
+
if (!base)
|
|
245
|
+
return overlay || {};
|
|
246
|
+
if (!overlay)
|
|
247
|
+
return base || {};
|
|
248
|
+
const result = {};
|
|
249
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
|
|
250
|
+
for (const key of allKeys) {
|
|
251
|
+
const baseValue = base[key];
|
|
252
|
+
const overlayValue = overlay[key];
|
|
253
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
254
|
+
if (overlayValue === void 0) {
|
|
255
|
+
result[key] = baseValue;
|
|
256
|
+
} else if (baseValue === void 0) {
|
|
257
|
+
result[key] = overlayValue;
|
|
258
|
+
} else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
259
|
+
const trackByField = trackByMap[currentPath];
|
|
260
|
+
if (trackByField) {
|
|
261
|
+
result[key] = mergeArraysByTrackBy(
|
|
262
|
+
baseValue,
|
|
263
|
+
overlayValue,
|
|
264
|
+
trackByField,
|
|
265
|
+
trackByMap,
|
|
266
|
+
currentPath
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
result[key] = overlayValue;
|
|
270
|
+
}
|
|
271
|
+
} else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
|
|
272
|
+
result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
|
|
273
|
+
} else {
|
|
274
|
+
result[key] = overlayValue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
|
|
280
|
+
const baseByKey = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const item of baseArray) {
|
|
282
|
+
const key = item[trackByField];
|
|
283
|
+
if (key !== void 0 && key !== null) {
|
|
284
|
+
if (baseByKey.has(key)) {
|
|
285
|
+
console.warn(
|
|
286
|
+
`Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
baseByKey.set(key, item);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const overlayByKey = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const item of overlayArray) {
|
|
294
|
+
const key = item[trackByField];
|
|
295
|
+
if (key !== void 0 && key !== null) {
|
|
296
|
+
overlayByKey.set(key, item);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return baseArray.map((baseItem) => {
|
|
300
|
+
const key = baseItem[trackByField];
|
|
301
|
+
if (key === void 0 || key === null) {
|
|
302
|
+
return baseItem;
|
|
303
|
+
}
|
|
304
|
+
const overlayItem = overlayByKey.get(key);
|
|
305
|
+
if (overlayItem) {
|
|
306
|
+
return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
|
|
307
|
+
} else {
|
|
308
|
+
return baseItem;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function collectInteractions(refs) {
|
|
313
|
+
const interactions = [];
|
|
314
|
+
if (!refs)
|
|
315
|
+
return interactions;
|
|
316
|
+
collectInteractionsRecursive(refs, interactions);
|
|
317
|
+
return interactions;
|
|
318
|
+
}
|
|
319
|
+
function collectInteractionsRecursive(refs, interactions) {
|
|
320
|
+
if (!refs)
|
|
321
|
+
return;
|
|
322
|
+
for (const [refName, refImpl] of Object.entries(refs)) {
|
|
323
|
+
if (!refImpl)
|
|
324
|
+
continue;
|
|
325
|
+
if (refImpl.elements && refImpl.elements instanceof Set) {
|
|
326
|
+
for (const elem of refImpl.elements) {
|
|
327
|
+
if (elem.element) {
|
|
328
|
+
interactions.push({
|
|
329
|
+
refName,
|
|
330
|
+
coordinate: elem.coordinate || [refName],
|
|
331
|
+
element: elem.element,
|
|
332
|
+
elementType: getElementType(elem.element),
|
|
333
|
+
supportedEvents: getSupportedEvents(elem.element),
|
|
334
|
+
itemContext: elem.viewState
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} else if (isNestedRefsObject(refImpl)) {
|
|
339
|
+
collectInteractionsRecursive(refImpl, interactions);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function isNestedRefsObject(obj) {
|
|
344
|
+
if (!obj || typeof obj !== "object")
|
|
345
|
+
return false;
|
|
346
|
+
if (obj.elements instanceof Set)
|
|
347
|
+
return false;
|
|
348
|
+
const proto = Object.getPrototypeOf(obj);
|
|
349
|
+
if (proto !== Object.prototype && proto !== null)
|
|
350
|
+
return false;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
function getElementType(element) {
|
|
354
|
+
return element.constructor.name;
|
|
355
|
+
}
|
|
356
|
+
function getSupportedEvents(element) {
|
|
357
|
+
const base = ["click", "focus", "blur"];
|
|
358
|
+
if (element instanceof HTMLInputElement) {
|
|
359
|
+
return [...base, "input", "change"];
|
|
360
|
+
}
|
|
361
|
+
if (element instanceof HTMLButtonElement) {
|
|
362
|
+
return ["click", "focus", "blur"];
|
|
363
|
+
}
|
|
364
|
+
if (element instanceof HTMLSelectElement) {
|
|
365
|
+
return [...base, "change"];
|
|
366
|
+
}
|
|
367
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
368
|
+
return [...base, "input", "change"];
|
|
369
|
+
}
|
|
370
|
+
if (element instanceof HTMLAnchorElement) {
|
|
371
|
+
return ["click"];
|
|
372
|
+
}
|
|
373
|
+
if (element instanceof HTMLFormElement) {
|
|
374
|
+
return ["submit", "reset"];
|
|
375
|
+
}
|
|
376
|
+
return base;
|
|
377
|
+
}
|
|
378
|
+
const VIEW_STATE_CHANGE = "viewStateChange";
|
|
379
|
+
class AutomationAgent {
|
|
380
|
+
constructor(component, options) {
|
|
381
|
+
__publicField2(this, "stateListeners", /* @__PURE__ */ new Set());
|
|
382
|
+
__publicField2(this, "cachedInteractions", null);
|
|
383
|
+
__publicField2(this, "viewStateHandler", null);
|
|
384
|
+
__publicField2(this, "mergedViewState");
|
|
385
|
+
__publicField2(this, "initialSlowViewState");
|
|
386
|
+
__publicField2(this, "trackByMap");
|
|
387
|
+
this.component = component;
|
|
388
|
+
if (options) {
|
|
389
|
+
this.initialSlowViewState = options.initialViewState;
|
|
390
|
+
this.trackByMap = options.trackByMap;
|
|
391
|
+
this.mergedViewState = deepMergeViewStates(
|
|
392
|
+
options.initialViewState,
|
|
393
|
+
this.component.viewState || {},
|
|
394
|
+
options.trackByMap
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
this.subscribeToUpdates();
|
|
398
|
+
}
|
|
399
|
+
subscribeToUpdates() {
|
|
400
|
+
this.viewStateHandler = () => {
|
|
401
|
+
this.cachedInteractions = null;
|
|
402
|
+
if (this.initialSlowViewState && this.trackByMap) {
|
|
403
|
+
this.mergedViewState = deepMergeViewStates(
|
|
404
|
+
this.initialSlowViewState,
|
|
405
|
+
this.component.viewState || {},
|
|
406
|
+
this.trackByMap
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
this.notifyListeners();
|
|
410
|
+
};
|
|
411
|
+
this.component.addEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
|
|
412
|
+
}
|
|
413
|
+
notifyListeners() {
|
|
414
|
+
if (this.stateListeners.size === 0)
|
|
415
|
+
return;
|
|
416
|
+
const state = this.getPageState();
|
|
417
|
+
this.stateListeners.forEach((callback) => callback(state));
|
|
418
|
+
}
|
|
419
|
+
getPageState() {
|
|
420
|
+
if (!this.cachedInteractions) {
|
|
421
|
+
this.cachedInteractions = collectInteractions(this.component.element?.refs);
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
// Use merged state if available (slow+fast), otherwise component's viewState
|
|
425
|
+
viewState: this.mergedViewState || this.component.viewState,
|
|
426
|
+
interactions: this.cachedInteractions,
|
|
427
|
+
customEvents: this.getCustomEvents()
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
triggerEvent(eventType, coordinate, eventData) {
|
|
431
|
+
const interaction = this.getInteraction(coordinate);
|
|
432
|
+
if (!interaction) {
|
|
433
|
+
throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
|
|
434
|
+
}
|
|
435
|
+
const event = new Event(eventType, { bubbles: true });
|
|
436
|
+
if (eventData) {
|
|
437
|
+
Object.assign(event, eventData);
|
|
438
|
+
}
|
|
439
|
+
interaction.element.dispatchEvent(event);
|
|
440
|
+
}
|
|
441
|
+
getInteraction(coordinate) {
|
|
442
|
+
const state = this.getPageState();
|
|
443
|
+
return state.interactions.find(
|
|
444
|
+
(i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
onStateChange(callback) {
|
|
448
|
+
this.stateListeners.add(callback);
|
|
449
|
+
return () => this.stateListeners.delete(callback);
|
|
450
|
+
}
|
|
451
|
+
getCustomEvents() {
|
|
452
|
+
const events = [];
|
|
453
|
+
const component = this.component;
|
|
454
|
+
for (const key in component) {
|
|
455
|
+
if (component[key]?.emit && typeof component[key].emit === "function") {
|
|
456
|
+
const name = key.startsWith("on") ? key.slice(2) : key;
|
|
457
|
+
events.push({ name });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return events;
|
|
461
|
+
}
|
|
462
|
+
onComponentEvent(eventName, callback) {
|
|
463
|
+
const component = this.component;
|
|
464
|
+
const handlerKey = eventName.startsWith("on") ? eventName : `on${eventName}`;
|
|
465
|
+
const handler = component[handlerKey];
|
|
466
|
+
if (!handler || typeof handler !== "function") {
|
|
467
|
+
throw new Error(`Unknown component event: ${eventName}`);
|
|
468
|
+
}
|
|
469
|
+
handler(({ event }) => callback(event));
|
|
470
|
+
return () => handler(void 0);
|
|
471
|
+
}
|
|
472
|
+
dispose() {
|
|
473
|
+
if (this.viewStateHandler) {
|
|
474
|
+
this.component.removeEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
|
|
475
|
+
this.viewStateHandler = null;
|
|
476
|
+
}
|
|
477
|
+
this.stateListeners.clear();
|
|
478
|
+
this.cachedInteractions = null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function wrapWithAutomation(component, options) {
|
|
482
|
+
const agent = new AutomationAgent(component, options);
|
|
483
|
+
return Object.assign(component, { automation: agent });
|
|
484
|
+
}
|
|
485
|
+
const AUTOMATION_CONTEXT = createJayContext();
|
|
48
486
|
export {
|
|
49
|
-
|
|
487
|
+
AUTOMATION_CONTEXT,
|
|
488
|
+
ActionError,
|
|
489
|
+
createActionCaller,
|
|
490
|
+
makeCompositeJayComponent,
|
|
491
|
+
setActionCallerOptions,
|
|
492
|
+
wrapWithAutomation
|
|
50
493
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/stack-client-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,13 +27,15 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jay-framework/component": "^0.
|
|
31
|
-
"@jay-framework/fullstack-component": "^0.
|
|
32
|
-
"@jay-framework/runtime": "^0.
|
|
30
|
+
"@jay-framework/component": "^0.11.0",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.11.0",
|
|
32
|
+
"@jay-framework/runtime": "^0.11.0",
|
|
33
|
+
"@jay-framework/runtime-automation": "^0.11.0",
|
|
34
|
+
"@jay-framework/view-state-merge": "^0.11.0"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
|
-
"@jay-framework/dev-environment": "^0.
|
|
36
|
-
"@jay-framework/jay-cli": "^0.
|
|
37
|
+
"@jay-framework/dev-environment": "^0.11.0",
|
|
38
|
+
"@jay-framework/jay-cli": "^0.11.0",
|
|
37
39
|
"@types/express": "^5.0.2",
|
|
38
40
|
"@types/node": "^22.15.21",
|
|
39
41
|
"nodemon": "^3.0.3",
|