featuredrop 1.0.1 → 1.2.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.
Files changed (60) hide show
  1. package/README.md +626 -186
  2. package/dist/angular.cjs +286 -0
  3. package/dist/angular.cjs.map +1 -0
  4. package/dist/angular.d.cts +229 -0
  5. package/dist/angular.d.ts +229 -0
  6. package/dist/angular.js +283 -0
  7. package/dist/angular.js.map +1 -0
  8. package/dist/featuredrop.cjs +1256 -0
  9. package/dist/featuredrop.cjs.map +1 -0
  10. package/dist/index.cjs +2769 -9
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +1020 -9
  13. package/dist/index.d.ts +1020 -9
  14. package/dist/index.js +2726 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/preact.cjs +7289 -0
  17. package/dist/preact.cjs.map +1 -0
  18. package/dist/preact.d.cts +1266 -0
  19. package/dist/preact.d.ts +1266 -0
  20. package/dist/preact.js +7259 -0
  21. package/dist/preact.js.map +1 -0
  22. package/dist/react.cjs +7142 -49
  23. package/dist/react.cjs.map +1 -1
  24. package/dist/react.d.cts +1119 -7
  25. package/dist/react.d.ts +1119 -7
  26. package/dist/react.js +7122 -52
  27. package/dist/react.js.map +1 -1
  28. package/dist/schema.cjs +215 -0
  29. package/dist/schema.cjs.map +1 -0
  30. package/dist/schema.d.cts +203 -0
  31. package/dist/schema.d.ts +203 -0
  32. package/dist/schema.js +209 -0
  33. package/dist/schema.js.map +1 -0
  34. package/dist/solid.cjs +373 -0
  35. package/dist/solid.cjs.map +1 -0
  36. package/dist/solid.d.cts +242 -0
  37. package/dist/solid.d.ts +242 -0
  38. package/dist/solid.js +366 -0
  39. package/dist/solid.js.map +1 -0
  40. package/dist/svelte.cjs +329 -0
  41. package/dist/svelte.cjs.map +1 -0
  42. package/dist/svelte.js +324 -0
  43. package/dist/svelte.js.map +1 -0
  44. package/dist/testing.cjs +1422 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +339 -0
  47. package/dist/testing.d.ts +339 -0
  48. package/dist/testing.js +1415 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/vue.cjs +1084 -0
  51. package/dist/vue.cjs.map +1 -0
  52. package/dist/vue.js +1072 -0
  53. package/dist/vue.js.map +1 -0
  54. package/dist/web-components.cjs +483 -0
  55. package/dist/web-components.cjs.map +1 -0
  56. package/dist/web-components.d.cts +211 -0
  57. package/dist/web-components.d.ts +211 -0
  58. package/dist/web-components.js +477 -0
  59. package/dist/web-components.js.map +1 -0
  60. package/package.json +126 -3
package/dist/index.cjs CHANGED
@@ -1,9 +1,265 @@
1
1
  'use strict';
2
2
 
