@databuddy/sdk 2.3.1 → 2.3.21
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/ai/vercel/index.d.mts +157 -7
- package/dist/ai/vercel/index.d.ts +157 -7
- package/dist/ai/vercel/index.mjs +229 -34
- package/dist/core/index.d.mts +3 -265
- package/dist/core/index.d.ts +3 -265
- package/dist/core/index.mjs +26 -3
- package/dist/node/index.d.mts +121 -10
- package/dist/node/index.d.ts +121 -10
- package/dist/node/index.mjs +256 -2
- package/dist/react/index.d.mts +58 -17
- package/dist/react/index.d.ts +58 -17
- package/dist/react/index.mjs +179 -86
- package/dist/shared/@databuddy/sdk.B6nwxnPC.d.mts +128 -0
- package/dist/shared/@databuddy/sdk.B6nwxnPC.d.ts +128 -0
- package/dist/shared/@databuddy/{sdk.CeYE_kaj.d.mts → sdk.BsF1xr6_.d.mts} +234 -11
- package/dist/shared/@databuddy/{sdk.CeYE_kaj.d.ts → sdk.BsF1xr6_.d.ts} +234 -11
- package/dist/shared/@databuddy/sdk.C8vEu9Y4.mjs +35 -0
- package/dist/shared/@databuddy/sdk.D0dyEsAb.mjs +482 -0
- package/dist/shared/@databuddy/sdk.DCKr2Zpd.mjs +202 -0
- package/dist/vue/index.d.mts +11 -2
- package/dist/vue/index.d.ts +11 -2
- package/dist/vue/index.mjs +19 -9
- package/package.json +3 -4
- package/dist/shared/@databuddy/sdk.BUsPV0LH.mjs +0 -25
- package/dist/shared/@databuddy/sdk.ByNF_UxE.mjs +0 -471
- package/dist/shared/@databuddy/sdk.OK9Nbqlf.d.mts +0 -64
- package/dist/shared/@databuddy/sdk.OK9Nbqlf.d.ts +0 -64
|
@@ -0,0 +1,482 @@
|
|
|
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.DCKr2Zpd.mjs';
|
|
2
|
+
|
|
3
|
+
class BrowserFlagStorage {
|
|
4
|
+
ttl = 24 * 60 * 60 * 1e3;
|
|
5
|
+
// 24 hours in milliseconds
|
|
6
|
+
get(key) {
|
|
7
|
+
return this.getFromLocalStorage(key);
|
|
8
|
+
}
|
|
9
|
+
set(key, value) {
|
|
10
|
+
this.setToLocalStorage(key, value);
|
|
11
|
+
}
|
|
12
|
+
getAll() {
|
|
13
|
+
const result = {};
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const keys = Object.keys(localStorage).filter(
|
|
16
|
+
(key) => key.startsWith("db-flag-")
|
|
17
|
+
);
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const flagKey = key.replace("db-flag-", "");
|
|
20
|
+
try {
|
|
21
|
+
const item = localStorage.getItem(key);
|
|
22
|
+
if (item) {
|
|
23
|
+
const parsed = JSON.parse(item);
|
|
24
|
+
if (parsed.expiresAt && now > parsed.expiresAt) {
|
|
25
|
+
localStorage.removeItem(key);
|
|
26
|
+
} else {
|
|
27
|
+
result[flagKey] = parsed.value || parsed;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
const keys = Object.keys(localStorage).filter(
|
|
37
|
+
(key) => key.startsWith("db-flag-")
|
|
38
|
+
);
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
localStorage.removeItem(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getFromLocalStorage(key) {
|
|
44
|
+
try {
|
|
45
|
+
const item = localStorage.getItem(`db-flag-${key}`);
|
|
46
|
+
if (!item) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const parsed = JSON.parse(item);
|
|
50
|
+
if (parsed.expiresAt) {
|
|
51
|
+
if (this.isExpired(parsed.expiresAt)) {
|
|
52
|
+
localStorage.removeItem(`db-flag-${key}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return parsed.value;
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
setToLocalStorage(key, value) {
|
|
63
|
+
try {
|
|
64
|
+
const item = {
|
|
65
|
+
value,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
expiresAt: Date.now() + this.ttl
|
|
68
|
+
};
|
|
69
|
+
localStorage.setItem(`db-flag-${key}`, JSON.stringify(item));
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
isExpired(expiresAt) {
|
|
74
|
+
if (!expiresAt) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return Date.now() > expiresAt;
|
|
78
|
+
}
|
|
79
|
+
delete(key) {
|
|
80
|
+
localStorage.removeItem(`db-flag-${key}`);
|
|
81
|
+
}
|
|
82
|
+
deleteMultiple(keys) {
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
localStorage.removeItem(`db-flag-${key}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setAll(flags) {
|
|
88
|
+
const currentFlags = this.getAll();
|
|
89
|
+
const currentKeys = Object.keys(currentFlags);
|
|
90
|
+
const newKeys = Object.keys(flags);
|
|
91
|
+
const removedKeys = currentKeys.filter((key) => !newKeys.includes(key));
|
|
92
|
+
if (removedKeys.length > 0) {
|
|
93
|
+
this.deleteMultiple(removedKeys);
|
|
94
|
+
}
|
|
95
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
96
|
+
this.set(key, value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
cleanupExpired() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const keys = Object.keys(localStorage).filter(
|
|
102
|
+
(key) => key.startsWith("db-flag-")
|
|
103
|
+
);
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
try {
|
|
106
|
+
const item = localStorage.getItem(key);
|
|
107
|
+
if (item) {
|
|
108
|
+
const parsed = JSON.parse(item);
|
|
109
|
+
if (parsed.expiresAt && now > parsed.expiresAt) {
|
|
110
|
+
localStorage.removeItem(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
localStorage.removeItem(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class CoreFlagsManager {
|
|
121
|
+
config;
|
|
122
|
+
storage;
|
|
123
|
+
onFlagsUpdate;
|
|
124
|
+
onConfigUpdate;
|
|
125
|
+
onReady;
|
|
126
|
+
/** In-memory cache with stale tracking */
|
|
127
|
+
cache = /* @__PURE__ */ new Map();
|
|
128
|
+
/** In-flight requests for deduplication */
|
|
129
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
130
|
+
/** Request batcher for batching multiple flag requests */
|
|
131
|
+
batcher = null;
|
|
132
|
+
/** Ready state */
|
|
133
|
+
ready = false;
|
|
134
|
+
/** Visibility state */
|
|
135
|
+
isVisible = true;
|
|
136
|
+
/** Visibility listener cleanup */
|
|
137
|
+
visibilityCleanup;
|
|
138
|
+
constructor(options) {
|
|
139
|
+
this.config = this.withDefaults(options.config);
|
|
140
|
+
this.storage = options.storage;
|
|
141
|
+
this.onFlagsUpdate = options.onFlagsUpdate;
|
|
142
|
+
this.onConfigUpdate = options.onConfigUpdate;
|
|
143
|
+
this.onReady = options.onReady;
|
|
144
|
+
logger.setDebug(this.config.debug ?? false);
|
|
145
|
+
logger.debug("FlagsManager initialized", {
|
|
146
|
+
clientId: this.config.clientId,
|
|
147
|
+
hasUser: Boolean(this.config.user)
|
|
148
|
+
});
|
|
149
|
+
this.setupVisibilityListener();
|
|
150
|
+
this.initialize();
|
|
151
|
+
}
|
|
152
|
+
withDefaults(config) {
|
|
153
|
+
return {
|
|
154
|
+
clientId: config.clientId,
|
|
155
|
+
apiUrl: config.apiUrl ?? "https://api.databuddy.cc",
|
|
156
|
+
user: config.user,
|
|
157
|
+
disabled: config.disabled ?? false,
|
|
158
|
+
debug: config.debug ?? false,
|
|
159
|
+
skipStorage: config.skipStorage ?? false,
|
|
160
|
+
isPending: config.isPending,
|
|
161
|
+
autoFetch: config.autoFetch !== false,
|
|
162
|
+
environment: config.environment,
|
|
163
|
+
cacheTtl: config.cacheTtl ?? 6e4,
|
|
164
|
+
// 1 minute
|
|
165
|
+
staleTime: config.staleTime ?? 3e4
|
|
166
|
+
// 30 seconds - revalidate after this
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
setupVisibilityListener() {
|
|
170
|
+
if (typeof document === "undefined") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const handleVisibility = () => {
|
|
174
|
+
this.isVisible = document.visibilityState === "visible";
|
|
175
|
+
if (this.isVisible) {
|
|
176
|
+
this.revalidateStale();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
180
|
+
this.visibilityCleanup = () => {
|
|
181
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async initialize() {
|
|
185
|
+
if (!this.config.skipStorage && this.storage) {
|
|
186
|
+
this.loadFromStorage();
|
|
187
|
+
}
|
|
188
|
+
if (this.config.autoFetch && !this.config.isPending) {
|
|
189
|
+
await this.fetchAllFlags();
|
|
190
|
+
}
|
|
191
|
+
this.ready = true;
|
|
192
|
+
this.onReady?.();
|
|
193
|
+
}
|
|
194
|
+
loadFromStorage() {
|
|
195
|
+
if (!this.storage) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const stored = this.storage.getAll();
|
|
200
|
+
const ttl = this.config.cacheTtl ?? 6e4;
|
|
201
|
+
const staleTime = this.config.staleTime ?? ttl / 2;
|
|
202
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
203
|
+
if (value && typeof value === "object") {
|
|
204
|
+
this.cache.set(
|
|
205
|
+
key,
|
|
206
|
+
createCacheEntry(value, ttl, staleTime)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (this.cache.size > 0) {
|
|
211
|
+
logger.debug(`Loaded ${this.cache.size} flags from storage`);
|
|
212
|
+
this.notifyUpdate();
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
logger.warn("Failed to load from storage:", err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
saveToStorage() {
|
|
219
|
+
if (!this.storage || this.config.skipStorage) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const flags = {};
|
|
224
|
+
for (const [key, entry] of this.cache) {
|
|
225
|
+
flags[key] = entry.result;
|
|
226
|
+
}
|
|
227
|
+
this.storage.setAll(flags);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
logger.warn("Failed to save to storage:", err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
getFromCache(key) {
|
|
233
|
+
const cached = this.cache.get(key);
|
|
234
|
+
if (isCacheValid(cached)) {
|
|
235
|
+
return cached;
|
|
236
|
+
}
|
|
237
|
+
if (cached) {
|
|
238
|
+
this.cache.delete(key);
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
getBatcher() {
|
|
243
|
+
if (!this.batcher) {
|
|
244
|
+
const apiUrl = this.config.apiUrl ?? "https://api.databuddy.cc";
|
|
245
|
+
const params = buildQueryParams(this.config);
|
|
246
|
+
this.batcher = new RequestBatcher(apiUrl, params);
|
|
247
|
+
}
|
|
248
|
+
return this.batcher;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Revalidate stale entries in background
|
|
252
|
+
*/
|
|
253
|
+
revalidateStale() {
|
|
254
|
+
const staleKeys = [];
|
|
255
|
+
for (const [key, entry] of this.cache) {
|
|
256
|
+
if (isCacheStale(entry)) {
|
|
257
|
+
staleKeys.push(key.split(":")[0]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (staleKeys.length > 0) {
|
|
261
|
+
logger.debug(`Revalidating ${staleKeys.length} stale flags`);
|
|
262
|
+
this.fetchAllFlags().catch(
|
|
263
|
+
(err) => logger.error("Revalidation error:", err)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Fetch a single flag from API with deduplication and batching
|
|
269
|
+
*/
|
|
270
|
+
async getFlag(key, user) {
|
|
271
|
+
if (this.config.disabled) {
|
|
272
|
+
return DEFAULT_RESULT;
|
|
273
|
+
}
|
|
274
|
+
if (this.config.isPending) {
|
|
275
|
+
return { ...DEFAULT_RESULT, reason: "SESSION_PENDING" };
|
|
276
|
+
}
|
|
277
|
+
const cacheKey = getCacheKey(key, user ?? this.config.user);
|
|
278
|
+
const cached = this.getFromCache(cacheKey);
|
|
279
|
+
if (cached) {
|
|
280
|
+
if (isCacheStale(cached) && this.isVisible) {
|
|
281
|
+
this.revalidateFlag(key, cacheKey);
|
|
282
|
+
}
|
|
283
|
+
return cached.result;
|
|
284
|
+
}
|
|
285
|
+
const existing = this.inFlight.get(cacheKey);
|
|
286
|
+
if (existing) {
|
|
287
|
+
logger.debug(`Deduplicating request: ${key}`);
|
|
288
|
+
return existing;
|
|
289
|
+
}
|
|
290
|
+
const promise = this.getBatcher().request(key);
|
|
291
|
+
this.inFlight.set(cacheKey, promise);
|
|
292
|
+
try {
|
|
293
|
+
const result = await promise;
|
|
294
|
+
const ttl = this.config.cacheTtl ?? 6e4;
|
|
295
|
+
const staleTime = this.config.staleTime ?? ttl / 2;
|
|
296
|
+
this.cache.set(cacheKey, createCacheEntry(result, ttl, staleTime));
|
|
297
|
+
this.notifyUpdate();
|
|
298
|
+
this.saveToStorage();
|
|
299
|
+
return result;
|
|
300
|
+
} finally {
|
|
301
|
+
this.inFlight.delete(cacheKey);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async revalidateFlag(key, cacheKey) {
|
|
305
|
+
if (this.inFlight.has(cacheKey)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const promise = this.getBatcher().request(key);
|
|
309
|
+
this.inFlight.set(cacheKey, promise);
|
|
310
|
+
try {
|
|
311
|
+
const result = await promise;
|
|
312
|
+
const ttl = this.config.cacheTtl ?? 6e4;
|
|
313
|
+
const staleTime = this.config.staleTime ?? ttl / 2;
|
|
314
|
+
this.cache.set(cacheKey, createCacheEntry(result, ttl, staleTime));
|
|
315
|
+
this.notifyUpdate();
|
|
316
|
+
this.saveToStorage();
|
|
317
|
+
logger.debug(`Revalidated flag: ${key}`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
logger.error(`Revalidation error: ${key}`, err);
|
|
320
|
+
} finally {
|
|
321
|
+
this.inFlight.delete(cacheKey);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Fetch all flags for current user
|
|
326
|
+
*/
|
|
327
|
+
async fetchAllFlags(user) {
|
|
328
|
+
if (this.config.disabled || this.config.isPending) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!this.isVisible && this.cache.size > 0) {
|
|
332
|
+
logger.debug("Skipping fetch - tab hidden");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const apiUrl = this.config.apiUrl ?? "https://api.databuddy.cc";
|
|
336
|
+
const params = buildQueryParams(this.config, user);
|
|
337
|
+
try {
|
|
338
|
+
const flags = await fetchAllFlags(apiUrl, params);
|
|
339
|
+
const ttl = this.config.cacheTtl ?? 6e4;
|
|
340
|
+
const staleTime = this.config.staleTime ?? ttl / 2;
|
|
341
|
+
for (const [key, result] of Object.entries(flags)) {
|
|
342
|
+
const cacheKey = getCacheKey(key, user ?? this.config.user);
|
|
343
|
+
this.cache.set(cacheKey, createCacheEntry(result, ttl, staleTime));
|
|
344
|
+
}
|
|
345
|
+
this.ready = true;
|
|
346
|
+
this.notifyUpdate();
|
|
347
|
+
this.saveToStorage();
|
|
348
|
+
logger.debug(`Fetched ${Object.keys(flags).length} flags`);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger.error("Bulk fetch error:", err);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if flag is enabled (synchronous, returns cached value)
|
|
355
|
+
* Uses stale-while-revalidate pattern
|
|
356
|
+
*/
|
|
357
|
+
isEnabled(key) {
|
|
358
|
+
const cacheKey = getCacheKey(key, this.config.user);
|
|
359
|
+
const cached = this.getFromCache(cacheKey);
|
|
360
|
+
if (cached) {
|
|
361
|
+
if (isCacheStale(cached) && this.isVisible) {
|
|
362
|
+
this.revalidateFlag(key, cacheKey);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
on: cached.result.enabled,
|
|
366
|
+
enabled: cached.result.enabled,
|
|
367
|
+
status: cached.result.reason === "ERROR" ? "error" : "ready",
|
|
368
|
+
loading: false,
|
|
369
|
+
isLoading: false,
|
|
370
|
+
isReady: true,
|
|
371
|
+
value: cached.result.value,
|
|
372
|
+
variant: cached.result.variant
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (this.inFlight.has(cacheKey)) {
|
|
376
|
+
return {
|
|
377
|
+
on: false,
|
|
378
|
+
enabled: false,
|
|
379
|
+
status: "loading",
|
|
380
|
+
loading: true,
|
|
381
|
+
isLoading: true,
|
|
382
|
+
isReady: false
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
this.getFlag(key).catch(
|
|
386
|
+
(err) => logger.error(`Background fetch error: ${key}`, err)
|
|
387
|
+
);
|
|
388
|
+
return {
|
|
389
|
+
on: false,
|
|
390
|
+
enabled: false,
|
|
391
|
+
status: "loading",
|
|
392
|
+
loading: true,
|
|
393
|
+
isLoading: true,
|
|
394
|
+
isReady: false
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get flag value with type (synchronous, returns cached or default)
|
|
399
|
+
*/
|
|
400
|
+
getValue(key, defaultValue) {
|
|
401
|
+
const cacheKey = getCacheKey(key, this.config.user);
|
|
402
|
+
const cached = this.getFromCache(cacheKey);
|
|
403
|
+
if (cached) {
|
|
404
|
+
if (isCacheStale(cached) && this.isVisible) {
|
|
405
|
+
this.revalidateFlag(key, cacheKey);
|
|
406
|
+
}
|
|
407
|
+
return cached.result.value;
|
|
408
|
+
}
|
|
409
|
+
if (!this.inFlight.has(cacheKey)) {
|
|
410
|
+
this.getFlag(key).catch(
|
|
411
|
+
(err) => logger.error(`Background fetch error: ${key}`, err)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return defaultValue ?? false;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Update user context and refresh flags
|
|
418
|
+
*/
|
|
419
|
+
updateUser(user) {
|
|
420
|
+
this.config = { ...this.config, user };
|
|
421
|
+
this.batcher?.destroy();
|
|
422
|
+
this.batcher = null;
|
|
423
|
+
this.onConfigUpdate?.(this.config);
|
|
424
|
+
this.refresh().catch((err) => logger.error("Refresh error:", err));
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Refresh all flags
|
|
428
|
+
*/
|
|
429
|
+
async refresh(forceClear = false) {
|
|
430
|
+
if (forceClear) {
|
|
431
|
+
this.cache.clear();
|
|
432
|
+
this.storage?.clear();
|
|
433
|
+
this.notifyUpdate();
|
|
434
|
+
}
|
|
435
|
+
await this.fetchAllFlags();
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Update configuration
|
|
439
|
+
*/
|
|
440
|
+
updateConfig(config) {
|
|
441
|
+
const wasDisabled = this.config.disabled;
|
|
442
|
+
const wasPending = this.config.isPending;
|
|
443
|
+
this.config = this.withDefaults(config);
|
|
444
|
+
this.batcher?.destroy();
|
|
445
|
+
this.batcher = null;
|
|
446
|
+
this.onConfigUpdate?.(this.config);
|
|
447
|
+
if ((wasDisabled || wasPending) && !this.config.disabled && !this.config.isPending) {
|
|
448
|
+
this.fetchAllFlags().catch((err) => logger.error("Fetch error:", err));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Get all cached flags
|
|
453
|
+
*/
|
|
454
|
+
getMemoryFlags() {
|
|
455
|
+
const flags = {};
|
|
456
|
+
for (const [key, entry] of this.cache) {
|
|
457
|
+
const flagKey = key.split(":")[0];
|
|
458
|
+
flags[flagKey] = entry.result;
|
|
459
|
+
}
|
|
460
|
+
return flags;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Check if manager is ready
|
|
464
|
+
*/
|
|
465
|
+
isReady() {
|
|
466
|
+
return this.ready;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Cleanup resources
|
|
470
|
+
*/
|
|
471
|
+
destroy() {
|
|
472
|
+
this.batcher?.destroy();
|
|
473
|
+
this.visibilityCleanup?.();
|
|
474
|
+
this.cache.clear();
|
|
475
|
+
this.inFlight.clear();
|
|
476
|
+
}
|
|
477
|
+
notifyUpdate() {
|
|
478
|
+
this.onFlagsUpdate?.(this.getMemoryFlags());
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export { BrowserFlagStorage as B, CoreFlagsManager as C };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
class Logger {
|
|
2
|
+
debugEnabled = false;
|
|
3
|
+
/**
|
|
4
|
+
* Enable or disable debug logging
|
|
5
|
+
*/
|
|
6
|
+
setDebug(enabled) {
|
|
7
|
+
this.debugEnabled = enabled;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Log debug messages (only when debug is enabled)
|
|
11
|
+
*/
|
|
12
|
+
debug(...args) {
|
|
13
|
+
if (this.debugEnabled) {
|
|
14
|
+
console.log("[Databuddy]", ...args);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Log info messages (always enabled)
|
|
19
|
+
*/
|
|
20
|
+
info(...args) {
|
|
21
|
+
console.info("[Databuddy]", ...args);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Log warning messages (always enabled)
|
|
25
|
+
*/
|
|
26
|
+
warn(...args) {
|
|
27
|
+
console.warn("[Databuddy]", ...args);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Log error messages (always enabled)
|
|
31
|
+
*/
|
|
32
|
+
error(...args) {
|
|
33
|
+
console.error("[Databuddy]", ...args);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Log with table format (only when debug is enabled)
|
|
37
|
+
*/
|
|
38
|
+
table(data) {
|
|
39
|
+
if (this.debugEnabled) {
|
|
40
|
+
console.table(data);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Time a function execution (only when debug is enabled)
|
|
45
|
+
*/
|
|
46
|
+
time(label) {
|
|
47
|
+
if (this.debugEnabled) {
|
|
48
|
+
console.time(`[Databuddy] ${label}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* End timing a function execution (only when debug is enabled)
|
|
53
|
+
*/
|
|
54
|
+
timeEnd(label) {
|
|
55
|
+
if (this.debugEnabled) {
|
|
56
|
+
console.timeEnd(`[Databuddy] ${label}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Log JSON data (only when debug is enabled)
|
|
61
|
+
*/
|
|
62
|
+
json(data) {
|
|
63
|
+
if (this.debugEnabled) {
|
|
64
|
+
console.log("[Databuddy]", JSON.stringify(data, null, 2));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const logger = new Logger();
|
|
69
|
+
|
|
70
|
+
const DEFAULT_RESULT = {
|
|
71
|
+
enabled: false,
|
|
72
|
+
value: false,
|
|
73
|
+
payload: null,
|
|
74
|
+
reason: "DEFAULT"
|
|
75
|
+
};
|
|
76
|
+
function getCacheKey(key, user) {
|
|
77
|
+
if (!(user?.userId || user?.email)) {
|
|
78
|
+
return key;
|
|
79
|
+
}
|
|
80
|
+
return `${key}:${user.userId ?? ""}:${user.email ?? ""}`;
|
|
81
|
+
}
|
|
82
|
+
function buildQueryParams(config, user) {
|
|
83
|
+
const params = new URLSearchParams();
|
|
84
|
+
params.set("clientId", config.clientId);
|
|
85
|
+
const u = user ?? config.user;
|
|
86
|
+
if (u?.userId) {
|
|
87
|
+
params.set("userId", u.userId);
|
|
88
|
+
}
|
|
89
|
+
if (u?.email) {
|
|
90
|
+
params.set("email", u.email);
|
|
91
|
+
}
|
|
92
|
+
if (u?.properties) {
|
|
93
|
+
params.set("properties", JSON.stringify(u.properties));
|
|
94
|
+
}
|
|
95
|
+
if (config.environment) {
|
|
96
|
+
params.set("environment", config.environment);
|
|
97
|
+
}
|
|
98
|
+
return params;
|
|
99
|
+
}
|
|
100
|
+
async function fetchFlags(apiUrl, keys, params) {
|
|
101
|
+
const batchParams = new URLSearchParams(params);
|
|
102
|
+
batchParams.set("keys", keys.join(","));
|
|
103
|
+
const url = `${apiUrl}/public/v1/flags/bulk?${batchParams}`;
|
|
104
|
+
const response = await fetch(url);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const result = {};
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
result[key] = { ...DEFAULT_RESULT, reason: "ERROR" };
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
return data.flags ?? {};
|
|
114
|
+
}
|
|
115
|
+
async function fetchAllFlags(apiUrl, params) {
|
|
116
|
+
const url = `${apiUrl}/public/v1/flags/bulk?${params}`;
|
|
117
|
+
const response = await fetch(url);
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
return data.flags ?? {};
|
|
123
|
+
}
|
|
124
|
+
function isCacheValid(entry) {
|
|
125
|
+
if (!entry) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return Date.now() <= entry.expiresAt;
|
|
129
|
+
}
|
|
130
|
+
function isCacheStale(entry) {
|
|
131
|
+
return Date.now() > entry.staleAt;
|
|
132
|
+
}
|
|
133
|
+
function createCacheEntry(result, ttl, staleTime) {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
return {
|
|
136
|
+
result,
|
|
137
|
+
staleAt: now + (staleTime ?? ttl / 2),
|
|
138
|
+
expiresAt: now + ttl
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
class RequestBatcher {
|
|
142
|
+
pending = /* @__PURE__ */ new Map();
|
|
143
|
+
timer = null;
|
|
144
|
+
batchDelayMs;
|
|
145
|
+
apiUrl;
|
|
146
|
+
params;
|
|
147
|
+
constructor(apiUrl, params, batchDelayMs = 10) {
|
|
148
|
+
this.apiUrl = apiUrl;
|
|
149
|
+
this.params = params;
|
|
150
|
+
this.batchDelayMs = batchDelayMs;
|
|
151
|
+
}
|
|
152
|
+
request(key) {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const existing = this.pending.get(key);
|
|
155
|
+
if (existing) {
|
|
156
|
+
existing.push({ resolve, reject });
|
|
157
|
+
} else {
|
|
158
|
+
this.pending.set(key, [{ resolve, reject }]);
|
|
159
|
+
}
|
|
160
|
+
if (!this.timer) {
|
|
161
|
+
this.timer = setTimeout(() => this.flush(), this.batchDelayMs);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async flush() {
|
|
166
|
+
this.timer = null;
|
|
167
|
+
const keys = [...this.pending.keys()];
|
|
168
|
+
const callbacks = new Map(this.pending);
|
|
169
|
+
this.pending.clear();
|
|
170
|
+
if (keys.length === 0) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const results = await fetchFlags(this.apiUrl, keys, this.params);
|
|
175
|
+
for (const [key, cbs] of callbacks) {
|
|
176
|
+
const result = results[key] ?? {
|
|
177
|
+
...DEFAULT_RESULT,
|
|
178
|
+
reason: "NOT_FOUND"
|
|
179
|
+
};
|
|
180
|
+
for (const cb of cbs) {
|
|
181
|
+
cb.resolve(result);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const error = err instanceof Error ? err : new Error("Fetch failed");
|
|
186
|
+
for (const cbs of callbacks.values()) {
|
|
187
|
+
for (const cb of cbs) {
|
|
188
|
+
cb.reject(error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
destroy() {
|
|
194
|
+
if (this.timer) {
|
|
195
|
+
clearTimeout(this.timer);
|
|
196
|
+
this.timer = null;
|
|
197
|
+
}
|
|
198
|
+
this.pending.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { DEFAULT_RESULT as D, RequestBatcher as R, isCacheStale as a, buildQueryParams as b, createCacheEntry as c, fetchAllFlags as f, getCacheKey as g, isCacheValid as i, logger as l };
|
package/dist/vue/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as vue from 'vue';
|
|
2
2
|
import { App, ComputedRef } from 'vue';
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
3
|
+
import { F as FlagsConfig, b as FlagState, d as FlagResult } from '../shared/@databuddy/sdk.B6nwxnPC.mjs';
|
|
4
|
+
export { c as FlagsContext } from '../shared/@databuddy/sdk.B6nwxnPC.mjs';
|
|
5
5
|
|
|
6
6
|
declare const Databuddy: vue.DefineComponent<{
|
|
7
7
|
clientId?: string | undefined;
|
|
@@ -78,9 +78,17 @@ declare function useFlags(): {
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
interface UseFlagReturn {
|
|
81
|
+
/** Whether the flag is on */
|
|
82
|
+
on: ComputedRef<boolean>;
|
|
83
|
+
/** @deprecated Use `on` instead */
|
|
81
84
|
enabled: ComputedRef<boolean>;
|
|
85
|
+
/** Whether the flag is loading */
|
|
86
|
+
loading: ComputedRef<boolean>;
|
|
87
|
+
/** @deprecated Use `loading` instead */
|
|
82
88
|
isLoading: ComputedRef<boolean>;
|
|
89
|
+
/** @deprecated Use `status === 'ready'` instead */
|
|
83
90
|
isReady: ComputedRef<boolean>;
|
|
91
|
+
/** Full flag state */
|
|
84
92
|
state: ComputedRef<FlagState>;
|
|
85
93
|
}
|
|
86
94
|
/**
|
|
@@ -89,6 +97,7 @@ interface UseFlagReturn {
|
|
|
89
97
|
declare function useFlag(key: string): UseFlagReturn;
|
|
90
98
|
/**
|
|
91
99
|
* Vue composable for boolean flag checking
|
|
100
|
+
* @deprecated Use `useFlag(key).on` instead
|
|
92
101
|
*/
|
|
93
102
|
declare function useBooleanFlag(key: string): ComputedRef<boolean>;
|
|
94
103
|
|