@findatruck/convex-client 0.1.0 → 0.2.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.
- package/dist/index.cjs +154 -13
- package/dist/index.d.cts +40 -4
- package/dist/index.d.ts +40 -4
- package/dist/index.js +154 -13
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -65,11 +65,19 @@ var ConvexClient = class {
|
|
|
65
65
|
constructor(config) {
|
|
66
66
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
67
67
|
this.workerSecret = config.workerSecret;
|
|
68
|
+
this.retryConfig = {
|
|
69
|
+
maxAttempts: config.retry?.maxAttempts ?? 5,
|
|
70
|
+
baseDelayMs: config.retry?.baseDelayMs ?? 250,
|
|
71
|
+
maxDelayMs: config.retry?.maxDelayMs ?? 15e3,
|
|
72
|
+
maxElapsedMs: config.retry?.maxElapsedMs ?? 6e4,
|
|
73
|
+
perAttemptTimeoutMs: config.retry?.perAttemptTimeoutMs ?? 2e4
|
|
74
|
+
};
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
70
77
|
* Posts driver view data to the Convex ingestion endpoint.
|
|
78
|
+
* Uses retry logic with exponential backoff for resilience.
|
|
71
79
|
*/
|
|
72
|
-
async postDriverView(data) {
|
|
80
|
+
async postDriverView(data, options) {
|
|
73
81
|
const route = routes.ingestDriverView;
|
|
74
82
|
const parseResult = route.requestSchema.safeParse(data);
|
|
75
83
|
if (!parseResult.success) {
|
|
@@ -78,12 +86,14 @@ var ConvexClient = class {
|
|
|
78
86
|
route.path
|
|
79
87
|
);
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
|
|
90
|
+
await this.makeRequestWithRetry(effectiveBaseUrl, route.path, route.method, data);
|
|
82
91
|
}
|
|
83
92
|
/**
|
|
84
93
|
* Posts provider account status updates to the Convex ingestion endpoint.
|
|
94
|
+
* Does NOT use retry logic (single attempt only).
|
|
85
95
|
*/
|
|
86
|
-
async postProviderAccountStatus(data) {
|
|
96
|
+
async postProviderAccountStatus(data, options) {
|
|
87
97
|
const route = routes.ingestProviderAccountStatus;
|
|
88
98
|
const parseResult = route.requestSchema.safeParse(data);
|
|
89
99
|
if (!parseResult.success) {
|
|
@@ -92,15 +102,16 @@ var ConvexClient = class {
|
|
|
92
102
|
route.path
|
|
93
103
|
);
|
|
94
104
|
}
|
|
95
|
-
|
|
105
|
+
const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
|
|
106
|
+
await this.makeRequest(effectiveBaseUrl, route.path, route.method, data);
|
|
96
107
|
}
|
|
97
|
-
async makeRequest(path, method, body) {
|
|
98
|
-
const url = `${
|
|
108
|
+
async makeRequest(baseUrl, path, method, body) {
|
|
109
|
+
const url = `${baseUrl}${path}`;
|
|
99
110
|
const response = await fetch(url, {
|
|
100
111
|
method,
|
|
101
112
|
headers: {
|
|
102
113
|
"Content-Type": "application/json",
|
|
103
|
-
"
|
|
114
|
+
"x-worker-secret": this.workerSecret
|
|
104
115
|
},
|
|
105
116
|
body: JSON.stringify(body)
|
|
106
117
|
});
|
|
@@ -119,21 +130,151 @@ var ConvexClient = class {
|
|
|
119
130
|
);
|
|
120
131
|
}
|
|
121
132
|
}
|
|
133
|
+
async makeRequestWithRetry(baseUrl, path, method, body) {
|
|
134
|
+
const url = `${baseUrl}${path}`;
|
|
135
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, maxElapsedMs, perAttemptTimeoutMs } = this.retryConfig;
|
|
136
|
+
const retryOnStatuses = [429, 500, 502, 503, 504];
|
|
137
|
+
const startedAt = Date.now();
|
|
138
|
+
let attempt = 0;
|
|
139
|
+
const calcBackoff = (retriesSoFar) => Math.floor(Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** retriesSoFar));
|
|
140
|
+
while (true) {
|
|
141
|
+
attempt += 1;
|
|
142
|
+
if (Date.now() - startedAt > maxElapsedMs) {
|
|
143
|
+
throw new ConvexClientError(
|
|
144
|
+
`Request to ${path} exceeded max elapsed time ${maxElapsedMs}ms`,
|
|
145
|
+
path
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
let response;
|
|
149
|
+
let error;
|
|
150
|
+
try {
|
|
151
|
+
response = await this.attemptFetch(url, method, body, perAttemptTimeoutMs);
|
|
152
|
+
if (response.ok) return;
|
|
153
|
+
if (!retryOnStatuses.includes(response.status)) {
|
|
154
|
+
let responseBody;
|
|
155
|
+
try {
|
|
156
|
+
responseBody = await response.json();
|
|
157
|
+
} catch {
|
|
158
|
+
responseBody = await response.text();
|
|
159
|
+
}
|
|
160
|
+
throw new ConvexClientError(
|
|
161
|
+
`Request to ${path} failed with status ${response.status}`,
|
|
162
|
+
path,
|
|
163
|
+
response.status,
|
|
164
|
+
responseBody
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
error = e;
|
|
169
|
+
if (e instanceof ConvexClientError) throw e;
|
|
170
|
+
if (this.isAbortError(e)) throw e;
|
|
171
|
+
}
|
|
172
|
+
if (attempt >= maxAttempts) {
|
|
173
|
+
if (response) {
|
|
174
|
+
let responseBody;
|
|
175
|
+
try {
|
|
176
|
+
responseBody = await response.json();
|
|
177
|
+
} catch {
|
|
178
|
+
responseBody = await response.text();
|
|
179
|
+
}
|
|
180
|
+
throw new ConvexClientError(
|
|
181
|
+
`Request to ${path} failed after ${attempt} attempts with status ${response.status}`,
|
|
182
|
+
path,
|
|
183
|
+
response.status,
|
|
184
|
+
responseBody
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (error instanceof Error) throw error;
|
|
188
|
+
throw new ConvexClientError(
|
|
189
|
+
`Request to ${path} failed without response after ${attempt} attempts`,
|
|
190
|
+
path
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
let delayMs = 0;
|
|
194
|
+
const retryAfter = response?.headers.get("retry-after");
|
|
195
|
+
if (retryAfter) {
|
|
196
|
+
const parsed = this.parseRetryAfter(retryAfter);
|
|
197
|
+
if (parsed != null) delayMs = Math.min(parsed, maxDelayMs);
|
|
198
|
+
}
|
|
199
|
+
if (delayMs === 0) {
|
|
200
|
+
delayMs = calcBackoff(attempt - 1);
|
|
201
|
+
}
|
|
202
|
+
const elapsed = Date.now() - startedAt;
|
|
203
|
+
if (elapsed + delayMs > maxElapsedMs) {
|
|
204
|
+
delayMs = Math.max(0, maxElapsedMs - elapsed);
|
|
205
|
+
if (delayMs === 0) {
|
|
206
|
+
if (response) {
|
|
207
|
+
throw new ConvexClientError(
|
|
208
|
+
`Request to ${path} time budget exhausted before retry`,
|
|
209
|
+
path,
|
|
210
|
+
response.status
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (error instanceof Error) throw error;
|
|
214
|
+
throw new ConvexClientError(
|
|
215
|
+
`Request to ${path} time budget exhausted before retry`,
|
|
216
|
+
path
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
await this.sleep(delayMs);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async attemptFetch(url, method, body, perAttemptTimeoutMs) {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const timeoutId = perAttemptTimeoutMs > 0 ? setTimeout(() => {
|
|
226
|
+
controller.abort(new Error(`per-attempt timeout ${perAttemptTimeoutMs}ms`));
|
|
227
|
+
}, perAttemptTimeoutMs) : null;
|
|
228
|
+
try {
|
|
229
|
+
return await fetch(url, {
|
|
230
|
+
method,
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
"x-worker-secret": this.workerSecret
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
signal: controller.signal
|
|
237
|
+
});
|
|
238
|
+
} finally {
|
|
239
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
parseRetryAfter(value) {
|
|
243
|
+
const secs = Number(value);
|
|
244
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
245
|
+
const dateMs = Date.parse(value);
|
|
246
|
+
if (!Number.isNaN(dateMs)) {
|
|
247
|
+
const delta = dateMs - Date.now();
|
|
248
|
+
return Math.max(0, delta);
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
sleep(ms) {
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
if (ms <= 0) {
|
|
255
|
+
resolve();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
setTimeout(resolve, ms);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
isAbortError(err) {
|
|
262
|
+
return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && /abort/i.test(err.name + " " + err.message);
|
|
263
|
+
}
|
|
122
264
|
};
|
|
123
265
|
|
|
124
266
|
// src/validation.ts
|
|
125
267
|
function checkWorkerSecret(req, expectedSecret) {
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
268
|
+
const secretHeader = req.headers.get("x-worker-secret");
|
|
269
|
+
if (!secretHeader) {
|
|
128
270
|
return false;
|
|
129
271
|
}
|
|
130
|
-
|
|
131
|
-
if (authHeader.length !== expectedValue.length) {
|
|
272
|
+
if (secretHeader.length !== expectedSecret.length) {
|
|
132
273
|
return false;
|
|
133
274
|
}
|
|
134
275
|
let result = 0;
|
|
135
|
-
for (let i = 0; i <
|
|
136
|
-
result |=
|
|
276
|
+
for (let i = 0; i < secretHeader.length; i++) {
|
|
277
|
+
result |= secretHeader.charCodeAt(i) ^ expectedSecret.charCodeAt(i);
|
|
137
278
|
}
|
|
138
279
|
return result === 0;
|
|
139
280
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -3,6 +3,21 @@ import { ConvexUpdateData, UpdateScrapeStatusMessage } from '@findatruck/shared-
|
|
|
3
3
|
export { BatchConvexUpdateData, BatchConvexUpdateSchema, ConvexDriverData, ConvexDriverSchema, ConvexUpdateData, ConvexUpdateSchema, ScrapeStatus, UpdateScrapeStatusMessage, UpdateScrapeStatusMessage as UpdateScrapeStatusMessageType } from '@findatruck/shared-schemas';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for retry behavior
|
|
8
|
+
*/
|
|
9
|
+
interface RetryConfig {
|
|
10
|
+
/** Max number of attempts (first try + retries). Default: 5 */
|
|
11
|
+
maxAttempts?: number;
|
|
12
|
+
/** Base delay in ms before backoff. Default: 250 */
|
|
13
|
+
baseDelayMs?: number;
|
|
14
|
+
/** Max delay cap in ms. Default: 15_000 */
|
|
15
|
+
maxDelayMs?: number;
|
|
16
|
+
/** Max total elapsed time budget in ms (includes all attempts). Default: 60_000 */
|
|
17
|
+
maxElapsedMs?: number;
|
|
18
|
+
/** Per-attempt timeout in ms. Default: 20_000 */
|
|
19
|
+
perAttemptTimeoutMs?: number;
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Configuration for the ConvexClient
|
|
8
23
|
*/
|
|
@@ -11,6 +26,15 @@ interface ConvexClientConfig {
|
|
|
11
26
|
baseUrl: string;
|
|
12
27
|
/** Worker shared secret for authentication */
|
|
13
28
|
workerSecret: string;
|
|
29
|
+
/** Optional retry configuration for postDriverView (postProviderAccountStatus does not retry) */
|
|
30
|
+
retry?: RetryConfig;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Options for individual request methods
|
|
34
|
+
*/
|
|
35
|
+
interface RequestOptions {
|
|
36
|
+
/** Override the base URL for this specific request */
|
|
37
|
+
baseUrl?: string;
|
|
14
38
|
}
|
|
15
39
|
/**
|
|
16
40
|
* Error thrown by the ConvexClient when requests fail
|
|
@@ -40,6 +64,10 @@ type ValidationResult<T> = {
|
|
|
40
64
|
* const client = new ConvexClient({
|
|
41
65
|
* baseUrl: process.env.CONVEX_HTTP_URL,
|
|
42
66
|
* workerSecret: process.env.WORKER_SHARED_SECRET,
|
|
67
|
+
* retry: {
|
|
68
|
+
* maxAttempts: 5,
|
|
69
|
+
* perAttemptTimeoutMs: 5000,
|
|
70
|
+
* },
|
|
43
71
|
* });
|
|
44
72
|
*
|
|
45
73
|
* await client.postDriverView(convexUpdate);
|
|
@@ -49,16 +77,24 @@ type ValidationResult<T> = {
|
|
|
49
77
|
declare class ConvexClient {
|
|
50
78
|
private readonly baseUrl;
|
|
51
79
|
private readonly workerSecret;
|
|
80
|
+
private readonly retryConfig;
|
|
52
81
|
constructor(config: ConvexClientConfig);
|
|
53
82
|
/**
|
|
54
83
|
* Posts driver view data to the Convex ingestion endpoint.
|
|
84
|
+
* Uses retry logic with exponential backoff for resilience.
|
|
55
85
|
*/
|
|
56
|
-
postDriverView(data: ConvexUpdateData): Promise<void>;
|
|
86
|
+
postDriverView(data: ConvexUpdateData, options?: RequestOptions): Promise<void>;
|
|
57
87
|
/**
|
|
58
88
|
* Posts provider account status updates to the Convex ingestion endpoint.
|
|
89
|
+
* Does NOT use retry logic (single attempt only).
|
|
59
90
|
*/
|
|
60
|
-
postProviderAccountStatus(data: UpdateScrapeStatusMessage): Promise<void>;
|
|
91
|
+
postProviderAccountStatus(data: UpdateScrapeStatusMessage, options?: RequestOptions): Promise<void>;
|
|
61
92
|
private makeRequest;
|
|
93
|
+
private makeRequestWithRetry;
|
|
94
|
+
private attemptFetch;
|
|
95
|
+
private parseRetryAfter;
|
|
96
|
+
private sleep;
|
|
97
|
+
private isAbortError;
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
/**
|
|
@@ -121,7 +157,7 @@ type RouteName = keyof typeof routes;
|
|
|
121
157
|
|
|
122
158
|
/**
|
|
123
159
|
* Constant-time string comparison to prevent timing attacks.
|
|
124
|
-
* Returns true if the
|
|
160
|
+
* Returns true if the x-worker-secret header matches the expected secret.
|
|
125
161
|
*/
|
|
126
162
|
declare function checkWorkerSecret(req: Request, expectedSecret: string): boolean;
|
|
127
163
|
/**
|
|
@@ -151,4 +187,4 @@ declare const responses: {
|
|
|
151
187
|
readonly serverError: (error?: string) => Response;
|
|
152
188
|
};
|
|
153
189
|
|
|
154
|
-
export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
|
|
190
|
+
export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RequestOptions, type RetryConfig, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,21 @@ import { ConvexUpdateData, UpdateScrapeStatusMessage } from '@findatruck/shared-
|
|
|
3
3
|
export { BatchConvexUpdateData, BatchConvexUpdateSchema, ConvexDriverData, ConvexDriverSchema, ConvexUpdateData, ConvexUpdateSchema, ScrapeStatus, UpdateScrapeStatusMessage, UpdateScrapeStatusMessage as UpdateScrapeStatusMessageType } from '@findatruck/shared-schemas';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for retry behavior
|
|
8
|
+
*/
|
|
9
|
+
interface RetryConfig {
|
|
10
|
+
/** Max number of attempts (first try + retries). Default: 5 */
|
|
11
|
+
maxAttempts?: number;
|
|
12
|
+
/** Base delay in ms before backoff. Default: 250 */
|
|
13
|
+
baseDelayMs?: number;
|
|
14
|
+
/** Max delay cap in ms. Default: 15_000 */
|
|
15
|
+
maxDelayMs?: number;
|
|
16
|
+
/** Max total elapsed time budget in ms (includes all attempts). Default: 60_000 */
|
|
17
|
+
maxElapsedMs?: number;
|
|
18
|
+
/** Per-attempt timeout in ms. Default: 20_000 */
|
|
19
|
+
perAttemptTimeoutMs?: number;
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Configuration for the ConvexClient
|
|
8
23
|
*/
|
|
@@ -11,6 +26,15 @@ interface ConvexClientConfig {
|
|
|
11
26
|
baseUrl: string;
|
|
12
27
|
/** Worker shared secret for authentication */
|
|
13
28
|
workerSecret: string;
|
|
29
|
+
/** Optional retry configuration for postDriverView (postProviderAccountStatus does not retry) */
|
|
30
|
+
retry?: RetryConfig;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Options for individual request methods
|
|
34
|
+
*/
|
|
35
|
+
interface RequestOptions {
|
|
36
|
+
/** Override the base URL for this specific request */
|
|
37
|
+
baseUrl?: string;
|
|
14
38
|
}
|
|
15
39
|
/**
|
|
16
40
|
* Error thrown by the ConvexClient when requests fail
|
|
@@ -40,6 +64,10 @@ type ValidationResult<T> = {
|
|
|
40
64
|
* const client = new ConvexClient({
|
|
41
65
|
* baseUrl: process.env.CONVEX_HTTP_URL,
|
|
42
66
|
* workerSecret: process.env.WORKER_SHARED_SECRET,
|
|
67
|
+
* retry: {
|
|
68
|
+
* maxAttempts: 5,
|
|
69
|
+
* perAttemptTimeoutMs: 5000,
|
|
70
|
+
* },
|
|
43
71
|
* });
|
|
44
72
|
*
|
|
45
73
|
* await client.postDriverView(convexUpdate);
|
|
@@ -49,16 +77,24 @@ type ValidationResult<T> = {
|
|
|
49
77
|
declare class ConvexClient {
|
|
50
78
|
private readonly baseUrl;
|
|
51
79
|
private readonly workerSecret;
|
|
80
|
+
private readonly retryConfig;
|
|
52
81
|
constructor(config: ConvexClientConfig);
|
|
53
82
|
/**
|
|
54
83
|
* Posts driver view data to the Convex ingestion endpoint.
|
|
84
|
+
* Uses retry logic with exponential backoff for resilience.
|
|
55
85
|
*/
|
|
56
|
-
postDriverView(data: ConvexUpdateData): Promise<void>;
|
|
86
|
+
postDriverView(data: ConvexUpdateData, options?: RequestOptions): Promise<void>;
|
|
57
87
|
/**
|
|
58
88
|
* Posts provider account status updates to the Convex ingestion endpoint.
|
|
89
|
+
* Does NOT use retry logic (single attempt only).
|
|
59
90
|
*/
|
|
60
|
-
postProviderAccountStatus(data: UpdateScrapeStatusMessage): Promise<void>;
|
|
91
|
+
postProviderAccountStatus(data: UpdateScrapeStatusMessage, options?: RequestOptions): Promise<void>;
|
|
61
92
|
private makeRequest;
|
|
93
|
+
private makeRequestWithRetry;
|
|
94
|
+
private attemptFetch;
|
|
95
|
+
private parseRetryAfter;
|
|
96
|
+
private sleep;
|
|
97
|
+
private isAbortError;
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
/**
|
|
@@ -121,7 +157,7 @@ type RouteName = keyof typeof routes;
|
|
|
121
157
|
|
|
122
158
|
/**
|
|
123
159
|
* Constant-time string comparison to prevent timing attacks.
|
|
124
|
-
* Returns true if the
|
|
160
|
+
* Returns true if the x-worker-secret header matches the expected secret.
|
|
125
161
|
*/
|
|
126
162
|
declare function checkWorkerSecret(req: Request, expectedSecret: string): boolean;
|
|
127
163
|
/**
|
|
@@ -151,4 +187,4 @@ declare const responses: {
|
|
|
151
187
|
readonly serverError: (error?: string) => Response;
|
|
152
188
|
};
|
|
153
189
|
|
|
154
|
-
export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
|
|
190
|
+
export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RequestOptions, type RetryConfig, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
|
package/dist/index.js
CHANGED
|
@@ -32,11 +32,19 @@ var ConvexClient = class {
|
|
|
32
32
|
constructor(config) {
|
|
33
33
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
34
34
|
this.workerSecret = config.workerSecret;
|
|
35
|
+
this.retryConfig = {
|
|
36
|
+
maxAttempts: config.retry?.maxAttempts ?? 5,
|
|
37
|
+
baseDelayMs: config.retry?.baseDelayMs ?? 250,
|
|
38
|
+
maxDelayMs: config.retry?.maxDelayMs ?? 15e3,
|
|
39
|
+
maxElapsedMs: config.retry?.maxElapsedMs ?? 6e4,
|
|
40
|
+
perAttemptTimeoutMs: config.retry?.perAttemptTimeoutMs ?? 2e4
|
|
41
|
+
};
|
|
35
42
|
}
|
|
36
43
|
/**
|
|
37
44
|
* Posts driver view data to the Convex ingestion endpoint.
|
|
45
|
+
* Uses retry logic with exponential backoff for resilience.
|
|
38
46
|
*/
|
|
39
|
-
async postDriverView(data) {
|
|
47
|
+
async postDriverView(data, options) {
|
|
40
48
|
const route = routes.ingestDriverView;
|
|
41
49
|
const parseResult = route.requestSchema.safeParse(data);
|
|
42
50
|
if (!parseResult.success) {
|
|
@@ -45,12 +53,14 @@ var ConvexClient = class {
|
|
|
45
53
|
route.path
|
|
46
54
|
);
|
|
47
55
|
}
|
|
48
|
-
|
|
56
|
+
const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
|
|
57
|
+
await this.makeRequestWithRetry(effectiveBaseUrl, route.path, route.method, data);
|
|
49
58
|
}
|
|
50
59
|
/**
|
|
51
60
|
* Posts provider account status updates to the Convex ingestion endpoint.
|
|
61
|
+
* Does NOT use retry logic (single attempt only).
|
|
52
62
|
*/
|
|
53
|
-
async postProviderAccountStatus(data) {
|
|
63
|
+
async postProviderAccountStatus(data, options) {
|
|
54
64
|
const route = routes.ingestProviderAccountStatus;
|
|
55
65
|
const parseResult = route.requestSchema.safeParse(data);
|
|
56
66
|
if (!parseResult.success) {
|
|
@@ -59,15 +69,16 @@ var ConvexClient = class {
|
|
|
59
69
|
route.path
|
|
60
70
|
);
|
|
61
71
|
}
|
|
62
|
-
|
|
72
|
+
const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
|
|
73
|
+
await this.makeRequest(effectiveBaseUrl, route.path, route.method, data);
|
|
63
74
|
}
|
|
64
|
-
async makeRequest(path, method, body) {
|
|
65
|
-
const url = `${
|
|
75
|
+
async makeRequest(baseUrl, path, method, body) {
|
|
76
|
+
const url = `${baseUrl}${path}`;
|
|
66
77
|
const response = await fetch(url, {
|
|
67
78
|
method,
|
|
68
79
|
headers: {
|
|
69
80
|
"Content-Type": "application/json",
|
|
70
|
-
"
|
|
81
|
+
"x-worker-secret": this.workerSecret
|
|
71
82
|
},
|
|
72
83
|
body: JSON.stringify(body)
|
|
73
84
|
});
|
|
@@ -86,21 +97,151 @@ var ConvexClient = class {
|
|
|
86
97
|
);
|
|
87
98
|
}
|
|
88
99
|
}
|
|
100
|
+
async makeRequestWithRetry(baseUrl, path, method, body) {
|
|
101
|
+
const url = `${baseUrl}${path}`;
|
|
102
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, maxElapsedMs, perAttemptTimeoutMs } = this.retryConfig;
|
|
103
|
+
const retryOnStatuses = [429, 500, 502, 503, 504];
|
|
104
|
+
const startedAt = Date.now();
|
|
105
|
+
let attempt = 0;
|
|
106
|
+
const calcBackoff = (retriesSoFar) => Math.floor(Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** retriesSoFar));
|
|
107
|
+
while (true) {
|
|
108
|
+
attempt += 1;
|
|
109
|
+
if (Date.now() - startedAt > maxElapsedMs) {
|
|
110
|
+
throw new ConvexClientError(
|
|
111
|
+
`Request to ${path} exceeded max elapsed time ${maxElapsedMs}ms`,
|
|
112
|
+
path
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
let response;
|
|
116
|
+
let error;
|
|
117
|
+
try {
|
|
118
|
+
response = await this.attemptFetch(url, method, body, perAttemptTimeoutMs);
|
|
119
|
+
if (response.ok) return;
|
|
120
|
+
if (!retryOnStatuses.includes(response.status)) {
|
|
121
|
+
let responseBody;
|
|
122
|
+
try {
|
|
123
|
+
responseBody = await response.json();
|
|
124
|
+
} catch {
|
|
125
|
+
responseBody = await response.text();
|
|
126
|
+
}
|
|
127
|
+
throw new ConvexClientError(
|
|
128
|
+
`Request to ${path} failed with status ${response.status}`,
|
|
129
|
+
path,
|
|
130
|
+
response.status,
|
|
131
|
+
responseBody
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
error = e;
|
|
136
|
+
if (e instanceof ConvexClientError) throw e;
|
|
137
|
+
if (this.isAbortError(e)) throw e;
|
|
138
|
+
}
|
|
139
|
+
if (attempt >= maxAttempts) {
|
|
140
|
+
if (response) {
|
|
141
|
+
let responseBody;
|
|
142
|
+
try {
|
|
143
|
+
responseBody = await response.json();
|
|
144
|
+
} catch {
|
|
145
|
+
responseBody = await response.text();
|
|
146
|
+
}
|
|
147
|
+
throw new ConvexClientError(
|
|
148
|
+
`Request to ${path} failed after ${attempt} attempts with status ${response.status}`,
|
|
149
|
+
path,
|
|
150
|
+
response.status,
|
|
151
|
+
responseBody
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (error instanceof Error) throw error;
|
|
155
|
+
throw new ConvexClientError(
|
|
156
|
+
`Request to ${path} failed without response after ${attempt} attempts`,
|
|
157
|
+
path
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
let delayMs = 0;
|
|
161
|
+
const retryAfter = response?.headers.get("retry-after");
|
|
162
|
+
if (retryAfter) {
|
|
163
|
+
const parsed = this.parseRetryAfter(retryAfter);
|
|
164
|
+
if (parsed != null) delayMs = Math.min(parsed, maxDelayMs);
|
|
165
|
+
}
|
|
166
|
+
if (delayMs === 0) {
|
|
167
|
+
delayMs = calcBackoff(attempt - 1);
|
|
168
|
+
}
|
|
169
|
+
const elapsed = Date.now() - startedAt;
|
|
170
|
+
if (elapsed + delayMs > maxElapsedMs) {
|
|
171
|
+
delayMs = Math.max(0, maxElapsedMs - elapsed);
|
|
172
|
+
if (delayMs === 0) {
|
|
173
|
+
if (response) {
|
|
174
|
+
throw new ConvexClientError(
|
|
175
|
+
`Request to ${path} time budget exhausted before retry`,
|
|
176
|
+
path,
|
|
177
|
+
response.status
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (error instanceof Error) throw error;
|
|
181
|
+
throw new ConvexClientError(
|
|
182
|
+
`Request to ${path} time budget exhausted before retry`,
|
|
183
|
+
path
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
await this.sleep(delayMs);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async attemptFetch(url, method, body, perAttemptTimeoutMs) {
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const timeoutId = perAttemptTimeoutMs > 0 ? setTimeout(() => {
|
|
193
|
+
controller.abort(new Error(`per-attempt timeout ${perAttemptTimeoutMs}ms`));
|
|
194
|
+
}, perAttemptTimeoutMs) : null;
|
|
195
|
+
try {
|
|
196
|
+
return await fetch(url, {
|
|
197
|
+
method,
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
"x-worker-secret": this.workerSecret
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify(body),
|
|
203
|
+
signal: controller.signal
|
|
204
|
+
});
|
|
205
|
+
} finally {
|
|
206
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
parseRetryAfter(value) {
|
|
210
|
+
const secs = Number(value);
|
|
211
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
212
|
+
const dateMs = Date.parse(value);
|
|
213
|
+
if (!Number.isNaN(dateMs)) {
|
|
214
|
+
const delta = dateMs - Date.now();
|
|
215
|
+
return Math.max(0, delta);
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
sleep(ms) {
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
if (ms <= 0) {
|
|
222
|
+
resolve();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
setTimeout(resolve, ms);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
isAbortError(err) {
|
|
229
|
+
return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && /abort/i.test(err.name + " " + err.message);
|
|
230
|
+
}
|
|
89
231
|
};
|
|
90
232
|
|
|
91
233
|
// src/validation.ts
|
|
92
234
|
function checkWorkerSecret(req, expectedSecret) {
|
|
93
|
-
const
|
|
94
|
-
if (!
|
|
235
|
+
const secretHeader = req.headers.get("x-worker-secret");
|
|
236
|
+
if (!secretHeader) {
|
|
95
237
|
return false;
|
|
96
238
|
}
|
|
97
|
-
|
|
98
|
-
if (authHeader.length !== expectedValue.length) {
|
|
239
|
+
if (secretHeader.length !== expectedSecret.length) {
|
|
99
240
|
return false;
|
|
100
241
|
}
|
|
101
242
|
let result = 0;
|
|
102
|
-
for (let i = 0; i <
|
|
103
|
-
result |=
|
|
243
|
+
for (let i = 0; i < secretHeader.length; i++) {
|
|
244
|
+
result |= secretHeader.charCodeAt(i) ^ expectedSecret.charCodeAt(i);
|
|
104
245
|
}
|
|
105
246
|
return result === 0;
|
|
106
247
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findatruck/convex-client",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test:watch": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@findatruck/shared-schemas": "^2.
|
|
26
|
+
"@findatruck/shared-schemas": "^2.10.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"zod": "^4.1.11"
|