@guidelytech/sdk-core 0.9.0 → 0.11.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 +15 -4
- package/dist/{capture-Dqd7up2n.d.ts → capture-CgN69Lxq.d.cts} +2 -1
- package/dist/{capture-C3faAYb7.d.cts → capture-Csz721-A.d.ts} +2 -1
- package/dist/capture.cjs +4 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.d.cts +3 -2
- package/dist/capture.d.ts +3 -2
- package/dist/capture.js +4 -6
- package/dist/capture.js.map +1 -1
- package/dist/chunk-JUSM7J5N.js +376 -0
- package/dist/chunk-JUSM7J5N.js.map +1 -0
- package/dist/{chunk-544KSOC3.js → chunk-ZAF2KRJA.js} +142 -22
- package/dist/chunk-ZAF2KRJA.js.map +1 -0
- package/dist/headless.cjs +307 -44
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +44 -3
- package/dist/headless.d.ts +44 -3
- package/dist/headless.js +21 -9
- package/dist/health-sbP9JwTE.d.cts +90 -0
- package/dist/health-sbP9JwTE.d.ts +90 -0
- package/dist/{http-47tiAlOM.d.ts → http-Cb6KYYFv.d.cts} +10 -35
- package/dist/{http-47tiAlOM.d.cts → http-D047Ngxk.d.ts} +10 -35
- package/dist/index.cjs +1352 -500
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -8
- package/dist/index.d.ts +81 -8
- package/dist/index.js +809 -211
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs +70 -9
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +9 -0
- package/dist/recorder.d.ts +9 -0
- package/dist/recorder.js +57 -11
- package/dist/recorder.js.map +1 -1
- package/dist/{types-DnJW_4ck.d.ts → types-BDlebXrm.d.ts} +68 -7
- package/dist/{types-zM5e3loY.d.cts → types-Mtlmlj5C.d.cts} +68 -7
- package/package.json +4 -4
- package/dist/chunk-544KSOC3.js.map +0 -1
- package/dist/chunk-AKNE7D5M.js +0 -160
- package/dist/chunk-AKNE7D5M.js.map +0 -1
- package/dist/chunk-CYAG6ZYX.js +0 -85
- package/dist/chunk-CYAG6ZYX.js.map +0 -1
- package/dist/chunk-GDF6RCHQ.js +0 -209
- package/dist/chunk-GDF6RCHQ.js.map +0 -1
- package/dist/types-DbKQPvFt.d.cts +0 -238
- package/dist/types-DqhNl5Am.d.ts +0 -238
- package/dist/types-RyeRQd9f.d.ts +0 -226
- package/dist/types-d4eOerxi.d.cts +0 -226
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { SdkEvent, PublishedGuide, FrequencyConfig, AutocaptureSettings, NetworkRequest, ProjectTheme, SdkConfigResponse, ChecklistTask, MessengerMessage, MessengerConversation } from '@guidelytech/shared';
|
|
2
|
-
import {
|
|
1
|
+
import { SdkEvent, PublishedGuide, FrequencyConfig, GuideThrottlingSettings, AutocaptureSettings, NetworkRequest, ProjectTheme, SdkConfigResponse, ChecklistTask, MessengerMessage, MessengerConversation } from '@guidelytech/shared';
|
|
2
|
+
import { K as KeyValueStore, S as ServerHealth, c as StorageKind } from './health-sbP9JwTE.cjs';
|
|
3
3
|
import { FlagsClientOptions } from '@guidelytech/flags';
|
|
4
|
+
import { AgentTurnInput } from '@guidelytech/shared/runtime';
|
|
4
5
|
|
|
5
6
|
interface EventQueueOptions {
|
|
6
7
|
store: KeyValueStore;
|
|
@@ -14,6 +15,8 @@ interface EventQueueOptions {
|
|
|
14
15
|
beacon?: (events: SdkEvent[]) => boolean;
|
|
15
16
|
/** Should events be flushed right now? (analytics enabled + consent granted). */
|
|
16
17
|
enabled: () => boolean;
|
|
18
|
+
/** API reachability: sends wait while its circuit is open, and beaconed events are only forgotten while it is reachable. */
|
|
19
|
+
health?: ServerHealth;
|
|
17
20
|
flushIntervalMs?: number;
|
|
18
21
|
maxQueue?: number;
|
|
19
22
|
batchSize?: number;
|
|
@@ -22,8 +25,9 @@ interface EventQueueOptions {
|
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
27
|
* Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by
|
|
25
|
-
* `eventId`,
|
|
26
|
-
*
|
|
28
|
+
* `eventId`, and keeps failed events (also across page loads) until the server takes them:
|
|
29
|
+
* retries back off 1s→60s, wait while the API's circuit is open, and start again as soon as it
|
|
30
|
+
* recovers. Drops the oldest events once the queue exceeds 500.
|
|
27
31
|
*/
|
|
28
32
|
declare class EventQueue {
|
|
29
33
|
private readonly opts;
|
|
@@ -34,11 +38,15 @@ declare class EventQueue {
|
|
|
34
38
|
private inflightIds;
|
|
35
39
|
private attempt;
|
|
36
40
|
private destroyed;
|
|
41
|
+
private offRecover;
|
|
37
42
|
private readonly flushIntervalMs;
|
|
38
43
|
private readonly maxQueue;
|
|
39
44
|
private readonly batchSize;
|
|
40
45
|
private readonly maxAttempts;
|
|
41
46
|
constructor(opts: EventQueueOptions);
|
|
47
|
+
/** The API is back: drop any pending backoff and send what is queued. */
|
|
48
|
+
private retryNow;
|
|
49
|
+
private retryIn;
|
|
42
50
|
get size(): number;
|
|
43
51
|
/** Pending events (read-only copy, for debugging). */
|
|
44
52
|
peek(): SdkEvent[];
|
|
@@ -79,8 +87,25 @@ declare function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalSta
|
|
|
79
87
|
declare function isExclusiveGuide(guide: PublishedGuide): boolean;
|
|
80
88
|
/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */
|
|
81
89
|
declare function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean;
|
|
82
|
-
/**
|
|
83
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Scheduling window: the guide's `schedule` (any kind) and, for surveys, `survey.startAt` / `endAt`.
|
|
92
|
+
* Unparsable dates are ignored.
|
|
93
|
+
*/
|
|
94
|
+
declare function isWithinSchedule(guide: Pick<PublishedGuide, "config">, now?: number): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Does showing this guide automatically count toward (and get limited by) the project's guide
|
|
97
|
+
* throttling? Only exclusive guides (flows, modals, tooltips, announcements, surveys) interrupt;
|
|
98
|
+
* banners, checklists and hotspot-only guides never count, nor does a guide set to ignore throttling.
|
|
99
|
+
* Guides started by the visitor or the app (`startGuide`, buttons, Help) are never throttled.
|
|
100
|
+
*/
|
|
101
|
+
declare function countsTowardThrottle(guide: PublishedGuide): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Pure throttling check. `launches` = ISO times of this visitor's earlier automatic launches of
|
|
104
|
+
* counted guides (any order). Disabled or missing settings never throttle.
|
|
105
|
+
*/
|
|
106
|
+
declare function isThrottleEligible(settings: GuideThrottlingSettings | null | undefined, launches: readonly string[], now?: number): boolean;
|
|
107
|
+
/** Launch times worth keeping: those still inside the window or the minimum gap (at most 100). */
|
|
108
|
+
declare function pruneLaunches(launches: readonly string[], settings: GuideThrottlingSettings | null | undefined, now?: number): string[];
|
|
84
109
|
/**
|
|
85
110
|
* A checklist's dismissal after merging server state. A local dismissal normally wins (it may not have
|
|
86
111
|
* synced yet), but when the server reports a reset this device hasn't applied, the server's value wins:
|
|
@@ -95,6 +120,15 @@ declare function dismissedAfterMerge(local: {
|
|
|
95
120
|
dismissResetAt?: string | null;
|
|
96
121
|
}): string | null;
|
|
97
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Agent Analytics in the browser: turns of conversations with the app's own AI agent, sent to
|
|
125
|
+
* `POST /api/v1/sdk/agent-events`. Turns are filled in with the visitor's ids and session, passed through
|
|
126
|
+
* the app's `agents.redact` callback, and kept in a persisted outbox until the server accepts them
|
|
127
|
+
* (retried with backoff while it is unreachable; the server ignores turns it already has).
|
|
128
|
+
*/
|
|
129
|
+
/** Last say over each turn before it leaves the browser: return a changed copy, or null to drop it. */
|
|
130
|
+
type AgentTurnRedactor = (turn: AgentTurnInput) => AgentTurnInput | null | undefined;
|
|
131
|
+
|
|
98
132
|
type GuidelyEnvironment = "development" | "staging" | "production";
|
|
99
133
|
type ConsentState = "granted" | "denied" | "pending";
|
|
100
134
|
interface GuidelyAccount {
|
|
@@ -134,6 +168,17 @@ interface GuidelyOptions {
|
|
|
134
168
|
autocapture?: boolean | Partial<AutocaptureSettings>;
|
|
135
169
|
/** App version or commit SHA (≤100 chars), sent with every `exception` to pick the matching uploaded source maps. */
|
|
136
170
|
release?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Version of the host application (e.g. an Electron app's `app.getVersion()`), used by **App version**
|
|
173
|
+
* targeting rules and as the `appVersion` flag attribute.
|
|
174
|
+
*/
|
|
175
|
+
appVersion?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Maps the browser URL to the page URL Guidely sees (targeting, URL patterns, page analytics). By
|
|
178
|
+
* default http(s) URLs are used as they are and `file:` / custom-protocol URLs (Electron, desktop
|
|
179
|
+
* shells) are made app-relative with `normalizeAppUrl`: `file:///…/index.html#/settings` → `file://app/settings`.
|
|
180
|
+
*/
|
|
181
|
+
pageUrl?: (href: string) => string;
|
|
137
182
|
/** `false` stops uncaught errors / unhandled rejections from being reported (`captureException` still works). */
|
|
138
183
|
captureErrors?: boolean;
|
|
139
184
|
/**
|
|
@@ -151,6 +196,11 @@ interface GuidelyOptions {
|
|
|
151
196
|
maskNetworkRequest?: (request: NetworkRequest) => NetworkRequest | null;
|
|
152
197
|
};
|
|
153
198
|
onError?: (error: Error) => void;
|
|
199
|
+
/**
|
|
200
|
+
* Language for guides and the resource center (BCP 47, e.g. `fr` or `pt-BR`). Defaults to the
|
|
201
|
+
* user's `language` / `locale` trait, then the browser language; see also `setLocale()`.
|
|
202
|
+
*/
|
|
203
|
+
locale?: string;
|
|
154
204
|
/** Overrides for the project theme (colors, radius, font). */
|
|
155
205
|
theme?: Partial<ProjectTheme>;
|
|
156
206
|
/** Identify this user as soon as the client initialises. */
|
|
@@ -166,6 +216,13 @@ interface GuidelyOptions {
|
|
|
166
216
|
* `{ pollingMode: "stream" }`. Identity, page and device attributes are maintained for you.
|
|
167
217
|
*/
|
|
168
218
|
flags?: false | Partial<FlagsClientOptions>;
|
|
219
|
+
/**
|
|
220
|
+
* Agent Analytics (`agentConversation()`): `redact` gets each turn before it leaves the browser and
|
|
221
|
+
* returns a changed copy (e.g. with personal details removed from `text`), or null to drop it.
|
|
222
|
+
*/
|
|
223
|
+
agents?: {
|
|
224
|
+
redact?: AgentTurnRedactor;
|
|
225
|
+
};
|
|
169
226
|
}
|
|
170
227
|
interface ChecklistProgressState {
|
|
171
228
|
completedTaskIds: string[];
|
|
@@ -181,6 +238,10 @@ interface PersistedState {
|
|
|
181
238
|
surveysSubmitted: string[];
|
|
182
239
|
audienceIds: string[];
|
|
183
240
|
firstSeenAt?: string;
|
|
241
|
+
/** ISO times of automatic launches that count toward guide throttling (pruned to the window). */
|
|
242
|
+
throttle?: string[];
|
|
243
|
+
/** When the visitor last opened the resource center's "What's new" (unread badge). */
|
|
244
|
+
announcementsSeenAt?: string;
|
|
184
245
|
}
|
|
185
246
|
interface SessionState {
|
|
186
247
|
id: string;
|
|
@@ -250,4 +311,4 @@ interface GuidelyEvents {
|
|
|
250
311
|
}
|
|
251
312
|
type GuidelyEventName = keyof GuidelyEvents;
|
|
252
313
|
|
|
253
|
-
export { type ChecklistProgressState as C, EventQueue as E, type GuideLocalState as G, type PersistedState as P, type SessionState as S, type ConsentState as a, type EventQueueOptions as b, type GuidelyAccount as c, type GuidelyEnvironment as d, type GuidelyUser as e, areMutuallyExclusive as f,
|
|
314
|
+
export { type AgentTurnRedactor as A, type ChecklistProgressState as C, EventQueue as E, type GuideLocalState as G, type PersistedState as P, type SessionState as S, type ConsentState as a, type EventQueueOptions as b, type GuidelyAccount as c, type GuidelyEnvironment as d, type GuidelyUser as e, areMutuallyExclusive as f, countsTowardThrottle as g, dismissedAfterMerge as h, isExclusiveGuide as i, isFrequencyEligible as j, isThrottleEligible as k, isWithinSchedule as l, type GuidelyOptions as m, type GuidelyEvents as n, type GuideStepInfo as o, pruneLaunches as p, type GuidelyEventName as q };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guidelytech/sdk-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Framework-independent Guidely browser SDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"@rrweb/record": "^2.1.7",
|
|
39
39
|
"@rrweb/rrweb-plugin-console-record": "^2.1.7",
|
|
40
40
|
"@rrweb/types": "^2.1.7",
|
|
41
|
-
"@guidelytech/guide-runtime": "0.
|
|
42
|
-
"@guidelytech/
|
|
43
|
-
"@guidelytech/
|
|
41
|
+
"@guidelytech/guide-runtime": "0.11.0",
|
|
42
|
+
"@guidelytech/shared": "0.11.0",
|
|
43
|
+
"@guidelytech/flags": "0.11.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@eslint/js": "^9.39.5",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/queue.ts","../src/emitter.ts","../src/frequency.ts"],"sourcesContent":["import type { SdkEvent } from \"@guidelytech/shared\";\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 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`, retries with exponential backoff (1s→30s, 5 attempts) and keeps failed events for\n * the next flush. Drops the oldest events once the queue exceeds 200.\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 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 ?? 200;\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 }\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 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 this.attempt += 1;\n if (this.attempt >= this.maxAttempts) {\n // Give up for now; events stay queued until the next flush trigger (navigation, timer, unload).\n this.attempt = 0;\n return;\n }\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoffMs(this.attempt - 1));\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 if (this.opts.beacon(batch)) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.persist();\n }\n }\n\n clear(): void {\n this.events = [];\n this.persist();\n }\n\n destroy(): void {\n this.destroyed = true;\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","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, 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/** Survey scheduling window. */\nexport function isWithinSchedule(guide: PublishedGuide, now: number = Date.now()): boolean {\n const s = guide.config.survey;\n if (!s) return true;\n if (s.startAt && now < Date.parse(s.startAt)) return false;\n if (s.endAt && now > Date.parse(s.endAt)) return false;\n return true;\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":"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","names":[]}
|
package/dist/chunk-AKNE7D5M.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
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/http.ts
|
|
6
|
-
var Api = class {
|
|
7
|
-
constructor(baseUrl, key, fetchImpl) {
|
|
8
|
-
__publicField(this, "baseUrl", baseUrl);
|
|
9
|
-
__publicField(this, "key", key);
|
|
10
|
-
__publicField(this, "fetchImpl", fetchImpl);
|
|
11
|
-
}
|
|
12
|
-
url(path, params = {}) {
|
|
13
|
-
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
14
|
-
return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
|
|
15
|
-
}
|
|
16
|
-
get fetcher() {
|
|
17
|
-
if (this.fetchImpl) return this.fetchImpl;
|
|
18
|
-
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
19
|
-
}
|
|
20
|
-
async request(method, path, options = {}) {
|
|
21
|
-
const f = this.fetcher;
|
|
22
|
-
if (!f) throw new Error("fetch is not available");
|
|
23
|
-
const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
24
|
-
const form = typeof FormData !== "undefined" && options.body instanceof FormData;
|
|
25
|
-
if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
|
|
26
|
-
if (options.etag) headers["If-None-Match"] = options.etag;
|
|
27
|
-
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
28
|
-
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
29
|
-
try {
|
|
30
|
-
const res = await f(this.url(path, options.params), {
|
|
31
|
-
method,
|
|
32
|
-
headers,
|
|
33
|
-
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
34
|
-
keepalive: options.keepalive,
|
|
35
|
-
signal: ac?.signal,
|
|
36
|
-
credentials: "omit",
|
|
37
|
-
mode: "cors"
|
|
38
|
-
});
|
|
39
|
-
let data = null;
|
|
40
|
-
if (res.status !== 304 && res.status !== 204) {
|
|
41
|
-
try {
|
|
42
|
-
data = await res.json();
|
|
43
|
-
} catch {
|
|
44
|
-
data = null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
48
|
-
} finally {
|
|
49
|
-
if (timer) clearTimeout(timer);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
get(path, options) {
|
|
53
|
-
return this.request("GET", path, options);
|
|
54
|
-
}
|
|
55
|
-
post(path, body, options) {
|
|
56
|
-
return this.request("POST", path, { body, ...options });
|
|
57
|
-
}
|
|
58
|
-
/** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
|
|
59
|
-
postForm(path, form, options) {
|
|
60
|
-
return this.request("POST", path, { body: form, ...options });
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
64
|
-
* returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
|
|
65
|
-
*/
|
|
66
|
-
async stream(path, body, onEvent, headers = {}) {
|
|
67
|
-
const f = this.fetcher;
|
|
68
|
-
if (!f) throw new Error("fetch is not available");
|
|
69
|
-
const res = await f(this.url(path), {
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
72
|
-
body: JSON.stringify(body),
|
|
73
|
-
credentials: "omit",
|
|
74
|
-
mode: "cors"
|
|
75
|
-
});
|
|
76
|
-
const type = res.headers?.get?.("content-type") ?? "";
|
|
77
|
-
if (!type.includes("text/event-stream") || !res.body) {
|
|
78
|
-
let data = null;
|
|
79
|
-
try {
|
|
80
|
-
data = await res.json();
|
|
81
|
-
} catch {
|
|
82
|
-
data = null;
|
|
83
|
-
}
|
|
84
|
-
return { status: res.status, ok: res.ok, data, etag: null };
|
|
85
|
-
}
|
|
86
|
-
await readEventStream(res.body, onEvent);
|
|
87
|
-
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
88
|
-
}
|
|
89
|
-
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
90
|
-
beacon(path, body) {
|
|
91
|
-
try {
|
|
92
|
-
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
|
|
93
|
-
const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
|
|
94
|
-
return navigator.sendBeacon(this.url(path), blob);
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
101
|
-
function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
|
|
102
|
-
return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
|
|
103
|
-
}
|
|
104
|
-
async function withRetry(send, attempts = 3, baseMs = 500) {
|
|
105
|
-
for (let i = 0; i < attempts; i++) {
|
|
106
|
-
try {
|
|
107
|
-
const res = await send();
|
|
108
|
-
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
109
|
-
} catch {
|
|
110
|
-
}
|
|
111
|
-
if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
116
|
-
return withRetry(() => api.post(path, body), attempts, baseMs);
|
|
117
|
-
}
|
|
118
|
-
async function readEventStream(body, onEvent) {
|
|
119
|
-
const reader = body.getReader();
|
|
120
|
-
const decoder = new TextDecoder();
|
|
121
|
-
let buffer = "";
|
|
122
|
-
const dispatch = (raw) => {
|
|
123
|
-
let event = "message";
|
|
124
|
-
const data = [];
|
|
125
|
-
for (const line of raw.split("\n")) {
|
|
126
|
-
if (!line || line.startsWith(":")) continue;
|
|
127
|
-
const i = line.indexOf(":");
|
|
128
|
-
const field = i < 0 ? line : line.slice(0, i);
|
|
129
|
-
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
130
|
-
if (field === "event") event = value;
|
|
131
|
-
else if (field === "data") data.push(value);
|
|
132
|
-
}
|
|
133
|
-
if (!data.length) return;
|
|
134
|
-
try {
|
|
135
|
-
onEvent(event, JSON.parse(data.join("\n")));
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
for (; ; ) {
|
|
140
|
-
const { done, value } = await reader.read();
|
|
141
|
-
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
142
|
-
let sep;
|
|
143
|
-
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
144
|
-
dispatch(buffer.slice(0, sep));
|
|
145
|
-
buffer = buffer.slice(sep + 2);
|
|
146
|
-
}
|
|
147
|
-
if (done) break;
|
|
148
|
-
}
|
|
149
|
-
if (buffer.trim()) dispatch(buffer);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export {
|
|
153
|
-
__publicField,
|
|
154
|
-
Api,
|
|
155
|
-
sleep,
|
|
156
|
-
backoffMs,
|
|
157
|
-
withRetry,
|
|
158
|
-
postWithRetry
|
|
159
|
-
};
|
|
160
|
-
//# sourceMappingURL=chunk-AKNE7D5M.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http.ts"],"sourcesContent":["export 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/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\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 const 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 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. No timeout: Scout replies can take a while and the server keeps the stream alive.\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 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 credentials: \"omit\",\n mode: \"cors\",\n });\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 await readEventStream(res.body, onEvent);\n return { status: res.status, ok: res.ok, data: null, etag: null };\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). */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => 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) 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"],"mappings":"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","names":[]}
|
package/dist/chunk-CYAG6ZYX.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// src/storage.ts
|
|
2
|
-
function memoryStore() {
|
|
3
|
-
const map = /* @__PURE__ */ new Map();
|
|
4
|
-
return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
|
|
5
|
-
}
|
|
6
|
-
function webStore(area) {
|
|
7
|
-
try {
|
|
8
|
-
if (typeof window === "undefined") return null;
|
|
9
|
-
const s = window[area];
|
|
10
|
-
if (!s) return null;
|
|
11
|
-
const probe = "__guidely_probe__";
|
|
12
|
-
s.setItem(probe, "1");
|
|
13
|
-
s.removeItem(probe);
|
|
14
|
-
return {
|
|
15
|
-
get: (k) => {
|
|
16
|
-
try {
|
|
17
|
-
return s.getItem(k);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
set: (k, v) => {
|
|
23
|
-
try {
|
|
24
|
-
s.setItem(k, v);
|
|
25
|
-
} catch {
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
remove: (k) => {
|
|
29
|
-
try {
|
|
30
|
-
s.removeItem(k);
|
|
31
|
-
} catch {
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function createStorage(kind = "local") {
|
|
40
|
-
if (kind === "memory") return memoryStore();
|
|
41
|
-
return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
|
|
42
|
-
}
|
|
43
|
-
function readJson(store, key, fallback) {
|
|
44
|
-
const raw = store.get(key);
|
|
45
|
-
if (raw == null) return fallback;
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(raw);
|
|
48
|
-
} catch {
|
|
49
|
-
return fallback;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function writeJson(store, key, value) {
|
|
53
|
-
try {
|
|
54
|
-
store.set(key, JSON.stringify(value));
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
var KEYS = {
|
|
59
|
-
anon: "guidely:anon",
|
|
60
|
-
seen: "guidely:seen",
|
|
61
|
-
config: (key) => `guidely:config:${key}`,
|
|
62
|
-
etag: (key) => `guidely:etag:${key}`,
|
|
63
|
-
state: (key) => `guidely:state:${key}`,
|
|
64
|
-
user: (key) => `guidely:user:${key}`,
|
|
65
|
-
queue: (key) => `guidely:queue:${key}`,
|
|
66
|
-
session: "guidely:session",
|
|
67
|
-
active: "guidely:active",
|
|
68
|
-
authoring: "guidely:authoring",
|
|
69
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
70
|
-
capture: "guidely:capture",
|
|
71
|
-
capturePending: "guidely:capture:pending",
|
|
72
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
73
|
-
conversations: (key) => `guidely:conversations:${key}`,
|
|
74
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
75
|
-
newsSeen: (key) => `guidely:news:${key}`
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export {
|
|
79
|
-
memoryStore,
|
|
80
|
-
createStorage,
|
|
81
|
-
readJson,
|
|
82
|
-
writeJson,
|
|
83
|
-
KEYS
|
|
84
|
-
};
|
|
85
|
-
//# sourceMappingURL=chunk-CYAG6ZYX.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/storage.ts"],"sourcesContent":["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 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} as const;\n"],"mappings":";AASO,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,EAC5C,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;AAChD;","names":[]}
|