@v-tilt/browser 1.0.11 → 1.1.1

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.
Files changed (45) hide show
  1. package/dist/array.js +1 -1
  2. package/dist/array.js.map +1 -1
  3. package/dist/array.no-external.js +1 -1
  4. package/dist/array.no-external.js.map +1 -1
  5. package/dist/constants.d.ts +172 -10
  6. package/dist/main.js +1 -1
  7. package/dist/main.js.map +1 -1
  8. package/dist/module.d.ts +230 -46
  9. package/dist/module.js +1 -1
  10. package/dist/module.js.map +1 -1
  11. package/dist/module.no-external.d.ts +230 -46
  12. package/dist/module.no-external.js +1 -1
  13. package/dist/module.no-external.js.map +1 -1
  14. package/dist/rate-limiter.d.ts +52 -0
  15. package/dist/request-queue.d.ts +78 -0
  16. package/dist/request.d.ts +54 -0
  17. package/dist/retry-queue.d.ts +64 -0
  18. package/dist/session.d.ts +2 -2
  19. package/dist/types.d.ts +154 -37
  20. package/dist/user-manager.d.ts +2 -2
  21. package/dist/vtilt.d.ts +51 -12
  22. package/lib/config.js +6 -13
  23. package/lib/constants.d.ts +172 -10
  24. package/lib/constants.js +644 -439
  25. package/lib/rate-limiter.d.ts +52 -0
  26. package/lib/rate-limiter.js +80 -0
  27. package/lib/request-queue.d.ts +78 -0
  28. package/lib/request-queue.js +156 -0
  29. package/lib/request.d.ts +54 -0
  30. package/lib/request.js +265 -0
  31. package/lib/retry-queue.d.ts +64 -0
  32. package/lib/retry-queue.js +182 -0
  33. package/lib/session.d.ts +2 -2
  34. package/lib/session.js +3 -3
  35. package/lib/types.d.ts +154 -37
  36. package/lib/types.js +6 -0
  37. package/lib/user-manager.d.ts +2 -2
  38. package/lib/user-manager.js +38 -11
  39. package/lib/utils/event-utils.js +88 -82
  40. package/lib/utils/index.js +2 -2
  41. package/lib/utils/request-utils.js +21 -19
  42. package/lib/vtilt.d.ts +51 -12
  43. package/lib/vtilt.js +199 -40
  44. package/lib/web-vitals.js +1 -1
  45. package/package.json +2 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Rate Limiter - Token Bucket Algorithm (PostHog-style)
