@layers/client 0.1.1-alpha.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,617 +1,3 @@
1
- //#region src/index.ts
2
- const SDK_VERSION = "0.0.0";
3
- const DEFAULT_BASE_URL = "https://in.layers.com";
4
- const SYSTEM_EVENTS = new Set(["consent_updated", "att_status_changed"]);
5
- const DEFAULT_CONSENT = {
6
- advertising: true,
7
- analytics: true
8
- };
9
- const DEFAULT_QUEUE_CONFIG = {
10
- flushIntervalMs: 1e4,
11
- maxQueueSize: 200,
12
- maxItemAgeMs: 5 * 6e4,
13
- requestTimeoutMs: 1e4,
14
- maxRetries: 5,
15
- baseRetryDelayMs: 1e3,
16
- maxRetryDelayMs: 3e4
17
- };
18
- function now() {
19
- return Date.now();
20
- }
21
- function generateId(prefix) {
22
- return `${prefix}_${Math.random().toString(36).slice(2)}_${now()}`;
23
- }
24
- var LayersClient = class {
25
- config;
26
- remoteConfig = null;
27
- remoteConfigMeta = {};
28
- eventQueue = [];
29
- isOnline = true;
30
- consentState = { ...DEFAULT_CONSENT };
31
- sessionId;
32
- deviceInfo = {};
33
- queueTimer = null;
34
- queueConfig;
35
- queueStorage;
36
- rateLimitState = {
37
- global: {},
38
- perEvent: /* @__PURE__ */ new Map()
39
- };
40
- logLevel() {
41
- if (typeof process !== "undefined" && process.env?.LOG_LEVEL) return process.env.LOG_LEVEL.toLowerCase();
42
- if (this.config.enableDebug) return "debug";
43
- return "info";
44
- }
45
- shouldLog(level) {
46
- const currentLevel = this.logLevel();
47
- const levels = [
48
- "error",
49
- "warn",
50
- "info",
51
- "debug",
52
- "trace"
53
- ];
54
- return levels.indexOf(currentLevel) >= levels.indexOf(level);
55
- }
56
- constructor(config) {
57
- this.queueConfig = {
58
- ...DEFAULT_QUEUE_CONFIG,
59
- ...config.queueOptions || {}
60
- };
61
- this.queueStorage = config.queueStorage ?? createDefaultQueueStorage(config.appId);
62
- const resolvedAppUserId = config.appUserId;
63
- this.config = {
64
- ...config,
65
- baseUrl: (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, ""),
66
- appUserId: resolvedAppUserId
67
- };
68
- this.sessionId = this.generateSessionId();
69
- this.resetRateLimitState();
70
- this.initializeDeviceInfo();
71
- }
72
- generateSessionId() {
73
- return generateId("sess");
74
- }
75
- initializeDeviceInfo() {
76
- this.deviceInfo = {
77
- platform: "react-native",
78
- os_version: "unknown",
79
- app_version: "unknown",
80
- locale: "en-US",
81
- device_model: "unknown"
82
- };
83
- }
84
- async init() {
85
- await this.hydrateQueue();
86
- await this.fetchRemoteConfig();
87
- this.setupNetworkListener();
88
- this.scheduleQueueFlush(this.eventQueue.length > 0);
89
- }
90
- async fetchRemoteConfig(force = false) {
91
- const expired = !this.remoteConfigMeta.expiresAt || this.remoteConfigMeta.expiresAt < now();
92
- if (!force && this.remoteConfig && !expired) return;
93
- try {
94
- const response = await this.makeRequest("/config", { method: "GET" });
95
- if (!response?.config) return;
96
- this.remoteConfig = response.config;
97
- const ttlMs = (response.cache_ttl ?? 300) * 1e3;
98
- this.remoteConfigMeta = {
99
- version: response.version,
100
- expiresAt: now() + ttlMs,
101
- ...this.extractRemoteConfigSettings(response)
102
- };
103
- this.resetRateLimitState();
104
- } catch (error) {
105
- if (this.shouldLog("debug")) console.warn("[Layers] Failed to fetch remote config:", error);
106
- }
107
- }
108
- extractRemoteConfigSettings(envelope) {
109
- const meta = {};
110
- const config = envelope.config ?? {};
111
- if (config.privacy) {
112
- if (typeof config.privacy.analytics_enabled === "boolean") meta.analyticsEnabled = config.privacy.analytics_enabled;
113
- if (typeof config.privacy.advertising_enabled === "boolean") meta.advertisingEnabled = config.privacy.advertising_enabled;
114
- if (config.privacy.killswitches) {
115
- const killswitches = new Set(config.privacy.killswitches);
116
- if (killswitches.has("analytics")) meta.analyticsEnabled = false;
117
- if (killswitches.has("advertising")) meta.advertisingEnabled = false;
118
- }
119
- }
120
- if (config.events) {
121
- if (Array.isArray(config.events.allowlist)) meta.eventAllowlist = new Set(config.events.allowlist);
122
- if (Array.isArray(config.events.denylist)) meta.eventDenylist = new Set(config.events.denylist);
123
- const samplingEntries = {};
124
- if (config.events.sampling) for (const [eventName, value] of Object.entries(config.events.sampling)) {
125
- const numeric = Number(value);
126
- if (Number.isFinite(numeric)) samplingEntries[eventName] = Math.max(0, Math.min(1, numeric));
127
- }
128
- if (typeof config.events.sampling_rate === "number") samplingEntries["*"] = Math.max(0, Math.min(1, config.events.sampling_rate));
129
- if (Object.keys(samplingEntries).length > 0) meta.samplingRates = samplingEntries;
130
- const rateLimitConfig = config.events.rate_limit;
131
- if (rateLimitConfig) {
132
- const globalRule = this.normalizeRateLimitRule(rateLimitConfig);
133
- let perEventRules;
134
- if (rateLimitConfig.per_event) {
135
- const entries = Object.entries(rateLimitConfig.per_event);
136
- if (entries.length > 0) {
137
- perEventRules = /* @__PURE__ */ new Map();
138
- for (const [eventName, ruleConfig] of entries) {
139
- const rule = this.normalizeRateLimitRule(ruleConfig);
140
- if (rule) perEventRules.set(eventName, rule);
141
- }
142
- if (perEventRules.size === 0) perEventRules = void 0;
143
- }
144
- }
145
- const rateLimits = {};
146
- if (globalRule) rateLimits.global = globalRule;
147
- if (perEventRules) rateLimits.perEvent = perEventRules;
148
- if (rateLimits.global || rateLimits.perEvent) meta.rateLimits = rateLimits;
149
- }
150
- }
151
- return meta;
152
- }
153
- setupNetworkListener() {
154
- if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
155
- window.addEventListener("online", () => {
156
- this.setOnlineState(true);
157
- });
158
- window.addEventListener("offline", () => {
159
- this.setOnlineState(false);
160
- });
161
- }
162
- }
163
- setOnlineState(isOnline) {
164
- if (this.isOnline === isOnline) return;
165
- this.isOnline = isOnline;
166
- if (isOnline) this.scheduleQueueFlush(true);
167
- }
168
- async hydrateQueue() {
169
- if (!this.queueStorage) return;
170
- try {
171
- const snapshot = await this.queueStorage.load();
172
- if (!snapshot || !Array.isArray(snapshot.items) || snapshot.items.length === 0) {
173
- if (snapshot) await this.queueStorage.clear();
174
- return;
175
- }
176
- const nowTs = now();
177
- const maxAge = this.queueConfig.maxItemAgeMs;
178
- const hydrated = [];
179
- for (const item of snapshot.items) if (item && typeof item.endpoint === "string") {
180
- const queuedAt = typeof item.queuedAt === "number" ? item.queuedAt : nowTs;
181
- if (nowTs - queuedAt <= maxAge) hydrated.push({
182
- endpoint: item.endpoint,
183
- data: item.data,
184
- attempts: typeof item.attempts === "number" ? item.attempts : 0,
185
- queuedAt,
186
- nextAttemptAt: typeof item.nextAttemptAt === "number" ? item.nextAttemptAt : nowTs,
187
- requestId: typeof item.requestId === "string" ? item.requestId : generateId("req"),
188
- retryable: item.retryable !== false
189
- });
190
- }
191
- if (hydrated.length === 0) {
192
- await this.queueStorage.clear();
193
- this.eventQueue = [];
194
- return;
195
- }
196
- const limited = hydrated.slice(0, this.queueConfig.maxQueueSize);
197
- const trimmed = limited.length !== hydrated.length;
198
- this.eventQueue = limited;
199
- if (trimmed || limited.length !== snapshot.items.length) await this.persistQueue();
200
- } catch (error) {
201
- if (this.shouldLog("debug")) console.warn("[Layers] Failed to hydrate queue", error);
202
- }
203
- }
204
- async persistQueue() {
205
- if (!this.queueStorage) return;
206
- const snapshot = {
207
- version: 1,
208
- items: this.eventQueue.map((item) => ({
209
- endpoint: item.endpoint,
210
- data: item.data,
211
- attempts: item.attempts,
212
- queuedAt: item.queuedAt,
213
- nextAttemptAt: item.nextAttemptAt,
214
- requestId: item.requestId,
215
- retryable: item.retryable
216
- }))
217
- };
218
- try {
219
- if (snapshot.items.length === 0) await this.queueStorage.clear();
220
- else await this.queueStorage.save(snapshot);
221
- } catch (error) {
222
- if (this.shouldLog("debug")) console.warn("[Layers] Failed to persist queue", error);
223
- }
224
- }
225
- async track(eventName, properties) {
226
- if (!this.shouldProcessEvent(eventName)) {
227
- if (this.shouldLog("debug")) console.log(`[Layers] Event '${eventName}' dropped due to consent or config gating`);
228
- return;
229
- }
230
- if (!this.passesSampling(eventName)) {
231
- if (this.shouldLog("debug")) console.log(`[Layers] Event '${eventName}' sampled out`);
232
- return;
233
- }
234
- const batchPayload = {
235
- events: [{
236
- event: eventName,
237
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
238
- event_id: this.generateEventId(),
239
- app_id: this.config.appId,
240
- session_id: this.sessionId,
241
- environment: this.config.environment,
242
- platform: this.deviceInfo.platform,
243
- os_version: this.deviceInfo.os_version,
244
- app_version: this.deviceInfo.app_version,
245
- device_model: this.deviceInfo.device_model,
246
- locale: this.deviceInfo.locale,
247
- ...this.deviceInfo,
248
- properties: properties || {},
249
- ...this.resolveAppUserId() && { app_user_id: this.resolveAppUserId() }
250
- }],
251
- batch_id: this.generateBatchId(),
252
- sent_at: (/* @__PURE__ */ new Date()).toISOString()
253
- };
254
- const appUserId = this.resolveAppUserId();
255
- console.log(`[Layers] Tracking event: ${eventName}${appUserId ? ` (user: ${appUserId})` : " (no user)"}`);
256
- await this.enqueue("/events", batchPayload);
257
- }
258
- shouldProcessEvent(eventName) {
259
- if (SYSTEM_EVENTS.has(eventName)) return true;
260
- if (!this.isAnalyticsEnabled()) return false;
261
- if (this.remoteConfigMeta.eventAllowlist && !this.remoteConfigMeta.eventAllowlist.has(eventName)) return false;
262
- if (this.remoteConfigMeta.eventDenylist && this.remoteConfigMeta.eventDenylist.has(eventName)) return false;
263
- if (!this.passesRateLimit(eventName)) return false;
264
- return true;
265
- }
266
- passesSampling(eventName) {
267
- const sampling = this.remoteConfigMeta.samplingRates;
268
- if (!sampling) return true;
269
- const rate = sampling[eventName] ?? sampling["*"];
270
- if (typeof rate !== "number") return true;
271
- return Math.random() <= Math.max(0, Math.min(1, rate));
272
- }
273
- resolveAppUserId() {
274
- return this.config.appUserId;
275
- }
276
- async screen(screenName, properties) {
277
- await this.track("screen_view", {
278
- screen_name: screenName,
279
- ...properties
280
- });
281
- }
282
- async setUserProperties(properties) {
283
- if (!this.isAnalyticsEnabled()) return;
284
- const payload = {
285
- app_id: this.config.appId,
286
- properties,
287
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
288
- ...this.resolveAppUserId() && { app_user_id: this.resolveAppUserId() }
289
- };
290
- await this.enqueue("/users/properties", payload);
291
- }
292
- isAnalyticsEnabled() {
293
- if (this.remoteConfigMeta.analyticsEnabled === false) return false;
294
- return this.consentState.analytics !== false;
295
- }
296
- isAdvertisingEnabled() {
297
- if (this.remoteConfigMeta.advertisingEnabled === false) return false;
298
- return this.consentState.advertising !== false;
299
- }
300
- async setConsent(consent) {
301
- this.consentState = {
302
- advertising: typeof consent.advertising === "boolean" ? consent.advertising : this.consentState.advertising,
303
- analytics: typeof consent.analytics === "boolean" ? consent.analytics : this.consentState.analytics
304
- };
305
- const payload = {
306
- app_id: this.config.appId,
307
- consent: { ...this.consentState },
308
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
309
- ...this.resolveAppUserId() && { app_user_id: this.resolveAppUserId() }
310
- };
311
- await this.enqueue("/consent", payload);
312
- }
313
- async enqueue(endpoint, data, options) {
314
- const retryable = options?.retryable !== false;
315
- if (this.eventQueue.length >= this.queueConfig.maxQueueSize) this.eventQueue.shift();
316
- this.eventQueue.push({
317
- endpoint,
318
- data,
319
- attempts: 0,
320
- queuedAt: now(),
321
- nextAttemptAt: now(),
322
- requestId: generateId("req"),
323
- retryable
324
- });
325
- if (this.isOnline) this.scheduleQueueFlush(true);
326
- else this.scheduleQueueFlush();
327
- this.persistQueue();
328
- }
329
- scheduleQueueFlush(immediate = false) {
330
- if (this.queueTimer) {
331
- clearTimeout(this.queueTimer);
332
- this.queueTimer = null;
333
- }
334
- const delay = immediate ? 0 : this.queueConfig.flushIntervalMs;
335
- this.queueTimer = setTimeout(() => {
336
- this.queueTimer = null;
337
- this.processQueue();
338
- }, delay);
339
- }
340
- async processQueue() {
341
- if (!this.isOnline || this.eventQueue.length === 0) {
342
- this.scheduleQueueFlush();
343
- return;
344
- }
345
- this.dropExpiredQueueItems();
346
- const pending = [];
347
- for (const item of this.eventQueue) if (item.nextAttemptAt > now()) pending.push(item);
348
- else {
349
- let shouldRequeue = false;
350
- try {
351
- await this.makeRequest(item.endpoint, {
352
- method: "POST",
353
- body: JSON.stringify(item.data)
354
- }, item.requestId);
355
- } catch (err) {
356
- if (!item.retryable) {
357
- if (this.shouldLog("debug")) console.warn("[Layers] Dropping non-retryable item", {
358
- endpoint: item.endpoint,
359
- error: err
360
- });
361
- } else {
362
- item.attempts += 1;
363
- if (item.attempts > this.queueConfig.maxRetries) {
364
- if (this.shouldLog("debug")) console.warn("[Layers] Dropping item after max retries", {
365
- endpoint: item.endpoint,
366
- error: err
367
- });
368
- } else {
369
- item.nextAttemptAt = now() + this.getBackoffDelay(item.attempts);
370
- shouldRequeue = true;
371
- if (this.shouldLog("debug")) console.warn("[Layers] Retry scheduled", {
372
- endpoint: item.endpoint,
373
- nextAttemptAt: item.nextAttemptAt,
374
- error: err
375
- });
376
- }
377
- }
378
- }
379
- if (shouldRequeue) pending.push(item);
380
- }
381
- this.eventQueue = pending;
382
- await this.persistQueue();
383
- this.scheduleQueueFlush(this.eventQueue.length > 0);
384
- }
385
- dropExpiredQueueItems() {
386
- const cutoff = now() - this.queueConfig.maxItemAgeMs;
387
- if (this.eventQueue.length === 0) return;
388
- const retained = this.eventQueue.filter((item) => item.queuedAt >= cutoff);
389
- if (this.shouldLog("debug") && retained.length !== this.eventQueue.length) console.warn("[Layers] Dropped stale queue items", { dropped: this.eventQueue.length - retained.length });
390
- this.eventQueue = retained;
391
- this.persistQueue();
392
- }
393
- getBackoffDelay(attempt) {
394
- const jitter = Math.random() * 250;
395
- const delay = this.queueConfig.baseRetryDelayMs * 2 ** (attempt - 1);
396
- return Math.min(delay + jitter, this.queueConfig.maxRetryDelayMs);
397
- }
398
- resetRateLimitState() {
399
- this.rateLimitState = {
400
- global: {},
401
- perEvent: /* @__PURE__ */ new Map()
402
- };
403
- }
404
- getEventRateCounter(eventName) {
405
- let counter = this.rateLimitState.perEvent.get(eventName);
406
- if (!counter) {
407
- counter = {};
408
- this.rateLimitState.perEvent.set(eventName, counter);
409
- }
410
- return counter;
411
- }
412
- normalizeRateLimitRule(input) {
413
- if (!input) return;
414
- const rule = {};
415
- if (typeof input.per_minute === "number" && input.per_minute > 0) rule.perMinute = Math.floor(input.per_minute);
416
- if (typeof input.per_hour === "number" && input.per_hour > 0) rule.perHour = Math.floor(input.per_hour);
417
- return Object.keys(rule).length > 0 ? rule : void 0;
418
- }
419
- passesRateLimit(eventName) {
420
- if (SYSTEM_EVENTS.has(eventName)) return true;
421
- const { rateLimits } = this.remoteConfigMeta;
422
- if (!rateLimits) return true;
423
- const timestamp = now();
424
- const perEventRules = rateLimits.perEvent;
425
- const eventRule = perEventRules?.get(eventName) ?? perEventRules?.get("*");
426
- if (eventRule && !this.canConsumeRateLimit(eventRule, this.getEventRateCounter(eventName), timestamp)) {
427
- if (this.shouldLog("debug")) console.warn("[Layers] Rate limit reached for event", { event: eventName });
428
- return false;
429
- }
430
- if (rateLimits.global && !this.canConsumeRateLimit(rateLimits.global, this.rateLimitState.global, timestamp)) {
431
- if (this.shouldLog("debug")) console.warn("[Layers] Global rate limit reached", { event: eventName });
432
- return false;
433
- }
434
- if (eventRule) this.recordRateLimit(eventRule, this.getEventRateCounter(eventName), timestamp);
435
- if (rateLimits.global) this.recordRateLimit(rateLimits.global, this.rateLimitState.global, timestamp);
436
- return true;
437
- }
438
- canConsumeRateLimit(rule, counter, timestamp) {
439
- return this.canConsumeWindow(rule.perMinute, counter, "perMinute", 6e4, timestamp) && this.canConsumeWindow(rule.perHour, counter, "perHour", 36e5, timestamp);
440
- }
441
- canConsumeWindow(limit, counter, key, windowMs, timestamp) {
442
- if (limit === void 0) return true;
443
- const { count } = this.getWindowInfo(counter, key, windowMs, timestamp);
444
- return count + 1 <= limit;
445
- }
446
- recordRateLimit(rule, counter, timestamp) {
447
- this.incrementWindow(rule.perMinute, counter, "perMinute", 6e4, timestamp);
448
- this.incrementWindow(rule.perHour, counter, "perHour", 36e5, timestamp);
449
- }
450
- incrementWindow(limit, counter, key, windowMs, timestamp) {
451
- if (limit === void 0) return;
452
- const windowStart = Math.floor(timestamp / windowMs) * windowMs;
453
- const existing = counter[key];
454
- if (!existing || existing.windowStart !== windowStart) counter[key] = {
455
- windowStart,
456
- count: 1
457
- };
458
- else existing.count += 1;
459
- }
460
- getWindowInfo(counter, key, windowMs, timestamp) {
461
- const windowStart = Math.floor(timestamp / windowMs) * windowMs;
462
- const existing = counter[key];
463
- if (!existing || existing.windowStart !== windowStart) return {
464
- windowStart,
465
- count: 0
466
- };
467
- return existing;
468
- }
469
- async makeRequest(endpoint, options, requestId = generateId("req")) {
470
- const url = `${this.config.baseUrl}${endpoint}`;
471
- const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
472
- const headers = {
473
- "Content-Type": "application/json",
474
- "X-Api-Key": this.config.apiKey,
475
- "X-App-Id": this.config.appId,
476
- "X-Environment": this.config.environment,
477
- "X-SDK-Version": SDK_VERSION,
478
- "X-Request-Id": requestId,
479
- ...options.headers
480
- };
481
- if (this.sessionId) headers["X-Session-Id"] = this.sessionId;
482
- let timeoutHandle;
483
- if (controller) {
484
- options = {
485
- ...options,
486
- signal: controller.signal
487
- };
488
- timeoutHandle = setTimeout(() => {
489
- controller.abort();
490
- }, this.queueConfig.requestTimeoutMs);
491
- }
492
- try {
493
- if (this.shouldLog("trace")) {
494
- console.log(`[Layers] → ${options.method || "GET"} ${url}`);
495
- if (options.method === "POST" && options.body) console.log(`[Layers] Request body:`, JSON.parse(options.body));
496
- }
497
- const response = await fetch(url, {
498
- ...options,
499
- headers
500
- });
501
- if (this.shouldLog("trace")) console.log(`[Layers] ← ${response.status} ${response.statusText}`);
502
- if (!response.ok) {
503
- if (!this.isRetryableStatus(response.status)) {
504
- const error$1 = Object.assign(/* @__PURE__ */ new Error(`HTTP ${response.status}: ${response.statusText}`), { status: response.status });
505
- if (this.shouldLog("debug")) console.warn(`[Layers] Non-retryable error for ${url}:`, error$1);
506
- throw error$1;
507
- }
508
- const error = Object.assign(/* @__PURE__ */ new Error(`Retryable HTTP ${response.status}`), { status: response.status });
509
- if (this.shouldLog("debug")) console.warn(`[Layers] Retryable error for ${url}:`, error);
510
- throw error;
511
- }
512
- if (response.status === 204) return;
513
- const text = await response.text();
514
- if (!text) return;
515
- const parsed = JSON.parse(text);
516
- if (this.shouldLog("trace")) console.log(`[Layers] Response:`, parsed);
517
- return parsed;
518
- } finally {
519
- if (timeoutHandle) clearTimeout(timeoutHandle);
520
- }
521
- }
522
- isRetryableStatus(status) {
523
- if (status === 429 || status >= 500 && status < 600) return true;
524
- return false;
525
- }
526
- generateEventId() {
527
- return generateId("evt");
528
- }
529
- generateBatchId() {
530
- return generateId("batch");
531
- }
532
- getRemoteConfig() {
533
- return this.remoteConfig;
534
- }
535
- getRemoteConfigVersion() {
536
- return this.remoteConfigMeta.version;
537
- }
538
- getConsentState() {
539
- return { ...this.consentState };
540
- }
541
- setAppUserId(appUserId) {
542
- this.config.appUserId = appUserId;
543
- if (this.shouldLog("info")) console.log(`[Layers] App user ID set: ${appUserId || "(cleared)"}`);
544
- }
545
- /** @deprecated Use setAppUserId instead */
546
- setUserId(userId) {
547
- this.setAppUserId(userId);
548
- }
549
- getConfig() {
550
- return { ...this.config };
551
- }
552
- getAppUserId() {
553
- return this.resolveAppUserId();
554
- }
555
- /** @deprecated Use getAppUserId instead */
556
- getUserId() {
557
- return this.resolveAppUserId();
558
- }
559
- getSessionId() {
560
- return this.sessionId;
561
- }
562
- async flush() {
563
- await this.processQueue();
564
- }
565
- setDeviceInfo(deviceInfo) {
566
- this.deviceInfo = {
567
- ...this.deviceInfo,
568
- ...Object.fromEntries(Object.entries(deviceInfo).filter(([, value]) => value !== void 0 && value !== null))
569
- };
570
- }
571
- startNewSession() {
572
- this.sessionId = this.generateSessionId();
573
- }
574
- };
575
- function createDefaultQueueStorage(appId) {
576
- if (typeof window === "undefined") return null;
577
- try {
578
- if (!window.localStorage) return null;
579
- } catch {
580
- return null;
581
- }
582
- return createLocalStorageQueueStorage(`layers_queue_${appId}`);
583
- }
584
- function createLocalStorageQueueStorage(storageKey) {
585
- return {
586
- async load() {
587
- try {
588
- const raw = window.localStorage.getItem(storageKey);
589
- if (!raw) return null;
590
- const parsed = JSON.parse(raw);
591
- if (!parsed || typeof parsed !== "object") return null;
592
- return parsed;
593
- } catch (error) {
594
- console.warn("Failed to load queue from localStorage", error);
595
- return null;
596
- }
597
- },
598
- async save(snapshot) {
599
- try {
600
- window.localStorage.setItem(storageKey, JSON.stringify(snapshot));
601
- } catch (error) {
602
- console.warn("Failed to persist queue to localStorage", error);
603
- }
604
- },
605
- async clear() {
606
- try {
607
- window.localStorage.removeItem(storageKey);
608
- } catch (error) {
609
- console.warn("Failed to clear queue storage", error);
610
- }
611
- }
612
- };
613
- }
1
+ import { i as getAttributionProperties, n as LayersError, r as getAttribution, t as LayersClient } from "./src-CDTj2A6Q.js";
614
2
 
615
- //#endregion
616
- export { LayersClient };
617
- //# sourceMappingURL=index.js.map
3
+ export { LayersClient, LayersError, getAttribution, getAttributionProperties };