@cross-deck/web 0.4.0 → 0.6.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/CHANGELOG.md +35 -0
- package/README.md +27 -5
- package/dist/index.cjs +207 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.mjs +207 -24
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +207 -24
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +207 -24
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -337,9 +337,16 @@ declare class CrossdeckClient {
|
|
|
337
337
|
/**
|
|
338
338
|
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
339
339
|
*
|
|
340
|
+
* Pass `{ keepalive: true }` from terminal handlers (pagehide /
|
|
341
|
+
* visibilitychange→hidden / beforeunload). The browser keeps the
|
|
342
|
+
* request alive after the page tears down, so the final batch
|
|
343
|
+
* actually lands instead of being cancelled with the unload.
|
|
344
|
+
*
|
|
340
345
|
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
341
346
|
*/
|
|
342
|
-
flush(
|
|
347
|
+
flush(options?: {
|
|
348
|
+
keepalive?: boolean;
|
|
349
|
+
}): Promise<void>;
|
|
343
350
|
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
344
351
|
flushEvents(): Promise<void>;
|
|
345
352
|
/**
|
|
@@ -441,12 +448,40 @@ declare class CrossdeckError extends Error {
|
|
|
441
448
|
/**
|
|
442
449
|
* Storage adapters for SDK-persisted state.
|
|
443
450
|
*
|
|
444
|
-
*
|
|
451
|
+
* Three flavours:
|
|
445
452
|
* - browser localStorage (default in browsers)
|
|
453
|
+
* - 1st-party document.cookie (redundancy for cleared localStorage)
|
|
446
454
|
* - in-memory (default in Node, or as an explicit fallback)
|
|
447
455
|
*
|
|
448
456
|
* Detection is at construction time, not at every call — picking the
|
|
449
457
|
* adapter once means we don't hit `typeof window` checks on hot paths.
|
|
458
|
+
*
|
|
459
|
+
* ----- Bank-grade identity continuity -----
|
|
460
|
+
*
|
|
461
|
+
* Plain localStorage is not enough. ITP, private browsing, "clear site
|
|
462
|
+
* data" actions, and aggressive privacy extensions all wipe it. When
|
|
463
|
+
* that happens, the SDK mints a fresh anonymousId on next page load
|
|
464
|
+
* and the customer's analytics see one human as multiple "new
|
|
465
|
+
* visitors" — a credibility hit on every dashboard chart that depends
|
|
466
|
+
* on visitor uniqueness (new vs returning, retention, funnels).
|
|
467
|
+
*
|
|
468
|
+
* The fix is redundancy: we write the same identity to BOTH
|
|
469
|
+
* localStorage AND a 1st-party cookie. On boot we read both; whichever
|
|
470
|
+
* survived wins. On set, we write to both stores so a future clear of
|
|
471
|
+
* either doesn't lose the user.
|
|
472
|
+
*
|
|
473
|
+
* Caveats (documented honestly):
|
|
474
|
+
* 1. Safari ITP caps client-set 1st-party cookies at 7 days. Cookie
|
|
475
|
+
* redundancy protects against localStorage clears WITHIN that
|
|
476
|
+
* 7-day window, not beyond it. The full ITP-bypass story (server-
|
|
477
|
+
* set cookies via a customer-CNAMEd subdomain) is a Phase 2
|
|
478
|
+
* follow-up that requires customer DNS configuration.
|
|
479
|
+
* 2. We never write fingerprintable data — only the same anonymousId
|
|
480
|
+
* already in localStorage. Privacy posture is unchanged from
|
|
481
|
+
* single-store identity.
|
|
482
|
+
* 3. `persistIdentity: false` disables BOTH stores so customers
|
|
483
|
+
* running strict consent flows can defer cookie writes until the
|
|
484
|
+
* user opts in.
|
|
450
485
|
*/
|
|
451
486
|
|
|
452
487
|
/**
|
|
@@ -469,7 +504,7 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
469
504
|
* fetch shim, no transitive deps.
|
|
470
505
|
*/
|
|
471
506
|
declare const SDK_NAME = "@cross-deck/web";
|
|
472
|
-
declare const SDK_VERSION = "0.
|
|
507
|
+
declare const SDK_VERSION = "0.6.0";
|
|
473
508
|
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
474
509
|
|
|
475
510
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -46,7 +46,7 @@ function typeMapForStatus(status) {
|
|
|
46
46
|
|
|
47
47
|
// src/http.ts
|
|
48
48
|
var SDK_NAME = "@cross-deck/web";
|
|
49
|
-
var SDK_VERSION = "0.
|
|
49
|
+
var SDK_VERSION = "0.6.0";
|
|
50
50
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
51
51
|
var HttpClient = class {
|
|
52
52
|
constructor(config) {
|
|
@@ -78,7 +78,8 @@ var HttpClient = class {
|
|
|
78
78
|
response = await fetch(url, {
|
|
79
79
|
method,
|
|
80
80
|
headers,
|
|
81
|
-
body: bodyInit
|
|
81
|
+
body: bodyInit,
|
|
82
|
+
keepalive: options.keepalive === true
|
|
82
83
|
});
|
|
83
84
|
} catch (err) {
|
|
84
85
|
throw new CrossdeckError({
|
|
@@ -123,19 +124,25 @@ var HttpClient = class {
|
|
|
123
124
|
var KEY_ANON = "anon_id";
|
|
124
125
|
var KEY_CDCUST = "cdcust_id";
|
|
125
126
|
var IdentityStore = class {
|
|
126
|
-
constructor(
|
|
127
|
-
this.
|
|
127
|
+
constructor(primary, prefix, secondary) {
|
|
128
|
+
this.primary = primary;
|
|
128
129
|
this.prefix = prefix;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
this.secondary = secondary ?? null;
|
|
131
|
+
const anonFromPrimary = primary.getItem(prefix + KEY_ANON);
|
|
132
|
+
const cdcustFromPrimary = primary.getItem(prefix + KEY_CDCUST);
|
|
133
|
+
const anonFromSecondary = this.secondary?.getItem(prefix + KEY_ANON) ?? null;
|
|
134
|
+
const cdcustFromSecondary = this.secondary?.getItem(prefix + KEY_CDCUST) ?? null;
|
|
135
|
+
const anon = anonFromPrimary ?? anonFromSecondary;
|
|
136
|
+
const cdcust = cdcustFromPrimary ?? cdcustFromSecondary;
|
|
133
137
|
this.state = {
|
|
134
|
-
anonymousId:
|
|
135
|
-
crossdeckCustomerId:
|
|
138
|
+
anonymousId: anon ?? this.mintAnonymousId(),
|
|
139
|
+
crossdeckCustomerId: cdcust
|
|
136
140
|
};
|
|
137
|
-
if (!
|
|
138
|
-
|
|
141
|
+
if (!anonFromPrimary || !anonFromSecondary) {
|
|
142
|
+
this.writeBoth(prefix + KEY_ANON, this.state.anonymousId);
|
|
143
|
+
}
|
|
144
|
+
if (cdcust && (!cdcustFromPrimary || !cdcustFromSecondary)) {
|
|
145
|
+
this.writeBoth(prefix + KEY_CDCUST, cdcust);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
/** Return the persisted anonymous device ID (always set). */
|
|
@@ -149,7 +156,7 @@ var IdentityStore = class {
|
|
|
149
156
|
/** Persist a newly-resolved Crossdeck customer ID. */
|
|
150
157
|
setCrossdeckCustomerId(value) {
|
|
151
158
|
this.state.crossdeckCustomerId = value;
|
|
152
|
-
this.
|
|
159
|
+
this.writeBoth(this.prefix + KEY_CDCUST, value);
|
|
153
160
|
}
|
|
154
161
|
/**
|
|
155
162
|
* Wipe persisted identity. Called by reset() — used when an end-user
|
|
@@ -157,13 +164,13 @@ var IdentityStore = class {
|
|
|
157
164
|
* pre-login session is a fresh customer in the identity graph.
|
|
158
165
|
*/
|
|
159
166
|
reset() {
|
|
160
|
-
this.
|
|
161
|
-
this.
|
|
167
|
+
this.deleteBoth(this.prefix + KEY_ANON);
|
|
168
|
+
this.deleteBoth(this.prefix + KEY_CDCUST);
|
|
162
169
|
this.state = {
|
|
163
170
|
anonymousId: this.mintAnonymousId(),
|
|
164
171
|
crossdeckCustomerId: null
|
|
165
172
|
};
|
|
166
|
-
this.
|
|
173
|
+
this.writeBoth(this.prefix + KEY_ANON, this.state.anonymousId);
|
|
167
174
|
}
|
|
168
175
|
/**
|
|
169
176
|
* Generate an anonymousId. Crockford-ish base32 timestamp + random
|
|
@@ -175,6 +182,30 @@ var IdentityStore = class {
|
|
|
175
182
|
const rand = randomChars(10);
|
|
176
183
|
return `anon_${ts}${rand}`;
|
|
177
184
|
}
|
|
185
|
+
writeBoth(key, value) {
|
|
186
|
+
try {
|
|
187
|
+
this.primary.setItem(key, value);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
if (this.secondary) {
|
|
191
|
+
try {
|
|
192
|
+
this.secondary.setItem(key, value);
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
deleteBoth(key) {
|
|
198
|
+
try {
|
|
199
|
+
this.primary.removeItem(key);
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (this.secondary) {
|
|
203
|
+
try {
|
|
204
|
+
this.secondary.removeItem(key);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
178
209
|
};
|
|
179
210
|
function randomChars(count) {
|
|
180
211
|
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
@@ -303,8 +334,12 @@ var EventQueue = class {
|
|
|
303
334
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
304
335
|
* completes (success or failure). On failure, events stay in the
|
|
305
336
|
* buffer for the next flush attempt.
|
|
337
|
+
*
|
|
338
|
+
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
339
|
+
* browser keeps the request alive past page unload. Use this for
|
|
340
|
+
* terminal flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
306
341
|
*/
|
|
307
|
-
async flush() {
|
|
342
|
+
async flush(options = {}) {
|
|
308
343
|
if (this.buffer.length === 0) return null;
|
|
309
344
|
this.cancelTimerIfSet();
|
|
310
345
|
const batch = this.buffer.splice(0);
|
|
@@ -320,7 +355,8 @@ var EventQueue = class {
|
|
|
320
355
|
environment: env.environment,
|
|
321
356
|
sdk: env.sdk,
|
|
322
357
|
events: batch
|
|
323
|
-
}
|
|
358
|
+
},
|
|
359
|
+
keepalive: options.keepalive === true
|
|
324
360
|
});
|
|
325
361
|
this.lastFlushAt = Date.now();
|
|
326
362
|
this.lastError = null;
|
|
@@ -395,6 +431,59 @@ var MemoryStorage = class {
|
|
|
395
431
|
this.store.delete(key);
|
|
396
432
|
}
|
|
397
433
|
};
|
|
434
|
+
var CookieStorage = class {
|
|
435
|
+
constructor(options) {
|
|
436
|
+
this.maxAgeSec = options?.maxAgeSec ?? 63072e3;
|
|
437
|
+
this.secure = options?.secure ?? defaultSecure();
|
|
438
|
+
this.sameSite = options?.sameSite ?? "Lax";
|
|
439
|
+
}
|
|
440
|
+
getItem(key) {
|
|
441
|
+
if (!hasDocument()) return null;
|
|
442
|
+
const doc = globalThis.document;
|
|
443
|
+
const cookies = doc.cookie ? doc.cookie.split(/;\s*/) : [];
|
|
444
|
+
const prefix = encodeURIComponent(key) + "=";
|
|
445
|
+
for (const c of cookies) {
|
|
446
|
+
if (c.startsWith(prefix)) {
|
|
447
|
+
try {
|
|
448
|
+
return decodeURIComponent(c.slice(prefix.length));
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
setItem(key, value) {
|
|
457
|
+
if (!hasDocument()) return;
|
|
458
|
+
const doc = globalThis.document;
|
|
459
|
+
const parts = [
|
|
460
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
|
461
|
+
"Path=/",
|
|
462
|
+
`Max-Age=${this.maxAgeSec}`,
|
|
463
|
+
`SameSite=${this.sameSite}`
|
|
464
|
+
];
|
|
465
|
+
if (this.secure) parts.push("Secure");
|
|
466
|
+
try {
|
|
467
|
+
doc.cookie = parts.join("; ");
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
removeItem(key) {
|
|
472
|
+
if (!hasDocument()) return;
|
|
473
|
+
const doc = globalThis.document;
|
|
474
|
+
const parts = [
|
|
475
|
+
`${encodeURIComponent(key)}=`,
|
|
476
|
+
"Path=/",
|
|
477
|
+
"Max-Age=0",
|
|
478
|
+
`SameSite=${this.sameSite}`
|
|
479
|
+
];
|
|
480
|
+
if (this.secure) parts.push("Secure");
|
|
481
|
+
try {
|
|
482
|
+
doc.cookie = parts.join("; ");
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
398
487
|
function detectDefaultStorage() {
|
|
399
488
|
try {
|
|
400
489
|
const ls = globalThis.localStorage;
|
|
@@ -408,6 +497,17 @@ function detectDefaultStorage() {
|
|
|
408
497
|
}
|
|
409
498
|
return new MemoryStorage();
|
|
410
499
|
}
|
|
500
|
+
function defaultSecure() {
|
|
501
|
+
try {
|
|
502
|
+
const loc = globalThis.location;
|
|
503
|
+
return loc?.protocol === "https:";
|
|
504
|
+
} catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function hasDocument() {
|
|
509
|
+
return typeof globalThis.document !== "undefined";
|
|
510
|
+
}
|
|
411
511
|
|
|
412
512
|
// src/device-info.ts
|
|
413
513
|
function isBrowser() {
|
|
@@ -508,6 +608,14 @@ var DEFAULT_AUTO_TRACK = {
|
|
|
508
608
|
deviceInfo: true
|
|
509
609
|
};
|
|
510
610
|
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
611
|
+
var EMPTY_ACQUISITION = {
|
|
612
|
+
utm_source: "",
|
|
613
|
+
utm_medium: "",
|
|
614
|
+
utm_campaign: "",
|
|
615
|
+
utm_content: "",
|
|
616
|
+
utm_term: "",
|
|
617
|
+
referrer: ""
|
|
618
|
+
};
|
|
511
619
|
var AutoTracker = class {
|
|
512
620
|
constructor(cfg, track) {
|
|
513
621
|
this.cfg = cfg;
|
|
@@ -543,6 +651,18 @@ var AutoTracker = class {
|
|
|
543
651
|
get currentSessionId() {
|
|
544
652
|
return this.session?.sessionId ?? null;
|
|
545
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Per-session acquisition context — utm_* + referrer, captured once
|
|
656
|
+
* at session start. Returns empty strings when there's no session
|
|
657
|
+
* (Node, before init, after uninstall) so callers can spread without
|
|
658
|
+
* conditional logic. Bank-grade rule: capture once, attach to every
|
|
659
|
+
* event of the session, don't re-read on every track() (the URL
|
|
660
|
+
* changes via SPA pushState; the source-of-record is the URL we
|
|
661
|
+
* landed on).
|
|
662
|
+
*/
|
|
663
|
+
get currentAcquisition() {
|
|
664
|
+
return this.session?.acquisition ?? EMPTY_ACQUISITION;
|
|
665
|
+
}
|
|
546
666
|
// ---------- sessions ----------
|
|
547
667
|
installSessionTracking() {
|
|
548
668
|
this.session = this.startNewSession();
|
|
@@ -580,7 +700,8 @@ var AutoTracker = class {
|
|
|
580
700
|
sessionId: mintSessionId(),
|
|
581
701
|
startedAt: Date.now(),
|
|
582
702
|
hiddenAt: null,
|
|
583
|
-
endedSent: false
|
|
703
|
+
endedSent: false,
|
|
704
|
+
acquisition: captureAcquisition()
|
|
584
705
|
};
|
|
585
706
|
}
|
|
586
707
|
emitSessionStart() {
|
|
@@ -646,6 +767,26 @@ function mintSessionId() {
|
|
|
646
767
|
const ts = Date.now().toString(36);
|
|
647
768
|
return `sess_${ts}${randomChars(10)}`;
|
|
648
769
|
}
|
|
770
|
+
function captureAcquisition() {
|
|
771
|
+
if (!isBrowserSafe()) return { ...EMPTY_ACQUISITION };
|
|
772
|
+
const result = { ...EMPTY_ACQUISITION };
|
|
773
|
+
try {
|
|
774
|
+
const w = globalThis.window;
|
|
775
|
+
const params = new URLSearchParams(w.location.search ?? "");
|
|
776
|
+
result.utm_source = params.get("utm_source") ?? "";
|
|
777
|
+
result.utm_medium = params.get("utm_medium") ?? "";
|
|
778
|
+
result.utm_campaign = params.get("utm_campaign") ?? "";
|
|
779
|
+
result.utm_content = params.get("utm_content") ?? "";
|
|
780
|
+
result.utm_term = params.get("utm_term") ?? "";
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const doc = globalThis.document;
|
|
785
|
+
if (typeof doc.referrer === "string") result.referrer = doc.referrer;
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
649
790
|
|
|
650
791
|
// src/debug.ts
|
|
651
792
|
var SENSITIVE_KEY_PATTERNS = [
|
|
@@ -750,7 +891,11 @@ var CrossdeckClient = class {
|
|
|
750
891
|
storagePrefix: options.storagePrefix ?? "crossdeck:",
|
|
751
892
|
autoHeartbeat: options.autoHeartbeat ?? true,
|
|
752
893
|
eventFlushBatchSize: options.eventFlushBatchSize ?? 20,
|
|
753
|
-
|
|
894
|
+
// 1500ms idle window. Short enough that an event queued on page
|
|
895
|
+
// load still flushes if the user leaves quickly (the keepalive
|
|
896
|
+
// pagehide handler picks up anything that doesn't); long enough
|
|
897
|
+
// that bursts of clicks coalesce into one network round-trip.
|
|
898
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 1500,
|
|
754
899
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
755
900
|
autoTrack,
|
|
756
901
|
appVersion: options.appVersion ?? null
|
|
@@ -763,7 +908,10 @@ var CrossdeckClient = class {
|
|
|
763
908
|
sdkVersion: opts.sdkVersion
|
|
764
909
|
});
|
|
765
910
|
const effectiveStorage = persistIdentity ? storage : new MemoryStorage();
|
|
766
|
-
const
|
|
911
|
+
const useCookieRedundancy = persistIdentity && !options.storage && // honour caller's adapter choice
|
|
912
|
+
typeof globalThis.document !== "undefined";
|
|
913
|
+
const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
|
|
914
|
+
const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
|
|
767
915
|
const entitlements = new EntitlementCache();
|
|
768
916
|
const events = new EventQueue({
|
|
769
917
|
http,
|
|
@@ -792,7 +940,8 @@ var CrossdeckClient = class {
|
|
|
792
940
|
deviceInfo,
|
|
793
941
|
options: opts,
|
|
794
942
|
debug,
|
|
795
|
-
developerUserId: null
|
|
943
|
+
developerUserId: null,
|
|
944
|
+
uninstallUnloadFlush: null
|
|
796
945
|
};
|
|
797
946
|
debug.emit("sdk.configured", `Crossdeck connected to ${opts.appId} in ${opts.environment} mode.`, {
|
|
798
947
|
appId: opts.appId,
|
|
@@ -807,6 +956,9 @@ var CrossdeckClient = class {
|
|
|
807
956
|
this.state.autoTracker = tracker;
|
|
808
957
|
tracker.install();
|
|
809
958
|
}
|
|
959
|
+
this.state.uninstallUnloadFlush = installUnloadFlush(() => {
|
|
960
|
+
void this.flush({ keepalive: true }).catch(() => void 0);
|
|
961
|
+
});
|
|
810
962
|
if (opts.autoHeartbeat) {
|
|
811
963
|
void this.heartbeat().catch(() => void 0);
|
|
812
964
|
}
|
|
@@ -940,6 +1092,15 @@ var CrossdeckClient = class {
|
|
|
940
1092
|
const enriched = { ...s.deviceInfo };
|
|
941
1093
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
942
1094
|
if (sessionId) enriched.sessionId = sessionId;
|
|
1095
|
+
const acquisition = s.autoTracker?.currentAcquisition;
|
|
1096
|
+
if (acquisition) {
|
|
1097
|
+
if (acquisition.utm_source) enriched.utm_source = acquisition.utm_source;
|
|
1098
|
+
if (acquisition.utm_medium) enriched.utm_medium = acquisition.utm_medium;
|
|
1099
|
+
if (acquisition.utm_campaign) enriched.utm_campaign = acquisition.utm_campaign;
|
|
1100
|
+
if (acquisition.utm_content) enriched.utm_content = acquisition.utm_content;
|
|
1101
|
+
if (acquisition.utm_term) enriched.utm_term = acquisition.utm_term;
|
|
1102
|
+
if (acquisition.referrer) enriched.referrer = acquisition.referrer;
|
|
1103
|
+
}
|
|
943
1104
|
if (properties) Object.assign(enriched, properties);
|
|
944
1105
|
const event = {
|
|
945
1106
|
eventId: this.mintEventId(),
|
|
@@ -953,11 +1114,16 @@ var CrossdeckClient = class {
|
|
|
953
1114
|
/**
|
|
954
1115
|
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
955
1116
|
*
|
|
1117
|
+
* Pass `{ keepalive: true }` from terminal handlers (pagehide /
|
|
1118
|
+
* visibilitychange→hidden / beforeunload). The browser keeps the
|
|
1119
|
+
* request alive after the page tears down, so the final batch
|
|
1120
|
+
* actually lands instead of being cancelled with the unload.
|
|
1121
|
+
*
|
|
956
1122
|
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
957
1123
|
*/
|
|
958
|
-
async flush() {
|
|
1124
|
+
async flush(options = {}) {
|
|
959
1125
|
const s = this.requireStarted();
|
|
960
|
-
await s.events.flush();
|
|
1126
|
+
await s.events.flush(options);
|
|
961
1127
|
}
|
|
962
1128
|
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
963
1129
|
async flushEvents() {
|
|
@@ -1140,6 +1306,23 @@ function resolveAutoTrack(input) {
|
|
|
1140
1306
|
deviceInfo: input.deviceInfo ?? DEFAULT_AUTO_TRACK.deviceInfo
|
|
1141
1307
|
};
|
|
1142
1308
|
}
|
|
1309
|
+
function installUnloadFlush(onUnload) {
|
|
1310
|
+
const w = globalThis.window;
|
|
1311
|
+
const doc = globalThis.document;
|
|
1312
|
+
if (!w || !doc) return () => void 0;
|
|
1313
|
+
const onVisChange = () => {
|
|
1314
|
+
if (doc.visibilityState === "hidden") onUnload();
|
|
1315
|
+
};
|
|
1316
|
+
const onTerminal = () => onUnload();
|
|
1317
|
+
doc.addEventListener("visibilitychange", onVisChange);
|
|
1318
|
+
w.addEventListener("pagehide", onTerminal);
|
|
1319
|
+
w.addEventListener("beforeunload", onTerminal);
|
|
1320
|
+
return () => {
|
|
1321
|
+
doc.removeEventListener("visibilitychange", onVisChange);
|
|
1322
|
+
w.removeEventListener("pagehide", onTerminal);
|
|
1323
|
+
w.removeEventListener("beforeunload", onTerminal);
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1143
1326
|
export {
|
|
1144
1327
|
Crossdeck,
|
|
1145
1328
|
CrossdeckClient,
|