@guidelytech/sdk-core 0.11.0 → 0.13.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.
@@ -0,0 +1,378 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/health.ts
6
+ var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
7
+ var OFFLINE_WAIT_MS = 3e4;
8
+ var ServerHealth = class {
9
+ constructor(opts = {}) {
10
+ __publicField(this, "failures", 0);
11
+ __publicField(this, "openUntil", 0);
12
+ __publicField(this, "lastOkAt", 0);
13
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
14
+ __publicField(this, "threshold");
15
+ __publicField(this, "baseMs");
16
+ __publicField(this, "maxMs");
17
+ __publicField(this, "now");
18
+ __publicField(this, "random");
19
+ this.threshold = opts.threshold ?? 3;
20
+ this.baseMs = opts.baseMs ?? 5e3;
21
+ this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
22
+ this.now = opts.now ?? Date.now;
23
+ this.random = opts.random ?? Math.random;
24
+ }
25
+ /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
26
+ success() {
27
+ const wasDown = this.failures > 0 || this.openUntil > 0;
28
+ this.failures = 0;
29
+ this.openUntil = 0;
30
+ this.lastOkAt = this.now();
31
+ if (wasDown) this.notify();
32
+ }
33
+ /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
34
+ failure(retryAfterMs) {
35
+ this.failures += 1;
36
+ const now = this.now();
37
+ if (retryAfterMs != null && retryAfterMs > 0) {
38
+ this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
39
+ return;
40
+ }
41
+ if (this.failures < this.threshold) return;
42
+ const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
43
+ this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
44
+ }
45
+ /** Milliseconds background traffic should wait before trying again (0: send now). */
46
+ waitMs() {
47
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
48
+ return Math.max(0, this.openUntil - this.now());
49
+ }
50
+ canSend() {
51
+ return this.waitMs() === 0;
52
+ }
53
+ /**
54
+ * The last request got an answer. Unload beacons can't report back, so what they carry is only
55
+ * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
56
+ */
57
+ get reachable() {
58
+ return this.failures === 0 && this.lastOkAt > 0;
59
+ }
60
+ /** The browser came back online: end the cooldown and let queued traffic try straight away. */
61
+ online() {
62
+ this.openUntil = 0;
63
+ this.notify();
64
+ }
65
+ /** Called when the server answers again after failures, or the browser comes back online. */
66
+ onRecover(cb) {
67
+ this.listeners.add(cb);
68
+ return () => this.listeners.delete(cb);
69
+ }
70
+ notify() {
71
+ for (const cb of Array.from(this.listeners)) {
72
+ try {
73
+ cb();
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ };
79
+ function parseRetryAfter(value, now = Date.now()) {
80
+ if (!value) return null;
81
+ const trimmed = value.trim();
82
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
83
+ const at = Date.parse(trimmed);
84
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
85
+ }
86
+ function isServerFailure(status) {
87
+ return status === 429 || status >= 500;
88
+ }
89
+
90
+ // src/http.ts
91
+ var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
92
+ var STREAM_IDLE_TIMEOUT_MS = 6e4;
93
+ var Api = class {
94
+ constructor(baseUrl, key, fetchImpl, health) {
95
+ __publicField(this, "baseUrl", baseUrl);
96
+ __publicField(this, "key", key);
97
+ __publicField(this, "fetchImpl", fetchImpl);
98
+ /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
99
+ __publicField(this, "health");
100
+ this.health = health ?? new ServerHealth();
101
+ }
102
+ /** Feeds an answer (or a failed request, `null`) into {@link health}. */
103
+ observe(res) {
104
+ if (!res) this.health.failure();
105
+ else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
106
+ else this.health.success();
107
+ }
108
+ url(path, params = {}) {
109
+ const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
110
+ return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
111
+ }
112
+ get fetcher() {
113
+ if (this.fetchImpl) return this.fetchImpl;
114
+ return typeof fetch === "function" ? fetch.bind(globalThis) : null;
115
+ }
116
+ async request(method, path, options = {}) {
117
+ const f = this.fetcher;
118
+ if (!f) throw new Error("fetch is not available");
119
+ const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
120
+ const form = typeof FormData !== "undefined" && options.body instanceof FormData;
121
+ if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
122
+ if (options.etag) headers["If-None-Match"] = options.etag;
123
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
124
+ const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
125
+ try {
126
+ let res;
127
+ try {
128
+ res = await f(this.url(path, options.params), {
129
+ method,
130
+ headers,
131
+ body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
132
+ keepalive: options.keepalive,
133
+ signal: ac?.signal,
134
+ credentials: "omit",
135
+ mode: "cors"
136
+ });
137
+ } catch (err) {
138
+ this.observe(null);
139
+ throw err;
140
+ }
141
+ this.observe(res);
142
+ let data = null;
143
+ if (res.status !== 304 && res.status !== 204) {
144
+ try {
145
+ data = await res.json();
146
+ } catch {
147
+ data = null;
148
+ }
149
+ }
150
+ return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
151
+ } finally {
152
+ if (timer) clearTimeout(timer);
153
+ }
154
+ }
155
+ get(path, options) {
156
+ return this.request("GET", path, options);
157
+ }
158
+ post(path, body, options) {
159
+ return this.request("POST", path, { body, ...options });
160
+ }
161
+ /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
162
+ postForm(path, form, options) {
163
+ return this.request("POST", path, { body: form, ...options });
164
+ }
165
+ /**
166
+ * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
167
+ * returned instead. Scout replies can take a while, so there is no overall timeout: the response
168
+ * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
169
+ * otherwise the request is aborted and rejects.
170
+ */
171
+ async stream(path, body, onEvent, headers = {}) {
172
+ const f = this.fetcher;
173
+ if (!f) throw new Error("fetch is not available");
174
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
175
+ let timer = null;
176
+ const arm = (ms) => {
177
+ if (!ac) return;
178
+ if (timer) clearTimeout(timer);
179
+ timer = setTimeout(() => ac.abort(), ms);
180
+ };
181
+ arm(STREAM_RESPONSE_TIMEOUT_MS);
182
+ try {
183
+ let res;
184
+ try {
185
+ res = await f(this.url(path), {
186
+ method: "POST",
187
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
188
+ body: JSON.stringify(body),
189
+ signal: ac?.signal,
190
+ credentials: "omit",
191
+ mode: "cors"
192
+ });
193
+ } catch (err) {
194
+ this.observe(null);
195
+ throw err;
196
+ }
197
+ this.observe(res);
198
+ const type = res.headers?.get?.("content-type") ?? "";
199
+ if (!type.includes("text/event-stream") || !res.body) {
200
+ let data = null;
201
+ try {
202
+ data = await res.json();
203
+ } catch {
204
+ data = null;
205
+ }
206
+ return { status: res.status, ok: res.ok, data, etag: null };
207
+ }
208
+ arm(STREAM_IDLE_TIMEOUT_MS);
209
+ await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
210
+ return { status: res.status, ok: res.ok, data: null, etag: null };
211
+ } finally {
212
+ if (timer) clearTimeout(timer);
213
+ }
214
+ }
215
+ /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
216
+ beacon(path, body) {
217
+ try {
218
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
219
+ const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
220
+ return navigator.sendBeacon(this.url(path), blob);
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ };
226
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
227
+ function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
228
+ return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
229
+ }
230
+ async function withRetry(send, attempts = 3, baseMs = 500) {
231
+ for (let i = 0; i < attempts; i++) {
232
+ try {
233
+ const res = await send();
234
+ if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
235
+ } catch {
236
+ }
237
+ if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
238
+ }
239
+ return null;
240
+ }
241
+ function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
242
+ return withRetry(() => api.post(path, body), attempts, baseMs);
243
+ }
244
+ async function readEventStream(body, onEvent, onChunk) {
245
+ const reader = body.getReader();
246
+ const decoder = new TextDecoder();
247
+ let buffer = "";
248
+ const dispatch = (raw) => {
249
+ let event = "message";
250
+ const data = [];
251
+ for (const line of raw.split("\n")) {
252
+ if (!line || line.startsWith(":")) continue;
253
+ const i = line.indexOf(":");
254
+ const field = i < 0 ? line : line.slice(0, i);
255
+ const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
256
+ if (field === "event") event = value;
257
+ else if (field === "data") data.push(value);
258
+ }
259
+ if (!data.length) return;
260
+ try {
261
+ onEvent(event, JSON.parse(data.join("\n")));
262
+ } catch {
263
+ }
264
+ };
265
+ for (; ; ) {
266
+ const { done, value } = await reader.read();
267
+ if (value) onChunk?.();
268
+ if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
269
+ let sep;
270
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
271
+ dispatch(buffer.slice(0, sep));
272
+ buffer = buffer.slice(sep + 2);
273
+ }
274
+ if (done) break;
275
+ }
276
+ if (buffer.trim()) dispatch(buffer);
277
+ }
278
+
279
+ // src/storage.ts
280
+ function memoryStore() {
281
+ const map = /* @__PURE__ */ new Map();
282
+ return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
283
+ }
284
+ function webStore(area) {
285
+ try {
286
+ if (typeof window === "undefined") return null;
287
+ const s = window[area];
288
+ if (!s) return null;
289
+ const probe = "__guidely_probe__";
290
+ s.setItem(probe, "1");
291
+ s.removeItem(probe);
292
+ return {
293
+ get: (k) => {
294
+ try {
295
+ return s.getItem(k);
296
+ } catch {
297
+ return null;
298
+ }
299
+ },
300
+ set: (k, v) => {
301
+ try {
302
+ s.setItem(k, v);
303
+ } catch {
304
+ }
305
+ },
306
+ remove: (k) => {
307
+ try {
308
+ s.removeItem(k);
309
+ } catch {
310
+ }
311
+ }
312
+ };
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+ function createStorage(kind = "local") {
318
+ if (kind === "memory") return memoryStore();
319
+ return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
320
+ }
321
+ function readJson(store, key, fallback) {
322
+ const raw = store.get(key);
323
+ if (raw == null) return fallback;
324
+ try {
325
+ return JSON.parse(raw);
326
+ } catch {
327
+ return fallback;
328
+ }
329
+ }
330
+ function writeJson(store, key, value) {
331
+ try {
332
+ store.set(key, JSON.stringify(value));
333
+ } catch {
334
+ }
335
+ }
336
+ var KEYS = {
337
+ anon: "guidely:anon",
338
+ seen: "guidely:seen",
339
+ config: (key) => `guidely:config:${key}`,
340
+ etag: (key) => `guidely:etag:${key}`,
341
+ state: (key) => `guidely:state:${key}`,
342
+ user: (key) => `guidely:user:${key}`,
343
+ queue: (key) => `guidely:queue:${key}`,
344
+ /** Survey answers and guide/checklist progress not yet accepted by the server. */
345
+ outbox: (key) => `guidely:outbox:${key}`,
346
+ /** Agent Analytics turns not yet accepted by the server. */
347
+ agentOutbox: (key) => `guidely:agents:${key}`,
348
+ session: "guidely:session",
349
+ active: "guidely:active",
350
+ authoring: "guidely:authoring",
351
+ /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
352
+ capture: "guidely:capture",
353
+ capturePending: "guidely:capture:pending",
354
+ /** Messenger conversations this browser can read: [{ id, token }]. */
355
+ conversations: (key) => `guidely:conversations:${key}`,
356
+ /** Announcement ids opened in the messenger's News tab. */
357
+ newsSeen: (key) => `guidely:news:${key}`,
358
+ /** Where the visitor dragged the messenger launcher ({ side, y }). */
359
+ launcher: (key) => `guidely:launcher:${key}`
360
+ };
361
+
362
+ export {
363
+ __publicField,
364
+ ServerHealth,
365
+ parseRetryAfter,
366
+ isServerFailure,
367
+ Api,
368
+ sleep,
369
+ backoffMs,
370
+ withRetry,
371
+ postWithRetry,
372
+ memoryStore,
373
+ createStorage,
374
+ readJson,
375
+ writeJson,
376
+ KEYS
377
+ };
378
+ //# sourceMappingURL=chunk-XML74K2P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/health.ts","../src/http.ts","../src/storage.ts"],"sourcesContent":["/**\n * Whether the Guidely API is reachable right now: a circuit breaker shared by everything the SDK\n * sends in the background (event batches, replay uploads, config refreshes, queued survey answers).\n *\n * Every API answer is reported here. After `threshold` failures in a row (network errors, timeouts,\n * 429, 5xx) the circuit opens for a cooldown that doubles with each further failure (5 s up to\n * 5 min, ±20% jitter so tabs don't retry in step); a `Retry-After` answer sets the cooldown\n * directly. While it is open, background senders wait `waitMs()` instead of sending. Requests the\n * person makes (a messenger message) still go out, and any answer closes the circuit again. The\n * browser's `online` event ends the cooldown early.\n */\nexport interface ServerHealthOptions {\n threshold?: number;\n baseMs?: number;\n maxMs?: number;\n now?: () => number;\n random?: () => number;\n}\n\n/** Longest `Retry-After` honoured, so a bad header can't silence the SDK for days. */\nconst MAX_RETRY_AFTER_MS = 60 * 60 * 1000;\n/** How long background senders wait while the browser reports being offline (the `online` event usually comes first). */\nconst OFFLINE_WAIT_MS = 30_000;\n\nexport class ServerHealth {\n private failures = 0;\n private openUntil = 0;\n private lastOkAt = 0;\n private readonly listeners = new Set<() => void>();\n private readonly threshold: number;\n private readonly baseMs: number;\n private readonly maxMs: number;\n private readonly now: () => number;\n private readonly random: () => number;\n\n constructor(opts: ServerHealthOptions = {}) {\n this.threshold = opts.threshold ?? 3;\n this.baseMs = opts.baseMs ?? 5000;\n this.maxMs = opts.maxMs ?? 5 * 60 * 1000;\n this.now = opts.now ?? Date.now;\n this.random = opts.random ?? Math.random;\n }\n\n /** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */\n success(): void {\n const wasDown = this.failures > 0 || this.openUntil > 0;\n this.failures = 0;\n this.openUntil = 0;\n this.lastOkAt = this.now();\n if (wasDown) this.notify();\n }\n\n /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */\n failure(retryAfterMs?: number | null): void {\n this.failures += 1;\n const now = this.now();\n if (retryAfterMs != null && retryAfterMs > 0) {\n this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));\n return;\n }\n if (this.failures < this.threshold) return;\n const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));\n this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));\n }\n\n /** Milliseconds background traffic should wait before trying again (0: send now). */\n waitMs(): number {\n if (typeof navigator !== \"undefined\" && navigator.onLine === false) return OFFLINE_WAIT_MS;\n return Math.max(0, this.openUntil - this.now());\n }\n\n canSend(): boolean {\n return this.waitMs() === 0;\n }\n\n /**\n * The last request got an answer. Unload beacons can't report back, so what they carry is only\n * forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).\n */\n get reachable(): boolean {\n return this.failures === 0 && this.lastOkAt > 0;\n }\n\n /** The browser came back online: end the cooldown and let queued traffic try straight away. */\n online(): void {\n this.openUntil = 0;\n this.notify();\n }\n\n /** Called when the server answers again after failures, or the browser comes back online. */\n onRecover(cb: () => void): () => void {\n this.listeners.add(cb);\n return () => this.listeners.delete(cb);\n }\n\n private notify(): void {\n for (const cb of Array.from(this.listeners)) {\n try {\n cb();\n } catch {\n /* a listener must not break the others */\n }\n }\n }\n}\n\n/** `Retry-After` as milliseconds: delta-seconds or an HTTP date. Null when absent or unparseable. */\nexport function parseRetryAfter(value: string | null | undefined, now = Date.now()): number | null {\n if (!value) return null;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed) * 1000;\n const at = Date.parse(trimmed);\n return Number.isNaN(at) ? null : Math.max(0, at - now);\n}\n\n/** Answers that mean the server is down or shedding load (retry later) rather than refusing the request. */\nexport function isServerFailure(status: number): boolean {\n return status === 429 || status >= 500;\n}\n","import { isServerFailure, parseRetryAfter, ServerHealth } from \"./health\";\n\nexport interface ApiResponse<T> {\n status: number;\n ok: boolean;\n data: T | null;\n etag: string | null;\n}\n\nexport type FetchLike = typeof fetch;\n\n/** Reply streams: the response must start within this long, and then never go quiet longer than the idle limit (the server sends a keepalive every 15 s). */\nexport const STREAM_RESPONSE_TIMEOUT_MS = 30_000;\nexport const STREAM_IDLE_TIMEOUT_MS = 60_000;\n\n/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n /** Reachability of the API, fed by every answer (see {@link ServerHealth}). */\n readonly health: ServerHealth;\n\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\n health?: ServerHealth,\n ) {\n this.health = health ?? new ServerHealth();\n }\n\n /** Feeds an answer (or a failed request, `null`) into {@link health}. */\n private observe(res: Response | null): void {\n if (!res) this.health.failure();\n else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.(\"retry-after\")));\n else this.health.success();\n }\n\n url(path: string, params: Record<string, string | undefined> = {}): string {\n // Built by hand rather than with URL/searchParams, which React Native does not implement.\n const query = Object.entries({ key: this.key, ...params })\n .filter((e): e is [string, string] => e[1] != null)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n return `${this.baseUrl.replace(/\\/$/, \"\")}/api/v1/sdk${path}?${query}`;\n }\n\n private get fetcher(): FetchLike | null {\n if (this.fetchImpl) return this.fetchImpl;\n return typeof fetch === \"function\" ? fetch.bind(globalThis) : null;\n }\n\n async request<T>(method: \"GET\" | \"POST\", path: string, options: { body?: unknown; etag?: string | null; params?: Record<string, string | undefined>; keepalive?: boolean; timeoutMs?: number; headers?: Record<string, string> } = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const headers: Record<string, string> = { ...options.headers, \"X-Guidely-Key\": this.key, Accept: \"application/json\" };\n // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n let res: Response;\n try {\n res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. Scout replies can take a while, so there is no overall timeout: the response\n * must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),\n * otherwise the request is aborted and rejects.\n */\n async stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers: Record<string, string> = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n const arm = (ms: number) => {\n if (!ac) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => ac.abort(), ms);\n };\n arm(STREAM_RESPONSE_TIMEOUT_MS);\n try {\n let res: Response;\n try {\n res = await f(this.url(path), {\n method: \"POST\",\n headers: { ...headers, \"X-Guidely-Key\": this.key, Accept: \"text/event-stream, application/json\", \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n } catch (err) {\n this.observe(null);\n throw err;\n }\n this.observe(res);\n const type = res.headers?.get?.(\"content-type\") ?? \"\";\n if (!type.includes(\"text/event-stream\") || !res.body) {\n let data: T | null = null;\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n return { status: res.status, ok: res.ok, data, etag: null };\n }\n arm(STREAM_IDLE_TIMEOUT_MS);\n await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));\n return { status: res.status, ok: res.ok, data: null, etag: null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */\n beacon(path: string, body: unknown): boolean {\n try {\n if (typeof navigator === \"undefined\" || typeof navigator.sendBeacon !== \"function\") return false;\n // text/plain is a CORS-safelisted type: cross-origin beacons need no preflight (which\n // browsers drop during unload). The API parses the body regardless of content type.\n const blob = new Blob([JSON.stringify(body)], { type: \"text/plain;charset=UTF-8\" });\n return navigator.sendBeacon(this.url(path), blob);\n } catch {\n return false;\n }\n }\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Exponential backoff helper: 1s → 2s → 4s … capped. */\nexport function backoffMs(attempt: number, baseMs = 1000, maxMs = 30000): number {\n return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));\n}\n\n/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). `onChunk` runs whenever bytes arrive. */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => void, onChunk?: () => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const dispatch = (raw: string) => {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (!data.length) return;\n try {\n onEvent(event, JSON.parse(data.join(\"\\n\")));\n } catch {\n /* ignore malformed events */\n }\n };\n for (;;) {\n const { done, value } = await reader.read();\n if (value) onChunk?.();\n if (value) buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n?/g, \"\\n\");\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n dispatch(buffer.slice(0, sep));\n buffer = buffer.slice(sep + 2);\n }\n if (done) break;\n }\n if (buffer.trim()) dispatch(buffer);\n}\n","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n /** Survey answers and guide/checklist progress not yet accepted by the server. */\n outbox: (key: string) => `guidely:outbox:${key}`,\n /** Agent Analytics turns not yet accepted by the server. */\n agentOutbox: (key: string) => `guidely:agents:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n /** Where the visitor dragged the messenger launcher ({ side, y }). */\n launcher: (key: string) => `guidely:launcher:${key}`,\n} as const;\n"],"mappings":";;;;;AAoBA,IAAM,qBAAqB,KAAK,KAAK;AAErC,IAAM,kBAAkB;AAEjB,IAAM,eAAN,MAAmB;AAAA,EAWxB,YAAY,OAA4B,CAAC,GAAG;AAV5C,wBAAQ,YAAW;AACnB,wBAAQ,aAAY;AACpB,wBAAQ,YAAW;AACnB,wBAAiB,aAAY,oBAAI,IAAgB;AACjD,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AAGf,SAAK,YAAY,KAAK,aAAa;AACnC,SAAK,SAAS,KAAK,UAAU;AAC7B,SAAK,QAAQ,KAAK,SAAS,IAAI,KAAK;AACpC,SAAK,MAAM,KAAK,OAAO,KAAK;AAC5B,SAAK,SAAS,KAAK,UAAU,KAAK;AAAA,EACpC;AAAA;AAAA,EAGA,UAAgB;AACd,UAAM,UAAU,KAAK,WAAW,KAAK,KAAK,YAAY;AACtD,SAAK,WAAW;AAChB,SAAK,YAAY;AACjB,SAAK,WAAW,KAAK,IAAI;AACzB,QAAI,QAAS,MAAK,OAAO;AAAA,EAC3B;AAAA;AAAA,EAGA,QAAQ,cAAoC;AAC1C,SAAK,YAAY;AACjB,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,gBAAgB,QAAQ,eAAe,GAAG;AAC5C,WAAK,YAAY,KAAK,IAAI,KAAK,WAAW,MAAM,KAAK,IAAI,cAAc,kBAAkB,CAAC;AAC1F;AAAA,IACF;AACA,QAAI,KAAK,WAAW,KAAK,UAAW;AACpC,UAAM,WAAW,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,MAAM,KAAK,WAAW,KAAK,UAAU;AACzF,SAAK,YAAY,KAAK,IAAI,KAAK,WAAW,MAAM,YAAY,MAAM,KAAK,OAAO,IAAI,IAAI;AAAA,EACxF;AAAA;AAAA,EAGA,SAAiB;AACf,QAAI,OAAO,cAAc,eAAe,UAAU,WAAW,MAAO,QAAO;AAC3E,WAAO,KAAK,IAAI,GAAG,KAAK,YAAY,KAAK,IAAI,CAAC;AAAA,EAChD;AAAA,EAEA,UAAmB;AACjB,WAAO,KAAK,OAAO,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,YAAqB;AACvB,WAAO,KAAK,aAAa,KAAK,KAAK,WAAW;AAAA,EAChD;AAAA;AAAA,EAGA,SAAe;AACb,SAAK,YAAY;AACjB,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,UAAU,IAA4B;AACpC,SAAK,UAAU,IAAI,EAAE;AACrB,WAAO,MAAM,KAAK,UAAU,OAAO,EAAE;AAAA,EACvC;AAAA,EAEQ,SAAe;AACrB,eAAW,MAAM,MAAM,KAAK,KAAK,SAAS,GAAG;AAC3C,UAAI;AACF,WAAG;AAAA,MACL,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,gBAAgB,OAAkC,MAAM,KAAK,IAAI,GAAkB;AACjG,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,QAAQ,KAAK,OAAO,EAAG,QAAO,OAAO,OAAO,IAAI;AACpD,QAAM,KAAK,KAAK,MAAM,OAAO;AAC7B,SAAO,OAAO,MAAM,EAAE,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,GAAG;AACvD;AAGO,SAAS,gBAAgB,QAAyB;AACvD,SAAO,WAAW,OAAO,UAAU;AACrC;;;AC1GO,IAAM,6BAA6B;AACnC,IAAM,yBAAyB;AAG/B,IAAM,MAAN,MAAU;AAAA,EAIf,YACmB,SACA,KACA,WACjB,QACA;AAJiB;AACA;AACA;AALnB;AAAA,wBAAS;AAQP,SAAK,SAAS,UAAU,IAAI,aAAa;AAAA,EAC3C;AAAA;AAAA,EAGQ,QAAQ,KAA4B;AAC1C,QAAI,CAAC,IAAK,MAAK,OAAO,QAAQ;AAAA,aACrB,gBAAgB,IAAI,MAAM,EAAG,MAAK,OAAO,QAAQ,gBAAgB,IAAI,SAAS,MAAM,aAAa,CAAC,CAAC;AAAA,QACvG,MAAK,OAAO,QAAQ;AAAA,EAC3B;AAAA,EAEA,IAAI,MAAc,SAA6C,CAAC,GAAW;AAEzE,UAAM,QAAQ,OAAO,QAAQ,EAAE,KAAK,KAAK,KAAK,GAAG,OAAO,CAAC,EACtD,OAAO,CAAC,MAA6B,EAAE,CAAC,KAAK,IAAI,EACjD,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC,IAAI,mBAAmB,CAAC,CAAC,EAAE,EACnE,KAAK,GAAG;AACX,WAAO,GAAG,KAAK,QAAQ,QAAQ,OAAO,EAAE,CAAC,cAAc,IAAI,IAAI,KAAK;AAAA,EACtE;AAAA,EAEA,IAAY,UAA4B;AACtC,QAAI,KAAK,UAAW,QAAO,KAAK;AAChC,WAAO,OAAO,UAAU,aAAa,MAAM,KAAK,UAAU,IAAI;AAAA,EAChE;AAAA,EAEA,MAAM,QAAW,QAAwB,MAAc,UAA4K,CAAC,GAA4B;AAC9P,UAAM,IAAI,KAAK;AACf,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,wBAAwB;AAChD,UAAM,UAAkC,EAAE,GAAG,QAAQ,SAAS,iBAAiB,KAAK,KAAK,QAAQ,mBAAmB;AAEpH,UAAM,OAAO,OAAO,aAAa,eAAe,QAAQ,gBAAgB;AACxE,QAAI,QAAQ,SAAS,UAAa,CAAC,KAAM,SAAQ,cAAc,IAAI;AACnE,QAAI,QAAQ,KAAM,SAAQ,eAAe,IAAI,QAAQ;AACrD,UAAM,KAAK,OAAO,oBAAoB,cAAc,IAAI,gBAAgB,IAAI;AAC5E,UAAM,QAAQ,KAAK,WAAW,MAAM,GAAG,MAAM,GAAG,QAAQ,aAAa,GAAK,IAAI;AAC9E,QAAI;AACF,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,EAAE,KAAK,IAAI,MAAM,QAAQ,MAAM,GAAG;AAAA,UAC5C;AAAA,UACA;AAAA,UACA,MAAM,QAAQ,SAAS,SAAY,SAAY,OAAQ,QAAQ,OAAoB,KAAK,UAAU,QAAQ,IAAI;AAAA,UAC9G,WAAW,QAAQ;AAAA,UACnB,QAAQ,IAAI;AAAA,UACZ,aAAa;AAAA,UACb,MAAM;AAAA,QACR,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,aAAK,QAAQ,IAAI;AACjB,cAAM;AAAA,MACR;AACA,WAAK,QAAQ,GAAG;AAChB,UAAI,OAAiB;AACrB,UAAI,IAAI,WAAW,OAAO,IAAI,WAAW,KAAK;AAC5C,YAAI;AACF,iBAAQ,MAAM,IAAI,KAAK;AAAA,QACzB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,EAAE,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,MAAM,MAAM,IAAI,SAAS,MAAM,MAAM,KAAK,KAAK;AAAA,IAC1F,UAAE;AACA,UAAI,MAAO,cAAa,KAAK;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,IAAO,MAAc,SAA4I;AAC/J,WAAO,KAAK,QAAW,OAAO,MAAM,OAAO;AAAA,EAC7C;AAAA,EAEA,KAAQ,MAAc,MAAe,SAA2I;AAC9K,WAAO,KAAK,QAAW,QAAQ,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;AAAA,EAC3D;AAAA;AAAA,EAGA,SAAY,MAAc,MAAgB,SAAwG;AAChJ,WAAO,KAAK,QAAW,QAAQ,MAAM,EAAE,MAAM,MAAM,GAAG,QAAQ,CAAC;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAU,MAAc,MAAe,SAAiD,UAAkC,CAAC,GAA4B;AAC3J,UAAM,IAAI,KAAK;AACf,QAAI,CAAC,EAAG,OAAM,IAAI,MAAM,wBAAwB;AAChD,UAAM,KAAK,OAAO,oBAAoB,cAAc,IAAI,gBAAgB,IAAI;AAC5E,QAAI,QAA8C;AAClD,UAAM,MAAM,CAAC,OAAe;AAC1B,UAAI,CAAC,GAAI;AACT,UAAI,MAAO,cAAa,KAAK;AAC7B,cAAQ,WAAW,MAAM,GAAG,MAAM,GAAG,EAAE;AAAA,IACzC;AACA,QAAI,0BAA0B;AAC9B,QAAI;AACF,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,EAAE,KAAK,IAAI,IAAI,GAAG;AAAA,UAC5B,QAAQ;AAAA,UACR,SAAS,EAAE,GAAG,SAAS,iBAAiB,KAAK,KAAK,QAAQ,uCAAuC,gBAAgB,mBAAmB;AAAA,UACpI,MAAM,KAAK,UAAU,IAAI;AAAA,UACzB,QAAQ,IAAI;AAAA,UACZ,aAAa;AAAA,UACb,MAAM;AAAA,QACR,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,aAAK,QAAQ,IAAI;AACjB,cAAM;AAAA,MACR;AACA,WAAK,QAAQ,GAAG;AAChB,YAAM,OAAO,IAAI,SAAS,MAAM,cAAc,KAAK;AACnD,UAAI,CAAC,KAAK,SAAS,mBAAmB,KAAK,CAAC,IAAI,MAAM;AACpD,YAAI,OAAiB;AACrB,YAAI;AACF,iBAAQ,MAAM,IAAI,KAAK;AAAA,QACzB,QAAQ;AACN,iBAAO;AAAA,QACT;AACA,eAAO,EAAE,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,MAAM,MAAM,KAAK;AAAA,MAC5D;AACA,UAAI,sBAAsB;AAC1B,YAAM,gBAAgB,IAAI,MAAM,SAAS,MAAM,IAAI,sBAAsB,CAAC;AAC1E,aAAO,EAAE,QAAQ,IAAI,QAAQ,IAAI,IAAI,IAAI,MAAM,MAAM,MAAM,KAAK;AAAA,IAClE,UAAE;AACA,UAAI,MAAO,cAAa,KAAK;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAGA,OAAO,MAAc,MAAwB;AAC3C,QAAI;AACF,UAAI,OAAO,cAAc,eAAe,OAAO,UAAU,eAAe,WAAY,QAAO;AAG3F,YAAM,OAAO,IAAI,KAAK,CAAC,KAAK,UAAU,IAAI,CAAC,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAClF,aAAO,UAAU,WAAW,KAAK,IAAI,IAAI,GAAG,IAAI;AAAA,IAClD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEO,IAAM,QAAQ,CAAC,OAAe,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAGxE,SAAS,UAAU,SAAiB,SAAS,KAAM,QAAQ,KAAe;AAC/E,SAAO,KAAK,IAAI,OAAO,SAAS,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC;AAC3D;AAMA,eAAsB,UAAa,MAAqC,WAAW,GAAG,SAAS,KAAqC;AAClI,WAAS,IAAI,GAAG,IAAI,UAAU,KAAK;AACjC,QAAI;AACF,YAAM,MAAM,MAAM,KAAK;AACvB,UAAI,IAAI,MAAO,IAAI,UAAU,OAAO,IAAI,SAAS,OAAO,IAAI,WAAW,IAAM,QAAO;AAAA,IACtF,QAAQ;AAAA,IAER;AACA,QAAI,IAAI,WAAW,EAAG,OAAM,MAAM,UAAU,GAAG,QAAQ,GAAI,CAAC;AAAA,EAC9D;AACA,SAAO;AACT;AAGO,SAAS,cAAiB,KAAU,MAAc,MAAe,WAAW,GAAG,SAAS,KAAqC;AAClI,SAAO,UAAU,MAAM,IAAI,KAAQ,MAAM,IAAI,GAAG,UAAU,MAAM;AAClE;AAGA,eAAsB,gBAAgB,MAAkC,SAAiD,SAAqC;AAC5J,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AACb,QAAM,WAAW,CAAC,QAAgB;AAChC,QAAI,QAAQ;AACZ,UAAM,OAAiB,CAAC;AACxB,eAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,UAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,EAAG;AACnC,YAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,YAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,CAAC;AAC5C,YAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,MAAM,IAAI,CAAC,EAAE,QAAQ,MAAM,EAAE;AAC7D,UAAI,UAAU,QAAS,SAAQ;AAAA,eACtB,UAAU,OAAQ,MAAK,KAAK,KAAK;AAAA,IAC5C;AACA,QAAI,CAAC,KAAK,OAAQ;AAClB,QAAI;AACF,cAAQ,OAAO,KAAK,MAAM,KAAK,KAAK,IAAI,CAAC,CAAC;AAAA,IAC5C,QAAQ;AAAA,IAER;AAAA,EACF;AACA,aAAS;AACP,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,MAAO,WAAU;AACrB,QAAI,MAAO,WAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,EAAE,QAAQ,UAAU,IAAI;AACnF,QAAI;AACJ,YAAQ,MAAM,OAAO,QAAQ,MAAM,MAAM,GAAG;AAC1C,eAAS,OAAO,MAAM,GAAG,GAAG,CAAC;AAC7B,eAAS,OAAO,MAAM,MAAM,CAAC;AAAA,IAC/B;AACA,QAAI,KAAM;AAAA,EACZ;AACA,MAAI,OAAO,KAAK,EAAG,UAAS,MAAM;AACpC;;;AChOO,SAAS,cAA6B;AAC3C,QAAM,MAAM,oBAAI,IAAoB;AACpC,SAAO,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,MAAM,KAAK,CAAC,GAAG,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,KAAK,IAAI,OAAO,CAAC,EAAE;AAChH;AAEA,SAAS,SAAS,MAA+D;AAC/E,MAAI;AACF,QAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,UAAM,IAAI,OAAO,IAAI;AACrB,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,QAAQ;AACd,MAAE,QAAQ,OAAO,GAAG;AACpB,MAAE,WAAW,KAAK;AAClB,WAAO;AAAA,MACL,KAAK,CAAC,MAAM;AACV,YAAI;AACF,iBAAO,EAAE,QAAQ,CAAC;AAAA,QACpB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA,KAAK,CAAC,GAAG,MAAM;AACb,YAAI;AACF,YAAE,QAAQ,GAAG,CAAC;AAAA,QAChB,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,MACA,QAAQ,CAAC,MAAM;AACb,YAAI;AACF,YAAE,WAAW,CAAC;AAAA,QAChB,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,cAAc,OAAoB,SAAwB;AACxE,MAAI,SAAS,SAAU,QAAO,YAAY;AAC1C,SAAO,SAAS,SAAS,UAAU,iBAAiB,gBAAgB,KAAK,YAAY;AACvF;AAEO,SAAS,SAAY,OAAsB,KAAa,UAAgB;AAC7E,QAAM,MAAM,MAAM,IAAI,GAAG;AACzB,MAAI,OAAO,KAAM,QAAO;AACxB,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,UAAU,OAAsB,KAAa,OAAsB;AACjF,MAAI;AACF,UAAM,IAAI,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,EACtC,QAAQ;AAAA,EAER;AACF;AAGO,IAAM,OAAO;AAAA,EAClB,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ,CAAC,QAAgB,kBAAkB,GAAG;AAAA,EAC9C,MAAM,CAAC,QAAgB,gBAAgB,GAAG;AAAA,EAC1C,OAAO,CAAC,QAAgB,iBAAiB,GAAG;AAAA,EAC5C,MAAM,CAAC,QAAgB,gBAAgB,GAAG;AAAA,EAC1C,OAAO,CAAC,QAAgB,iBAAiB,GAAG;AAAA;AAAA,EAE5C,QAAQ,CAAC,QAAgB,kBAAkB,GAAG;AAAA;AAAA,EAE9C,aAAa,CAAC,QAAgB,kBAAkB,GAAG;AAAA,EACnD,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,WAAW;AAAA;AAAA,EAEX,SAAS;AAAA,EACT,gBAAgB;AAAA;AAAA,EAEhB,eAAe,CAAC,QAAgB,yBAAyB,GAAG;AAAA;AAAA,EAE5D,UAAU,CAAC,QAAgB,gBAAgB,GAAG;AAAA;AAAA,EAE9C,UAAU,CAAC,QAAgB,oBAAoB,GAAG;AACpD;","names":[]}
package/dist/headless.cjs CHANGED
@@ -362,7 +362,9 @@ var KEYS = {
362
362
  /** Messenger conversations this browser can read: [{ id, token }]. */
363
363
  conversations: (key) => `guidely:conversations:${key}`,
364
364
  /** Announcement ids opened in the messenger's News tab. */
365
- newsSeen: (key) => `guidely:news:${key}`
365
+ newsSeen: (key) => `guidely:news:${key}`,
366
+ /** Where the visitor dragged the messenger launcher ({ side, y }). */
367
+ launcher: (key) => `guidely:launcher:${key}`
366
368
  };
367
369
 
368
370
  // src/queue.ts