@databuddy/sdk 2.3.30 → 2.4.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.
@@ -1,559 +0,0 @@
1
- import { l as logger, c as createCacheEntry, i as isCacheValid, b as buildQueryParams, R as RequestBatcher, a as isCacheStale, D as DEFAULT_RESULT, g as getCacheKey, f as fetchAllFlags } from './sdk.CALvx07o.mjs';
2
-
3
- const isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
4
- class BrowserFlagStorage {
5
- ttl = 24 * 60 * 60 * 1e3;
6
- // 24 hours in milliseconds
7
- get(key) {
8
- if (!isBrowser) {
9
- return null;
10
- }
11
- return this.getFromLocalStorage(key);
12
- }
13
- set(key, value) {
14
- if (!isBrowser) {
15
- return;
16
- }
17
- this.setToLocalStorage(key, value);
18
- }
19
- getAll() {
20
- if (!isBrowser) {
21
- return {};
22
- }
23
- const result = {};
24
- const now = Date.now();
25
- const keys = Object.keys(localStorage).filter(
26
- (key) => key.startsWith("db-flag-")
27
- );
28
- for (const key of keys) {
29
- const flagKey = key.replace("db-flag-", "");
30
- try {
31
- const item = localStorage.getItem(key);
32
- if (item) {
33
- const parsed = JSON.parse(item);
34
- if (parsed.expiresAt && now > parsed.expiresAt) {
35
- localStorage.removeItem(key);
36
- } else {
37
- result[flagKey] = parsed.value || parsed;
38
- }
39
- }
40
- } catch {
41
- }
42
- }
43
- return result;
44
- }
45
- clear() {
46
- if (!isBrowser) {
47
- return;
48
- }
49
- const keys = Object.keys(localStorage).filter(
50
- (key) => key.startsWith("db-flag-")
51
- );
52
- for (const key of keys) {
53
- localStorage.removeItem(key);
54
- }
55
- }
56
- getFromLocalStorage(key) {
57
- try {
58
- const item = localStorage.getItem(`db-flag-${key}`);
59
- if (!item) {
60
- return null;
61
- }
62
- const parsed = JSON.parse(item);
63
- if (parsed.expiresAt) {
64
- if (this.isExpired(parsed.expiresAt)) {
65
- localStorage.removeItem(`db-flag-${key}`);
66
- return null;
67
- }
68
- return parsed.value;
69
- }
70
- return parsed;
71
- } catch {
72
- return null;
73
- }
74
- }
75
- setToLocalStorage(key, value) {
76
- try {
77
- const item = {
78
- value,
79
- timestamp: Date.now(),
80
- expiresAt: Date.now() + this.ttl
81
- };
82
- localStorage.setItem(`db-flag-${key}`, JSON.stringify(item));
83
- } catch {
84
- }
85
- }
86
- isExpired(expiresAt) {
87
- if (!expiresAt) {
88
- return false;
89
- }
90
- return Date.now() > expiresAt;
91
- }
92
- delete(key) {
93
- if (!isBrowser) {
94
- return;
95
- }
96
- localStorage.removeItem(`db-flag-${key}`);
97
- }
98
- deleteMultiple(keys) {
99
- if (!isBrowser) {
100
- return;
101
- }
102
- for (const key of keys) {
103
- localStorage.removeItem(`db-flag-${key}`);
104
- }
105
- }
106
- setAll(flags) {
107
- if (!isBrowser) {
108
- return;
109
- }
110
- const currentFlags = this.getAll();
111
- const currentKeys = Object.keys(currentFlags);
112
- const newKeys = Object.keys(flags);
113
- const removedKeys = currentKeys.filter((key) => !newKeys.includes(key));
114
- if (removedKeys.length > 0) {
115
- this.deleteMultiple(removedKeys);
116
- }
117
- for (const [key, value] of Object.entries(flags)) {
118
- this.set(key, value);
119
- }
120
- }
121
- cleanupExpired() {
122
- if (!isBrowser) {
123
- return;
124
- }
125
- const now = Date.now();
126
- const keys = Object.keys(localStorage).filter(
127
- (key) => key.startsWith("db-flag-")
128
- );
129
- for (const key of keys) {
130
- try {
131
- const item = localStorage.getItem(key);
132
- if (item) {
133
- const parsed = JSON.parse(item);
134
- if (parsed.expiresAt && now > parsed.expiresAt) {
135
- localStorage.removeItem(key);
136
- }
137
- }
138
- } catch {
139
- localStorage.removeItem(key);
140
- }
141
- }
142
- }
143
- }
144
-
145
- const ANONYMOUS_ID_KEY = "did";
146
- class CoreFlagsManager {
147
- config;
148
- storage;
149
- onFlagsUpdate;
150
- onConfigUpdate;
151
- onReady;
152
- /** In-memory cache with stale tracking */
153
- cache = /* @__PURE__ */ new Map();
154
- /** In-flight requests for deduplication */
155
- inFlight = /* @__PURE__ */ new Map();
156
- /** Request batcher for batching multiple flag requests */
157
- batcher = null;
158
- /** Ready state */
159
- ready = false;
160
- /** Visibility state */
161
- isVisible = true;
162
- /** Visibility listener cleanup */
163
- visibilityCleanup;
164
- constructor(options) {
165
- this.config = this.withDefaults(options.config);
166
- this.storage = options.storage;
167
- this.onFlagsUpdate = options.onFlagsUpdate;
168
- this.onConfigUpdate = options.onConfigUpdate;
169
- this.onReady = options.onReady;
170
- logger.setDebug(this.config.debug ?? false);
171
- logger.debug("FlagsManager initialized", {
172
- clientId: this.config.clientId,
173
- hasUser: Boolean(this.config.user)
174
- });
175
- this.setupVisibilityListener();
176
- this.initialize();
177
- }
178
- /**
179
- * Get or create anonymous ID for deterministic rollouts.
180
- * Uses the same storage key as the tracker ("did") for consistency.
181
- */
182
- getOrCreateAnonymousId() {
183
- if (typeof localStorage === "undefined") {
184
- return null;
185
- }
186
- try {
187
- let id = localStorage.getItem(ANONYMOUS_ID_KEY);
188
- if (id) {
189
- return id;
190
- }
191
- id = `anon_${crypto.randomUUID()}`;
192
- localStorage.setItem(ANONYMOUS_ID_KEY, id);
193
- return id;
194
- } catch {
195
- return null;
196
- }
197
- }
198
- /**
199
- * Ensure user context has an identifier for deterministic flag evaluation.
200
- * If no userId/email provided, inject anonymous ID as userId.
201
- */
202
- ensureUserIdentity(user) {
203
- if (user?.userId || user?.email) {
204
- return user;
205
- }
206
- const anonymousId = this.getOrCreateAnonymousId();
207
- if (!anonymousId) {
208
- return user;
209
- }
210
- return { ...user, userId: anonymousId };
211
- }
212
- withDefaults(config) {
213
- return {
214
- clientId: config.clientId,
215
- apiUrl: config.apiUrl ?? "https://api.databuddy.cc",
216
- user: this.ensureUserIdentity(config.user),
217
- disabled: config.disabled ?? false,
218
- debug: config.debug ?? false,
219
- skipStorage: config.skipStorage ?? false,
220
- isPending: config.isPending,
221
- autoFetch: config.autoFetch !== false,
222
- environment: config.environment,
223
- cacheTtl: config.cacheTtl ?? 6e4,
224
- // 1 minute
225
- staleTime: config.staleTime ?? 3e4
226
- // 30 seconds - revalidate after this
227
- };
228
- }
229
- setupVisibilityListener() {
230
- if (typeof document === "undefined") {
231
- return;
232
- }
233
- const handleVisibility = () => {
234
- this.isVisible = document.visibilityState === "visible";
235
- if (this.isVisible) {
236
- this.revalidateStale();
237
- }
238
- };
239
- document.addEventListener("visibilitychange", handleVisibility);
240
- this.visibilityCleanup = () => {
241
- document.removeEventListener("visibilitychange", handleVisibility);
242
- };
243
- }
244
- removeStaleKeys(validKeys, user) {
245
- const ctx = user ?? this.config.user;
246
- const suffix = ctx?.userId || ctx?.email ? `:${ctx.userId ?? ""}:${ctx.email ?? ""}` : "";
247
- for (const key of this.cache.keys()) {
248
- const belongsToUser = suffix ? key.endsWith(suffix) : !key.includes(":");
249
- if (belongsToUser && !validKeys.has(key)) {
250
- this.cache.delete(key);
251
- }
252
- }
253
- }
254
- async initialize() {
255
- if (!this.config.skipStorage && this.storage) {
256
- this.loadFromStorage();
257
- }
258
- if (this.config.autoFetch && !this.config.isPending) {
259
- await this.fetchAllFlags();
260
- }
261
- this.ready = true;
262
- this.onReady?.();
263
- }
264
- loadFromStorage() {
265
- if (!this.storage) {
266
- return;
267
- }
268
- try {
269
- const stored = this.storage.getAll();
270
- const ttl = this.config.cacheTtl ?? 6e4;
271
- const staleTime = this.config.staleTime ?? ttl / 2;
272
- for (const [key, value] of Object.entries(stored)) {
273
- if (value && typeof value === "object") {
274
- this.cache.set(
275
- key,
276
- createCacheEntry(value, ttl, staleTime)
277
- );
278
- }
279
- }
280
- if (this.cache.size > 0) {
281
- logger.debug(`Loaded ${this.cache.size} flags from storage`);
282
- this.notifyUpdate();
283
- }
284
- } catch (err) {
285
- logger.warn("Failed to load from storage:", err);
286
- }
287
- }
288
- saveToStorage() {
289
- if (!this.storage || this.config.skipStorage) {
290
- return;
291
- }
292
- try {
293
- const flags = {};
294
- for (const [key, entry] of this.cache) {
295
- flags[key] = entry.result;
296
- }
297
- this.storage.setAll(flags);
298
- } catch (err) {
299
- logger.warn("Failed to save to storage:", err);
300
- }
301
- }
302
- getFromCache(key) {
303
- const cached = this.cache.get(key);
304
- if (isCacheValid(cached)) {
305
- return cached;
306
- }
307
- if (cached) {
308
- this.cache.delete(key);
309
- }
310
- return null;
311
- }
312
- getBatcher() {
313
- if (!this.batcher) {
314
- const apiUrl = this.config.apiUrl ?? "https://api.databuddy.cc";
315
- const params = buildQueryParams(this.config);
316
- this.batcher = new RequestBatcher(apiUrl, params);
317
- }
318
- return this.batcher;
319
- }
320
- /**
321
- * Revalidate stale entries in background
322
- */
323
- revalidateStale() {
324
- const staleKeys = [];
325
- for (const [key, entry] of this.cache) {
326
- if (isCacheStale(entry)) {
327
- staleKeys.push(key.split(":")[0]);
328
- }
329
- }
330
- if (staleKeys.length > 0) {
331
- logger.debug(`Revalidating ${staleKeys.length} stale flags`);
332
- this.fetchAllFlags().catch(
333
- (err) => logger.error("Revalidation error:", err)
334
- );
335
- }
336
- }
337
- /**
338
- * Fetch a single flag from API with deduplication and batching
339
- */
340
- async getFlag(key, user) {
341
- if (this.config.disabled) {
342
- return DEFAULT_RESULT;
343
- }
344
- if (this.config.isPending) {
345
- return { ...DEFAULT_RESULT, reason: "SESSION_PENDING" };
346
- }
347
- const cacheKey = getCacheKey(key, user ?? this.config.user);
348
- const cached = this.getFromCache(cacheKey);
349
- if (cached) {
350
- if (isCacheStale(cached) && this.isVisible) {
351
- this.revalidateFlag(key, cacheKey);
352
- }
353
- return cached.result;
354
- }
355
- const existing = this.inFlight.get(cacheKey);
356
- if (existing) {
357
- logger.debug(`Deduplicating request: ${key}`);
358
- return existing;
359
- }
360
- const promise = this.getBatcher().request(key);
361
- this.inFlight.set(cacheKey, promise);
362
- try {
363
- const result = await promise;
364
- const ttl = this.config.cacheTtl ?? 6e4;
365
- const staleTime = this.config.staleTime ?? ttl / 2;
366
- this.cache.set(cacheKey, createCacheEntry(result, ttl, staleTime));
367
- this.notifyUpdate();
368
- this.saveToStorage();
369
- return result;
370
- } finally {
371
- this.inFlight.delete(cacheKey);
372
- }
373
- }
374
- async revalidateFlag(key, cacheKey) {
375
- if (this.inFlight.has(cacheKey)) {
376
- return;
377
- }
378
- const promise = this.getBatcher().request(key);
379
- this.inFlight.set(cacheKey, promise);
380
- try {
381
- const result = await promise;
382
- const ttl = this.config.cacheTtl ?? 6e4;
383
- const staleTime = this.config.staleTime ?? ttl / 2;
384
- this.cache.set(cacheKey, createCacheEntry(result, ttl, staleTime));
385
- this.notifyUpdate();
386
- this.saveToStorage();
387
- logger.debug(`Revalidated flag: ${key}`);
388
- } catch (err) {
389
- logger.error(`Revalidation error: ${key}`, err);
390
- } finally {
391
- this.inFlight.delete(cacheKey);
392
- }
393
- }
394
- /**
395
- * Fetch all flags for current user
396
- */
397
- async fetchAllFlags(user) {
398
- if (this.config.disabled || this.config.isPending) {
399
- return;
400
- }
401
- if (!this.isVisible && this.cache.size > 0) {
402
- logger.debug("Skipping fetch - tab hidden");
403
- return;
404
- }
405
- const apiUrl = this.config.apiUrl ?? "https://api.databuddy.cc";
406
- const params = buildQueryParams(this.config, user);
407
- const ttl = this.config.cacheTtl ?? 6e4;
408
- const staleTime = this.config.staleTime ?? ttl / 2;
409
- try {
410
- const flags = await fetchAllFlags(apiUrl, params);
411
- const flagCacheEntries = Object.entries(flags).map(([key, result]) => ({
412
- cacheKey: getCacheKey(key, user ?? this.config.user),
413
- cacheEntry: createCacheEntry(result, ttl, staleTime)
414
- }));
415
- this.removeStaleKeys(
416
- new Set(flagCacheEntries.map(({ cacheKey }) => cacheKey)),
417
- user
418
- );
419
- for (const { cacheKey, cacheEntry } of flagCacheEntries) {
420
- this.cache.set(cacheKey, cacheEntry);
421
- }
422
- this.ready = true;
423
- this.notifyUpdate();
424
- this.saveToStorage();
425
- logger.debug(`Fetched ${Object.keys(flags).length} flags`);
426
- } catch (err) {
427
- logger.error("Bulk fetch error:", err);
428
- }
429
- }
430
- /**
431
- * Check if flag is enabled (synchronous, returns cached value)
432
- * Uses stale-while-revalidate pattern
433
- */
434
- isEnabled(key) {
435
- const cacheKey = getCacheKey(key, this.config.user);
436
- const cached = this.getFromCache(cacheKey);
437
- if (cached) {
438
- if (isCacheStale(cached) && this.isVisible) {
439
- this.revalidateFlag(key, cacheKey);
440
- }
441
- return {
442
- on: cached.result.enabled,
443
- enabled: cached.result.enabled,
444
- status: cached.result.reason === "ERROR" ? "error" : "ready",
445
- loading: false,
446
- isLoading: false,
447
- isReady: true,
448
- value: cached.result.value,
449
- variant: cached.result.variant
450
- };
451
- }
452
- if (this.inFlight.has(cacheKey)) {
453
- return {
454
- on: false,
455
- enabled: false,
456
- status: "loading",
457
- loading: true,
458
- isLoading: true,
459
- isReady: false
460
- };
461
- }
462
- this.getFlag(key).catch(
463
- (err) => logger.error(`Background fetch error: ${key}`, err)
464
- );
465
- return {
466
- on: false,
467
- enabled: false,
468
- status: "loading",
469
- loading: true,
470
- isLoading: true,
471
- isReady: false
472
- };
473
- }
474
- /**
475
- * Get flag value with type (synchronous, returns cached or default)
476
- */
477
- getValue(key, defaultValue) {
478
- const cacheKey = getCacheKey(key, this.config.user);
479
- const cached = this.getFromCache(cacheKey);
480
- if (cached) {
481
- if (isCacheStale(cached) && this.isVisible) {
482
- this.revalidateFlag(key, cacheKey);
483
- }
484
- return cached.result.value;
485
- }
486
- if (!this.inFlight.has(cacheKey)) {
487
- this.getFlag(key).catch(
488
- (err) => logger.error(`Background fetch error: ${key}`, err)
489
- );
490
- }
491
- return defaultValue ?? false;
492
- }
493
- /**
494
- * Update user context and refresh flags
495
- */
496
- updateUser(user) {
497
- this.config = { ...this.config, user: this.ensureUserIdentity(user) };
498
- this.batcher?.destroy();
499
- this.batcher = null;
500
- this.onConfigUpdate?.(this.config);
501
- this.refresh().catch((err) => logger.error("Refresh error:", err));
502
- }
503
- /**
504
- * Refresh all flags
505
- */
506
- async refresh(forceClear = false) {
507
- if (forceClear) {
508
- this.cache.clear();
509
- this.storage?.clear();
510
- this.notifyUpdate();
511
- }
512
- await this.fetchAllFlags();
513
- }
514
- /**
515
- * Update configuration
516
- */
517
- updateConfig(config) {
518
- const wasDisabled = this.config.disabled;
519
- const wasPending = this.config.isPending;
520
- this.config = this.withDefaults(config);
521
- this.batcher?.destroy();
522
- this.batcher = null;
523
- this.onConfigUpdate?.(this.config);
524
- if ((wasDisabled || wasPending) && !this.config.disabled && !this.config.isPending) {
525
- this.fetchAllFlags().catch((err) => logger.error("Fetch error:", err));
526
- }
527
- }
528
- /**
529
- * Get all cached flags
530
- */
531
- getMemoryFlags() {
532
- const flags = {};
533
- for (const [key, entry] of this.cache) {
534
- const flagKey = key.split(":")[0];
535
- flags[flagKey] = entry.result;
536
- }
537
- return flags;
538
- }
539
- /**
540
- * Check if manager is ready
541
- */
542
- isReady() {
543
- return this.ready;
544
- }
545
- /**
546
- * Cleanup resources
547
- */
548
- destroy() {
549
- this.batcher?.destroy();
550
- this.visibilityCleanup?.();
551
- this.cache.clear();
552
- this.inFlight.clear();
553
- }
554
- notifyUpdate() {
555
- this.onFlagsUpdate?.(this.getMemoryFlags());
556
- }
557
- }
558
-
559
- export { BrowserFlagStorage as B, CoreFlagsManager as C };