featuredrop 1.3.0 → 2.1.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 +134 -15
- 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 +114 -15
- 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/featuredrop.cjs +23 -2
- package/dist/featuredrop.cjs.map +1 -1
- 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 -4460
- 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 -4387
- 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/preact.cjs +37 -12
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.js +17 -12
- package/dist/preact.js.map +1 -1
- package/dist/react.cjs +37 -12
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +17 -12
- package/dist/react.js.map +1 -1
- 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/dist/vue.cjs +23 -2
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +3 -2
- package/dist/vue.js.map +1 -1
- package/package.json +72 -6
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,1745 @@
|
|
|
1
|
+
// src/adapters/local-storage.ts
|
|
2
|
+
var DISMISSED_SUFFIX = ":dismissed";
|
|
3
|
+
var LocalStorageAdapter = class {
|
|
4
|
+
prefix;
|
|
5
|
+
watermarkValue;
|
|
6
|
+
onDismissAllCallback;
|
|
7
|
+
dismissedKey;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.prefix = options.prefix ?? "featuredrop";
|
|
10
|
+
this.watermarkValue = options.watermark ?? null;
|
|
11
|
+
this.onDismissAllCallback = options.onDismissAll;
|
|
12
|
+
this.dismissedKey = `${this.prefix}${DISMISSED_SUFFIX}`;
|
|
13
|
+
}
|
|
14
|
+
getWatermark() {
|
|
15
|
+
return this.watermarkValue;
|
|
16
|
+
}
|
|
17
|
+
getDismissedIds() {
|
|
18
|
+
try {
|
|
19
|
+
if (typeof window === "undefined") return /* @__PURE__ */ new Set();
|
|
20
|
+
const raw = localStorage.getItem(this.dismissedKey);
|
|
21
|
+
if (!raw) return /* @__PURE__ */ new Set();
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (Array.isArray(parsed)) return new Set(parsed);
|
|
24
|
+
return /* @__PURE__ */ new Set();
|
|
25
|
+
} catch {
|
|
26
|
+
return /* @__PURE__ */ new Set();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
dismiss(id) {
|
|
30
|
+
try {
|
|
31
|
+
if (typeof window === "undefined") return;
|
|
32
|
+
const raw = localStorage.getItem(this.dismissedKey);
|
|
33
|
+
const existing = raw ? JSON.parse(raw) : [];
|
|
34
|
+
if (!existing.includes(id)) {
|
|
35
|
+
existing.push(id);
|
|
36
|
+
localStorage.setItem(this.dismissedKey, JSON.stringify(existing));
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async dismissAll(now) {
|
|
42
|
+
try {
|
|
43
|
+
if (typeof window !== "undefined") {
|
|
44
|
+
localStorage.removeItem(this.dismissedKey);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
if (this.onDismissAllCallback) {
|
|
49
|
+
await this.onDismissAllCallback(now);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/adapters/indexeddb.ts
|
|
55
|
+
var DISMISSED_SUFFIX2 = ":dismissed";
|
|
56
|
+
var WATERMARK_SUFFIX = ":watermark";
|
|
57
|
+
var QUEUE_SUFFIX = ":queue";
|
|
58
|
+
function canUseLocalStorage() {
|
|
59
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
60
|
+
}
|
|
61
|
+
function readLocalStorageState(prefix) {
|
|
62
|
+
if (!canUseLocalStorage()) {
|
|
63
|
+
return { watermark: null, dismissed: [], queue: [] };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const dismissedRaw = localStorage.getItem(`${prefix}${DISMISSED_SUFFIX2}`);
|
|
67
|
+
const watermarkRaw = localStorage.getItem(`${prefix}${WATERMARK_SUFFIX}`);
|
|
68
|
+
const queueRaw = localStorage.getItem(`${prefix}${QUEUE_SUFFIX}`);
|
|
69
|
+
const dismissedParsed = dismissedRaw ? JSON.parse(dismissedRaw) : [];
|
|
70
|
+
const queueParsed = queueRaw ? JSON.parse(queueRaw) : [];
|
|
71
|
+
return {
|
|
72
|
+
watermark: typeof watermarkRaw === "string" ? watermarkRaw : null,
|
|
73
|
+
dismissed: Array.isArray(dismissedParsed) ? dismissedParsed.filter((value) => typeof value === "string") : [],
|
|
74
|
+
queue: normalizeQueue(queueParsed)
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return { watermark: null, dismissed: [], queue: [] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function writeLocalStorageState(prefix, state) {
|
|
81
|
+
if (!canUseLocalStorage()) return;
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem(`${prefix}${DISMISSED_SUFFIX2}`, JSON.stringify(state.dismissed));
|
|
84
|
+
if (state.watermark) {
|
|
85
|
+
localStorage.setItem(`${prefix}${WATERMARK_SUFFIX}`, state.watermark);
|
|
86
|
+
} else {
|
|
87
|
+
localStorage.removeItem(`${prefix}${WATERMARK_SUFFIX}`);
|
|
88
|
+
}
|
|
89
|
+
if (state.queue && state.queue.length > 0) {
|
|
90
|
+
localStorage.setItem(`${prefix}${QUEUE_SUFFIX}`, JSON.stringify(state.queue));
|
|
91
|
+
} else {
|
|
92
|
+
localStorage.removeItem(`${prefix}${QUEUE_SUFFIX}`);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function getIndexedDBFactory() {
|
|
98
|
+
if (typeof globalThis === "undefined") return null;
|
|
99
|
+
const candidate = globalThis.indexedDB;
|
|
100
|
+
return candidate ?? null;
|
|
101
|
+
}
|
|
102
|
+
function normalizeQueue(value) {
|
|
103
|
+
if (!Array.isArray(value)) return [];
|
|
104
|
+
const queue = [];
|
|
105
|
+
for (const item of value) {
|
|
106
|
+
if (!item || typeof item !== "object") continue;
|
|
107
|
+
const candidate = item;
|
|
108
|
+
if (candidate.type === "dismiss" && typeof candidate.id === "string") {
|
|
109
|
+
queue.push({ type: "dismiss", id: candidate.id });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (candidate.type === "dismissAll" && typeof candidate.watermark === "string") {
|
|
113
|
+
queue.push({ type: "dismissAll", watermark: candidate.watermark });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return queue;
|
|
118
|
+
}
|
|
119
|
+
function normalizeDismissedIds(value) {
|
|
120
|
+
if (!Array.isArray(value)) return [];
|
|
121
|
+
return value.filter((entry) => typeof entry === "string");
|
|
122
|
+
}
|
|
123
|
+
function parseIso(value) {
|
|
124
|
+
if (!value) return Number.NaN;
|
|
125
|
+
return new Date(value).getTime();
|
|
126
|
+
}
|
|
127
|
+
function resolveLatestWatermark(a, b) {
|
|
128
|
+
if (!a) return b ?? null;
|
|
129
|
+
if (!b) return a;
|
|
130
|
+
const aTs = parseIso(a);
|
|
131
|
+
const bTs = parseIso(b);
|
|
132
|
+
if (!Number.isFinite(aTs)) return b;
|
|
133
|
+
if (!Number.isFinite(bTs)) return a;
|
|
134
|
+
return aTs >= bTs ? a : b;
|
|
135
|
+
}
|
|
136
|
+
var IndexedDBAdapter = class {
|
|
137
|
+
prefix;
|
|
138
|
+
dbName;
|
|
139
|
+
storeName;
|
|
140
|
+
onDismissAllCallback;
|
|
141
|
+
onSyncStateCallback;
|
|
142
|
+
onFlushDismissBatchCallback;
|
|
143
|
+
onFlushDismissAllCallback;
|
|
144
|
+
flushDebounceMs;
|
|
145
|
+
autoSyncOnOnline;
|
|
146
|
+
watermark;
|
|
147
|
+
dismissed;
|
|
148
|
+
queue;
|
|
149
|
+
hydratePromise;
|
|
150
|
+
flushTimer = null;
|
|
151
|
+
flushing = false;
|
|
152
|
+
boundOnlineHandler;
|
|
153
|
+
boundVisibilityHandler;
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
this.prefix = options.prefix ?? "featuredrop";
|
|
156
|
+
this.dbName = options.dbName ?? "featuredrop";
|
|
157
|
+
this.storeName = options.storeName ?? "state";
|
|
158
|
+
this.onDismissAllCallback = options.onDismissAll;
|
|
159
|
+
this.onSyncStateCallback = options.onSyncState;
|
|
160
|
+
this.onFlushDismissBatchCallback = options.onFlushDismissBatch;
|
|
161
|
+
this.onFlushDismissAllCallback = options.onFlushDismissAll;
|
|
162
|
+
this.flushDebounceMs = options.flushDebounceMs ?? 500;
|
|
163
|
+
this.autoSyncOnOnline = options.autoSyncOnOnline ?? true;
|
|
164
|
+
const localState = readLocalStorageState(this.prefix);
|
|
165
|
+
this.watermark = options.watermark ?? localState.watermark;
|
|
166
|
+
this.dismissed = new Set(localState.dismissed);
|
|
167
|
+
this.queue = localState.queue ?? [];
|
|
168
|
+
this.hydratePromise = this.hydrateFromIndexedDB();
|
|
169
|
+
const canAttachListeners = this.autoSyncOnOnline && typeof window !== "undefined";
|
|
170
|
+
if (canAttachListeners) {
|
|
171
|
+
this.boundOnlineHandler = () => {
|
|
172
|
+
void this.syncFromRemote();
|
|
173
|
+
};
|
|
174
|
+
this.boundVisibilityHandler = () => {
|
|
175
|
+
if (document.visibilityState === "visible") {
|
|
176
|
+
void this.syncFromRemote();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
window.addEventListener("online", this.boundOnlineHandler);
|
|
180
|
+
document.addEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
181
|
+
} else {
|
|
182
|
+
this.boundOnlineHandler = null;
|
|
183
|
+
this.boundVisibilityHandler = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
getWatermark() {
|
|
187
|
+
return this.watermark;
|
|
188
|
+
}
|
|
189
|
+
getDismissedIds() {
|
|
190
|
+
return this.dismissed;
|
|
191
|
+
}
|
|
192
|
+
dismiss(id) {
|
|
193
|
+
if (!id || this.dismissed.has(id)) return;
|
|
194
|
+
this.dismissed = new Set(this.dismissed).add(id);
|
|
195
|
+
this.queue.push({ type: "dismiss", id });
|
|
196
|
+
this.persist();
|
|
197
|
+
this.scheduleFlush();
|
|
198
|
+
}
|
|
199
|
+
async dismissAll(now) {
|
|
200
|
+
this.watermark = now.toISOString();
|
|
201
|
+
this.dismissed = /* @__PURE__ */ new Set();
|
|
202
|
+
this.queue = [{ type: "dismissAll", watermark: this.watermark }];
|
|
203
|
+
this.persist();
|
|
204
|
+
this.scheduleFlush();
|
|
205
|
+
await this.onDismissAllCallback?.(now);
|
|
206
|
+
}
|
|
207
|
+
/** Flush queued dismiss operations to optional remote callbacks. */
|
|
208
|
+
async flushQueue() {
|
|
209
|
+
if (this.flushing || this.queue.length === 0) return;
|
|
210
|
+
if (!this.onFlushDismissBatchCallback && !this.onFlushDismissAllCallback) return;
|
|
211
|
+
this.flushing = true;
|
|
212
|
+
try {
|
|
213
|
+
const operations = [...this.queue];
|
|
214
|
+
const lastDismissAll = this.getLastDismissAll(operations);
|
|
215
|
+
const dismissIds = this.collectDismissBatch(operations, !!lastDismissAll);
|
|
216
|
+
const hasDismissAll = !!lastDismissAll;
|
|
217
|
+
const needsDismissBatch = dismissIds.length > 0;
|
|
218
|
+
if (hasDismissAll && !this.onFlushDismissAllCallback) return;
|
|
219
|
+
if (needsDismissBatch && !this.onFlushDismissBatchCallback) return;
|
|
220
|
+
if (lastDismissAll && this.onFlushDismissAllCallback) {
|
|
221
|
+
await this.onFlushDismissAllCallback(lastDismissAll.watermark);
|
|
222
|
+
}
|
|
223
|
+
if (dismissIds.length > 0 && this.onFlushDismissBatchCallback) {
|
|
224
|
+
await this.onFlushDismissBatchCallback(dismissIds);
|
|
225
|
+
}
|
|
226
|
+
if (this.queue.length <= operations.length) {
|
|
227
|
+
this.queue = [];
|
|
228
|
+
} else {
|
|
229
|
+
this.queue = this.queue.slice(operations.length);
|
|
230
|
+
}
|
|
231
|
+
this.persist();
|
|
232
|
+
} catch {
|
|
233
|
+
} finally {
|
|
234
|
+
this.flushing = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Merge local state with optional remote source, then flush queued writes. */
|
|
238
|
+
async syncFromRemote() {
|
|
239
|
+
await this.hydratePromise.catch(() => void 0);
|
|
240
|
+
if (this.onSyncStateCallback) {
|
|
241
|
+
try {
|
|
242
|
+
const remote = await this.onSyncStateCallback();
|
|
243
|
+
const mergedDismissed = new Set(this.dismissed);
|
|
244
|
+
for (const id of normalizeDismissedIds(remote.dismissedIds)) {
|
|
245
|
+
mergedDismissed.add(id);
|
|
246
|
+
}
|
|
247
|
+
this.dismissed = mergedDismissed;
|
|
248
|
+
this.watermark = resolveLatestWatermark(this.watermark, remote.watermark ?? null);
|
|
249
|
+
this.persist();
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
await this.flushQueue();
|
|
254
|
+
}
|
|
255
|
+
/** Cleanup optional browser listeners. */
|
|
256
|
+
destroy() {
|
|
257
|
+
if (this.flushTimer) {
|
|
258
|
+
clearTimeout(this.flushTimer);
|
|
259
|
+
this.flushTimer = null;
|
|
260
|
+
}
|
|
261
|
+
if (this.boundOnlineHandler && typeof window !== "undefined") {
|
|
262
|
+
window.removeEventListener("online", this.boundOnlineHandler);
|
|
263
|
+
}
|
|
264
|
+
if (this.boundVisibilityHandler && typeof document !== "undefined") {
|
|
265
|
+
document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
persist() {
|
|
269
|
+
const snapshot = {
|
|
270
|
+
watermark: this.watermark,
|
|
271
|
+
dismissed: Array.from(this.dismissed),
|
|
272
|
+
queue: this.queue
|
|
273
|
+
};
|
|
274
|
+
writeLocalStorageState(this.prefix, snapshot);
|
|
275
|
+
void this.writeIndexedDBState(snapshot);
|
|
276
|
+
}
|
|
277
|
+
async hydrateFromIndexedDB() {
|
|
278
|
+
const state = await this.readIndexedDBState();
|
|
279
|
+
if (!state) return;
|
|
280
|
+
this.watermark = state.watermark;
|
|
281
|
+
this.dismissed = new Set(state.dismissed);
|
|
282
|
+
this.queue = state.queue ?? [];
|
|
283
|
+
writeLocalStorageState(this.prefix, state);
|
|
284
|
+
}
|
|
285
|
+
async readIndexedDBState() {
|
|
286
|
+
const db = await this.openDb();
|
|
287
|
+
if (!db) return null;
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
290
|
+
const store = tx.objectStore(this.storeName);
|
|
291
|
+
const request = store.get(this.prefix);
|
|
292
|
+
request.onsuccess = () => {
|
|
293
|
+
const value = request.result;
|
|
294
|
+
if (!value) {
|
|
295
|
+
resolve(null);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
resolve({
|
|
299
|
+
watermark: typeof value.watermark === "string" ? value.watermark : null,
|
|
300
|
+
dismissed: normalizeDismissedIds(value.dismissed),
|
|
301
|
+
queue: normalizeQueue(value.queue)
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
request.onerror = () => resolve(null);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async writeIndexedDBState(state) {
|
|
308
|
+
await this.hydratePromise.catch(() => void 0);
|
|
309
|
+
const db = await this.openDb();
|
|
310
|
+
if (!db) return;
|
|
311
|
+
await new Promise((resolve) => {
|
|
312
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
313
|
+
const store = tx.objectStore(this.storeName);
|
|
314
|
+
store.put(state, this.prefix);
|
|
315
|
+
tx.oncomplete = () => resolve();
|
|
316
|
+
tx.onerror = () => resolve();
|
|
317
|
+
tx.onabort = () => resolve();
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async openDb() {
|
|
321
|
+
const factory = getIndexedDBFactory();
|
|
322
|
+
if (!factory) return null;
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
const request = factory.open(this.dbName, 1);
|
|
325
|
+
request.onerror = () => resolve(null);
|
|
326
|
+
request.onupgradeneeded = () => {
|
|
327
|
+
const db = request.result;
|
|
328
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
329
|
+
db.createObjectStore(this.storeName);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
request.onsuccess = () => resolve(request.result);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
scheduleFlush() {
|
|
336
|
+
if (this.flushTimer) return;
|
|
337
|
+
this.flushTimer = setTimeout(() => {
|
|
338
|
+
this.flushTimer = null;
|
|
339
|
+
void this.flushQueue();
|
|
340
|
+
}, this.flushDebounceMs);
|
|
341
|
+
}
|
|
342
|
+
getLastDismissAll(operations) {
|
|
343
|
+
for (let index = operations.length - 1; index >= 0; index--) {
|
|
344
|
+
const operation = operations[index];
|
|
345
|
+
if (operation.type === "dismissAll") {
|
|
346
|
+
return { watermark: operation.watermark };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
collectDismissBatch(operations, skipBeforeDismissAll) {
|
|
352
|
+
const startIndex = skipBeforeDismissAll ? operations.reduce(
|
|
353
|
+
(lastIndex, operation, index) => operation.type === "dismissAll" ? index : lastIndex,
|
|
354
|
+
-1
|
|
355
|
+
) : -1;
|
|
356
|
+
const batch = /* @__PURE__ */ new Set();
|
|
357
|
+
for (let index = startIndex + 1; index < operations.length; index++) {
|
|
358
|
+
const operation = operations[index];
|
|
359
|
+
if (operation.type === "dismiss") {
|
|
360
|
+
batch.add(operation.id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return Array.from(batch);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/adapters/memory.ts
|
|
368
|
+
var MemoryAdapter = class {
|
|
369
|
+
watermark;
|
|
370
|
+
dismissed;
|
|
371
|
+
constructor(options = {}) {
|
|
372
|
+
this.watermark = options.watermark ?? null;
|
|
373
|
+
this.dismissed = /* @__PURE__ */ new Set();
|
|
374
|
+
}
|
|
375
|
+
getWatermark() {
|
|
376
|
+
return this.watermark;
|
|
377
|
+
}
|
|
378
|
+
getDismissedIds() {
|
|
379
|
+
return this.dismissed;
|
|
380
|
+
}
|
|
381
|
+
dismiss(id) {
|
|
382
|
+
this.dismissed.add(id);
|
|
383
|
+
}
|
|
384
|
+
async dismissAll(now) {
|
|
385
|
+
this.watermark = now.toISOString();
|
|
386
|
+
this.dismissed.clear();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/adapters/remote.ts
|
|
391
|
+
var RemoteHttpError = class extends Error {
|
|
392
|
+
status;
|
|
393
|
+
constructor(message, status) {
|
|
394
|
+
super(message);
|
|
395
|
+
this.name = "RemoteHttpError";
|
|
396
|
+
this.status = status;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function assertFetch() {
|
|
400
|
+
if (typeof fetch === "undefined") {
|
|
401
|
+
throw new Error("RemoteAdapter requires global fetch (Node 18+ or polyfill)");
|
|
402
|
+
}
|
|
403
|
+
return fetch;
|
|
404
|
+
}
|
|
405
|
+
var RemoteAdapter = class {
|
|
406
|
+
baseUrl;
|
|
407
|
+
headers;
|
|
408
|
+
fetchInterval;
|
|
409
|
+
userId;
|
|
410
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
411
|
+
watermark = null;
|
|
412
|
+
lastManifest = null;
|
|
413
|
+
lastFetchTs = 0;
|
|
414
|
+
retryAttempts;
|
|
415
|
+
retryBaseDelayMs;
|
|
416
|
+
circuitBreakerThreshold;
|
|
417
|
+
circuitBreakerCooldownMs;
|
|
418
|
+
sleep;
|
|
419
|
+
now;
|
|
420
|
+
requestTimeoutMs;
|
|
421
|
+
retryOnStatuses;
|
|
422
|
+
dismissBatchWindowMs;
|
|
423
|
+
maxDismissBatchSize;
|
|
424
|
+
disableDismissBatch;
|
|
425
|
+
syncOnOnline;
|
|
426
|
+
syncOnVisibilityChange;
|
|
427
|
+
syncIntervalMs;
|
|
428
|
+
onError;
|
|
429
|
+
consecutiveFailures = 0;
|
|
430
|
+
circuitOpenUntil = 0;
|
|
431
|
+
manifestInFlight = null;
|
|
432
|
+
stateSyncInFlight = null;
|
|
433
|
+
dismissFlushInFlight = null;
|
|
434
|
+
dismissFlushTimer = null;
|
|
435
|
+
syncTimer = null;
|
|
436
|
+
pendingDismissIds = /* @__PURE__ */ new Set();
|
|
437
|
+
supportsDismissBatch = true;
|
|
438
|
+
boundOnlineHandler = null;
|
|
439
|
+
boundVisibilityHandler = null;
|
|
440
|
+
constructor(options) {
|
|
441
|
+
this.baseUrl = options.url.replace(/\/$/, "");
|
|
442
|
+
this.headers = options.headers ?? {};
|
|
443
|
+
this.fetchInterval = options.fetchInterval ?? 5 * 60 * 1e3;
|
|
444
|
+
this.userId = options.userId;
|
|
445
|
+
this.retryAttempts = options.retryAttempts ?? 3;
|
|
446
|
+
this.retryBaseDelayMs = options.retryBaseDelayMs ?? 250;
|
|
447
|
+
this.circuitBreakerThreshold = options.circuitBreakerThreshold ?? 5;
|
|
448
|
+
this.circuitBreakerCooldownMs = options.circuitBreakerCooldownMs ?? 6e4;
|
|
449
|
+
this.sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
450
|
+
this.now = options.now ?? (() => Date.now());
|
|
451
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
452
|
+
this.retryOnStatuses = new Set(options.retryOnStatuses ?? [408, 429, 500, 502, 503, 504]);
|
|
453
|
+
this.dismissBatchWindowMs = options.dismissBatchWindowMs ?? 150;
|
|
454
|
+
this.maxDismissBatchSize = Math.max(1, options.maxDismissBatchSize ?? 100);
|
|
455
|
+
this.disableDismissBatch = options.disableDismissBatch ?? false;
|
|
456
|
+
this.syncOnOnline = options.syncOnOnline ?? false;
|
|
457
|
+
this.syncOnVisibilityChange = options.syncOnVisibilityChange ?? false;
|
|
458
|
+
this.syncIntervalMs = Math.max(0, options.syncIntervalMs ?? 0);
|
|
459
|
+
this.onError = options.onError;
|
|
460
|
+
if (typeof window !== "undefined" && this.syncOnOnline) {
|
|
461
|
+
this.boundOnlineHandler = () => {
|
|
462
|
+
void this.runRecoverySync();
|
|
463
|
+
};
|
|
464
|
+
window.addEventListener("online", this.boundOnlineHandler);
|
|
465
|
+
}
|
|
466
|
+
if (typeof document !== "undefined" && this.syncOnVisibilityChange) {
|
|
467
|
+
this.boundVisibilityHandler = () => {
|
|
468
|
+
if (document.visibilityState === "visible") {
|
|
469
|
+
void this.runRecoverySync();
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
document.addEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
473
|
+
}
|
|
474
|
+
if (this.syncIntervalMs > 0) {
|
|
475
|
+
this.syncTimer = setInterval(() => {
|
|
476
|
+
void this.syncState();
|
|
477
|
+
}, this.syncIntervalMs);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Fetch manifest with stale-while-revalidate */
|
|
481
|
+
async fetchManifest(force = false) {
|
|
482
|
+
const now = this.now();
|
|
483
|
+
if (!force && this.lastManifest && now - this.lastFetchTs < this.fetchInterval) {
|
|
484
|
+
return this.lastManifest;
|
|
485
|
+
}
|
|
486
|
+
if (this.manifestInFlight) return this.manifestInFlight;
|
|
487
|
+
const task = (async () => {
|
|
488
|
+
try {
|
|
489
|
+
const json = await this.withRetry(
|
|
490
|
+
"fetchManifest",
|
|
491
|
+
async () => this.requestJson("", {
|
|
492
|
+
method: "GET",
|
|
493
|
+
headers: this.headers
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
this.lastManifest = json;
|
|
497
|
+
this.lastFetchTs = now;
|
|
498
|
+
return json;
|
|
499
|
+
} catch {
|
|
500
|
+
return this.lastManifest ?? [];
|
|
501
|
+
}
|
|
502
|
+
})();
|
|
503
|
+
this.manifestInFlight = task.finally(() => {
|
|
504
|
+
this.manifestInFlight = null;
|
|
505
|
+
});
|
|
506
|
+
return this.manifestInFlight;
|
|
507
|
+
}
|
|
508
|
+
/** Fetch state (watermark + dismissed IDs) */
|
|
509
|
+
async syncState() {
|
|
510
|
+
if (this.stateSyncInFlight) return this.stateSyncInFlight;
|
|
511
|
+
const task = (async () => {
|
|
512
|
+
try {
|
|
513
|
+
const query = this.userId ? `?userId=${encodeURIComponent(this.userId)}` : "";
|
|
514
|
+
const json = await this.withRetry(
|
|
515
|
+
"syncState",
|
|
516
|
+
async () => this.requestJson(`/state${query}`, {
|
|
517
|
+
method: "GET",
|
|
518
|
+
headers: this.headers
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
if (json.watermark !== void 0) this.watermark = json.watermark;
|
|
522
|
+
if (Array.isArray(json.dismissedIds)) this.dismissedIds = new Set(json.dismissedIds);
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
})();
|
|
526
|
+
this.stateSyncInFlight = task.finally(() => {
|
|
527
|
+
this.stateSyncInFlight = null;
|
|
528
|
+
});
|
|
529
|
+
return this.stateSyncInFlight;
|
|
530
|
+
}
|
|
531
|
+
getWatermark() {
|
|
532
|
+
return this.watermark;
|
|
533
|
+
}
|
|
534
|
+
getDismissedIds() {
|
|
535
|
+
return this.dismissedIds;
|
|
536
|
+
}
|
|
537
|
+
dismiss(id) {
|
|
538
|
+
if (!id) return;
|
|
539
|
+
this.dismissedIds.add(id);
|
|
540
|
+
this.pendingDismissIds.add(id);
|
|
541
|
+
this.scheduleDismissFlush();
|
|
542
|
+
}
|
|
543
|
+
async dismissAll(now) {
|
|
544
|
+
await this.flushPendingDismisses().catch(() => {
|
|
545
|
+
});
|
|
546
|
+
this.watermark = now.toISOString();
|
|
547
|
+
this.dismissedIds.clear();
|
|
548
|
+
await this.flushDismissAll(now).catch(() => {
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/** Cleanup timers/listeners and flush any queued dismiss operations. */
|
|
552
|
+
async destroy() {
|
|
553
|
+
if (this.dismissFlushTimer) {
|
|
554
|
+
clearTimeout(this.dismissFlushTimer);
|
|
555
|
+
this.dismissFlushTimer = null;
|
|
556
|
+
}
|
|
557
|
+
if (this.syncTimer) {
|
|
558
|
+
clearInterval(this.syncTimer);
|
|
559
|
+
this.syncTimer = null;
|
|
560
|
+
}
|
|
561
|
+
if (this.boundOnlineHandler && typeof window !== "undefined") {
|
|
562
|
+
window.removeEventListener("online", this.boundOnlineHandler);
|
|
563
|
+
}
|
|
564
|
+
if (this.boundVisibilityHandler && typeof document !== "undefined") {
|
|
565
|
+
document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
566
|
+
}
|
|
567
|
+
await this.flushPendingDismisses().catch(() => {
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/** Returns current adapter health; false while circuit breaker is open. */
|
|
571
|
+
async isHealthy() {
|
|
572
|
+
if (this.isCircuitOpen()) return false;
|
|
573
|
+
try {
|
|
574
|
+
await this.withRetry(
|
|
575
|
+
"isHealthy",
|
|
576
|
+
async () => this.requestVoid("", {
|
|
577
|
+
method: "GET",
|
|
578
|
+
headers: this.headers
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
return true;
|
|
582
|
+
} catch {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async flushDismiss(id) {
|
|
587
|
+
await this.withRetry(
|
|
588
|
+
"dismiss",
|
|
589
|
+
async () => this.requestVoid("/dismiss", {
|
|
590
|
+
method: "POST",
|
|
591
|
+
jsonBody: { featureId: id }
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
async flushDismissAll(now) {
|
|
596
|
+
await this.withRetry(
|
|
597
|
+
"dismissAll",
|
|
598
|
+
async () => this.requestVoid("/dismiss-all", {
|
|
599
|
+
method: "POST",
|
|
600
|
+
jsonBody: { watermark: now.toISOString() }
|
|
601
|
+
})
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
isCircuitOpen() {
|
|
605
|
+
return this.now() < this.circuitOpenUntil;
|
|
606
|
+
}
|
|
607
|
+
markFailure() {
|
|
608
|
+
this.consecutiveFailures += 1;
|
|
609
|
+
if (this.consecutiveFailures >= this.circuitBreakerThreshold) {
|
|
610
|
+
this.circuitOpenUntil = this.now() + this.circuitBreakerCooldownMs;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
markSuccess() {
|
|
614
|
+
this.consecutiveFailures = 0;
|
|
615
|
+
this.circuitOpenUntil = 0;
|
|
616
|
+
}
|
|
617
|
+
/** Force-flush queued dismiss IDs immediately. */
|
|
618
|
+
async flushPendingDismisses() {
|
|
619
|
+
if (this.dismissFlushTimer) {
|
|
620
|
+
clearTimeout(this.dismissFlushTimer);
|
|
621
|
+
this.dismissFlushTimer = null;
|
|
622
|
+
}
|
|
623
|
+
if (this.dismissFlushInFlight) return this.dismissFlushInFlight;
|
|
624
|
+
const task = this.flushPendingDismissesInternal();
|
|
625
|
+
this.dismissFlushInFlight = task.finally(() => {
|
|
626
|
+
this.dismissFlushInFlight = null;
|
|
627
|
+
});
|
|
628
|
+
return this.dismissFlushInFlight;
|
|
629
|
+
}
|
|
630
|
+
async flushPendingDismissesInternal() {
|
|
631
|
+
if (this.pendingDismissIds.size === 0) return;
|
|
632
|
+
const ids = Array.from(this.pendingDismissIds);
|
|
633
|
+
this.pendingDismissIds.clear();
|
|
634
|
+
try {
|
|
635
|
+
await this.flushDismissBatch(ids);
|
|
636
|
+
} catch {
|
|
637
|
+
for (const id of ids) this.pendingDismissIds.add(id);
|
|
638
|
+
throw new Error("RemoteAdapter dismiss flush failed");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async flushDismissBatch(ids) {
|
|
642
|
+
for (let index = 0; index < ids.length; index += this.maxDismissBatchSize) {
|
|
643
|
+
const chunk = ids.slice(index, index + this.maxDismissBatchSize);
|
|
644
|
+
if (chunk.length === 1 || this.disableDismissBatch || !this.supportsDismissBatch) {
|
|
645
|
+
await this.flushDismiss(chunk[0]);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
await this.withRetry(
|
|
650
|
+
"dismissBatch",
|
|
651
|
+
async () => this.requestVoid("/dismiss-batch", {
|
|
652
|
+
method: "POST",
|
|
653
|
+
jsonBody: { featureIds: chunk }
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
const status = error instanceof RemoteHttpError ? error.status : void 0;
|
|
658
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
659
|
+
this.supportsDismissBatch = false;
|
|
660
|
+
for (const id of chunk) {
|
|
661
|
+
await this.flushDismiss(id);
|
|
662
|
+
}
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
scheduleDismissFlush() {
|
|
670
|
+
if (this.dismissFlushTimer) return;
|
|
671
|
+
this.dismissFlushTimer = setTimeout(() => {
|
|
672
|
+
this.dismissFlushTimer = null;
|
|
673
|
+
void this.flushPendingDismisses().catch(() => {
|
|
674
|
+
this.scheduleDismissFlush();
|
|
675
|
+
});
|
|
676
|
+
}, this.dismissBatchWindowMs);
|
|
677
|
+
}
|
|
678
|
+
async runRecoverySync() {
|
|
679
|
+
await this.flushPendingDismisses().catch(() => {
|
|
680
|
+
});
|
|
681
|
+
await this.syncState();
|
|
682
|
+
}
|
|
683
|
+
async requestJson(path, init) {
|
|
684
|
+
const response = await this.request(path, init);
|
|
685
|
+
return await response.json();
|
|
686
|
+
}
|
|
687
|
+
async requestVoid(path, init) {
|
|
688
|
+
await this.request(path, init);
|
|
689
|
+
}
|
|
690
|
+
async request(path, init) {
|
|
691
|
+
const fetchImpl = assertFetch();
|
|
692
|
+
const controller = new AbortController();
|
|
693
|
+
const timeoutId = this.requestTimeoutMs > 0 ? setTimeout(() => {
|
|
694
|
+
controller.abort();
|
|
695
|
+
}, this.requestTimeoutMs) : null;
|
|
696
|
+
try {
|
|
697
|
+
const res = await fetchImpl(`${this.baseUrl}${path}`, {
|
|
698
|
+
method: init.method,
|
|
699
|
+
headers: {
|
|
700
|
+
...init.jsonBody ? { "Content-Type": "application/json" } : {},
|
|
701
|
+
...this.headers,
|
|
702
|
+
...init.headers ?? {}
|
|
703
|
+
},
|
|
704
|
+
body: init.jsonBody === void 0 ? void 0 : JSON.stringify(init.jsonBody),
|
|
705
|
+
signal: controller.signal
|
|
706
|
+
});
|
|
707
|
+
if (!res.ok) {
|
|
708
|
+
throw new RemoteHttpError(
|
|
709
|
+
`RemoteAdapter request failed (${res.status}) for ${path || "/"}`,
|
|
710
|
+
res.status
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return res;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
const name = error instanceof DOMException ? error.name : error && typeof error === "object" && "name" in error ? String(error.name) : "";
|
|
716
|
+
if (name === "AbortError") {
|
|
717
|
+
throw new Error(`RemoteAdapter request timed out after ${this.requestTimeoutMs}ms`);
|
|
718
|
+
}
|
|
719
|
+
throw error;
|
|
720
|
+
} finally {
|
|
721
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
shouldRetry(error) {
|
|
725
|
+
if (error instanceof RemoteHttpError) {
|
|
726
|
+
return this.retryOnStatuses.has(error.status);
|
|
727
|
+
}
|
|
728
|
+
const message = error instanceof Error ? error.message : "";
|
|
729
|
+
if (message.includes("circuit breaker is open")) return false;
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
async withRetry(operationName, operation) {
|
|
733
|
+
if (this.isCircuitOpen()) {
|
|
734
|
+
throw new Error("RemoteAdapter circuit breaker is open");
|
|
735
|
+
}
|
|
736
|
+
let lastError = new Error("RemoteAdapter request failed");
|
|
737
|
+
for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
|
|
738
|
+
try {
|
|
739
|
+
const result = await operation();
|
|
740
|
+
this.markSuccess();
|
|
741
|
+
return result;
|
|
742
|
+
} catch (error) {
|
|
743
|
+
lastError = error;
|
|
744
|
+
this.onError?.(error, { operation: operationName, attempt });
|
|
745
|
+
if (attempt >= this.retryAttempts || !this.shouldRetry(error)) break;
|
|
746
|
+
const delayMs = this.retryBaseDelayMs * 2 ** attempt;
|
|
747
|
+
await this.sleep(delayMs);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
this.markFailure();
|
|
751
|
+
throw lastError instanceof Error ? lastError : new Error("RemoteAdapter request failed");
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/adapters/postgres.ts
|
|
756
|
+
function normalizeDismissedIds2(row) {
|
|
757
|
+
if (!row) return [];
|
|
758
|
+
const ids = row.dismissed_ids ?? row.dismissedIds;
|
|
759
|
+
if (!Array.isArray(ids)) return [];
|
|
760
|
+
return ids.filter((id) => typeof id === "string");
|
|
761
|
+
}
|
|
762
|
+
function normalizeWatermark(row) {
|
|
763
|
+
if (!row) return null;
|
|
764
|
+
return row.watermark ?? null;
|
|
765
|
+
}
|
|
766
|
+
function normalizeLastSeen(row) {
|
|
767
|
+
if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
768
|
+
return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
769
|
+
}
|
|
770
|
+
var PostgresAdapter = class {
|
|
771
|
+
userId;
|
|
772
|
+
query;
|
|
773
|
+
tableName;
|
|
774
|
+
autoMigrate;
|
|
775
|
+
watermark = null;
|
|
776
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
777
|
+
initialized = false;
|
|
778
|
+
constructor(options) {
|
|
779
|
+
if (!options.userId) {
|
|
780
|
+
throw new Error("PostgresAdapter: userId is required");
|
|
781
|
+
}
|
|
782
|
+
this.userId = options.userId;
|
|
783
|
+
this.query = options.query;
|
|
784
|
+
this.tableName = options.tableName ?? "featuredrop_state";
|
|
785
|
+
this.autoMigrate = options.autoMigrate ?? true;
|
|
786
|
+
}
|
|
787
|
+
getWatermark() {
|
|
788
|
+
return this.watermark;
|
|
789
|
+
}
|
|
790
|
+
getDismissedIds() {
|
|
791
|
+
return this.dismissedIds;
|
|
792
|
+
}
|
|
793
|
+
dismiss(id) {
|
|
794
|
+
this.dismissedIds.add(id);
|
|
795
|
+
void this.dismissBatch([id]);
|
|
796
|
+
}
|
|
797
|
+
async dismissAll(now) {
|
|
798
|
+
this.watermark = now.toISOString();
|
|
799
|
+
this.dismissedIds.clear();
|
|
800
|
+
await this.ensureReady();
|
|
801
|
+
await this.query(
|
|
802
|
+
`INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
|
|
803
|
+
VALUES ($1, $2, '{}', NOW(), NOW(), NOW())
|
|
804
|
+
ON CONFLICT (user_id)
|
|
805
|
+
DO UPDATE SET watermark = EXCLUDED.watermark, dismissed_ids = '{}', last_seen = NOW(), updated_at = NOW()`,
|
|
806
|
+
[this.userId, this.watermark]
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
async sync() {
|
|
810
|
+
await this.ensureReady();
|
|
811
|
+
const result = await this.query(
|
|
812
|
+
`SELECT watermark, dismissed_ids, last_seen
|
|
813
|
+
FROM ${this.tableName}
|
|
814
|
+
WHERE user_id = $1`,
|
|
815
|
+
[this.userId]
|
|
816
|
+
);
|
|
817
|
+
const row = result.rows[0];
|
|
818
|
+
this.watermark = normalizeWatermark(row);
|
|
819
|
+
this.dismissedIds = new Set(normalizeDismissedIds2(row));
|
|
820
|
+
}
|
|
821
|
+
async dismissBatch(ids) {
|
|
822
|
+
if (ids.length === 0) return;
|
|
823
|
+
await this.ensureReady();
|
|
824
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
825
|
+
await this.query(
|
|
826
|
+
`INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
|
|
827
|
+
VALUES ($1, NULL, $2::text[], NOW(), NOW(), NOW())
|
|
828
|
+
ON CONFLICT (user_id)
|
|
829
|
+
DO UPDATE SET
|
|
830
|
+
dismissed_ids = (
|
|
831
|
+
SELECT ARRAY(
|
|
832
|
+
SELECT DISTINCT x FROM UNNEST(
|
|
833
|
+
COALESCE(${this.tableName}.dismissed_ids, '{}') || EXCLUDED.dismissed_ids
|
|
834
|
+
) AS x
|
|
835
|
+
)
|
|
836
|
+
),
|
|
837
|
+
last_seen = NOW(),
|
|
838
|
+
updated_at = NOW()`,
|
|
839
|
+
[this.userId, uniqueIds]
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
async resetUser(userId) {
|
|
843
|
+
await this.ensureReady();
|
|
844
|
+
await this.query(
|
|
845
|
+
`DELETE FROM ${this.tableName} WHERE user_id = $1`,
|
|
846
|
+
[userId]
|
|
847
|
+
);
|
|
848
|
+
if (userId === this.userId) {
|
|
849
|
+
this.watermark = null;
|
|
850
|
+
this.dismissedIds.clear();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async getBulkState(userIds) {
|
|
854
|
+
await this.ensureReady();
|
|
855
|
+
if (userIds.length === 0) return /* @__PURE__ */ new Map();
|
|
856
|
+
const result = await this.query(
|
|
857
|
+
`SELECT user_id, watermark, dismissed_ids, last_seen
|
|
858
|
+
FROM ${this.tableName}
|
|
859
|
+
WHERE user_id = ANY($1::text[])`,
|
|
860
|
+
[userIds]
|
|
861
|
+
);
|
|
862
|
+
const out = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const row of result.rows) {
|
|
864
|
+
out.set(row.user_id, {
|
|
865
|
+
watermark: normalizeWatermark(row),
|
|
866
|
+
dismissedIds: normalizeDismissedIds2(row),
|
|
867
|
+
lastSeen: normalizeLastSeen(row),
|
|
868
|
+
deviceCount: 1
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
return out;
|
|
872
|
+
}
|
|
873
|
+
async isHealthy() {
|
|
874
|
+
try {
|
|
875
|
+
await this.query("SELECT 1");
|
|
876
|
+
return true;
|
|
877
|
+
} catch {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async destroy() {
|
|
882
|
+
}
|
|
883
|
+
async ensureReady() {
|
|
884
|
+
if (this.initialized) return;
|
|
885
|
+
if (this.autoMigrate) {
|
|
886
|
+
await this.query(
|
|
887
|
+
`CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
888
|
+
user_id TEXT PRIMARY KEY,
|
|
889
|
+
watermark TIMESTAMPTZ,
|
|
890
|
+
dismissed_ids TEXT[] DEFAULT '{}',
|
|
891
|
+
last_seen TIMESTAMPTZ DEFAULT NOW(),
|
|
892
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
893
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
894
|
+
)`
|
|
895
|
+
);
|
|
896
|
+
await this.query(
|
|
897
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
this.initialized = true;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// src/adapters/redis.ts
|
|
905
|
+
var RedisAdapter = class {
|
|
906
|
+
userId;
|
|
907
|
+
client;
|
|
908
|
+
keyPrefix;
|
|
909
|
+
watermark = null;
|
|
910
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
911
|
+
constructor(options) {
|
|
912
|
+
if (!options.userId) {
|
|
913
|
+
throw new Error("RedisAdapter: userId is required");
|
|
914
|
+
}
|
|
915
|
+
this.userId = options.userId;
|
|
916
|
+
this.client = options.client;
|
|
917
|
+
this.keyPrefix = options.keyPrefix ?? "fd:";
|
|
918
|
+
}
|
|
919
|
+
getWatermark() {
|
|
920
|
+
return this.watermark;
|
|
921
|
+
}
|
|
922
|
+
getDismissedIds() {
|
|
923
|
+
return this.dismissedIds;
|
|
924
|
+
}
|
|
925
|
+
dismiss(id) {
|
|
926
|
+
this.dismissedIds.add(id);
|
|
927
|
+
void this.client.sadd(this.dismissedKey(this.userId), id);
|
|
928
|
+
void this.client.set(this.lastSeenKey(this.userId), (/* @__PURE__ */ new Date()).toISOString());
|
|
929
|
+
}
|
|
930
|
+
async dismissAll(now) {
|
|
931
|
+
this.watermark = now.toISOString();
|
|
932
|
+
this.dismissedIds.clear();
|
|
933
|
+
await this.client.multi().set(this.watermarkKey(this.userId), this.watermark).del(this.dismissedKey(this.userId)).set(this.lastSeenKey(this.userId), now.toISOString()).exec();
|
|
934
|
+
}
|
|
935
|
+
async sync() {
|
|
936
|
+
const [watermark, dismissedIds] = await Promise.all([
|
|
937
|
+
this.client.get(this.watermarkKey(this.userId)),
|
|
938
|
+
this.client.smembers(this.dismissedKey(this.userId))
|
|
939
|
+
]);
|
|
940
|
+
this.watermark = watermark;
|
|
941
|
+
this.dismissedIds = new Set(dismissedIds);
|
|
942
|
+
}
|
|
943
|
+
async dismissBatch(ids) {
|
|
944
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
945
|
+
if (uniqueIds.length === 0) return;
|
|
946
|
+
this.dismissedIds = /* @__PURE__ */ new Set([...this.dismissedIds, ...uniqueIds]);
|
|
947
|
+
await this.client.sadd(this.dismissedKey(this.userId), ...uniqueIds);
|
|
948
|
+
await this.client.set(this.lastSeenKey(this.userId), (/* @__PURE__ */ new Date()).toISOString());
|
|
949
|
+
}
|
|
950
|
+
async resetUser(userId) {
|
|
951
|
+
await this.client.multi().del(this.watermarkKey(userId)).del(this.dismissedKey(userId)).del(this.lastSeenKey(userId)).exec();
|
|
952
|
+
if (userId === this.userId) {
|
|
953
|
+
this.watermark = null;
|
|
954
|
+
this.dismissedIds.clear();
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async getBulkState(userIds) {
|
|
958
|
+
const map = /* @__PURE__ */ new Map();
|
|
959
|
+
await Promise.all(
|
|
960
|
+
userIds.map(async (userId) => {
|
|
961
|
+
const [watermark, dismissedIds, lastSeen] = await Promise.all([
|
|
962
|
+
this.client.get(this.watermarkKey(userId)),
|
|
963
|
+
this.client.smembers(this.dismissedKey(userId)),
|
|
964
|
+
this.client.get(this.lastSeenKey(userId))
|
|
965
|
+
]);
|
|
966
|
+
map.set(userId, {
|
|
967
|
+
watermark,
|
|
968
|
+
dismissedIds,
|
|
969
|
+
lastSeen: lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
970
|
+
deviceCount: 1
|
|
971
|
+
});
|
|
972
|
+
})
|
|
973
|
+
);
|
|
974
|
+
return map;
|
|
975
|
+
}
|
|
976
|
+
async isHealthy() {
|
|
977
|
+
try {
|
|
978
|
+
const response = await this.client.ping();
|
|
979
|
+
return response.toUpperCase() === "PONG";
|
|
980
|
+
} catch {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async destroy() {
|
|
985
|
+
if (this.client.quit) {
|
|
986
|
+
await this.client.quit();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
this.client.disconnect?.();
|
|
990
|
+
}
|
|
991
|
+
watermarkKey(userId) {
|
|
992
|
+
return `${this.keyPrefix}${userId}:watermark`;
|
|
993
|
+
}
|
|
994
|
+
dismissedKey(userId) {
|
|
995
|
+
return `${this.keyPrefix}${userId}:dismissed`;
|
|
996
|
+
}
|
|
997
|
+
lastSeenKey(userId) {
|
|
998
|
+
return `${this.keyPrefix}${userId}:last_seen`;
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// src/adapters/hybrid.ts
|
|
1003
|
+
var HybridAdapter = class {
|
|
1004
|
+
userId;
|
|
1005
|
+
local;
|
|
1006
|
+
remote;
|
|
1007
|
+
syncBeforeWrite;
|
|
1008
|
+
dismissBatchWindowMs;
|
|
1009
|
+
syncIntervalMs;
|
|
1010
|
+
syncOnVisibilityChange;
|
|
1011
|
+
syncOnOnline;
|
|
1012
|
+
pendingDismissIds = /* @__PURE__ */ new Set();
|
|
1013
|
+
dismissTimer = null;
|
|
1014
|
+
syncTimer = null;
|
|
1015
|
+
flushInFlight = null;
|
|
1016
|
+
syncInFlight = null;
|
|
1017
|
+
boundVisibilityHandler;
|
|
1018
|
+
boundOnlineHandler;
|
|
1019
|
+
constructor(options) {
|
|
1020
|
+
this.local = options.local;
|
|
1021
|
+
this.remote = options.remote;
|
|
1022
|
+
this.userId = options.remote.userId;
|
|
1023
|
+
this.syncBeforeWrite = options.syncBeforeWrite ?? false;
|
|
1024
|
+
this.dismissBatchWindowMs = options.dismissBatchWindowMs ?? 500;
|
|
1025
|
+
this.syncIntervalMs = options.syncIntervalMs ?? 0;
|
|
1026
|
+
this.syncOnVisibilityChange = options.syncOnVisibilityChange ?? true;
|
|
1027
|
+
this.syncOnOnline = options.syncOnOnline ?? true;
|
|
1028
|
+
if (typeof window !== "undefined" && this.syncOnOnline) {
|
|
1029
|
+
this.boundOnlineHandler = () => {
|
|
1030
|
+
void this.sync();
|
|
1031
|
+
};
|
|
1032
|
+
window.addEventListener("online", this.boundOnlineHandler);
|
|
1033
|
+
} else {
|
|
1034
|
+
this.boundOnlineHandler = null;
|
|
1035
|
+
}
|
|
1036
|
+
if (typeof document !== "undefined" && this.syncOnVisibilityChange) {
|
|
1037
|
+
this.boundVisibilityHandler = () => {
|
|
1038
|
+
if (document.visibilityState === "visible") {
|
|
1039
|
+
void this.sync();
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
document.addEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
1043
|
+
} else {
|
|
1044
|
+
this.boundVisibilityHandler = null;
|
|
1045
|
+
}
|
|
1046
|
+
if (this.syncIntervalMs > 0) {
|
|
1047
|
+
this.syncTimer = setInterval(() => {
|
|
1048
|
+
void this.sync();
|
|
1049
|
+
}, this.syncIntervalMs);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
getWatermark() {
|
|
1053
|
+
return this.local.getWatermark() ?? this.remote.getWatermark();
|
|
1054
|
+
}
|
|
1055
|
+
getDismissedIds() {
|
|
1056
|
+
const merged = /* @__PURE__ */ new Set();
|
|
1057
|
+
for (const id of this.local.getDismissedIds()) merged.add(id);
|
|
1058
|
+
for (const id of this.remote.getDismissedIds()) merged.add(id);
|
|
1059
|
+
return merged;
|
|
1060
|
+
}
|
|
1061
|
+
dismiss(id) {
|
|
1062
|
+
this.local.dismiss(id);
|
|
1063
|
+
this.pendingDismissIds.add(id);
|
|
1064
|
+
this.scheduleDismissFlush();
|
|
1065
|
+
}
|
|
1066
|
+
async dismissAll(now) {
|
|
1067
|
+
await this.flushPendingDismisses();
|
|
1068
|
+
this.pendingDismissIds.clear();
|
|
1069
|
+
await Promise.all([
|
|
1070
|
+
this.local.dismissAll(now),
|
|
1071
|
+
this.remote.dismissAll(now)
|
|
1072
|
+
]);
|
|
1073
|
+
}
|
|
1074
|
+
async sync() {
|
|
1075
|
+
if (this.syncInFlight) return this.syncInFlight;
|
|
1076
|
+
const syncTask = (async () => {
|
|
1077
|
+
await this.remote.sync();
|
|
1078
|
+
await this.flushPendingDismissesInternal(true);
|
|
1079
|
+
})();
|
|
1080
|
+
const inFlight = syncTask.finally(() => {
|
|
1081
|
+
if (this.syncInFlight === inFlight) this.syncInFlight = null;
|
|
1082
|
+
});
|
|
1083
|
+
this.syncInFlight = inFlight;
|
|
1084
|
+
return inFlight;
|
|
1085
|
+
}
|
|
1086
|
+
async dismissBatch(ids) {
|
|
1087
|
+
if (this.syncBeforeWrite) {
|
|
1088
|
+
await this.remote.sync();
|
|
1089
|
+
}
|
|
1090
|
+
for (const id of ids) {
|
|
1091
|
+
this.local.dismiss(id);
|
|
1092
|
+
}
|
|
1093
|
+
await this.remote.dismissBatch(ids);
|
|
1094
|
+
}
|
|
1095
|
+
async resetUser(userId) {
|
|
1096
|
+
await this.remote.resetUser(userId);
|
|
1097
|
+
if (userId === this.userId) {
|
|
1098
|
+
await this.local.dismissAll(/* @__PURE__ */ new Date(0));
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async getBulkState(userIds) {
|
|
1102
|
+
return this.remote.getBulkState(userIds);
|
|
1103
|
+
}
|
|
1104
|
+
async isHealthy() {
|
|
1105
|
+
return this.remote.isHealthy();
|
|
1106
|
+
}
|
|
1107
|
+
async destroy() {
|
|
1108
|
+
if (this.dismissTimer) {
|
|
1109
|
+
clearTimeout(this.dismissTimer);
|
|
1110
|
+
this.dismissTimer = null;
|
|
1111
|
+
}
|
|
1112
|
+
if (this.syncTimer) {
|
|
1113
|
+
clearInterval(this.syncTimer);
|
|
1114
|
+
this.syncTimer = null;
|
|
1115
|
+
}
|
|
1116
|
+
if (this.boundOnlineHandler && typeof window !== "undefined") {
|
|
1117
|
+
window.removeEventListener("online", this.boundOnlineHandler);
|
|
1118
|
+
}
|
|
1119
|
+
if (this.boundVisibilityHandler && typeof document !== "undefined") {
|
|
1120
|
+
document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
1121
|
+
}
|
|
1122
|
+
await this.flushPendingDismisses();
|
|
1123
|
+
await this.remote.destroy();
|
|
1124
|
+
}
|
|
1125
|
+
/** Manually flush queued dismiss operations to the remote adapter. */
|
|
1126
|
+
async flushPendingDismisses() {
|
|
1127
|
+
if (this.flushInFlight) return this.flushInFlight;
|
|
1128
|
+
const flushTask = this.flushPendingDismissesInternal(false);
|
|
1129
|
+
const inFlight = flushTask.finally(() => {
|
|
1130
|
+
if (this.flushInFlight === inFlight) this.flushInFlight = null;
|
|
1131
|
+
});
|
|
1132
|
+
this.flushInFlight = inFlight;
|
|
1133
|
+
return inFlight;
|
|
1134
|
+
}
|
|
1135
|
+
async flushPendingDismissesInternal(skipSyncBeforeWrite) {
|
|
1136
|
+
while (this.pendingDismissIds.size > 0) {
|
|
1137
|
+
const ids = Array.from(this.pendingDismissIds);
|
|
1138
|
+
this.pendingDismissIds.clear();
|
|
1139
|
+
try {
|
|
1140
|
+
if (this.syncBeforeWrite && !skipSyncBeforeWrite) {
|
|
1141
|
+
await this.remote.sync();
|
|
1142
|
+
}
|
|
1143
|
+
await this.remote.dismissBatch(ids);
|
|
1144
|
+
} catch {
|
|
1145
|
+
for (const id of ids) this.pendingDismissIds.add(id);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
scheduleDismissFlush() {
|
|
1151
|
+
if (this.dismissTimer) return;
|
|
1152
|
+
this.dismissTimer = setTimeout(() => {
|
|
1153
|
+
this.dismissTimer = null;
|
|
1154
|
+
void this.flushPendingDismisses();
|
|
1155
|
+
}, this.dismissBatchWindowMs);
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// src/adapters/mysql.ts
|
|
1160
|
+
function parseDismissedIds(value) {
|
|
1161
|
+
if (Array.isArray(value)) {
|
|
1162
|
+
return value.filter((item) => typeof item === "string");
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof value === "string" && value.trim()) {
|
|
1165
|
+
try {
|
|
1166
|
+
const parsed = JSON.parse(value);
|
|
1167
|
+
if (Array.isArray(parsed)) {
|
|
1168
|
+
return parsed.filter((item) => typeof item === "string");
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
function normalizeDismissedIds3(row) {
|
|
1177
|
+
if (!row) return [];
|
|
1178
|
+
return parseDismissedIds(row.dismissed_ids ?? row.dismissedIds);
|
|
1179
|
+
}
|
|
1180
|
+
function normalizeWatermark2(row) {
|
|
1181
|
+
if (!row) return null;
|
|
1182
|
+
return row.watermark ?? null;
|
|
1183
|
+
}
|
|
1184
|
+
function normalizeLastSeen2(row) {
|
|
1185
|
+
if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1186
|
+
return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1187
|
+
}
|
|
1188
|
+
var MySQLAdapter = class {
|
|
1189
|
+
userId;
|
|
1190
|
+
query;
|
|
1191
|
+
tableName;
|
|
1192
|
+
autoMigrate;
|
|
1193
|
+
watermark = null;
|
|
1194
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
1195
|
+
initialized = false;
|
|
1196
|
+
constructor(options) {
|
|
1197
|
+
if (!options.userId) {
|
|
1198
|
+
throw new Error("MySQLAdapter: userId is required");
|
|
1199
|
+
}
|
|
1200
|
+
this.userId = options.userId;
|
|
1201
|
+
this.query = options.query;
|
|
1202
|
+
this.tableName = options.tableName ?? "featuredrop_state";
|
|
1203
|
+
this.autoMigrate = options.autoMigrate ?? true;
|
|
1204
|
+
}
|
|
1205
|
+
getWatermark() {
|
|
1206
|
+
return this.watermark;
|
|
1207
|
+
}
|
|
1208
|
+
getDismissedIds() {
|
|
1209
|
+
return this.dismissedIds;
|
|
1210
|
+
}
|
|
1211
|
+
dismiss(id) {
|
|
1212
|
+
this.dismissedIds.add(id);
|
|
1213
|
+
void this.dismissBatch([id]);
|
|
1214
|
+
}
|
|
1215
|
+
async dismissAll(now) {
|
|
1216
|
+
this.watermark = now.toISOString();
|
|
1217
|
+
this.dismissedIds.clear();
|
|
1218
|
+
await this.ensureReady();
|
|
1219
|
+
await this.query(
|
|
1220
|
+
`INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
|
|
1221
|
+
VALUES (?, ?, ?, NOW(3), NOW(3), NOW(3))
|
|
1222
|
+
ON DUPLICATE KEY UPDATE watermark = VALUES(watermark), dismissed_ids = VALUES(dismissed_ids), last_seen = NOW(3), updated_at = NOW(3)`,
|
|
1223
|
+
[this.userId, this.watermark, JSON.stringify([])]
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
async sync() {
|
|
1227
|
+
await this.ensureReady();
|
|
1228
|
+
const result = await this.query(
|
|
1229
|
+
`SELECT watermark, dismissed_ids, last_seen FROM ${this.tableName} WHERE user_id = ? LIMIT 1`,
|
|
1230
|
+
[this.userId]
|
|
1231
|
+
);
|
|
1232
|
+
const row = result.rows[0];
|
|
1233
|
+
this.watermark = normalizeWatermark2(row);
|
|
1234
|
+
this.dismissedIds = new Set(normalizeDismissedIds3(row));
|
|
1235
|
+
}
|
|
1236
|
+
async dismissBatch(ids) {
|
|
1237
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
1238
|
+
if (uniqueIds.length === 0) return;
|
|
1239
|
+
await this.ensureReady();
|
|
1240
|
+
const merged = /* @__PURE__ */ new Set([
|
|
1241
|
+
...Array.from(this.dismissedIds),
|
|
1242
|
+
...uniqueIds
|
|
1243
|
+
]);
|
|
1244
|
+
const mergedArray = Array.from(merged);
|
|
1245
|
+
this.dismissedIds = merged;
|
|
1246
|
+
await this.query(
|
|
1247
|
+
`INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
|
|
1248
|
+
VALUES (?, ?, ?, NOW(3), NOW(3), NOW(3))
|
|
1249
|
+
ON DUPLICATE KEY UPDATE dismissed_ids = VALUES(dismissed_ids), last_seen = NOW(3), updated_at = NOW(3)`,
|
|
1250
|
+
[this.userId, this.watermark, JSON.stringify(mergedArray)]
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
async resetUser(userId) {
|
|
1254
|
+
await this.ensureReady();
|
|
1255
|
+
await this.query(`DELETE FROM ${this.tableName} WHERE user_id = ?`, [userId]);
|
|
1256
|
+
if (userId === this.userId) {
|
|
1257
|
+
this.watermark = null;
|
|
1258
|
+
this.dismissedIds.clear();
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async getBulkState(userIds) {
|
|
1262
|
+
await this.ensureReady();
|
|
1263
|
+
if (userIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1264
|
+
const placeholders = userIds.map(() => "?").join(", ");
|
|
1265
|
+
const result = await this.query(
|
|
1266
|
+
`SELECT user_id, watermark, dismissed_ids, last_seen
|
|
1267
|
+
FROM ${this.tableName}
|
|
1268
|
+
WHERE user_id IN (${placeholders})`,
|
|
1269
|
+
userIds
|
|
1270
|
+
);
|
|
1271
|
+
const out = /* @__PURE__ */ new Map();
|
|
1272
|
+
for (const row of result.rows) {
|
|
1273
|
+
out.set(row.user_id, {
|
|
1274
|
+
watermark: normalizeWatermark2(row),
|
|
1275
|
+
dismissedIds: normalizeDismissedIds3(row),
|
|
1276
|
+
lastSeen: normalizeLastSeen2(row),
|
|
1277
|
+
deviceCount: 1
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
return out;
|
|
1281
|
+
}
|
|
1282
|
+
async isHealthy() {
|
|
1283
|
+
try {
|
|
1284
|
+
await this.query("SELECT 1");
|
|
1285
|
+
return true;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async destroy() {
|
|
1291
|
+
}
|
|
1292
|
+
async ensureReady() {
|
|
1293
|
+
if (this.initialized) return;
|
|
1294
|
+
if (this.autoMigrate) {
|
|
1295
|
+
await this.query(
|
|
1296
|
+
`CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
1297
|
+
user_id VARCHAR(255) PRIMARY KEY,
|
|
1298
|
+
watermark DATETIME(3) NULL,
|
|
1299
|
+
dismissed_ids JSON NOT NULL,
|
|
1300
|
+
last_seen DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
1301
|
+
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
1302
|
+
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
|
1303
|
+
)`
|
|
1304
|
+
);
|
|
1305
|
+
await this.query(
|
|
1306
|
+
`CREATE INDEX idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
this.initialized = true;
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// src/adapters/mongo.ts
|
|
1314
|
+
function normalizeDismissedIds4(ids) {
|
|
1315
|
+
if (!Array.isArray(ids)) return [];
|
|
1316
|
+
return ids.filter((id) => typeof id === "string");
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeLastSeen3(value) {
|
|
1319
|
+
return typeof value === "string" && value ? value : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1320
|
+
}
|
|
1321
|
+
var MongoAdapter = class {
|
|
1322
|
+
userId;
|
|
1323
|
+
collection;
|
|
1324
|
+
watermark = null;
|
|
1325
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
1326
|
+
constructor(options) {
|
|
1327
|
+
if (!options.userId) {
|
|
1328
|
+
throw new Error("MongoAdapter: userId is required");
|
|
1329
|
+
}
|
|
1330
|
+
this.userId = options.userId;
|
|
1331
|
+
this.collection = options.collection;
|
|
1332
|
+
}
|
|
1333
|
+
getWatermark() {
|
|
1334
|
+
return this.watermark;
|
|
1335
|
+
}
|
|
1336
|
+
getDismissedIds() {
|
|
1337
|
+
return this.dismissedIds;
|
|
1338
|
+
}
|
|
1339
|
+
dismiss(id) {
|
|
1340
|
+
this.dismissedIds.add(id);
|
|
1341
|
+
void this.collection.updateOne(
|
|
1342
|
+
{ userId: this.userId },
|
|
1343
|
+
{
|
|
1344
|
+
$addToSet: { dismissedIds: id },
|
|
1345
|
+
$set: { lastSeen: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1346
|
+
},
|
|
1347
|
+
{ upsert: true }
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
async dismissAll(now) {
|
|
1351
|
+
this.watermark = now.toISOString();
|
|
1352
|
+
this.dismissedIds.clear();
|
|
1353
|
+
await this.collection.updateOne(
|
|
1354
|
+
{ userId: this.userId },
|
|
1355
|
+
{
|
|
1356
|
+
$set: {
|
|
1357
|
+
userId: this.userId,
|
|
1358
|
+
watermark: this.watermark,
|
|
1359
|
+
dismissedIds: [],
|
|
1360
|
+
lastSeen: this.watermark
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
{ upsert: true }
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
async sync() {
|
|
1367
|
+
const doc = await this.collection.findOne({ userId: this.userId });
|
|
1368
|
+
this.watermark = doc?.watermark ?? null;
|
|
1369
|
+
this.dismissedIds = new Set(normalizeDismissedIds4(doc?.dismissedIds));
|
|
1370
|
+
}
|
|
1371
|
+
async dismissBatch(ids) {
|
|
1372
|
+
const unique = Array.from(new Set(ids));
|
|
1373
|
+
if (unique.length === 0) return;
|
|
1374
|
+
this.dismissedIds = /* @__PURE__ */ new Set([...this.dismissedIds, ...unique]);
|
|
1375
|
+
await this.collection.updateOne(
|
|
1376
|
+
{ userId: this.userId },
|
|
1377
|
+
{
|
|
1378
|
+
$addToSet: { dismissedIds: { $each: unique } },
|
|
1379
|
+
$set: { lastSeen: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1380
|
+
},
|
|
1381
|
+
{ upsert: true }
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
async resetUser(userId) {
|
|
1385
|
+
await this.collection.deleteOne({ userId });
|
|
1386
|
+
if (userId === this.userId) {
|
|
1387
|
+
this.watermark = null;
|
|
1388
|
+
this.dismissedIds.clear();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async getBulkState(userIds) {
|
|
1392
|
+
const out = /* @__PURE__ */ new Map();
|
|
1393
|
+
if (userIds.length === 0) return out;
|
|
1394
|
+
if (this.collection.find) {
|
|
1395
|
+
const rows = await this.collection.find({ userId: { $in: userIds } }).toArray();
|
|
1396
|
+
for (const row of rows) {
|
|
1397
|
+
out.set(row.userId, {
|
|
1398
|
+
watermark: row.watermark ?? null,
|
|
1399
|
+
dismissedIds: normalizeDismissedIds4(row.dismissedIds),
|
|
1400
|
+
lastSeen: normalizeLastSeen3(row.lastSeen),
|
|
1401
|
+
deviceCount: 1
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
return out;
|
|
1405
|
+
}
|
|
1406
|
+
await Promise.all(
|
|
1407
|
+
userIds.map(async (userId) => {
|
|
1408
|
+
const row = await this.collection.findOne({ userId });
|
|
1409
|
+
if (!row) return;
|
|
1410
|
+
out.set(userId, {
|
|
1411
|
+
watermark: row.watermark ?? null,
|
|
1412
|
+
dismissedIds: normalizeDismissedIds4(row.dismissedIds),
|
|
1413
|
+
lastSeen: normalizeLastSeen3(row.lastSeen),
|
|
1414
|
+
deviceCount: 1
|
|
1415
|
+
});
|
|
1416
|
+
})
|
|
1417
|
+
);
|
|
1418
|
+
return out;
|
|
1419
|
+
}
|
|
1420
|
+
async isHealthy() {
|
|
1421
|
+
try {
|
|
1422
|
+
await this.collection.findOne({});
|
|
1423
|
+
return true;
|
|
1424
|
+
} catch {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async destroy() {
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
// src/adapters/sqlite.ts
|
|
1433
|
+
function parseDismissedIds2(value) {
|
|
1434
|
+
if (Array.isArray(value)) {
|
|
1435
|
+
return value.filter((item) => typeof item === "string");
|
|
1436
|
+
}
|
|
1437
|
+
if (typeof value === "string" && value.trim()) {
|
|
1438
|
+
try {
|
|
1439
|
+
const parsed = JSON.parse(value);
|
|
1440
|
+
if (Array.isArray(parsed)) {
|
|
1441
|
+
return parsed.filter((item) => typeof item === "string");
|
|
1442
|
+
}
|
|
1443
|
+
} catch {
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return [];
|
|
1448
|
+
}
|
|
1449
|
+
function normalizeDismissedIds5(row) {
|
|
1450
|
+
if (!row) return [];
|
|
1451
|
+
return parseDismissedIds2(row.dismissed_ids ?? row.dismissedIds);
|
|
1452
|
+
}
|
|
1453
|
+
function normalizeWatermark3(row) {
|
|
1454
|
+
if (!row) return null;
|
|
1455
|
+
return row.watermark ?? null;
|
|
1456
|
+
}
|
|
1457
|
+
function normalizeLastSeen4(row) {
|
|
1458
|
+
if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1459
|
+
return row.last_seen ?? row.lastSeen ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1460
|
+
}
|
|
1461
|
+
var SQLiteAdapter = class {
|
|
1462
|
+
userId;
|
|
1463
|
+
query;
|
|
1464
|
+
tableName;
|
|
1465
|
+
autoMigrate;
|
|
1466
|
+
watermark = null;
|
|
1467
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
1468
|
+
initialized = false;
|
|
1469
|
+
constructor(options) {
|
|
1470
|
+
if (!options.userId) {
|
|
1471
|
+
throw new Error("SQLiteAdapter: userId is required");
|
|
1472
|
+
}
|
|
1473
|
+
this.userId = options.userId;
|
|
1474
|
+
this.query = options.query;
|
|
1475
|
+
this.tableName = options.tableName ?? "featuredrop_state";
|
|
1476
|
+
this.autoMigrate = options.autoMigrate ?? true;
|
|
1477
|
+
}
|
|
1478
|
+
getWatermark() {
|
|
1479
|
+
return this.watermark;
|
|
1480
|
+
}
|
|
1481
|
+
getDismissedIds() {
|
|
1482
|
+
return this.dismissedIds;
|
|
1483
|
+
}
|
|
1484
|
+
dismiss(id) {
|
|
1485
|
+
this.dismissedIds.add(id);
|
|
1486
|
+
void this.dismissBatch([id]);
|
|
1487
|
+
}
|
|
1488
|
+
async dismissAll(now) {
|
|
1489
|
+
this.watermark = now.toISOString();
|
|
1490
|
+
this.dismissedIds.clear();
|
|
1491
|
+
await this.ensureReady();
|
|
1492
|
+
await this.query(
|
|
1493
|
+
`INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
|
|
1494
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1495
|
+
ON CONFLICT(user_id)
|
|
1496
|
+
DO UPDATE SET watermark = excluded.watermark, dismissed_ids = excluded.dismissed_ids, last_seen = excluded.last_seen, updated_at = excluded.updated_at`,
|
|
1497
|
+
[this.userId, this.watermark, JSON.stringify([]), this.watermark, this.watermark, this.watermark]
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
async sync() {
|
|
1501
|
+
await this.ensureReady();
|
|
1502
|
+
const result = await this.query(
|
|
1503
|
+
`SELECT watermark, dismissed_ids, last_seen FROM ${this.tableName} WHERE user_id = ? LIMIT 1`,
|
|
1504
|
+
[this.userId]
|
|
1505
|
+
);
|
|
1506
|
+
const row = result.rows[0];
|
|
1507
|
+
this.watermark = normalizeWatermark3(row);
|
|
1508
|
+
this.dismissedIds = new Set(normalizeDismissedIds5(row));
|
|
1509
|
+
}
|
|
1510
|
+
async dismissBatch(ids) {
|
|
1511
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
1512
|
+
if (uniqueIds.length === 0) return;
|
|
1513
|
+
await this.ensureReady();
|
|
1514
|
+
const merged = /* @__PURE__ */ new Set([
|
|
1515
|
+
...Array.from(this.dismissedIds),
|
|
1516
|
+
...uniqueIds
|
|
1517
|
+
]);
|
|
1518
|
+
const mergedArray = Array.from(merged);
|
|
1519
|
+
this.dismissedIds = merged;
|
|
1520
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1521
|
+
await this.query(
|
|
1522
|
+
`INSERT INTO ${this.tableName} (user_id, watermark, dismissed_ids, last_seen, created_at, updated_at)
|
|
1523
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1524
|
+
ON CONFLICT(user_id)
|
|
1525
|
+
DO UPDATE SET dismissed_ids = excluded.dismissed_ids, last_seen = excluded.last_seen, updated_at = excluded.updated_at`,
|
|
1526
|
+
[this.userId, this.watermark, JSON.stringify(mergedArray), nowIso, nowIso, nowIso]
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
async resetUser(userId) {
|
|
1530
|
+
await this.ensureReady();
|
|
1531
|
+
await this.query(`DELETE FROM ${this.tableName} WHERE user_id = ?`, [userId]);
|
|
1532
|
+
if (userId === this.userId) {
|
|
1533
|
+
this.watermark = null;
|
|
1534
|
+
this.dismissedIds.clear();
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
async getBulkState(userIds) {
|
|
1538
|
+
await this.ensureReady();
|
|
1539
|
+
if (userIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1540
|
+
const placeholders = userIds.map(() => "?").join(", ");
|
|
1541
|
+
const result = await this.query(
|
|
1542
|
+
`SELECT user_id, watermark, dismissed_ids, last_seen
|
|
1543
|
+
FROM ${this.tableName}
|
|
1544
|
+
WHERE user_id IN (${placeholders})`,
|
|
1545
|
+
userIds
|
|
1546
|
+
);
|
|
1547
|
+
const out = /* @__PURE__ */ new Map();
|
|
1548
|
+
for (const row of result.rows) {
|
|
1549
|
+
out.set(row.user_id, {
|
|
1550
|
+
watermark: normalizeWatermark3(row),
|
|
1551
|
+
dismissedIds: normalizeDismissedIds5(row),
|
|
1552
|
+
lastSeen: normalizeLastSeen4(row),
|
|
1553
|
+
deviceCount: 1
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
return out;
|
|
1557
|
+
}
|
|
1558
|
+
async isHealthy() {
|
|
1559
|
+
try {
|
|
1560
|
+
await this.query("SELECT 1");
|
|
1561
|
+
return true;
|
|
1562
|
+
} catch {
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
async destroy() {
|
|
1567
|
+
}
|
|
1568
|
+
async ensureReady() {
|
|
1569
|
+
if (this.initialized) return;
|
|
1570
|
+
if (this.autoMigrate) {
|
|
1571
|
+
await this.query(
|
|
1572
|
+
`CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
1573
|
+
user_id TEXT PRIMARY KEY,
|
|
1574
|
+
watermark TEXT,
|
|
1575
|
+
dismissed_ids TEXT NOT NULL,
|
|
1576
|
+
last_seen TEXT NOT NULL,
|
|
1577
|
+
created_at TEXT NOT NULL,
|
|
1578
|
+
updated_at TEXT NOT NULL
|
|
1579
|
+
)`
|
|
1580
|
+
);
|
|
1581
|
+
await this.query(
|
|
1582
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_last_seen ON ${this.tableName}(last_seen)`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
this.initialized = true;
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// src/adapters/supabase.ts
|
|
1590
|
+
function normalizeDismissedIds6(row) {
|
|
1591
|
+
if (!row || !Array.isArray(row.dismissed_ids)) return [];
|
|
1592
|
+
return row.dismissed_ids.filter((id) => typeof id === "string");
|
|
1593
|
+
}
|
|
1594
|
+
function normalizeWatermark4(row) {
|
|
1595
|
+
if (!row) return null;
|
|
1596
|
+
return row.watermark ?? null;
|
|
1597
|
+
}
|
|
1598
|
+
function normalizeLastSeen5(row) {
|
|
1599
|
+
if (!row) return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1600
|
+
return row.last_seen ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1601
|
+
}
|
|
1602
|
+
function throwOnError(error) {
|
|
1603
|
+
if (!error) return;
|
|
1604
|
+
throw new Error(`SupabaseAdapter: ${error.message ?? "unknown error"}`);
|
|
1605
|
+
}
|
|
1606
|
+
var SupabaseAdapter = class {
|
|
1607
|
+
userId;
|
|
1608
|
+
client;
|
|
1609
|
+
tableName;
|
|
1610
|
+
realtime;
|
|
1611
|
+
watermark = null;
|
|
1612
|
+
dismissedIds = /* @__PURE__ */ new Set();
|
|
1613
|
+
realtimeChannel = null;
|
|
1614
|
+
syncing = false;
|
|
1615
|
+
constructor(options) {
|
|
1616
|
+
if (!options.userId) {
|
|
1617
|
+
throw new Error("SupabaseAdapter: userId is required");
|
|
1618
|
+
}
|
|
1619
|
+
this.userId = options.userId;
|
|
1620
|
+
this.client = options.client;
|
|
1621
|
+
this.tableName = options.tableName ?? "featuredrop_state";
|
|
1622
|
+
this.realtime = options.realtime ?? false;
|
|
1623
|
+
if (this.realtime && this.client.channel) {
|
|
1624
|
+
this.setupRealtime();
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
getWatermark() {
|
|
1628
|
+
return this.watermark;
|
|
1629
|
+
}
|
|
1630
|
+
getDismissedIds() {
|
|
1631
|
+
return this.dismissedIds;
|
|
1632
|
+
}
|
|
1633
|
+
dismiss(id) {
|
|
1634
|
+
this.dismissedIds.add(id);
|
|
1635
|
+
void this.dismissBatch([id]);
|
|
1636
|
+
}
|
|
1637
|
+
async dismissAll(now) {
|
|
1638
|
+
this.watermark = now.toISOString();
|
|
1639
|
+
this.dismissedIds.clear();
|
|
1640
|
+
await this.upsertState({
|
|
1641
|
+
watermark: this.watermark,
|
|
1642
|
+
dismissed_ids: [],
|
|
1643
|
+
last_seen: this.watermark
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
async sync() {
|
|
1647
|
+
if (this.syncing) return;
|
|
1648
|
+
this.syncing = true;
|
|
1649
|
+
try {
|
|
1650
|
+
const row = await this.fetchState(this.userId);
|
|
1651
|
+
this.watermark = normalizeWatermark4(row);
|
|
1652
|
+
this.dismissedIds = new Set(normalizeDismissedIds6(row));
|
|
1653
|
+
} finally {
|
|
1654
|
+
this.syncing = false;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
async dismissBatch(ids) {
|
|
1658
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
1659
|
+
if (uniqueIds.length === 0) return;
|
|
1660
|
+
const merged = /* @__PURE__ */ new Set([
|
|
1661
|
+
...Array.from(this.dismissedIds),
|
|
1662
|
+
...uniqueIds
|
|
1663
|
+
]);
|
|
1664
|
+
this.dismissedIds = merged;
|
|
1665
|
+
await this.upsertState({
|
|
1666
|
+
watermark: this.watermark,
|
|
1667
|
+
dismissed_ids: Array.from(merged),
|
|
1668
|
+
last_seen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
async resetUser(userId) {
|
|
1672
|
+
const result = await this.client.from(this.tableName).delete().eq("user_id", userId);
|
|
1673
|
+
throwOnError(result.error);
|
|
1674
|
+
if (userId === this.userId) {
|
|
1675
|
+
this.watermark = null;
|
|
1676
|
+
this.dismissedIds.clear();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
async getBulkState(userIds) {
|
|
1680
|
+
const out = /* @__PURE__ */ new Map();
|
|
1681
|
+
if (userIds.length === 0) return out;
|
|
1682
|
+
await Promise.all(
|
|
1683
|
+
userIds.map(async (userId) => {
|
|
1684
|
+
const row = await this.fetchState(userId);
|
|
1685
|
+
if (!row) return;
|
|
1686
|
+
out.set(userId, {
|
|
1687
|
+
watermark: normalizeWatermark4(row),
|
|
1688
|
+
dismissedIds: normalizeDismissedIds6(row),
|
|
1689
|
+
lastSeen: normalizeLastSeen5(row),
|
|
1690
|
+
deviceCount: 1
|
|
1691
|
+
});
|
|
1692
|
+
})
|
|
1693
|
+
);
|
|
1694
|
+
return out;
|
|
1695
|
+
}
|
|
1696
|
+
async isHealthy() {
|
|
1697
|
+
try {
|
|
1698
|
+
await this.client.from(this.tableName).select("user_id").eq("user_id", this.userId).maybeSingle();
|
|
1699
|
+
return true;
|
|
1700
|
+
} catch {
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
async destroy() {
|
|
1705
|
+
if (this.realtimeChannel && this.client.removeChannel) {
|
|
1706
|
+
await this.client.removeChannel(this.realtimeChannel);
|
|
1707
|
+
}
|
|
1708
|
+
this.realtimeChannel = null;
|
|
1709
|
+
}
|
|
1710
|
+
async fetchState(userId) {
|
|
1711
|
+
const result = await this.client.from(this.tableName).select("user_id, watermark, dismissed_ids, last_seen").eq("user_id", userId).maybeSingle();
|
|
1712
|
+
throwOnError(result.error);
|
|
1713
|
+
return result.data;
|
|
1714
|
+
}
|
|
1715
|
+
async upsertState(state) {
|
|
1716
|
+
const payload = {
|
|
1717
|
+
user_id: this.userId,
|
|
1718
|
+
watermark: state.watermark,
|
|
1719
|
+
dismissed_ids: state.dismissed_ids,
|
|
1720
|
+
last_seen: state.last_seen
|
|
1721
|
+
};
|
|
1722
|
+
const result = await this.client.from(this.tableName).upsert(payload);
|
|
1723
|
+
throwOnError(result.error);
|
|
1724
|
+
}
|
|
1725
|
+
setupRealtime() {
|
|
1726
|
+
const channelFactory = this.client.channel;
|
|
1727
|
+
if (!channelFactory) return;
|
|
1728
|
+
this.realtimeChannel = channelFactory(`featuredrop:${this.tableName}:${this.userId}`).on(
|
|
1729
|
+
"postgres_changes",
|
|
1730
|
+
{
|
|
1731
|
+
event: "*",
|
|
1732
|
+
schema: "public",
|
|
1733
|
+
table: this.tableName,
|
|
1734
|
+
filter: `user_id=eq.${this.userId}`
|
|
1735
|
+
},
|
|
1736
|
+
() => {
|
|
1737
|
+
void this.sync();
|
|
1738
|
+
}
|
|
1739
|
+
).subscribe();
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
export { HybridAdapter, IndexedDBAdapter, LocalStorageAdapter, MemoryAdapter, MongoAdapter, MySQLAdapter, PostgresAdapter, RedisAdapter, RemoteAdapter, SQLiteAdapter, SupabaseAdapter };
|
|
1744
|
+
//# sourceMappingURL=adapters.js.map
|
|
1745
|
+
//# sourceMappingURL=adapters.js.map
|