@sailfish-ai/recorder 1.7.35 → 1.7.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eventStore.js +89 -31
- package/dist/inAppReportIssueModal.js +45 -21
- package/dist/index.js +180 -112
- package/dist/notifyEventStore.js +73 -19
- package/dist/sailfish-recorder.cjs.js +1 -1
- package/dist/sailfish-recorder.cjs.js.br +0 -0
- package/dist/sailfish-recorder.cjs.js.gz +0 -0
- package/dist/sailfish-recorder.es.js +1 -1
- package/dist/sailfish-recorder.es.js.br +0 -0
- package/dist/sailfish-recorder.es.js.gz +0 -0
- package/dist/sailfish-recorder.umd.js +1 -1
- package/dist/sailfish-recorder.umd.js.br +0 -0
- package/dist/sailfish-recorder.umd.js.gz +0 -0
- package/dist/segmentHelpers.js +150 -0
- package/dist/sendSailfishMessages.js +10 -0
- package/dist/types/inAppReportIssueModal.d.ts +6 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/segmentHelpers.d.ts +10 -0
- package/dist/types/sendSailfishMessages.d.ts +1 -0
- package/dist/types/utils.d.ts +2 -0
- package/dist/utils.js +7 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const DEBUG = import.meta.env.VITE_DEBUG ? import.meta.env.VITE_DEBUG : false;
|
|
2
|
+
// ── SSR guards ─────────────────────────────────────────
|
|
3
|
+
const HAS_WINDOW = typeof globalThis !== "undefined" &&
|
|
4
|
+
typeof globalThis.window !== "undefined";
|
|
5
|
+
const HAS_DOCUMENT = typeof globalThis !== "undefined" &&
|
|
6
|
+
typeof globalThis.document !== "undefined";
|
|
7
|
+
const HAS_LOCAL_STORAGE = typeof globalThis !== "undefined" && "localStorage" in globalThis;
|
|
8
|
+
const HAS_SESSION_STORAGE = typeof globalThis !== "undefined" && "sessionStorage" in globalThis;
|
|
2
9
|
// import { NetworkRecordOptions } from "@sailfish-rrweb/rrweb-plugin-network-record";
|
|
3
10
|
import { v4 as uuidv4 } from "uuid";
|
|
4
11
|
import { NetworkRequestEventId, STATIC_EXTENSIONS, xSf3RidHeader, } from "./constants";
|
|
@@ -57,16 +64,18 @@ dynamicExcludedHosts.add = (host) => {
|
|
|
57
64
|
}
|
|
58
65
|
// 4. Persist wrapper to localStorage
|
|
59
66
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if (HAS_LOCAL_STORAGE) {
|
|
68
|
+
// TODO decouple into two logical pieces
|
|
69
|
+
const wrapper = {
|
|
70
|
+
version: STORAGE_VERSION,
|
|
71
|
+
entries: {},
|
|
72
|
+
};
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const p of dynamicExcludedHosts) {
|
|
75
|
+
wrapper.entries[p] = now;
|
|
76
|
+
}
|
|
77
|
+
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify(wrapper));
|
|
68
78
|
}
|
|
69
|
-
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify(wrapper));
|
|
70
79
|
}
|
|
71
80
|
catch (e) {
|
|
72
81
|
if (DEBUG)
|
|
@@ -85,35 +94,37 @@ dynamicPassedHosts.add = (host) => {
|
|
|
85
94
|
}
|
|
86
95
|
// 1. Persist wrapper to localStorage (update timestamp unconditionally)
|
|
87
96
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
else {
|
|
95
|
-
const parsed = JSON.parse(raw);
|
|
96
|
-
if (Array.isArray(parsed)) {
|
|
97
|
-
// migrate old array → wrapper
|
|
98
|
-
wrapper = {
|
|
99
|
-
version: STORAGE_VERSION,
|
|
100
|
-
entries: Object.fromEntries(parsed.map((h) => [h, Date.now()])),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
else if (parsed &&
|
|
104
|
-
typeof parsed === "object" &&
|
|
105
|
-
"entries" in parsed &&
|
|
106
|
-
typeof parsed.entries === "object") {
|
|
107
|
-
wrapper = parsed;
|
|
97
|
+
if (HAS_LOCAL_STORAGE) {
|
|
98
|
+
// TODO decouple into two logical pieces
|
|
99
|
+
const raw = localStorage.getItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
100
|
+
let wrapper;
|
|
101
|
+
if (!raw) {
|
|
102
|
+
wrapper = { version: STORAGE_VERSION, entries: {} };
|
|
108
103
|
}
|
|
109
104
|
else {
|
|
110
|
-
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
if (Array.isArray(parsed)) {
|
|
107
|
+
// migrate old array → wrapper
|
|
108
|
+
wrapper = {
|
|
109
|
+
version: STORAGE_VERSION,
|
|
110
|
+
entries: Object.fromEntries(parsed.map((h) => [h, Date.now()])),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
else if (parsed &&
|
|
114
|
+
typeof parsed === "object" &&
|
|
115
|
+
"entries" in parsed &&
|
|
116
|
+
typeof parsed.entries === "object") {
|
|
117
|
+
wrapper = parsed;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
wrapper = { version: STORAGE_VERSION, entries: {} };
|
|
121
|
+
}
|
|
111
122
|
}
|
|
123
|
+
// always set/update the timestamp
|
|
124
|
+
wrapper.entries[cleaned] = Date.now();
|
|
125
|
+
wrapper.version = STORAGE_VERSION;
|
|
126
|
+
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(wrapper));
|
|
112
127
|
}
|
|
113
|
-
// always set/update the timestamp
|
|
114
|
-
wrapper.entries[cleaned] = Date.now();
|
|
115
|
-
wrapper.version = STORAGE_VERSION;
|
|
116
|
-
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify(wrapper));
|
|
117
128
|
}
|
|
118
129
|
catch (e) {
|
|
119
130
|
if (DEBUG)
|
|
@@ -129,6 +140,8 @@ dynamicPassedHosts.add = (host) => {
|
|
|
129
140
|
* Notify the backend of the updated dynamicExcludedHosts
|
|
130
141
|
*/
|
|
131
142
|
function updateExcludedHostsStorageAndBackend(dynamicExcludedHosts) {
|
|
143
|
+
if (!HAS_SESSION_STORAGE)
|
|
144
|
+
return;
|
|
132
145
|
const apiKeyForUpdate = sessionStorage.getItem(SF_API_KEY_FOR_UPDATE) || "";
|
|
133
146
|
const apiForUpdate = sessionStorage.getItem(SF_BACKEND_API) || "";
|
|
134
147
|
if (!apiForUpdate) {
|
|
@@ -190,60 +203,64 @@ export function consolidateDynamicExclusions(hostPathSet, threshold = 3) {
|
|
|
190
203
|
return newSet;
|
|
191
204
|
}
|
|
192
205
|
// 2️⃣ Load & evict old entries (>7 days) + version check for Excluded
|
|
193
|
-
(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const wrapper = JSON.parse(stored);
|
|
199
|
-
// if it's from an old version, drop it
|
|
200
|
-
if (wrapper.version !== STORAGE_VERSION) {
|
|
201
|
-
localStorage.removeItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
206
|
+
if (HAS_LOCAL_STORAGE) {
|
|
207
|
+
(() => {
|
|
208
|
+
const stored = localStorage.getItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
209
|
+
if (!stored)
|
|
202
210
|
return;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
try {
|
|
212
|
+
const wrapper = JSON.parse(stored);
|
|
213
|
+
// if it's from an old version, drop it
|
|
214
|
+
if (wrapper.version !== STORAGE_VERSION) {
|
|
215
|
+
localStorage.removeItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const valid = {};
|
|
220
|
+
for (const [host, ts] of Object.entries(wrapper.entries)) {
|
|
221
|
+
if (now - ts < 7 * 24 * 60 * 60 * 1000) {
|
|
222
|
+
dynamicExcludedHosts.add(host);
|
|
223
|
+
valid[host] = ts;
|
|
224
|
+
}
|
|
210
225
|
}
|
|
226
|
+
localStorage.setItem(DYNAMIC_EXCLUDED_HOSTS_KEY, JSON.stringify({ version: STORAGE_VERSION, entries: valid }));
|
|
211
227
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
})();
|
|
228
|
+
catch (e) {
|
|
229
|
+
if (DEBUG)
|
|
230
|
+
console.warn("Failed to parse dynamicExcludedHosts:", e);
|
|
231
|
+
localStorage.removeItem(DYNAMIC_EXCLUDED_HOSTS_KEY);
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
}
|
|
220
235
|
// 3️⃣ Load & evict old entries (>7 days) + version check for Passed
|
|
221
|
-
(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const wrapper = JSON.parse(stored);
|
|
227
|
-
if (wrapper.version !== STORAGE_VERSION) {
|
|
228
|
-
localStorage.removeItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
236
|
+
if (HAS_LOCAL_STORAGE) {
|
|
237
|
+
(() => {
|
|
238
|
+
const stored = localStorage.getItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
239
|
+
if (!stored)
|
|
229
240
|
return;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
try {
|
|
242
|
+
const wrapper = JSON.parse(stored);
|
|
243
|
+
if (wrapper.version !== STORAGE_VERSION) {
|
|
244
|
+
localStorage.removeItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const valid = {};
|
|
249
|
+
for (const [host, ts] of Object.entries(wrapper.entries)) {
|
|
250
|
+
if (now - ts < 7 * 24 * 60 * 60 * 1000) {
|
|
251
|
+
dynamicPassedHosts.add(host);
|
|
252
|
+
valid[host] = ts;
|
|
253
|
+
}
|
|
237
254
|
}
|
|
255
|
+
localStorage.setItem(DYNAMIC_PASSED_HOSTS_KEY, JSON.stringify({ version: STORAGE_VERSION, entries: valid }));
|
|
238
256
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})();
|
|
257
|
+
catch (e) {
|
|
258
|
+
if (DEBUG)
|
|
259
|
+
console.warn("Failed to parse dynamicPassedHosts:", e);
|
|
260
|
+
localStorage.removeItem(DYNAMIC_PASSED_HOSTS_KEY);
|
|
261
|
+
}
|
|
262
|
+
})();
|
|
263
|
+
}
|
|
247
264
|
const ActionType = {
|
|
248
265
|
PROPAGATE: "propagate",
|
|
249
266
|
IGNORE: "ignore",
|
|
@@ -266,7 +283,7 @@ export const DEFAULT_CONSOLE_RECORDING_SETTINGS = {
|
|
|
266
283
|
stringifyOptions: {
|
|
267
284
|
stringLengthLimit: 1000,
|
|
268
285
|
numOfKeysLimit: 20,
|
|
269
|
-
depthOfLimit:
|
|
286
|
+
depthOfLimit: 4,
|
|
270
287
|
},
|
|
271
288
|
logger: "console",
|
|
272
289
|
};
|
|
@@ -342,15 +359,25 @@ function sendTimeZone() {
|
|
|
342
359
|
sendMessage(message);
|
|
343
360
|
}
|
|
344
361
|
// Send standard information like userDeviceUuid and timeZone
|
|
345
|
-
|
|
346
|
-
|
|
362
|
+
if (HAS_WINDOW) {
|
|
363
|
+
sendUserDeviceUuid();
|
|
364
|
+
sendTimeZone();
|
|
365
|
+
}
|
|
347
366
|
// Function to get or set the device & program UUID in localStorage
|
|
348
367
|
function getOrSetUserDeviceUuid() {
|
|
349
|
-
let userDeviceUuid =
|
|
368
|
+
let userDeviceUuid = null;
|
|
369
|
+
if (HAS_LOCAL_STORAGE) {
|
|
370
|
+
try {
|
|
371
|
+
userDeviceUuid = localStorage.getItem("sailfishUserDeviceUuid");
|
|
372
|
+
}
|
|
373
|
+
catch { }
|
|
374
|
+
}
|
|
350
375
|
if (!userDeviceUuid) {
|
|
351
376
|
userDeviceUuid = uuidv4();
|
|
352
377
|
try {
|
|
353
|
-
|
|
378
|
+
if (HAS_LOCAL_STORAGE) {
|
|
379
|
+
localStorage.setItem("sailfishUserDeviceUuid", userDeviceUuid);
|
|
380
|
+
}
|
|
354
381
|
}
|
|
355
382
|
catch { }
|
|
356
383
|
}
|
|
@@ -359,6 +386,8 @@ function getOrSetUserDeviceUuid() {
|
|
|
359
386
|
// Storing the sailfishSessionId in window.name, as window.name retains its value after a page refresh
|
|
360
387
|
// but resets when a new tab (including a duplicated tab) is opened.
|
|
361
388
|
function getOrSetSessionId() {
|
|
389
|
+
if (!HAS_WINDOW)
|
|
390
|
+
return uuidv4();
|
|
362
391
|
if (!window.name) {
|
|
363
392
|
window.name = uuidv4();
|
|
364
393
|
}
|
|
@@ -371,37 +400,68 @@ function handleVisibilityChange() {
|
|
|
371
400
|
}
|
|
372
401
|
}
|
|
373
402
|
function clearPageVisitUuid() {
|
|
403
|
+
if (!HAS_SESSION_STORAGE)
|
|
404
|
+
return;
|
|
374
405
|
sessionStorage.removeItem("pageVisitUUID");
|
|
375
406
|
sessionStorage.removeItem("prevPageVisitUUID");
|
|
376
407
|
}
|
|
377
408
|
// Initialize event listeners for visibility change and page unload
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
409
|
+
if (HAS_DOCUMENT) {
|
|
410
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
411
|
+
}
|
|
412
|
+
if (HAS_WINDOW) {
|
|
413
|
+
window.addEventListener("beforeunload", () => {
|
|
414
|
+
window.name = "";
|
|
415
|
+
clearPageVisitUuid();
|
|
416
|
+
});
|
|
417
|
+
}
|
|
384
418
|
function storeCredentialsAndConnection({ apiKey, backendApi, }) {
|
|
419
|
+
if (!HAS_SESSION_STORAGE)
|
|
420
|
+
return;
|
|
385
421
|
sessionStorage.setItem("sailfishApiKey", apiKey);
|
|
386
422
|
sessionStorage.setItem("sailfishBackendApi", backendApi);
|
|
387
423
|
}
|
|
388
424
|
// Utility function to match domains or paths with wildcard support
|
|
389
|
-
export function matchUrlWithWildcard(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
425
|
+
export function matchUrlWithWildcard(input, patterns) {
|
|
426
|
+
// Tolerate non-string inputs (Request, URL, etc.); coerce to string when possible.
|
|
427
|
+
let urlStr;
|
|
428
|
+
if (typeof input === "string") {
|
|
429
|
+
urlStr = input;
|
|
430
|
+
}
|
|
431
|
+
else if (typeof URL !== "undefined" && input instanceof URL) {
|
|
432
|
+
urlStr = input.href;
|
|
433
|
+
}
|
|
434
|
+
else if (typeof Request !== "undefined" && input instanceof Request) {
|
|
435
|
+
urlStr = input.url;
|
|
436
|
+
}
|
|
437
|
+
else if (input != null && typeof input.toString === "function") {
|
|
438
|
+
// As per web APIs, fetch/open may accept any object with a stringifier.
|
|
439
|
+
urlStr = input.toString();
|
|
440
|
+
}
|
|
441
|
+
if (!urlStr)
|
|
442
|
+
return false;
|
|
443
|
+
// Use WHATWG URL parsing with a base for relative URLs.
|
|
444
|
+
let parsed;
|
|
445
|
+
try {
|
|
446
|
+
const base = typeof window !== "undefined"
|
|
447
|
+
? window.location.href
|
|
448
|
+
: "http://localhost/";
|
|
449
|
+
parsed = new URL(urlStr, base);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return false; // If we can't parse, just say "no match" instead of throwing.
|
|
453
|
+
}
|
|
454
|
+
const { hostname, pathname, port, protocol } = parsed;
|
|
455
|
+
// Only match http(s) hosts/paths; ignore others like data:, blob:, about:.
|
|
456
|
+
if (!/^https?:$/.test(protocol))
|
|
457
|
+
return false;
|
|
398
458
|
// Handle stripping 'www.' and port
|
|
399
459
|
const domain = hostname.startsWith("www.")
|
|
400
460
|
? hostname.slice(4).toLowerCase()
|
|
401
461
|
: hostname.toLowerCase();
|
|
402
462
|
return patterns.some((pattern) => {
|
|
403
463
|
// Strip any protocol and handle port in the pattern if present
|
|
404
|
-
const strippedPattern = pattern.replace(/^[a-zA-Z]+:\/\//, "");
|
|
464
|
+
const strippedPattern = String(pattern || "").replace(/^[a-zA-Z]+:\/\//, "");
|
|
405
465
|
let [patternDomain, patternPath] = strippedPattern.split("/", 2);
|
|
406
466
|
// Handle port in pattern
|
|
407
467
|
let patternPort = "";
|
|
@@ -417,14 +477,16 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
417
477
|
// Strip 'www.' from both the input domain and the pattern domain for comparison
|
|
418
478
|
const strippedDomain = domain.startsWith("www.") ? domain.slice(4) : domain;
|
|
419
479
|
// If pattern specifies a port, match the exact port
|
|
420
|
-
if (patternPort && port !== patternPort)
|
|
480
|
+
if (patternPort && port !== patternPort)
|
|
421
481
|
return false;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
strippedDomain ===
|
|
427
|
-
|
|
482
|
+
// handle patterns like "*.example.com"
|
|
483
|
+
if (patternDomain.startsWith("*.")) {
|
|
484
|
+
const base = patternDomain.slice(2).toLowerCase();
|
|
485
|
+
const isDomainMatch = domain === base ||
|
|
486
|
+
strippedDomain === base ||
|
|
487
|
+
domain.endsWith("." + base);
|
|
488
|
+
if (!isDomainMatch)
|
|
489
|
+
return false;
|
|
428
490
|
if (patternPath) {
|
|
429
491
|
const normalizedPatternPath = patternPath
|
|
430
492
|
.replace(/\*/g, ".*") // Replace '*' with regex to match any characters
|
|
@@ -435,9 +497,8 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
435
497
|
return true; // Domain matched, no path required
|
|
436
498
|
}
|
|
437
499
|
// Check if the domain matches (include check for base domain without www)
|
|
438
|
-
if (!domainRegex.test(strippedDomain) && !domainRegex.test(domain))
|
|
500
|
+
if (!domainRegex.test(strippedDomain) && !domainRegex.test(domain))
|
|
439
501
|
return false;
|
|
440
|
-
}
|
|
441
502
|
// If there's a path in the pattern, match the path
|
|
442
503
|
if (patternPath) {
|
|
443
504
|
const normalizedPatternPath = patternPath
|
|
@@ -451,7 +512,14 @@ export function matchUrlWithWildcard(url, patterns) {
|
|
|
451
512
|
});
|
|
452
513
|
}
|
|
453
514
|
function shouldSkipHeadersPropagation(url, domainsToNotPropagateHeaderTo = []) {
|
|
454
|
-
|
|
515
|
+
let urlObj;
|
|
516
|
+
try {
|
|
517
|
+
urlObj = new URL(typeof url === "string" ? url : String(url?.url ?? url), window.location.href);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// If we cannot parse, play it safe and do NOT inject headers.
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
455
523
|
// 1️⃣ STATIC ASSET EXCLUSIONS (by comprehensive file extension list)
|
|
456
524
|
for (const ext of STATIC_EXTENSIONS) {
|
|
457
525
|
if (urlObj.pathname.toLowerCase().endsWith(ext)) {
|
package/dist/notifyEventStore.js
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// SSR/Edge-safe IndexedDB helpers for notify messages.
|
|
2
|
+
// No top-level access to `indexedDB`. No dependency on `idb` package.
|
|
3
|
+
// On server/edge (no IDB), calls are no-ops / return safe defaults.
|
|
4
|
+
const DB_NAME = "leapsNotifyDB";
|
|
5
|
+
const STORE_NAME = "notifyMessages";
|
|
6
|
+
const DB_VERSION = 1;
|
|
7
|
+
let _dbPromise = null;
|
|
8
|
+
// Narrow feature check that won’t throw in SSR/Edge
|
|
9
|
+
function hasIndexedDB() {
|
|
10
|
+
return typeof globalThis !== "undefined" && !!globalThis.indexedDB;
|
|
11
|
+
}
|
|
12
|
+
// Lazily open DB only in real browsers. Never at module init.
|
|
13
|
+
function openDb() {
|
|
14
|
+
if (!hasIndexedDB())
|
|
15
|
+
return Promise.resolve(null);
|
|
16
|
+
if (_dbPromise)
|
|
17
|
+
return _dbPromise;
|
|
18
|
+
_dbPromise = new Promise((resolve) => {
|
|
19
|
+
try {
|
|
20
|
+
const req = globalThis.indexedDB.open(DB_NAME, DB_VERSION);
|
|
21
|
+
req.onupgradeneeded = () => {
|
|
22
|
+
const db = req.result;
|
|
23
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
24
|
+
db.createObjectStore(STORE_NAME, {
|
|
25
|
+
keyPath: "id",
|
|
26
|
+
autoIncrement: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
req.onsuccess = () => resolve(req.result);
|
|
31
|
+
req.onerror = () => resolve(null); // fail closed, don’t throw
|
|
32
|
+
req.onblocked = () => resolve(null); // avoid hanging if blocked
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
resolve(null);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return _dbPromise;
|
|
39
|
+
}
|
|
40
|
+
async function withStore(mode, fn) {
|
|
41
|
+
const db = await openDb();
|
|
42
|
+
if (!db)
|
|
43
|
+
return null;
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
try {
|
|
46
|
+
const tx = db.transaction(STORE_NAME, mode);
|
|
47
|
+
const store = tx.objectStore(STORE_NAME);
|
|
48
|
+
Promise.resolve(fn(store))
|
|
49
|
+
.then((result) => {
|
|
50
|
+
tx.oncomplete = () => resolve(result);
|
|
51
|
+
tx.onerror = () => resolve(null);
|
|
52
|
+
})
|
|
53
|
+
.catch(() => resolve(null));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
resolve(null);
|
|
8
57
|
}
|
|
9
|
-
}
|
|
10
|
-
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// ── Public API (safe for SSR/Edge) ─────────────────────────────────────────────
|
|
11
61
|
export async function saveNotifyMessageToIDB(message) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
await tx.done;
|
|
62
|
+
await withStore("readwrite", (store) => {
|
|
63
|
+
store.add({ value: message });
|
|
64
|
+
});
|
|
16
65
|
}
|
|
17
66
|
export async function getAllNotifyMessages() {
|
|
18
|
-
const
|
|
19
|
-
|
|
67
|
+
const result = await withStore("readonly", (store) => {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const req = store.getAll();
|
|
70
|
+
req.onsuccess = () => resolve(req.result);
|
|
71
|
+
req.onerror = () => resolve([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return result ?? []; // SSR/Edge → []
|
|
20
75
|
}
|
|
21
76
|
export async function deleteNotifyMessageById(id) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await tx.done;
|
|
77
|
+
await withStore("readwrite", (store) => {
|
|
78
|
+
store.delete(id);
|
|
79
|
+
});
|
|
26
80
|
}
|