@syntrologie/adapt-chatbot 2.55.1 → 2.57.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.
Files changed (75) hide show
  1. package/dist/AdaptiveChatBar.d.ts +18 -0
  2. package/dist/AdaptiveChatBar.d.ts.map +1 -1
  3. package/dist/AdaptiveChatBar.interleave.test.d.ts +9 -0
  4. package/dist/AdaptiveChatBar.interleave.test.d.ts.map +1 -0
  5. package/dist/AdaptiveChatBar.js +5 -4
  6. package/dist/AdaptiveChatBarMountable.d.ts +28 -17
  7. package/dist/AdaptiveChatBarMountable.d.ts.map +1 -1
  8. package/dist/AdaptiveChatTrail.d.ts.map +1 -1
  9. package/dist/AdaptiveChatTrail.js +4 -3
  10. package/dist/AdaptiveChipsStrip.d.ts +20 -20
  11. package/dist/AdaptiveChipsStrip.js +3 -3
  12. package/dist/ChatAssistantLit.d.ts.map +1 -1
  13. package/dist/ChatAssistantLit.js +5 -4
  14. package/dist/ChatTransport.d.ts +6 -1
  15. package/dist/ChatTransport.d.ts.map +1 -1
  16. package/dist/cdn.d.ts +1 -0
  17. package/dist/cdn.d.ts.map +1 -1
  18. package/dist/chunk-4V5L7IRV.js +4711 -0
  19. package/dist/chunk-4V5L7IRV.js.map +7 -0
  20. package/dist/{chunk-EXDL3OKE.js → chunk-AIQMQZBV.js} +314 -908
  21. package/dist/{chunk-EXDL3OKE.js.map → chunk-AIQMQZBV.js.map} +4 -4
  22. package/dist/{chunk-TZA7572J.js → chunk-GN7Z5IVP.js} +2 -2
  23. package/dist/{chunk-ETDHHQS3.js → chunk-HV52LYST.js} +12 -12
  24. package/dist/{chunk-ETDHHQS3.js.map → chunk-HV52LYST.js.map} +1 -1
  25. package/dist/{chunk-LJEPZI6Y.js → chunk-LFJ54PIF.js} +27 -8
  26. package/dist/chunk-LFJ54PIF.js.map +7 -0
  27. package/dist/chunk-LY5AGXVD.js +21 -0
  28. package/dist/chunk-VIRIVP7H.js +898 -0
  29. package/dist/chunk-VIRIVP7H.js.map +7 -0
  30. package/dist/{chunk-VFWA5W56.js → chunk-XPUHW2YO.js} +44 -7
  31. package/dist/chunk-XPUHW2YO.js.map +7 -0
  32. package/dist/editor-lit.js +1 -1
  33. package/dist/element-session.d.ts +3 -0
  34. package/dist/element-session.d.ts.map +1 -0
  35. package/dist/element-session.js +9 -0
  36. package/dist/element-session.js.map +7 -0
  37. package/dist/elements/ElementInstanceStore.d.ts +15 -1
  38. package/dist/elements/ElementInstanceStore.d.ts.map +1 -1
  39. package/dist/elements/ElementSessionController.d.ts +37 -0
  40. package/dist/elements/ElementSessionController.d.ts.map +1 -0
  41. package/dist/elements/ElementSessionController.test.d.ts +2 -0
  42. package/dist/elements/ElementSessionController.test.d.ts.map +1 -0
  43. package/dist/elements/TileHandler.d.ts.map +1 -1
  44. package/dist/elements/createElementStore.d.ts +11 -0
  45. package/dist/elements/createElementStore.d.ts.map +1 -0
  46. package/dist/elements/tileLifecycleProps.d.ts +13 -0
  47. package/dist/elements/tileLifecycleProps.d.ts.map +1 -0
  48. package/dist/elements/types.d.ts +7 -0
  49. package/dist/elements/types.d.ts.map +1 -1
  50. package/dist/hostAgentController.d.ts +65 -0
  51. package/dist/hostAgentController.d.ts.map +1 -0
  52. package/dist/hostAgentController.test.d.ts +2 -0
  53. package/dist/hostAgentController.test.d.ts.map +1 -0
  54. package/dist/observer/wire.d.ts +54 -0
  55. package/dist/observer/wire.d.ts.map +1 -0
  56. package/dist/observer/wire.test.d.ts +2 -0
  57. package/dist/observer/wire.test.d.ts.map +1 -0
  58. package/dist/runtime.d.ts +3 -0
  59. package/dist/runtime.d.ts.map +1 -1
  60. package/dist/runtime.js +824 -163
  61. package/dist/runtime.js.map +4 -4
  62. package/dist/schema.d.ts +66 -66
  63. package/dist/schema.d.ts.map +1 -1
  64. package/dist/schema.js +113 -75
  65. package/dist/schema.js.map +3 -3
  66. package/dist/types.d.ts +17 -0
  67. package/dist/types.d.ts.map +1 -1
  68. package/package.json +9 -7
  69. package/dist/chunk-LJEPZI6Y.js.map +0 -7
  70. package/dist/chunk-NGV35N4T.js +0 -474
  71. package/dist/chunk-NGV35N4T.js.map +0 -7
  72. package/dist/chunk-UVKRO5ER.js +0 -13
  73. package/dist/chunk-VFWA5W56.js.map +0 -7
  74. /package/dist/{chunk-TZA7572J.js.map → chunk-GN7Z5IVP.js.map} +0 -0
  75. /package/dist/{chunk-UVKRO5ER.js.map → chunk-LY5AGXVD.js.map} +0 -0
