@syntrologie/adapt-chatbot 2.55.1 → 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.
- package/dist/AdaptiveChatBar.d.ts +18 -0
- package/dist/AdaptiveChatBar.d.ts.map +1 -1
- package/dist/AdaptiveChatBar.interleave.test.d.ts +9 -0
- package/dist/AdaptiveChatBar.interleave.test.d.ts.map +1 -0
- package/dist/AdaptiveChatBar.js +4 -4
- package/dist/AdaptiveChatBarMountable.d.ts +28 -17
- package/dist/AdaptiveChatBarMountable.d.ts.map +1 -1
- package/dist/AdaptiveChatTrail.js +3 -3
- package/dist/AdaptiveChipsStrip.d.ts +20 -20
- package/dist/AdaptiveChipsStrip.js +3 -3
- package/dist/ChatAssistantLit.d.ts.map +1 -1
- package/dist/ChatAssistantLit.js +5 -4
- package/dist/ChatTransport.d.ts +6 -1
- package/dist/ChatTransport.d.ts.map +1 -1
- package/dist/cdn.d.ts +1 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/chunk-23E6WZOO.js +882 -0
- package/dist/chunk-23E6WZOO.js.map +7 -0
- package/dist/{chunk-EXDL3OKE.js → chunk-7QXC475Y.js} +314 -908
- package/dist/{chunk-EXDL3OKE.js.map → chunk-7QXC475Y.js.map} +4 -4
- package/dist/{chunk-TZA7572J.js → chunk-GN7Z5IVP.js} +2 -2
- package/dist/chunk-LY5AGXVD.js +21 -0
- package/dist/{chunk-ETDHHQS3.js → chunk-NMYFYMJI.js} +12 -12
- package/dist/{chunk-ETDHHQS3.js.map → chunk-NMYFYMJI.js.map} +1 -1
- package/dist/{chunk-LJEPZI6Y.js → chunk-WQIDXGGS.js} +2 -2
- package/dist/{chunk-VFWA5W56.js → chunk-XPUHW2YO.js} +44 -7
- package/dist/chunk-XPUHW2YO.js.map +7 -0
- package/dist/chunk-Z3YR4JFE.js +4702 -0
- package/dist/chunk-Z3YR4JFE.js.map +7 -0
- package/dist/editor-lit.js +1 -1
- package/dist/element-session.d.ts +3 -0
- package/dist/element-session.d.ts.map +1 -0
- package/dist/element-session.js +9 -0
- package/dist/element-session.js.map +7 -0
- package/dist/elements/ElementInstanceStore.d.ts +15 -1
- package/dist/elements/ElementInstanceStore.d.ts.map +1 -1
- package/dist/elements/ElementSessionController.d.ts +37 -0
- package/dist/elements/ElementSessionController.d.ts.map +1 -0
- package/dist/elements/ElementSessionController.test.d.ts +2 -0
- package/dist/elements/ElementSessionController.test.d.ts.map +1 -0
- package/dist/elements/TileHandler.d.ts.map +1 -1
- package/dist/elements/createElementStore.d.ts +11 -0
- package/dist/elements/createElementStore.d.ts.map +1 -0
- package/dist/elements/tileLifecycleProps.d.ts +13 -0
- package/dist/elements/tileLifecycleProps.d.ts.map +1 -0
- package/dist/elements/types.d.ts +7 -0
- package/dist/elements/types.d.ts.map +1 -1
- package/dist/hostAgentController.d.ts +65 -0
- package/dist/hostAgentController.d.ts.map +1 -0
- package/dist/hostAgentController.test.d.ts +2 -0
- package/dist/hostAgentController.test.d.ts.map +1 -0
- package/dist/observer/wire.d.ts +54 -0
- package/dist/observer/wire.d.ts.map +1 -0
- package/dist/observer/wire.test.d.ts +2 -0
- package/dist/observer/wire.test.d.ts.map +1 -0
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +824 -163
- package/dist/runtime.js.map +4 -4
- package/dist/schema.d.ts +66 -66
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +75 -76
- package/dist/schema.js.map +3 -3
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -7
- package/dist/chunk-NGV35N4T.js +0 -474
- package/dist/chunk-NGV35N4T.js.map +0 -7
- package/dist/chunk-UVKRO5ER.js +0 -13
- package/dist/chunk-VFWA5W56.js.map +0 -7
- /package/dist/{chunk-TZA7572J.js.map → chunk-GN7Z5IVP.js.map} +0 -0
- /package/dist/{chunk-UVKRO5ER.js.map → chunk-LY5AGXVD.js.map} +0 -0
- /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
|