featuredrop 1.4.0 → 2.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 +287 -760
- package/dist/adapters.cjs +1757 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +744 -0
- package/dist/adapters.d.ts +744 -0
- package/dist/adapters.js +1745 -0
- package/dist/adapters.js.map +1 -0
- package/dist/admin.cjs +148 -32
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +14 -3
- package/dist/admin.d.ts +14 -3
- package/dist/admin.js +148 -32
- package/dist/admin.js.map +1 -1
- package/dist/bridges.cjs +111 -13
- package/dist/bridges.cjs.map +1 -1
- package/dist/bridges.d.cts +12 -5
- package/dist/bridges.d.ts +12 -5
- package/dist/bridges.js +111 -13
- package/dist/bridges.js.map +1 -1
- package/dist/ci.cjs +34 -0
- package/dist/ci.cjs.map +1 -1
- package/dist/ci.d.cts +5 -1
- package/dist/ci.d.ts +5 -1
- package/dist/ci.js +34 -1
- package/dist/ci.js.map +1 -1
- package/dist/cms.cjs +835 -0
- package/dist/cms.cjs.map +1 -0
- package/dist/cms.d.cts +236 -0
- package/dist/cms.d.ts +236 -0
- package/dist/cms.js +829 -0
- package/dist/cms.js.map +1 -0
- package/dist/flags.cjs +27 -7
- package/dist/flags.cjs.map +1 -1
- package/dist/flags.d.cts +14 -0
- package/dist/flags.d.ts +14 -0
- package/dist/flags.js +27 -7
- package/dist/flags.js.map +1 -1
- package/dist/index.cjs +52 -4481
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1340
- package/dist/index.d.ts +1 -1340
- package/dist/index.js +53 -4388
- package/dist/index.js.map +1 -1
- package/dist/markdown.cjs +257 -0
- package/dist/markdown.cjs.map +1 -0
- package/dist/markdown.d.cts +9 -0
- package/dist/markdown.d.ts +9 -0
- package/dist/markdown.js +234 -0
- package/dist/markdown.js.map +1 -0
- package/dist/renderer.cjs +503 -0
- package/dist/renderer.cjs.map +1 -0
- package/dist/renderer.d.cts +250 -0
- package/dist/renderer.d.ts +250 -0
- package/dist/renderer.js +501 -0
- package/dist/renderer.js.map +1 -0
- package/dist/rss.cjs +291 -0
- package/dist/rss.cjs.map +1 -0
- package/dist/rss.d.cts +158 -0
- package/dist/rss.d.ts +158 -0
- package/dist/rss.js +268 -0
- package/dist/rss.js.map +1 -0
- package/package.json +72 -6
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
/** Entry type label — determines default icon/color in UI */
|
|
2
|
+
type FeatureType = "feature" | "improvement" | "fix" | "breaking";
|
|
3
|
+
/** Priority level for announcements */
|
|
4
|
+
type FeaturePriority = "critical" | "normal" | "low";
|
|
5
|
+
/** Call-to-action for a feature entry */
|
|
6
|
+
interface FeatureCTA {
|
|
7
|
+
/** Button/link label */
|
|
8
|
+
label: string;
|
|
9
|
+
/** URL to navigate to */
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
/** Variant-level overrides for A/B announcement testing */
|
|
13
|
+
interface FeatureVariant {
|
|
14
|
+
/** Optional variant-specific label override */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Optional variant-specific description override */
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Optional variant-specific image override */
|
|
19
|
+
image?: string;
|
|
20
|
+
/** Optional variant-specific CTA override */
|
|
21
|
+
cta?: FeatureCTA;
|
|
22
|
+
/** Optional variant-specific metadata overrides */
|
|
23
|
+
meta?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** Audience targeting rule — determines which user segments see a feature */
|
|
26
|
+
interface AudienceRule {
|
|
27
|
+
/** Plans that should see this feature (e.g. ["pro", "enterprise"]) */
|
|
28
|
+
plan?: string[];
|
|
29
|
+
/** Roles that should see this feature (e.g. ["admin", "editor"]) */
|
|
30
|
+
role?: string[];
|
|
31
|
+
/** Regions that should see this feature (e.g. ["us", "eu"]) */
|
|
32
|
+
region?: string[];
|
|
33
|
+
/** Arbitrary key-value pairs for custom matching logic */
|
|
34
|
+
custom?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
/** Dependency gates for progressive feature discovery */
|
|
37
|
+
interface FeatureDependencies {
|
|
38
|
+
/** Features the user must have seen before this one can surface */
|
|
39
|
+
seen?: string[];
|
|
40
|
+
/** Features the user must have clicked before this one can surface */
|
|
41
|
+
clicked?: string[];
|
|
42
|
+
/** Features the user must have dismissed before this one can surface */
|
|
43
|
+
dismissed?: string[];
|
|
44
|
+
}
|
|
45
|
+
/** Runtime context used by trigger evaluation */
|
|
46
|
+
interface TriggerContext {
|
|
47
|
+
/** Current app route/path */
|
|
48
|
+
path?: string;
|
|
49
|
+
/** Named events observed in this session */
|
|
50
|
+
events?: ReadonlySet<string>;
|
|
51
|
+
/** Named milestone flags reached in this session */
|
|
52
|
+
milestones?: ReadonlySet<string>;
|
|
53
|
+
/** Usage counters keyed by event/pattern name */
|
|
54
|
+
usage?: Record<string, number>;
|
|
55
|
+
/** Session elapsed time in milliseconds */
|
|
56
|
+
elapsedMs?: number;
|
|
57
|
+
/** Scroll completion percentage (0-100) */
|
|
58
|
+
scrollPercent?: number;
|
|
59
|
+
/** Optional additional trigger context */
|
|
60
|
+
metadata?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
type FeatureTrigger = {
|
|
63
|
+
type: "page";
|
|
64
|
+
match: string | RegExp;
|
|
65
|
+
} | {
|
|
66
|
+
type: "usage";
|
|
67
|
+
event: string;
|
|
68
|
+
minActions?: number;
|
|
69
|
+
} | {
|
|
70
|
+
type: "time";
|
|
71
|
+
minSeconds: number;
|
|
72
|
+
} | {
|
|
73
|
+
type: "milestone";
|
|
74
|
+
event: string;
|
|
75
|
+
} | {
|
|
76
|
+
type: "frustration";
|
|
77
|
+
pattern: string;
|
|
78
|
+
threshold?: number;
|
|
79
|
+
} | {
|
|
80
|
+
type: "scroll";
|
|
81
|
+
minPercent?: number;
|
|
82
|
+
} | {
|
|
83
|
+
type: "custom";
|
|
84
|
+
evaluate: (context: TriggerContext) => boolean;
|
|
85
|
+
};
|
|
86
|
+
/** A single feature entry in the manifest */
|
|
87
|
+
interface FeatureEntry {
|
|
88
|
+
/** Unique identifier for the feature */
|
|
89
|
+
id: string;
|
|
90
|
+
/** Human-readable label (e.g. "Decision Journal") */
|
|
91
|
+
label: string;
|
|
92
|
+
/** Optional longer description (supports markdown in UI components) */
|
|
93
|
+
description?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Semantic version targeting.
|
|
96
|
+
* If provided as an object, requires `appVersion` to be supplied to the provider/helpers.
|
|
97
|
+
* - introduced: earliest app version that includes this feature
|
|
98
|
+
* - showNewUntil: stop showing "new" once appVersion reaches this
|
|
99
|
+
* - deprecatedAt: hide feature for app versions at or above this (optional safety)
|
|
100
|
+
* - showIn: range string, e.g. ">=2.5.0 <3.0.0"
|
|
101
|
+
*/
|
|
102
|
+
version?: string | {
|
|
103
|
+
introduced?: string;
|
|
104
|
+
showNewUntil?: string;
|
|
105
|
+
deprecatedAt?: string;
|
|
106
|
+
showIn?: string;
|
|
107
|
+
};
|
|
108
|
+
/** ISO date when this feature was released */
|
|
109
|
+
releasedAt: string;
|
|
110
|
+
/** ISO date after which the "new" badge should stop showing */
|
|
111
|
+
showNewUntil: string;
|
|
112
|
+
/** Optional key to match navigation items (e.g. "/journal", "settings") */
|
|
113
|
+
sidebarKey?: string;
|
|
114
|
+
/** Optional grouping category (e.g. "ai", "billing", "core") */
|
|
115
|
+
category?: string;
|
|
116
|
+
/** Optional product scope (`"*"`, `"askverdict"`, etc.) for multi-product manifests */
|
|
117
|
+
product?: string;
|
|
118
|
+
/** Optional URL to link to (e.g. docs page, changelog entry) */
|
|
119
|
+
url?: string;
|
|
120
|
+
/** Optional feature flag key; requires a flag bridge to evaluate */
|
|
121
|
+
flagKey?: string;
|
|
122
|
+
/** Entry type — determines default icon/color in UI components */
|
|
123
|
+
type?: FeatureType;
|
|
124
|
+
/** Priority level — critical entries get special treatment in UI */
|
|
125
|
+
priority?: FeaturePriority;
|
|
126
|
+
/** Optional image/screenshot URL */
|
|
127
|
+
image?: string;
|
|
128
|
+
/** Optional call-to-action button */
|
|
129
|
+
cta?: FeatureCTA;
|
|
130
|
+
/** ISO date — entry is hidden until this date (scheduled publishing) */
|
|
131
|
+
publishAt?: string;
|
|
132
|
+
/** Optional arbitrary metadata */
|
|
133
|
+
meta?: Record<string, unknown>;
|
|
134
|
+
/** A/B variants keyed by variant name (e.g. control, treatment_a) */
|
|
135
|
+
variants?: Record<string, FeatureVariant>;
|
|
136
|
+
/** Percentage split per variant (same order as variants object keys) */
|
|
137
|
+
variantSplit?: number[];
|
|
138
|
+
/** Audience targeting — if set, only matching users see this feature */
|
|
139
|
+
audience?: AudienceRule;
|
|
140
|
+
/** Dependency requirements (progressive disclosure sequencing) */
|
|
141
|
+
dependsOn?: FeatureDependencies;
|
|
142
|
+
/** Contextual trigger rule */
|
|
143
|
+
trigger?: FeatureTrigger;
|
|
144
|
+
}
|
|
145
|
+
/** The full feature manifest — an array of feature entries */
|
|
146
|
+
type FeatureManifest = readonly FeatureEntry[];
|
|
147
|
+
/**
|
|
148
|
+
* Storage adapter interface — implement for your persistence layer.
|
|
149
|
+
*
|
|
150
|
+
* The adapter bridges two data sources:
|
|
151
|
+
* - **Watermark**: a server-side timestamp ("features seen at")
|
|
152
|
+
* - **Dismissed IDs**: client-side per-feature dismissals
|
|
153
|
+
*/
|
|
154
|
+
interface StorageAdapter {
|
|
155
|
+
/** Get the user's "features seen at" watermark (ISO string or null) */
|
|
156
|
+
getWatermark(): string | null;
|
|
157
|
+
/** Get the set of individually dismissed feature IDs */
|
|
158
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
159
|
+
/** Dismiss a single feature by ID */
|
|
160
|
+
dismiss(id: string): void;
|
|
161
|
+
/** Dismiss all features — sets watermark to `now` and clears dismissals */
|
|
162
|
+
dismissAll(now: Date): Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
/** Extended server-side dismissal state */
|
|
165
|
+
interface DismissalState {
|
|
166
|
+
/** Server-side watermark */
|
|
167
|
+
watermark: string | null;
|
|
168
|
+
/** Dismissed feature IDs */
|
|
169
|
+
dismissedIds: string[];
|
|
170
|
+
/** ISO timestamp of last interaction */
|
|
171
|
+
lastSeen: string;
|
|
172
|
+
/** Estimated device count contributing to this state */
|
|
173
|
+
deviceCount: number;
|
|
174
|
+
}
|
|
175
|
+
/** Server-capable storage adapter */
|
|
176
|
+
interface ServerStorageAdapter extends StorageAdapter {
|
|
177
|
+
/** Current user for this adapter instance */
|
|
178
|
+
userId: string;
|
|
179
|
+
/** Pull latest state from the server/database */
|
|
180
|
+
sync(): Promise<void>;
|
|
181
|
+
/** Dismiss multiple features at once */
|
|
182
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
183
|
+
/** Reset state for a target user */
|
|
184
|
+
resetUser(userId: string): Promise<void>;
|
|
185
|
+
/** Fetch multiple users' state */
|
|
186
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
187
|
+
/** Health check */
|
|
188
|
+
isHealthy(): Promise<boolean>;
|
|
189
|
+
/** Cleanup resources */
|
|
190
|
+
destroy(): Promise<void>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface LocalStorageAdapterOptions {
|
|
194
|
+
/** Key prefix for localStorage entries. Default: "featuredrop" */
|
|
195
|
+
prefix?: string;
|
|
196
|
+
/** Server-side watermark (ISO string). Typically from user profile. */
|
|
197
|
+
watermark?: string | null;
|
|
198
|
+
/** Callback when dismissAll is called. Use for server-side watermark updates. */
|
|
199
|
+
onDismissAll?: (now: Date) => Promise<void>;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* localStorage-based storage adapter.
|
|
203
|
+
*
|
|
204
|
+
* Architecture:
|
|
205
|
+
* - **Watermark** comes from the server (passed at construction time)
|
|
206
|
+
* - **Per-feature dismissals** are stored in localStorage (zero server writes)
|
|
207
|
+
* - **dismissAll()** optionally calls a server callback, then clears localStorage
|
|
208
|
+
*
|
|
209
|
+
* Gracefully handles SSR environments where `window`/`localStorage` is unavailable.
|
|
210
|
+
*/
|
|
211
|
+
declare class LocalStorageAdapter implements StorageAdapter {
|
|
212
|
+
private readonly prefix;
|
|
213
|
+
private readonly watermarkValue;
|
|
214
|
+
private readonly onDismissAllCallback?;
|
|
215
|
+
private readonly dismissedKey;
|
|
216
|
+
constructor(options?: LocalStorageAdapterOptions);
|
|
217
|
+
getWatermark(): string | null;
|
|
218
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
219
|
+
dismiss(id: string): void;
|
|
220
|
+
dismissAll(now: Date): Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface IndexedDBAdapterOptions {
|
|
224
|
+
prefix?: string;
|
|
225
|
+
watermark?: string | null;
|
|
226
|
+
dbName?: string;
|
|
227
|
+
storeName?: string;
|
|
228
|
+
onDismissAll?: (now: Date) => Promise<void>;
|
|
229
|
+
/** Optional remote state fetch for offline-first sync reconciliation. */
|
|
230
|
+
onSyncState?: () => Promise<{
|
|
231
|
+
watermark?: string | null;
|
|
232
|
+
dismissedIds?: string[];
|
|
233
|
+
}>;
|
|
234
|
+
/** Optional remote flush for queued single-dismiss operations. */
|
|
235
|
+
onFlushDismissBatch?: (ids: string[]) => Promise<void>;
|
|
236
|
+
/** Optional remote flush for queued dismiss-all operations. */
|
|
237
|
+
onFlushDismissAll?: (watermark: string) => Promise<void>;
|
|
238
|
+
/** Delay before queued operations are flushed. Default: 500ms. */
|
|
239
|
+
flushDebounceMs?: number;
|
|
240
|
+
/** Attach online/visibility listeners to trigger sync+flush. Default: true in browser. */
|
|
241
|
+
autoSyncOnOnline?: boolean;
|
|
242
|
+
}
|
|
243
|
+
declare class IndexedDBAdapter implements StorageAdapter {
|
|
244
|
+
private readonly prefix;
|
|
245
|
+
private readonly dbName;
|
|
246
|
+
private readonly storeName;
|
|
247
|
+
private readonly onDismissAllCallback?;
|
|
248
|
+
private readonly onSyncStateCallback?;
|
|
249
|
+
private readonly onFlushDismissBatchCallback?;
|
|
250
|
+
private readonly onFlushDismissAllCallback?;
|
|
251
|
+
private readonly flushDebounceMs;
|
|
252
|
+
private readonly autoSyncOnOnline;
|
|
253
|
+
private watermark;
|
|
254
|
+
private dismissed;
|
|
255
|
+
private queue;
|
|
256
|
+
private readonly hydratePromise;
|
|
257
|
+
private flushTimer;
|
|
258
|
+
private flushing;
|
|
259
|
+
private readonly boundOnlineHandler;
|
|
260
|
+
private readonly boundVisibilityHandler;
|
|
261
|
+
constructor(options?: IndexedDBAdapterOptions);
|
|
262
|
+
getWatermark(): string | null;
|
|
263
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
264
|
+
dismiss(id: string): void;
|
|
265
|
+
dismissAll(now: Date): Promise<void>;
|
|
266
|
+
/** Flush queued dismiss operations to optional remote callbacks. */
|
|
267
|
+
flushQueue(): Promise<void>;
|
|
268
|
+
/** Merge local state with optional remote source, then flush queued writes. */
|
|
269
|
+
syncFromRemote(): Promise<void>;
|
|
270
|
+
/** Cleanup optional browser listeners. */
|
|
271
|
+
destroy(): void;
|
|
272
|
+
private persist;
|
|
273
|
+
private hydrateFromIndexedDB;
|
|
274
|
+
private readIndexedDBState;
|
|
275
|
+
private writeIndexedDBState;
|
|
276
|
+
private openDb;
|
|
277
|
+
private scheduleFlush;
|
|
278
|
+
private getLastDismissAll;
|
|
279
|
+
private collectDismissBatch;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* In-memory storage adapter.
|
|
284
|
+
*
|
|
285
|
+
* Useful for:
|
|
286
|
+
* - Testing (no side effects)
|
|
287
|
+
* - Server-side rendering (no `window`/`localStorage`)
|
|
288
|
+
* - Environments without persistent storage
|
|
289
|
+
*/
|
|
290
|
+
declare class MemoryAdapter implements StorageAdapter {
|
|
291
|
+
private watermark;
|
|
292
|
+
private dismissed;
|
|
293
|
+
constructor(options?: {
|
|
294
|
+
watermark?: string | null;
|
|
295
|
+
});
|
|
296
|
+
getWatermark(): string | null;
|
|
297
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
298
|
+
dismiss(id: string): void;
|
|
299
|
+
dismissAll(now: Date): Promise<void>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface RemoteAdapterOptions {
|
|
303
|
+
/** Base URL for the feature API (e.g. https://api.example.com/api/features) */
|
|
304
|
+
url: string;
|
|
305
|
+
/** Optional headers applied to all requests */
|
|
306
|
+
headers?: Record<string, string>;
|
|
307
|
+
/** Polling interval in ms for stale-while-revalidate (default: 5 minutes) */
|
|
308
|
+
fetchInterval?: number;
|
|
309
|
+
/** Data format, currently supports 'rest' (default) */
|
|
310
|
+
format?: "rest";
|
|
311
|
+
/** Optional user identifier to pass to state endpoint */
|
|
312
|
+
userId?: string;
|
|
313
|
+
/** Number of retries after the initial request (default: 3) */
|
|
314
|
+
retryAttempts?: number;
|
|
315
|
+
/** Base backoff delay used between retries (default: 250ms) */
|
|
316
|
+
retryBaseDelayMs?: number;
|
|
317
|
+
/** Consecutive failed operations before opening the circuit (default: 5) */
|
|
318
|
+
circuitBreakerThreshold?: number;
|
|
319
|
+
/** Cooldown period while the circuit is open (default: 60s) */
|
|
320
|
+
circuitBreakerCooldownMs?: number;
|
|
321
|
+
/** Optional sleep function override for test environments */
|
|
322
|
+
sleep?: (delayMs: number) => Promise<void>;
|
|
323
|
+
/** Optional timestamp function override for test environments */
|
|
324
|
+
now?: () => number;
|
|
325
|
+
/** Request timeout in milliseconds (default: 10s). */
|
|
326
|
+
requestTimeoutMs?: number;
|
|
327
|
+
/** HTTP statuses that should be retried (default: 408,429,500,502,503,504). */
|
|
328
|
+
retryOnStatuses?: number[];
|
|
329
|
+
/** Debounce window for batching dismiss calls (default: 150ms). */
|
|
330
|
+
dismissBatchWindowMs?: number;
|
|
331
|
+
/** Max ids sent in a single dismiss-batch request (default: 100). */
|
|
332
|
+
maxDismissBatchSize?: number;
|
|
333
|
+
/** Disable `/dismiss-batch` endpoint usage and always send single dismiss requests. */
|
|
334
|
+
disableDismissBatch?: boolean;
|
|
335
|
+
/** Flush/sync pending state when browser regains connectivity (default: false). */
|
|
336
|
+
syncOnOnline?: boolean;
|
|
337
|
+
/** Flush/sync pending state when tab becomes visible again (default: false). */
|
|
338
|
+
syncOnVisibilityChange?: boolean;
|
|
339
|
+
/** Optional periodic state sync interval in ms (default: disabled). */
|
|
340
|
+
syncIntervalMs?: number;
|
|
341
|
+
/** Optional error hook for diagnostics/telemetry. */
|
|
342
|
+
onError?: (error: unknown, context: {
|
|
343
|
+
operation: string;
|
|
344
|
+
attempt: number;
|
|
345
|
+
}) => void;
|
|
346
|
+
}
|
|
347
|
+
declare class RemoteAdapter implements StorageAdapter {
|
|
348
|
+
private readonly baseUrl;
|
|
349
|
+
private readonly headers;
|
|
350
|
+
private readonly fetchInterval;
|
|
351
|
+
private readonly userId?;
|
|
352
|
+
private dismissedIds;
|
|
353
|
+
private watermark;
|
|
354
|
+
private lastManifest;
|
|
355
|
+
private lastFetchTs;
|
|
356
|
+
private readonly retryAttempts;
|
|
357
|
+
private readonly retryBaseDelayMs;
|
|
358
|
+
private readonly circuitBreakerThreshold;
|
|
359
|
+
private readonly circuitBreakerCooldownMs;
|
|
360
|
+
private readonly sleep;
|
|
361
|
+
private readonly now;
|
|
362
|
+
private readonly requestTimeoutMs;
|
|
363
|
+
private readonly retryOnStatuses;
|
|
364
|
+
private readonly dismissBatchWindowMs;
|
|
365
|
+
private readonly maxDismissBatchSize;
|
|
366
|
+
private readonly disableDismissBatch;
|
|
367
|
+
private readonly syncOnOnline;
|
|
368
|
+
private readonly syncOnVisibilityChange;
|
|
369
|
+
private readonly syncIntervalMs;
|
|
370
|
+
private readonly onError?;
|
|
371
|
+
private consecutiveFailures;
|
|
372
|
+
private circuitOpenUntil;
|
|
373
|
+
private manifestInFlight;
|
|
374
|
+
private stateSyncInFlight;
|
|
375
|
+
private dismissFlushInFlight;
|
|
376
|
+
private dismissFlushTimer;
|
|
377
|
+
private syncTimer;
|
|
378
|
+
private pendingDismissIds;
|
|
379
|
+
private supportsDismissBatch;
|
|
380
|
+
private readonly boundOnlineHandler;
|
|
381
|
+
private readonly boundVisibilityHandler;
|
|
382
|
+
constructor(options: RemoteAdapterOptions);
|
|
383
|
+
/** Fetch manifest with stale-while-revalidate */
|
|
384
|
+
fetchManifest(force?: boolean): Promise<FeatureManifest>;
|
|
385
|
+
/** Fetch state (watermark + dismissed IDs) */
|
|
386
|
+
syncState(): Promise<void>;
|
|
387
|
+
getWatermark(): string | null;
|
|
388
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
389
|
+
dismiss(id: string): void;
|
|
390
|
+
dismissAll(now: Date): Promise<void>;
|
|
391
|
+
/** Cleanup timers/listeners and flush any queued dismiss operations. */
|
|
392
|
+
destroy(): Promise<void>;
|
|
393
|
+
/** Returns current adapter health; false while circuit breaker is open. */
|
|
394
|
+
isHealthy(): Promise<boolean>;
|
|
395
|
+
private flushDismiss;
|
|
396
|
+
private flushDismissAll;
|
|
397
|
+
private isCircuitOpen;
|
|
398
|
+
private markFailure;
|
|
399
|
+
private markSuccess;
|
|
400
|
+
/** Force-flush queued dismiss IDs immediately. */
|
|
401
|
+
flushPendingDismisses(): Promise<void>;
|
|
402
|
+
private flushPendingDismissesInternal;
|
|
403
|
+
private flushDismissBatch;
|
|
404
|
+
private scheduleDismissFlush;
|
|
405
|
+
private runRecoverySync;
|
|
406
|
+
private requestJson;
|
|
407
|
+
private requestVoid;
|
|
408
|
+
private request;
|
|
409
|
+
private shouldRetry;
|
|
410
|
+
private withRetry;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface QueryResultRow {
|
|
414
|
+
watermark?: string | null;
|
|
415
|
+
dismissed_ids?: string[] | null;
|
|
416
|
+
dismissedIds?: string[] | null;
|
|
417
|
+
last_seen?: string | null;
|
|
418
|
+
lastSeen?: string | null;
|
|
419
|
+
}
|
|
420
|
+
interface PostgresQueryResult<T = QueryResultRow> {
|
|
421
|
+
rows: T[];
|
|
422
|
+
rowCount?: number | null;
|
|
423
|
+
}
|
|
424
|
+
type PostgresQueryFn = <T = QueryResultRow>(sql: string, params?: unknown[]) => Promise<PostgresQueryResult<T>>;
|
|
425
|
+
interface PostgresAdapterOptions {
|
|
426
|
+
userId: string;
|
|
427
|
+
query: PostgresQueryFn;
|
|
428
|
+
tableName?: string;
|
|
429
|
+
autoMigrate?: boolean;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Postgres-backed storage adapter.
|
|
433
|
+
*
|
|
434
|
+
* This adapter is dependency-free by design and accepts a user-provided
|
|
435
|
+
* query function, allowing integration with pg, drizzle, prisma, etc.
|
|
436
|
+
*/
|
|
437
|
+
declare class PostgresAdapter implements ServerStorageAdapter {
|
|
438
|
+
readonly userId: string;
|
|
439
|
+
private readonly query;
|
|
440
|
+
private readonly tableName;
|
|
441
|
+
private readonly autoMigrate;
|
|
442
|
+
private watermark;
|
|
443
|
+
private dismissedIds;
|
|
444
|
+
private initialized;
|
|
445
|
+
constructor(options: PostgresAdapterOptions);
|
|
446
|
+
getWatermark(): string | null;
|
|
447
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
448
|
+
dismiss(id: string): void;
|
|
449
|
+
dismissAll(now: Date): Promise<void>;
|
|
450
|
+
sync(): Promise<void>;
|
|
451
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
452
|
+
resetUser(userId: string): Promise<void>;
|
|
453
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
454
|
+
isHealthy(): Promise<boolean>;
|
|
455
|
+
destroy(): Promise<void>;
|
|
456
|
+
private ensureReady;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
interface RedisLikePipeline {
|
|
460
|
+
set(key: string, value: string): RedisLikePipeline;
|
|
461
|
+
del(key: string): RedisLikePipeline;
|
|
462
|
+
sadd(key: string, ...members: string[]): RedisLikePipeline;
|
|
463
|
+
exec(): Promise<unknown>;
|
|
464
|
+
}
|
|
465
|
+
interface RedisLikeClient {
|
|
466
|
+
get(key: string): Promise<string | null>;
|
|
467
|
+
set(key: string, value: string): Promise<unknown>;
|
|
468
|
+
del(key: string): Promise<unknown>;
|
|
469
|
+
smembers(key: string): Promise<string[]>;
|
|
470
|
+
sadd(key: string, ...members: string[]): Promise<unknown>;
|
|
471
|
+
ping(): Promise<string>;
|
|
472
|
+
multi(): RedisLikePipeline;
|
|
473
|
+
quit?(): Promise<unknown>;
|
|
474
|
+
disconnect?(): void;
|
|
475
|
+
}
|
|
476
|
+
interface RedisAdapterOptions {
|
|
477
|
+
userId: string;
|
|
478
|
+
client: RedisLikeClient;
|
|
479
|
+
keyPrefix?: string;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Redis-backed storage adapter.
|
|
483
|
+
* Uses simple key + set structures to keep operations predictable.
|
|
484
|
+
*/
|
|
485
|
+
declare class RedisAdapter implements ServerStorageAdapter {
|
|
486
|
+
readonly userId: string;
|
|
487
|
+
private readonly client;
|
|
488
|
+
private readonly keyPrefix;
|
|
489
|
+
private watermark;
|
|
490
|
+
private dismissedIds;
|
|
491
|
+
constructor(options: RedisAdapterOptions);
|
|
492
|
+
getWatermark(): string | null;
|
|
493
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
494
|
+
dismiss(id: string): void;
|
|
495
|
+
dismissAll(now: Date): Promise<void>;
|
|
496
|
+
sync(): Promise<void>;
|
|
497
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
498
|
+
resetUser(userId: string): Promise<void>;
|
|
499
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
500
|
+
isHealthy(): Promise<boolean>;
|
|
501
|
+
destroy(): Promise<void>;
|
|
502
|
+
private watermarkKey;
|
|
503
|
+
private dismissedKey;
|
|
504
|
+
private lastSeenKey;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
interface HybridAdapterOptions {
|
|
508
|
+
local: StorageAdapter;
|
|
509
|
+
remote: ServerStorageAdapter;
|
|
510
|
+
/** If true, sync from remote before writes. Default false */
|
|
511
|
+
syncBeforeWrite?: boolean;
|
|
512
|
+
/** Batch window for queued dismiss writes. Default: 500ms */
|
|
513
|
+
dismissBatchWindowMs?: number;
|
|
514
|
+
/** Optional periodic sync interval in ms. Default: disabled (0) */
|
|
515
|
+
syncIntervalMs?: number;
|
|
516
|
+
/** Sync on browser visibility return. Default: true */
|
|
517
|
+
syncOnVisibilityChange?: boolean;
|
|
518
|
+
/** Sync on browser online event. Default: true */
|
|
519
|
+
syncOnOnline?: boolean;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Hybrid adapter that combines local immediacy with remote persistence.
|
|
523
|
+
*/
|
|
524
|
+
declare class HybridAdapter implements ServerStorageAdapter {
|
|
525
|
+
readonly userId: string;
|
|
526
|
+
private readonly local;
|
|
527
|
+
private readonly remote;
|
|
528
|
+
private readonly syncBeforeWrite;
|
|
529
|
+
private readonly dismissBatchWindowMs;
|
|
530
|
+
private readonly syncIntervalMs;
|
|
531
|
+
private readonly syncOnVisibilityChange;
|
|
532
|
+
private readonly syncOnOnline;
|
|
533
|
+
private pendingDismissIds;
|
|
534
|
+
private dismissTimer;
|
|
535
|
+
private syncTimer;
|
|
536
|
+
private flushInFlight;
|
|
537
|
+
private syncInFlight;
|
|
538
|
+
private readonly boundVisibilityHandler;
|
|
539
|
+
private readonly boundOnlineHandler;
|
|
540
|
+
constructor(options: HybridAdapterOptions);
|
|
541
|
+
getWatermark(): string | null;
|
|
542
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
543
|
+
dismiss(id: string): void;
|
|
544
|
+
dismissAll(now: Date): Promise<void>;
|
|
545
|
+
sync(): Promise<void>;
|
|
546
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
547
|
+
resetUser(userId: string): Promise<void>;
|
|
548
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
549
|
+
isHealthy(): Promise<boolean>;
|
|
550
|
+
destroy(): Promise<void>;
|
|
551
|
+
/** Manually flush queued dismiss operations to the remote adapter. */
|
|
552
|
+
flushPendingDismisses(): Promise<void>;
|
|
553
|
+
private flushPendingDismissesInternal;
|
|
554
|
+
private scheduleDismissFlush;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
interface MySQLRow {
|
|
558
|
+
user_id?: string;
|
|
559
|
+
watermark?: string | null;
|
|
560
|
+
dismissed_ids?: string[] | string | null;
|
|
561
|
+
dismissedIds?: string[] | string | null;
|
|
562
|
+
last_seen?: string | null;
|
|
563
|
+
lastSeen?: string | null;
|
|
564
|
+
}
|
|
565
|
+
interface MySQLQueryResult<T = MySQLRow> {
|
|
566
|
+
rows: T[];
|
|
567
|
+
}
|
|
568
|
+
type MySQLQueryFn = <T = MySQLRow>(sql: string, params?: unknown[]) => Promise<MySQLQueryResult<T>>;
|
|
569
|
+
interface MySQLAdapterOptions {
|
|
570
|
+
userId: string;
|
|
571
|
+
query: MySQLQueryFn;
|
|
572
|
+
tableName?: string;
|
|
573
|
+
autoMigrate?: boolean;
|
|
574
|
+
}
|
|
575
|
+
declare class MySQLAdapter implements ServerStorageAdapter {
|
|
576
|
+
readonly userId: string;
|
|
577
|
+
private readonly query;
|
|
578
|
+
private readonly tableName;
|
|
579
|
+
private readonly autoMigrate;
|
|
580
|
+
private watermark;
|
|
581
|
+
private dismissedIds;
|
|
582
|
+
private initialized;
|
|
583
|
+
constructor(options: MySQLAdapterOptions);
|
|
584
|
+
getWatermark(): string | null;
|
|
585
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
586
|
+
dismiss(id: string): void;
|
|
587
|
+
dismissAll(now: Date): Promise<void>;
|
|
588
|
+
sync(): Promise<void>;
|
|
589
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
590
|
+
resetUser(userId: string): Promise<void>;
|
|
591
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
592
|
+
isHealthy(): Promise<boolean>;
|
|
593
|
+
destroy(): Promise<void>;
|
|
594
|
+
private ensureReady;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
interface MongoStateDoc {
|
|
598
|
+
userId: string;
|
|
599
|
+
watermark?: string | null;
|
|
600
|
+
dismissedIds?: string[];
|
|
601
|
+
lastSeen?: string;
|
|
602
|
+
}
|
|
603
|
+
interface MongoFindCursor<T> {
|
|
604
|
+
toArray: () => Promise<T[]>;
|
|
605
|
+
}
|
|
606
|
+
interface MongoLikeCollection {
|
|
607
|
+
findOne: (filter: Record<string, unknown>) => Promise<MongoStateDoc | null>;
|
|
608
|
+
updateOne: (filter: Record<string, unknown>, update: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
609
|
+
deleteOne: (filter: Record<string, unknown>) => Promise<unknown>;
|
|
610
|
+
find?: (filter: Record<string, unknown>) => MongoFindCursor<MongoStateDoc>;
|
|
611
|
+
}
|
|
612
|
+
interface MongoAdapterOptions {
|
|
613
|
+
userId: string;
|
|
614
|
+
collection: MongoLikeCollection;
|
|
615
|
+
}
|
|
616
|
+
declare class MongoAdapter implements ServerStorageAdapter {
|
|
617
|
+
readonly userId: string;
|
|
618
|
+
private readonly collection;
|
|
619
|
+
private watermark;
|
|
620
|
+
private dismissedIds;
|
|
621
|
+
constructor(options: MongoAdapterOptions);
|
|
622
|
+
getWatermark(): string | null;
|
|
623
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
624
|
+
dismiss(id: string): void;
|
|
625
|
+
dismissAll(now: Date): Promise<void>;
|
|
626
|
+
sync(): Promise<void>;
|
|
627
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
628
|
+
resetUser(userId: string): Promise<void>;
|
|
629
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
630
|
+
isHealthy(): Promise<boolean>;
|
|
631
|
+
destroy(): Promise<void>;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
interface SQLiteRow {
|
|
635
|
+
user_id?: string;
|
|
636
|
+
watermark?: string | null;
|
|
637
|
+
dismissed_ids?: string[] | string | null;
|
|
638
|
+
dismissedIds?: string[] | string | null;
|
|
639
|
+
last_seen?: string | null;
|
|
640
|
+
lastSeen?: string | null;
|
|
641
|
+
}
|
|
642
|
+
interface SQLiteQueryResult<T = SQLiteRow> {
|
|
643
|
+
rows: T[];
|
|
644
|
+
}
|
|
645
|
+
type SQLiteQueryFn = <T = SQLiteRow>(sql: string, params?: unknown[]) => Promise<SQLiteQueryResult<T>>;
|
|
646
|
+
interface SQLiteAdapterOptions {
|
|
647
|
+
userId: string;
|
|
648
|
+
query: SQLiteQueryFn;
|
|
649
|
+
tableName?: string;
|
|
650
|
+
autoMigrate?: boolean;
|
|
651
|
+
}
|
|
652
|
+
declare class SQLiteAdapter implements ServerStorageAdapter {
|
|
653
|
+
readonly userId: string;
|
|
654
|
+
private readonly query;
|
|
655
|
+
private readonly tableName;
|
|
656
|
+
private readonly autoMigrate;
|
|
657
|
+
private watermark;
|
|
658
|
+
private dismissedIds;
|
|
659
|
+
private initialized;
|
|
660
|
+
constructor(options: SQLiteAdapterOptions);
|
|
661
|
+
getWatermark(): string | null;
|
|
662
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
663
|
+
dismiss(id: string): void;
|
|
664
|
+
dismissAll(now: Date): Promise<void>;
|
|
665
|
+
sync(): Promise<void>;
|
|
666
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
667
|
+
resetUser(userId: string): Promise<void>;
|
|
668
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
669
|
+
isHealthy(): Promise<boolean>;
|
|
670
|
+
destroy(): Promise<void>;
|
|
671
|
+
private ensureReady;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
interface SupabaseErrorLike {
|
|
675
|
+
message?: string;
|
|
676
|
+
}
|
|
677
|
+
interface SupabaseMaybeSingleResult<T> {
|
|
678
|
+
data: T | null;
|
|
679
|
+
error: SupabaseErrorLike | null;
|
|
680
|
+
}
|
|
681
|
+
interface SupabaseMutationResult {
|
|
682
|
+
error: SupabaseErrorLike | null;
|
|
683
|
+
}
|
|
684
|
+
interface SupabaseSelectQuery<T> {
|
|
685
|
+
eq: (column: string, value: unknown) => SupabaseSelectQuery<T>;
|
|
686
|
+
maybeSingle: () => Promise<SupabaseMaybeSingleResult<T>>;
|
|
687
|
+
}
|
|
688
|
+
interface SupabaseMutationQuery {
|
|
689
|
+
eq: (column: string, value: unknown) => Promise<SupabaseMutationResult>;
|
|
690
|
+
}
|
|
691
|
+
interface SupabaseTableClient<T> {
|
|
692
|
+
select: (columns: string) => SupabaseSelectQuery<T>;
|
|
693
|
+
upsert: (value: Record<string, unknown>) => Promise<SupabaseMutationResult>;
|
|
694
|
+
update: (value: Record<string, unknown>) => SupabaseMutationQuery;
|
|
695
|
+
delete: () => SupabaseMutationQuery;
|
|
696
|
+
}
|
|
697
|
+
interface SupabaseRealtimeChannelLike {
|
|
698
|
+
on: (event: "postgres_changes", filter: Record<string, unknown>, callback: () => void) => SupabaseRealtimeChannelLike;
|
|
699
|
+
subscribe: (callback?: (status: string) => void) => SupabaseRealtimeChannelLike;
|
|
700
|
+
}
|
|
701
|
+
interface SupabaseClientLike {
|
|
702
|
+
from: <T = SupabaseStateRow>(table: string) => SupabaseTableClient<T>;
|
|
703
|
+
channel?: (name: string) => SupabaseRealtimeChannelLike;
|
|
704
|
+
removeChannel?: (channel: SupabaseRealtimeChannelLike) => void | Promise<void>;
|
|
705
|
+
}
|
|
706
|
+
interface SupabaseStateRow {
|
|
707
|
+
[key: string]: unknown;
|
|
708
|
+
user_id: string;
|
|
709
|
+
watermark?: string | null;
|
|
710
|
+
dismissed_ids?: string[] | null;
|
|
711
|
+
last_seen?: string | null;
|
|
712
|
+
}
|
|
713
|
+
interface SupabaseAdapterOptions {
|
|
714
|
+
userId: string;
|
|
715
|
+
client: SupabaseClientLike;
|
|
716
|
+
tableName?: string;
|
|
717
|
+
realtime?: boolean;
|
|
718
|
+
}
|
|
719
|
+
declare class SupabaseAdapter implements ServerStorageAdapter {
|
|
720
|
+
readonly userId: string;
|
|
721
|
+
private readonly client;
|
|
722
|
+
private readonly tableName;
|
|
723
|
+
private readonly realtime;
|
|
724
|
+
private watermark;
|
|
725
|
+
private dismissedIds;
|
|
726
|
+
private realtimeChannel;
|
|
727
|
+
private syncing;
|
|
728
|
+
constructor(options: SupabaseAdapterOptions);
|
|
729
|
+
getWatermark(): string | null;
|
|
730
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
731
|
+
dismiss(id: string): void;
|
|
732
|
+
dismissAll(now: Date): Promise<void>;
|
|
733
|
+
sync(): Promise<void>;
|
|
734
|
+
dismissBatch(ids: string[]): Promise<void>;
|
|
735
|
+
resetUser(userId: string): Promise<void>;
|
|
736
|
+
getBulkState(userIds: string[]): Promise<Map<string, DismissalState>>;
|
|
737
|
+
isHealthy(): Promise<boolean>;
|
|
738
|
+
destroy(): Promise<void>;
|
|
739
|
+
private fetchState;
|
|
740
|
+
private upsertState;
|
|
741
|
+
private setupRealtime;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export { HybridAdapter, type HybridAdapterOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LocalStorageAdapter, type LocalStorageAdapterOptions, MemoryAdapter, MongoAdapter, type MongoAdapterOptions, type MongoLikeCollection, MySQLAdapter, type MySQLAdapterOptions, type MySQLQueryFn, type MySQLQueryResult, PostgresAdapter, type PostgresAdapterOptions, type PostgresQueryFn, type PostgresQueryResult, RedisAdapter, type RedisAdapterOptions, type RedisLikeClient, type RedisLikePipeline, RemoteAdapter, type RemoteAdapterOptions, SQLiteAdapter, type SQLiteAdapterOptions, type SQLiteQueryFn, type SQLiteQueryResult, SupabaseAdapter, type SupabaseAdapterOptions, type SupabaseClientLike, type SupabaseRealtimeChannelLike };
|