@@ -0,0 +1,898 @@
1
+ import {
2
+ TILE_MOUNTED_EVENT,
3
+ TILE_PATCHED_EVENT,
4
+ TILE_REPLAY_REQUEST_EVENT,
5
+ TILE_UNMOUNTED_EVENT,
6
+ parseTileLayout
7
+ } from "./chunk-4V5L7IRV.js";
8
+
9
+ // src/elements/tileLifecycleProps.ts
10
+ function tileMountProps(instance2, widget) {
11
+ const layout = parseTileLayout(instance2.layout);
12
+ return {
13
+ slot: instance2.placement.slot,
14
+ instance_id: instance2.instance_id,
15
+ widget,
16
+ props: instance2.content ?? {},
17
+ ...layout ? { layout } : {}
18
+ };
19
+ }
20
+
21
+ // src/elements/ElementInstanceStore.ts
22
+ var COMPOSITIONAL_REPLAY_REQUEST_EVENT = "element.compositional_replay_request";
23
+ var COMPOSITIONAL_APPEND_EVENT = "element.compositional_append";
24
+ var CHAT_INLINE_SLOT = "chat-inline";
25
+ function firstText(...candidates) {
26
+ for (const c of candidates) if (typeof c === "string" && c.trim()) return c.trim();
27
+ return void 0;
28
+ }
29
+ function receiptTitle(raw) {
30
+ const c = raw ?? {};
31
+ const list = (v) => Array.isArray(v) ? v : [];
32
+ return firstText(
33
+ c.product?.name,
34
+ c.title,
35
+ c.heading,
36
+ ...list(c.questions).map((q) => q?.question),
37
+ ...list(c.actions).map((a) => a?.config?.question),
38
+ setTitle(list(c.products), " vs "),
39
+ setTitle(list(c.items), null)
40
+ ) ?? "Details";
41
+ }
42
+ function setTitle(entries, pair) {
43
+ const names = entries.map((e) => e?.name).filter((n) => typeof n === "string" && n.trim().length > 0).map((n) => n.trim());
44
+ if (names.length === 0) return void 0;
45
+ if (names.length === 1) return names[0];
46
+ if (names.length === 2 && pair) return `${names[0]}${pair}${names[1]}`;
47
+ return `${names[0]} + ${names.length - 1} more`;
48
+ }
49
+ var ElementInstanceStore = class {
50
+ constructor(options) {
51
+ this._instances = /* @__PURE__ */ new Map();
52
+ /** Active TTL timeout — replaced on each call to `scheduleClientTtl`. */
53
+ this._ttlTimer = null;
54
+ this._disposed = false;
55
+ this._disposing = false;
56
+ /** Instance IDs the client has dismissed. Persists across canvas remounts
57
+ * so a dismissed tile never silently reappears on replay/broadcast. */
58
+ this._dismissedIds = /* @__PURE__ */ new Set();
59
+ this._actions = options.actions;
60
+ this._events = options.events;
61
+ this._resolveTileWidget = options.resolveTileWidget;
62
+ this._handlers = /* @__PURE__ */ new Map();
63
+ for (const handler of options.handlers ?? []) {
64
+ this._handlers.set(handler.typeName, handler);
65
+ }
66
+ this._logger = options.logger ?? console;
67
+ this._subscribeReplayRequests();
68
+ }
69
+ /**
70
+ * Listen for `element.compositional_replay_request` from container
71
+ * widgets and re-publish `element.compositional_append` for every
72
+ * matching item still tracked in `_instances`. This keeps LLM-authored
73
+ * items visible across SPA navigations: the store is a module-level
74
+ * singleton that survives chat-bar unmount/remount, so when a fresh
75
+ * chips-strip mounts and subscribes, asking for a replay re-populates
76
+ * its chip array.
77
+ */
78
+ _subscribeReplayRequests() {
79
+ const subscribe = this._events?.subscribe;
80
+ const publish = this._events?.publish;
81
+ if (!subscribe || !publish) return;
82
+ this._unsubscribeReplay = subscribe(
83
+ {
84
+ names: [
85
+ COMPOSITIONAL_REPLAY_REQUEST_EVENT,
86
+ TILE_REPLAY_REQUEST_EVENT,
87
+ TILE_UNMOUNTED_EVENT
88
+ ]
89
+ },
90
+ (event) => {
91
+ if (this._disposed) return;
92
+ if (event.name === TILE_UNMOUNTED_EVENT) {
93
+ const id = String(event.props?.instance_id ?? "");
94
+ if (id) this._dismissedIds.add(id);
95
+ return;
96
+ }
97
+ if (event.name === COMPOSITIONAL_REPLAY_REQUEST_EVENT) {
98
+ const tileId = String(event.props?.tile_id ?? "");
99
+ if (!tileId) return;
100
+ this._replayItems(tileId, publish);
101
+ return;
102
+ }
103
+ if (event.name === TILE_REPLAY_REQUEST_EVENT) {
104
+ this._replayTiles(publish);
105
+ return;
106
+ }
107
+ }
108
+ );
109
+ }
110
+ _replayItems(tileId, publish) {
111
+ for (const instance2 of this._instances.values()) {
112
+ if (this._disposed) return;
113
+ if (instance2.element_type !== "item") continue;
114
+ if (instance2.placement.type !== "parent_tile") continue;
115
+ if (instance2.placement.tile_id !== tileId) continue;
116
+ if (instance2.state !== "active") continue;
117
+ publish(COMPOSITIONAL_APPEND_EVENT, {
118
+ tile_id: tileId,
119
+ instance_id: instance2.instance_id,
120
+ item: instance2.content,
121
+ position: instance2.placement.position
122
+ });
123
+ }
124
+ }
125
+ _replayTiles(publish) {
126
+ for (const instance2 of this._instances.values()) {
127
+ if (this._disposed) return;
128
+ if (instance2.element_type !== "tile") continue;
129
+ if (instance2.placement.type !== "slot") continue;
130
+ if (instance2.state !== "active") continue;
131
+ if (this._dismissedIds.has(instance2.instance_id)) continue;
132
+ const props = tileMountProps(
133
+ { ...instance2, placement: instance2.placement },
134
+ this._resolveTileWidget?.(instance2.template_id) ?? instance2.template_id
135
+ );
136
+ publish(TILE_MOUNTED_EVENT, props);
137
+ }
138
+ }
139
+ registerHandler(handler) {
140
+ this._handlers.set(handler.typeName, handler);
141
+ }
142
+ isDismissed(instanceId) {
143
+ return this._dismissedIds.has(instanceId);
144
+ }
145
+ getDismissedIds() {
146
+ return this._dismissedIds;
147
+ }
148
+ /** Store-driven receipt source for the chat trail (one per active slot-tile). */
149
+ listTileReceipts() {
150
+ const out = [];
151
+ for (const inst of this._instances.values()) {
152
+ if (inst.element_type !== "tile" || inst.placement.type !== "slot") continue;
153
+ if (inst.state !== "active") continue;
154
+ if (inst.placement.slot === CHAT_INLINE_SLOT) continue;
155
+ const title = receiptTitle(inst.content);
156
+ const imgContent = inst.content;
157
+ const imageCandidate = imgContent?.product?.image?.src ?? imgContent?.items?.[0]?.image_url ?? imgContent?.products?.[0]?.image?.src;
158
+ const imageUrl = typeof imageCandidate === "string" && imageCandidate.trim() ? imageCandidate : void 0;
159
+ if (inst.placement.slot === "alert") {
160
+ out.push({
161
+ kind: "alert",
162
+ instanceId: inst.instance_id,
163
+ title,
164
+ turnId: inst.mounted_by?.turn_id,
165
+ toolCallId: inst.tool_call_id ?? void 0
166
+ });
167
+ } else {
168
+ out.push({
169
+ kind: "tile",
170
+ instanceId: inst.instance_id,
171
+ title,
172
+ dismissed: this._dismissedIds.has(inst.instance_id),
173
+ turnId: inst.mounted_by?.turn_id,
174
+ toolCallId: inst.tool_call_id ?? void 0,
175
+ ...imageUrl ? { imageUrl } : {}
176
+ });
177
+ }
178
+ }
179
+ return out;
180
+ }
181
+ /**
182
+ * Store-driven inline-widget source for the chat trail — one entry per
183
+ * active `chat-inline` slot-tile. The trail mounts each entry's widget
184
+ * element (by tag name, via `document.createElement`) inline in the
185
+ * conversation flow. Distinct from {@link listTileReceipts}: those are
186
+ * chips that recall a deck tile; these ARE the rendered widget.
187
+ */
188
+ listInlineWidgets() {
189
+ const out = [];
190
+ for (const inst of this._instances.values()) {
191
+ if (inst.element_type !== "tile" || inst.placement.type !== "slot") continue;
192
+ if (inst.placement.slot !== CHAT_INLINE_SLOT) continue;
193
+ if (inst.state !== "active") continue;
194
+ if (this._dismissedIds.has(inst.instance_id)) continue;
195
+ out.push({
196
+ instanceId: inst.instance_id,
197
+ tag: this._resolveTileWidget?.(inst.template_id) ?? inst.template_id,
198
+ props: inst.content ?? {},
199
+ turnId: inst.mounted_by?.turn_id,
200
+ toolCallId: inst.tool_call_id ?? void 0
201
+ });
202
+ }
203
+ return out;
204
+ }
205
+ /** Clear a client dismissal and re-publish the tile's mount from retained content. */
206
+ recreate(instanceId) {
207
+ if (this._disposed) return;
208
+ const instance2 = this._instances.get(instanceId);
209
+ const publish = this._events?.publish;
210
+ if (!instance2 || instance2.element_type !== "tile" || instance2.placement.type !== "slot" || !publish) {
211
+ return;
212
+ }
213
+ this._dismissedIds.delete(instanceId);
214
+ const props = tileMountProps(
215
+ { ...instance2, placement: instance2.placement },
216
+ this._resolveTileWidget?.(instance2.template_id) ?? instance2.template_id
217
+ );
218
+ publish(TILE_MOUNTED_EVENT, props);
219
+ }
220
+ /**
221
+ * Apply the server-provided boot snapshot. For each instance:
222
+ * - stateful: validated + mounted via the type handler;
223
+ * - imperative + already-applied: recorded only (no DOM action);
224
+ * - any: tracked in the in-memory map for future patches.
225
+ *
226
+ * Safe to call once on SDK init. Calling again would re-mount
227
+ * stateful instances; callers should not.
228
+ */
229
+ async hydrate(snapshot) {
230
+ await this._hydrateWithReport(snapshot);
231
+ }
232
+ async _hydrateWithReport(snapshot, reconcile = false) {
233
+ const rejectedInstanceIds = [];
234
+ for (const instance2 of snapshot) {
235
+ if (this._disposed) return { ok: false, rejectedInstanceIds };
236
+ const existing = this._instances.get(instance2.instance_id);
237
+ if (existing && (existing.version > instance2.version || existing.version === instance2.version && (!reconcile || existing.state === instance2.state))) {
238
+ continue;
239
+ }
240
+ if (reconcile && existing?.state !== "active")
241
+ this._dismissedIds.delete(instance2.instance_id);
242
+ const handler = this._handlers.get(instance2.element_type);
243
+ if (!handler) {
244
+ rejectedInstanceIds.push(instance2.instance_id);
245
+ this._logger.warn("elements: hydrate skipped \u2014 unknown element_type", {
246
+ element_type: instance2.element_type,
247
+ instance_id: instance2.instance_id
248
+ });
249
+ continue;
250
+ }
251
+ this._instances.set(instance2.instance_id, instance2);
252
+ if (handler.lifecycle === "imperative") {
253
+ continue;
254
+ }
255
+ if (instance2.state !== "active") {
256
+ continue;
257
+ }
258
+ if (!await this._mount(handler, instance2)) {
259
+ rejectedInstanceIds.push(instance2.instance_id);
260
+ if (!this._disposed) {
261
+ if (existing) this._instances.set(instance2.instance_id, existing);
262
+ else this._instances.delete(instance2.instance_id);
263
+ }
264
+ }
265
+ }
266
+ this._broadcastActive();
267
+ return { ok: !this._disposed && rejectedInstanceIds.length === 0, rejectedInstanceIds };
268
+ }
269
+ /**
270
+ * Re-publish `element.tile_mounted` / `element.compositional_append`
271
+ * for every active LLM-authored instance in the store. Idempotent —
272
+ * receivers key by `instance_id` (tiles) or by widget identity
273
+ * (items via tile_id filter).
274
+ */
275
+ _broadcastActive() {
276
+ if (this._disposed) return;
277
+ const publish = this._events?.publish;
278
+ if (!publish) return;
279
+ this._replayTiles(publish);
280
+ const itemTileIds = /* @__PURE__ */ new Set();
281
+ for (const instance2 of this._instances.values()) {
282
+ if (instance2.element_type !== "item") continue;
283
+ if (instance2.placement.type !== "parent_tile") continue;
284
+ if (instance2.state !== "active") continue;
285
+ itemTileIds.add(instance2.placement.tile_id);
286
+ }
287
+ for (const tileId of itemTileIds) {
288
+ this._replayItems(tileId, publish);
289
+ }
290
+ }
291
+ /**
292
+ * Apply a batch of mutations, atomically per-batch. Each mutation is
293
+ * version-checked (mount: must not conflict with existing instance;
294
+ * patch: resulting_version must be > current; unmount: idempotent).
295
+ */
296
+ async apply(mutations) {
297
+ await this.applyWithReport(mutations);
298
+ }
299
+ async applyWithReport(mutations) {
300
+ return this._applyWithReport(mutations, false);
301
+ }
302
+ async _applyWithReport(mutations, authoritative) {
303
+ const rejectedInstanceIds = [];
304
+ for (const mutation of mutations) {
305
+ if (this._disposed) return { ok: false, rejectedInstanceIds };
306
+ const id = (mutation?.op === "mount" ? mutation.instance?.instance_id : mutation?.instance_id) ?? "<invalid>";
307
+ try {
308
+ if (!await this._applyOne(mutation, authoritative)) rejectedInstanceIds.push(id);
309
+ } catch {
310
+ rejectedInstanceIds.push(id);
311
+ this._logger.error("elements: mutation failed", { instance_id: id });
312
+ }
313
+ }
314
+ return { ok: !this._disposed && rejectedInstanceIds.length === 0, rejectedInstanceIds };
315
+ }
316
+ /** Read-only snapshot of currently-known instances. */
317
+ getInstances() {
318
+ return Array.from(this._instances.values());
319
+ }
320
+ getInstance(instance_id) {
321
+ return this._instances.get(instance_id);
322
+ }
323
+ /** Replace a host-provided complete UI snapshot; never replay imperative actions. */
324
+ async reconcileSnapshot(snapshot) {
325
+ const rejected = /* @__PURE__ */ new Set();
326
+ if (this._disposed) return { ok: false, rejectedInstanceIds: [] };
327
+ const active = snapshot.filter(
328
+ (instance2) => instance2.state === "active" && instance2.element_type !== "action"
329
+ );
330
+ const ids = new Set(active.map((instance2) => instance2.instance_id));
331
+ for (const instance2 of this._instances.values()) {
332
+ if (this._disposed) return { ok: false, rejectedInstanceIds: [...rejected] };
333
+ if (ids.has(instance2.instance_id)) continue;
334
+ if (instance2.state === "active") {
335
+ const report2 = await this.applyWithReport([
336
+ { op: "unmount", instance_id: instance2.instance_id }
337
+ ]);
338
+ if (!report2.ok) {
339
+ rejected.add(instance2.instance_id);
340
+ continue;
341
+ }
342
+ }
343
+ if (this._disposed) return { ok: false, rejectedInstanceIds: [...rejected] };
344
+ this._instances.delete(instance2.instance_id);
345
+ this._dismissedIds.delete(instance2.instance_id);
346
+ }
347
+ for (const instance2 of active) {
348
+ if (this._disposed) return { ok: false, rejectedInstanceIds: [...rejected] };
349
+ const existing = this._instances.get(instance2.instance_id);
350
+ if (!existing || existing.state !== "active" || existing.version === instance2.version)
351
+ continue;
352
+ const report2 = await this._applyWithReport(
353
+ [
354
+ {
355
+ op: "patch",
356
+ instance_id: instance2.instance_id,
357
+ patches: [],
358
+ expected_version: existing.version,
359
+ resulting_version: instance2.version,
360
+ resulting_content: instance2.content
361
+ }
362
+ ],
363
+ true
364
+ );
365
+ for (const id of report2.rejectedInstanceIds) rejected.add(id);
366
+ }
367
+ const report = await this._hydrateWithReport(
368
+ active.filter((instance2) => !rejected.has(instance2.instance_id)),
369
+ true
370
+ );
371
+ for (const id of report.rejectedInstanceIds) rejected.add(id);
372
+ return { ok: report.ok && rejected.size === 0, rejectedInstanceIds: [...rejected] };
373
+ }
374
+ /** Release page-lifetime listeners and timers; queued work becomes inert. */
375
+ dispose(unmount = false) {
376
+ if (this._disposed || this._disposing) return;
377
+ this._disposing = true;
378
+ if (unmount) {
379
+ for (const instance2 of this._instances.values()) {
380
+ const handler = this._handlers.get(instance2.element_type);
381
+ if (instance2.state === "active" && handler?.lifecycle !== "imperative") {
382
+ void handler?.unmount(instance2, this._ctx()).catch(() => {
383
+ });
384
+ }
385
+ }
386
+ }
387
+ this._disposed = true;
388
+ this._disposing = false;
389
+ this._unsubscribeReplay?.();
390
+ this._unsubscribeReplay = void 0;
391
+ if (this._ttlTimer !== null) clearTimeout(this._ttlTimer);
392
+ this._ttlTimer = null;
393
+ this._instances.clear();
394
+ this._dismissedIds.clear();
395
+ }
396
+ /**
397
+ * Schedule a client-side TTL that unmounts every active stateful
398
+ * instance when the server-side session expires. Replaces any
399
+ * previously-scheduled TTL.
400
+ *
401
+ * Avoids the orphan-window where session-expired tiles linger on the
402
+ * page until manual refresh — the server forgets at the 24h mark and
403
+ * the SDK tears down concurrently. Re-schedule on each boot fetch /
404
+ * mutation event that carries a refreshed `session_expires_at`.
405
+ *
406
+ * Pass `null` to clear the existing timer without scheduling a new one.
407
+ * Pass a past timestamp to trigger cleanup immediately.
408
+ */
409
+ scheduleClientTtl(session_expires_at) {
410
+ if (this._disposed) return;
411
+ if (this._ttlTimer !== null) {
412
+ clearTimeout(this._ttlTimer);
413
+ this._ttlTimer = null;
414
+ }
415
+ if (session_expires_at === null) return;
416
+ const expiryMs = Date.parse(session_expires_at);
417
+ if (Number.isNaN(expiryMs)) {
418
+ this._logger.warn("elements: invalid session_expires_at", { session_expires_at });
419
+ return;
420
+ }
421
+ const delay = Math.max(0, expiryMs - Date.now());
422
+ this._ttlTimer = setTimeout(() => {
423
+ this._ttlTimer = null;
424
+ void this._handleSessionExpiry();
425
+ }, delay);
426
+ }
427
+ // -------------------------------------------------------------------
428
+ // Internals
429
+ // -------------------------------------------------------------------
430
+ async _handleSessionExpiry() {
431
+ const unmounts = [];
432
+ for (const instance2 of this._instances.values()) {
433
+ if (instance2.state !== "active") continue;
434
+ const handler = this._handlers.get(instance2.element_type);
435
+ if (!handler || handler.lifecycle === "imperative") continue;
436
+ unmounts.push({ op: "unmount", instance_id: instance2.instance_id });
437
+ }
438
+ if (unmounts.length > 0) {
439
+ this._logger.info("elements: client TTL fired \u2014 unmounting active instances", {
440
+ count: unmounts.length
441
+ });
442
+ await this.apply(unmounts);
443
+ }
444
+ }
445
+ async _applyOne(mutation, authoritative = false) {
446
+ if (mutation.op === "mount") {
447
+ const existing = this._instances.get(mutation.instance.instance_id);
448
+ if (existing && existing.version >= mutation.instance.version) {
449
+ return true;
450
+ }
451
+ const handler = this._handlers.get(mutation.instance.element_type);
452
+ if (!handler) {
453
+ this._logger.warn("elements: mount dropped \u2014 unknown element_type", {
454
+ element_type: mutation.instance.element_type
455
+ });
456
+ return false;
457
+ }
458
+ const isActiveStateful = mutation.instance.state === "active" && handler.lifecycle !== "imperative";
459
+ const isAppliedImperative = mutation.instance.state === "applied" && handler.lifecycle === "imperative";
460
+ if (isActiveStateful || isAppliedImperative) {
461
+ this._instances.set(mutation.instance.instance_id, mutation.instance);
462
+ const applied = await this._mount(handler, mutation.instance);
463
+ if (this._disposed) return false;
464
+ if (!applied) {
465
+ if (existing) {
466
+ this._instances.set(mutation.instance.instance_id, existing);
467
+ } else {
468
+ this._instances.delete(mutation.instance.instance_id);
469
+ }
470
+ }
471
+ return applied;
472
+ }
473
+ this._instances.set(mutation.instance.instance_id, mutation.instance);
474
+ return true;
475
+ }
476
+ if (mutation.op === "patch") {
477
+ const existing = this._instances.get(mutation.instance_id);
478
+ if (!existing) {
479
+ this._logger.warn("elements: patch dropped \u2014 unknown instance", {
480
+ instance_id: mutation.instance_id
481
+ });
482
+ return false;
483
+ }
484
+ if (typeof mutation.resulting_version !== "number" || mutation.resulting_content == null || typeof mutation.resulting_content !== "object") {
485
+ this._logger.warn("elements: patch dropped \u2014 malformed envelope (missing resulting_*)", {
486
+ instance_id: mutation.instance_id
487
+ });
488
+ return false;
489
+ }
490
+ if (!authoritative && mutation.resulting_version <= existing.version) {
491
+ return true;
492
+ }
493
+ const patched = {
494
+ ...existing,
495
+ content: mutation.resulting_content,
496
+ version: mutation.resulting_version
497
+ };
498
+ const handler = this._handlers.get(patched.element_type);
499
+ if (handler) {
500
+ const validation = handler.validate(patched, this._ctx());
501
+ if (!validation.accepted) {
502
+ this._logger.warn("elements: patch validation failed", {
503
+ instance_id: mutation.instance_id,
504
+ reason: validation.reason
505
+ });
506
+ return false;
507
+ }
508
+ try {
509
+ await handler.patch(patched, this._ctx());
510
+ } catch (err) {
511
+ this._logger.warn("elements: handler.patch threw", {
512
+ instance_id: mutation.instance_id,
513
+ err
514
+ });
515
+ return false;
516
+ }
517
+ }
518
+ if (this._disposed) return false;
519
+ this._instances.set(mutation.instance_id, patched);
520
+ return true;
521
+ }
522
+ if (mutation.op === "unmount") {
523
+ const existing = this._instances.get(mutation.instance_id);
524
+ if (!existing) {
525
+ return true;
526
+ }
527
+ const reverted = { ...existing, state: "reverted" };
528
+ this._instances.set(mutation.instance_id, reverted);
529
+ const handler = this._handlers.get(existing.element_type);
530
+ if (handler) {
531
+ try {
532
+ await handler.unmount(reverted, this._ctx());
533
+ } catch (err) {
534
+ if (!this._disposed) this._instances.set(mutation.instance_id, existing);
535
+ this._logger.warn("elements: handler.unmount threw", {
536
+ instance_id: mutation.instance_id,
537
+ err
538
+ });
539
+ return false;
540
+ }
541
+ }
542
+ return !this._disposed;
543
+ }
544
+ return false;
545
+ }
546
+ async _mount(handler, instance2) {
547
+ try {
548
+ const validation = handler.validate(instance2, this._ctx());
549
+ if (!validation.accepted) {
550
+ this._logger.warn("elements: mount validation failed", {
551
+ instance_id: instance2.instance_id,
552
+ reason: validation.reason,
553
+ detail: validation.detail
554
+ });
555
+ return false;
556
+ }
557
+ await handler.mount(instance2, this._ctx());
558
+ } catch (err) {
559
+ this._logger.warn("elements: handler.mount threw", {
560
+ instance_id: instance2.instance_id,
561
+ err
562
+ });
563
+ return false;
564
+ }
565
+ return !this._disposed;
566
+ }
567
+ _ctx() {
568
+ return {
569
+ actions: this._actions,
570
+ events: this._events && {
571
+ publish: (name, props) => {
572
+ if (!this._disposed) this._events?.publish(name, props);
573
+ }
574
+ }
575
+ };
576
+ }
577
+ };
578
+
579
+ // src/elements/TileHandler.ts
580
+ var TileHandler = class {
581
+ constructor(resolveWidget) {
582
+ this.resolveWidget = resolveWidget;
583
+ this.typeName = "tile";
584
+ this.lifecycle = "stateful";
585
+ }
586
+ _widgetFor(instance2) {
587
+ return this.resolveWidget?.(instance2.template_id) ?? instance2.template_id;
588
+ }
589
+ validate(instance2, _ctx) {
590
+ if (instance2.element_type !== "tile") {
591
+ return { accepted: false, reason: "wrong_element_type", detail: instance2.element_type };
592
+ }
593
+ if (instance2.placement.type !== "slot") {
594
+ return {
595
+ accepted: false,
596
+ reason: "tile_requires_slot_placement",
597
+ detail: `got placement.type=${instance2.placement.type}`
598
+ };
599
+ }
600
+ if (typeof instance2.template_id !== "string" || instance2.template_id.length === 0) {
601
+ return { accepted: false, reason: "missing_template_id" };
602
+ }
603
+ if (this.resolveWidget && this.resolveWidget(instance2.template_id) === void 0) {
604
+ return { accepted: false, reason: "unknown_template_id", detail: instance2.template_id };
605
+ }
606
+ return { accepted: true };
607
+ }
608
+ async mount(instance2, ctx) {
609
+ if (!ctx.events) return;
610
+ const placement = instance2.placement;
611
+ const props = tileMountProps({ ...instance2, placement }, this._widgetFor(instance2));
612
+ ctx.events.publish(TILE_MOUNTED_EVENT, props);
613
+ }
614
+ async patch(instance2, ctx) {
615
+ if (!ctx.events) return;
616
+ const placement = instance2.placement;
617
+ const props = tileMountProps({ ...instance2, placement }, this._widgetFor(instance2));
618
+ ctx.events.publish(TILE_PATCHED_EVENT, props);
619
+ }
620
+ async unmount(instance2, ctx) {
621
+ if (!ctx.events) return;
622
+ const placement = instance2.placement;
623
+ const props = {
624
+ slot: placement.slot,
625
+ instance_id: instance2.instance_id
626
+ };
627
+ ctx.events.publish(TILE_UNMOUNTED_EVENT, props);
628
+ }
629
+ };
630
+
631
+ // src/elements/ActionHandler.ts
632
+ var ActionHandler = class {
633
+ constructor() {
634
+ this.typeName = "action";
635
+ // v1: only imperative actions are wired. Stateful content-mutation
636
+ // actions arrive with Phase 7 (Portable Text renderer).
637
+ this.lifecycle = "imperative";
638
+ }
639
+ validate(instance2, _ctx) {
640
+ if (instance2.element_type !== "action") {
641
+ return { accepted: false, reason: "wrong_element_type" };
642
+ }
643
+ const kind = instance2.content.__kind;
644
+ if (typeof kind !== "string" || kind.length === 0) {
645
+ return { accepted: false, reason: "missing_action_kind" };
646
+ }
647
+ return { accepted: true };
648
+ }
649
+ async mount(instance2, ctx) {
650
+ const action = this._actionFromInstance(instance2);
651
+ await ctx.actions.applyBatch([action]);
652
+ }
653
+ async patch(_instance, _ctx) {
654
+ }
655
+ async unmount(_instance, _ctx) {
656
+ }
657
+ _actionFromInstance(instance2) {
658
+ const { __kind, ...params } = instance2.content;
659
+ const placement = instance2.placement;
660
+ const anchorOverride = placement.type === "selector" ? { anchorId: { selector: placement.selector, route: "**" } } : {};
661
+ if (typeof params.anchorId === "string") {
662
+ params.anchorId = { selector: params.anchorId, route: "**" };
663
+ }
664
+ return {
665
+ kind: __kind,
666
+ ...params,
667
+ ...anchorOverride,
668
+ label: `action:${instance2.template_id}:${instance2.instance_id}`
669
+ };
670
+ }
671
+ };
672
+
673
+ // src/elements/ItemHandler.ts
674
+ var COMPOSITIONAL_APPEND_EVENT2 = "element.compositional_append";
675
+ var COMPOSITIONAL_PATCH_EVENT = "element.compositional_patch";
676
+ var COMPOSITIONAL_REMOVE_EVENT = "element.compositional_remove";
677
+ var ItemHandler = class {
678
+ constructor() {
679
+ this.typeName = "item";
680
+ this.lifecycle = "stateful";
681
+ }
682
+ validate(instance2, _ctx) {
683
+ if (instance2.element_type !== "item") {
684
+ return {
685
+ accepted: false,
686
+ reason: "wrong_element_type",
687
+ detail: instance2.element_type
688
+ };
689
+ }
690
+ if (instance2.placement.type !== "parent_tile") {
691
+ return {
692
+ accepted: false,
693
+ reason: "item_requires_parent_tile_placement",
694
+ detail: `got placement.type=${instance2.placement.type}`
695
+ };
696
+ }
697
+ if (typeof instance2.template_id !== "string" || instance2.template_id.length === 0) {
698
+ return { accepted: false, reason: "missing_template_id" };
699
+ }
700
+ return { accepted: true };
701
+ }
702
+ async mount(instance2, ctx) {
703
+ if (!ctx.events) return;
704
+ const placement = instance2.placement;
705
+ ctx.events.publish(COMPOSITIONAL_APPEND_EVENT2, {
706
+ tile_id: placement.tile_id,
707
+ instance_id: instance2.instance_id,
708
+ item: instance2.content,
709
+ position: placement.position
710
+ });
711
+ }
712
+ async patch(instance2, ctx) {
713
+ if (!ctx.events) return;
714
+ const placement = instance2.placement;
715
+ ctx.events.publish(COMPOSITIONAL_PATCH_EVENT, {
716
+ tile_id: placement.tile_id,
717
+ instance_id: instance2.instance_id,
718
+ item: instance2.content
719
+ });
720
+ }
721
+ async unmount(instance2, ctx) {
722
+ if (!ctx.events) return;
723
+ const placement = instance2.placement;
724
+ ctx.events.publish(COMPOSITIONAL_REMOVE_EVENT, {
725
+ tile_id: placement.tile_id,
726
+ instance_id: instance2.instance_id
727
+ });
728
+ }
729
+ };
730
+
731
+ // src/elements/createElementStore.ts
732
+ function createTemplateWidgetResolver() {
733
+ const widgets = /* @__PURE__ */ new Map();
734
+ return {
735
+ resolve: (templateId) => widgets.get(templateId),
736
+ refresh(uiTemplates) {
737
+ widgets.clear();
738
+ widgets.set("syntro-product-reco-carousel", "syntro-product-reco-carousel");
739
+ const tiles = uiTemplates?.tiles;
740
+ if (!Array.isArray(tiles)) return;
741
+ for (const template of tiles) {
742
+ if (typeof template?.id === "string" && template.id && typeof template?.widget === "string" && template.widget) {
743
+ widgets.set(template.id, template.widget);
744
+ }
745
+ }
746
+ }
747
+ };
748
+ }
749
+ function createElementStore(runtime, resolveTileWidget) {
750
+ return new ElementInstanceStore({
751
+ actions: runtime.actions,
752
+ events: {
753
+ publish: runtime.events.publish.bind(runtime.events),
754
+ subscribe: runtime.events.subscribe?.bind(runtime.events)
755
+ },
756
+ handlers: [new TileHandler(resolveTileWidget), new ActionHandler(), new ItemHandler()],
757
+ resolveTileWidget
758
+ });
759
+ }
760
+
761
+ // src/elements/ElementSessionController.ts
762
+ function record(value) {
763
+ return value !== null && typeof value === "object" && !Array.isArray(value);
764
+ }
765
+ function text(value) {
766
+ return typeof value === "string" && value.trim().length > 0;
767
+ }
768
+ function integer(value) {
769
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
770
+ }
771
+ function expiry(value) {
772
+ return value === null || typeof value === "string" && Number.isFinite(Date.parse(value));
773
+ }
774
+ function instance(value) {
775
+ if (!record(value) || !text(value.instance_id) || !text(value.template_id) || !record(value.content) || !record(value.placement) || !record(value.mounted_by) || !text(value.mounted_by.agent_id) || !text(value.mounted_by.turn_id) || typeof value.mounted_at !== "string" || !Number.isFinite(Date.parse(value.mounted_at)) || !integer(value.version) || value.version < 1 || typeof value.state !== "string" || !["active", "applied", "reverted", "expired"].includes(value.state))
776
+ return false;
777
+ if (value.element_type === "tile")
778
+ return value.placement.type === "slot" && text(value.placement.slot);
779
+ if (value.element_type === "item")
780
+ return value.placement.type === "parent_tile" && text(value.placement.tile_id) && typeof value.placement.position === "string" && ["append", "prepend"].includes(value.placement.position);
781
+ if (value.element_type !== "action") return false;
782
+ return value.placement.type === "none" || value.placement.type === "selector" && text(value.placement.selector) && typeof value.placement.position === "string" && ["beforebegin", "afterbegin", "beforeend", "afterend", "replace", "modify"].includes(
783
+ value.placement.position
784
+ );
785
+ }
786
+ function frame(value) {
787
+ if (!record(value) || value.schema_version !== "v1" || value.kind !== "snapshot" || !text(value.session_ref) || !text(value.result_id) || !expiry(value.session_expires_at) || !Array.isArray(value.mounted_elements) || !value.mounted_elements.every(instance))
788
+ return false;
789
+ return new Set(value.mounted_elements.map((item) => item.instance_id)).size === value.mounted_elements.length;
790
+ }
791
+ var APPLIED_HISTORY = 32;
792
+ function createElementSessionController(options) {
793
+ const templates = createTemplateWidgetResolver();
794
+ templates.refresh(options.templates);
795
+ const resolveTileWidget = (id) => templates.resolve(id) ?? (options.legacyTemplateFallback && !options.hostScope ? id : void 0);
796
+ const store = createElementStore(options, resolveTileWidget);
797
+ const cleanups = /* @__PURE__ */ new Set();
798
+ const scope = options.hostScope;
799
+ let generation = 0;
800
+ let disposed = false;
801
+ let hydrated = false;
802
+ let pending = Promise.resolve();
803
+ let lastAppliedResultId;
804
+ const appliedResultIds = /* @__PURE__ */ new Set();
805
+ function enqueue(operation) {
806
+ const next = pending.then(operation);
807
+ pending = next.then(
808
+ () => {
809
+ },
810
+ () => {
811
+ }
812
+ );
813
+ return next;
814
+ }
815
+ const rejected = (reason) => ({ status: "rejected", reason });
816
+ return {
817
+ store,
818
+ resolveTileWidget,
819
+ whenIdle: () => pending,
820
+ configureTemplates(next) {
821
+ if (!disposed) templates.refresh(next);
822
+ },
823
+ acceptHostFrame(payload) {
824
+ return enqueue(async () => {
825
+ if (disposed) return rejected("disposed");
826
+ if (!frame(payload)) return rejected("invalid");
827
+ if (!scope || payload.session_ref !== scope.sessionRef) return rejected("wrong_scope");
828
+ if (payload.result_id === lastAppliedResultId || appliedResultIds.has(payload.result_id))
829
+ return { status: "duplicate", resultId: payload.result_id };
830
+ const report = await store.reconcileSnapshot(payload.mounted_elements);
831
+ if (disposed) return rejected("disposed");
832
+ if (!report.ok) return rejected("projection_failed");
833
+ lastAppliedResultId = payload.result_id;
834
+ appliedResultIds.add(payload.result_id);
835
+ if (appliedResultIds.size > APPLIED_HISTORY) {
836
+ appliedResultIds.delete(appliedResultIds.values().next().value);
837
+ }
838
+ store.scheduleClientTtl(payload.session_expires_at);
839
+ return { status: "applied", resultId: payload.result_id };
840
+ });
841
+ },
842
+ applyLegacyEnvelope(envelope) {
843
+ return enqueue(async () => {
844
+ if (!disposed) await store.apply(envelope.mutations);
845
+ });
846
+ },
847
+ hydrateLegacy(instances, expiresAt) {
848
+ return enqueue(async () => {
849
+ if (disposed) return;
850
+ await store.hydrate(instances);
851
+ if (!disposed) store.scheduleClientTtl(expiresAt);
852
+ });
853
+ },
854
+ hydrateOnce(load) {
855
+ if (hydrated || disposed) return;
856
+ hydrated = true;
857
+ const started = generation;
858
+ void load().then((response) => {
859
+ if (disposed || started !== generation || !response) return;
860
+ return enqueue(async () => {
861
+ if (disposed || started !== generation) return;
862
+ await store.hydrate(response.mounted_elements);
863
+ if (!disposed) store.scheduleClientTtl(response.session_expires_at);
864
+ });
865
+ }).catch(() => {
866
+ });
867
+ },
868
+ listen(target, name, listener) {
869
+ if (disposed) return () => {
870
+ };
871
+ target.addEventListener(name, listener);
872
+ const remove = () => {
873
+ target.removeEventListener(name, listener);
874
+ cleanups.delete(remove);
875
+ };
876
+ cleanups.add(remove);
877
+ return remove;
878
+ },
879
+ dispose() {
880
+ if (!disposed) {
881
+ disposed = true;
882
+ generation++;
883
+ for (const cleanup of cleanups) cleanup();
884
+ store.dispose(true);
885
+ templates.refresh(void 0);
886
+ }
887
+ return pending;
888
+ }
889
+ };
890
+ }
891
+
892
+ export {
893
+ ElementInstanceStore,
894
+ TileHandler,
895
+ createTemplateWidgetResolver,
896
+ createElementSessionController
897
+ };
898
+ //# sourceMappingURL=chunk-VIRIVP7H.js.map