@incodetech/tri-sdk 0.1.1 → 0.1.2-dev-20260528
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/bundle-analysis.html +4950 -0
- package/dist/chunks/index-BAwek9Id.cjs +489 -0
- package/dist/chunks/index-BAwek9Id.cjs.map +1 -0
- package/dist/chunks/index-BBnHO5vA.js +477 -0
- package/dist/chunks/index-BBnHO5vA.js.map +1 -0
- package/dist/chunks/index-BMVAKptw.cjs +432 -0
- package/dist/chunks/index-BMVAKptw.cjs.map +1 -0
- package/dist/chunks/index-B_sPgtA6.cjs +489 -0
- package/dist/chunks/index-B_sPgtA6.cjs.map +1 -0
- package/dist/chunks/index-BlclLEsV.js +420 -0
- package/dist/chunks/index-BlclLEsV.js.map +1 -0
- package/dist/chunks/index-CECrIW-A.js +420 -0
- package/dist/chunks/index-CECrIW-A.js.map +1 -0
- package/dist/chunks/index-DUZ-lBzx.js +477 -0
- package/dist/chunks/index-DUZ-lBzx.js.map +1 -0
- package/dist/chunks/index-SQXEe_Yi.cjs +432 -0
- package/dist/chunks/index-SQXEe_Yi.cjs.map +1 -0
- package/dist/chunks/index-kx-dpbK_.cjs +432 -0
- package/dist/chunks/index-kx-dpbK_.cjs.map +1 -0
- package/dist/chunks/index-uSwsY9-F.js +420 -0
- package/dist/chunks/index-uSwsY9-F.js.map +1 -0
- package/dist/chunks/start-BnvikGDT.cjs +5059 -0
- package/dist/chunks/start-BnvikGDT.cjs.map +1 -0
- package/dist/chunks/start-CMqC8j2A.cjs +5561 -0
- package/dist/chunks/start-CMqC8j2A.cjs.map +1 -0
- package/dist/chunks/start-CkLlbFOM.cjs +5554 -0
- package/dist/chunks/start-CkLlbFOM.cjs.map +1 -0
- package/dist/chunks/start-CwAEBqvW.js +5057 -0
- package/dist/chunks/start-CwAEBqvW.js.map +1 -0
- package/dist/chunks/start-CymdUpX5.js +5060 -0
- package/dist/chunks/start-CymdUpX5.js.map +1 -0
- package/dist/chunks/start-DTFRIfeO.js +5552 -0
- package/dist/chunks/start-DTFRIfeO.js.map +1 -0
- package/dist/chunks/start-Dpgr8IbP.js +5559 -0
- package/dist/chunks/start-Dpgr8IbP.js.map +1 -0
- package/dist/chunks/start-Dtnxf7k4.cjs +5554 -0
- package/dist/chunks/start-Dtnxf7k4.cjs.map +1 -0
- package/dist/chunks/start-DwQtOigF.cjs +5062 -0
- package/dist/chunks/start-DwQtOigF.cjs.map +1 -0
- package/dist/chunks/start-MKWGpbIO.js +5552 -0
- package/dist/chunks/start-MKWGpbIO.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/tri.cjs.js +1 -1
- package/dist/tri.esm.js +1 -1
- package/dist/tri.umd.js +156 -591
- package/dist/tri.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/tri.umd.js
CHANGED
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
*/
|
|
107
107
|
const ENV_TO_BASE_API_URL_LOOKUP = {
|
|
108
108
|
STAGE_K8S: 'https://falcon-ingest-k8s.stage.incodetest.com',
|
|
109
|
-
SAAS: '
|
|
109
|
+
SAAS: '',
|
|
110
110
|
STAGE: '',
|
|
111
111
|
DEMO: '',
|
|
112
112
|
DEMO_K8S: ''
|
|
@@ -267,6 +267,58 @@
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @module sdk/internal/collection-status
|
|
272
|
+
*
|
|
273
|
+
* @description
|
|
274
|
+
* Preflight check — queries the backend to decide whether the SDK should load
|
|
275
|
+
* its full manager bundle for this session.
|
|
276
|
+
*
|
|
277
|
+
* ### Sanctioned `fetch` exception
|
|
278
|
+
* CLAUDE.md prohibits calling `fetch` outside `src/transport/`. This module is
|
|
279
|
+
* the **only** approved exception outside transport, analogous to the History
|
|
280
|
+
* API wrap in the navigation collector. The preflight runs before the transport
|
|
281
|
+
* layer exists (the lazy chunk hasn't loaded yet), so it must call fetch
|
|
282
|
+
* directly. `_fetch` is captured at module-evaluation time to survive host-side
|
|
283
|
+
* patching of `globalThis.fetch` (same hazard model as `lib/http/http.ts`).
|
|
284
|
+
*
|
|
285
|
+
* @internal
|
|
286
|
+
*/
|
|
287
|
+
const _fetch$1 = globalThis.fetch.bind(globalThis);
|
|
288
|
+
const TIMEOUT_MS = 10000;
|
|
289
|
+
/**
|
|
290
|
+
* Queries `GET {apiURL}/api/v1/events/collection-status` and returns whether the backend has
|
|
291
|
+
* collection enabled for this API key.
|
|
292
|
+
*
|
|
293
|
+
* Returns `false` on any network error, non-OK response, or timeout so that a
|
|
294
|
+
* hung or unreachable backend silently disables the SDK rather than blocking the
|
|
295
|
+
* host application.
|
|
296
|
+
*/
|
|
297
|
+
async function getCollectionStatus(apiURL, apiKey) {
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
300
|
+
try {
|
|
301
|
+
const base = apiURL.replace(/\/$/, '');
|
|
302
|
+
const response = await _fetch$1(`${base}/api/v1/events/collection-status`, {
|
|
303
|
+
method: 'GET',
|
|
304
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
305
|
+
signal: controller.signal
|
|
306
|
+
});
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const data = (await response.json());
|
|
311
|
+
return Boolean(data.eventsCollectionEnabled);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
logger.warn('CollectionStatus', 'failed to load collection status', error);
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
270
322
|
/**
|
|
271
323
|
* Singleton entry-point for the TRI SDK.
|
|
272
324
|
*
|
|
@@ -327,7 +379,7 @@
|
|
|
327
379
|
}
|
|
328
380
|
this.startState = 'starting';
|
|
329
381
|
const generation = ++this.startGeneration;
|
|
330
|
-
void this.
|
|
382
|
+
void this._start(config, generation);
|
|
331
383
|
}
|
|
332
384
|
/**
|
|
333
385
|
* @description Stops all collectors, flushes pending events, and tears down
|
|
@@ -359,12 +411,12 @@
|
|
|
359
411
|
* event silently (with a dev warning) if called before `start` completes.
|
|
360
412
|
*/
|
|
361
413
|
reportEvent(name, metadata) {
|
|
362
|
-
if (this.
|
|
414
|
+
if (this._shouldEarlyEscapeReport()) {
|
|
363
415
|
return;
|
|
364
416
|
}
|
|
365
417
|
void safeAsync(() => this.eventReportManager.report(name, metadata));
|
|
366
418
|
}
|
|
367
|
-
async
|
|
419
|
+
async _start(config, generation) {
|
|
368
420
|
if (typeof document === 'undefined') {
|
|
369
421
|
logger.fatal('error', 'Start', 'start() called in a non-browser environment. The SDK will not start.');
|
|
370
422
|
this.startState = 'idle';
|
|
@@ -377,10 +429,15 @@
|
|
|
377
429
|
this.startState = 'idle';
|
|
378
430
|
return;
|
|
379
431
|
}
|
|
432
|
+
const enabled = await getCollectionStatus(preflight.baseApiURL, preflight.apiKey);
|
|
380
433
|
// Bail if stop() or a concurrent start() invalidated this run.
|
|
381
434
|
if (this.startGeneration !== generation) {
|
|
382
435
|
return;
|
|
383
436
|
}
|
|
437
|
+
if (!enabled) {
|
|
438
|
+
this.startState = 'idle';
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
384
441
|
// Flip DEBUG only after the SDK is confirmed to start.
|
|
385
442
|
logger.DEBUG = preflight.debug;
|
|
386
443
|
// Dynamically import the lazy chunk with a hard timeout so a slow/dead
|
|
@@ -433,7 +490,7 @@
|
|
|
433
490
|
* no-op. Guards against calling before the SDK is fully started, or when
|
|
434
491
|
* the `eventReportManager` is unexpectedly absent.
|
|
435
492
|
*/
|
|
436
|
-
|
|
493
|
+
_shouldEarlyEscapeReport() {
|
|
437
494
|
if (this.startState !== 'started') {
|
|
438
495
|
logger.warn('ReportEvent', 'reportEvent() called before start() completed.');
|
|
439
496
|
return true;
|
|
@@ -482,6 +539,58 @@
|
|
|
482
539
|
return navigator.locks.request(lockName, { mode: 'exclusive' }, async () => safeAsync(fn));
|
|
483
540
|
}
|
|
484
541
|
|
|
542
|
+
function createStorage(storageId) {
|
|
543
|
+
function hasStorage() {
|
|
544
|
+
return (globalThis.window &&
|
|
545
|
+
globalThis.window[storageId] &&
|
|
546
|
+
typeof globalThis.window[storageId] !== 'undefined');
|
|
547
|
+
}
|
|
548
|
+
function get(key) {
|
|
549
|
+
if (!hasStorage())
|
|
550
|
+
return null;
|
|
551
|
+
try {
|
|
552
|
+
const raw = window[storageId].getItem(key) ?? 'null';
|
|
553
|
+
if (raw === 'null')
|
|
554
|
+
return null;
|
|
555
|
+
return JSON.parse(raw);
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function set(key, value) {
|
|
562
|
+
if (!hasStorage())
|
|
563
|
+
return;
|
|
564
|
+
safeSync(() => window[storageId].setItem(key, JSON.stringify(value)));
|
|
565
|
+
}
|
|
566
|
+
function remove(key) {
|
|
567
|
+
if (!hasStorage())
|
|
568
|
+
return;
|
|
569
|
+
safeSync(() => window[storageId].removeItem(key));
|
|
570
|
+
}
|
|
571
|
+
function key(index) {
|
|
572
|
+
if (!hasStorage())
|
|
573
|
+
return null;
|
|
574
|
+
return window[storageId].key(index);
|
|
575
|
+
}
|
|
576
|
+
function length() {
|
|
577
|
+
if (!hasStorage())
|
|
578
|
+
return 0;
|
|
579
|
+
return window[storageId].length;
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
get,
|
|
583
|
+
set,
|
|
584
|
+
remove,
|
|
585
|
+
key,
|
|
586
|
+
get length() {
|
|
587
|
+
return length();
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const LocalStorage = createStorage('localStorage');
|
|
592
|
+
const SessionStorage = createStorage('sessionStorage');
|
|
593
|
+
|
|
485
594
|
const KEY_TOKEN = 'activity_token';
|
|
486
595
|
const KEY_ACTIVITY_ID = 'activity_id';
|
|
487
596
|
const KEY_ACTIVITY_STATUS = 'activity_status';
|
|
@@ -508,11 +617,13 @@
|
|
|
508
617
|
}
|
|
509
618
|
/* Doesn't have a lock because it is called from the lifecycle manager on pagehide, if there is a lock, this won't fire */
|
|
510
619
|
async stop() {
|
|
620
|
+
/** clear persisted activity state from storage - don't invalidate in-memory state
|
|
621
|
+
* because token has to survive for api.stopActivity() call, because api auth middleware uses activity.getToken() */
|
|
622
|
+
this._updatePersistedActivityState(null);
|
|
511
623
|
this.startPromise = null;
|
|
512
624
|
if (this.token) {
|
|
513
625
|
await this.context.stopActivity();
|
|
514
626
|
}
|
|
515
|
-
await this._invalidate();
|
|
516
627
|
}
|
|
517
628
|
/* despite facading the this.start() method, this method is still needed
|
|
518
629
|
if we decide to have separated endpoint for the token refresh */
|
|
@@ -526,7 +637,7 @@
|
|
|
526
637
|
this.activityStatus === 'ACTIVE') {
|
|
527
638
|
return;
|
|
528
639
|
}
|
|
529
|
-
const stored =
|
|
640
|
+
const stored = this._load();
|
|
530
641
|
const didOtherTabRefresh = stored &&
|
|
531
642
|
stored.token !== tokenAtRequestTime &&
|
|
532
643
|
!jwt.isExpired(stored.token) &&
|
|
@@ -535,7 +646,7 @@
|
|
|
535
646
|
this._updateActivityState(stored);
|
|
536
647
|
return;
|
|
537
648
|
}
|
|
538
|
-
|
|
649
|
+
this._invalidate();
|
|
539
650
|
await this._resolveStartActivity();
|
|
540
651
|
});
|
|
541
652
|
}
|
|
@@ -558,7 +669,7 @@
|
|
|
558
669
|
*/
|
|
559
670
|
async _resolveStartActivity() {
|
|
560
671
|
/* 2. try to load from persistent storage */
|
|
561
|
-
const stored =
|
|
672
|
+
const stored = this._load();
|
|
562
673
|
const shouldUsePersisted = stored &&
|
|
563
674
|
!jwt.isExpired(stored.token) &&
|
|
564
675
|
stored.activityStatus === 'ACTIVE';
|
|
@@ -569,7 +680,7 @@
|
|
|
569
680
|
if (stored) {
|
|
570
681
|
// Stale — drop it before we fetch fresh, so a partial save below
|
|
571
682
|
// doesn't leave the old token behind.
|
|
572
|
-
|
|
683
|
+
this._invalidate();
|
|
573
684
|
}
|
|
574
685
|
/* 3. if no token or token is expired, start a new activity */
|
|
575
686
|
const result = await this.context.startActivity();
|
|
@@ -582,18 +693,18 @@
|
|
|
582
693
|
return;
|
|
583
694
|
const newActivityState = result.data;
|
|
584
695
|
this._updateActivityState(newActivityState);
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const [token, activityId, activityStatus] =
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
]
|
|
696
|
+
this._save(newActivityState);
|
|
697
|
+
}
|
|
698
|
+
_load() {
|
|
699
|
+
const [token, activityId, activityStatus] = [
|
|
700
|
+
LocalStorage.get(KEY_TOKEN),
|
|
701
|
+
LocalStorage.get(KEY_ACTIVITY_ID),
|
|
702
|
+
LocalStorage.get(KEY_ACTIVITY_STATUS)
|
|
703
|
+
];
|
|
593
704
|
if (!token || !activityId || !activityStatus) {
|
|
594
705
|
// Partial state — clean up the orphan so future loads start fresh.
|
|
595
706
|
if (token || activityId || activityStatus) {
|
|
596
|
-
|
|
707
|
+
this._invalidate();
|
|
597
708
|
}
|
|
598
709
|
return null;
|
|
599
710
|
}
|
|
@@ -603,21 +714,26 @@
|
|
|
603
714
|
activityStatus: activityStatus
|
|
604
715
|
};
|
|
605
716
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
async _invalidate() {
|
|
614
|
-
await Promise.all([
|
|
615
|
-
this.context.db.remove(KEY_TOKEN),
|
|
616
|
-
this.context.db.remove(KEY_ACTIVITY_ID),
|
|
617
|
-
this.context.db.remove(KEY_ACTIVITY_STATUS)
|
|
618
|
-
]);
|
|
717
|
+
_save(data) {
|
|
718
|
+
LocalStorage.set(KEY_TOKEN, data.token);
|
|
719
|
+
LocalStorage.set(KEY_ACTIVITY_ID, data.activityId);
|
|
720
|
+
LocalStorage.set(KEY_ACTIVITY_STATUS, data.activityStatus);
|
|
721
|
+
}
|
|
722
|
+
_invalidate() {
|
|
723
|
+
this._updatePersistedActivityState(null);
|
|
619
724
|
this._updateActivityState(null);
|
|
620
725
|
}
|
|
726
|
+
_updatePersistedActivityState(data) {
|
|
727
|
+
if (!data) {
|
|
728
|
+
LocalStorage.remove(KEY_TOKEN);
|
|
729
|
+
LocalStorage.remove(KEY_ACTIVITY_ID);
|
|
730
|
+
LocalStorage.remove(KEY_ACTIVITY_STATUS);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
LocalStorage.set(KEY_TOKEN, data.token);
|
|
734
|
+
LocalStorage.set(KEY_ACTIVITY_ID, data.activityId);
|
|
735
|
+
LocalStorage.set(KEY_ACTIVITY_STATUS, data.activityStatus);
|
|
736
|
+
}
|
|
621
737
|
_updateActivityState(data) {
|
|
622
738
|
if (!data) {
|
|
623
739
|
this.token = null;
|
|
@@ -645,7 +761,7 @@
|
|
|
645
761
|
*/
|
|
646
762
|
const WIRE = {
|
|
647
763
|
/** Injected at build time from package.json `version` via @rollup/plugin-replace. */
|
|
648
|
-
sdkVersion: "0.1.
|
|
764
|
+
sdkVersion: "0.1.2-dev-20260528",
|
|
649
765
|
/** Injected at build time via @rollup/plugin-replace. Bump when the wire schema changes. */
|
|
650
766
|
schemaVersion: "0.1.0",
|
|
651
767
|
sdkPlatform: 'web',
|
|
@@ -1198,8 +1314,11 @@
|
|
|
1198
1314
|
/**
|
|
1199
1315
|
* Returns whether a failed `HttpResult` is worth retrying.
|
|
1200
1316
|
*
|
|
1201
|
-
* - `
|
|
1202
|
-
*
|
|
1317
|
+
* - `timeout` / `server` / `rate_limited`: transient or back-pressure
|
|
1318
|
+
* failure — the backend may recover; retry with backoff.
|
|
1319
|
+
* - `network`: connection-level failure. Intentionally non-retryable: the
|
|
1320
|
+
* `onConnectionChange()` offline path preserves the queue and re-arms the
|
|
1321
|
+
* drain when connectivity returns, making a separate retry redundant.
|
|
1203
1322
|
* - `abort`: deliberate cancellation; retrying defeats the cancel.
|
|
1204
1323
|
* - `client` / `parse` / `serialize`: our bug; the next attempt will
|
|
1205
1324
|
* produce the same result. Drop the batch.
|
|
@@ -1228,58 +1347,6 @@
|
|
|
1228
1347
|
}
|
|
1229
1348
|
}
|
|
1230
1349
|
|
|
1231
|
-
function createStorage(storageId) {
|
|
1232
|
-
function hasStorage() {
|
|
1233
|
-
return (globalThis.window &&
|
|
1234
|
-
globalThis.window[storageId] &&
|
|
1235
|
-
typeof globalThis.window[storageId] !== 'undefined');
|
|
1236
|
-
}
|
|
1237
|
-
function get(key) {
|
|
1238
|
-
if (!hasStorage())
|
|
1239
|
-
return null;
|
|
1240
|
-
try {
|
|
1241
|
-
const raw = window[storageId].getItem(key) ?? 'null';
|
|
1242
|
-
if (raw === 'null')
|
|
1243
|
-
return null;
|
|
1244
|
-
return JSON.parse(raw);
|
|
1245
|
-
}
|
|
1246
|
-
catch {
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
function set(key, value) {
|
|
1251
|
-
if (!hasStorage())
|
|
1252
|
-
return;
|
|
1253
|
-
safeSync(() => window[storageId].setItem(key, JSON.stringify(value)));
|
|
1254
|
-
}
|
|
1255
|
-
function remove(key) {
|
|
1256
|
-
if (!hasStorage())
|
|
1257
|
-
return;
|
|
1258
|
-
safeSync(() => window[storageId].removeItem(key));
|
|
1259
|
-
}
|
|
1260
|
-
function key(index) {
|
|
1261
|
-
if (!hasStorage())
|
|
1262
|
-
return null;
|
|
1263
|
-
return window[storageId].key(index);
|
|
1264
|
-
}
|
|
1265
|
-
function length() {
|
|
1266
|
-
if (!hasStorage())
|
|
1267
|
-
return 0;
|
|
1268
|
-
return window[storageId].length;
|
|
1269
|
-
}
|
|
1270
|
-
return {
|
|
1271
|
-
get,
|
|
1272
|
-
set,
|
|
1273
|
-
remove,
|
|
1274
|
-
key,
|
|
1275
|
-
get length() {
|
|
1276
|
-
return length();
|
|
1277
|
-
}
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
const LocalStorage = createStorage('localStorage');
|
|
1281
|
-
const SessionStorage = createStorage('sessionStorage');
|
|
1282
|
-
|
|
1283
1350
|
/**
|
|
1284
1351
|
* @module storage-event-channel
|
|
1285
1352
|
*
|
|
@@ -4291,7 +4358,7 @@
|
|
|
4291
4358
|
}
|
|
4292
4359
|
/** @description Stops an activity and returns a result — authenticated endpoint */
|
|
4293
4360
|
async function stopActivity() {
|
|
4294
|
-
return authMiddleware.withAuth((token) => http.post('/
|
|
4361
|
+
return authMiddleware.withAuth((token) => http.post('/end-activity', { token, keepalive: true }), { refreshable: false });
|
|
4295
4362
|
}
|
|
4296
4363
|
/** @description Ingests a batch of events — authenticated endpoint */
|
|
4297
4364
|
async function ingest(payload, keepalive) {
|
|
@@ -4368,507 +4435,6 @@
|
|
|
4368
4435
|
}
|
|
4369
4436
|
}
|
|
4370
4437
|
|
|
4371
|
-
/**
|
|
4372
|
-
* Internal RFC6265-compliant cookie utility.
|
|
4373
|
-
*
|
|
4374
|
-
* Encoding is intentionally bit-for-bit compatible with js-cookie v3.0.7 so
|
|
4375
|
-
* that cookies written by earlier SDK versions (which used js-cookie directly)
|
|
4376
|
-
* continue to decode correctly after upgrading to this implementation.
|
|
4377
|
-
*
|
|
4378
|
-
* The module is internal-only and must not be re-exported from src/index.ts.
|
|
4379
|
-
*
|
|
4380
|
-
* @module
|
|
4381
|
-
*/
|
|
4382
|
-
/**
|
|
4383
|
-
* Encodes a cookie value using the js-cookie v3 RFC6265 scheme:
|
|
4384
|
-
* 1. `encodeURIComponent` percent-encodes everything except `A–Z a–z 0–9 - _ . ! ~ * ' ( )`.
|
|
4385
|
-
* 2. A second pass un-escapes the RFC6265 cookie-octet whitelist characters
|
|
4386
|
-
* that are safe to transmit without encoding:
|
|
4387
|
-
* `# $ & + / : < = > ? @ [ ] ^ \` { | }`.
|
|
4388
|
-
* 3. Semicolons (`;`) remain percent-encoded because they act as the cookie
|
|
4389
|
-
* attribute separator and must not appear unescaped in a value.
|
|
4390
|
-
*/
|
|
4391
|
-
function encodeValue(value) {
|
|
4392
|
-
return encodeURIComponent(value).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/gi, decodeURIComponent);
|
|
4393
|
-
}
|
|
4394
|
-
/**
|
|
4395
|
-
* Encodes a cookie name using the js-cookie v3 RFC6265 scheme:
|
|
4396
|
-
* 1. `encodeURIComponent` percent-encodes the name.
|
|
4397
|
-
* 2. A second pass un-escapes the RFC6265 cookie-name token whitelist:
|
|
4398
|
-
* `# $ & + ^ \` |`.
|
|
4399
|
-
* 3. `(` and `)` are left as `%28` / `%29`; raw parentheses are disallowed in
|
|
4400
|
-
* cookie names per the spec, and js-cookie explicitly re-encodes them via
|
|
4401
|
-
* the deprecated global `escape()` which produces the same result.
|
|
4402
|
-
*/
|
|
4403
|
-
function encodeName(name) {
|
|
4404
|
-
return encodeURIComponent(name)
|
|
4405
|
-
.replace(/%(2[346B]|5E|60|7C)/gi, decodeURIComponent)
|
|
4406
|
-
.replace(/[()]/g, (char) => (char === '(' ? '%28' : '%29'));
|
|
4407
|
-
}
|
|
4408
|
-
/**
|
|
4409
|
-
* Decodes a cookie value using the js-cookie v3 scheme.
|
|
4410
|
-
*
|
|
4411
|
-
* Strips a surrounding pair of double quotes (some Set-Cookie implementations
|
|
4412
|
-
* wrap values in quotes), then replaces runs of percent-encoded bytes with
|
|
4413
|
-
* their decoded form. Mirrors js-cookie's per-sequence try/catch fallback so
|
|
4414
|
-
* that malformed percent-sequences in cookies written by other parties degrade
|
|
4415
|
-
* to the raw string rather than throwing.
|
|
4416
|
-
*/
|
|
4417
|
-
function decodeValue(raw) {
|
|
4418
|
-
let value = raw;
|
|
4419
|
-
if (value[0] === '"') {
|
|
4420
|
-
value = value.slice(1, -1);
|
|
4421
|
-
}
|
|
4422
|
-
return value.replace(/(%[\dA-F]{2})+/gi, (sequence) => {
|
|
4423
|
-
try {
|
|
4424
|
-
return decodeURIComponent(sequence);
|
|
4425
|
-
}
|
|
4426
|
-
catch {
|
|
4427
|
-
return sequence;
|
|
4428
|
-
}
|
|
4429
|
-
});
|
|
4430
|
-
}
|
|
4431
|
-
/**
|
|
4432
|
-
* Serialises `CookieAttributes` into the `; Key=Value` fragment appended to
|
|
4433
|
-
* the `name=value` pair written to `document.cookie`.
|
|
4434
|
-
*/
|
|
4435
|
-
function serializeAttributes(attributes) {
|
|
4436
|
-
let result = '';
|
|
4437
|
-
if (attributes.expires !== undefined) {
|
|
4438
|
-
let expiresDate;
|
|
4439
|
-
if (attributes.expires instanceof Date) {
|
|
4440
|
-
expiresDate = attributes.expires;
|
|
4441
|
-
}
|
|
4442
|
-
else {
|
|
4443
|
-
expiresDate = new Date(Date.now() + attributes.expires * 864e5);
|
|
4444
|
-
}
|
|
4445
|
-
result += '; Expires=' + expiresDate.toUTCString();
|
|
4446
|
-
}
|
|
4447
|
-
if (attributes.path !== undefined) {
|
|
4448
|
-
result += '; Path=' + attributes.path;
|
|
4449
|
-
}
|
|
4450
|
-
if (attributes.domain !== undefined) {
|
|
4451
|
-
result += '; Domain=' + attributes.domain;
|
|
4452
|
-
}
|
|
4453
|
-
if (attributes.sameSite !== undefined) {
|
|
4454
|
-
const capitalised = attributes.sameSite.charAt(0).toUpperCase() +
|
|
4455
|
-
attributes.sameSite.slice(1);
|
|
4456
|
-
result += '; SameSite=' + capitalised;
|
|
4457
|
-
}
|
|
4458
|
-
if (attributes.secure === true) {
|
|
4459
|
-
result += '; Secure';
|
|
4460
|
-
}
|
|
4461
|
-
return result;
|
|
4462
|
-
}
|
|
4463
|
-
/**
|
|
4464
|
-
* Reads a cookie by name from `document.cookie`.
|
|
4465
|
-
*
|
|
4466
|
-
* Decoding mirrors js-cookie v3 exactly:
|
|
4467
|
-
* - Segments are split on `'; '`.
|
|
4468
|
-
* - Each segment is split on the **first** `=`.
|
|
4469
|
-
* - The name is decoded with plain `decodeURIComponent`.
|
|
4470
|
-
* - The value is decoded with the js-cookie percent-sequence approach (see
|
|
4471
|
-
* `decodeValue`).
|
|
4472
|
-
*
|
|
4473
|
-
* Returns `undefined` when the cookie is absent or when `document` is not
|
|
4474
|
-
* available in the current environment.
|
|
4475
|
-
*/
|
|
4476
|
-
function getCookie(name) {
|
|
4477
|
-
if (typeof document === 'undefined')
|
|
4478
|
-
return undefined;
|
|
4479
|
-
const cookies = document.cookie ? document.cookie.split('; ') : [];
|
|
4480
|
-
for (const cookie of cookies) {
|
|
4481
|
-
const separatorIndex = cookie.indexOf('=');
|
|
4482
|
-
if (separatorIndex === -1)
|
|
4483
|
-
continue;
|
|
4484
|
-
const rawName = cookie.slice(0, separatorIndex);
|
|
4485
|
-
let decodedName;
|
|
4486
|
-
try {
|
|
4487
|
-
decodedName = decodeURIComponent(rawName);
|
|
4488
|
-
}
|
|
4489
|
-
catch {
|
|
4490
|
-
decodedName = rawName;
|
|
4491
|
-
}
|
|
4492
|
-
if (decodedName === name) {
|
|
4493
|
-
const rawValue = cookie.slice(separatorIndex + 1);
|
|
4494
|
-
return decodeValue(rawValue);
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
return undefined;
|
|
4498
|
-
}
|
|
4499
|
-
/**
|
|
4500
|
-
* Writes a cookie to `document.cookie`.
|
|
4501
|
-
*
|
|
4502
|
-
* Name and value encoding mirrors js-cookie v3 exactly (see `encodeName` and
|
|
4503
|
-
* `encodeValue`). No-ops silently when `document` is not available.
|
|
4504
|
-
*/
|
|
4505
|
-
function setCookie(name, value, attributes = {}) {
|
|
4506
|
-
if (typeof document === 'undefined')
|
|
4507
|
-
return;
|
|
4508
|
-
document.cookie =
|
|
4509
|
-
encodeName(name) +
|
|
4510
|
-
'=' +
|
|
4511
|
-
encodeValue(value) +
|
|
4512
|
-
serializeAttributes(attributes);
|
|
4513
|
-
}
|
|
4514
|
-
/**
|
|
4515
|
-
* Removes a cookie by expiring it one day in the past.
|
|
4516
|
-
*
|
|
4517
|
-
* Mirrors js-cookie v3's removal approach (`expires: -1`). The `path` and
|
|
4518
|
-
* `domain` attributes must match those used when the cookie was written;
|
|
4519
|
-
* mismatches silently leave the cookie in place.
|
|
4520
|
-
*/
|
|
4521
|
-
function removeCookie(name, attributes = {}) {
|
|
4522
|
-
setCookie(name, '', { ...attributes, expires: -1 });
|
|
4523
|
-
}
|
|
4524
|
-
|
|
4525
|
-
/**
|
|
4526
|
-
* Hard cap on cookie size. Browsers vary slightly (the spec is 4096 bytes
|
|
4527
|
-
* including name, value, and attributes), but ~4000 leaves headroom for
|
|
4528
|
-
* the attribute serialization.
|
|
4529
|
-
*/
|
|
4530
|
-
const COOKIE_BYTE_BUDGET = 4000;
|
|
4531
|
-
/**
|
|
4532
|
-
* Cookie storage adapter backed by `document.cookie`.
|
|
4533
|
-
*
|
|
4534
|
-
* Cookies are durable across tabs and survive localStorage clears, but they're
|
|
4535
|
-
* capped at ~4KB per cookie and ~50 cookies per domain — large values are
|
|
4536
|
-
* rejected rather than silently truncated so the orchestrator can degrade
|
|
4537
|
-
* gracefully.
|
|
4538
|
-
*
|
|
4539
|
-
* Encoding is bit-for-bit compatible with js-cookie v3 (RFC6265) so that
|
|
4540
|
-
* cookies written by earlier SDK versions continue to read correctly.
|
|
4541
|
-
*/
|
|
4542
|
-
class CookieStorage {
|
|
4543
|
-
constructor(options = {}) {
|
|
4544
|
-
this.name = 'cookies';
|
|
4545
|
-
this.attrs = CookieStorage.getCookieAttributes(options);
|
|
4546
|
-
if (options.domain)
|
|
4547
|
-
this.attrs.domain = options.domain;
|
|
4548
|
-
}
|
|
4549
|
-
/**
|
|
4550
|
-
* `navigator.cookieEnabled` returns true even in some embedded contexts
|
|
4551
|
-
* where third-party cookie writes silently fail, so we round-trip a probe.
|
|
4552
|
-
*/
|
|
4553
|
-
async isAvailable() {
|
|
4554
|
-
if (typeof document === 'undefined' ||
|
|
4555
|
-
(typeof navigator !== 'undefined' && navigator.cookieEnabled === false))
|
|
4556
|
-
return false;
|
|
4557
|
-
try {
|
|
4558
|
-
const probeKey = `__immortal_probe_${generateUUID()}__`;
|
|
4559
|
-
safeSync(() => setCookie(probeKey, '1', { path: '/' }));
|
|
4560
|
-
const ok = safeSync(() => getCookie(probeKey)) === '1';
|
|
4561
|
-
safeSync(() => removeCookie(probeKey, { path: '/' }));
|
|
4562
|
-
return ok;
|
|
4563
|
-
}
|
|
4564
|
-
catch {
|
|
4565
|
-
return false;
|
|
4566
|
-
}
|
|
4567
|
-
}
|
|
4568
|
-
async get(key) {
|
|
4569
|
-
const value = safeSync(() => getCookie(key));
|
|
4570
|
-
return value === undefined ? null : value;
|
|
4571
|
-
}
|
|
4572
|
-
async set(key, value) {
|
|
4573
|
-
if (key.length + value.length > COOKIE_BYTE_BUDGET) {
|
|
4574
|
-
throw new Error(`CookieStorage: combined key+value size (${key.length + value.length}) ` +
|
|
4575
|
-
`exceeds budget (${COOKIE_BYTE_BUDGET}) for key "${key}"`);
|
|
4576
|
-
}
|
|
4577
|
-
safeSync(() => setCookie(key, value, this.attrs));
|
|
4578
|
-
}
|
|
4579
|
-
async remove(key) {
|
|
4580
|
-
// Path must match the original write to actually clear the cookie.
|
|
4581
|
-
safeSync(() => removeCookie(key, {
|
|
4582
|
-
path: this.attrs.path,
|
|
4583
|
-
domain: this.attrs.domain,
|
|
4584
|
-
}));
|
|
4585
|
-
}
|
|
4586
|
-
static getCookieAttributes(options) {
|
|
4587
|
-
return {
|
|
4588
|
-
expires: options.expires ?? 365,
|
|
4589
|
-
path: options.path ?? '/',
|
|
4590
|
-
sameSite: options.sameSite ?? 'lax',
|
|
4591
|
-
secure: options.secure ?? false,
|
|
4592
|
-
};
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
/**
|
|
4597
|
-
* Adapter over IndexedDB. Single object store keyed by string, value is
|
|
4598
|
-
* a string. The DB handle is opened lazily on first operation and cached
|
|
4599
|
-
* on the instance — failed opens are not cached so the next call retries.
|
|
4600
|
-
*
|
|
4601
|
-
* Transaction completion (not just request success) is awaited for writes,
|
|
4602
|
-
* since `request.onsuccess` can fire before a transaction has actually
|
|
4603
|
-
* committed in some browsers.
|
|
4604
|
-
*/
|
|
4605
|
-
class IndexedDBStorage {
|
|
4606
|
-
constructor(options = {}) {
|
|
4607
|
-
this.name = 'indexedDB';
|
|
4608
|
-
this.dbPromise = null;
|
|
4609
|
-
this.dbName = options.dbName ?? 'tri:_immortal_db';
|
|
4610
|
-
this.storeName = options.storeName ?? 'tri:kv';
|
|
4611
|
-
this.version = options.version ?? 1;
|
|
4612
|
-
}
|
|
4613
|
-
async isAvailable() {
|
|
4614
|
-
if (typeof indexedDB === 'undefined')
|
|
4615
|
-
return false;
|
|
4616
|
-
try {
|
|
4617
|
-
await this.open();
|
|
4618
|
-
return true;
|
|
4619
|
-
}
|
|
4620
|
-
catch {
|
|
4621
|
-
return false;
|
|
4622
|
-
}
|
|
4623
|
-
}
|
|
4624
|
-
async get(key) {
|
|
4625
|
-
const result = await this.tx('readonly', (s) => s.get(key));
|
|
4626
|
-
if (result === undefined || result === null)
|
|
4627
|
-
return null;
|
|
4628
|
-
return typeof result === 'string' ? result : String(result);
|
|
4629
|
-
}
|
|
4630
|
-
async set(key, value) {
|
|
4631
|
-
await this.tx('readwrite', (s) => s.put(value, key));
|
|
4632
|
-
}
|
|
4633
|
-
async remove(key) {
|
|
4634
|
-
await this.tx('readwrite', (s) => s.delete(key));
|
|
4635
|
-
}
|
|
4636
|
-
open() {
|
|
4637
|
-
if (this.dbPromise)
|
|
4638
|
-
return this.dbPromise;
|
|
4639
|
-
const promise = new Promise((resolve, reject) => {
|
|
4640
|
-
if (typeof indexedDB === 'undefined') {
|
|
4641
|
-
reject(new Error('IndexedDB is not available in this environment'));
|
|
4642
|
-
return;
|
|
4643
|
-
}
|
|
4644
|
-
const req = indexedDB.open(this.dbName, this.version);
|
|
4645
|
-
req.onerror = () => reject(req.error);
|
|
4646
|
-
req.onblocked = () => reject(new Error('IndexedDB open blocked'));
|
|
4647
|
-
req.onsuccess = () => resolve(req.result);
|
|
4648
|
-
req.onupgradeneeded = () => {
|
|
4649
|
-
const db = req.result;
|
|
4650
|
-
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
4651
|
-
db.createObjectStore(this.storeName);
|
|
4652
|
-
}
|
|
4653
|
-
};
|
|
4654
|
-
});
|
|
4655
|
-
// Don't cache a rejected promise — let the next call retry from scratch.
|
|
4656
|
-
promise.catch(() => {
|
|
4657
|
-
if (this.dbPromise === promise)
|
|
4658
|
-
this.dbPromise = null;
|
|
4659
|
-
});
|
|
4660
|
-
this.dbPromise = promise;
|
|
4661
|
-
return promise;
|
|
4662
|
-
}
|
|
4663
|
-
async tx(mode, fn) {
|
|
4664
|
-
return this.open().then((db) => new Promise((resolve, reject) => {
|
|
4665
|
-
const tx = db.transaction(this.storeName, mode);
|
|
4666
|
-
const objectStore = tx.objectStore(this.storeName);
|
|
4667
|
-
const req = fn(objectStore);
|
|
4668
|
-
let result;
|
|
4669
|
-
req.onsuccess = () => {
|
|
4670
|
-
result = req.result;
|
|
4671
|
-
};
|
|
4672
|
-
req.onerror = () => reject(req.error);
|
|
4673
|
-
tx.oncomplete = () => resolve(result);
|
|
4674
|
-
tx.onerror = () => reject(tx.error);
|
|
4675
|
-
tx.onabort = () => reject(tx.error ?? new Error('IndexedDB transaction aborted'));
|
|
4676
|
-
}));
|
|
4677
|
-
}
|
|
4678
|
-
}
|
|
4679
|
-
|
|
4680
|
-
/**
|
|
4681
|
-
* Adapter for the Web Storage API (localStorage / sessionStorage).
|
|
4682
|
-
* The underlying Storage instance is injected via constructor so the same
|
|
4683
|
-
* class can serve both — pass `window.localStorage` or `window.sessionStorage`.
|
|
4684
|
-
*/
|
|
4685
|
-
class WebStorage {
|
|
4686
|
-
constructor(storage, name) {
|
|
4687
|
-
this.storage = storage;
|
|
4688
|
-
this.name = name;
|
|
4689
|
-
}
|
|
4690
|
-
/**
|
|
4691
|
-
* Probe via write/read/delete of a sentinel key. Catches:
|
|
4692
|
-
* - Safari private mode (throws on setItem)
|
|
4693
|
-
* - Storage disabled by user / policy
|
|
4694
|
-
* - Quota-exhausted state
|
|
4695
|
-
* `typeof Storage !== 'undefined'` alone is unreliable.
|
|
4696
|
-
*/
|
|
4697
|
-
async isAvailable() {
|
|
4698
|
-
try {
|
|
4699
|
-
const probeKey = `__immortal_probe_${generateUUID()}__`;
|
|
4700
|
-
safeSync(() => this.storage.setItem(probeKey, '1'));
|
|
4701
|
-
const ok = safeSync(() => this.storage.getItem(probeKey)) === '1';
|
|
4702
|
-
safeSync(() => this.storage.removeItem(probeKey));
|
|
4703
|
-
return ok;
|
|
4704
|
-
}
|
|
4705
|
-
catch {
|
|
4706
|
-
return false;
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
async get(key) {
|
|
4710
|
-
return safeSync(() => this.storage.getItem(key)) ?? null;
|
|
4711
|
-
}
|
|
4712
|
-
async set(key, value) {
|
|
4713
|
-
safeSync(() => this.storage.setItem(key, value));
|
|
4714
|
-
}
|
|
4715
|
-
async remove(key) {
|
|
4716
|
-
safeSync(() => this.storage.removeItem(key));
|
|
4717
|
-
}
|
|
4718
|
-
}
|
|
4719
|
-
|
|
4720
|
-
/**
|
|
4721
|
-
* Redundant key-value store fanning out across multiple storage backends.
|
|
4722
|
-
*
|
|
4723
|
-
* - `set` / `remove` fan out to every available backend in parallel; per-store
|
|
4724
|
-
* failures are logged via `onError` but do not fail the operation.
|
|
4725
|
-
* - `get` reads from all backends in parallel and resolves divergence by
|
|
4726
|
-
* majority vote, with non-null preferred on ties (a present value is
|
|
4727
|
-
* stronger evidence than absence) and earlier-configured stores winning
|
|
4728
|
-
* further ties.
|
|
4729
|
-
* - After every read, any backend whose value differs from the winner is
|
|
4730
|
-
* healed via a corrective write (fire-and-forget — does not block the read).
|
|
4731
|
-
*/
|
|
4732
|
-
let ImmortalDB$1 = class ImmortalDB {
|
|
4733
|
-
constructor(options = {}) {
|
|
4734
|
-
this._stores = [];
|
|
4735
|
-
this._initialized = false;
|
|
4736
|
-
this._initPromise = null;
|
|
4737
|
-
this._configured = options.stores ?? ImmortalDB._defaultStores();
|
|
4738
|
-
this._keyPrefix = options.keyPrefix ?? '_tri_immortal_';
|
|
4739
|
-
this._onError = options.onError ?? (() => { });
|
|
4740
|
-
}
|
|
4741
|
-
/** Names of backends that passed availability probing. Useful for diagnostics. */
|
|
4742
|
-
get activeStores() {
|
|
4743
|
-
return this._stores.map((s) => s.name);
|
|
4744
|
-
}
|
|
4745
|
-
/**
|
|
4746
|
-
* Probe configured backends in parallel and retain only the live ones.
|
|
4747
|
-
* Idempotent: subsequent calls are no-ops; concurrent calls share the
|
|
4748
|
-
* same promise so probes only run once.
|
|
4749
|
-
*/
|
|
4750
|
-
async init() {
|
|
4751
|
-
if (this._initialized)
|
|
4752
|
-
return;
|
|
4753
|
-
if (this._initPromise)
|
|
4754
|
-
return this._initPromise;
|
|
4755
|
-
this._initPromise = (async () => {
|
|
4756
|
-
const probes = await Promise.allSettled(this._configured.map(async (store) => ({
|
|
4757
|
-
store,
|
|
4758
|
-
ok: await store.isAvailable()
|
|
4759
|
-
})));
|
|
4760
|
-
this._stores = probes.reduce((acc, p) => {
|
|
4761
|
-
if (p.status === 'fulfilled' && p.value.ok) {
|
|
4762
|
-
acc.push(p.value.store);
|
|
4763
|
-
}
|
|
4764
|
-
return acc;
|
|
4765
|
-
}, []);
|
|
4766
|
-
if (this._stores.length === 0 && true) {
|
|
4767
|
-
throw new Error('ImmortalDB: no storage backends are available');
|
|
4768
|
-
}
|
|
4769
|
-
this._initialized = true;
|
|
4770
|
-
})();
|
|
4771
|
-
try {
|
|
4772
|
-
await this._initPromise;
|
|
4773
|
-
}
|
|
4774
|
-
finally {
|
|
4775
|
-
this._initPromise = null;
|
|
4776
|
-
}
|
|
4777
|
-
}
|
|
4778
|
-
async get(key) {
|
|
4779
|
-
await this._ensureInitialized();
|
|
4780
|
-
const namespacedKey = this._namespace(key);
|
|
4781
|
-
const reads = await Promise.allSettled(this._stores.map((s) => s.get(namespacedKey)));
|
|
4782
|
-
// Map each store to its observed value, treating rejections as "missing".
|
|
4783
|
-
const observations = reads.map((r, i) => {
|
|
4784
|
-
if (r.status === 'fulfilled')
|
|
4785
|
-
return { store: this._stores[i], value: r.value };
|
|
4786
|
-
this._onError(this._stores[i].name, 'get', r.reason);
|
|
4787
|
-
return { store: this._stores[i], value: null };
|
|
4788
|
-
});
|
|
4789
|
-
const winner = ImmortalDB._pickWinner(observations);
|
|
4790
|
-
// Self-heal divergent backends. Fire-and-forget — don't block the read.
|
|
4791
|
-
for (const { store, value } of observations) {
|
|
4792
|
-
if (value === winner)
|
|
4793
|
-
continue;
|
|
4794
|
-
winner === null
|
|
4795
|
-
? store
|
|
4796
|
-
.remove(namespacedKey)
|
|
4797
|
-
.catch((e) => this._onError(store.name, 'heal-remove', e))
|
|
4798
|
-
: store
|
|
4799
|
-
.set(namespacedKey, winner)
|
|
4800
|
-
.catch((e) => this._onError(store.name, 'heal-set', e));
|
|
4801
|
-
}
|
|
4802
|
-
return winner;
|
|
4803
|
-
}
|
|
4804
|
-
async set(key, value) {
|
|
4805
|
-
await this._ensureInitialized();
|
|
4806
|
-
const namespacedKey = this._namespace(key);
|
|
4807
|
-
const writes = await Promise.allSettled(this._stores.map((s) => s.set(namespacedKey, value)));
|
|
4808
|
-
writes.forEach((w, i) => {
|
|
4809
|
-
if (w.status === 'rejected')
|
|
4810
|
-
this._onError(this._stores[i].name, 'set', w.reason);
|
|
4811
|
-
});
|
|
4812
|
-
}
|
|
4813
|
-
async remove(key) {
|
|
4814
|
-
await this._ensureInitialized();
|
|
4815
|
-
const namespacedKey = this._namespace(key);
|
|
4816
|
-
const removes = await Promise.allSettled(this._stores.map((s) => s.remove(namespacedKey)));
|
|
4817
|
-
removes.forEach((r, i) => {
|
|
4818
|
-
if (r.status === 'rejected')
|
|
4819
|
-
this._onError(this._stores[i].name, 'remove', r.reason);
|
|
4820
|
-
});
|
|
4821
|
-
}
|
|
4822
|
-
_namespace(key) {
|
|
4823
|
-
return `${this._keyPrefix}${key}`;
|
|
4824
|
-
}
|
|
4825
|
-
async _ensureInitialized() {
|
|
4826
|
-
if (!this._initialized)
|
|
4827
|
-
await this.init();
|
|
4828
|
-
}
|
|
4829
|
-
/**
|
|
4830
|
-
* Pick the canonical value from a set of observations.
|
|
4831
|
-
* Rules:
|
|
4832
|
-
* 1. Highest vote count wins.
|
|
4833
|
-
* 2. On tie between null and non-null, prefer non-null
|
|
4834
|
-
* (a present value is stronger evidence than absence).
|
|
4835
|
-
* 3. On further tie, the earliest-configured store wins
|
|
4836
|
-
* (deterministic, predictable from `stores` ordering).
|
|
4837
|
-
*/
|
|
4838
|
-
static _pickWinner(observations) {
|
|
4839
|
-
const counts = new Map();
|
|
4840
|
-
for (const { value } of observations) {
|
|
4841
|
-
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
4842
|
-
}
|
|
4843
|
-
const maxCount = Math.max(...counts.values());
|
|
4844
|
-
const tied = observations.filter((o) => counts.get(o.value) === maxCount);
|
|
4845
|
-
// Prefer the first non-null observation among the tied; fall back to null.
|
|
4846
|
-
const firstNonNull = tied.find((o) => o.value !== null);
|
|
4847
|
-
return firstNonNull ? firstNonNull.value : null;
|
|
4848
|
-
}
|
|
4849
|
-
static _defaultStores() {
|
|
4850
|
-
const stores = [];
|
|
4851
|
-
if (typeof window !== 'undefined') {
|
|
4852
|
-
try {
|
|
4853
|
-
if (window.localStorage)
|
|
4854
|
-
stores.push(new WebStorage(window.localStorage, 'localStorage'));
|
|
4855
|
-
}
|
|
4856
|
-
catch {
|
|
4857
|
-
// Accessing window.localStorage can throw in some sandboxed contexts.
|
|
4858
|
-
}
|
|
4859
|
-
}
|
|
4860
|
-
if (typeof document !== 'undefined') {
|
|
4861
|
-
stores.push(new CookieStorage());
|
|
4862
|
-
}
|
|
4863
|
-
if (typeof indexedDB !== 'undefined') {
|
|
4864
|
-
stores.push(new IndexedDBStorage());
|
|
4865
|
-
}
|
|
4866
|
-
return stores;
|
|
4867
|
-
}
|
|
4868
|
-
};
|
|
4869
|
-
|
|
4870
|
-
const ImmortalDB = new ImmortalDB$1();
|
|
4871
|
-
|
|
4872
4438
|
const INTERACTIVE_TAGS = new Set([
|
|
4873
4439
|
'A',
|
|
4874
4440
|
'BUTTON',
|
|
@@ -5883,8 +5449,7 @@
|
|
|
5883
5449
|
/** Activity Manager */
|
|
5884
5450
|
activityManagerRef = new ActivityManager({
|
|
5885
5451
|
startActivity: () => falconServiceManagerRef.api.startActivity(),
|
|
5886
|
-
stopActivity: () => falconServiceManagerRef.api.stopActivity()
|
|
5887
|
-
db: ImmortalDB
|
|
5452
|
+
stopActivity: () => falconServiceManagerRef.api.stopActivity()
|
|
5888
5453
|
});
|
|
5889
5454
|
await activityManagerRef.start();
|
|
5890
5455
|
/** Transport Manager */
|