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.
- package/README.md +626 -186
- package/dist/angular.cjs +286 -0
- package/dist/angular.cjs.map +1 -0
- package/dist/angular.d.cts +229 -0
- package/dist/angular.d.ts +229 -0
- package/dist/angular.js +283 -0
- package/dist/angular.js.map +1 -0
- package/dist/featuredrop.cjs +1256 -0
- package/dist/featuredrop.cjs.map +1 -0
- package/dist/index.cjs +2769 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1020 -9
- package/dist/index.d.ts +1020 -9
- package/dist/index.js +2726 -10
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +7289 -0
- package/dist/preact.cjs.map +1 -0
- package/dist/preact.d.cts +1266 -0
- package/dist/preact.d.ts +1266 -0
- package/dist/preact.js +7259 -0
- package/dist/preact.js.map +1 -0
- package/dist/react.cjs +7142 -49
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1119 -7
- package/dist/react.d.ts +1119 -7
- package/dist/react.js +7122 -52
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +215 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +203 -0
- package/dist/schema.d.ts +203 -0
- package/dist/schema.js +209 -0
- package/dist/schema.js.map +1 -0
- package/dist/solid.cjs +373 -0
- package/dist/solid.cjs.map +1 -0
- package/dist/solid.d.cts +242 -0
- package/dist/solid.d.ts +242 -0
- package/dist/solid.js +366 -0
- package/dist/solid.js.map +1 -0
- package/dist/svelte.cjs +329 -0
- package/dist/svelte.cjs.map +1 -0
- package/dist/svelte.js +324 -0
- package/dist/svelte.js.map +1 -0
- package/dist/testing.cjs +1422 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +339 -0
- package/dist/testing.d.ts +339 -0
- package/dist/testing.js +1415 -0
- package/dist/testing.js.map +1 -0
- package/dist/vue.cjs +1084 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.js +1072 -0
- package/dist/vue.js.map +1 -0
- package/dist/web-components.cjs +483 -0
- package/dist/web-components.cjs.map +1 -0
- package/dist/web-components.d.cts +211 -0
- package/dist/web-components.d.ts +211 -0
- package/dist/web-components.js +477 -0
- package/dist/web-components.js.map +1 -0
- 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
|
|
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(
|
|
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(
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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(/<(\/?)([a-z0-9]+)([^>]*)>/gi, (match, slash, tag, rest) => {
|
|
415
|
+
if (!allowTags.includes(tag.toLowerCase())) return match;
|
|
416
|
+
const decodedRest = rest.replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|