@gobing-ai/ts-infra 0.4.6 → 0.4.8
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/api-client.d.ts.map +1 -1
- package/dist/api-client.js +81 -54
- package/dist/job-queue/db-job-queue.js +25 -7
- package/package.json +6 -6
- package/src/api-client.ts +95 -69
- package/src/job-queue/db-job-queue.ts +28 -7
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAUhD,0GAA0G;AAC1G,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACtC;AACD,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,mHAAmH;AACnH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AACD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;aAEX,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAKnC;
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAUhD,0GAA0G;AAC1G,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACtC;AACD,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,mHAAmH;AACnH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB;AACD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,KAAK;aAEX,MAAM,EAAE,MAAM;aACd,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;CAKnC;AAoED;;;;;;;;;GASG;AACH,qBAAa,SAAS;IAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;gBAEnD,MAAM,EAAE,eAAe;IAQnC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;YACW,UAAU;YAkGV,OAAO;IAmCrB;;;;;;;OAOG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IA+C3G,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxE,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIvE,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAIzE,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;CAGnE"}
|
package/dist/api-client.js
CHANGED
|
@@ -19,6 +19,24 @@ export class APIError extends Error {
|
|
|
19
19
|
this.name = 'APIError';
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
function sanitizeUrlForObservability(url) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(url);
|
|
25
|
+
parsed.username = '';
|
|
26
|
+
parsed.password = '';
|
|
27
|
+
parsed.search = '';
|
|
28
|
+
parsed.hash = '';
|
|
29
|
+
return parsed.toString();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return url.split(/[?#]/, 1)[0] ?? '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function errorForObservability(error) {
|
|
36
|
+
if (error instanceof APIError)
|
|
37
|
+
return error.status > 0 ? `HTTP ${error.status}` : 'Request timeout';
|
|
38
|
+
return error instanceof Error ? error.name : 'Unknown error';
|
|
39
|
+
}
|
|
22
40
|
// ── Body reading ────────────────────────────────────────────────────
|
|
23
41
|
/**
|
|
24
42
|
* Read a response body capped at `maxResponseBytes` (UTF-8 bytes). Returns the
|
|
@@ -101,65 +119,74 @@ export class APIClient {
|
|
|
101
119
|
* mapping. `consume` owns response handling (parse/throw policy).
|
|
102
120
|
*/
|
|
103
121
|
async runRequest(method, url, headers, body, opts, redirect, consume) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
getHttpClientRequestDuration().record(performance.now() - start, {
|
|
132
|
-
'http.request.method': method,
|
|
133
|
-
'http.response.status_code': response.status,
|
|
134
|
-
});
|
|
135
|
-
return await consume(response, span);
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
// Timeout (our own AbortController fired) — but never relabel a
|
|
139
|
-
// caller-initiated abort via opts.signal as a timeout.
|
|
140
|
-
if (error instanceof DOMException && error.name === 'AbortError' && !opts?.signal?.aborted) {
|
|
141
|
-
getHttpClientRequestErrors().add(1, {
|
|
122
|
+
const observableUrl = sanitizeUrlForObservability(url);
|
|
123
|
+
const operationName = opts?.operationName ?? `HTTP ${method} ${observableUrl}`;
|
|
124
|
+
let callerError;
|
|
125
|
+
try {
|
|
126
|
+
return await traceAsync(operationName, async (span) => {
|
|
127
|
+
span.setAttribute(ATTR_HTTP_REQUEST_METHOD, method);
|
|
128
|
+
span.setAttribute(ATTR_URL_FULL, observableUrl);
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeoutMs = opts?.timeout ?? this.timeout;
|
|
131
|
+
let timer;
|
|
132
|
+
if (timeoutMs > 0) {
|
|
133
|
+
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
134
|
+
}
|
|
135
|
+
const combinedSignal = opts?.signal
|
|
136
|
+
? AbortSignal.any([opts.signal, controller.signal])
|
|
137
|
+
: controller.signal;
|
|
138
|
+
try {
|
|
139
|
+
const start = performance.now();
|
|
140
|
+
const response = await this.fetchFn(url, {
|
|
141
|
+
method,
|
|
142
|
+
headers,
|
|
143
|
+
body,
|
|
144
|
+
signal: combinedSignal,
|
|
145
|
+
...(redirect ? { redirect } : {}),
|
|
146
|
+
});
|
|
147
|
+
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
148
|
+
getHttpClientRequestTotal().add(1, {
|
|
142
149
|
'http.request.method': method,
|
|
143
|
-
'
|
|
150
|
+
'http.response.status_code': response.status,
|
|
144
151
|
});
|
|
145
|
-
|
|
146
|
-
this.emitRequestError(method, url, timeoutError.message);
|
|
147
|
-
throw timeoutError;
|
|
148
|
-
}
|
|
149
|
-
if (!(error instanceof APIError)) {
|
|
150
|
-
getHttpClientRequestErrors().add(1, {
|
|
152
|
+
getHttpClientRequestDuration().record(performance.now() - start, {
|
|
151
153
|
'http.request.method': method,
|
|
152
|
-
'
|
|
154
|
+
'http.response.status_code': response.status,
|
|
153
155
|
});
|
|
156
|
+
return await consume(response, span);
|
|
154
157
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
catch (error) {
|
|
159
|
+
// Timeout (our own AbortController fired) — but never relabel a
|
|
160
|
+
// caller-initiated abort via opts.signal as a timeout.
|
|
161
|
+
if (error instanceof DOMException && error.name === 'AbortError' && !opts?.signal?.aborted) {
|
|
162
|
+
getHttpClientRequestErrors().add(1, {
|
|
163
|
+
'http.request.method': method,
|
|
164
|
+
'error.type': 'Timeout',
|
|
165
|
+
});
|
|
166
|
+
const timeoutError = new APIError(0, `Request timed out after ${timeoutMs}ms: ${method} ${url}`);
|
|
167
|
+
this.emitRequestError(method, observableUrl, errorForObservability(timeoutError));
|
|
168
|
+
callerError = timeoutError;
|
|
169
|
+
throw new Error(errorForObservability(timeoutError));
|
|
170
|
+
}
|
|
171
|
+
if (!(error instanceof APIError)) {
|
|
172
|
+
getHttpClientRequestErrors().add(1, {
|
|
173
|
+
'http.request.method': method,
|
|
174
|
+
'error.type': error instanceof Error ? error.name : 'Unknown',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
this.emitRequestError(method, observableUrl, errorForObservability(error), error instanceof APIError && error.status > 0 ? error.status : undefined);
|
|
178
|
+
callerError = error;
|
|
179
|
+
throw new Error(errorForObservability(error));
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
if (timer)
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
}, { kind: SpanKind.CLIENT });
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw callerError ?? error;
|
|
189
|
+
}
|
|
163
190
|
}
|
|
164
191
|
async request(method, path, body, opts) {
|
|
165
192
|
const url = this.buildUrl(path);
|
|
@@ -52,13 +52,13 @@ export class DBQueueConsumer {
|
|
|
52
52
|
inFlight = 0;
|
|
53
53
|
constructor(dao, config = {}) {
|
|
54
54
|
this.dao = dao;
|
|
55
|
-
this.pollInterval = config.pollInterval ?? 1_000;
|
|
56
|
-
this.batchSize = config.batchSize ?? 10;
|
|
57
|
-
this.maxConcurrency = config.maxConcurrency ?? this.batchSize;
|
|
58
|
-
this.visibilityTimeout = config.visibilityTimeout ?? 30_000;
|
|
59
|
-
this.baseDelay = config.baseDelay ?? 1_000;
|
|
60
|
-
this.maxDelay = config.maxDelay ?? 60_000;
|
|
61
|
-
this.drainTimeoutMs = config.drainTimeoutMs ?? 30_000;
|
|
55
|
+
this.pollInterval = nonNegativeFiniteConfig('pollInterval', config.pollInterval ?? 1_000);
|
|
56
|
+
this.batchSize = positiveIntegerConfig('batchSize', config.batchSize ?? 10);
|
|
57
|
+
this.maxConcurrency = positiveIntegerConfig('maxConcurrency', config.maxConcurrency ?? this.batchSize);
|
|
58
|
+
this.visibilityTimeout = positiveFiniteConfig('visibilityTimeout', config.visibilityTimeout ?? 30_000);
|
|
59
|
+
this.baseDelay = nonNegativeFiniteConfig('baseDelay', config.baseDelay ?? 1_000);
|
|
60
|
+
this.maxDelay = nonNegativeFiniteConfig('maxDelay', config.maxDelay ?? 60_000);
|
|
61
|
+
this.drainTimeoutMs = nonNegativeFiniteConfig('drainTimeoutMs', config.drainTimeoutMs ?? 30_000);
|
|
62
62
|
this.events = config.events;
|
|
63
63
|
}
|
|
64
64
|
register(type, handler) {
|
|
@@ -215,3 +215,21 @@ function toJob(record) {
|
|
|
215
215
|
function sleep(ms) {
|
|
216
216
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
217
217
|
}
|
|
218
|
+
function positiveIntegerConfig(name, value) {
|
|
219
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
220
|
+
throw new RangeError(`Queue consumer ${name} must be a positive integer; received ${value}`);
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function positiveFiniteConfig(name, value) {
|
|
225
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
226
|
+
throw new RangeError(`Queue consumer ${name} must be a positive finite number; received ${value}`);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function nonNegativeFiniteConfig(name, value) {
|
|
231
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
232
|
+
throw new RangeError(`Queue consumer ${name} must be a non-negative finite number; received ${value}`);
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -78,12 +78,12 @@
|
|
|
78
78
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-infra-v<version> && git push --tags' && exit 1"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
81
|
+
"@gobing-ai/ts-utils": "^0.4.8",
|
|
82
82
|
"@logtape/logtape": "^2.0.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
86
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
85
|
+
"@gobing-ai/ts-db": "^0.4.8",
|
|
86
|
+
"@gobing-ai/ts-runtime": "^0.4.8",
|
|
87
87
|
"@opentelemetry/api": "^1.9.0",
|
|
88
88
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
89
89
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -116,8 +116,8 @@
|
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
120
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
119
|
+
"@gobing-ai/ts-db": "^0.4.8",
|
|
120
|
+
"@gobing-ai/ts-runtime": "^0.4.8",
|
|
121
121
|
"@types/bun": "1.3.14",
|
|
122
122
|
"@opentelemetry/api": "^1.9.0",
|
|
123
123
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
package/src/api-client.ts
CHANGED
|
@@ -68,6 +68,24 @@ export class APIError extends Error {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function sanitizeUrlForObservability(url: string): string {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = new URL(url);
|
|
74
|
+
parsed.username = '';
|
|
75
|
+
parsed.password = '';
|
|
76
|
+
parsed.search = '';
|
|
77
|
+
parsed.hash = '';
|
|
78
|
+
return parsed.toString();
|
|
79
|
+
} catch {
|
|
80
|
+
return url.split(/[?#]/, 1)[0] ?? '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function errorForObservability(error: unknown): string {
|
|
85
|
+
if (error instanceof APIError) return error.status > 0 ? `HTTP ${error.status}` : 'Request timeout';
|
|
86
|
+
return error instanceof Error ? error.name : 'Unknown error';
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
// ── Body reading ────────────────────────────────────────────────────
|
|
72
90
|
|
|
73
91
|
/**
|
|
@@ -168,85 +186,93 @@ export class APIClient {
|
|
|
168
186
|
redirect: RawRequestOptions['redirect'] | undefined,
|
|
169
187
|
consume: (response: Response, span: Span) => Promise<T>,
|
|
170
188
|
): Promise<T> {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
timer
|
|
185
|
-
|
|
189
|
+
const observableUrl = sanitizeUrlForObservability(url);
|
|
190
|
+
const operationName = opts?.operationName ?? `HTTP ${method} ${observableUrl}`;
|
|
191
|
+
let callerError: unknown;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return await traceAsync(
|
|
195
|
+
operationName,
|
|
196
|
+
async (span: Span) => {
|
|
197
|
+
span.setAttribute(ATTR_HTTP_REQUEST_METHOD, method);
|
|
198
|
+
span.setAttribute(ATTR_URL_FULL, observableUrl);
|
|
199
|
+
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const timeoutMs = opts?.timeout ?? this.timeout;
|
|
202
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
203
|
+
|
|
204
|
+
if (timeoutMs > 0) {
|
|
205
|
+
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
206
|
+
}
|
|
186
207
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
const combinedSignal = opts?.signal
|
|
209
|
+
? AbortSignal.any([opts.signal, controller.signal])
|
|
210
|
+
: controller.signal;
|
|
190
211
|
|
|
191
|
-
|
|
192
|
-
|
|
212
|
+
try {
|
|
213
|
+
const start = performance.now();
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
const response = await this.fetchFn(url, {
|
|
216
|
+
method,
|
|
217
|
+
headers,
|
|
218
|
+
body,
|
|
219
|
+
signal: combinedSignal,
|
|
220
|
+
...(redirect ? { redirect } : {}),
|
|
221
|
+
});
|
|
201
222
|
|
|
202
|
-
|
|
223
|
+
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
203
224
|
|
|
204
|
-
|
|
205
|
-
'http.request.method': method,
|
|
206
|
-
'http.response.status_code': response.status,
|
|
207
|
-
});
|
|
208
|
-
getHttpClientRequestDuration().record(performance.now() - start, {
|
|
209
|
-
'http.request.method': method,
|
|
210
|
-
'http.response.status_code': response.status,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
return await consume(response, span);
|
|
214
|
-
} catch (error) {
|
|
215
|
-
// Timeout (our own AbortController fired) — but never relabel a
|
|
216
|
-
// caller-initiated abort via opts.signal as a timeout.
|
|
217
|
-
if (error instanceof DOMException && error.name === 'AbortError' && !opts?.signal?.aborted) {
|
|
218
|
-
getHttpClientRequestErrors().add(1, {
|
|
225
|
+
getHttpClientRequestTotal().add(1, {
|
|
219
226
|
'http.request.method': method,
|
|
220
|
-
'
|
|
227
|
+
'http.response.status_code': response.status,
|
|
221
228
|
});
|
|
222
|
-
|
|
223
|
-
0,
|
|
224
|
-
`Request timed out after ${timeoutMs}ms: ${method} ${url}`,
|
|
225
|
-
);
|
|
226
|
-
this.emitRequestError(method, url, timeoutError.message);
|
|
227
|
-
throw timeoutError;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!(error instanceof APIError)) {
|
|
231
|
-
getHttpClientRequestErrors().add(1, {
|
|
229
|
+
getHttpClientRequestDuration().record(performance.now() - start, {
|
|
232
230
|
'http.request.method': method,
|
|
233
|
-
'
|
|
231
|
+
'http.response.status_code': response.status,
|
|
234
232
|
});
|
|
233
|
+
|
|
234
|
+
return await consume(response, span);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Timeout (our own AbortController fired) — but never relabel a
|
|
237
|
+
// caller-initiated abort via opts.signal as a timeout.
|
|
238
|
+
if (error instanceof DOMException && error.name === 'AbortError' && !opts?.signal?.aborted) {
|
|
239
|
+
getHttpClientRequestErrors().add(1, {
|
|
240
|
+
'http.request.method': method,
|
|
241
|
+
'error.type': 'Timeout',
|
|
242
|
+
});
|
|
243
|
+
const timeoutError = new APIError(
|
|
244
|
+
0,
|
|
245
|
+
`Request timed out after ${timeoutMs}ms: ${method} ${url}`,
|
|
246
|
+
);
|
|
247
|
+
this.emitRequestError(method, observableUrl, errorForObservability(timeoutError));
|
|
248
|
+
callerError = timeoutError;
|
|
249
|
+
throw new Error(errorForObservability(timeoutError));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!(error instanceof APIError)) {
|
|
253
|
+
getHttpClientRequestErrors().add(1, {
|
|
254
|
+
'http.request.method': method,
|
|
255
|
+
'error.type': error instanceof Error ? error.name : 'Unknown',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
this.emitRequestError(
|
|
259
|
+
method,
|
|
260
|
+
observableUrl,
|
|
261
|
+
errorForObservability(error),
|
|
262
|
+
error instanceof APIError && error.status > 0 ? error.status : undefined,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
callerError = error;
|
|
266
|
+
throw new Error(errorForObservability(error));
|
|
267
|
+
} finally {
|
|
268
|
+
if (timer) clearTimeout(timer);
|
|
235
269
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
throw error;
|
|
244
|
-
} finally {
|
|
245
|
-
if (timer) clearTimeout(timer);
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
{ kind: SpanKind.CLIENT },
|
|
249
|
-
);
|
|
270
|
+
},
|
|
271
|
+
{ kind: SpanKind.CLIENT },
|
|
272
|
+
);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw callerError ?? error;
|
|
275
|
+
}
|
|
250
276
|
}
|
|
251
277
|
|
|
252
278
|
private async request<T>(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
|
|
@@ -66,13 +66,13 @@ export class DBQueueConsumer<T = unknown> implements QueueConsumer<T> {
|
|
|
66
66
|
private readonly dao: QueueJobDao,
|
|
67
67
|
config: QueueConsumerConfig = {},
|
|
68
68
|
) {
|
|
69
|
-
this.pollInterval = config.pollInterval ?? 1_000;
|
|
70
|
-
this.batchSize = config.batchSize ?? 10;
|
|
71
|
-
this.maxConcurrency = config.maxConcurrency ?? this.batchSize;
|
|
72
|
-
this.visibilityTimeout = config.visibilityTimeout ?? 30_000;
|
|
73
|
-
this.baseDelay = config.baseDelay ?? 1_000;
|
|
74
|
-
this.maxDelay = config.maxDelay ?? 60_000;
|
|
75
|
-
this.drainTimeoutMs = config.drainTimeoutMs ?? 30_000;
|
|
69
|
+
this.pollInterval = nonNegativeFiniteConfig('pollInterval', config.pollInterval ?? 1_000);
|
|
70
|
+
this.batchSize = positiveIntegerConfig('batchSize', config.batchSize ?? 10);
|
|
71
|
+
this.maxConcurrency = positiveIntegerConfig('maxConcurrency', config.maxConcurrency ?? this.batchSize);
|
|
72
|
+
this.visibilityTimeout = positiveFiniteConfig('visibilityTimeout', config.visibilityTimeout ?? 30_000);
|
|
73
|
+
this.baseDelay = nonNegativeFiniteConfig('baseDelay', config.baseDelay ?? 1_000);
|
|
74
|
+
this.maxDelay = nonNegativeFiniteConfig('maxDelay', config.maxDelay ?? 60_000);
|
|
75
|
+
this.drainTimeoutMs = nonNegativeFiniteConfig('drainTimeoutMs', config.drainTimeoutMs ?? 30_000);
|
|
76
76
|
this.events = config.events;
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -243,3 +243,24 @@ function toJob<T>(record: QueueJobRecord): Job<T> {
|
|
|
243
243
|
function sleep(ms: number): Promise<void> {
|
|
244
244
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
245
245
|
}
|
|
246
|
+
|
|
247
|
+
function positiveIntegerConfig(name: string, value: number): number {
|
|
248
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
249
|
+
throw new RangeError(`Queue consumer ${name} must be a positive integer; received ${value}`);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function positiveFiniteConfig(name: string, value: number): number {
|
|
255
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
256
|
+
throw new RangeError(`Queue consumer ${name} must be a positive finite number; received ${value}`);
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function nonNegativeFiniteConfig(name: string, value: number): number {
|
|
262
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
263
|
+
throw new RangeError(`Queue consumer ${name} must be a non-negative finite number; received ${value}`);
|
|
264
|
+
}
|
|
265
|
+
return value;
|
|
266
|
+
}
|