@v-tilt/browser 1.0.11 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/array.js +1 -1
- package/dist/array.js.map +1 -1
- package/dist/array.no-external.js +1 -1
- package/dist/array.no-external.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.d.ts +55 -3
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.d.ts +55 -3
- package/dist/module.no-external.js +1 -1
- package/dist/module.no-external.js.map +1 -1
- package/dist/rate-limiter.d.ts +52 -0
- package/dist/request-queue.d.ts +78 -0
- package/dist/request.d.ts +54 -0
- package/dist/retry-queue.d.ts +64 -0
- package/dist/types.d.ts +1 -0
- package/dist/vtilt.d.ts +38 -7
- package/lib/rate-limiter.d.ts +52 -0
- package/lib/rate-limiter.js +80 -0
- package/lib/request-queue.d.ts +78 -0
- package/lib/request-queue.js +156 -0
- package/lib/request.d.ts +54 -0
- package/lib/request.js +265 -0
- package/lib/retry-queue.d.ts +64 -0
- package/lib/retry-queue.js +182 -0
- package/lib/types.d.ts +1 -0
- package/lib/utils/event-utils.js +88 -82
- package/lib/utils/index.js +2 -2
- package/lib/utils/request-utils.js +21 -19
- package/lib/vtilt.d.ts +38 -7
- package/lib/vtilt.js +143 -15
- package/package.json +13 -13
package/lib/request.d.ts
ADDED
|
@@ -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 req = new XMLHttpRequest();
|
|
76
|
+
req.open(options.method || "POST", options.url, true);
|
|
77
|
+
const encoded = encodePostData(options);
|
|
78
|
+
if (!encoded) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { contentType, body } = encoded;
|
|
82
|
+
// Set headers
|
|
83
|
+
if (options.headers) {
|
|
84
|
+
Object.entries(options.headers).forEach(([key, value]) => {
|
|
85
|
+
req.setRequestHeader(key, value);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
req.setRequestHeader("Content-Type", contentType);
|
|
89
|
+
// Add compression indicator for gzip
|
|
90
|
+
if (contentType === CONTENT_TYPE_PLAIN &&
|
|
91
|
+
options.compression === Compression.GZipJS) {
|
|
92
|
+
req.setRequestHeader("Content-Encoding", "gzip");
|
|
93
|
+
}
|
|
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
|
+
const headers = {
|
|
131
|
+
"Content-Type": contentType,
|
|
132
|
+
...(options.headers || {}),
|
|
133
|
+
};
|
|
134
|
+
// Add compression indicator for gzip
|
|
135
|
+
if (contentType === CONTENT_TYPE_PLAIN &&
|
|
136
|
+
options.compression === Compression.GZipJS) {
|
|
137
|
+
headers["Content-Encoding"] = "gzip";
|
|
138
|
+
}
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timeoutId = options.timeout
|
|
141
|
+
? setTimeout(() => controller.abort(), options.timeout)
|
|
142
|
+
: null;
|
|
143
|
+
fetch(options.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;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Queue - Exponential Backoff (PostHog-style)
|
|
3
|
+
*
|
|
4
|
+
* Retries failed requests with jittered exponential backoff.
|
|
5
|
+
* Detects online/offline status and pauses retries when offline.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Exponential backoff: 3s, 6s, 12s, 24s... up to 30 minutes
|
|
9
|
+
* - Jitter: +/- 50% to prevent thundering herd
|
|
10
|
+
* - Online/offline detection
|
|
11
|
+
* - Max 10 retries before giving up
|
|
12
|
+
* - Uses sendBeacon on page unload for final attempt
|
|
13
|
+
*/
|
|
14
|
+
import type { BatchedRequest } from "./request-queue";
|
|
15
|
+
/**
|
|
16
|
+
* Generates a jittered exponential backoff delay in milliseconds
|
|
17
|
+
*
|
|
18
|
+
* Base value is 3 seconds, doubled with each retry up to 30 minutes max.
|
|
19
|
+
* Each value has +/- 50% jitter.
|
|
20
|
+
*
|
|
21
|
+
* @param retriesPerformedSoFar - Number of retries already attempted
|
|
22
|
+
* @returns Delay in milliseconds
|
|
23
|
+
*/
|
|
24
|
+
export declare function pickNextRetryDelay(retriesPerformedSoFar: number): number;
|
|
25
|
+
export interface RetryQueueConfig {
|
|
26
|
+
sendRequest: (req: BatchedRequest) => Promise<{
|
|
27
|
+
statusCode: number;
|
|
28
|
+
}>;
|
|
29
|
+
sendBeacon: (req: BatchedRequest) => void;
|
|
30
|
+
}
|
|
31
|
+
export declare class RetryQueue {
|
|
32
|
+
private _isPolling;
|
|
33
|
+
private _poller?;
|
|
34
|
+
private _pollIntervalMs;
|
|
35
|
+
private _queue;
|
|
36
|
+
private _areWeOnline;
|
|
37
|
+
private _sendRequest;
|
|
38
|
+
private _sendBeacon;
|
|
39
|
+
constructor(config: RetryQueueConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Get current queue length
|
|
42
|
+
*/
|
|
43
|
+
get length(): number;
|
|
44
|
+
/**
|
|
45
|
+
* Enqueue a failed request for retry
|
|
46
|
+
*/
|
|
47
|
+
enqueue(request: BatchedRequest, retriesPerformedSoFar?: number): void;
|
|
48
|
+
/**
|
|
49
|
+
* Attempt to send a request with retry on failure
|
|
50
|
+
*/
|
|
51
|
+
retriableRequest(request: BatchedRequest): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Start polling for retries
|
|
54
|
+
*/
|
|
55
|
+
private _poll;
|
|
56
|
+
/**
|
|
57
|
+
* Flush ready items from the queue
|
|
58
|
+
*/
|
|
59
|
+
private _flush;
|
|
60
|
+
/**
|
|
61
|
+
* Flush all queued requests using sendBeacon on page unload
|
|
62
|
+
*/
|
|
63
|
+
unload(): void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Retry Queue - Exponential Backoff (PostHog-style)
|
|
4
|
+
*
|
|
5
|
+
* Retries failed requests with jittered exponential backoff.
|
|
6
|
+
* Detects online/offline status and pauses retries when offline.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Exponential backoff: 3s, 6s, 12s, 24s... up to 30 minutes
|
|
10
|
+
* - Jitter: +/- 50% to prevent thundering herd
|
|
11
|
+
* - Online/offline detection
|
|
12
|
+
* - Max 10 retries before giving up
|
|
13
|
+
* - Uses sendBeacon on page unload for final attempt
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.RetryQueue = void 0;
|
|
17
|
+
exports.pickNextRetryDelay = pickNextRetryDelay;
|
|
18
|
+
const utils_1 = require("./utils");
|
|
19
|
+
const globals_1 = require("./utils/globals");
|
|
20
|
+
const THIRTY_MINUTES = 30 * 60 * 1000;
|
|
21
|
+
const MAX_RETRIES = 10;
|
|
22
|
+
/**
|
|
23
|
+
* Generates a jittered exponential backoff delay in milliseconds
|
|
24
|
+
*
|
|
25
|
+
* Base value is 3 seconds, doubled with each retry up to 30 minutes max.
|
|
26
|
+
* Each value has +/- 50% jitter.
|
|
27
|
+
*
|
|
28
|
+
* @param retriesPerformedSoFar - Number of retries already attempted
|
|
29
|
+
* @returns Delay in milliseconds
|
|
30
|
+
*/
|
|
31
|
+
function pickNextRetryDelay(retriesPerformedSoFar) {
|
|
32
|
+
const rawBackoffTime = 3000 * 2 ** retriesPerformedSoFar;
|
|
33
|
+
const minBackoff = rawBackoffTime / 2;
|
|
34
|
+
const cappedBackoffTime = Math.min(THIRTY_MINUTES, rawBackoffTime);
|
|
35
|
+
const jitterFraction = Math.random() - 0.5; // Random between -0.5 and 0.5
|
|
36
|
+
const jitter = jitterFraction * (cappedBackoffTime - minBackoff);
|
|
37
|
+
return Math.ceil(cappedBackoffTime + jitter);
|
|
38
|
+
}
|
|
39
|
+
class RetryQueue {
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this._isPolling = false;
|
|
42
|
+
this._pollIntervalMs = 3000;
|
|
43
|
+
this._queue = [];
|
|
44
|
+
this._areWeOnline = true;
|
|
45
|
+
this._sendRequest = config.sendRequest;
|
|
46
|
+
this._sendBeacon = config.sendBeacon;
|
|
47
|
+
// Set up online/offline detection
|
|
48
|
+
if (globals_1.window && typeof globals_1.navigator !== "undefined" && "onLine" in globals_1.navigator) {
|
|
49
|
+
this._areWeOnline = globals_1.navigator.onLine;
|
|
50
|
+
(0, utils_1.addEventListener)(globals_1.window, "online", () => {
|
|
51
|
+
this._areWeOnline = true;
|
|
52
|
+
this._flush();
|
|
53
|
+
});
|
|
54
|
+
(0, utils_1.addEventListener)(globals_1.window, "offline", () => {
|
|
55
|
+
this._areWeOnline = false;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get current queue length
|
|
61
|
+
*/
|
|
62
|
+
get length() {
|
|
63
|
+
return this._queue.length;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Enqueue a failed request for retry
|
|
67
|
+
*/
|
|
68
|
+
enqueue(request, retriesPerformedSoFar = 0) {
|
|
69
|
+
// Don't retry if we've exceeded max retries
|
|
70
|
+
if (retriesPerformedSoFar >= MAX_RETRIES) {
|
|
71
|
+
console.warn(`VTilt: Request failed after ${MAX_RETRIES} retries, giving up`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const msToNextRetry = pickNextRetryDelay(retriesPerformedSoFar);
|
|
75
|
+
const retryAt = Date.now() + msToNextRetry;
|
|
76
|
+
this._queue.push({
|
|
77
|
+
retryAt,
|
|
78
|
+
request,
|
|
79
|
+
retriesPerformedSoFar: retriesPerformedSoFar + 1,
|
|
80
|
+
});
|
|
81
|
+
let logMessage = `VTilt: Enqueued failed request for retry in ${Math.round(msToNextRetry / 1000)}s`;
|
|
82
|
+
if (!this._areWeOnline) {
|
|
83
|
+
logMessage += " (Browser is offline)";
|
|
84
|
+
}
|
|
85
|
+
console.warn(logMessage);
|
|
86
|
+
// Start polling if not already
|
|
87
|
+
if (!this._isPolling) {
|
|
88
|
+
this._isPolling = true;
|
|
89
|
+
this._poll();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Attempt to send a request with retry on failure
|
|
94
|
+
*/
|
|
95
|
+
async retriableRequest(request) {
|
|
96
|
+
try {
|
|
97
|
+
const response = await this._sendRequest(request);
|
|
98
|
+
// Retry on server errors (5xx) or network errors (0)
|
|
99
|
+
// Don't retry on client errors (4xx)
|
|
100
|
+
if (response.statusCode !== 200 &&
|
|
101
|
+
(response.statusCode < 400 || response.statusCode >= 500)) {
|
|
102
|
+
this.enqueue(request, 0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (_a) {
|
|
106
|
+
// Network error - enqueue for retry
|
|
107
|
+
this.enqueue(request, 0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Start polling for retries
|
|
112
|
+
*/
|
|
113
|
+
_poll() {
|
|
114
|
+
if (this._poller) {
|
|
115
|
+
clearTimeout(this._poller);
|
|
116
|
+
}
|
|
117
|
+
this._poller = setTimeout(() => {
|
|
118
|
+
if (this._areWeOnline && this._queue.length > 0) {
|
|
119
|
+
this._flush();
|
|
120
|
+
}
|
|
121
|
+
// Continue polling if there are items in queue
|
|
122
|
+
if (this._queue.length > 0) {
|
|
123
|
+
this._poll();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this._isPolling = false;
|
|
127
|
+
}
|
|
128
|
+
}, this._pollIntervalMs);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Flush ready items from the queue
|
|
132
|
+
*/
|
|
133
|
+
_flush() {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const notReady = [];
|
|
136
|
+
const ready = [];
|
|
137
|
+
this._queue.forEach((item) => {
|
|
138
|
+
if (item.retryAt < now) {
|
|
139
|
+
ready.push(item);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
notReady.push(item);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
this._queue = notReady;
|
|
146
|
+
// Retry ready items
|
|
147
|
+
ready.forEach(async ({ request, retriesPerformedSoFar }) => {
|
|
148
|
+
try {
|
|
149
|
+
const response = await this._sendRequest(request);
|
|
150
|
+
// If still failing, re-enqueue
|
|
151
|
+
if (response.statusCode !== 200 &&
|
|
152
|
+
(response.statusCode < 400 || response.statusCode >= 500)) {
|
|
153
|
+
this.enqueue(request, retriesPerformedSoFar);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (_a) {
|
|
157
|
+
// Network error - re-enqueue
|
|
158
|
+
this.enqueue(request, retriesPerformedSoFar);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Flush all queued requests using sendBeacon on page unload
|
|
164
|
+
*/
|
|
165
|
+
unload() {
|
|
166
|
+
if (this._poller) {
|
|
167
|
+
clearTimeout(this._poller);
|
|
168
|
+
this._poller = undefined;
|
|
169
|
+
}
|
|
170
|
+
// Attempt final send of all queued requests
|
|
171
|
+
this._queue.forEach(({ request }) => {
|
|
172
|
+
try {
|
|
173
|
+
this._sendBeacon(request);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.error("VTilt: Failed to send beacon on unload", e);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
this._queue = [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.RetryQueue = RetryQueue;
|
package/lib/types.d.ts
CHANGED