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