@guidelytech/sdk-core 0.15.0 → 0.16.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/README.md +1 -0
- package/dist/index.cjs +25 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/dist/capture-CgN69Lxq.d.cts +0 -100
- package/dist/capture-Csz721-A.d.ts +0 -100
- package/dist/chunk-JUSM7J5N.js +0 -376
- package/dist/chunk-JUSM7J5N.js.map +0 -1
- package/dist/chunk-ZAF2KRJA.js +0 -334
- package/dist/chunk-ZAF2KRJA.js.map +0 -1
- package/dist/health-sbP9JwTE.d.cts +0 -90
- package/dist/health-sbP9JwTE.d.ts +0 -90
- package/dist/http-Cb6KYYFv.d.cts +0 -61
- package/dist/http-D047Ngxk.d.ts +0 -61
- package/dist/types-BDlebXrm.d.ts +0 -314
- package/dist/types-Mtlmlj5C.d.cts +0 -314
package/dist/chunk-ZAF2KRJA.js
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__publicField,
|
|
3
|
-
backoffMs,
|
|
4
|
-
readJson,
|
|
5
|
-
writeJson
|
|
6
|
-
} from "./chunk-JUSM7J5N.js";
|
|
7
|
-
|
|
8
|
-
// src/queue.ts
|
|
9
|
-
var EventQueue = class {
|
|
10
|
-
constructor(opts) {
|
|
11
|
-
__publicField(this, "opts", opts);
|
|
12
|
-
__publicField(this, "events", []);
|
|
13
|
-
__publicField(this, "timer", null);
|
|
14
|
-
__publicField(this, "retryTimer", null);
|
|
15
|
-
__publicField(this, "inflight", false);
|
|
16
|
-
__publicField(this, "inflightIds", /* @__PURE__ */ new Set());
|
|
17
|
-
__publicField(this, "attempt", 0);
|
|
18
|
-
__publicField(this, "destroyed", false);
|
|
19
|
-
__publicField(this, "offRecover", null);
|
|
20
|
-
__publicField(this, "flushIntervalMs");
|
|
21
|
-
__publicField(this, "maxQueue");
|
|
22
|
-
__publicField(this, "batchSize");
|
|
23
|
-
__publicField(this, "maxAttempts");
|
|
24
|
-
this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
|
|
25
|
-
this.maxQueue = opts.maxQueue ?? 500;
|
|
26
|
-
this.batchSize = opts.batchSize ?? 50;
|
|
27
|
-
this.maxAttempts = opts.maxAttempts ?? 5;
|
|
28
|
-
const persisted = readJson(opts.store, opts.storageKey, []);
|
|
29
|
-
if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
|
|
30
|
-
if (this.events.length) this.schedule();
|
|
31
|
-
this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
|
|
32
|
-
}
|
|
33
|
-
/** The API is back: drop any pending backoff and send what is queued. */
|
|
34
|
-
retryNow() {
|
|
35
|
-
if (this.destroyed) return;
|
|
36
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
37
|
-
this.retryTimer = null;
|
|
38
|
-
this.attempt = 0;
|
|
39
|
-
void this.flush();
|
|
40
|
-
}
|
|
41
|
-
retryIn(ms) {
|
|
42
|
-
if (this.retryTimer || this.destroyed) return;
|
|
43
|
-
this.retryTimer = setTimeout(() => {
|
|
44
|
-
this.retryTimer = null;
|
|
45
|
-
void this.flush();
|
|
46
|
-
}, ms);
|
|
47
|
-
}
|
|
48
|
-
get size() {
|
|
49
|
-
return this.events.length;
|
|
50
|
-
}
|
|
51
|
-
/** Pending events (read-only copy, for debugging). */
|
|
52
|
-
peek() {
|
|
53
|
-
return this.events.slice();
|
|
54
|
-
}
|
|
55
|
-
enqueue(event) {
|
|
56
|
-
if (this.destroyed) return;
|
|
57
|
-
if (this.events.some((e) => e.eventId === event.eventId)) return;
|
|
58
|
-
this.events.push(event);
|
|
59
|
-
if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);
|
|
60
|
-
this.persist();
|
|
61
|
-
this.schedule();
|
|
62
|
-
}
|
|
63
|
-
persist() {
|
|
64
|
-
writeJson(this.opts.store, this.opts.storageKey, this.events);
|
|
65
|
-
}
|
|
66
|
-
schedule() {
|
|
67
|
-
if (this.timer || this.retryTimer || this.destroyed) return;
|
|
68
|
-
this.timer = setTimeout(() => {
|
|
69
|
-
this.timer = null;
|
|
70
|
-
void this.flush();
|
|
71
|
-
}, this.flushIntervalMs);
|
|
72
|
-
}
|
|
73
|
-
/** Sends queued events now (one batch at a time). Safe to call at any time. */
|
|
74
|
-
async flush() {
|
|
75
|
-
if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;
|
|
76
|
-
if (this.timer) {
|
|
77
|
-
clearTimeout(this.timer);
|
|
78
|
-
this.timer = null;
|
|
79
|
-
}
|
|
80
|
-
const wait = this.opts.health?.waitMs() ?? 0;
|
|
81
|
-
if (wait > 0) return this.retryIn(wait);
|
|
82
|
-
this.inflight = true;
|
|
83
|
-
const batch = this.events.slice(0, this.batchSize);
|
|
84
|
-
this.inflightIds = new Set(batch.map((e) => e.eventId));
|
|
85
|
-
let ok = false;
|
|
86
|
-
let retry = true;
|
|
87
|
-
try {
|
|
88
|
-
const res = await this.opts.send(batch);
|
|
89
|
-
ok = res.ok;
|
|
90
|
-
retry = res.retry ?? !res.ok;
|
|
91
|
-
} catch (err) {
|
|
92
|
-
this.opts.onError?.(err);
|
|
93
|
-
}
|
|
94
|
-
this.inflight = false;
|
|
95
|
-
this.inflightIds.clear();
|
|
96
|
-
if (this.destroyed) return;
|
|
97
|
-
if (ok || !retry) {
|
|
98
|
-
const ids = new Set(batch.map((e) => e.eventId));
|
|
99
|
-
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
100
|
-
this.attempt = 0;
|
|
101
|
-
this.persist();
|
|
102
|
-
if (this.events.length) this.schedule();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
|
|
106
|
-
const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
|
|
107
|
-
this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
|
|
108
|
-
}
|
|
109
|
-
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
110
|
-
flushSync() {
|
|
111
|
-
if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
|
|
112
|
-
const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
|
|
113
|
-
if (!batch.length) return;
|
|
114
|
-
if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
|
|
115
|
-
const ids = new Set(batch.map((e) => e.eventId));
|
|
116
|
-
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
117
|
-
}
|
|
118
|
-
this.persist();
|
|
119
|
-
}
|
|
120
|
-
clear() {
|
|
121
|
-
this.events = [];
|
|
122
|
-
this.persist();
|
|
123
|
-
}
|
|
124
|
-
destroy() {
|
|
125
|
-
this.destroyed = true;
|
|
126
|
-
this.offRecover?.();
|
|
127
|
-
if (this.timer) clearTimeout(this.timer);
|
|
128
|
-
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
129
|
-
this.timer = null;
|
|
130
|
-
this.retryTimer = null;
|
|
131
|
-
this.persist();
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// src/outbox.ts
|
|
136
|
-
var Outbox = class {
|
|
137
|
-
constructor(opts) {
|
|
138
|
-
__publicField(this, "opts", opts);
|
|
139
|
-
__publicField(this, "items");
|
|
140
|
-
__publicField(this, "sending", false);
|
|
141
|
-
__publicField(this, "attempt", 0);
|
|
142
|
-
__publicField(this, "timer", null);
|
|
143
|
-
__publicField(this, "destroyed", false);
|
|
144
|
-
__publicField(this, "offRecover");
|
|
145
|
-
__publicField(this, "maxItems");
|
|
146
|
-
__publicField(this, "maxAgeMs");
|
|
147
|
-
this.maxItems = opts.maxItems ?? 50;
|
|
148
|
-
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
149
|
-
const stored = readJson(opts.store, opts.storageKey, []);
|
|
150
|
-
const cutoff = Date.now() - this.maxAgeMs;
|
|
151
|
-
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
152
|
-
this.offRecover = opts.health?.onRecover(() => {
|
|
153
|
-
this.attempt = 0;
|
|
154
|
-
this.retryIn(0);
|
|
155
|
-
}) ?? null;
|
|
156
|
-
if (this.items.length) this.retryIn(0);
|
|
157
|
-
}
|
|
158
|
-
get size() {
|
|
159
|
-
return this.items.length;
|
|
160
|
-
}
|
|
161
|
-
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
162
|
-
add(key, path, body) {
|
|
163
|
-
if (this.destroyed) return;
|
|
164
|
-
this.items = this.items.filter((i) => i.key !== key);
|
|
165
|
-
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
166
|
-
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
167
|
-
this.persist();
|
|
168
|
-
void this.flush();
|
|
169
|
-
}
|
|
170
|
-
persist() {
|
|
171
|
-
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
172
|
-
}
|
|
173
|
-
retryIn(ms) {
|
|
174
|
-
if (this.destroyed) return;
|
|
175
|
-
if (this.timer) clearTimeout(this.timer);
|
|
176
|
-
this.timer = setTimeout(() => {
|
|
177
|
-
this.timer = null;
|
|
178
|
-
void this.flush();
|
|
179
|
-
}, ms);
|
|
180
|
-
}
|
|
181
|
-
/** Sends queued requests in order, one at a time. */
|
|
182
|
-
async flush() {
|
|
183
|
-
if (this.destroyed || this.sending || !this.items.length) return;
|
|
184
|
-
const wait = this.opts.health?.waitMs() ?? 0;
|
|
185
|
-
if (wait > 0) return this.retryIn(wait);
|
|
186
|
-
this.sending = true;
|
|
187
|
-
try {
|
|
188
|
-
while (this.items.length && !this.destroyed) {
|
|
189
|
-
const item = this.items[0];
|
|
190
|
-
let retry = false;
|
|
191
|
-
try {
|
|
192
|
-
const res = await this.opts.api.post(item.path, item.body);
|
|
193
|
-
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
retry = true;
|
|
196
|
-
this.opts.onError?.(err);
|
|
197
|
-
}
|
|
198
|
-
if (retry) {
|
|
199
|
-
this.attempt += 1;
|
|
200
|
-
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
this.attempt = 0;
|
|
204
|
-
this.items = this.items.filter((i) => i !== item);
|
|
205
|
-
this.persist();
|
|
206
|
-
}
|
|
207
|
-
} finally {
|
|
208
|
-
this.sending = false;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
destroy() {
|
|
212
|
-
this.destroyed = true;
|
|
213
|
-
this.offRecover?.();
|
|
214
|
-
if (this.timer) clearTimeout(this.timer);
|
|
215
|
-
this.timer = null;
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// src/emitter.ts
|
|
220
|
-
var Emitter = class {
|
|
221
|
-
constructor(onError) {
|
|
222
|
-
__publicField(this, "onError", onError);
|
|
223
|
-
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
224
|
-
}
|
|
225
|
-
on(event, cb) {
|
|
226
|
-
let set = this.listeners.get(event);
|
|
227
|
-
if (!set) {
|
|
228
|
-
set = /* @__PURE__ */ new Set();
|
|
229
|
-
this.listeners.set(event, set);
|
|
230
|
-
}
|
|
231
|
-
set.add(cb);
|
|
232
|
-
return () => this.off(event, cb);
|
|
233
|
-
}
|
|
234
|
-
off(event, cb) {
|
|
235
|
-
this.listeners.get(event)?.delete(cb);
|
|
236
|
-
}
|
|
237
|
-
emit(event, ...args) {
|
|
238
|
-
const set = this.listeners.get(event);
|
|
239
|
-
if (!set) return;
|
|
240
|
-
for (const cb of Array.from(set)) {
|
|
241
|
-
try {
|
|
242
|
-
cb(...args);
|
|
243
|
-
} catch (err) {
|
|
244
|
-
this.onError?.(err);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
clear() {
|
|
249
|
-
this.listeners.clear();
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
// src/frequency.ts
|
|
254
|
-
var HOUR = 3600 * 1e3;
|
|
255
|
-
function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
|
|
256
|
-
const s = state ?? { impressions: 0 };
|
|
257
|
-
if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
|
|
258
|
-
if (s.completedAt && freq.mode !== "every_time") return false;
|
|
259
|
-
if (s.dismissedAt) {
|
|
260
|
-
if (!freq.reshowAfterDismiss) return false;
|
|
261
|
-
if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
|
|
262
|
-
}
|
|
263
|
-
switch (freq.mode) {
|
|
264
|
-
case "once":
|
|
265
|
-
return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
|
|
266
|
-
case "once_per_session":
|
|
267
|
-
return !shownThisSession;
|
|
268
|
-
case "interval": {
|
|
269
|
-
if (!s.lastShownAt) return true;
|
|
270
|
-
const hours = freq.intervalHours ?? 24;
|
|
271
|
-
return now - Date.parse(s.lastShownAt) >= hours * HOUR;
|
|
272
|
-
}
|
|
273
|
-
case "every_time":
|
|
274
|
-
default:
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function isExclusiveGuide(guide) {
|
|
279
|
-
if (guide.kind === "banner" || guide.kind === "checklist") return false;
|
|
280
|
-
const steps = guide.config.steps;
|
|
281
|
-
if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
function areMutuallyExclusive(a, b) {
|
|
285
|
-
const la = a.config.frequency.mutuallyExclusiveWith;
|
|
286
|
-
const lb = b.config.frequency.mutuallyExclusiveWith;
|
|
287
|
-
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
288
|
-
}
|
|
289
|
-
function isWithinSchedule(guide, now = Date.now()) {
|
|
290
|
-
for (const w of [guide.config.schedule, guide.config.survey]) {
|
|
291
|
-
if (!w) continue;
|
|
292
|
-
if (w.startAt && now < Date.parse(w.startAt)) return false;
|
|
293
|
-
if (w.endAt && now > Date.parse(w.endAt)) return false;
|
|
294
|
-
}
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
var MINUTE = 60 * 1e3;
|
|
298
|
-
function countsTowardThrottle(guide) {
|
|
299
|
-
return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
|
|
300
|
-
}
|
|
301
|
-
function isThrottleEligible(settings, launches, now = Date.now()) {
|
|
302
|
-
if (!settings || !settings.enabled) return true;
|
|
303
|
-
const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
|
|
304
|
-
const windowMs = Math.max(1, settings.windowHours) * HOUR;
|
|
305
|
-
if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
|
|
306
|
-
if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
function pruneLaunches(launches, settings, now = Date.now()) {
|
|
310
|
-
const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
|
|
311
|
-
return launches.filter((l) => {
|
|
312
|
-
const t = Date.parse(l);
|
|
313
|
-
return !Number.isNaN(t) && now - t < keepMs;
|
|
314
|
-
}).slice(-100);
|
|
315
|
-
}
|
|
316
|
-
function dismissedAfterMerge(local, server) {
|
|
317
|
-
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
318
|
-
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export {
|
|
322
|
-
EventQueue,
|
|
323
|
-
Outbox,
|
|
324
|
-
Emitter,
|
|
325
|
-
isFrequencyEligible,
|
|
326
|
-
isExclusiveGuide,
|
|
327
|
-
areMutuallyExclusive,
|
|
328
|
-
isWithinSchedule,
|
|
329
|
-
countsTowardThrottle,
|
|
330
|
-
isThrottleEligible,
|
|
331
|
-
pruneLaunches,
|
|
332
|
-
dismissedAfterMerge
|
|
333
|
-
};
|
|
334
|
-
//# sourceMappingURL=chunk-ZAF2KRJA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/queue.ts","../src/outbox.ts","../src/emitter.ts","../src/frequency.ts"],"sourcesContent":["import type { SdkEvent } from \"@guidelytech/shared\";\nimport type { ServerHealth } from \"./health\";\nimport { backoffMs } from \"./http\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\nexport interface EventQueueOptions {\n store: KeyValueStore;\n storageKey: string;\n /** Sends one batch. Resolve `{ ok: true }` on success, `{ ok: false, retry }` otherwise. */\n send: (events: SdkEvent[]) => Promise<{ ok: boolean; retry?: boolean }>;\n /** Synchronous best-effort send used on page hide. */\n beacon?: (events: SdkEvent[]) => boolean;\n /** Should events be flushed right now? (analytics enabled + consent granted). */\n enabled: () => boolean;\n /** API reachability: sends wait while its circuit is open, and beaconed events are only forgotten while it is reachable. */\n health?: ServerHealth;\n flushIntervalMs?: number;\n maxQueue?: number;\n batchSize?: number;\n maxAttempts?: number;\n onError?: (err: unknown) => void;\n}\n\n/**\n * Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by\n * `eventId`, and keeps failed events (also across page loads) until the server takes them:\n * retries back off 1s→60s, wait while the API's circuit is open, and start again as soon as it\n * recovers. Drops the oldest events once the queue exceeds 500.\n */\nexport class EventQueue {\n private events: SdkEvent[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private inflight = false;\n private inflightIds = new Set<string>();\n private attempt = 0;\n private destroyed = false;\n private offRecover: (() => void) | null = null;\n private readonly flushIntervalMs: number;\n private readonly maxQueue: number;\n private readonly batchSize: number;\n private readonly maxAttempts: number;\n\n constructor(private readonly opts: EventQueueOptions) {\n this.flushIntervalMs = opts.flushIntervalMs ?? 2000;\n this.maxQueue = opts.maxQueue ?? 500;\n this.batchSize = opts.batchSize ?? 50;\n this.maxAttempts = opts.maxAttempts ?? 5;\n const persisted = readJson<SdkEvent[]>(opts.store, opts.storageKey, []);\n if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === \"string\").slice(-this.maxQueue);\n // Events left over from a previous page load are flushed on the next tick.\n if (this.events.length) this.schedule();\n this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;\n }\n\n /** The API is back: drop any pending backoff and send what is queued. */\n private retryNow(): void {\n if (this.destroyed) return;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.retryTimer = null;\n this.attempt = 0;\n void this.flush();\n }\n\n private retryIn(ms: number): void {\n if (this.retryTimer || this.destroyed) return;\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, ms);\n }\n\n get size(): number {\n return this.events.length;\n }\n\n /** Pending events (read-only copy, for debugging). */\n peek(): SdkEvent[] {\n return this.events.slice();\n }\n\n enqueue(event: SdkEvent): void {\n if (this.destroyed) return;\n if (this.events.some((e) => e.eventId === event.eventId)) return;\n this.events.push(event);\n if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);\n this.persist();\n this.schedule();\n }\n\n private persist(): void {\n writeJson(this.opts.store, this.opts.storageKey, this.events);\n }\n\n private schedule(): void {\n if (this.timer || this.retryTimer || this.destroyed) return;\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n /** Sends queued events now (one batch at a time). Safe to call at any time. */\n async flush(): Promise<void> {\n if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n const wait = this.opts.health?.waitMs() ?? 0;\n if (wait > 0) return this.retryIn(wait);\n this.inflight = true;\n const batch = this.events.slice(0, this.batchSize);\n this.inflightIds = new Set(batch.map((e) => e.eventId));\n let ok = false;\n let retry = true;\n try {\n const res = await this.opts.send(batch);\n ok = res.ok;\n retry = res.retry ?? !res.ok;\n } catch (err) {\n this.opts.onError?.(err);\n }\n this.inflight = false;\n this.inflightIds.clear();\n if (this.destroyed) return;\n if (ok || !retry) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.attempt = 0;\n this.persist();\n if (this.events.length) this.schedule();\n return;\n }\n // Fast retries first, then about once a minute until the server takes the events (or the\n // API's circuit says to wait longer); a recovery or the next page load sends them sooner.\n this.attempt = Math.min(this.attempt + 1, this.maxAttempts);\n const delay = this.attempt >= this.maxAttempts ? 60_000 : backoffMs(this.attempt - 1);\n this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));\n }\n\n /** Best-effort synchronous flush on unload using `sendBeacon`. */\n flushSync(): void {\n if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;\n // Events already in flight (fetch with keepalive) survive unload; only beacon the rest to avoid duplicate batches.\n const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);\n if (!batch.length) return;\n // A beacon that was queued may still never arrive (the server is down), and nothing reports\n // back. Only forget its events while the API is answering; otherwise they stay persisted and the\n // next page load sends them again (the server drops events it already has, by eventId).\n if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n }\n this.persist();\n }\n\n clear(): void {\n this.events = [];\n this.persist();\n }\n\n destroy(): void {\n this.destroyed = true;\n this.offRecover?.();\n if (this.timer) clearTimeout(this.timer);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.timer = null;\n this.retryTimer = null;\n this.persist();\n }\n}\n","import type { ServerHealth } from \"./health\";\nimport { backoffMs, type Api } from \"./http\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\n/** One request waiting to reach the server. */\ninterface OutboxItem {\n /** Same key → the newer request replaces the older one (e.g. a checklist's latest progress). */\n key: string;\n path: string;\n body: unknown;\n createdAt: number;\n}\n\nexport interface OutboxOptions {\n api: Api;\n store: KeyValueStore;\n storageKey: string;\n health?: ServerHealth;\n maxItems?: number;\n maxAgeMs?: number;\n onError?: (err: unknown) => void;\n}\n\n/**\n * Persisted queue for writes that must not be lost when the API is down: survey answers, guide\n * completions and checklist progress. Each request is kept (also across page loads) until the\n * server answers it; 5xx, 429 and network errors are retried with backoff, waiting while the API's\n * circuit is open. Every request is idempotent on the server (survey answers dedupe by\n * `responseId`, progress merges), so sending one twice is harmless. Bounded to 50 requests and 7 days.\n */\nexport class Outbox {\n private items: OutboxItem[];\n private sending = false;\n private attempt = 0;\n private timer: ReturnType<typeof setTimeout> | null = null;\n private destroyed = false;\n private readonly offRecover: (() => void) | null;\n private readonly maxItems: number;\n private readonly maxAgeMs: number;\n\n constructor(private readonly opts: OutboxOptions) {\n this.maxItems = opts.maxItems ?? 50;\n this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;\n const stored = readJson<OutboxItem[]>(opts.store, opts.storageKey, []);\n const cutoff = Date.now() - this.maxAgeMs;\n this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === \"string\" && typeof i.key === \"string\" && i.createdAt > cutoff).slice(-this.maxItems) : [];\n this.offRecover = opts.health?.onRecover(() => {\n this.attempt = 0;\n this.retryIn(0);\n }) ?? null;\n if (this.items.length) this.retryIn(0);\n }\n\n get size(): number {\n return this.items.length;\n }\n\n /** Queues `POST path` and sends it now (or as soon as the API is reachable). */\n add(key: string, path: string, body: unknown): void {\n if (this.destroyed) return;\n this.items = this.items.filter((i) => i.key !== key);\n this.items.push({ key, path, body, createdAt: Date.now() });\n if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);\n this.persist();\n void this.flush();\n }\n\n private persist(): void {\n writeJson(this.opts.store, this.opts.storageKey, this.items);\n }\n\n private retryIn(ms: number): void {\n if (this.destroyed) return;\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.flush();\n }, ms);\n }\n\n /** Sends queued requests in order, one at a time. */\n async flush(): Promise<void> {\n if (this.destroyed || this.sending || !this.items.length) return;\n const wait = this.opts.health?.waitMs() ?? 0;\n if (wait > 0) return this.retryIn(wait);\n this.sending = true;\n try {\n while (this.items.length && !this.destroyed) {\n const item = this.items[0] as OutboxItem;\n let retry = false;\n try {\n const res = await this.opts.api.post(item.path, item.body);\n retry = !res.ok && (res.status === 429 || res.status >= 500);\n } catch (err) {\n retry = true;\n this.opts.onError?.(err);\n }\n if (retry) {\n this.attempt += 1;\n this.retryIn(Math.max(backoffMs(this.attempt - 1, 1000, 60_000), this.opts.health?.waitMs() ?? 0));\n return;\n }\n // Answered (or refused for good, e.g. the survey was deleted): done with it. A newer request\n // under the same key may have replaced it while this one was in flight; keep that one.\n this.attempt = 0;\n this.items = this.items.filter((i) => i !== item);\n this.persist();\n }\n } finally {\n this.sending = false;\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.offRecover?.();\n if (this.timer) clearTimeout(this.timer);\n this.timer = null;\n }\n}\n","type AnyFn = (...args: any[]) => void;\n\n/** Tiny typed event emitter. Listener errors are reported, never thrown into the caller. */\nexport class Emitter<Events extends { [K in keyof Events]: AnyFn }> {\n private listeners = new Map<keyof Events, Set<AnyFn>>();\n constructor(private readonly onError?: (err: unknown) => void) {}\n\n on<K extends keyof Events>(event: K, cb: Events[K]): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(cb);\n return () => this.off(event, cb);\n }\n\n off<K extends keyof Events>(event: K, cb: Events[K]): void {\n this.listeners.get(event)?.delete(cb);\n }\n\n emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const cb of Array.from(set)) {\n try {\n cb(...args);\n } catch (err) {\n this.onError?.(err);\n }\n }\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n","import type { FrequencyConfig, GuideThrottlingSettings, PublishedGuide } from \"@guidelytech/shared\";\n\n/** Per-guide state kept locally and merged with the server's `IdentifyResponse.guideState`. */\nexport interface GuideLocalState {\n impressions: number;\n completedAt?: string | null;\n dismissedAt?: string | null;\n lastShownAt?: string | null;\n /** Last step reached in a multi-step flow (local only), so the messenger can offer to resume it. */\n lastStepId?: string | null;\n}\n\nconst HOUR = 3600 * 1000;\n\n/**\n * Pure frequency evaluation. `shownThisSession` comes from session storage.\n * - `once`: a single impression ever (unless `reshowAfterDismiss` and it was only dismissed).\n * - `once_per_session`: at most once per browser session.\n * - `every_time`: on every trigger (still blocked by completion/dismissal rules below).\n * - `interval`: at most once every `intervalHours`.\n * `maxImpressions` caps total impressions; dismissal blocks re-showing unless `reshowAfterDismiss`\n * (then `cooldownHours` must have elapsed); completion blocks unless mode is `every_time`.\n */\nexport function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalState | undefined, shownThisSession: boolean, now: number = Date.now()): boolean {\n const s = state ?? { impressions: 0 };\n if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;\n if (s.completedAt && freq.mode !== \"every_time\") return false;\n if (s.dismissedAt) {\n if (!freq.reshowAfterDismiss) return false;\n if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;\n }\n switch (freq.mode) {\n case \"once\":\n return s.impressions === 0 || (!!s.dismissedAt && freq.reshowAfterDismiss);\n case \"once_per_session\":\n return !shownThisSession;\n case \"interval\": {\n if (!s.lastShownAt) return true;\n const hours = freq.intervalHours ?? 24;\n return now - Date.parse(s.lastShownAt) >= hours * HOUR;\n }\n case \"every_time\":\n default:\n return true;\n }\n}\n\n/**\n * Exclusive guides (flows, modals, tooltips, announcements, surveys) run one at a time.\n * Banners, checklists and hotspot-only tooltips can coexist with an active flow.\n */\nexport function isExclusiveGuide(guide: PublishedGuide): boolean {\n if (guide.kind === \"banner\" || guide.kind === \"checklist\") return false;\n const steps = guide.config.steps;\n if (steps.length > 0 && steps.every((s) => s.type === \"hotspot\" || s.type === \"banner\")) return false;\n return true;\n}\n\n/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */\nexport function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean {\n const la = a.config.frequency.mutuallyExclusiveWith;\n const lb = b.config.frequency.mutuallyExclusiveWith;\n return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);\n}\n\n/**\n * Scheduling window: the guide's `schedule` (any kind) and, for surveys, `survey.startAt` / `endAt`.\n * Unparsable dates are ignored.\n */\nexport function isWithinSchedule(guide: Pick<PublishedGuide, \"config\">, now: number = Date.now()): boolean {\n for (const w of [guide.config.schedule, guide.config.survey]) {\n if (!w) continue;\n if (w.startAt && now < Date.parse(w.startAt)) return false;\n if (w.endAt && now > Date.parse(w.endAt)) return false;\n }\n return true;\n}\n\nconst MINUTE = 60 * 1000;\n\n/**\n * Does showing this guide automatically count toward (and get limited by) the project's guide\n * throttling? Only exclusive guides (flows, modals, tooltips, announcements, surveys) interrupt;\n * banners, checklists and hotspot-only guides never count, nor does a guide set to ignore throttling.\n * Guides started by the visitor or the app (`startGuide`, buttons, Help) are never throttled.\n */\nexport function countsTowardThrottle(guide: PublishedGuide): boolean {\n return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;\n}\n\n/**\n * Pure throttling check. `launches` = ISO times of this visitor's earlier automatic launches of\n * counted guides (any order). Disabled or missing settings never throttle.\n */\nexport function isThrottleEligible(settings: GuideThrottlingSettings | null | undefined, launches: readonly string[], now: number = Date.now()): boolean {\n if (!settings || !settings.enabled) return true;\n const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));\n const windowMs = Math.max(1, settings.windowHours) * HOUR;\n if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;\n if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;\n return true;\n}\n\n/** Launch times worth keeping: those still inside the window or the minimum gap (at most 100). */\nexport function pruneLaunches(launches: readonly string[], settings: GuideThrottlingSettings | null | undefined, now: number = Date.now()): string[] {\n const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;\n return launches.filter((l) => { const t = Date.parse(l); return !Number.isNaN(t) && now - t < keepMs; }).slice(-100);\n}\n\n/**\n * A checklist's dismissal after merging server state. A local dismissal normally wins (it may not have\n * synced yet), but when the server reports a reset this device hasn't applied, the server's value wins:\n * null, or a dismissal made on another device after the reset. Compared as tokens, not times, so a\n * skewed device clock can't keep a reset dismissal.\n */\nexport function dismissedAfterMerge(local: { dismissedAt?: string | null; dismissResetAt?: string | null } | undefined, server: { dismissedAt?: string | null; dismissResetAt?: string | null }): string | null {\n if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;\n return local?.dismissedAt ?? server.dismissedAt ?? null;\n}\n"],"mappings":";;;;;;;;AA6BO,IAAM,aAAN,MAAiB;AAAA,EActB,YAA6B,MAAyB;AAAzB;AAb7B,wBAAQ,UAAqB,CAAC;AAC9B,wBAAQ,SAA8C;AACtD,wBAAQ,cAAmD;AAC3D,wBAAQ,YAAW;AACnB,wBAAQ,eAAc,oBAAI,IAAY;AACtC,wBAAQ,WAAU;AAClB,wBAAQ,aAAY;AACpB,wBAAQ,cAAkC;AAC1C,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AAGf,SAAK,kBAAkB,KAAK,mBAAmB;AAC/C,SAAK,WAAW,KAAK,YAAY;AACjC,SAAK,YAAY,KAAK,aAAa;AACnC,SAAK,cAAc,KAAK,eAAe;AACvC,UAAM,YAAY,SAAqB,KAAK,OAAO,KAAK,YAAY,CAAC,CAAC;AACtE,QAAI,MAAM,QAAQ,SAAS,EAAG,MAAK,SAAS,UAAU,OAAO,CAAC,MAAM,KAAK,OAAO,EAAE,YAAY,QAAQ,EAAE,MAAM,CAAC,KAAK,QAAQ;AAE5H,QAAI,KAAK,OAAO,OAAQ,MAAK,SAAS;AACtC,SAAK,aAAa,KAAK,QAAQ,UAAU,MAAM,KAAK,SAAS,CAAC,KAAK;AAAA,EACrE;AAAA;AAAA,EAGQ,WAAiB;AACvB,QAAI,KAAK,UAAW;AACpB,QAAI,KAAK,WAAY,cAAa,KAAK,UAAU;AACjD,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEQ,QAAQ,IAAkB;AAChC,QAAI,KAAK,cAAc,KAAK,UAAW;AACvC,SAAK,aAAa,WAAW,MAAM;AACjC,WAAK,aAAa;AAClB,WAAK,KAAK,MAAM;AAAA,IAClB,GAAG,EAAE;AAAA,EACP;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA,EAGA,OAAmB;AACjB,WAAO,KAAK,OAAO,MAAM;AAAA,EAC3B;AAAA,EAEA,QAAQ,OAAuB;AAC7B,QAAI,KAAK,UAAW;AACpB,QAAI,KAAK,OAAO,KAAK,CAAC,MAAM,EAAE,YAAY,MAAM,OAAO,EAAG;AAC1D,SAAK,OAAO,KAAK,KAAK;AACtB,QAAI,KAAK,OAAO,SAAS,KAAK,SAAU,MAAK,OAAO,OAAO,GAAG,KAAK,OAAO,SAAS,KAAK,QAAQ;AAChG,SAAK,QAAQ;AACb,SAAK,SAAS;AAAA,EAChB;AAAA,EAEQ,UAAgB;AACtB,cAAU,KAAK,KAAK,OAAO,KAAK,KAAK,YAAY,KAAK,MAAM;AAAA,EAC9D;AAAA,EAEQ,WAAiB;AACvB,QAAI,KAAK,SAAS,KAAK,cAAc,KAAK,UAAW;AACrD,SAAK,QAAQ,WAAW,MAAM;AAC5B,WAAK,QAAQ;AACb,WAAK,KAAK,MAAM;AAAA,IAClB,GAAG,KAAK,eAAe;AAAA,EACzB;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,aAAa,KAAK,YAAY,CAAC,KAAK,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ,EAAG;AACpF,QAAI,KAAK,OAAO;AACd,mBAAa,KAAK,KAAK;AACvB,WAAK,QAAQ;AAAA,IACf;AACA,UAAM,OAAO,KAAK,KAAK,QAAQ,OAAO,KAAK;AAC3C,QAAI,OAAO,EAAG,QAAO,KAAK,QAAQ,IAAI;AACtC,SAAK,WAAW;AAChB,UAAM,QAAQ,KAAK,OAAO,MAAM,GAAG,KAAK,SAAS;AACjD,SAAK,cAAc,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AACtD,QAAI,KAAK;AACT,QAAI,QAAQ;AACZ,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,KAAK,KAAK,KAAK;AACtC,WAAK,IAAI;AACT,cAAQ,IAAI,SAAS,CAAC,IAAI;AAAA,IAC5B,SAAS,KAAK;AACZ,WAAK,KAAK,UAAU,GAAG;AAAA,IACzB;AACA,SAAK,WAAW;AAChB,SAAK,YAAY,MAAM;AACvB,QAAI,KAAK,UAAW;AACpB,QAAI,MAAM,CAAC,OAAO;AAChB,YAAM,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AAC/C,WAAK,SAAS,KAAK,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,OAAO,CAAC;AAC3D,WAAK,UAAU;AACf,WAAK,QAAQ;AACb,UAAI,KAAK,OAAO,OAAQ,MAAK,SAAS;AACtC;AAAA,IACF;AAGA,SAAK,UAAU,KAAK,IAAI,KAAK,UAAU,GAAG,KAAK,WAAW;AAC1D,UAAM,QAAQ,KAAK,WAAW,KAAK,cAAc,MAAS,UAAU,KAAK,UAAU,CAAC;AACpF,SAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,OAAO,KAAK,CAAC,CAAC;AAAA,EAC/D;AAAA;AAAA,EAGA,YAAkB;AAChB,QAAI,KAAK,aAAa,CAAC,KAAK,KAAK,QAAQ,KAAK,CAAC,KAAK,KAAK,OAAQ;AAEjE,UAAM,QAAQ,KAAK,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,YAAY,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,SAAS;AACjG,QAAI,CAAC,MAAM,OAAQ;AAInB,QAAI,KAAK,KAAK,OAAO,KAAK,MAAM,KAAK,KAAK,QAAQ,aAAa,OAAO;AACpE,YAAM,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC;AAC/C,WAAK,SAAS,KAAK,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,OAAO,CAAC;AAAA,IAC7D;AACA,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,QAAc;AACZ,SAAK,SAAS,CAAC;AACf,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,QAAI,KAAK,MAAO,cAAa,KAAK,KAAK;AACvC,QAAI,KAAK,WAAY,cAAa,KAAK,UAAU;AACjD,SAAK,QAAQ;AACb,SAAK,aAAa;AAClB,SAAK,QAAQ;AAAA,EACf;AACF;;;AC7IO,IAAM,SAAN,MAAa;AAAA,EAUlB,YAA6B,MAAqB;AAArB;AAT7B,wBAAQ;AACR,wBAAQ,WAAU;AAClB,wBAAQ,WAAU;AAClB,wBAAQ,SAA8C;AACtD,wBAAQ,aAAY;AACpB,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AAGf,SAAK,WAAW,KAAK,YAAY;AACjC,SAAK,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK,KAAK;AACpD,UAAM,SAAS,SAAuB,KAAK,OAAO,KAAK,YAAY,CAAC,CAAC;AACrE,UAAM,SAAS,KAAK,IAAI,IAAI,KAAK;AACjC,SAAK,QAAQ,MAAM,QAAQ,MAAM,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,OAAO,EAAE,SAAS,YAAY,OAAO,EAAE,QAAQ,YAAY,EAAE,YAAY,MAAM,EAAE,MAAM,CAAC,KAAK,QAAQ,IAAI,CAAC;AACzK,SAAK,aAAa,KAAK,QAAQ,UAAU,MAAM;AAC7C,WAAK,UAAU;AACf,WAAK,QAAQ,CAAC;AAAA,IAChB,CAAC,KAAK;AACN,QAAI,KAAK,MAAM,OAAQ,MAAK,QAAQ,CAAC;AAAA,EACvC;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,KAAa,MAAc,MAAqB;AAClD,QAAI,KAAK,UAAW;AACpB,SAAK,QAAQ,KAAK,MAAM,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG;AACnD,SAAK,MAAM,KAAK,EAAE,KAAK,MAAM,MAAM,WAAW,KAAK,IAAI,EAAE,CAAC;AAC1D,QAAI,KAAK,MAAM,SAAS,KAAK,SAAU,MAAK,MAAM,OAAO,GAAG,KAAK,MAAM,SAAS,KAAK,QAAQ;AAC7F,SAAK,QAAQ;AACb,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEQ,UAAgB;AACtB,cAAU,KAAK,KAAK,OAAO,KAAK,KAAK,YAAY,KAAK,KAAK;AAAA,EAC7D;AAAA,EAEQ,QAAQ,IAAkB;AAChC,QAAI,KAAK,UAAW;AACpB,QAAI,KAAK,MAAO,cAAa,KAAK,KAAK;AACvC,SAAK,QAAQ,WAAW,MAAM;AAC5B,WAAK,QAAQ;AACb,WAAK,KAAK,MAAM;AAAA,IAClB,GAAG,EAAE;AAAA,EACP;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,aAAa,KAAK,WAAW,CAAC,KAAK,MAAM,OAAQ;AAC1D,UAAM,OAAO,KAAK,KAAK,QAAQ,OAAO,KAAK;AAC3C,QAAI,OAAO,EAAG,QAAO,KAAK,QAAQ,IAAI;AACtC,SAAK,UAAU;AACf,QAAI;AACF,aAAO,KAAK,MAAM,UAAU,CAAC,KAAK,WAAW;AAC3C,cAAM,OAAO,KAAK,MAAM,CAAC;AACzB,YAAI,QAAQ;AACZ,YAAI;AACF,gBAAM,MAAM,MAAM,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,KAAK,IAAI;AACzD,kBAAQ,CAAC,IAAI,OAAO,IAAI,WAAW,OAAO,IAAI,UAAU;AAAA,QAC1D,SAAS,KAAK;AACZ,kBAAQ;AACR,eAAK,KAAK,UAAU,GAAG;AAAA,QACzB;AACA,YAAI,OAAO;AACT,eAAK,WAAW;AAChB,eAAK,QAAQ,KAAK,IAAI,UAAU,KAAK,UAAU,GAAG,KAAM,GAAM,GAAG,KAAK,KAAK,QAAQ,OAAO,KAAK,CAAC,CAAC;AACjG;AAAA,QACF;AAGA,aAAK,UAAU;AACf,aAAK,QAAQ,KAAK,MAAM,OAAO,CAAC,MAAM,MAAM,IAAI;AAChD,aAAK,QAAQ;AAAA,MACf;AAAA,IACF,UAAE;AACA,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,QAAI,KAAK,MAAO,cAAa,KAAK,KAAK;AACvC,SAAK,QAAQ;AAAA,EACf;AACF;;;ACpHO,IAAM,UAAN,MAA6D;AAAA,EAElE,YAA6B,SAAkC;AAAlC;AAD7B,wBAAQ,aAAY,oBAAI,IAA8B;AAAA,EACU;AAAA,EAEhE,GAA2B,OAAU,IAA2B;AAC9D,QAAI,MAAM,KAAK,UAAU,IAAI,KAAK;AAClC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,UAAU,IAAI,OAAO,GAAG;AAAA,IAC/B;AACA,QAAI,IAAI,EAAE;AACV,WAAO,MAAM,KAAK,IAAI,OAAO,EAAE;AAAA,EACjC;AAAA,EAEA,IAA4B,OAAU,IAAqB;AACzD,SAAK,UAAU,IAAI,KAAK,GAAG,OAAO,EAAE;AAAA,EACtC;AAAA,EAEA,KAA6B,UAAa,MAAmC;AAC3E,UAAM,MAAM,KAAK,UAAU,IAAI,KAAK;AACpC,QAAI,CAAC,IAAK;AACV,eAAW,MAAM,MAAM,KAAK,GAAG,GAAG;AAChC,UAAI;AACF,WAAG,GAAG,IAAI;AAAA,MACZ,SAAS,KAAK;AACZ,aAAK,UAAU,GAAG;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;;;ACxBA,IAAM,OAAO,OAAO;AAWb,SAAS,oBAAoB,MAAuB,OAAoC,kBAA2B,MAAc,KAAK,IAAI,GAAY;AAC3J,QAAM,IAAI,SAAS,EAAE,aAAa,EAAE;AACpC,MAAI,KAAK,kBAAkB,QAAQ,EAAE,eAAe,KAAK,eAAgB,QAAO;AAChF,MAAI,EAAE,eAAe,KAAK,SAAS,aAAc,QAAO;AACxD,MAAI,EAAE,aAAa;AACjB,QAAI,CAAC,KAAK,mBAAoB,QAAO;AACrC,QAAI,KAAK,gBAAgB,KAAK,MAAM,KAAK,MAAM,EAAE,WAAW,IAAI,KAAK,gBAAgB,KAAM,QAAO;AAAA,EACpG;AACA,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,EAAE,gBAAgB,KAAM,CAAC,CAAC,EAAE,eAAe,KAAK;AAAA,IACzD,KAAK;AACH,aAAO,CAAC;AAAA,IACV,KAAK,YAAY;AACf,UAAI,CAAC,EAAE,YAAa,QAAO;AAC3B,YAAM,QAAQ,KAAK,iBAAiB;AACpC,aAAO,MAAM,KAAK,MAAM,EAAE,WAAW,KAAK,QAAQ;AAAA,IACpD;AAAA,IACA,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAMO,SAAS,iBAAiB,OAAgC;AAC/D,MAAI,MAAM,SAAS,YAAY,MAAM,SAAS,YAAa,QAAO;AAClE,QAAM,QAAQ,MAAM,OAAO;AAC3B,MAAI,MAAM,SAAS,KAAK,MAAM,MAAM,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,SAAS,QAAQ,EAAG,QAAO;AAChG,SAAO;AACT;AAGO,SAAS,qBAAqB,GAAmB,GAA4B;AAClF,QAAM,KAAK,EAAE,OAAO,UAAU;AAC9B,QAAM,KAAK,EAAE,OAAO,UAAU;AAC9B,SAAO,GAAG,SAAS,EAAE,EAAE,KAAK,GAAG,SAAS,EAAE,GAAG,KAAK,GAAG,SAAS,EAAE,EAAE,KAAK,GAAG,SAAS,EAAE,GAAG;AAC1F;AAMO,SAAS,iBAAiB,OAAuC,MAAc,KAAK,IAAI,GAAY;AACzG,aAAW,KAAK,CAAC,MAAM,OAAO,UAAU,MAAM,OAAO,MAAM,GAAG;AAC5D,QAAI,CAAC,EAAG;AACR,QAAI,EAAE,WAAW,MAAM,KAAK,MAAM,EAAE,OAAO,EAAG,QAAO;AACrD,QAAI,EAAE,SAAS,MAAM,KAAK,MAAM,EAAE,KAAK,EAAG,QAAO;AAAA,EACnD;AACA,SAAO;AACT;AAEA,IAAM,SAAS,KAAK;AAQb,SAAS,qBAAqB,OAAgC;AACnE,SAAO,iBAAiB,KAAK,KAAK,CAAC,MAAM,OAAO,UAAU;AAC5D;AAMO,SAAS,mBAAmB,UAAsD,UAA6B,MAAc,KAAK,IAAI,GAAY;AACvJ,MAAI,CAAC,YAAY,CAAC,SAAS,QAAS,QAAO;AAC3C,QAAM,QAAQ,SAAS,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,MAAM,CAAC,CAAC;AAC/E,QAAM,WAAW,KAAK,IAAI,GAAG,SAAS,WAAW,IAAI;AACrD,MAAI,MAAM,OAAO,CAAC,MAAM,MAAM,IAAI,QAAQ,EAAE,UAAU,KAAK,IAAI,GAAG,SAAS,SAAS,EAAG,QAAO;AAC9F,MAAI,SAAS,gBAAgB,KAAK,MAAM,KAAK,CAAC,MAAM,MAAM,IAAI,SAAS,gBAAgB,MAAM,EAAG,QAAO;AACvG,SAAO;AACT;AAGO,SAAS,cAAc,UAA6B,UAAsD,MAAc,KAAK,IAAI,GAAa;AACnJ,QAAM,SAAS,KAAK,IAAI,UAAU,eAAe,IAAI,CAAC,IAAI,QAAQ,UAAU,iBAAiB,KAAK;AAClG,SAAO,SAAS,OAAO,CAAC,MAAM;AAAE,UAAM,IAAI,KAAK,MAAM,CAAC;AAAG,WAAO,CAAC,OAAO,MAAM,CAAC,KAAK,MAAM,IAAI;AAAA,EAAQ,CAAC,EAAE,MAAM,IAAI;AACrH;AAQO,SAAS,oBAAoB,OAAoF,QAAwF;AAC9M,MAAI,OAAO,kBAAkB,OAAO,mBAAmB,OAAO,eAAgB,QAAO,OAAO,eAAe;AAC3G,SAAO,OAAO,eAAe,OAAO,eAAe;AACrD;","names":[]}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
type StorageKind = "local" | "session" | "memory";
|
|
2
|
-
/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */
|
|
3
|
-
interface KeyValueStore {
|
|
4
|
-
get(key: string): string | null;
|
|
5
|
-
set(key: string, value: string): void;
|
|
6
|
-
remove(key: string): void;
|
|
7
|
-
}
|
|
8
|
-
declare function memoryStore(): KeyValueStore;
|
|
9
|
-
/** Creates a store of the requested kind, degrading to memory when unavailable. */
|
|
10
|
-
declare function createStorage(kind?: StorageKind): KeyValueStore;
|
|
11
|
-
declare function readJson<T>(store: KeyValueStore, key: string, fallback: T): T;
|
|
12
|
-
declare function writeJson(store: KeyValueStore, key: string, value: unknown): void;
|
|
13
|
-
/** Storage keys used by the SDK (documented for integrators). */
|
|
14
|
-
declare const KEYS: {
|
|
15
|
-
readonly anon: "guidely:anon";
|
|
16
|
-
readonly seen: "guidely:seen";
|
|
17
|
-
readonly config: (key: string) => string;
|
|
18
|
-
readonly etag: (key: string) => string;
|
|
19
|
-
readonly state: (key: string) => string;
|
|
20
|
-
readonly user: (key: string) => string;
|
|
21
|
-
readonly queue: (key: string) => string;
|
|
22
|
-
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
23
|
-
readonly outbox: (key: string) => string;
|
|
24
|
-
/** Agent Analytics turns not yet accepted by the server. */
|
|
25
|
-
readonly agentOutbox: (key: string) => string;
|
|
26
|
-
readonly session: "guidely:session";
|
|
27
|
-
readonly active: "guidely:active";
|
|
28
|
-
readonly authoring: "guidely:authoring";
|
|
29
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
30
|
-
readonly capture: "guidely:capture";
|
|
31
|
-
readonly capturePending: "guidely:capture:pending";
|
|
32
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
33
|
-
readonly conversations: (key: string) => string;
|
|
34
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
35
|
-
readonly newsSeen: (key: string) => string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Whether the Guidely API is reachable right now: a circuit breaker shared by everything the SDK
|
|
40
|
-
* sends in the background (event batches, replay uploads, config refreshes, queued survey answers).
|
|
41
|
-
*
|
|
42
|
-
* Every API answer is reported here. After `threshold` failures in a row (network errors, timeouts,
|
|
43
|
-
* 429, 5xx) the circuit opens for a cooldown that doubles with each further failure (5 s up to
|
|
44
|
-
* 5 min, ±20% jitter so tabs don't retry in step); a `Retry-After` answer sets the cooldown
|
|
45
|
-
* directly. While it is open, background senders wait `waitMs()` instead of sending. Requests the
|
|
46
|
-
* person makes (a messenger message) still go out, and any answer closes the circuit again. The
|
|
47
|
-
* browser's `online` event ends the cooldown early.
|
|
48
|
-
*/
|
|
49
|
-
interface ServerHealthOptions {
|
|
50
|
-
threshold?: number;
|
|
51
|
-
baseMs?: number;
|
|
52
|
-
maxMs?: number;
|
|
53
|
-
now?: () => number;
|
|
54
|
-
random?: () => number;
|
|
55
|
-
}
|
|
56
|
-
declare class ServerHealth {
|
|
57
|
-
private failures;
|
|
58
|
-
private openUntil;
|
|
59
|
-
private lastOkAt;
|
|
60
|
-
private readonly listeners;
|
|
61
|
-
private readonly threshold;
|
|
62
|
-
private readonly baseMs;
|
|
63
|
-
private readonly maxMs;
|
|
64
|
-
private readonly now;
|
|
65
|
-
private readonly random;
|
|
66
|
-
constructor(opts?: ServerHealthOptions);
|
|
67
|
-
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
68
|
-
success(): void;
|
|
69
|
-
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
70
|
-
failure(retryAfterMs?: number | null): void;
|
|
71
|
-
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
72
|
-
waitMs(): number;
|
|
73
|
-
canSend(): boolean;
|
|
74
|
-
/**
|
|
75
|
-
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
76
|
-
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
77
|
-
*/
|
|
78
|
-
get reachable(): boolean;
|
|
79
|
-
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
80
|
-
online(): void;
|
|
81
|
-
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
82
|
-
onRecover(cb: () => void): () => void;
|
|
83
|
-
private notify;
|
|
84
|
-
}
|
|
85
|
-
/** `Retry-After` as milliseconds: delta-seconds or an HTTP date. Null when absent or unparseable. */
|
|
86
|
-
declare function parseRetryAfter(value: string | null | undefined, now?: number): number | null;
|
|
87
|
-
/** Answers that mean the server is down or shedding load (retry later) rather than refusing the request. */
|
|
88
|
-
declare function isServerFailure(status: number): boolean;
|
|
89
|
-
|
|
90
|
-
export { type KeyValueStore as K, ServerHealth as S, KEYS as a, type ServerHealthOptions as b, type StorageKind as c, createStorage as d, isServerFailure as i, memoryStore as m, parseRetryAfter as p, readJson as r, writeJson as w };
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
type StorageKind = "local" | "session" | "memory";
|
|
2
|
-
/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */
|
|
3
|
-
interface KeyValueStore {
|
|
4
|
-
get(key: string): string | null;
|
|
5
|
-
set(key: string, value: string): void;
|
|
6
|
-
remove(key: string): void;
|
|
7
|
-
}
|
|
8
|
-
declare function memoryStore(): KeyValueStore;
|
|
9
|
-
/** Creates a store of the requested kind, degrading to memory when unavailable. */
|
|
10
|
-
declare function createStorage(kind?: StorageKind): KeyValueStore;
|
|
11
|
-
declare function readJson<T>(store: KeyValueStore, key: string, fallback: T): T;
|
|
12
|
-
declare function writeJson(store: KeyValueStore, key: string, value: unknown): void;
|
|
13
|
-
/** Storage keys used by the SDK (documented for integrators). */
|
|
14
|
-
declare const KEYS: {
|
|
15
|
-
readonly anon: "guidely:anon";
|
|
16
|
-
readonly seen: "guidely:seen";
|
|
17
|
-
readonly config: (key: string) => string;
|
|
18
|
-
readonly etag: (key: string) => string;
|
|
19
|
-
readonly state: (key: string) => string;
|
|
20
|
-
readonly user: (key: string) => string;
|
|
21
|
-
readonly queue: (key: string) => string;
|
|
22
|
-
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
23
|
-
readonly outbox: (key: string) => string;
|
|
24
|
-
/** Agent Analytics turns not yet accepted by the server. */
|
|
25
|
-
readonly agentOutbox: (key: string) => string;
|
|
26
|
-
readonly session: "guidely:session";
|
|
27
|
-
readonly active: "guidely:active";
|
|
28
|
-
readonly authoring: "guidely:authoring";
|
|
29
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
30
|
-
readonly capture: "guidely:capture";
|
|
31
|
-
readonly capturePending: "guidely:capture:pending";
|
|
32
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
33
|
-
readonly conversations: (key: string) => string;
|
|
34
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
35
|
-
readonly newsSeen: (key: string) => string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Whether the Guidely API is reachable right now: a circuit breaker shared by everything the SDK
|
|
40
|
-
* sends in the background (event batches, replay uploads, config refreshes, queued survey answers).
|
|
41
|
-
*
|
|
42
|
-
* Every API answer is reported here. After `threshold` failures in a row (network errors, timeouts,
|
|
43
|
-
* 429, 5xx) the circuit opens for a cooldown that doubles with each further failure (5 s up to
|
|
44
|
-
* 5 min, ±20% jitter so tabs don't retry in step); a `Retry-After` answer sets the cooldown
|
|
45
|
-
* directly. While it is open, background senders wait `waitMs()` instead of sending. Requests the
|
|
46
|
-
* person makes (a messenger message) still go out, and any answer closes the circuit again. The
|
|
47
|
-
* browser's `online` event ends the cooldown early.
|
|
48
|
-
*/
|
|
49
|
-
interface ServerHealthOptions {
|
|
50
|
-
threshold?: number;
|
|
51
|
-
baseMs?: number;
|
|
52
|
-
maxMs?: number;
|
|
53
|
-
now?: () => number;
|
|
54
|
-
random?: () => number;
|
|
55
|
-
}
|
|
56
|
-
declare class ServerHealth {
|
|
57
|
-
private failures;
|
|
58
|
-
private openUntil;
|
|
59
|
-
private lastOkAt;
|
|
60
|
-
private readonly listeners;
|
|
61
|
-
private readonly threshold;
|
|
62
|
-
private readonly baseMs;
|
|
63
|
-
private readonly maxMs;
|
|
64
|
-
private readonly now;
|
|
65
|
-
private readonly random;
|
|
66
|
-
constructor(opts?: ServerHealthOptions);
|
|
67
|
-
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
68
|
-
success(): void;
|
|
69
|
-
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
70
|
-
failure(retryAfterMs?: number | null): void;
|
|
71
|
-
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
72
|
-
waitMs(): number;
|
|
73
|
-
canSend(): boolean;
|
|
74
|
-
/**
|
|
75
|
-
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
76
|
-
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
77
|
-
*/
|
|
78
|
-
get reachable(): boolean;
|
|
79
|
-
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
80
|
-
online(): void;
|
|
81
|
-
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
82
|
-
onRecover(cb: () => void): () => void;
|
|
83
|
-
private notify;
|
|
84
|
-
}
|
|
85
|
-
/** `Retry-After` as milliseconds: delta-seconds or an HTTP date. Null when absent or unparseable. */
|
|
86
|
-
declare function parseRetryAfter(value: string | null | undefined, now?: number): number | null;
|
|
87
|
-
/** Answers that mean the server is down or shedding load (retry later) rather than refusing the request. */
|
|
88
|
-
declare function isServerFailure(status: number): boolean;
|
|
89
|
-
|
|
90
|
-
export { type KeyValueStore as K, ServerHealth as S, KEYS as a, type ServerHealthOptions as b, type StorageKind as c, createStorage as d, isServerFailure as i, memoryStore as m, parseRetryAfter as p, readJson as r, writeJson as w };
|
package/dist/http-Cb6KYYFv.d.cts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { S as ServerHealth } from './health-sbP9JwTE.cjs';
|
|
2
|
-
|
|
3
|
-
interface ApiResponse<T> {
|
|
4
|
-
status: number;
|
|
5
|
-
ok: boolean;
|
|
6
|
-
data: T | null;
|
|
7
|
-
etag: string | null;
|
|
8
|
-
}
|
|
9
|
-
type FetchLike = typeof fetch;
|
|
10
|
-
/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */
|
|
11
|
-
declare class Api {
|
|
12
|
-
private readonly baseUrl;
|
|
13
|
-
private readonly key;
|
|
14
|
-
private readonly fetchImpl?;
|
|
15
|
-
/** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
|
|
16
|
-
readonly health: ServerHealth;
|
|
17
|
-
constructor(baseUrl: string, key: string, fetchImpl?: FetchLike | undefined, health?: ServerHealth);
|
|
18
|
-
/** Feeds an answer (or a failed request, `null`) into {@link health}. */
|
|
19
|
-
private observe;
|
|
20
|
-
url(path: string, params?: Record<string, string | undefined>): string;
|
|
21
|
-
private get fetcher();
|
|
22
|
-
request<T>(method: "GET" | "POST", path: string, options?: {
|
|
23
|
-
body?: unknown;
|
|
24
|
-
etag?: string | null;
|
|
25
|
-
params?: Record<string, string | undefined>;
|
|
26
|
-
keepalive?: boolean;
|
|
27
|
-
timeoutMs?: number;
|
|
28
|
-
headers?: Record<string, string>;
|
|
29
|
-
}): Promise<ApiResponse<T>>;
|
|
30
|
-
get<T>(path: string, options?: {
|
|
31
|
-
etag?: string | null;
|
|
32
|
-
params?: Record<string, string | undefined>;
|
|
33
|
-
headers?: Record<string, string>;
|
|
34
|
-
}): Promise<ApiResponse<T>>;
|
|
35
|
-
post<T>(path: string, body: unknown, options?: {
|
|
36
|
-
params?: Record<string, string | undefined>;
|
|
37
|
-
keepalive?: boolean;
|
|
38
|
-
headers?: Record<string, string>;
|
|
39
|
-
}): Promise<ApiResponse<T>>;
|
|
40
|
-
/** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
|
|
41
|
-
postForm<T>(path: string, form: FormData, options?: {
|
|
42
|
-
params?: Record<string, string | undefined>;
|
|
43
|
-
timeoutMs?: number;
|
|
44
|
-
}): Promise<ApiResponse<T>>;
|
|
45
|
-
/**
|
|
46
|
-
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
47
|
-
* returned instead. Scout replies can take a while, so there is no overall timeout: the response
|
|
48
|
-
* must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
|
|
49
|
-
* otherwise the request is aborted and rejects.
|
|
50
|
-
*/
|
|
51
|
-
stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
52
|
-
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
53
|
-
beacon(path: string, body: unknown): boolean;
|
|
54
|
-
}
|
|
55
|
-
declare const sleep: (ms: number) => Promise<void>;
|
|
56
|
-
/** Exponential backoff helper: 1s → 2s → 4s … capped. */
|
|
57
|
-
declare function backoffMs(attempt: number, baseMs?: number, maxMs?: number): number;
|
|
58
|
-
/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */
|
|
59
|
-
declare function postWithRetry<T>(api: Api, path: string, body: unknown, attempts?: number, baseMs?: number): Promise<ApiResponse<T> | null>;
|
|
60
|
-
|
|
61
|
-
export { Api as A, type FetchLike as F, type ApiResponse as a, backoffMs as b, postWithRetry as p, sleep as s };
|