3
+ var module$1 = require('module');
4
+ var zod = require('zod');
5
+
6
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
7
+ // src/semver.ts
8
+ var SEMVER_REGEX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
9
+ function parseSemver(input) {
10
+ const match = input.trim().match(SEMVER_REGEX);
11
+ if (!match) return null;
12
+ return {
13
+ major: Number(match[1]),
14
+ minor: Number(match[2]),
15
+ patch: Number(match[3]),
16
+ prerelease: match[4] ? match[4].split(".") : []
17
+ };
18
+ }
19
+ function compareSemver(a, b) {
20
+ const pa = parseSemver(a);
21
+ const pb = parseSemver(b);
22
+ if (!pa || !pb) return 0;
23
+ for (const key of ["major", "minor", "patch"]) {
24
+ if (pa[key] !== pb[key]) return pa[key] - pb[key];
25
+ }
26
+ const aPre = pa.prerelease;
27
+ const bPre = pb.prerelease;
28
+ if (aPre.length === 0 && bPre.length === 0) return 0;
29
+ if (aPre.length === 0) return 1;
30
+ if (bPre.length === 0) return -1;
31
+ const len = Math.max(aPre.length, bPre.length);
32
+ for (let i = 0; i < len; i++) {
33
+ const ai = aPre[i];
34
+ const bi = bPre[i];
35
+ if (ai === void 0) return -1;
36
+ if (bi === void 0) return 1;
37
+ const aNum = Number(ai);
38
+ const bNum = Number(bi);
39
+ const aIsNum = Number.isInteger(aNum);
40
+ const bIsNum = Number.isInteger(bNum);
41
+ if (aIsNum && bIsNum && aNum !== bNum) return aNum - bNum;
42
+ if (aIsNum !== bIsNum) return aIsNum ? -1 : 1;
43
+ if (ai !== bi) return ai < bi ? -1 : 1;
44
+ }
45
+ return 0;
46
+ }
47
+ function parseComparator(comp) {
48
+ const match = comp.trim().match(/^(>=|<=|>|<|=)?\\s*(.+)$/);
49
+ if (!match) return null;
50
+ const op = match[1] || ">=";
51
+ const version = match[2];
52
+ if (!parseSemver(version)) return null;
53
+ return { op, version };
54
+ }
55
+ function satisfiesComparator(version, comp) {
56
+ const diff = compareSemver(version, comp.version);
57
+ switch (comp.op) {
58
+ case ">":
59
+ return diff > 0;
60
+ case ">=":
61
+ return diff >= 0;
62
+ case "<":
63
+ return diff < 0;
64
+ case "<=":
65
+ return diff <= 0;
66
+ case "=":
67
+ return diff === 0;
68
+ default:
69
+ return false;
70
+ }
71
+ }
72
+ function satisfiesRange(version, range) {
73
+ const parts = range.split(/\s+/).filter(Boolean);
74
+ if (parts.length === 0) return true;
75
+ for (const part of parts) {
76
+ const comp = parseComparator(part);
77
+ if (!comp) return false;
78
+ if (!satisfiesComparator(version, comp)) return false;
79
+ }
80
+ return true;
81
+ }
82
+
83
+ // src/triggers.ts
84
+ function wildcardToRegExp(value) {
85
+ const escaped = value.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
86
+ const pattern = `^${escaped.replace(/\*/g, ".*")}$`;
87
+ return new RegExp(pattern);
88
+ }
89
+ function matchPath(path, pattern) {
90
+ if (pattern instanceof RegExp) return pattern.test(path);
91
+ if (!pattern) return false;
92
+ if (pattern.includes("*")) return wildcardToRegExp(pattern).test(path);
93
+ return path === pattern || path.startsWith(pattern);
94
+ }
95
+ function isTriggerMatch(trigger, context) {
96
+ if (!trigger) return true;
97
+ if (!context) return false;
98
+ if (trigger.type === "page") {
99
+ const path = context.path;
100
+ if (!path) return false;
101
+ return matchPath(path, trigger.match);
102
+ }
103
+ if (trigger.type === "usage") {
104
+ const usage = context.usage ?? {};
105
+ const count = usage[trigger.event] ?? 0;
106
+ return count >= (trigger.minActions ?? 1);
107
+ }
108
+ if (trigger.type === "time") {
109
+ const elapsedMs = context.elapsedMs ?? 0;
110
+ return elapsedMs >= trigger.minSeconds * 1e3;
111
+ }
112
+ if (trigger.type === "milestone") {
113
+ return context.milestones?.has(trigger.event) ?? false;
114
+ }
115
+ if (trigger.type === "frustration") {
116
+ const usage = context.usage ?? {};
117
+ const count = usage[trigger.pattern] ?? 0;
118
+ return count >= (trigger.threshold ?? 1);
119
+ }
120
+ if (trigger.type === "scroll") {
121
+ return (context.scrollPercent ?? 0) >= (trigger.minPercent ?? 50);
122
+ }
123
+ try {
124
+ return trigger.evaluate(context);
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ var TriggerEngine = class {
130
+ context;
131
+ constructor(initial) {
132
+ this.context = {
133
+ path: initial?.path,
134
+ events: new Set(initial?.events ?? []),
135
+ milestones: new Set(initial?.milestones ?? []),
136
+ usage: { ...initial?.usage ?? {} },
137
+ elapsedMs: initial?.elapsedMs ?? 0,
138
+ scrollPercent: initial?.scrollPercent ?? 0,
139
+ metadata: { ...initial?.metadata ?? {} }
140
+ };
141
+ }
142
+ setPath(path) {
143
+ this.context.path = path;
144
+ }
145
+ trackEvent(event) {
146
+ const next = new Set(this.context.events ?? /* @__PURE__ */ new Set());
147
+ next.add(event);
148
+ this.context.events = next;
149
+ }
150
+ trackUsage(event, delta = 1) {
151
+ const usage = { ...this.context.usage ?? {} };
152
+ usage[event] = (usage[event] ?? 0) + Math.max(1, delta);
153
+ this.context.usage = usage;
154
+ }
155
+ trackMilestone(event) {
156
+ const next = new Set(this.context.milestones ?? /* @__PURE__ */ new Set());
157
+ next.add(event);
158
+ this.context.milestones = next;
159
+ }
160
+ setElapsedMs(elapsedMs) {
161
+ this.context.elapsedMs = Math.max(0, elapsedMs);
162
+ }
163
+ setScrollPercent(scrollPercent) {
164
+ const clamped = Math.max(0, Math.min(100, scrollPercent));
165
+ this.context.scrollPercent = clamped;
166
+ }
167
+ setMetadata(next) {
168
+ this.context.metadata = { ...next };
169
+ }
170
+ getContext() {
171
+ return {
172
+ path: this.context.path,
173
+ events: new Set(this.context.events ?? []),
174
+ milestones: new Set(this.context.milestones ?? []),
175
+ usage: { ...this.context.usage ?? {} },
176
+ elapsedMs: this.context.elapsedMs,
177
+ scrollPercent: this.context.scrollPercent,
178
+ metadata: { ...this.context.metadata ?? {} }
179
+ };
180
+ }
181
+ evaluate(trigger) {
182
+ return isTriggerMatch(trigger, this.context);
183
+ }
184
+ evaluateFeature(feature) {
185
+ return this.evaluate(feature.trigger);
186
+ }
187
+ };
188
+
3
189
  // src/core.ts
4
- function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date()) {
190
+ function matchesAudience(audience, userContext) {
191
+ if (audience.plan && audience.plan.length > 0) {
192
+ if (!userContext.plan || !audience.plan.includes(userContext.plan)) {
193
+ return false;
194
+ }
195
+ }
196
+ if (audience.role && audience.role.length > 0) {
197
+ if (!userContext.role || !audience.role.includes(userContext.role)) {
198
+ return false;
199
+ }
200
+ }
201
+ if (audience.region && audience.region.length > 0) {
202
+ if (!userContext.region || !audience.region.includes(userContext.region)) {
203
+ return false;
204
+ }
205
+ }
206
+ return true;
207
+ }
208
+ function isAudienceMatch(feature, userContext, matchFn) {
209
+ if (!feature.audience) return true;
210
+ const { plan, role, region, custom } = feature.audience;
211
+ const hasRules = plan && plan.length > 0 || role && role.length > 0 || region && region.length > 0 || custom && Object.keys(custom).length > 0;
212
+ if (!hasRules) return true;
213
+ if (!userContext) return false;
214
+ if (matchFn) return matchFn(feature.audience, userContext);
215
+ return matchesAudience(feature.audience, userContext);
216
+ }
217
+ function isVersionMatch(feature, appVersion) {
218
+ const v = feature.version;
219
+ if (!v || typeof v === "string") return true;
220
+ if (!appVersion) return false;
221
+ if (!v.introduced && !v.showNewUntil && !v.deprecatedAt && !v.showIn) return true;
222
+ if (v.showIn && !satisfiesRange(appVersion, v.showIn)) return false;
223
+ if (v.introduced && compareSemver(appVersion, v.introduced) < 0) return false;
224
+ if (v.deprecatedAt && compareSemver(appVersion, v.deprecatedAt) >= 0) return false;
225
+ if (v.showNewUntil && compareSemver(appVersion, v.showNewUntil) >= 0) return false;
226
+ return true;
227
+ }
228
+ function isDependencyMatch(feature, dismissedIds, dependencyState) {
229
+ const dependsOn = feature.dependsOn;
230
+ if (!dependsOn) return true;
231
+ const seenIds = dependencyState?.seenIds;
232
+ const clickedIds = dependencyState?.clickedIds;
233
+ const dismissedDependencyIds = dependencyState?.dismissedIds ?? dismissedIds;
234
+ if (dependsOn.seen && dependsOn.seen.length > 0) {
235
+ for (const id of dependsOn.seen) {
236
+ const seen = seenIds?.has(id) ?? false;
237
+ if (!seen && !dismissedDependencyIds.has(id)) return false;
238
+ }
239
+ }
240
+ if (dependsOn.clicked && dependsOn.clicked.length > 0) {
241
+ for (const id of dependsOn.clicked) {
242
+ if (!(clickedIds?.has(id) ?? false)) return false;
243
+ }
244
+ }
245
+ if (dependsOn.dismissed && dependsOn.dismissed.length > 0) {
246
+ for (const id of dependsOn.dismissed) {
247
+ if (!dismissedDependencyIds.has(id)) return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
252
+ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
5
253
  if (dismissedIds.has(feature.id)) return false;
254
+ if (!isAudienceMatch(feature, userContext, matchAudience)) return false;
255
+ if (!isDependencyMatch(feature, dismissedIds, dependencyState)) return false;
256
+ if (!isVersionMatch(feature, appVersion)) return false;
257
+ if (!isTriggerMatch(feature.trigger, triggerContext)) return false;
6
258
  const nowMs = now.getTime();
259
+ if (feature.publishAt) {
260
+ const publishMs = new Date(feature.publishAt).getTime();
261
+ if (nowMs < publishMs) return false;
262
+ }
7
263
  const showUntilMs = new Date(feature.showNewUntil).getTime();
8
264
  if (nowMs >= showUntilMs) return false;
9
265
  if (watermark) {
@@ -13,19 +269,70 @@ function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date(
13
269
  }
14
270
  return true;
15
271
  }
16
- function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date()) {
272
+ function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
17
273
  const watermark = storage.getWatermark();
18
274
  const dismissedIds = storage.getDismissedIds();
19
- return manifest.filter((f) => isNew(f, watermark, dismissedIds, now));
275
+ return manifest.filter(
276
+ (f) => isNew(
277
+ f,
278
+ watermark,
279
+ dismissedIds,
280
+ now,
281
+ userContext,
282
+ matchAudience,
283
+ appVersion,
284
+ dependencyState,
285
+ triggerContext
286
+ )
287
+ );
20
288
  }
21
- function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date()) {
22
- return getNewFeatures(manifest, storage, now).length;
289
+ function getNewFeatureCount(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
290
+ return getNewFeatures(
291
+ manifest,
292
+ storage,
293
+ now,
294
+ userContext,
295
+ matchAudience,
296
+ appVersion,
297
+ dependencyState,
298
+ triggerContext
299
+ ).length;
23
300
  }
24
- function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date()) {
301
+ function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
25
302
  const watermark = storage.getWatermark();
26
303
  const dismissedIds = storage.getDismissedIds();
27
304
  return manifest.some(
28
- (f) => f.sidebarKey === sidebarKey && isNew(f, watermark, dismissedIds, now)
305
+ (f) => f.sidebarKey === sidebarKey && isNew(
306
+ f,
307
+ watermark,
308
+ dismissedIds,
309
+ now,
310
+ userContext,
311
+ matchAudience,
312
+ appVersion,
313
+ dependencyState,
314
+ triggerContext
315
+ )
316
+ );
317
+ }
318
+ function getNewFeaturesSorted(manifest, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion, dependencyState, triggerContext) {
319
+ const priorityOrder = { critical: 0, normal: 1, low: 2 };
320
+ return getNewFeatures(
321
+ manifest,
322
+ storage,
323
+ now,
324
+ userContext,
325
+ matchAudience,
326
+ appVersion,
327
+ dependencyState,
328
+ triggerContext
329
+ ).sort(
330
+ (a, b) => {
331
+ const pa = priorityOrder[a.priority ?? "normal"];
332
+ const pb = priorityOrder[b.priority ?? "normal"];
333
+ if (pa !== pb) return pa - pb;
334
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
335
+ }
29
336
  );
30
337
  }
31
338
 
@@ -36,13 +343,1445 @@ function createManifest(entries) {
36
343
  function getFeatureById(manifest, id) {
37
344
  return manifest.find((f) => f.id === id);
38
345
  }
39
- function getNewFeaturesByCategory(manifest, category, storage, now = /* @__PURE__ */ new Date()) {
346
+ function getNewFeaturesByCategory(manifest, category, storage, now = /* @__PURE__ */ new Date(), userContext, matchAudience, appVersion) {
40
347
  const watermark = storage.getWatermark();
41
348
  const dismissedIds = storage.getDismissedIds();
42
349
  return manifest.filter(
43
- (f) => f.category === category && isNew(f, watermark, dismissedIds, now)
350
+ (f) => f.category === category && isNew(f, watermark, dismissedIds, now, userContext, matchAudience, appVersion)
44
351
  );
45
352
  }
353
+ var dynamicRequire = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
354
+ var cachedMarked = null;
355
+ var cachedShiki = null;
356
+ function optionalRequire(name) {
357
+ try {
358
+ return dynamicRequire(name);
359
+ } catch (error) {
360
+ if (error && typeof error === "object" && "code" in error && error.code === "MODULE_NOT_FOUND") {
361
+ return null;
362
+ }
363
+ return null;
364
+ }
365
+ }
366
+ function getMarked() {
367
+ if (cachedMarked !== null) return cachedMarked || null;
368
+ cachedMarked = optionalRequire("marked") ?? false;
369
+ return cachedMarked || null;
370
+ }
371
+ function getShiki() {
372
+ if (cachedShiki !== null) return cachedShiki || null;
373
+ cachedShiki = optionalRequire("shiki") ?? false;
374
+ return cachedShiki || null;
375
+ }
376
+ function escapeHtml(value) {
377
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
378
+ }
379
+ function sanitizeUrl(url) {
380
+ if (!url) return null;
381
+ const trimmed = url.trim();
382
+ if (!trimmed) return null;
383
+ const lower = trimmed.toLowerCase();
384
+ if (lower.startsWith("javascript:")) return null;
385
+ if (lower.startsWith("data:")) return null;
386
+ if (lower.startsWith("vbscript:")) return null;
387
+ if (/['"<>\s]/.test(trimmed)) return null;
388
+ return trimmed;
389
+ }
390
+ function sanitizeHtml(html) {
391
+ return html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, "").replace(/\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/\s+(?:href|src|xlink:href)\s*=\s*("|')(?:javascript:|data:)[^"']*\1/gi, "");
392
+ }
393
+ function decodeAllowedEntities(html) {
394
+ const allowTags = [
395
+ "p",
396
+ "strong",
397
+ "em",
398
+ "a",
399
+ "code",
400
+ "pre",
401
+ "img",
402
+ "ul",
403
+ "ol",
404
+ "li",
405
+ "blockquote",
406
+ "h1",
407
+ "h2",
408
+ "h3",
409
+ "h4",
410
+ "h5",
411
+ "h6",
412
+ "br"
413
+ ];
414
+ return html.replace(/&lt;(\/?)([a-z0-9]+)([^>]*)&gt;/gi, (match, slash, tag, rest) => {
415
+ if (!allowTags.includes(tag.toLowerCase())) return match;
416
+ const decodedRest = rest.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
417
+ return `<${slash}${tag}${decodedRest}>`;
418
+ });
419
+ }
420
+ function renderCodeBlock(code, language) {
421
+ const shiki = getShiki();
422
+ if (shiki?.codeToHtml) {
423
+ try {
424
+ const rendered = shiki.codeToHtml(code, { lang: language || "text", theme: "github-dark" });
425
+ if (typeof rendered === "string") return rendered;
426
+ } catch {
427
+ }
428
+ }
429
+ const langAttr = language ? ` class="language-${escapeHtml(language)}"` : "";
430
+ return `<pre><code${langAttr}>${escapeHtml(code)}</code></pre>`;
431
+ }
432
+ function inlineMarkdown(text) {
433
+ let result = escapeHtml(text);
434
+ const codeSpans = [];
435
+ result = result.replace(/`([^`]+)`/g, (_match, code) => {
436
+ const idx = codeSpans.length;
437
+ codeSpans.push(`<code>${escapeHtml(code)}</code>`);
438
+ return `\xA7\xA7CODE${idx}\xA7\xA7`;
439
+ });
440
+ result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, url) => {
441
+ const safeUrl = sanitizeUrl(url);
442
+ const safeAlt = escapeHtml(alt ?? "");
443
+ if (!safeUrl) return safeAlt;
444
+ return `<img src="${escapeHtml(safeUrl)}" alt="${safeAlt}" />`;
445
+ });
446
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => {
447
+ const safeUrl = sanitizeUrl(url);
448
+ const safeLabel = escapeHtml(label ?? "");
449
+ if (!safeUrl) return safeLabel;
450
+ return `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
451
+ });
452
+ result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
453
+ result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
454
+ result = result.replace(/§§CODE(\d+)§§/g, (_m, idx) => codeSpans[Number(idx)] ?? "");
455
+ return result;
456
+ }
457
+ function fallbackParse(markdown) {
458
+ const lines = markdown.split(/\r?\n/);
459
+ const blocks = [];
460
+ let listBuffer = null;
461
+ let quoteBuffer = null;
462
+ let inCodeBlock = false;
463
+ let codeLang;
464
+ let codeLines = [];
465
+ const flushList = () => {
466
+ if (!listBuffer) return;
467
+ blocks.push(`<ul>${listBuffer.map((item) => `<li>${item}</li>`).join("")}</ul>`);
468
+ listBuffer = null;
469
+ };
470
+ const flushQuote = () => {
471
+ if (!quoteBuffer) return;
472
+ const content = quoteBuffer.map((line) => inlineMarkdown(line.trim())).join("<br>");
473
+ blocks.push(`<blockquote>${content}</blockquote>`);
474
+ quoteBuffer = null;
475
+ };
476
+ const flushCode = () => {
477
+ if (!inCodeBlock) return;
478
+ blocks.push(renderCodeBlock(codeLines.join("\n"), codeLang));
479
+ codeLines = [];
480
+ codeLang = void 0;
481
+ inCodeBlock = false;
482
+ };
483
+ for (const rawLine of lines) {
484
+ const line = rawLine.replace(/\s+$/, "");
485
+ const codeFence = line.match(/^```(.*)$/);
486
+ if (codeFence) {
487
+ if (inCodeBlock) {
488
+ flushCode();
489
+ } else {
490
+ flushList();
491
+ flushQuote();
492
+ inCodeBlock = true;
493
+ codeLang = codeFence[1]?.trim() || void 0;
494
+ codeLines = [];
495
+ }
496
+ continue;
497
+ }
498
+ if (inCodeBlock) {
499
+ codeLines.push(rawLine);
500
+ continue;
501
+ }
502
+ const listMatch = line.match(/^\s*[-*+]\s+(.*)$/);
503
+ if (listMatch) {
504
+ flushQuote();
505
+ listBuffer = listBuffer ?? [];
506
+ listBuffer.push(inlineMarkdown(listMatch[1].trim()));
507
+ continue;
508
+ }
509
+ if (listBuffer) flushList();
510
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
511
+ if (headingMatch) {
512
+ flushQuote();
513
+ const level = headingMatch[1].length;
514
+ const content = inlineMarkdown(headingMatch[2].trim());
515
+ blocks.push(`<h${level}>${content}</h${level}>`);
516
+ continue;
517
+ }
518
+ const quoteMatch = line.match(/^>\s?(.*)$/);
519
+ if (quoteMatch) {
520
+ quoteBuffer = quoteBuffer ?? [];
521
+ quoteBuffer.push(quoteMatch[1]);
522
+ continue;
523
+ }
524
+ if (quoteBuffer) flushQuote();
525
+ if (!line.trim()) {
526
+ continue;
527
+ }
528
+ blocks.push(`<p>${inlineMarkdown(line.trim())}</p>`);
529
+ }
530
+ flushList();
531
+ flushQuote();
532
+ flushCode();
533
+ return blocks.join("\n");
534
+ }
535
+ function renderWithMarked(markdown, marked) {
536
+ if (!marked.parse) return null;
537
+ const renderer = marked.Renderer ? new marked.Renderer() : void 0;
538
+ if (renderer) {
539
+ renderer.link = (href, _title, text) => {
540
+ const safeUrl = sanitizeUrl(href);
541
+ if (!safeUrl) return escapeHtml(text);
542
+ return `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
543
+ };
544
+ renderer.image = (href, _title, text) => {
545
+ const safeUrl = sanitizeUrl(href);
546
+ const safeAlt = escapeHtml(text ?? "");
547
+ if (!safeUrl) return safeAlt;
548
+ return `<img src="${escapeHtml(safeUrl)}" alt="${safeAlt}" />`;
549
+ };
550
+ }
551
+ const output = marked.parse(markdown, renderer ? { renderer } : void 0);
552
+ if (typeof output === "string") return output;
553
+ return output ? String(output) : null;
554
+ }
555
+ function parseDescription(markdown) {
556
+ if (!markdown) return "";
557
+ const marked = getMarked();
558
+ if (marked) {
559
+ try {
560
+ const rendered = renderWithMarked(markdown, marked);
561
+ if (rendered) {
562
+ const sanitized2 = sanitizeHtml(rendered);
563
+ const decoded2 = decodeAllowedEntities(sanitized2);
564
+ return sanitizeHtml(decoded2);
565
+ }
566
+ } catch {
567
+ }
568
+ }
569
+ if (/<[^>]+>/.test(markdown)) {
570
+ const sanitized2 = sanitizeHtml(markdown);
571
+ const decoded2 = decodeAllowedEntities(sanitized2);
572
+ return sanitizeHtml(decoded2);
573
+ }
574
+ const fallback = fallbackParse(markdown);
575
+ const sanitized = sanitizeHtml(fallback);
576
+ const decoded = decodeAllowedEntities(sanitized);
577
+ return sanitizeHtml(decoded);
578
+ }
579
+
580
+ // src/theme.ts
581
+ var LIGHT_THEME = {
582
+ colors: {
583
+ primary: "#2563eb",
584
+ background: "#ffffff",
585
+ text: "#111827",
586
+ textMuted: "#6b7280",
587
+ border: "#e5e7eb",
588
+ success: "#16a34a",
589
+ warning: "#f59e0b",
590
+ error: "#dc2626"
591
+ },
592
+ fonts: {
593
+ family: "system-ui, -apple-system, Segoe UI, sans-serif",
594
+ sizeBase: "14px",
595
+ sizeSm: "12px",
596
+ sizeLg: "16px"
597
+ },
598
+ spacing: {
599
+ xs: "4px",
600
+ sm: "8px",
601
+ md: "12px",
602
+ lg: "16px",
603
+ xl: "24px"
604
+ },
605
+ radii: {
606
+ sm: "6px",
607
+ md: "8px",
608
+ lg: "12px",
609
+ full: "999px"
610
+ },
611
+ shadows: {
612
+ sm: "0 2px 8px rgba(0, 0, 0, 0.08)",
613
+ md: "0 8px 24px rgba(0, 0, 0, 0.12)",
614
+ lg: "0 20px 60px rgba(0, 0, 0, 0.16)"
615
+ },
616
+ zIndex: {
617
+ base: 9998,
618
+ tooltip: 1e4,
619
+ modal: 10001,
620
+ overlay: 9997
621
+ }
622
+ };
623
+ var DARK_THEME = {
624
+ ...LIGHT_THEME,
625
+ colors: {
626
+ primary: "#60a5fa",
627
+ background: "#0b1220",
628
+ text: "#f3f4f6",
629
+ textMuted: "#9ca3af",
630
+ border: "#1f2937",
631
+ success: "#4ade80",
632
+ warning: "#fbbf24",
633
+ error: "#f87171"
634
+ },
635
+ shadows: {
636
+ sm: "0 2px 8px rgba(0, 0, 0, 0.35)",
637
+ md: "0 8px 24px rgba(0, 0, 0, 0.42)",
638
+ lg: "0 20px 60px rgba(0, 0, 0, 0.52)"
639
+ }
640
+ };
641
+ var MINIMAL_THEME = {
642
+ ...LIGHT_THEME,
643
+ colors: {
644
+ ...LIGHT_THEME.colors,
645
+ primary: "#111827",
646
+ background: "#ffffff",
647
+ text: "#111827",
648
+ textMuted: "#6b7280",
649
+ border: "#d1d5db",
650
+ success: "#111827",
651
+ warning: "#111827",
652
+ error: "#111827"
653
+ },
654
+ shadows: {
655
+ sm: "none",
656
+ md: "none",
657
+ lg: "none"
658
+ },
659
+ radii: {
660
+ sm: "0",
661
+ md: "0",
662
+ lg: "0",
663
+ full: "0"
664
+ }
665
+ };
666
+ var VIBRANT_THEME = {
667
+ ...LIGHT_THEME,
668
+ colors: {
669
+ primary: "#ec4899",
670
+ background: "#fff7ed",
671
+ text: "#3f1d57",
672
+ textMuted: "#6d4c84",
673
+ border: "#fdba74",
674
+ success: "#10b981",
675
+ warning: "#f59e0b",
676
+ error: "#ef4444"
677
+ },
678
+ shadows: {
679
+ sm: "0 2px 10px rgba(236, 72, 153, 0.15)",
680
+ md: "0 10px 26px rgba(236, 72, 153, 0.22)",
681
+ lg: "0 22px 58px rgba(236, 72, 153, 0.28)"
682
+ }
683
+ };
684
+ var FEATUREDROP_THEMES = {
685
+ light: LIGHT_THEME,
686
+ dark: DARK_THEME,
687
+ minimal: MINIMAL_THEME,
688
+ vibrant: VIBRANT_THEME
689
+ };
690
+ function isThemePreset(value) {
691
+ return value === "light" || value === "dark" || value === "auto" || value === "minimal" || value === "vibrant";
692
+ }
693
+ function mergeTheme(base, overrides) {
694
+ if (!overrides) return base;
695
+ return {
696
+ colors: {
697
+ ...base.colors,
698
+ ...overrides.colors ?? {}
699
+ },
700
+ fonts: {
701
+ ...base.fonts,
702
+ ...overrides.fonts ?? {}
703
+ },
704
+ spacing: {
705
+ ...base.spacing,
706
+ ...overrides.spacing ?? {}
707
+ },
708
+ radii: {
709
+ ...base.radii,
710
+ ...overrides.radii ?? {}
711
+ },
712
+ shadows: {
713
+ ...base.shadows,
714
+ ...overrides.shadows ?? {}
715
+ },
716
+ zIndex: {
717
+ ...base.zIndex,
718
+ ...overrides.zIndex ?? {}
719
+ }
720
+ };
721
+ }
722
+ function createTheme(overrides, base = LIGHT_THEME) {
723
+ return mergeTheme(base, overrides);
724
+ }
725
+ function resolveTheme(input = "light", options = {}) {
726
+ if (isThemePreset(input)) {
727
+ if (input === "auto") {
728
+ return options.prefersDark ? DARK_THEME : LIGHT_THEME;
729
+ }
730
+ return FEATUREDROP_THEMES[input];
731
+ }
732
+ return mergeTheme(LIGHT_THEME, input);
733
+ }
734
+ function applyThemeSection(vars, key, values) {
735
+ for (const [token, value] of Object.entries(values)) {
736
+ vars[`--featuredrop-${key}-${token}`] = value;
737
+ }
738
+ }
739
+ function themeToCSSVariables(theme) {
740
+ const vars = {};
741
+ applyThemeSection(vars, "color", theme.colors);
742
+ applyThemeSection(vars, "font", theme.fonts);
743
+ applyThemeSection(vars, "space", theme.spacing);
744
+ applyThemeSection(vars, "radius", theme.radii);
745
+ applyThemeSection(vars, "shadow", theme.shadows);
746
+ applyThemeSection(vars, "z", theme.zIndex);
747
+ vars["--featuredrop-font-family"] = theme.fonts.family;
748
+ vars["--featuredrop-widget-bg"] = theme.colors.background;
749
+ vars["--featuredrop-trigger-bg"] = theme.colors.background;
750
+ vars["--featuredrop-trigger-color"] = theme.colors.text;
751
+ vars["--featuredrop-entry-title-color"] = theme.colors.text;
752
+ vars["--featuredrop-entry-desc-color"] = theme.colors.textMuted;
753
+ vars["--featuredrop-title-color"] = theme.colors.text;
754
+ vars["--featuredrop-border-color"] = theme.colors.border;
755
+ vars["--featuredrop-cta-bg"] = theme.colors.primary;
756
+ vars["--featuredrop-cta-color"] = theme.colors.background;
757
+ vars["--featuredrop-mark-all-color"] = theme.colors.primary;
758
+ vars["--featuredrop-widget-shadow"] = theme.shadows.md;
759
+ vars["--featuredrop-widget-radius"] = theme.radii.lg;
760
+ vars["--featuredrop-trigger-radius"] = theme.radii.md;
761
+ vars["--featuredrop-badge-bg"] = theme.colors.warning;
762
+ vars["--featuredrop-z-index"] = theme.zIndex.base;
763
+ vars["--featuredrop-toast-z-index"] = theme.zIndex.tooltip;
764
+ vars["--featuredrop-tour-z-index"] = theme.zIndex.modal;
765
+ vars["--featuredrop-tour-overlay-z-index"] = theme.zIndex.overlay;
766
+ return vars;
767
+ }
768
+
769
+ // src/i18n.ts
770
+ var EN_TRANSLATIONS = {
771
+ newBadge: "New",
772
+ whatsNewTitle: "What's New",
773
+ markAllRead: "Mark all as read",
774
+ allCaughtUp: "You're all caught up!",
775
+ close: "Close",
776
+ changelogTitle: "Changelog",
777
+ searchPlaceholder: "Search updates",
778
+ allCategories: "All categories",
779
+ noUpdatesYet: "No updates yet",
780
+ loadMore: "Load more",
781
+ share: "Share",
782
+ skipToEntries: "Skip to changelog entries",
783
+ stepOf: (current, total) => `Step ${current} of ${total}`,
784
+ back: "Back",
785
+ next: "Next",
786
+ skip: "Skip",
787
+ finish: "Finish",
788
+ gotIt: "Got it",
789
+ announcement: "Announcement",
790
+ feedbackTitle: "Share feedback",
791
+ feedbackTrigger: "Feedback",
792
+ feedbackSubmitted: "Thanks for the feedback.",
793
+ submit: "Submit",
794
+ cancel: "Cancel",
795
+ askLater: "Ask me later"
796
+ };
797
+ var SIMPLE_TRANSLATIONS = {
798
+ es: {
799
+ newBadge: "Nuevo",
800
+ whatsNewTitle: "Novedades",
801
+ markAllRead: "Marcar todo como le\xEDdo",
802
+ allCaughtUp: "Est\xE1s al d\xEDa.",
803
+ close: "Cerrar",
804
+ changelogTitle: "Registro de cambios",
805
+ searchPlaceholder: "Buscar actualizaciones",
806
+ allCategories: "Todas las categor\xEDas",
807
+ noUpdatesYet: "A\xFAn no hay actualizaciones",
808
+ loadMore: "Cargar m\xE1s",
809
+ share: "Compartir",
810
+ skipToEntries: "Saltar a las entradas del changelog",
811
+ back: "Atr\xE1s",
812
+ next: "Siguiente",
813
+ skip: "Saltar",
814
+ finish: "Finalizar",
815
+ gotIt: "Entendido",
816
+ announcement: "Anuncio",
817
+ feedbackTitle: "Enviar comentarios",
818
+ feedbackTrigger: "Comentarios",
819
+ feedbackSubmitted: "Gracias por tus comentarios.",
820
+ submit: "Enviar",
821
+ cancel: "Cancelar",
822
+ askLater: "Preguntar m\xE1s tarde"
823
+ },
824
+ fr: {
825
+ newBadge: "Nouveau",
826
+ whatsNewTitle: "Nouveaut\xE9s",
827
+ markAllRead: "Tout marquer comme lu",
828
+ allCaughtUp: "Vous \xEAtes \xE0 jour.",
829
+ close: "Fermer",
830
+ changelogTitle: "Journal des changements",
831
+ searchPlaceholder: "Rechercher des mises \xE0 jour",
832
+ allCategories: "Toutes les cat\xE9gories",
833
+ noUpdatesYet: "Aucune mise \xE0 jour",
834
+ loadMore: "Charger plus",
835
+ share: "Partager",
836
+ skipToEntries: "Aller aux entr\xE9es du changelog",
837
+ back: "Retour",
838
+ next: "Suivant",
839
+ skip: "Passer",
840
+ finish: "Terminer",
841
+ gotIt: "Compris",
842
+ announcement: "Annonce",
843
+ feedbackTitle: "Partager un avis",
844
+ feedbackTrigger: "Avis",
845
+ feedbackSubmitted: "Merci pour votre avis.",
846
+ submit: "Envoyer",
847
+ cancel: "Annuler",
848
+ askLater: "Demander plus tard"
849
+ },
850
+ de: {
851
+ newBadge: "Neu",
852
+ whatsNewTitle: "Neuigkeiten",
853
+ markAllRead: "Alles als gelesen markieren",
854
+ allCaughtUp: "Alles erledigt.",
855
+ close: "Schlie\xDFen",
856
+ changelogTitle: "\xC4nderungsprotokoll",
857
+ searchPlaceholder: "Updates suchen",
858
+ allCategories: "Alle Kategorien",
859
+ noUpdatesYet: "Noch keine Updates",
860
+ loadMore: "Mehr laden",
861
+ share: "Teilen",
862
+ skipToEntries: "Zu den Eintr\xE4gen springen",
863
+ back: "Zur\xFCck",
864
+ next: "Weiter",
865
+ skip: "\xDCberspringen",
866
+ finish: "Fertig",
867
+ gotIt: "Verstanden",
868
+ announcement: "Ank\xFCndigung",
869
+ feedbackTitle: "Feedback teilen",
870
+ feedbackTrigger: "Feedback",
871
+ feedbackSubmitted: "Danke f\xFCr dein Feedback.",
872
+ submit: "Senden",
873
+ cancel: "Abbrechen",
874
+ askLater: "Sp\xE4ter fragen"
875
+ },
876
+ pt: {
877
+ newBadge: "Novo",
878
+ whatsNewTitle: "Novidades",
879
+ markAllRead: "Marcar tudo como lido",
880
+ allCaughtUp: "Tudo em dia.",
881
+ close: "Fechar",
882
+ changelogTitle: "Hist\xF3rico de mudan\xE7as",
883
+ searchPlaceholder: "Buscar atualiza\xE7\xF5es",
884
+ allCategories: "Todas as categorias",
885
+ noUpdatesYet: "Sem atualiza\xE7\xF5es ainda",
886
+ loadMore: "Carregar mais",
887
+ share: "Compartilhar",
888
+ skipToEntries: "Ir para entradas do changelog",
889
+ back: "Voltar",
890
+ next: "Pr\xF3ximo",
891
+ skip: "Pular",
892
+ finish: "Concluir",
893
+ gotIt: "Entendi",
894
+ announcement: "An\xFAncio",
895
+ feedbackTitle: "Enviar feedback",
896
+ feedbackTrigger: "Feedback",
897
+ feedbackSubmitted: "Obrigado pelo feedback.",
898
+ submit: "Enviar",
899
+ cancel: "Cancelar",
900
+ askLater: "Perguntar depois"
901
+ },
902
+ "zh-cn": {
903
+ newBadge: "\u65B0",
904
+ whatsNewTitle: "\u6700\u65B0\u52A8\u6001",
905
+ markAllRead: "\u5168\u90E8\u6807\u8BB0\u4E3A\u5DF2\u8BFB",
906
+ allCaughtUp: "\u4F60\u5DF2\u67E5\u770B\u5168\u90E8\u66F4\u65B0\u3002",
907
+ close: "\u5173\u95ED",
908
+ changelogTitle: "\u66F4\u65B0\u65E5\u5FD7",
909
+ searchPlaceholder: "\u641C\u7D22\u66F4\u65B0",
910
+ allCategories: "\u5168\u90E8\u5206\u7C7B",
911
+ noUpdatesYet: "\u6682\u65E0\u66F4\u65B0",
912
+ loadMore: "\u52A0\u8F7D\u66F4\u591A",
913
+ share: "\u5206\u4EAB",
914
+ skipToEntries: "\u8DF3\u8F6C\u5230\u66F4\u65B0\u6761\u76EE",
915
+ back: "\u8FD4\u56DE",
916
+ next: "\u4E0B\u4E00\u6B65",
917
+ skip: "\u8DF3\u8FC7",
918
+ finish: "\u5B8C\u6210",
919
+ gotIt: "\u77E5\u9053\u4E86",
920
+ announcement: "\u516C\u544A",
921
+ feedbackTitle: "\u63D0\u4EA4\u53CD\u9988",
922
+ feedbackTrigger: "\u53CD\u9988",
923
+ feedbackSubmitted: "\u611F\u8C22\u4F60\u7684\u53CD\u9988\u3002",
924
+ submit: "\u63D0\u4EA4",
925
+ cancel: "\u53D6\u6D88",
926
+ askLater: "\u7A0D\u540E\u8BE2\u95EE"
927
+ },
928
+ ja: {
929
+ newBadge: "\u65B0\u7740",
930
+ whatsNewTitle: "\u65B0\u6A5F\u80FD",
931
+ markAllRead: "\u3059\u3079\u3066\u65E2\u8AAD\u306B\u3059\u308B",
932
+ allCaughtUp: "\u3059\u3079\u3066\u78BA\u8A8D\u6E08\u307F\u3067\u3059\u3002",
933
+ close: "\u9589\u3058\u308B",
934
+ changelogTitle: "\u5909\u66F4\u5C65\u6B74",
935
+ searchPlaceholder: "\u66F4\u65B0\u3092\u691C\u7D22",
936
+ allCategories: "\u3059\u3079\u3066\u306E\u30AB\u30C6\u30B4\u30EA",
937
+ noUpdatesYet: "\u66F4\u65B0\u306F\u3042\u308A\u307E\u305B\u3093",
938
+ loadMore: "\u3055\u3089\u306B\u8868\u793A",
939
+ share: "\u5171\u6709",
940
+ skipToEntries: "\u5909\u66F4\u5C65\u6B74\u3078\u79FB\u52D5",
941
+ back: "\u623B\u308B",
942
+ next: "\u6B21\u3078",
943
+ skip: "\u30B9\u30AD\u30C3\u30D7",
944
+ finish: "\u5B8C\u4E86",
945
+ gotIt: "\u4E86\u89E3",
946
+ announcement: "\u304A\u77E5\u3089\u305B",
947
+ feedbackTitle: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3092\u9001\u4FE1",
948
+ feedbackTrigger: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF",
949
+ feedbackSubmitted: "\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3042\u308A\u304C\u3068\u3046\u3054\u3056\u3044\u307E\u3059\u3002",
950
+ submit: "\u9001\u4FE1",
951
+ cancel: "\u30AD\u30E3\u30F3\u30BB\u30EB",
952
+ askLater: "\u5F8C\u3067\u805E\u304F"
953
+ },
954
+ ko: {
955
+ newBadge: "\uC0C8\uB85C\uC6C0",
956
+ whatsNewTitle: "\uC0C8 \uC18C\uC2DD",
957
+ markAllRead: "\uBAA8\uB450 \uC77D\uC74C \uCC98\uB9AC",
958
+ allCaughtUp: "\uBAA8\uB4E0 \uC5C5\uB370\uC774\uD2B8\uB97C \uD655\uC778\uD588\uC2B5\uB2C8\uB2E4.",
959
+ close: "\uB2EB\uAE30",
960
+ changelogTitle: "\uBCC0\uACBD \uB85C\uADF8",
961
+ searchPlaceholder: "\uC5C5\uB370\uC774\uD2B8 \uAC80\uC0C9",
962
+ allCategories: "\uC804\uCCB4 \uCE74\uD14C\uACE0\uB9AC",
963
+ noUpdatesYet: "\uC5C5\uB370\uC774\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
964
+ loadMore: "\uB354 \uBCF4\uAE30",
965
+ share: "\uACF5\uC720",
966
+ skipToEntries: "\uBCC0\uACBD \uD56D\uBAA9\uC73C\uB85C \uC774\uB3D9",
967
+ back: "\uB4A4\uB85C",
968
+ next: "\uB2E4\uC74C",
969
+ skip: "\uAC74\uB108\uB6F0\uAE30",
970
+ finish: "\uC644\uB8CC",
971
+ gotIt: "\uD655\uC778",
972
+ announcement: "\uACF5\uC9C0",
973
+ feedbackTitle: "\uD53C\uB4DC\uBC31 \uBCF4\uB0B4\uAE30",
974
+ feedbackTrigger: "\uD53C\uB4DC\uBC31",
975
+ feedbackSubmitted: "\uD53C\uB4DC\uBC31 \uAC10\uC0AC\uD569\uB2C8\uB2E4.",
976
+ submit: "\uC81C\uCD9C",
977
+ cancel: "\uCDE8\uC18C",
978
+ askLater: "\uB098\uC911\uC5D0 \uBB3B\uAE30"
979
+ },
980
+ ar: {
981
+ newBadge: "\u062C\u062F\u064A\u062F",
982
+ whatsNewTitle: "\u0645\u0627 \u0627\u0644\u062C\u062F\u064A\u062F",
983
+ markAllRead: "\u062A\u062D\u062F\u064A\u062F \u0627\u0644\u0643\u0644 \u0643\u0645\u0642\u0631\u0648\u0621",
984
+ allCaughtUp: "\u062A\u0645\u062A \u0645\u062A\u0627\u0628\u0639\u0629 \u0643\u0644 \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A.",
985
+ close: "\u0625\u063A\u0644\u0627\u0642",
986
+ changelogTitle: "\u0633\u062C\u0644 \u0627\u0644\u062A\u063A\u064A\u064A\u0631\u0627\u062A",
987
+ searchPlaceholder: "\u0627\u0628\u062D\u062B \u0641\u064A \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A",
988
+ allCategories: "\u0643\u0644 \u0627\u0644\u0641\u0626\u0627\u062A",
989
+ noUpdatesYet: "\u0644\u0627 \u062A\u0648\u062C\u062F \u062A\u062D\u062F\u064A\u062B\u0627\u062A \u0628\u0639\u062F",
990
+ loadMore: "\u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0645\u0632\u064A\u062F",
991
+ share: "\u0645\u0634\u0627\u0631\u0643\u0629",
992
+ skipToEntries: "\u062A\u062E\u0637\u064A \u0625\u0644\u0649 \u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0633\u062C\u0644",
993
+ back: "\u0631\u062C\u0648\u0639",
994
+ next: "\u0627\u0644\u062A\u0627\u0644\u064A",
995
+ skip: "\u062A\u062E\u0637\u064A",
996
+ finish: "\u0625\u0646\u0647\u0627\u0621",
997
+ gotIt: "\u062A\u0645",
998
+ announcement: "\u0625\u0639\u0644\u0627\u0646",
999
+ feedbackTitle: "\u0634\u0627\u0631\u0643 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643",
1000
+ feedbackTrigger: "\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
1001
+ feedbackSubmitted: "\u0634\u0643\u0631\u064B\u0627 \u0639\u0644\u0649 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643.",
1002
+ submit: "\u0625\u0631\u0633\u0627\u0644",
1003
+ cancel: "\u0625\u0644\u063A\u0627\u0621",
1004
+ askLater: "\u0627\u0633\u0623\u0644\u0646\u064A \u0644\u0627\u062D\u0642\u064B\u0627"
1005
+ },
1006
+ hi: {
1007
+ newBadge: "\u0928\u092F\u093E",
1008
+ whatsNewTitle: "\u0928\u092F\u093E \u0915\u094D\u092F\u093E \u0939\u0948",
1009
+ markAllRead: "\u0938\u092D\u0940 \u0915\u094B \u092A\u0922\u093C\u093E \u0939\u0941\u0906 \u091A\u093F\u0939\u094D\u0928\u093F\u0924 \u0915\u0930\u0947\u0902",
1010
+ allCaughtUp: "\u0906\u092A\u0928\u0947 \u0938\u092D\u0940 \u0905\u092A\u0921\u0947\u091F \u0926\u0947\u0916 \u0932\u093F\u090F \u0939\u0948\u0902\u0964",
1011
+ close: "\u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
1012
+ changelogTitle: "\u092A\u0930\u093F\u0935\u0930\u094D\u0924\u0928 \u0938\u0942\u091A\u0940",
1013
+ searchPlaceholder: "\u0905\u092A\u0921\u0947\u091F \u0916\u094B\u091C\u0947\u0902",
1014
+ allCategories: "\u0938\u092D\u0940 \u0936\u094D\u0930\u0947\u0923\u093F\u092F\u093E\u0902",
1015
+ noUpdatesYet: "\u0905\u092D\u0940 \u0915\u094B\u0908 \u0905\u092A\u0921\u0947\u091F \u0928\u0939\u0940\u0902",
1016
+ loadMore: "\u0914\u0930 \u0932\u094B\u0921 \u0915\u0930\u0947\u0902",
1017
+ share: "\u0938\u093E\u091D\u093E \u0915\u0930\u0947\u0902",
1018
+ skipToEntries: "\u091A\u0947\u0902\u091C\u0932\u0949\u0917 \u092A\u094D\u0930\u0935\u093F\u0937\u094D\u091F\u093F\u092F\u094B\u0902 \u092A\u0930 \u091C\u093E\u090F\u0902",
1019
+ back: "\u0935\u093E\u092A\u0938",
1020
+ next: "\u0905\u0917\u0932\u093E",
1021
+ skip: "\u091B\u094B\u0921\u093C\u0947\u0902",
1022
+ finish: "\u0938\u092E\u093E\u092A\u094D\u0924",
1023
+ gotIt: "\u0920\u0940\u0915 \u0939\u0948",
1024
+ announcement: "\u0918\u094B\u0937\u0923\u093E",
1025
+ feedbackTitle: "\u092B\u0940\u0921\u092C\u0948\u0915 \u0938\u093E\u091D\u093E \u0915\u0930\u0947\u0902",
1026
+ feedbackTrigger: "\u092B\u0940\u0921\u092C\u0948\u0915",
1027
+ feedbackSubmitted: "\u092B\u0940\u0921\u092C\u0948\u0915 \u0915\u0947 \u0932\u093F\u090F \u0927\u0928\u094D\u092F\u0935\u093E\u0926\u0964",
1028
+ submit: "\u091C\u092E\u093E \u0915\u0930\u0947\u0902",
1029
+ cancel: "\u0930\u0926\u094D\u0926 \u0915\u0930\u0947\u0902",
1030
+ askLater: "\u092C\u093E\u0926 \u092E\u0947\u0902 \u092A\u0942\u091B\u0947\u0902"
1031
+ }
1032
+ };
1033
+ function resolveTranslations(locale, overrides) {
1034
+ const normalizedLocale = (locale ?? "en").toLowerCase();
1035
+ const base = SIMPLE_TRANSLATIONS[normalizedLocale] ?? SIMPLE_TRANSLATIONS[normalizedLocale.split("-")[0]] ?? {};
1036
+ return {
1037
+ ...EN_TRANSLATIONS,
1038
+ ...base,
1039
+ ...overrides ?? {},
1040
+ stepOf: overrides?.stepOf ?? EN_TRANSLATIONS.stepOf
1041
+ };
1042
+ }
1043
+ var FEATUREDROP_TRANSLATIONS = {
1044
+ en: EN_TRANSLATIONS,
1045
+ ...SIMPLE_TRANSLATIONS
1046
+ };
1047
+
1048
+ // src/rss.ts
1049
+ function escape(str) {
1050
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1051
+ }
1052
+ function generateRSS(manifest, options) {
1053
+ const title = escape(options?.title ?? "Featuredrop Changelog");
1054
+ const link = escape(options?.link ?? "");
1055
+ const desc = escape(options?.description ?? "Product updates");
1056
+ const items = manifest.slice().sort((a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()).map((item) => {
1057
+ const descriptionHtml = item.description ? parseDescription(item.description) : "";
1058
+ const itemLink = item.url ? escape(item.url) : "";
1059
+ return [
1060
+ "<item>",
1061
+ `<title>${escape(item.label)}</title>`,
1062
+ itemLink ? `<link>${itemLink}</link>` : "",
1063
+ `<guid isPermaLink="false">${escape(item.id)}</guid>`,
1064
+ `<pubDate>${new Date(item.releasedAt).toUTCString()}</pubDate>`,
1065
+ `<description><![CDATA[${descriptionHtml}]]></description>`,
1066
+ "</item>"
1067
+ ].join("");
1068
+ }).join("");
1069
+ return [
1070
+ '<?xml version="1.0" encoding="UTF-8"?>',
1071
+ '<rss version="2.0">',
1072
+ "<channel>",
1073
+ `<title>${title}</title>`,
1074
+ link ? `<link>${link}</link>` : "",
1075
+ `<description>${desc}</description>`,
1076
+ items,
1077
+ "</channel>",
1078
+ "</rss>"
1079
+ ].join("");
1080
+ }
1081
+
1082
+ // src/throttle.ts
1083
+ function sortByPriorityAndRecency(features) {
1084
+ const priorityWeight = { critical: 3, normal: 2, low: 1 };
1085
+ return [...features].sort((a, b) => {
1086
+ const scoreA = priorityWeight[a.priority ?? "normal"];
1087
+ const scoreB = priorityWeight[b.priority ?? "normal"];
1088
+ if (scoreA !== scoreB) return scoreB - scoreA;
1089
+ return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime();
1090
+ });
1091
+ }
1092
+ function applyAnnouncementThrottle(features, options, state, now = Date.now()) {
1093
+ const sorted = sortByPriorityAndRecency(features);
1094
+ if (sorted.length === 0) {
1095
+ return { visible: [], queued: [] };
1096
+ }
1097
+ if (options?.sessionCooldown && options.sessionCooldown > 0) {
1098
+ const elapsed = now - state.sessionStartedAt;
1099
+ if (elapsed < options.sessionCooldown) {
1100
+ return { visible: [], queued: sorted };
1101
+ }
1102
+ }
1103
+ if (options?.respectDoNotDisturb && state.quietMode) {
1104
+ const visible = sorted.filter((feature) => feature.priority === "critical");
1105
+ const queued = sorted.filter((feature) => feature.priority !== "critical");
1106
+ return { visible, queued };
1107
+ }
1108
+ const maxVisible = options?.maxSimultaneousBadges;
1109
+ if (!maxVisible || !Number.isFinite(maxVisible) || maxVisible < 1) {
1110
+ return { visible: sorted, queued: [] };
1111
+ }
1112
+ return {
1113
+ visible: sorted.slice(0, maxVisible),
1114
+ queued: sorted.slice(maxVisible)
1115
+ };
1116
+ }
1117
+
1118
+ // src/analytics.ts
1119
+ var AnalyticsCollector = class {
1120
+ adapter;
1121
+ queue = [];
1122
+ batchSize;
1123
+ flushInterval;
1124
+ sampleRate;
1125
+ enabled;
1126
+ now;
1127
+ random;
1128
+ sessionId;
1129
+ userId;
1130
+ timer = null;
1131
+ flushing = false;
1132
+ constructor(options) {
1133
+ this.adapter = options.adapter;
1134
+ this.batchSize = options.batchSize ?? 20;
1135
+ this.flushInterval = options.flushInterval ?? 1e4;
1136
+ this.sampleRate = options.sampleRate ?? 1;
1137
+ this.enabled = options.enabled ?? true;
1138
+ this.sessionId = options.sessionId;
1139
+ this.userId = options.userId;
1140
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1141
+ this.random = options.random ?? Math.random;
1142
+ this.startTimer();
1143
+ }
1144
+ setEnabled(enabled) {
1145
+ this.enabled = enabled;
1146
+ }
1147
+ setContext(context) {
1148
+ if (context.sessionId !== void 0) this.sessionId = context.sessionId;
1149
+ if (context.userId !== void 0) this.userId = context.userId;
1150
+ }
1151
+ getQueueSize() {
1152
+ return this.queue.length;
1153
+ }
1154
+ track(event) {
1155
+ if (!this.enabled) return;
1156
+ if (this.sampleRate < 1 && this.random() > this.sampleRate) return;
1157
+ const normalized = {
1158
+ ...event,
1159
+ timestamp: event.timestamp ?? this.now().toISOString(),
1160
+ sessionId: event.sessionId ?? this.sessionId,
1161
+ userId: event.userId ?? this.userId
1162
+ };
1163
+ this.queue.push(normalized);
1164
+ if (this.queue.length >= this.batchSize) {
1165
+ void this.flush();
1166
+ }
1167
+ }
1168
+ async flush() {
1169
+ if (this.flushing) return;
1170
+ if (this.queue.length === 0) return;
1171
+ this.flushing = true;
1172
+ const batch = this.queue.splice(0, this.queue.length);
1173
+ try {
1174
+ if (this.adapter.trackBatch) {
1175
+ await this.adapter.trackBatch(batch);
1176
+ } else {
1177
+ for (const event of batch) {
1178
+ await this.adapter.track(event);
1179
+ }
1180
+ }
1181
+ } catch {
1182
+ this.queue = [...batch, ...this.queue];
1183
+ } finally {
1184
+ this.flushing = false;
1185
+ }
1186
+ }
1187
+ async destroy() {
1188
+ if (this.timer) {
1189
+ clearInterval(this.timer);
1190
+ this.timer = null;
1191
+ }
1192
+ await this.flush();
1193
+ }
1194
+ startTimer() {
1195
+ if (this.flushInterval <= 0) return;
1196
+ this.timer = setInterval(() => {
1197
+ void this.flush();
1198
+ }, this.flushInterval);
1199
+ }
1200
+ };
1201
+ var PostHogAdapter = class {
1202
+ constructor(client) {
1203
+ this.client = client;
1204
+ }
1205
+ track(event) {
1206
+ this.client.capture(event.type, {
1207
+ featureId: event.featureId,
1208
+ tourId: event.tourId,
1209
+ variant: event.variant,
1210
+ timestamp: event.timestamp,
1211
+ sessionId: event.sessionId,
1212
+ userId: event.userId,
1213
+ ...event.metadata
1214
+ });
1215
+ }
1216
+ };
1217
+ var AmplitudeAdapter = class {
1218
+ constructor(client) {
1219
+ this.client = client;
1220
+ }
1221
+ track(event) {
1222
+ this.client.track(event.type, {
1223
+ featureId: event.featureId,
1224
+ tourId: event.tourId,
1225
+ variant: event.variant,
1226
+ timestamp: event.timestamp,
1227
+ sessionId: event.sessionId,
1228
+ userId: event.userId,
1229
+ ...event.metadata
1230
+ });
1231
+ }
1232
+ };
1233
+ var MixpanelAdapter = class {
1234
+ constructor(client) {
1235
+ this.client = client;
1236
+ }
1237
+ track(event) {
1238
+ this.client.track(event.type, {
1239
+ featureId: event.featureId,
1240
+ tourId: event.tourId,
1241
+ variant: event.variant,
1242
+ timestamp: event.timestamp,
1243
+ sessionId: event.sessionId,
1244
+ userId: event.userId,
1245
+ ...event.metadata
1246
+ });
1247
+ }
1248
+ };
1249
+ var SegmentAdapter = class {
1250
+ constructor(client) {
1251
+ this.client = client;
1252
+ }
1253
+ track(event) {
1254
+ this.client.track(event.type, {
1255
+ featureId: event.featureId,
1256
+ tourId: event.tourId,
1257
+ variant: event.variant,
1258
+ timestamp: event.timestamp,
1259
+ sessionId: event.sessionId,
1260
+ userId: event.userId,
1261
+ ...event.metadata
1262
+ });
1263
+ }
1264
+ };
1265
+ var CustomAdapter = class {
1266
+ constructor(handler) {
1267
+ this.handler = handler;
1268
+ }
1269
+ track(event) {
1270
+ return this.handler(event);
1271
+ }
1272
+ };
1273
+ function createAdoptionMetrics(events) {
1274
+ const getAdoptionRate = (featureId) => {
1275
+ const seen = events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length;
1276
+ if (seen === 0) return 0;
1277
+ const clicked = events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length;
1278
+ return clicked / seen;
1279
+ };
1280
+ const getTourCompletionRate = (tourId) => {
1281
+ const started = events.filter((event) => event.type === "tour_started" && event.tourId === tourId).length;
1282
+ if (started === 0) return 0;
1283
+ const completed = events.filter((event) => event.type === "tour_completed" && event.tourId === tourId).length;
1284
+ return completed / started;
1285
+ };
1286
+ const getChecklistCompletionRate = (checklistId) => {
1287
+ const taskCompleted = events.filter(
1288
+ (event) => event.type === "checklist_task_completed" && event.metadata?.checklistId === checklistId
1289
+ ).length;
1290
+ if (taskCompleted === 0) return 0;
1291
+ const completed = events.filter(
1292
+ (event) => event.type === "checklist_completed" && event.metadata?.checklistId === checklistId
1293
+ ).length;
1294
+ return completed / taskCompleted;
1295
+ };
1296
+ const getFeatureEngagement = (featureId) => ({
1297
+ seen: events.filter((event) => event.type === "feature_seen" && event.featureId === featureId).length,
1298
+ clicked: events.filter((event) => event.type === "feature_clicked" && event.featureId === featureId).length,
1299
+ dismissed: events.filter((event) => event.type === "feature_dismissed" && event.featureId === featureId).length
1300
+ });
1301
+ const getVariantPerformance = (featureId) => {
1302
+ const byVariant = /* @__PURE__ */ new Map();
1303
+ for (const event of events) {
1304
+ if (event.featureId !== featureId) continue;
1305
+ const variant = event.variant ?? "control";
1306
+ const bucket = byVariant.get(variant) ?? { seen: 0, clicked: 0 };
1307
+ if (event.type === "feature_seen") bucket.seen += 1;
1308
+ if (event.type === "feature_clicked") bucket.clicked += 1;
1309
+ byVariant.set(variant, bucket);
1310
+ }
1311
+ const output = {};
1312
+ for (const [variant, bucket] of byVariant.entries()) {
1313
+ output[variant] = bucket.seen === 0 ? 0 : bucket.clicked / bucket.seen;
1314
+ }
1315
+ return output;
1316
+ };
1317
+ return {
1318
+ getAdoptionRate,
1319
+ getTourCompletionRate,
1320
+ getChecklistCompletionRate,
1321
+ getFeatureEngagement,
1322
+ getVariantPerformance
1323
+ };
1324
+ }
1325
+
1326
+ // src/dependencies.ts
1327
+ function getDirectDependencies(feature) {
1328
+ const dependsOn = feature.dependsOn;
1329
+ if (!dependsOn) return [];
1330
+ const seen = dependsOn.seen ?? [];
1331
+ const clicked = dependsOn.clicked ?? [];
1332
+ const dismissed = dependsOn.dismissed ?? [];
1333
+ const unique = /* @__PURE__ */ new Set();
1334
+ for (const id of [...seen, ...clicked, ...dismissed]) {
1335
+ if (id) unique.add(id);
1336
+ }
1337
+ return Array.from(unique);
1338
+ }
1339
+ function resolveDependencyOrder(manifest) {
1340
+ const ids = new Set(manifest.map((feature) => feature.id));
1341
+ const outgoing = /* @__PURE__ */ new Map();
1342
+ const indegree = /* @__PURE__ */ new Map();
1343
+ for (const feature of manifest) {
1344
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
1345
+ indegree.set(feature.id, 0);
1346
+ }
1347
+ for (const feature of manifest) {
1348
+ for (const dependencyId of getDirectDependencies(feature)) {
1349
+ if (!ids.has(dependencyId)) continue;
1350
+ const edges = outgoing.get(dependencyId);
1351
+ if (!edges || edges.has(feature.id)) continue;
1352
+ edges.add(feature.id);
1353
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
1354
+ }
1355
+ }
1356
+ const queue = [];
1357
+ for (const feature of manifest) {
1358
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
1359
+ }
1360
+ const ordered = [];
1361
+ while (queue.length > 0) {
1362
+ const id = queue.shift();
1363
+ if (!id) continue;
1364
+ ordered.push(id);
1365
+ const edges = outgoing.get(id);
1366
+ if (!edges) continue;
1367
+ for (const nextId of edges) {
1368
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
1369
+ indegree.set(nextId, nextDegree);
1370
+ if (nextDegree === 0) queue.push(nextId);
1371
+ }
1372
+ }
1373
+ if (ordered.length < manifest.length) {
1374
+ const included = new Set(ordered);
1375
+ for (const feature of manifest) {
1376
+ if (included.has(feature.id)) continue;
1377
+ ordered.push(feature.id);
1378
+ }
1379
+ }
1380
+ return ordered;
1381
+ }
1382
+ function hasDependencyCycle(manifest) {
1383
+ const ids = new Set(manifest.map((feature) => feature.id));
1384
+ const outgoing = /* @__PURE__ */ new Map();
1385
+ const indegree = /* @__PURE__ */ new Map();
1386
+ for (const feature of manifest) {
1387
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
1388
+ indegree.set(feature.id, 0);
1389
+ }
1390
+ for (const feature of manifest) {
1391
+ for (const dependencyId of getDirectDependencies(feature)) {
1392
+ if (!ids.has(dependencyId)) continue;
1393
+ const edges = outgoing.get(dependencyId);
1394
+ if (!edges || edges.has(feature.id)) continue;
1395
+ edges.add(feature.id);
1396
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
1397
+ }
1398
+ }
1399
+ const queue = [];
1400
+ for (const feature of manifest) {
1401
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
1402
+ }
1403
+ let visited = 0;
1404
+ while (queue.length > 0) {
1405
+ const id = queue.shift();
1406
+ if (!id) continue;
1407
+ visited += 1;
1408
+ const edges = outgoing.get(id);
1409
+ if (!edges) continue;
1410
+ for (const nextId of edges) {
1411
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
1412
+ indegree.set(nextId, nextDegree);
1413
+ if (nextDegree === 0) queue.push(nextId);
1414
+ }
1415
+ }
1416
+ return visited !== manifest.length;
1417
+ }
1418
+ function sortFeaturesByDependencies(features) {
1419
+ if (features.length <= 1) return [...features];
1420
+ const order = resolveDependencyOrder(features);
1421
+ const rank = new Map(order.map((id, index) => [id, index]));
1422
+ return [...features].sort((a, b) => {
1423
+ const ra = rank.get(a.id);
1424
+ const rb = rank.get(b.id);
1425
+ if (ra === void 0 || rb === void 0) return 0;
1426
+ return ra - rb;
1427
+ });
1428
+ }
1429
+
1430
+ // src/variants.ts
1431
+ var VARIANT_META_KEY = "featuredropVariant";
1432
+ var VARIANT_KEY_STORAGE = "featuredrop:variant-key";
1433
+ function readStorageValue(key) {
1434
+ const storage = globalThis.localStorage;
1435
+ if (!storage || typeof storage.getItem !== "function") return null;
1436
+ try {
1437
+ return storage.getItem(key);
1438
+ } catch {
1439
+ return null;
1440
+ }
1441
+ }
1442
+ function writeStorageValue(key, value) {
1443
+ const storage = globalThis.localStorage;
1444
+ if (!storage || typeof storage.setItem !== "function") return;
1445
+ try {
1446
+ storage.setItem(key, value);
1447
+ } catch {
1448
+ }
1449
+ }
1450
+ function hashToPercent(value) {
1451
+ let hash = 2166136261;
1452
+ for (let i = 0; i < value.length; i += 1) {
1453
+ hash ^= value.charCodeAt(i);
1454
+ hash = Math.imul(hash, 16777619);
1455
+ }
1456
+ return (hash >>> 0) % 100;
1457
+ }
1458
+ function normalizeSplit(count, split) {
1459
+ if (!split || split.length !== count) {
1460
+ return Array.from({ length: count }, () => 100 / count);
1461
+ }
1462
+ const cleaned = split.map((value) => Number.isFinite(value) && value > 0 ? value : 0);
1463
+ const total = cleaned.reduce((sum, value) => sum + value, 0);
1464
+ if (total <= 0) {
1465
+ return Array.from({ length: count }, () => 100 / count);
1466
+ }
1467
+ return cleaned.map((value) => value / total * 100);
1468
+ }
1469
+ function pickVariantName(feature, variantKey) {
1470
+ const variants = feature.variants;
1471
+ if (!variants) return null;
1472
+ const names = Object.keys(variants);
1473
+ if (names.length === 0) return null;
1474
+ if (names.length === 1) return names[0];
1475
+ const split = normalizeSplit(names.length, feature.variantSplit);
1476
+ const bucket = hashToPercent(`${feature.id}:${variantKey}`);
1477
+ let cumulative = 0;
1478
+ for (let i = 0; i < names.length; i += 1) {
1479
+ cumulative += split[i];
1480
+ if (bucket < cumulative) return names[i];
1481
+ }
1482
+ return names[names.length - 1];
1483
+ }
1484
+ function getFeatureVariantName(feature) {
1485
+ const raw = feature.meta?.[VARIANT_META_KEY];
1486
+ return typeof raw === "string" ? raw : void 0;
1487
+ }
1488
+ function applyFeatureVariant(feature, variantKey) {
1489
+ const variantName = pickVariantName(feature, variantKey);
1490
+ if (!variantName) return feature;
1491
+ const variant = feature.variants?.[variantName];
1492
+ if (!variant) return feature;
1493
+ return {
1494
+ ...feature,
1495
+ label: variant.label ?? feature.label,
1496
+ description: variant.description ?? feature.description,
1497
+ image: variant.image ?? feature.image,
1498
+ cta: variant.cta ?? feature.cta,
1499
+ meta: {
1500
+ ...feature.meta ?? {},
1501
+ ...variant.meta ?? {},
1502
+ [VARIANT_META_KEY]: variantName
1503
+ }
1504
+ };
1505
+ }
1506
+ function applyFeatureVariants(manifest, variantKey) {
1507
+ return manifest.map((feature) => applyFeatureVariant(feature, variantKey));
1508
+ }
1509
+ function createRandomKey() {
1510
+ return Math.random().toString(36).slice(2, 12);
1511
+ }
1512
+ function getOrCreateVariantKey(explicitKey) {
1513
+ if (explicitKey) return explicitKey;
1514
+ const existing = readStorageValue(VARIANT_KEY_STORAGE);
1515
+ if (existing) return existing;
1516
+ const next = createRandomKey();
1517
+ writeStorageValue(VARIANT_KEY_STORAGE, next);
1518
+ return next;
1519
+ }
1520
+
1521
+ // src/cli-utils.ts
1522
+ function computeManifestStats(entries) {
1523
+ const byType = {};
1524
+ const byCategory = {};
1525
+ for (const entry of entries) {
1526
+ const type = entry.type ?? "feature";
1527
+ byType[type] = (byType[type] ?? 0) + 1;
1528
+ if (entry.category) {
1529
+ byCategory[entry.category] = (byCategory[entry.category] ?? 0) + 1;
1530
+ }
1531
+ }
1532
+ const sortedByDate = [...entries].sort(
1533
+ (a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()
1534
+ );
1535
+ return {
1536
+ total: entries.length,
1537
+ byType,
1538
+ byCategory,
1539
+ newestRelease: sortedByDate[0]?.releasedAt ?? null,
1540
+ oldestRelease: sortedByDate[sortedByDate.length - 1]?.releasedAt ?? null
1541
+ };
1542
+ }
1543
+ function generateMarkdownChangelog(entries) {
1544
+ const sorted = [...entries].sort(
1545
+ (a, b) => new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime()
1546
+ );
1547
+ const sections = sorted.map((entry) => {
1548
+ const lines = [
1549
+ `## ${entry.label}`,
1550
+ "",
1551
+ `- **ID**: \`${entry.id}\``,
1552
+ `- **Released**: ${entry.releasedAt}`
1553
+ ];
1554
+ if (entry.type) lines.push(`- **Type**: ${entry.type}`);
1555
+ if (entry.category) lines.push(`- **Category**: ${entry.category}`);
1556
+ if (entry.showNewUntil) lines.push(`- **Show new until**: ${entry.showNewUntil}`);
1557
+ if (entry.cta) lines.push(`- **CTA**: [${entry.cta.label}](${entry.cta.url})`);
1558
+ if (entry.description) {
1559
+ lines.push("", entry.description.trim());
1560
+ }
1561
+ return lines.join("\n");
1562
+ });
1563
+ return `# Generated Changelog
1564
+
1565
+ ${sections.join("\n\n---\n\n")}
1566
+ `;
1567
+ }
1568
+ function isIsoWithTimezone(value) {
1569
+ if (!value.includes("T")) return false;
1570
+ if (!(value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value))) return false;
1571
+ return Number.isFinite(new Date(value).getTime());
1572
+ }
1573
+ function runDoctor(entries, now = /* @__PURE__ */ new Date()) {
1574
+ const checks = [];
1575
+ const warnings = [];
1576
+ const errors = [];
1577
+ checks.push(`Manifest entries loaded: ${entries.length}`);
1578
+ const ids = /* @__PURE__ */ new Set();
1579
+ let duplicateCount = 0;
1580
+ for (const entry of entries) {
1581
+ if (ids.has(entry.id)) duplicateCount += 1;
1582
+ ids.add(entry.id);
1583
+ }
1584
+ if (duplicateCount > 0) {
1585
+ errors.push(`${duplicateCount} duplicate feature id(s) found`);
1586
+ } else {
1587
+ checks.push("No duplicate IDs");
1588
+ }
1589
+ let invalidDateCount = 0;
1590
+ let reversedDateCount = 0;
1591
+ let expiredCount = 0;
1592
+ let scheduledCount = 0;
1593
+ let missingDescriptionCount = 0;
1594
+ for (const entry of entries) {
1595
+ if (!entry.description?.trim()) missingDescriptionCount += 1;
1596
+ if (!isIsoWithTimezone(entry.releasedAt) || !isIsoWithTimezone(entry.showNewUntil)) {
1597
+ invalidDateCount += 1;
1598
+ continue;
1599
+ }
1600
+ const released = new Date(entry.releasedAt).getTime();
1601
+ const showUntil = new Date(entry.showNewUntil).getTime();
1602
+ if (showUntil <= released) reversedDateCount += 1;
1603
+ if (showUntil < now.getTime()) expiredCount += 1;
1604
+ if (entry.publishAt) {
1605
+ const publishMs = new Date(entry.publishAt).getTime();
1606
+ if (Number.isFinite(publishMs) && publishMs > now.getTime()) scheduledCount += 1;
1607
+ }
1608
+ }
1609
+ if (invalidDateCount > 0) {
1610
+ errors.push(`${invalidDateCount} entries have invalid ISO 8601 dates with timezone`);
1611
+ } else {
1612
+ checks.push("All dates are valid ISO 8601 with timezone");
1613
+ }
1614
+ if (reversedDateCount > 0) {
1615
+ errors.push(`${reversedDateCount} entries have showNewUntil before/at releasedAt`);
1616
+ }
1617
+ if (expiredCount > 0) warnings.push(`${expiredCount} entries have showNewUntil in the past`);
1618
+ if (scheduledCount > 0) warnings.push(`${scheduledCount} entries have publishAt in the future`);
1619
+ if (missingDescriptionCount > 0) {
1620
+ errors.push(`${missingDescriptionCount} entries have no description`);
1621
+ } else {
1622
+ checks.push("All entries have descriptions");
1623
+ }
1624
+ if (hasDependencyCycle(entries)) {
1625
+ errors.push("Circular dependsOn relationship detected");
1626
+ } else {
1627
+ checks.push("No circular dependencies in dependsOn chains");
1628
+ }
1629
+ return { checks, warnings, errors };
1630
+ }
1631
+ var featureEntryJsonSchema = {
1632
+ type: "object",
1633
+ required: ["id", "label", "releasedAt", "showNewUntil"],
1634
+ properties: {
1635
+ id: { type: "string" },
1636
+ label: { type: "string" },
1637
+ description: { type: "string" },
1638
+ releasedAt: { type: "string", format: "date-time" },
1639
+ showNewUntil: { type: "string", format: "date-time" },
1640
+ type: { enum: ["feature", "improvement", "fix", "breaking"] },
1641
+ priority: { enum: ["critical", "normal", "low"] }
1642
+ }
1643
+ };
1644
+ var featureManifestJsonSchema = {
1645
+ type: "array",
1646
+ items: featureEntryJsonSchema
1647
+ };
1648
+ function isRecord(value) {
1649
+ return !!value && typeof value === "object" && !Array.isArray(value);
1650
+ }
1651
+ function isValidDate(value) {
1652
+ return Number.isFinite(new Date(value).getTime());
1653
+ }
1654
+ var nonEmptyString = zod.z.string().trim().min(1, "must be a non-empty string");
1655
+ var isoDateString = nonEmptyString.refine(isValidDate, {
1656
+ message: "must be a valid date",
1657
+ params: { featuredropCode: "invalid_date" }
1658
+ });
1659
+ var dependsOnSchema = zod.z.object({
1660
+ seen: zod.z.array(zod.z.string()).optional(),
1661
+ clicked: zod.z.array(zod.z.string()).optional(),
1662
+ dismissed: zod.z.array(zod.z.string()).optional()
1663
+ }).optional();
1664
+ var featureEntrySchema = zod.z.object({
1665
+ id: nonEmptyString,
1666
+ label: nonEmptyString,
1667
+ releasedAt: isoDateString,
1668
+ showNewUntil: isoDateString,
1669
+ description: zod.z.string().optional(),
1670
+ type: zod.z.enum(["feature", "improvement", "fix", "breaking"]).optional(),
1671
+ priority: zod.z.enum(["critical", "normal", "low"]).optional(),
1672
+ dependsOn: dependsOnSchema
1673
+ }).passthrough();
1674
+ var featureManifestSchema = zod.z.array(featureEntrySchema);
1675
+ function toIssuePath(path) {
1676
+ if (path.length === 0) return "$";
1677
+ let output = "";
1678
+ for (const part of path) {
1679
+ if (typeof part === "number") output += `[${part}]`;
1680
+ else output += output ? `.${part}` : part;
1681
+ }
1682
+ return output;
1683
+ }
1684
+ function mapZodIssue(issue) {
1685
+ const codeParam = issue.params?.featuredropCode;
1686
+ if (codeParam === "invalid_date") {
1687
+ return {
1688
+ path: toIssuePath(issue.path),
1689
+ message: issue.message,
1690
+ code: "invalid_date"
1691
+ };
1692
+ }
1693
+ if (issue.code === "invalid_type") {
1694
+ return {
1695
+ path: toIssuePath(issue.path),
1696
+ message: issue.message,
1697
+ code: issue.received === "undefined" ? "missing_required" : "invalid_type"
1698
+ };
1699
+ }
1700
+ return {
1701
+ path: toIssuePath(issue.path),
1702
+ message: issue.message,
1703
+ code: "invalid_value"
1704
+ };
1705
+ }
1706
+ function validateFeatureEntry(raw, index) {
1707
+ if (!isRecord(raw)) {
1708
+ return {
1709
+ issues: [
1710
+ {
1711
+ path: `[${index}]`,
1712
+ message: "Feature entry must be an object",
1713
+ code: "invalid_type"
1714
+ }
1715
+ ]
1716
+ };
1717
+ }
1718
+ const parsed = featureEntrySchema.safeParse(raw);
1719
+ if (!parsed.success) {
1720
+ return {
1721
+ issues: parsed.error.issues.map((issue) => ({
1722
+ ...mapZodIssue(issue),
1723
+ path: `[${index}]${issue.path.length > 0 ? `.${toIssuePath(issue.path)}` : ""}`
1724
+ }))
1725
+ };
1726
+ }
1727
+ return {
1728
+ issues: [],
1729
+ entry: parsed.data
1730
+ };
1731
+ }
1732
+ function validateManifest(data) {
1733
+ const errors = [];
1734
+ if (!Array.isArray(data)) {
1735
+ return {
1736
+ valid: false,
1737
+ errors: [
1738
+ {
1739
+ path: "$",
1740
+ message: "Manifest must be an array",
1741
+ code: "invalid_type"
1742
+ }
1743
+ ]
1744
+ };
1745
+ }
1746
+ const entries = [];
1747
+ const seenIds = /* @__PURE__ */ new Set();
1748
+ data.forEach((item, index) => {
1749
+ const result = validateFeatureEntry(item, index);
1750
+ errors.push(...result.issues);
1751
+ if (!result.entry) return;
1752
+ if (seenIds.has(result.entry.id)) {
1753
+ errors.push({
1754
+ path: `[${index}].id`,
1755
+ message: `Duplicate feature id "${result.entry.id}"`,
1756
+ code: "duplicate_id"
1757
+ });
1758
+ return;
1759
+ }
1760
+ seenIds.add(result.entry.id);
1761
+ entries.push(result.entry);
1762
+ });
1763
+ if (entries.length > 0 && hasDependencyCycle(entries)) {
1764
+ errors.push({
1765
+ path: "$",
1766
+ message: "Circular dependsOn relationship detected",
1767
+ code: "circular_dependency"
1768
+ });
1769
+ }
1770
+ for (let index = 0; index < entries.length; index++) {
1771
+ const entry = entries[index];
1772
+ if (new Date(entry.showNewUntil).getTime() <= new Date(entry.releasedAt).getTime()) {
1773
+ errors.push({
1774
+ path: `[${index}].showNewUntil`,
1775
+ message: "showNewUntil must be after releasedAt",
1776
+ code: "invalid_value"
1777
+ });
1778
+ }
1779
+ }
1780
+ return {
1781
+ valid: errors.length === 0,
1782
+ errors
1783
+ };
1784
+ }
46
1785
 
47
1786
  // src/adapters/local-storage.ts
48
1787
  var DISMISSED_SUFFIX = ":dismissed";
@@ -120,14 +1859,1035 @@ var MemoryAdapter = class {
120
1859
  }
121
1860
  };
122
1861
 
1862
+ // src/adapters/remote.ts
1863
+ function assertFetch() {
1864
+ if (typeof fetch === "undefined") {
1865
+ throw new Error("RemoteAdapter requires global fetch (Node 18+ or polyfill)");
1866
+ }
1867
+ return fetch;
1868
+ }
1869
+ var RemoteAdapter = class {
1870
+ baseUrl;
1871
+ headers;
1872
+ fetchInterval;
1873
+ userId;
1874
+ dismissedIds = /* @__PURE__ */ new Set();
1875
+ watermark = null;
1876
+ lastManifest = null;
1877
+ lastFetchTs = 0;
1878
+ constructor(options) {
1879
+ this.baseUrl = options.url.replace(/\/$/, "");
1880
+ this.headers = options.headers ?? {};
1881
+ this.fetchInterval = options.fetchInterval ?? 5 * 60 * 1e3;
1882
+ this.userId = options.userId;
1883
+ }
1884
+ /** Fetch manifest with stale-while-revalidate */
1885
+ async fetchManifest(force = false) {
1886
+ const now = Date.now();
1887
+ if (!force && this.lastManifest && now - this.lastFetchTs < this.fetchInterval) {
1888
+ return this.lastManifest;
1889
+ }
1890
+ const fetchImpl = assertFetch();
1891
+ const res = await fetchImpl(this.baseUrl, {
1892
+ method: "GET",
1893
+ headers: this.headers
1894
+ });
1895
+ if (!res.ok) throw new Error(`RemoteAdapter manifest fetch failed: ${res.status}`);
1896
+ const json = await res.json();
1897
+ this.lastManifest = json;
1898
+ this.lastFetchTs = now;
1899
+ return json;
1900
+ }
1901
+ /** Fetch state (watermark + dismissed IDs) */
1902
+ async syncState() {
1903
+ const fetchImpl = assertFetch();
1904
+ const url = this.userId ? `${this.baseUrl}/state?userId=${encodeURIComponent(this.userId)}` : `${this.baseUrl}/state`;
1905
+ const res = await fetchImpl(url, {
1906
+ method: "GET",
1907
+ headers: this.headers
1908
+ });
1909
+ if (!res.ok) return;
1910
+ const json = await res.json();
1911
+ if (json.watermark !== void 0) this.watermark = json.watermark;
1912
+ if (Array.isArray(json.dismissedIds)) this.dismissedIds = new Set(json.dismissedIds);
1913
+ }
1914
+ getWatermark() {
1915
+ return this.watermark;
1916
+ }
1917
+ getDismissedIds() {
1918
+ return this.dismissedIds;
1919
+ }
1920
+ dismiss(id) {
1921
+ this.dismissedIds.add(id);
1922
+ this.flushDismiss(id).catch(() => {
1923
+ });
1924
+ }
1925
+ async dismissAll(now) {
1926
+ this.watermark = now.toISOString();
1927
+ this.dismissedIds.clear();
1928
+ await this.flushDismissAll(now).catch(() => {
1929
+ });
1930
+ }
1931
+ async flushDismiss(id) {
1932
+ const fetchImpl = assertFetch();
1933
+ await fetchImpl(`${this.baseUrl}/dismiss`, {
1934
+ method: "POST",
1935
+ headers: { "Content-Type": "application/json", ...this.headers },
1936
+ body: JSON.stringify({ featureId: id })
1937
+ });
1938
+ }
1939
+ async flushDismissAll(now) {
1940
+ const fetchImpl = assertFetch();
1941
+ await fetchImpl(`${this.baseUrl}/dismiss-all`, {
1942
+ method: "POST",
1943
+ headers: { "Content-Type": "application/json", ...this.headers },
1944
+ body: JSON.stringify({ watermark: now.toISOString() })
1945
+ });
1946
+ }
1947
+ };
1948
+
1949
+ // src/adapters/postgres.ts
1950
+ function normalizeDismissedIds(row) {
1951
+ if (!row) return [];
1952
+ const ids = row.dismissed_ids ?? row.dismissedIds;
1953
+ if (!Array.isArray(ids)) return [];
1954
+ return ids.filter((id) => typeof id === "string");
1955
+ }
1956
+ function normalizeWatermark(row) {
1957
+ if (!row) return null;
1958
+ return row.watermark ?? null;
1959
+ }
1960
+ function normalizeLastSeen(row) {
1961
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
1962
+ return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
1963
+ }
1964
+ var PostgresAdapter = class {
1965
+ userId;
1966
+ query;
1967
+ tableName;
1968
+ autoMigrate;
1969
+ watermark = null;
1970
+ dismissedIds = /* @__PURE__ */ new Set();
1971
+ initialized = false;
1972
+ constructor(options) {
1973
+ if (!options.userId) {
1974
+ throw new Error("PostgresAdapter: userId is required");
1975
+ }
1976
+ this.userId = options.userId;
1977
+ this.query = options.query;
1978
+ this.tableName = options.tableName ?? "featuredrop_state";
1979
+ this.autoMigrate = options.autoMigrate ?? true;
1980
+ }
1981
+ getWatermark() {
1982
+ return this.watermark;
1983
+ }
1984
+ getDismissedIds() {
1985
+ return this.dismissedIds;
1986
+ }
1987
+ dismiss(id) {
1988
+ this.dismissedIds.add(id);
1989
+ void this.dismissBatch([id]);
1990
+ }
1991
+ async dismissAll(now) {
1992
+ this.watermark = now.toISOString();
1993
+ this.dismissedIds.clear();
1994
+ await this.ensureReady();
1995
+ await this.query(
1996
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
1997
+ VALUES ($1, $2, '{}', NOW(), NOW(), NOW())
1998
+ ON CONFLICT (user_id)
1999
+ DO UPDATE SET watermark = EXCLUDED.watermark, dismissed_ids = '{}', last_seen = NOW(), updated_at = NOW()`,
2000
+ [this.userId, this.watermark]
2001
+ );
2002
+ }
2003
+ async sync() {
2004
+ await this.ensureReady();
2005
+ const result = await this.query(
2006
+ `SELECT watermark, dismissed_ids, last_seen
2007
+ FROM ${this.tableName}
2008
+ WHERE user_id = $1`,
2009
+ [this.userId]
2010
+ );
2011
+ const row = result.rows[0];
2012
+ this.watermark = normalizeWatermark(row);
2013
+ this.dismissedIds = new Set(normalizeDismissedIds(row));
2014
+ }
2015
+ async dismissBatch(ids) {
2016
+ if (ids.length === 0) return;
2017
+ await this.ensureReady();
2018
+ const uniqueIds = Array.from(new Set(ids));
2019
+ await this.query(
2020
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
2021
+ VALUES ($1, NULL, $2::text[], NOW(), NOW(), NOW())
2022
+ ON CONFLICT (user_id)
2023
+ DO UPDATE SET
2024
+ dismissed_ids = (
2025
+ SELECT ARRAY(
2026
+ SELECT DISTINCT x FROM UNNEST(
2027
+ COALESCE(${this.tableName}.dismissed_ids, '{}') || EXCLUDED.dismissed_ids
2028
+ ) AS x
2029
+ )
2030
+ ),
2031
+ last_seen = NOW(),
2032
+ updated_at = NOW()`,
2033
+ [this.userId, uniqueIds]
2034
+ );
2035
+ }
2036
+ async resetUser(userId) {
2037
+ await this.ensureReady();
2038
+ await this.query(
2039
+ `DELETE FROM ${this.tableName} WHERE user_id = $1`,
2040
+ [userId]
2041
+ );
2042
+ if (userId === this.userId) {
2043
+ this.watermark = null;
2044
+ this.dismissedIds.clear();
2045
+ }
2046
+ }
2047
+ async getBulkState(userIds) {
2048
+ await this.ensureReady();
2049
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
2050
+ const result = await this.query(
2051
+ `SELECT user_id, watermark, dismissed_ids, last_seen
2052
+ FROM ${this.tableName}
2053
+ WHERE user_id = ANY($1::text[])`,
2054
+ [userIds]
2055
+ );
2056
+ const out = /* @__PURE__ */ new Map();
2057
+ for (const row of result.rows) {
2058
+ out.set(row.user_id, {
2059
+ watermark: normalizeWatermark(row),
2060
+ dismissedIds: normalizeDismissedIds(row),
2061
+ lastSeen: normalizeLastSeen(row),
2062
+ deviceCount: 1
2063
+ });
2064
+ }
2065
+ return out;
2066
+ }
2067
+ async isHealthy() {
2068
+ try {
2069
+ await this.query("SELECT 1");
2070
+ return true;
2071
+ } catch {
2072
+ return false;
2073
+ }
2074
+ }
2075
+ async destroy() {
2076
+ }
2077
+ async ensureReady() {
2078
+ if (this.initialized) return;
2079
+ if (this.autoMigrate) {
2080
+ await this.query(
2081
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (
2082
+ user_id TEXT PRIMARY KEY,
2083
+ watermark TIMESTAMPTZ,
2084
+ dismissed_ids TEXT[] DEFAULT '{}',
2085
+ last_seen TIMESTAMPTZ DEFAULT NOW(),
2086
+ created_at TIMESTAMPTZ DEFAULT NOW(),
2087
+ updated_at TIMESTAMPTZ DEFAULT NOW()
2088
+ )`
2089
+ );
2090
+ await this.query(
2091
+ `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
2092
+ );
2093
+ }
2094
+ this.initialized = true;
2095
+ }
2096
+ };
2097
+
2098
+ // src/adapters/redis.ts
2099
+ var RedisAdapter = class {
2100
+ userId;
2101
+ client;
2102
+ keyPrefix;
2103
+ watermark = null;
2104
+ dismissedIds = /* @__PURE__ */ new Set();
2105
+ constructor(options) {
2106
+ if (!options.userId) {
2107
+ throw new Error("RedisAdapter: userId is required");
2108
+ }
2109
+ this.userId = options.userId;
2110
+ this.client = options.client;
2111
+ this.keyPrefix = options.keyPrefix ?? "fd:";
2112
+ }
2113
+ getWatermark() {
2114
+ return this.watermark;
2115
+ }
2116
+ getDismissedIds() {
2117
+ return this.dismissedIds;
2118
+ }
2119
+ dismiss(id) {
2120
+ this.dismissedIds.add(id);
2121
+ void this.client.sadd(this.dismissedKey(this.userId), id);
2122
+ void this.client.set(this.lastSeenKey(this.userId), (/* @__PURE__ */ new Date()).toISOString());
2123
+ }
2124
+ async dismissAll(now) {
2125
+ this.watermark = now.toISOString();
2126
+ this.dismissedIds.clear();
2127
+ await this.client.multi().set(this.watermarkKey(this.userId), this.watermark).del(this.dismissedKey(this.userId)).set(this.lastSeenKey(this.userId), now.toISOString()).exec();
2128
+ }
2129
+ async sync() {
2130
+ const [watermark, dismissedIds] = await Promise.all([
2131
+ this.client.get(this.watermarkKey(this.userId)),
2132
+ this.client.smembers(this.dismissedKey(this.userId))
2133
+ ]);
2134
+ this.watermark = watermark;
2135
+ this.dismissedIds = new Set(dismissedIds);
2136
+ }
2137
+ async dismissBatch(ids) {
2138
+ const uniqueIds = Array.from(new Set(ids));
2139
+ if (uniqueIds.length === 0) return;
2140
+ this.dismissedIds = /* @__PURE__ */ new Set([...this.dismissedIds, ...uniqueIds]);
2141
+ await this.client.sadd(this.dismissedKey(this.userId), ...uniqueIds);
2142
+ await this.client.set(this.lastSeenKey(this.userId), (/* @__PURE__ */ new Date()).toISOString());
2143
+ }
2144
+ async resetUser(userId) {
2145
+ await this.client.multi().del(this.watermarkKey(userId)).del(this.dismissedKey(userId)).del(this.lastSeenKey(userId)).exec();
2146
+ if (userId === this.userId) {
2147
+ this.watermark = null;
2148
+ this.dismissedIds.clear();
2149
+ }
2150
+ }
2151
+ async getBulkState(userIds) {
2152
+ const map = /* @__PURE__ */ new Map();
2153
+ await Promise.all(
2154
+ userIds.map(async (userId) => {
2155
+ const [watermark, dismissedIds, lastSeen] = await Promise.all([
2156
+ this.client.get(this.watermarkKey(userId)),
2157
+ this.client.smembers(this.dismissedKey(userId)),
2158
+ this.client.get(this.lastSeenKey(userId))
2159
+ ]);
2160
+ map.set(userId, {
2161
+ watermark,
2162
+ dismissedIds,
2163
+ lastSeen: lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2164
+ deviceCount: 1
2165
+ });
2166
+ })
2167
+ );
2168
+ return map;
2169
+ }
2170
+ async isHealthy() {
2171
+ try {
2172
+ const response = await this.client.ping();
2173
+ return response.toUpperCase() === "PONG";
2174
+ } catch {
2175
+ return false;
2176
+ }
2177
+ }
2178
+ async destroy() {
2179
+ if (this.client.quit) {
2180
+ await this.client.quit();
2181
+ return;
2182
+ }
2183
+ this.client.disconnect?.();
2184
+ }
2185
+ watermarkKey(userId) {
2186
+ return `${this.keyPrefix}${userId}:watermark`;
2187
+ }
2188
+ dismissedKey(userId) {
2189
+ return `${this.keyPrefix}${userId}:dismissed`;
2190
+ }
2191
+ lastSeenKey(userId) {
2192
+ return `${this.keyPrefix}${userId}:last_seen`;
2193
+ }
2194
+ };
2195
+
2196
+ // src/adapters/hybrid.ts
2197
+ var HybridAdapter = class {
2198
+ userId;
2199
+ local;
2200
+ remote;
2201
+ syncBeforeWrite;
2202
+ constructor(options) {
2203
+ this.local = options.local;
2204
+ this.remote = options.remote;
2205
+ this.userId = options.remote.userId;
2206
+ this.syncBeforeWrite = options.syncBeforeWrite ?? false;
2207
+ }
2208
+ getWatermark() {
2209
+ return this.local.getWatermark() ?? this.remote.getWatermark();
2210
+ }
2211
+ getDismissedIds() {
2212
+ const merged = /* @__PURE__ */ new Set();
2213
+ for (const id of this.local.getDismissedIds()) merged.add(id);
2214
+ for (const id of this.remote.getDismissedIds()) merged.add(id);
2215
+ return merged;
2216
+ }
2217
+ dismiss(id) {
2218
+ this.local.dismiss(id);
2219
+ this.remote.dismiss(id);
2220
+ }
2221
+ async dismissAll(now) {
2222
+ await Promise.all([
2223
+ this.local.dismissAll(now),
2224
+ this.remote.dismissAll(now)
2225
+ ]);
2226
+ }
2227
+ async sync() {
2228
+ await this.remote.sync();
2229
+ }
2230
+ async dismissBatch(ids) {
2231
+ if (this.syncBeforeWrite) {
2232
+ await this.remote.sync();
2233
+ }
2234
+ for (const id of ids) {
2235
+ this.local.dismiss(id);
2236
+ }
2237
+ await this.remote.dismissBatch(ids);
2238
+ }
2239
+ async resetUser(userId) {
2240
+ await this.remote.resetUser(userId);
2241
+ if (userId === this.userId) {
2242
+ await this.local.dismissAll(/* @__PURE__ */ new Date(0));
2243
+ }
2244
+ }
2245
+ async getBulkState(userIds) {
2246
+ return this.remote.getBulkState(userIds);
2247
+ }
2248
+ async isHealthy() {
2249
+ return this.remote.isHealthy();
2250
+ }
2251
+ async destroy() {
2252
+ await this.remote.destroy();
2253
+ }
2254
+ };
2255
+
2256
+ // src/adapters/mysql.ts
2257
+ function parseDismissedIds(value) {
2258
+ if (Array.isArray(value)) {
2259
+ return value.filter((item) => typeof item === "string");
2260
+ }
2261
+ if (typeof value === "string" && value.trim()) {
2262
+ try {
2263
+ const parsed = JSON.parse(value);
2264
+ if (Array.isArray(parsed)) {
2265
+ return parsed.filter((item) => typeof item === "string");
2266
+ }
2267
+ } catch {
2268
+ return [];
2269
+ }
2270
+ }
2271
+ return [];
2272
+ }
2273
+ function normalizeDismissedIds2(row) {
2274
+ if (!row) return [];
2275
+ return parseDismissedIds(row.dismissed_ids ?? row.dismissedIds);
2276
+ }
2277
+ function normalizeWatermark2(row) {
2278
+ if (!row) return null;
2279
+ return row.watermark ?? null;
2280
+ }
2281
+ function normalizeLastSeen2(row) {
2282
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
2283
+ return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
2284
+ }
2285
+ var MySQLAdapter = class {
2286
+ userId;
2287
+ query;
2288
+ tableName;
2289
+ autoMigrate;
2290
+ watermark = null;
2291
+ dismissedIds = /* @__PURE__ */ new Set();
2292
+ initialized = false;
2293
+ constructor(options) {
2294
+ if (!options.userId) {
2295
+ throw new Error("MySQLAdapter: userId is required");
2296
+ }
2297
+ this.userId = options.userId;
2298
+ this.query = options.query;
2299
+ this.tableName = options.tableName ?? "featuredrop_state";
2300
+ this.autoMigrate = options.autoMigrate ?? true;
2301
+ }
2302
+ getWatermark() {
2303
+ return this.watermark;
2304
+ }
2305
+ getDismissedIds() {
2306
+ return this.dismissedIds;
2307
+ }
2308
+ dismiss(id) {
2309
+ this.dismissedIds.add(id);
2310
+ void this.dismissBatch([id]);
2311
+ }
2312
+ async dismissAll(now) {
2313
+ this.watermark = now.toISOString();
2314
+ this.dismissedIds.clear();
2315
+ await this.ensureReady();
2316
+ await this.query(
2317
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
2318
+ VALUES (?, ?, ?, NOW(3), NOW(3), NOW(3))
2319
+ ON DUPLICATE KEY UPDATE watermark = VALUES(watermark), dismissed_ids = VALUES(dismissed_ids), last_seen = NOW(3), updated_at = NOW(3)`,
2320
+ [this.userId, this.watermark, JSON.stringify([])]
2321
+ );
2322
+ }
2323
+ async sync() {
2324
+ await this.ensureReady();
2325
+ const result = await this.query(
2326
+ `SELECT watermark, dismissed_ids, last_seen FROM ${this.tableName} WHERE user_id = ? LIMIT 1`,
2327
+ [this.userId]
2328
+ );
2329
+ const row = result.rows[0];
2330
+ this.watermark = normalizeWatermark2(row);
2331
+ this.dismissedIds = new Set(normalizeDismissedIds2(row));
2332
+ }
2333
+ async dismissBatch(ids) {
2334
+ const uniqueIds = Array.from(new Set(ids));
2335
+ if (uniqueIds.length === 0) return;
2336
+ await this.ensureReady();
2337
+ const merged = /* @__PURE__ */ new Set([
2338
+ ...Array.from(this.dismissedIds),
2339
+ ...uniqueIds
2340
+ ]);
2341
+ const mergedArray = Array.from(merged);
2342
+ this.dismissedIds = merged;
2343
+ await this.query(
2344
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
2345
+ VALUES (?, ?, ?, NOW(3), NOW(3), NOW(3))
2346
+ ON DUPLICATE KEY UPDATE dismissed_ids = VALUES(dismissed_ids), last_seen = NOW(3), updated_at = NOW(3)`,
2347
+ [this.userId, this.watermark, JSON.stringify(mergedArray)]
2348
+ );
2349
+ }
2350
+ async resetUser(userId) {
2351
+ await this.ensureReady();
2352
+ await this.query(`DELETE FROM ${this.tableName} WHERE user_id = ?`, [userId]);
2353
+ if (userId === this.userId) {
2354
+ this.watermark = null;
2355
+ this.dismissedIds.clear();
2356
+ }
2357
+ }
2358
+ async getBulkState(userIds) {
2359
+ await this.ensureReady();
2360
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
2361
+ const placeholders = userIds.map(() => "?").join(", ");
2362
+ const result = await this.query(
2363
+ `SELECT user_id, watermark, dismissed_ids, last_seen
2364
+ FROM ${this.tableName}
2365
+ WHERE user_id IN (${placeholders})`,
2366
+ userIds
2367
+ );
2368
+ const out = /* @__PURE__ */ new Map();
2369
+ for (const row of result.rows) {
2370
+ out.set(row.user_id, {
2371
+ watermark: normalizeWatermark2(row),
2372
+ dismissedIds: normalizeDismissedIds2(row),
2373
+ lastSeen: normalizeLastSeen2(row),
2374
+ deviceCount: 1
2375
+ });
2376
+ }
2377
+ return out;
2378
+ }
2379
+ async isHealthy() {
2380
+ try {
2381
+ await this.query("SELECT 1");
2382
+ return true;
2383
+ } catch {
2384
+ return false;
2385
+ }
2386
+ }
2387
+ async destroy() {
2388
+ }
2389
+ async ensureReady() {
2390
+ if (this.initialized) return;
2391
+ if (this.autoMigrate) {
2392
+ await this.query(
2393
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (
2394
+ user_id VARCHAR(255) PRIMARY KEY,
2395
+ watermark DATETIME(3) NULL,
2396
+ dismissed_ids JSON NOT NULL,
2397
+ last_seen DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
2398
+ created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
2399
+ updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
2400
+ )`
2401
+ );
2402
+ await this.query(
2403
+ `CREATE INDEX idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
2404
+ );
2405
+ }
2406
+ this.initialized = true;
2407
+ }
2408
+ };
2409
+
2410
+ // src/adapters/mongo.ts
2411
+ function normalizeDismissedIds3(ids) {
2412
+ if (!Array.isArray(ids)) return [];
2413
+ return ids.filter((id) => typeof id === "string");
2414
+ }
2415
+ function normalizeLastSeen3(value) {
2416
+ return typeof value === "string" && value ? value : (/* @__PURE__ */ new Date(0)).toISOString();
2417
+ }
2418
+ var MongoAdapter = class {
2419
+ userId;
2420
+ collection;
2421
+ watermark = null;
2422
+ dismissedIds = /* @__PURE__ */ new Set();
2423
+ constructor(options) {
2424
+ if (!options.userId) {
2425
+ throw new Error("MongoAdapter: userId is required");
2426
+ }
2427
+ this.userId = options.userId;
2428
+ this.collection = options.collection;
2429
+ }
2430
+ getWatermark() {
2431
+ return this.watermark;
2432
+ }
2433
+ getDismissedIds() {
2434
+ return this.dismissedIds;
2435
+ }
2436
+ dismiss(id) {
2437
+ this.dismissedIds.add(id);
2438
+ void this.collection.updateOne(
2439
+ { userId: this.userId },
2440
+ {
2441
+ $addToSet: { dismissedIds: id },
2442
+ $set: { lastSeen: (/* @__PURE__ */ new Date()).toISOString() }
2443
+ },
2444
+ { upsert: true }
2445
+ );
2446
+ }
2447
+ async dismissAll(now) {
2448
+ this.watermark = now.toISOString();
2449
+ this.dismissedIds.clear();
2450
+ await this.collection.updateOne(
2451
+ { userId: this.userId },
2452
+ {
2453
+ $set: {
2454
+ userId: this.userId,
2455
+ watermark: this.watermark,
2456
+ dismissedIds: [],
2457
+ lastSeen: this.watermark
2458
+ }
2459
+ },
2460
+ { upsert: true }
2461
+ );
2462
+ }
2463
+ async sync() {
2464
+ const doc = await this.collection.findOne({ userId: this.userId });
2465
+ this.watermark = doc?.watermark ?? null;
2466
+ this.dismissedIds = new Set(normalizeDismissedIds3(doc?.dismissedIds));
2467
+ }
2468
+ async dismissBatch(ids) {
2469
+ const unique = Array.from(new Set(ids));
2470
+ if (unique.length === 0) return;
2471
+ this.dismissedIds = /* @__PURE__ */ new Set([...this.dismissedIds, ...unique]);
2472
+ await this.collection.updateOne(
2473
+ { userId: this.userId },
2474
+ {
2475
+ $addToSet: { dismissedIds: { $each: unique } },
2476
+ $set: { lastSeen: (/* @__PURE__ */ new Date()).toISOString() }
2477
+ },
2478
+ { upsert: true }
2479
+ );
2480
+ }
2481
+ async resetUser(userId) {
2482
+ await this.collection.deleteOne({ userId });
2483
+ if (userId === this.userId) {
2484
+ this.watermark = null;
2485
+ this.dismissedIds.clear();
2486
+ }
2487
+ }
2488
+ async getBulkState(userIds) {
2489
+ const out = /* @__PURE__ */ new Map();
2490
+ if (userIds.length === 0) return out;
2491
+ if (this.collection.find) {
2492
+ const rows = await this.collection.find({ userId: { $in: userIds } }).toArray();
2493
+ for (const row of rows) {
2494
+ out.set(row.userId, {
2495
+ watermark: row.watermark ?? null,
2496
+ dismissedIds: normalizeDismissedIds3(row.dismissedIds),
2497
+ lastSeen: normalizeLastSeen3(row.lastSeen),
2498
+ deviceCount: 1
2499
+ });
2500
+ }
2501
+ return out;
2502
+ }
2503
+ await Promise.all(
2504
+ userIds.map(async (userId) => {
2505
+ const row = await this.collection.findOne({ userId });
2506
+ if (!row) return;
2507
+ out.set(userId, {
2508
+ watermark: row.watermark ?? null,
2509
+ dismissedIds: normalizeDismissedIds3(row.dismissedIds),
2510
+ lastSeen: normalizeLastSeen3(row.lastSeen),
2511
+ deviceCount: 1
2512
+ });
2513
+ })
2514
+ );
2515
+ return out;
2516
+ }
2517
+ async isHealthy() {
2518
+ try {
2519
+ await this.collection.findOne({});
2520
+ return true;
2521
+ } catch {
2522
+ return false;
2523
+ }
2524
+ }
2525
+ async destroy() {
2526
+ }
2527
+ };
2528
+
2529
+ // src/adapters/sqlite.ts
2530
+ function parseDismissedIds2(value) {
2531
+ if (Array.isArray(value)) {
2532
+ return value.filter((item) => typeof item === "string");
2533
+ }
2534
+ if (typeof value === "string" && value.trim()) {
2535
+ try {
2536
+ const parsed = JSON.parse(value);
2537
+ if (Array.isArray(parsed)) {
2538
+ return parsed.filter((item) => typeof item === "string");
2539
+ }
2540
+ } catch {
2541
+ return [];
2542
+ }
2543
+ }
2544
+ return [];
2545
+ }
2546
+ function normalizeDismissedIds4(row) {
2547
+ if (!row) return [];
2548
+ return parseDismissedIds2(row.dismissed_ids ?? row.dismissedIds);
2549
+ }
2550
+ function normalizeWatermark3(row) {
2551
+ if (!row) return null;
2552
+ return row.watermark ?? null;
2553
+ }
2554
+ function normalizeLastSeen4(row) {
2555
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
2556
+ return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
2557
+ }
2558
+ var SQLiteAdapter = class {
2559
+ userId;
2560
+ query;
2561
+ tableName;
2562
+ autoMigrate;
2563
+ watermark = null;
2564
+ dismissedIds = /* @__PURE__ */ new Set();
2565
+ initialized = false;
2566
+ constructor(options) {
2567
+ if (!options.userId) {
2568
+ throw new Error("SQLiteAdapter: userId is required");
2569
+ }
2570
+ this.userId = options.userId;
2571
+ this.query = options.query;
2572
+ this.tableName = options.tableName ?? "featuredrop_state";
2573
+ this.autoMigrate = options.autoMigrate ?? true;
2574
+ }
2575
+ getWatermark() {
2576
+ return this.watermark;
2577
+ }
2578
+ getDismissedIds() {
2579
+ return this.dismissedIds;
2580
+ }
2581
+ dismiss(id) {
2582
+ this.dismissedIds.add(id);
2583
+ void this.dismissBatch([id]);
2584
+ }
2585
+ async dismissAll(now) {
2586
+ this.watermark = now.toISOString();
2587
+ this.dismissedIds.clear();
2588
+ await this.ensureReady();
2589
+ await this.query(
2590
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
2591
+ VALUES (?, ?, ?, ?, ?, ?)
2592
+ ON CONFLICT(user_id)
2593
+ DO UPDATE SET watermark = excluded.watermark, dismissed_ids = excluded.dismissed_ids, last_seen = excluded.last_seen, updated_at = excluded.updated_at`,
2594
+ [this.userId, this.watermark, JSON.stringify([]), this.watermark, this.watermark, this.watermark]
2595
+ );
2596
+ }
2597
+ async sync() {
2598
+ await this.ensureReady();
2599
+ const result = await this.query(
2600
+ `SELECT watermark, dismissed_ids, last_seen FROM ${this.tableName} WHERE user_id = ? LIMIT 1`,
2601
+ [this.userId]
2602
+ );
2603
+ const row = result.rows[0];
2604
+ this.watermark = normalizeWatermark3(row);
2605
+ this.dismissedIds = new Set(normalizeDismissedIds4(row));
2606
+ }
2607
+ async dismissBatch(ids) {
2608
+ const uniqueIds = Array.from(new Set(ids));
2609
+ if (uniqueIds.length === 0) return;
2610
+ await this.ensureReady();
2611
+ const merged = /* @__PURE__ */ new Set([
2612
+ ...Array.from(this.dismissedIds),
2613
+ ...uniqueIds
2614
+ ]);
2615
+ const mergedArray = Array.from(merged);
2616
+ this.dismissedIds = merged;
2617
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2618
+ await this.query(
2619
+ `INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
2620
+ VALUES (?, ?, ?, ?, ?, ?)
2621
+ ON CONFLICT(user_id)
2622
+ DO UPDATE SET dismissed_ids = excluded.dismissed_ids, last_seen = excluded.last_seen, updated_at = excluded.updated_at`,
2623
+ [this.userId, this.watermark, JSON.stringify(mergedArray), nowIso, nowIso, nowIso]
2624
+ );
2625
+ }
2626
+ async resetUser(userId) {
2627
+ await this.ensureReady();
2628
+ await this.query(`DELETE FROM ${this.tableName} WHERE user_id = ?`, [userId]);
2629
+ if (userId === this.userId) {
2630
+ this.watermark = null;
2631
+ this.dismissedIds.clear();
2632
+ }
2633
+ }
2634
+ async getBulkState(userIds) {
2635
+ await this.ensureReady();
2636
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
2637
+ const placeholders = userIds.map(() => "?").join(", ");
2638
+ const result = await this.query(
2639
+ `SELECT user_id, watermark, dismissed_ids, last_seen
2640
+ FROM ${this.tableName}
2641
+ WHERE user_id IN (${placeholders})`,
2642
+ userIds
2643
+ );
2644
+ const out = /* @__PURE__ */ new Map();
2645
+ for (const row of result.rows) {
2646
+ out.set(row.user_id, {
2647
+ watermark: normalizeWatermark3(row),
2648
+ dismissedIds: normalizeDismissedIds4(row),
2649
+ lastSeen: normalizeLastSeen4(row),
2650
+ deviceCount: 1
2651
+ });
2652
+ }
2653
+ return out;
2654
+ }
2655
+ async isHealthy() {
2656
+ try {
2657
+ await this.query("SELECT 1");
2658
+ return true;
2659
+ } catch {
2660
+ return false;
2661
+ }
2662
+ }
2663
+ async destroy() {
2664
+ }
2665
+ async ensureReady() {
2666
+ if (this.initialized) return;
2667
+ if (this.autoMigrate) {
2668
+ await this.query(
2669
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (
2670
+ user_id TEXT PRIMARY KEY,
2671
+ watermark TEXT,
2672
+ dismissed_ids TEXT NOT NULL,
2673
+ last_seen TEXT NOT NULL,
2674
+ created_at TEXT NOT NULL,
2675
+ updated_at TEXT NOT NULL
2676
+ )`
2677
+ );
2678
+ await this.query(
2679
+ `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
2680
+ );
2681
+ }
2682
+ this.initialized = true;
2683
+ }
2684
+ };
2685
+
2686
+ // src/adapters/supabase.ts
2687
+ function normalizeDismissedIds5(row) {
2688
+ if (!row || !Array.isArray(row.dismissed_ids)) return [];
2689
+ return row.dismissed_ids.filter((id) => typeof id === "string");
2690
+ }
2691
+ function normalizeWatermark4(row) {
2692
+ if (!row) return null;
2693
+ return row.watermark ?? null;
2694
+ }
2695
+ function normalizeLastSeen5(row) {
2696
+ if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
2697
+ return row.last_seen ?? (/* @__PURE__ */ new Date(0)).toISOString();
2698
+ }
2699
+ function throwOnError(error) {
2700
+ if (!error) return;
2701
+ throw new Error(`SupabaseAdapter: ${error.message ?? "unknown error"}`);
2702
+ }
2703
+ var SupabaseAdapter = class {
2704
+ userId;
2705
+ client;
2706
+ tableName;
2707
+ realtime;
2708
+ watermark = null;
2709
+ dismissedIds = /* @__PURE__ */ new Set();
2710
+ realtimeChannel = null;
2711
+ syncing = false;
2712
+ constructor(options) {
2713
+ if (!options.userId) {
2714
+ throw new Error("SupabaseAdapter: userId is required");
2715
+ }
2716
+ this.userId = options.userId;
2717
+ this.client = options.client;
2718
+ this.tableName = options.tableName ?? "featuredrop_state";
2719
+ this.realtime = options.realtime ?? false;
2720
+ if (this.realtime && this.client.channel) {
2721
+ this.setupRealtime();
2722
+ }
2723
+ }
2724
+ getWatermark() {
2725
+ return this.watermark;
2726
+ }
2727
+ getDismissedIds() {
2728
+ return this.dismissedIds;
2729
+ }
2730
+ dismiss(id) {
2731
+ this.dismissedIds.add(id);
2732
+ void this.dismissBatch([id]);
2733
+ }
2734
+ async dismissAll(now) {
2735
+ this.watermark = now.toISOString();
2736
+ this.dismissedIds.clear();
2737
+ await this.upsertState({
2738
+ watermark: this.watermark,
2739
+ dismissed_ids: [],
2740
+ last_seen: this.watermark
2741
+ });
2742
+ }
2743
+ async sync() {
2744
+ if (this.syncing) return;
2745
+ this.syncing = true;
2746
+ try {
2747
+ const row = await this.fetchState(this.userId);
2748
+ this.watermark = normalizeWatermark4(row);
2749
+ this.dismissedIds = new Set(normalizeDismissedIds5(row));
2750
+ } finally {
2751
+ this.syncing = false;
2752
+ }
2753
+ }
2754
+ async dismissBatch(ids) {
2755
+ const uniqueIds = Array.from(new Set(ids));
2756
+ if (uniqueIds.length === 0) return;
2757
+ const merged = /* @__PURE__ */ new Set([
2758
+ ...Array.from(this.dismissedIds),
2759
+ ...uniqueIds
2760
+ ]);
2761
+ this.dismissedIds = merged;
2762
+ await this.upsertState({
2763
+ watermark: this.watermark,
2764
+ dismissed_ids: Array.from(merged),
2765
+ last_seen: (/* @__PURE__ */ new Date()).toISOString()
2766
+ });
2767
+ }
2768
+ async resetUser(userId) {
2769
+ const result = await this.client.from(this.tableName).delete().eq("user_id", userId);
2770
+ throwOnError(result.error);
2771
+ if (userId === this.userId) {
2772
+ this.watermark = null;
2773
+ this.dismissedIds.clear();
2774
+ }
2775
+ }
2776
+ async getBulkState(userIds) {
2777
+ const out = /* @__PURE__ */ new Map();
2778
+ if (userIds.length === 0) return out;
2779
+ await Promise.all(
2780
+ userIds.map(async (userId) => {
2781
+ const row = await this.fetchState(userId);
2782
+ if (!row) return;
2783
+ out.set(userId, {
2784
+ watermark: normalizeWatermark4(row),
2785
+ dismissedIds: normalizeDismissedIds5(row),
2786
+ lastSeen: normalizeLastSeen5(row),
2787
+ deviceCount: 1
2788
+ });
2789
+ })
2790
+ );
2791
+ return out;
2792
+ }
2793
+ async isHealthy() {
2794
+ try {
2795
+ await this.client.from(this.tableName).select("user_id").eq("user_id", this.userId).maybeSingle();
2796
+ return true;
2797
+ } catch {
2798
+ return false;
2799
+ }
2800
+ }
2801
+ async destroy() {
2802
+ if (this.realtimeChannel && this.client.removeChannel) {
2803
+ await this.client.removeChannel(this.realtimeChannel);
2804
+ }
2805
+ this.realtimeChannel = null;
2806
+ }
2807
+ async fetchState(userId) {
2808
+ const result = await this.client.from(this.tableName).select("user_id, watermark, dismissed_ids, last_seen").eq("user_id", userId).maybeSingle();
2809
+ throwOnError(result.error);
2810
+ return result.data;
2811
+ }
2812
+ async upsertState(state) {
2813
+ const payload = {
2814
+ user_id: this.userId,
2815
+ watermark: state.watermark,
2816
+ dismissed_ids: state.dismissed_ids,
2817
+ last_seen: state.last_seen
2818
+ };
2819
+ const result = await this.client.from(this.tableName).upsert(payload);
2820
+ throwOnError(result.error);
2821
+ }
2822
+ setupRealtime() {
2823
+ const channelFactory = this.client.channel;
2824
+ if (!channelFactory) return;
2825
+ this.realtimeChannel = channelFactory(`featuredrop:${this.tableName}:${this.userId}`).on(
2826
+ "postgres_changes",
2827
+ {
2828
+ event: "*",
2829
+ schema: "public",
2830
+ table: this.tableName,
2831
+ filter: `user_id=eq.${this.userId}`
2832
+ },
2833
+ () => {
2834
+ void this.sync();
2835
+ }
2836
+ ).subscribe();
2837
+ }
2838
+ };
2839
+
2840
+ exports.AmplitudeAdapter = AmplitudeAdapter;
2841
+ exports.AnalyticsCollector = AnalyticsCollector;
2842
+ exports.CustomAdapter = CustomAdapter;
2843
+ exports.FEATUREDROP_THEMES = FEATUREDROP_THEMES;
2844
+ exports.FEATUREDROP_TRANSLATIONS = FEATUREDROP_TRANSLATIONS;
2845
+ exports.HybridAdapter = HybridAdapter;
123
2846
  exports.LocalStorageAdapter = LocalStorageAdapter;
124
2847
  exports.MemoryAdapter = MemoryAdapter;
2848
+ exports.MixpanelAdapter = MixpanelAdapter;
2849
+ exports.MongoAdapter = MongoAdapter;
2850
+ exports.MySQLAdapter = MySQLAdapter;
2851
+ exports.PostHogAdapter = PostHogAdapter;
2852
+ exports.PostgresAdapter = PostgresAdapter;
2853
+ exports.RedisAdapter = RedisAdapter;
2854
+ exports.RemoteAdapter = RemoteAdapter;
2855
+ exports.SQLiteAdapter = SQLiteAdapter;
2856
+ exports.SegmentAdapter = SegmentAdapter;
2857
+ exports.SupabaseAdapter = SupabaseAdapter;
2858
+ exports.TriggerEngine = TriggerEngine;
2859
+ exports.applyAnnouncementThrottle = applyAnnouncementThrottle;
2860
+ exports.applyFeatureVariant = applyFeatureVariant;
2861
+ exports.applyFeatureVariants = applyFeatureVariants;
2862
+ exports.computeManifestStats = computeManifestStats;
2863
+ exports.createAdoptionMetrics = createAdoptionMetrics;
125
2864
  exports.createManifest = createManifest;
2865
+ exports.createTheme = createTheme;
2866
+ exports.featureEntryJsonSchema = featureEntryJsonSchema;
2867
+ exports.featureEntrySchema = featureEntrySchema;
2868
+ exports.featureManifestJsonSchema = featureManifestJsonSchema;
2869
+ exports.featureManifestSchema = featureManifestSchema;
2870
+ exports.generateMarkdownChangelog = generateMarkdownChangelog;
2871
+ exports.generateRSS = generateRSS;
126
2872
  exports.getFeatureById = getFeatureById;
2873
+ exports.getFeatureVariantName = getFeatureVariantName;
127
2874
  exports.getNewFeatureCount = getNewFeatureCount;
128
2875
  exports.getNewFeatures = getNewFeatures;
129
2876
  exports.getNewFeaturesByCategory = getNewFeaturesByCategory;
2877
+ exports.getNewFeaturesSorted = getNewFeaturesSorted;
2878
+ exports.getOrCreateVariantKey = getOrCreateVariantKey;
2879
+ exports.hasDependencyCycle = hasDependencyCycle;
130
2880
  exports.hasNewFeature = hasNewFeature;
131
2881
  exports.isNew = isNew;
2882
+ exports.isTriggerMatch = isTriggerMatch;
2883
+ exports.matchesAudience = matchesAudience;
2884
+ exports.parseDescription = parseDescription;
2885
+ exports.resolveDependencyOrder = resolveDependencyOrder;
2886
+ exports.resolveTheme = resolveTheme;
2887
+ exports.resolveTranslations = resolveTranslations;
2888
+ exports.runDoctor = runDoctor;
2889
+ exports.sortFeaturesByDependencies = sortFeaturesByDependencies;
2890
+ exports.themeToCSSVariables = themeToCSSVariables;
2891
+ exports.validateManifest = validateManifest;
132
2892
  //# sourceMappingURL=index.cjs.map
133
2893
  //# sourceMappingURL=index.cjs.map