3
+ *
4
+ * Prevents runaway loops from flooding the server with events.
5
+ * Uses a token bucket algorithm with configurable rate and burst limits.
6
+ *
7
+ * Features:
8
+ * - Configurable events per second (default: 10)
9
+ * - Configurable burst limit (default: 100)
10
+ * - Token replenishment over time
11
+ * - Warning event when rate limited
12
+ */
13
+ export declare const RATE_LIMIT_WARNING_EVENT = "$$client_ingestion_warning";
14
+ export interface RateLimitBucket {
15
+ tokens: number;
16
+ last: number;
17
+ }
18
+ export interface RateLimiterConfig {
19
+ eventsPerSecond?: number;
20
+ eventsBurstLimit?: number;
21
+ persistence?: {
22
+ get: (key: string) => RateLimitBucket | null;
23
+ set: (key: string, value: RateLimitBucket) => void;
24
+ };
25
+ captureWarning?: (message: string) => void;
26
+ }
27
+ export declare class RateLimiter {
28
+ private eventsPerSecond;
29
+ private eventsBurstLimit;
30
+ private lastEventRateLimited;
31
+ private persistence?;
32
+ private captureWarning?;
33
+ constructor(config?: RateLimiterConfig);
34
+ /**
35
+ * Check if the client should be rate limited
36
+ *
37
+ * @param checkOnly - If true, don't consume a token (just check)
38
+ * @returns Object with isRateLimited flag and remaining tokens
39
+ */
40
+ checkRateLimit(checkOnly?: boolean): {
41
+ isRateLimited: boolean;
42
+ remainingTokens: number;
43
+ };
44
+ /**
45
+ * Check if an event should be allowed (consumes a token if allowed)
46
+ */
47
+ shouldAllowEvent(): boolean;
48
+ /**
49
+ * Get remaining tokens without consuming
50
+ */
51
+ getRemainingTokens(): number;
52
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * Rate Limiter - Token Bucket Algorithm (PostHog-style)
4
+ *
5
+ * Prevents runaway loops from flooding the server with events.
6
+ * Uses a token bucket algorithm with configurable rate and burst limits.
7
+ *
8
+ * Features:
9
+ * - Configurable events per second (default: 10)
10
+ * - Configurable burst limit (default: 100)
11
+ * - Token replenishment over time
12
+ * - Warning event when rate limited
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.RateLimiter = exports.RATE_LIMIT_WARNING_EVENT = void 0;
16
+ const RATE_LIMIT_STORAGE_KEY = "vt_rate_limit";
17
+ exports.RATE_LIMIT_WARNING_EVENT = "$$client_ingestion_warning";
18
+ class RateLimiter {
19
+ constructor(config = {}) {
20
+ var _a, _b;
21
+ this.lastEventRateLimited = false;
22
+ this.eventsPerSecond = (_a = config.eventsPerSecond) !== null && _a !== void 0 ? _a : 10;
23
+ this.eventsBurstLimit = Math.max((_b = config.eventsBurstLimit) !== null && _b !== void 0 ? _b : this.eventsPerSecond * 10, this.eventsPerSecond);
24
+ this.persistence = config.persistence;
25
+ this.captureWarning = config.captureWarning;
26
+ // Initialize lastEventRateLimited from current state
27
+ this.lastEventRateLimited = this.checkRateLimit(true).isRateLimited;
28
+ }
29
+ /**
30
+ * Check if the client should be rate limited
31
+ *
32
+ * @param checkOnly - If true, don't consume a token (just check)
33
+ * @returns Object with isRateLimited flag and remaining tokens
34
+ */
35
+ checkRateLimit(checkOnly = false) {
36
+ var _a, _b, _c, _d;
37
+ const now = Date.now();
38
+ // Get current bucket state from persistence or create new
39
+ const bucket = (_b = (_a = this.persistence) === null || _a === void 0 ? void 0 : _a.get(RATE_LIMIT_STORAGE_KEY)) !== null && _b !== void 0 ? _b : {
40
+ tokens: this.eventsBurstLimit,
41
+ last: now,
42
+ };
43
+ // Replenish tokens based on time elapsed
44
+ const secondsElapsed = (now - bucket.last) / 1000;
45
+ bucket.tokens += secondsElapsed * this.eventsPerSecond;
46
+ bucket.last = now;
47
+ // Cap tokens at burst limit
48
+ if (bucket.tokens > this.eventsBurstLimit) {
49
+ bucket.tokens = this.eventsBurstLimit;
50
+ }
51
+ const isRateLimited = bucket.tokens < 1;
52
+ // Consume a token if not just checking
53
+ if (!isRateLimited && !checkOnly) {
54
+ bucket.tokens = Math.max(0, bucket.tokens - 1);
55
+ }
56
+ // Capture warning event when first rate limited
57
+ if (isRateLimited && !this.lastEventRateLimited && !checkOnly) {
58
+ (_c = this.captureWarning) === null || _c === void 0 ? void 0 : _c.call(this, `vTilt client rate limited. Config: ${this.eventsPerSecond} events/second, ${this.eventsBurstLimit} burst limit.`);
59
+ }
60
+ this.lastEventRateLimited = isRateLimited;
61
+ (_d = this.persistence) === null || _d === void 0 ? void 0 : _d.set(RATE_LIMIT_STORAGE_KEY, bucket);
62
+ return {
63
+ isRateLimited,
64
+ remainingTokens: bucket.tokens,
65
+ };
66
+ }
67
+ /**
68
+ * Check if an event should be allowed (consumes a token if allowed)
69
+ */
70
+ shouldAllowEvent() {
71
+ return !this.checkRateLimit(false).isRateLimited;
72
+ }
73
+ /**
74
+ * Get remaining tokens without consuming
75
+ */
76
+ getRemainingTokens() {
77
+ return this.checkRateLimit(true).remainingTokens;
78
+ }
79
+ }
80
+ exports.RateLimiter = RateLimiter;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Request Queue - Event Batching (PostHog-style)
3
+ *
4
+ * Batches multiple events together and sends them at configurable intervals.
5
+ * This reduces the number of HTTP requests significantly for active users.
6
+ *
7
+ * Features:
8
+ * - Configurable flush interval (default 3 seconds)
9
+ * - Batches events by URL/batchKey
10
+ * - Uses sendBeacon on page unload for reliable delivery
11
+ * - Converts absolute timestamps to relative offsets before sending
12
+ */
13
+ import type { TrackingEvent } from "./types";
14
+ export declare const DEFAULT_FLUSH_INTERVAL_MS = 3000;
15
+ export interface QueuedRequest {
16
+ url: string;
17
+ event: TrackingEvent;
18
+ batchKey?: string;
19
+ transport?: "xhr" | "sendBeacon";
20
+ }
21
+ export interface BatchedRequest {
22
+ url: string;
23
+ events: TrackingEvent[];
24
+ batchKey?: string;
25
+ transport?: "xhr" | "sendBeacon";
26
+ }
27
+ export interface RequestQueueConfig {
28
+ flush_interval_ms?: number;
29
+ }
30
+ export declare class RequestQueue {
31
+ private _isPaused;
32
+ private _queue;
33
+ private _flushTimeout?;
34
+ private _flushTimeoutMs;
35
+ private _sendRequest;
36
+ constructor(sendRequest: (req: BatchedRequest) => void, config?: RequestQueueConfig);
37
+ /**
38
+ * Get the current queue length
39
+ */
40
+ get length(): number;
41
+ /**
42
+ * Enqueue an event for batched sending
43
+ */
44
+ enqueue(req: QueuedRequest): void;
45
+ /**
46
+ * Flush all queued events immediately using sendBeacon
47
+ * Called on page unload to ensure events are delivered
48
+ */
49
+ unload(): void;
50
+ /**
51
+ * Enable the queue and start flushing
52
+ */
53
+ enable(): void;
54
+ /**
55
+ * Pause the queue (stops flushing but keeps events)
56
+ */
57
+ pause(): void;
58
+ /**
59
+ * Force an immediate flush
60
+ */
61
+ flush(): void;
62
+ /**
63
+ * Set up the flush timeout
64
+ */
65
+ private _setFlushTimeout;
66
+ /**
67
+ * Clear the flush timeout
68
+ */
69
+ private _clearFlushTimeout;
70
+ /**
71
+ * Flush all queued events now
72
+ */
73
+ private _flushNow;
74
+ /**
75
+ * Format the queue into batched requests by URL/batchKey
76
+ */
77
+ private _formatQueue;
78
+ }
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * Request Queue - Event Batching (PostHog-style)
4
+ *
5
+ * Batches multiple events together and sends them at configurable intervals.
6
+ * This reduces the number of HTTP requests significantly for active users.
7
+ *
8
+ * Features:
9
+ * - Configurable flush interval (default 3 seconds)
10
+ * - Batches events by URL/batchKey
11
+ * - Uses sendBeacon on page unload for reliable delivery
12
+ * - Converts absolute timestamps to relative offsets before sending
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.RequestQueue = exports.DEFAULT_FLUSH_INTERVAL_MS = void 0;
16
+ exports.DEFAULT_FLUSH_INTERVAL_MS = 3000;
17
+ /**
18
+ * Clamp a value to a range
19
+ */
20
+ function clampToRange(value, min, max, defaultValue) {
21
+ if (typeof value !== "number" || isNaN(value)) {
22
+ return defaultValue;
23
+ }
24
+ return Math.min(Math.max(value, min), max);
25
+ }
26
+ class RequestQueue {
27
+ constructor(sendRequest, config) {
28
+ // We start in a paused state and only start flushing when enabled
29
+ this._isPaused = true;
30
+ this._queue = [];
31
+ this._flushTimeoutMs = clampToRange((config === null || config === void 0 ? void 0 : config.flush_interval_ms) || exports.DEFAULT_FLUSH_INTERVAL_MS, 250, // Min 250ms
32
+ 5000, // Max 5 seconds
33
+ exports.DEFAULT_FLUSH_INTERVAL_MS);
34
+ this._sendRequest = sendRequest;
35
+ }
36
+ /**
37
+ * Get the current queue length
38
+ */
39
+ get length() {
40
+ return this._queue.length;
41
+ }
42
+ /**
43
+ * Enqueue an event for batched sending
44
+ */
45
+ enqueue(req) {
46
+ this._queue.push(req);
47
+ // Start flush timer if not already running
48
+ if (!this._flushTimeout) {
49
+ this._setFlushTimeout();
50
+ }
51
+ }
52
+ /**
53
+ * Flush all queued events immediately using sendBeacon
54
+ * Called on page unload to ensure events are delivered
55
+ */
56
+ unload() {
57
+ this._clearFlushTimeout();
58
+ if (this._queue.length === 0) {
59
+ return;
60
+ }
61
+ const requests = this._formatQueue();
62
+ // Send each batched request using sendBeacon for reliable delivery
63
+ for (const key in requests) {
64
+ const req = requests[key];
65
+ this._sendRequest({ ...req, transport: "sendBeacon" });
66
+ }
67
+ }
68
+ /**
69
+ * Enable the queue and start flushing
70
+ */
71
+ enable() {
72
+ this._isPaused = false;
73
+ this._setFlushTimeout();
74
+ }
75
+ /**
76
+ * Pause the queue (stops flushing but keeps events)
77
+ */
78
+ pause() {
79
+ this._isPaused = true;
80
+ this._clearFlushTimeout();
81
+ }
82
+ /**
83
+ * Force an immediate flush
84
+ */
85
+ flush() {
86
+ this._clearFlushTimeout();
87
+ this._flushNow();
88
+ this._setFlushTimeout();
89
+ }
90
+ /**
91
+ * Set up the flush timeout
92
+ */
93
+ _setFlushTimeout() {
94
+ if (this._isPaused) {
95
+ return;
96
+ }
97
+ this._flushTimeout = setTimeout(() => {
98
+ this._clearFlushTimeout();
99
+ this._flushNow();
100
+ // Restart the timeout for continuous flushing
101
+ if (this._queue.length > 0) {
102
+ this._setFlushTimeout();
103
+ }
104
+ }, this._flushTimeoutMs);
105
+ }
106
+ /**
107
+ * Clear the flush timeout
108
+ */
109
+ _clearFlushTimeout() {
110
+ if (this._flushTimeout) {
111
+ clearTimeout(this._flushTimeout);
112
+ this._flushTimeout = undefined;
113
+ }
114
+ }
115
+ /**
116
+ * Flush all queued events now
117
+ */
118
+ _flushNow() {
119
+ if (this._queue.length === 0) {
120
+ return;
121
+ }
122
+ const requests = this._formatQueue();
123
+ const now = Date.now();
124
+ for (const key in requests) {
125
+ const req = requests[key];
126
+ // Convert absolute timestamps to relative offsets
127
+ // This helps with clock skew between client and server
128
+ req.events.forEach((event) => {
129
+ const eventTime = new Date(event.timestamp).getTime();
130
+ event.$offset = Math.abs(eventTime - now);
131
+ });
132
+ this._sendRequest(req);
133
+ }
134
+ }
135
+ /**
136
+ * Format the queue into batched requests by URL/batchKey
137
+ */
138
+ _formatQueue() {
139
+ const requests = {};
140
+ this._queue.forEach((request) => {
141
+ const key = request.batchKey || request.url;
142
+ if (!requests[key]) {
143
+ requests[key] = {
144
+ url: request.url,
145
+ events: [],
146
+ batchKey: request.batchKey,
147
+ };
148
+ }
149
+ requests[key].events.push(request.event);
150
+ });
151
+ // Clear the queue
152
+ this._queue = [];
153
+ return requests;
154
+ }
155
+ }
156
+ exports.RequestQueue = RequestQueue;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Request utilities for vTilt tracking
3
+ *
4
+ * Handles HTTP requests with:
5
+ * - GZip compression (via fflate)
6
+ * - Multiple transport methods (fetch, XHR, sendBeacon)
7
+ * - Automatic fallback between transports
8
+ *
9
+ * Based on PostHog's request.ts pattern
10
+ */
11
+ /**
12
+ * Compression methods supported by the SDK
13
+ */
14
+ export declare enum Compression {
15
+ GZipJS = "gzip-js",
16
+ None = "none"
17
+ }
18
+ /**
19
+ * Response from a request
20
+ */
21
+ export interface RequestResponse {
22
+ statusCode: number;
23
+ text?: string;
24
+ json?: any;
25
+ }
26
+ /**
27
+ * Options for making a request
28
+ */
29
+ export interface RequestOptions {
30
+ url: string;
31
+ data?: any;
32
+ method?: "POST" | "GET";
33
+ headers?: Record<string, string>;
34
+ transport?: "XHR" | "fetch" | "sendBeacon";
35
+ compression?: Compression;
36
+ timeout?: number;
37
+ callback?: (response: RequestResponse) => void;
38
+ }
39
+ /**
40
+ * JSON stringify with BigInt support
41
+ */
42
+ export declare const jsonStringify: (data: any) => string;
43
+ /**
44
+ * Main request function - handles transport selection and dispatching
45
+ */
46
+ export declare const request: (options: RequestOptions) => void;
47
+ /**
48
+ * Promise-based request wrapper
49
+ */
50
+ export declare const requestAsync: (options: Omit<RequestOptions, "callback">) => Promise<RequestResponse>;
51
+ /**
52
+ * Check if compression is supported and beneficial
53
+ */
54
+ export declare const shouldCompress: (data: any) => boolean;
package/lib/request.js ADDED
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ /**
3
+ * Request utilities for vTilt tracking
4
+ *
5
+ * Handles HTTP requests with:
6
+ * - GZip compression (via fflate)
7
+ * - Multiple transport methods (fetch, XHR, sendBeacon)
8
+ * - Automatic fallback between transports
9
+ *
10
+ * Based on PostHog's request.ts pattern
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.shouldCompress = exports.requestAsync = exports.request = exports.jsonStringify = exports.Compression = void 0;
14
+ const fflate_1 = require("fflate");
15
+ // Content types
16
+ const CONTENT_TYPE_PLAIN = "text/plain";
17
+ const CONTENT_TYPE_JSON = "application/json";
18
+ // Compression threshold - skip compression for small payloads
19
+ const MIN_COMPRESSION_SIZE = 1024; // 1KB
20
+ /**
21
+ * Compression methods supported by the SDK
22
+ */
23
+ var Compression;
24
+ (function (Compression) {
25
+ Compression["GZipJS"] = "gzip-js";
26
+ Compression["None"] = "none";
27
+ })(Compression || (exports.Compression = Compression = {}));
28
+ /**
29
+ * JSON stringify with BigInt support
30
+ */
31
+ const jsonStringify = (data) => {
32
+ return JSON.stringify(data, (_, value) => typeof value === "bigint" ? value.toString() : value);
33
+ };
34
+ exports.jsonStringify = jsonStringify;
35
+ /**
36
+ * Encode request data with optional compression
37
+ */
38
+ const encodePostData = (options) => {
39
+ const { data, compression } = options;
40
+ if (!data) {
41
+ return;
42
+ }
43
+ const jsonBody = (0, exports.jsonStringify)(data);
44
+ const estimatedSize = new Blob([jsonBody]).size;
45
+ // Use GZip compression if enabled and payload is large enough
46
+ if (compression === Compression.GZipJS &&
47
+ estimatedSize >= MIN_COMPRESSION_SIZE) {
48
+ try {
49
+ const gzipData = (0, fflate_1.gzipSync)((0, fflate_1.strToU8)(jsonBody), { mtime: 0 });
50
+ const blob = new Blob([gzipData], { type: CONTENT_TYPE_PLAIN });
51
+ // Only use compression if it actually reduces size
52
+ if (blob.size < estimatedSize * 0.95) {
53
+ return {
54
+ contentType: CONTENT_TYPE_PLAIN,
55
+ body: blob,
56
+ estimatedSize: blob.size,
57
+ };
58
+ }
59
+ }
60
+ catch (_a) {
61
+ // Fallback to uncompressed on error
62
+ }
63
+ }
64
+ // Return uncompressed JSON
65
+ return {
66
+ contentType: CONTENT_TYPE_JSON,
67
+ body: jsonBody,
68
+ estimatedSize,
69
+ };
70
+ };
71
+ /**
72
+ * Send request using XMLHttpRequest
73
+ */
74
+ const xhrRequest = (options) => {
75
+ const encoded = encodePostData(options);
76
+ if (!encoded) {
77
+ return;
78
+ }
79
+ const { contentType, body } = encoded;
80
+ // Add compression query param for server to detect (consistent with sendBeacon)
81
+ const url = options.compression === Compression.GZipJS &&
82
+ contentType === CONTENT_TYPE_PLAIN
83
+ ? `${options.url}${options.url.includes("?") ? "&" : "?"}compression=gzip-js`
84
+ : options.url;
85
+ const req = new XMLHttpRequest();
86
+ req.open(options.method || "POST", url, true);
87
+ // Set headers
88
+ if (options.headers) {
89
+ Object.entries(options.headers).forEach(([key, value]) => {
90
+ req.setRequestHeader(key, value);
91
+ });
92
+ }
93
+ req.setRequestHeader("Content-Type", contentType);
94
+ if (options.timeout) {
95
+ req.timeout = options.timeout;
96
+ }
97
+ req.onreadystatechange = () => {
98
+ var _a;
99
+ if (req.readyState === 4) {
100
+ const response = {
101
+ statusCode: req.status,
102
+ text: req.responseText,
103
+ };
104
+ if (req.status === 200) {
105
+ try {
106
+ response.json = JSON.parse(req.responseText);
107
+ }
108
+ catch (_b) {
109
+ // Ignore parse errors
110
+ }
111
+ }
112
+ (_a = options.callback) === null || _a === void 0 ? void 0 : _a.call(options, response);
113
+ }
114
+ };
115
+ req.onerror = () => {
116
+ var _a;
117
+ (_a = options.callback) === null || _a === void 0 ? void 0 : _a.call(options, { statusCode: 0 });
118
+ };
119
+ req.send(body);
120
+ };
121
+ /**
122
+ * Send request using fetch API
123
+ */
124
+ const fetchRequest = (options) => {
125
+ const encoded = encodePostData(options);
126
+ if (!encoded) {
127
+ return;
128
+ }
129
+ const { contentType, body, estimatedSize } = encoded;
130
+ // Add compression query param for server to detect (consistent with sendBeacon)
131
+ const url = options.compression === Compression.GZipJS &&
132
+ contentType === CONTENT_TYPE_PLAIN
133
+ ? `${options.url}${options.url.includes("?") ? "&" : "?"}compression=gzip-js`
134
+ : options.url;
135
+ const headers = {
136
+ "Content-Type": contentType,
137
+ ...(options.headers || {}),
138
+ };
139
+ const controller = new AbortController();
140
+ const timeoutId = options.timeout
141
+ ? setTimeout(() => controller.abort(), options.timeout)
142
+ : null;
143
+ fetch(url, {
144
+ method: options.method || "POST",
145
+ headers,
146
+ body,
147
+ // Use keepalive for smaller payloads to ensure delivery on page close
148
+ keepalive: estimatedSize < 64 * 1024 * 0.8,
149
+ signal: controller.signal,
150
+ })
151
+ .then(async (response) => {
152
+ var _a;
153
+ const text = await response.text();
154
+ const res = {
155
+ statusCode: response.status,
156
+ text,
157
+ };
158
+ if (response.status === 200) {
159
+ try {
160
+ res.json = JSON.parse(text);
161
+ }
162
+ catch (_b) {
163
+ // Ignore parse errors
164
+ }
165
+ }
166
+ (_a = options.callback) === null || _a === void 0 ? void 0 : _a.call(options, res);
167
+ })
168
+ .catch(() => {
169
+ var _a;
170
+ (_a = options.callback) === null || _a === void 0 ? void 0 : _a.call(options, { statusCode: 0 });
171
+ })
172
+ .finally(() => {
173
+ if (timeoutId) {
174
+ clearTimeout(timeoutId);
175
+ }
176
+ });
177
+ };
178
+ /**
179
+ * Send request using sendBeacon for reliable delivery on page unload
180
+ */
181
+ const sendBeaconRequest = (options) => {
182
+ const encoded = encodePostData(options);
183
+ if (!encoded) {
184
+ return;
185
+ }
186
+ const { contentType, body } = encoded;
187
+ try {
188
+ // sendBeacon requires a Blob
189
+ const beaconBody = typeof body === "string" ? new Blob([body], { type: contentType }) : body;
190
+ // Add compression query param for server to detect
191
+ const url = options.compression === Compression.GZipJS &&
192
+ contentType === CONTENT_TYPE_PLAIN
193
+ ? `${options.url}${options.url.includes("?") ? "&" : "?"}compression=gzip-js`
194
+ : options.url;
195
+ navigator.sendBeacon(url, beaconBody);
196
+ }
197
+ catch (_a) {
198
+ // sendBeacon is best-effort, ignore errors
199
+ }
200
+ };
201
+ /**
202
+ * Available transport methods in order of preference
203
+ */
204
+ const TRANSPORTS = [
205
+ {
206
+ name: "fetch",
207
+ available: typeof fetch !== "undefined",
208
+ method: fetchRequest,
209
+ },
210
+ {
211
+ name: "XHR",
212
+ available: typeof XMLHttpRequest !== "undefined",
213
+ method: xhrRequest,
214
+ },
215
+ {
216
+ name: "sendBeacon",
217
+ available: typeof navigator !== "undefined" && !!navigator.sendBeacon,
218
+ method: sendBeaconRequest,
219
+ },
220
+ ];
221
+ /**
222
+ * Main request function - handles transport selection and dispatching
223
+ */
224
+ const request = (options) => {
225
+ var _a;
226
+ const transport = options.transport || "fetch";
227
+ // Find the requested transport or fall back to first available
228
+ const transportConfig = TRANSPORTS.find((t) => t.name === transport && t.available) ||
229
+ TRANSPORTS.find((t) => t.available);
230
+ if (!transportConfig) {
231
+ console.error("vTilt: No available transport method");
232
+ (_a = options.callback) === null || _a === void 0 ? void 0 : _a.call(options, { statusCode: 0 });
233
+ return;
234
+ }
235
+ transportConfig.method(options);
236
+ };
237
+ exports.request = request;
238
+ /**
239
+ * Promise-based request wrapper
240
+ */
241
+ const requestAsync = (options) => {
242
+ return new Promise((resolve) => {
243
+ (0, exports.request)({
244
+ ...options,
245
+ callback: resolve,
246
+ });
247
+ });
248
+ };
249
+ exports.requestAsync = requestAsync;
250
+ /**
251
+ * Check if compression is supported and beneficial
252
+ */
253
+ const shouldCompress = (data) => {
254
+ if (typeof data === "undefined") {
255
+ return false;
256
+ }
257
+ try {
258
+ const size = new Blob([(0, exports.jsonStringify)(data)]).size;
259
+ return size >= MIN_COMPRESSION_SIZE;
260
+ }
261
+ catch (_a) {
262
+ return false;
263
+ }
264
+ };
265
+ exports.shouldCompress = shouldCompress;