@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.
@@ -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;AAkDD;;;;;;;;;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;YA0FV,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"}
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"}
@@ -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 operationName = opts?.operationName ?? `HTTP ${method} ${url}`;
105
- return traceAsync(operationName, async (span) => {
106
- span.setAttribute(ATTR_HTTP_REQUEST_METHOD, method);
107
- span.setAttribute(ATTR_URL_FULL, url);
108
- const controller = new AbortController();
109
- const timeoutMs = opts?.timeout ?? this.timeout;
110
- let timer;
111
- if (timeoutMs > 0) {
112
- timer = setTimeout(() => controller.abort(), timeoutMs);
113
- }
114
- const combinedSignal = opts?.signal
115
- ? AbortSignal.any([opts.signal, controller.signal])
116
- : controller.signal;
117
- try {
118
- const start = performance.now();
119
- const response = await this.fetchFn(url, {
120
- method,
121
- headers,
122
- body,
123
- signal: combinedSignal,
124
- ...(redirect ? { redirect } : {}),
125
- });
126
- span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
127
- getHttpClientRequestTotal().add(1, {
128
- 'http.request.method': method,
129
- 'http.response.status_code': response.status,
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
- 'error.type': 'Timeout',
150
+ 'http.response.status_code': response.status,
144
151
  });
145
- const timeoutError = new APIError(0, `Request timed out after ${timeoutMs}ms: ${method} ${url}`);
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
- 'error.type': error instanceof Error ? error.name : 'Unknown',
154
+ 'http.response.status_code': response.status,
153
155
  });
156
+ return await consume(response, span);
154
157
  }
155
- this.emitRequestError(method, url, error instanceof Error ? error.message : String(error), error instanceof APIError && error.status > 0 ? error.status : undefined);
156
- throw error;
157
- }
158
- finally {
159
- if (timer)
160
- clearTimeout(timer);
161
- }
162
- }, { kind: SpanKind.CLIENT });
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.6",
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.6",
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.6",
86
- "@gobing-ai/ts-runtime": "^0.4.6",
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.6",
120
- "@gobing-ai/ts-runtime": "^0.4.6",
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 operationName = opts?.operationName ?? `HTTP ${method} ${url}`;
172
-
173
- return traceAsync(
174
- operationName,
175
- async (span: Span) => {
176
- span.setAttribute(ATTR_HTTP_REQUEST_METHOD, method);
177
- span.setAttribute(ATTR_URL_FULL, url);
178
-
179
- const controller = new AbortController();
180
- const timeoutMs = opts?.timeout ?? this.timeout;
181
- let timer: ReturnType<typeof setTimeout> | undefined;
182
-
183
- if (timeoutMs > 0) {
184
- timer = setTimeout(() => controller.abort(), timeoutMs);
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
- const combinedSignal = opts?.signal
188
- ? AbortSignal.any([opts.signal, controller.signal])
189
- : controller.signal;
208
+ const combinedSignal = opts?.signal
209
+ ? AbortSignal.any([opts.signal, controller.signal])
210
+ : controller.signal;
190
211
 
191
- try {
192
- const start = performance.now();
212
+ try {
213
+ const start = performance.now();
193
214
 
194
- const response = await this.fetchFn(url, {
195
- method,
196
- headers,
197
- body,
198
- signal: combinedSignal,
199
- ...(redirect ? { redirect } : {}),
200
- });
215
+ const response = await this.fetchFn(url, {
216
+ method,
217
+ headers,
218
+ body,
219
+ signal: combinedSignal,
220
+ ...(redirect ? { redirect } : {}),
221
+ });
201
222
 
202
- span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
223
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
203
224
 
204
- getHttpClientRequestTotal().add(1, {
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
- 'error.type': 'Timeout',
227
+ 'http.response.status_code': response.status,
221
228
  });
222
- const timeoutError = new APIError(
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
- 'error.type': error instanceof Error ? error.name : 'Unknown',
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
- this.emitRequestError(
237
- method,
238
- url,
239
- error instanceof Error ? error.message : String(error),
240
- error instanceof APIError && error.status > 0 ? error.status : undefined,
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
+ }