@angular-helpers/worker-http 0.5.0 → 0.7.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/README.md +106 -2
- package/fesm2022/angular-helpers-worker-http-backend.mjs +147 -12
- package/fesm2022/angular-helpers-worker-http-crypto.mjs +5 -1
- package/fesm2022/angular-helpers-worker-http-interceptors.mjs +13 -6
- package/fesm2022/angular-helpers-worker-http-transport.mjs +150 -10
- package/fesm2022/angular-helpers-worker-http.mjs +0 -17
- package/package.json +1 -1
- package/types/angular-helpers-worker-http-backend.d.ts +122 -3
- package/types/angular-helpers-worker-http-transport.d.ts +67 -5
- package/types/angular-helpers-worker-http.d.ts +1 -15
package/README.md
CHANGED
|
@@ -67,9 +67,40 @@ transport.terminate();
|
|
|
67
67
|
**Features:**
|
|
68
68
|
|
|
69
69
|
- Round-robin pool (`maxInstances`) for parallel request handling
|
|
70
|
-
- Request cancellation via `AbortController` in the worker
|
|
71
|
-
- Automatic `Transferable` detection for zero-copy `ArrayBuffer` transfer
|
|
72
70
|
- Lazy worker instantiation — no worker created until first request
|
|
71
|
+
- **Cancellation that actually aborts `fetch()`** — unsubscribing posts a
|
|
72
|
+
cancel message; the worker-side message loop threads an `AbortSignal` all
|
|
73
|
+
the way into `fetch()` so the in-flight HTTP request is truly aborted
|
|
74
|
+
- **Per-request timeout** (default `30_000` ms) via `requestTimeout`; errors
|
|
75
|
+
with `WorkerHttpTimeoutError` and sends a cancel message to the worker.
|
|
76
|
+
Set to `0` to disable.
|
|
77
|
+
- **Opt-in transferable detection** via `transferDetection: 'auto'` — passes
|
|
78
|
+
detected `ArrayBuffer` / `MessagePort` / `ImageBitmap` /
|
|
79
|
+
`OffscreenCanvas` / streams as the transfer list of `postMessage`, enabling
|
|
80
|
+
zero-copy transfer of large buffers. Default is `'none'` to preserve the
|
|
81
|
+
caller's access to the original data after post.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import {
|
|
85
|
+
createWorkerTransport,
|
|
86
|
+
WorkerHttpTimeoutError,
|
|
87
|
+
} from '@angular-helpers/worker-http/transport';
|
|
88
|
+
|
|
89
|
+
const transport = createWorkerTransport({
|
|
90
|
+
workerUrl: new URL('./workers/api.worker', import.meta.url),
|
|
91
|
+
maxInstances: 2,
|
|
92
|
+
requestTimeout: 10_000, // override default 30 s
|
|
93
|
+
transferDetection: 'auto', // zero-copy ArrayBuffer at postMessage
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
transport.execute(request).subscribe({
|
|
97
|
+
error: (err) => {
|
|
98
|
+
if (err instanceof WorkerHttpTimeoutError) {
|
|
99
|
+
// dedicated timeout handling
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
73
104
|
|
|
74
105
|
---
|
|
75
106
|
|
|
@@ -406,6 +437,79 @@ manual control.
|
|
|
406
437
|
|
|
407
438
|
---
|
|
408
439
|
|
|
440
|
+
## Telemetry
|
|
441
|
+
|
|
442
|
+
Main-thread extension point for APM / metrics. `withTelemetry(...)` registers
|
|
443
|
+
a subscriber that fires synchronously at three lifecycle points of every
|
|
444
|
+
request handled by `WorkerHttpBackend`:
|
|
445
|
+
|
|
446
|
+
- **`onRequest`** — after worker resolution, before dispatch
|
|
447
|
+
- **`onResponse`** — when a successful response is emitted
|
|
448
|
+
- **`onError`** — when the request fails (transport or non-2xx)
|
|
449
|
+
|
|
450
|
+
Events share a `requestId` so you can correlate the three emissions for one
|
|
451
|
+
request. `transport` is `'worker'` or `'fallback-fetch'` (SSR /
|
|
452
|
+
no-route). Errors in your subscriber are caught and logged — they **never**
|
|
453
|
+
affect the actual HTTP request.
|
|
454
|
+
|
|
455
|
+
Call `withTelemetry(...)` multiple times to attach independent subscribers
|
|
456
|
+
(e.g. one for Sentry, one for custom metrics). All subscribers receive every
|
|
457
|
+
event in registration order.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
provideWorkerHttpClient(
|
|
461
|
+
withWorkerConfigs([{ id: 'api', workerUrl: new URL('./workers/api.worker', import.meta.url) }]),
|
|
462
|
+
withWorkerRoutes([{ pattern: /\/api\//, worker: 'api' }]),
|
|
463
|
+
|
|
464
|
+
// Latency histogram
|
|
465
|
+
withTelemetry({
|
|
466
|
+
onResponse: (e) =>
|
|
467
|
+
histogram.record(e.durationMs, {
|
|
468
|
+
workerId: e.workerId,
|
|
469
|
+
status: e.status,
|
|
470
|
+
transport: e.transport,
|
|
471
|
+
}),
|
|
472
|
+
onError: (e) =>
|
|
473
|
+
histogram.record(e.durationMs, {
|
|
474
|
+
workerId: e.workerId,
|
|
475
|
+
status: 'error',
|
|
476
|
+
}),
|
|
477
|
+
}),
|
|
478
|
+
|
|
479
|
+
// Sentry breadcrumbs
|
|
480
|
+
withTelemetry({
|
|
481
|
+
onRequest: (e) =>
|
|
482
|
+
Sentry.addBreadcrumb({
|
|
483
|
+
category: 'worker-http',
|
|
484
|
+
message: `${e.method} ${e.url}`,
|
|
485
|
+
data: { requestId: e.requestId, workerId: e.workerId },
|
|
486
|
+
}),
|
|
487
|
+
onError: (e) =>
|
|
488
|
+
Sentry.captureException(e.error, {
|
|
489
|
+
tags: { workerId: e.workerId ?? 'fallback' },
|
|
490
|
+
}),
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Event interface:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
interface WorkerHttpTelemetryEventBase {
|
|
499
|
+
readonly requestId: string; // unique per request, correlates events
|
|
500
|
+
readonly method: string;
|
|
501
|
+
readonly url: string;
|
|
502
|
+
readonly urlWithParams: string;
|
|
503
|
+
readonly workerId: string | null; // null = fallback fetch
|
|
504
|
+
readonly transport: 'worker' | 'fallback-fetch';
|
|
505
|
+
readonly timestamp: number; // performance.now() at emission
|
|
506
|
+
}
|
|
507
|
+
// onResponse adds: kind: 'response', status, durationMs
|
|
508
|
+
// onError adds: kind: 'error', error, durationMs
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
409
513
|
## SSR + hydration
|
|
410
514
|
|
|
411
515
|
Worker HTTP integrates transparently with Angular SSR. The two problems SSR
|
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { InjectionToken, inject, Injectable, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { HttpContextToken, HttpHeaders, HttpResponse, HttpBackend, FetchBackend, HttpErrorResponse, HttpClient, HttpContext, provideHttpClient, withFetch } from '@angular/common/http';
|
|
4
4
|
import { throwError } from 'rxjs';
|
|
5
|
-
import { map, catchError } from 'rxjs/operators';
|
|
5
|
+
import { map, tap, catchError } from 'rxjs/operators';
|
|
6
6
|
import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -53,6 +53,15 @@ const WORKER_HTTP_SERIALIZER_TOKEN = new InjectionToken('WorkerHttpSerializer',
|
|
|
53
53
|
* Defaults to an empty map (no interceptors).
|
|
54
54
|
*/
|
|
55
55
|
const WORKER_HTTP_INTERCEPTORS_TOKEN = new InjectionToken('WorkerHttpInterceptors', { factory: () => ({}) });
|
|
56
|
+
/**
|
|
57
|
+
* Telemetry subscribers registered via `withTelemetry()`.
|
|
58
|
+
*
|
|
59
|
+
* Multi-provider token — each `withTelemetry(...)` call appends one
|
|
60
|
+
* subscriber. All subscribers are invoked on every emission; a throwing
|
|
61
|
+
* subscriber is isolated (the backend swallows the error so it never
|
|
62
|
+
* affects the HTTP request).
|
|
63
|
+
*/
|
|
64
|
+
const WORKER_HTTP_TELEMETRY_TOKEN = new InjectionToken('WorkerHttpTelemetry', { factory: () => [] });
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Converts an Angular `HttpRequest` into a structured-clone-safe POJO
|
|
@@ -117,6 +126,16 @@ function matchWorkerRoute(url, routes) {
|
|
|
117
126
|
return null;
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
let telemetryRequestCounter = 0;
|
|
130
|
+
function nextTelemetryRequestId() {
|
|
131
|
+
telemetryRequestCounter = (telemetryRequestCounter + 1) >>> 0;
|
|
132
|
+
return `whttp-${telemetryRequestCounter.toString(36)}`;
|
|
133
|
+
}
|
|
134
|
+
function now() {
|
|
135
|
+
return typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
136
|
+
? performance.now()
|
|
137
|
+
: Date.now();
|
|
138
|
+
}
|
|
120
139
|
/**
|
|
121
140
|
* Angular `HttpBackend` replacement that routes HTTP requests to web workers.
|
|
122
141
|
*
|
|
@@ -136,14 +155,15 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
136
155
|
serializer = inject(WORKER_HTTP_SERIALIZER_TOKEN);
|
|
137
156
|
interceptorSpecs = inject(WORKER_HTTP_INTERCEPTORS_TOKEN);
|
|
138
157
|
fetchBackend = inject(FetchBackend, { optional: true });
|
|
158
|
+
telemetry = inject(WORKER_HTTP_TELEMETRY_TOKEN);
|
|
139
159
|
transports = new Map();
|
|
140
160
|
handle(req) {
|
|
141
161
|
if (typeof Worker === 'undefined') {
|
|
142
|
-
return this.handleFallback(req, 'Web Workers are not available in this environment (SSR)');
|
|
162
|
+
return this.handleFallback(req, null, 'Web Workers are not available in this environment (SSR)');
|
|
143
163
|
}
|
|
144
164
|
const workerId = req.context.get(WORKER_TARGET) ?? matchWorkerRoute(req.url, this.routes);
|
|
145
165
|
if (!workerId) {
|
|
146
|
-
return this.handleFallback(req, `No worker route matched for URL: ${req.url}`);
|
|
166
|
+
return this.handleFallback(req, null, `No worker route matched for URL: ${req.url}`);
|
|
147
167
|
}
|
|
148
168
|
const config = this.configs.find((c) => c.id === workerId);
|
|
149
169
|
if (!config) {
|
|
@@ -156,12 +176,22 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
156
176
|
? this.serializer.serialize(serializable.body).data
|
|
157
177
|
: serializable.body;
|
|
158
178
|
const payload = body !== serializable.body ? { ...serializable, body } : serializable;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
179
|
+
const base = this.buildEventBase(req, workerId, 'worker');
|
|
180
|
+
this.emitRequest(base);
|
|
181
|
+
return transport.execute(payload).pipe(map((res) => toHttpResponse(res, req)), tap((event) => {
|
|
182
|
+
if (event instanceof HttpResponse) {
|
|
183
|
+
this.emitResponse(base, event.status);
|
|
184
|
+
}
|
|
185
|
+
}), catchError((err) => {
|
|
186
|
+
const httpError = new HttpErrorResponse({
|
|
187
|
+
error: err,
|
|
188
|
+
status: 0,
|
|
189
|
+
statusText: 'Worker Error',
|
|
190
|
+
url: req.urlWithParams,
|
|
191
|
+
});
|
|
192
|
+
this.emitError(base, httpError);
|
|
193
|
+
return throwError(() => httpError);
|
|
194
|
+
}));
|
|
165
195
|
}
|
|
166
196
|
ngOnDestroy() {
|
|
167
197
|
for (const transport of this.transports.values()) {
|
|
@@ -191,11 +221,74 @@ class WorkerHttpBackend extends HttpBackend {
|
|
|
191
221
|
return wildcard;
|
|
192
222
|
return [...wildcard, ...specific];
|
|
193
223
|
}
|
|
194
|
-
handleFallback(req, reason) {
|
|
224
|
+
handleFallback(req, workerId, reason) {
|
|
195
225
|
if (this.fallback === 'error' || !this.fetchBackend) {
|
|
196
226
|
return throwError(() => new Error(`[WorkerHttpBackend] ${reason}`));
|
|
197
227
|
}
|
|
198
|
-
|
|
228
|
+
const base = this.buildEventBase(req, workerId, 'fallback-fetch');
|
|
229
|
+
this.emitRequest(base);
|
|
230
|
+
return this.fetchBackend.handle(req).pipe(tap((event) => {
|
|
231
|
+
if (event instanceof HttpResponse) {
|
|
232
|
+
this.emitResponse(base, event.status);
|
|
233
|
+
}
|
|
234
|
+
}), catchError((err) => {
|
|
235
|
+
this.emitError(base, err);
|
|
236
|
+
return throwError(() => err);
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
// --- Telemetry ---------------------------------------------------------
|
|
240
|
+
buildEventBase(req, workerId, transport) {
|
|
241
|
+
return {
|
|
242
|
+
requestId: nextTelemetryRequestId(),
|
|
243
|
+
method: req.method,
|
|
244
|
+
url: req.url,
|
|
245
|
+
urlWithParams: req.urlWithParams,
|
|
246
|
+
workerId,
|
|
247
|
+
transport,
|
|
248
|
+
timestamp: now(),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
emitRequest(base) {
|
|
252
|
+
if (this.telemetry.length === 0)
|
|
253
|
+
return;
|
|
254
|
+
const event = { ...base, kind: 'request' };
|
|
255
|
+
this.dispatch((sub) => sub.onRequest?.(event));
|
|
256
|
+
}
|
|
257
|
+
emitResponse(base, status) {
|
|
258
|
+
if (this.telemetry.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
const event = {
|
|
261
|
+
...base,
|
|
262
|
+
kind: 'response',
|
|
263
|
+
status,
|
|
264
|
+
durationMs: now() - base.timestamp,
|
|
265
|
+
timestamp: now(),
|
|
266
|
+
};
|
|
267
|
+
this.dispatch((sub) => sub.onResponse?.(event));
|
|
268
|
+
}
|
|
269
|
+
emitError(base, error) {
|
|
270
|
+
if (this.telemetry.length === 0)
|
|
271
|
+
return;
|
|
272
|
+
const event = {
|
|
273
|
+
...base,
|
|
274
|
+
kind: 'error',
|
|
275
|
+
error,
|
|
276
|
+
durationMs: now() - base.timestamp,
|
|
277
|
+
timestamp: now(),
|
|
278
|
+
};
|
|
279
|
+
this.dispatch((sub) => sub.onError?.(event));
|
|
280
|
+
}
|
|
281
|
+
dispatch(invoke) {
|
|
282
|
+
for (const subscriber of this.telemetry) {
|
|
283
|
+
try {
|
|
284
|
+
invoke(subscriber);
|
|
285
|
+
}
|
|
286
|
+
catch (telemetryError) {
|
|
287
|
+
// A throwing telemetry subscriber must never affect the HTTP request.
|
|
288
|
+
// oxlint-disable-next-line no-console -- defensive log when user-provided telemetry throws
|
|
289
|
+
console.error('[WorkerHttpBackend] telemetry subscriber threw:', telemetryError);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
199
292
|
}
|
|
200
293
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
201
294
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend });
|
|
@@ -423,9 +516,51 @@ function withWorkerInterceptors(specs) {
|
|
|
423
516
|
providers: [{ provide: WORKER_HTTP_INTERCEPTORS_TOKEN, useValue: map }],
|
|
424
517
|
};
|
|
425
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Registers a telemetry subscriber for `WorkerHttpBackend`.
|
|
521
|
+
*
|
|
522
|
+
* Telemetry hooks are a main-thread extension point for APM integrations
|
|
523
|
+
* (Sentry, Datadog, OpenTelemetry, ad-hoc logging). They fire synchronously
|
|
524
|
+
* at three lifecycle points of every HTTP request handled by the backend:
|
|
525
|
+
*
|
|
526
|
+
* - `onRequest` — after worker resolution, before dispatch
|
|
527
|
+
* - `onResponse` — when a successful response is emitted
|
|
528
|
+
* - `onError` — when the request fails
|
|
529
|
+
*
|
|
530
|
+
* The feature is repeatable — call `withTelemetry(...)` multiple times to
|
|
531
|
+
* attach independent subscribers. All of them receive every event, in
|
|
532
|
+
* registration order. A throwing subscriber is isolated: the backend
|
|
533
|
+
* catches and logs the error so telemetry bugs never break the request.
|
|
534
|
+
*
|
|
535
|
+
* @example Basic counter
|
|
536
|
+
* ```typescript
|
|
537
|
+
* const counters = { requests: 0, errors: 0 };
|
|
538
|
+
* provideWorkerHttpClient(
|
|
539
|
+
* withWorkerConfigs([...]),
|
|
540
|
+
* withTelemetry({
|
|
541
|
+
* onRequest: () => counters.requests++,
|
|
542
|
+
* onError: () => counters.errors++,
|
|
543
|
+
* }),
|
|
544
|
+
* );
|
|
545
|
+
* ```
|
|
546
|
+
*
|
|
547
|
+
* @example Latency histogram
|
|
548
|
+
* ```typescript
|
|
549
|
+
* withTelemetry({
|
|
550
|
+
* onResponse: (e) => histogram.record(e.durationMs, { workerId: e.workerId }),
|
|
551
|
+
* onError: (e) => histogram.record(e.durationMs, { workerId: e.workerId, error: true }),
|
|
552
|
+
* });
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
function withTelemetry(telemetry) {
|
|
556
|
+
return {
|
|
557
|
+
kind: 'Telemetry',
|
|
558
|
+
providers: [{ provide: WORKER_HTTP_TELEMETRY_TOKEN, useValue: telemetry, multi: true }],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
426
561
|
|
|
427
562
|
/**
|
|
428
563
|
* Generated bundle index. Do not edit.
|
|
429
564
|
*/
|
|
430
565
|
|
|
431
|
-
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|
|
566
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|
|
@@ -80,9 +80,13 @@ async function createAesEncryptor(config) {
|
|
|
80
80
|
? new Uint8Array(config.keyMaterial.buffer.slice(0))
|
|
81
81
|
: new Uint8Array(config.keyMaterial);
|
|
82
82
|
const cryptoKey = await crypto.subtle.importKey('raw', keyMaterial, { name: algorithm, length: keyLength }, false, ['encrypt', 'decrypt']);
|
|
83
|
+
// AES-GCM uses a 96-bit (12-byte) IV per NIST SP 800-38D recommendation.
|
|
84
|
+
// AES-CBC and AES-CTR require exactly one block (16 bytes) for their IV /
|
|
85
|
+
// counter respectively; passing 12 bytes causes `OperationError`.
|
|
86
|
+
const ivLength = algorithm === 'AES-GCM' ? 12 : 16;
|
|
83
87
|
return {
|
|
84
88
|
async encrypt(data) {
|
|
85
|
-
const iv = crypto.getRandomValues(new Uint8Array(
|
|
89
|
+
const iv = crypto.getRandomValues(new Uint8Array(ivLength));
|
|
86
90
|
const params = algorithm === 'AES-GCM'
|
|
87
91
|
? { name: algorithm, iv }
|
|
88
92
|
: algorithm === 'AES-CBC'
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composes interceptor functions around a final handler, producing a single
|
|
3
|
-
* `(req) => Promise<resp>` chain. Pure — no side effects.
|
|
3
|
+
* `(req, signal?) => Promise<resp>` chain. Pure — no side effects.
|
|
4
|
+
*
|
|
5
|
+
* The optional `signal` is threaded through interceptor boundaries automatically
|
|
6
|
+
* via a wrapper around `next`, so legacy `WorkerInterceptorFn` implementations
|
|
7
|
+
* (which take only `req, next`) keep working and still propagate cancellation
|
|
8
|
+
* when they invoke `next(req)`.
|
|
4
9
|
*/
|
|
5
10
|
function buildChain(fns, finalHandler) {
|
|
6
|
-
return fns.reduceRight((next, interceptor) => (req) => interceptor(req, next), finalHandler);
|
|
11
|
+
return fns.reduceRight((next, interceptor) => (req, signal) => interceptor(req, (r) => next(r, signal)), finalHandler);
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
14
|
* Performs the actual `fetch()` call inside the worker, translating the
|
|
@@ -98,7 +103,7 @@ function attachRequestLoop(chain) {
|
|
|
98
103
|
const controller = new AbortController();
|
|
99
104
|
controllers.set(requestId, controller);
|
|
100
105
|
try {
|
|
101
|
-
const response = await chain(payload);
|
|
106
|
+
const response = await chain(payload, controller.signal);
|
|
102
107
|
self.postMessage({ type: 'response', requestId, result: response });
|
|
103
108
|
}
|
|
104
109
|
catch (error) {
|
|
@@ -145,7 +150,7 @@ function attachRequestLoop(chain) {
|
|
|
145
150
|
* ```
|
|
146
151
|
*/
|
|
147
152
|
function createWorkerPipeline(interceptors) {
|
|
148
|
-
const chain = buildChain(interceptors, (req) => executeFetch(req));
|
|
153
|
+
const chain = buildChain(interceptors, (req, signal) => executeFetch(req, signal));
|
|
149
154
|
attachRequestLoop(chain);
|
|
150
155
|
}
|
|
151
156
|
|
|
@@ -325,7 +330,9 @@ function hmacSigningInterceptor(config) {
|
|
|
325
330
|
* ```
|
|
326
331
|
*/
|
|
327
332
|
function loggingInterceptor(config) {
|
|
328
|
-
const logger = config?.logger ??
|
|
333
|
+
const logger = config?.logger ??
|
|
334
|
+
// oxlint-disable-next-line no-console -- default logger of a logging interceptor; consumers inject their own via config.logger
|
|
335
|
+
((msg, data) => console.log(msg, data));
|
|
329
336
|
const includeHeaders = config?.includeHeaders ?? false;
|
|
330
337
|
function safeLog(message, data) {
|
|
331
338
|
try {
|
|
@@ -538,7 +545,7 @@ function createConfigurableWorkerPipeline() {
|
|
|
538
545
|
if (data.type === INIT_MESSAGE_TYPE) {
|
|
539
546
|
const specs = data.specs ?? [];
|
|
540
547
|
const fns = specs.map((spec) => resolveSpec(spec));
|
|
541
|
-
const chain = buildChain(fns, (req) => executeFetch(req));
|
|
548
|
+
const chain = buildChain(fns, (req, signal) => executeFetch(req, signal));
|
|
542
549
|
// Swap to the regular request loop and replay any buffered messages.
|
|
543
550
|
attachRequestLoop(chain);
|
|
544
551
|
const handler = self.onmessage;
|
|
@@ -1,14 +1,118 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Scans a payload one level deep and collects every `Transferable` instance
|
|
5
|
+
* (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream,
|
|
6
|
+
* WritableStream, TransformStream) found in its own enumerable properties.
|
|
7
|
+
*
|
|
8
|
+
* Used by `createWorkerTransport` when `transferDetection === 'auto'` to build
|
|
9
|
+
* the second argument of `worker.postMessage(data, transfer)` so large buffers
|
|
10
|
+
* move zero-copy instead of being structured-cloned.
|
|
11
|
+
*
|
|
12
|
+
* Design notes:
|
|
13
|
+
* - Only one level deep by design: deep traversal has quadratic cost on heavy
|
|
14
|
+
* graphs and makes the transfer list surprising. Real payloads that care
|
|
15
|
+
* about zero-copy put the buffer at the top level.
|
|
16
|
+
* - Duplicates are filtered — the same buffer referenced twice is transferred
|
|
17
|
+
* only once (required by the structured-clone algorithm).
|
|
18
|
+
* - Returns an empty array for primitives, plain serializable values, or when
|
|
19
|
+
* no transferable is found; `postMessage` accepts `[]` safely.
|
|
20
|
+
*/
|
|
21
|
+
function detectTransferables(payload) {
|
|
22
|
+
if (payload === null || payload === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
if (typeof payload !== 'object')
|
|
25
|
+
return [];
|
|
26
|
+
const found = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const collect = (value) => {
|
|
29
|
+
if (value === null || value === undefined)
|
|
30
|
+
return;
|
|
31
|
+
if (isTransferable(value)) {
|
|
32
|
+
if (!seen.has(value)) {
|
|
33
|
+
seen.add(value);
|
|
34
|
+
found.push(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (isTransferable(payload)) {
|
|
39
|
+
collect(payload);
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(payload)) {
|
|
43
|
+
for (const item of payload)
|
|
44
|
+
collect(item);
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(payload)) {
|
|
48
|
+
collect(payload[key]);
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
function isTransferable(value) {
|
|
53
|
+
if (value === null || value === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
if (typeof value !== 'object')
|
|
56
|
+
return false;
|
|
57
|
+
// ArrayBuffer is the common case; check first.
|
|
58
|
+
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer)
|
|
59
|
+
return true;
|
|
60
|
+
// Typed-array views carry an underlying buffer but are NOT Transferable —
|
|
61
|
+
// only the buffer is. Caller must pass `.buffer` explicitly if they want it.
|
|
62
|
+
if (typeof MessagePort !== 'undefined' && value instanceof MessagePort)
|
|
63
|
+
return true;
|
|
64
|
+
if (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap)
|
|
65
|
+
return true;
|
|
66
|
+
if (typeof OffscreenCanvas !== 'undefined' && value instanceof OffscreenCanvas)
|
|
67
|
+
return true;
|
|
68
|
+
if (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream)
|
|
69
|
+
return true;
|
|
70
|
+
if (typeof WritableStream !== 'undefined' && value instanceof WritableStream)
|
|
71
|
+
return true;
|
|
72
|
+
if (typeof TransformStream !== 'undefined' && value instanceof TransformStream)
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Thrown by `createWorkerTransport` when a request exceeds its configured
|
|
79
|
+
* `requestTimeout`. Consumers can `instanceof`-check this error to distinguish
|
|
80
|
+
* timeout rejections from transport/worker errors.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* transport.execute(req).subscribe({
|
|
85
|
+
* error: (err) => {
|
|
86
|
+
* if (err instanceof WorkerHttpTimeoutError) {
|
|
87
|
+
* // dedicated timeout handling
|
|
88
|
+
* }
|
|
89
|
+
* },
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
class WorkerHttpTimeoutError extends Error {
|
|
94
|
+
name = 'WorkerHttpTimeoutError';
|
|
95
|
+
timeoutMs;
|
|
96
|
+
constructor(timeoutMs) {
|
|
97
|
+
super(`Worker request timed out after ${timeoutMs} ms`);
|
|
98
|
+
this.timeoutMs = timeoutMs;
|
|
99
|
+
// Maintain a proper prototype chain across TS transpile targets.
|
|
100
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
3
105
|
/**
|
|
4
106
|
* Creates a typed, Observable-based transport for communicating with a web worker.
|
|
5
107
|
*
|
|
6
108
|
* Features:
|
|
7
109
|
* - Request/response correlation via `requestId`
|
|
8
|
-
* -
|
|
110
|
+
* - Cancellation on Observable unsubscribe (also aborts `fetch()` in the worker)
|
|
111
|
+
* - Per-request timeout (default 30 s) rejecting with `WorkerHttpTimeoutError`
|
|
9
112
|
* - Optional worker pool with round-robin dispatch
|
|
10
113
|
* - Lazy worker creation (default)
|
|
11
|
-
* -
|
|
114
|
+
* - Opt-in transferable detection (`transferDetection: 'auto'`) for zero-copy
|
|
115
|
+
* `ArrayBuffer` / stream payloads
|
|
12
116
|
*
|
|
13
117
|
* @example
|
|
14
118
|
* ```typescript
|
|
@@ -28,6 +132,8 @@ function createWorkerTransport(config) {
|
|
|
28
132
|
let roundRobinIndex = 0;
|
|
29
133
|
let terminated = false;
|
|
30
134
|
const maxInstances = Math.min(config.maxInstances ?? 1, typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 1);
|
|
135
|
+
const requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
136
|
+
const transferDetection = config.transferDetection ?? 'none';
|
|
31
137
|
function createWorker() {
|
|
32
138
|
if (config.workerFactory) {
|
|
33
139
|
return config.workerFactory();
|
|
@@ -62,12 +168,24 @@ function createWorkerTransport(config) {
|
|
|
62
168
|
const requestId = crypto.randomUUID();
|
|
63
169
|
return new Observable((subscriber) => {
|
|
64
170
|
const worker = getOrCreateWorker();
|
|
171
|
+
let settled = false;
|
|
172
|
+
let timeoutHandle;
|
|
173
|
+
const cleanup = () => {
|
|
174
|
+
worker.removeEventListener('message', messageHandler);
|
|
175
|
+
worker.removeEventListener('error', errorHandler);
|
|
176
|
+
if (timeoutHandle !== undefined) {
|
|
177
|
+
clearTimeout(timeoutHandle);
|
|
178
|
+
timeoutHandle = undefined;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
65
181
|
const messageHandler = (event) => {
|
|
66
182
|
const data = event.data;
|
|
67
183
|
if (data.requestId !== requestId)
|
|
68
184
|
return;
|
|
69
|
-
|
|
70
|
-
|
|
185
|
+
if (settled)
|
|
186
|
+
return;
|
|
187
|
+
settled = true;
|
|
188
|
+
cleanup();
|
|
71
189
|
if (data.type === 'error') {
|
|
72
190
|
const err = data.error;
|
|
73
191
|
subscriber.error(new Error(err.message));
|
|
@@ -78,17 +196,39 @@ function createWorkerTransport(config) {
|
|
|
78
196
|
}
|
|
79
197
|
};
|
|
80
198
|
const errorHandler = (event) => {
|
|
81
|
-
|
|
82
|
-
|
|
199
|
+
if (settled)
|
|
200
|
+
return;
|
|
201
|
+
settled = true;
|
|
202
|
+
cleanup();
|
|
83
203
|
subscriber.error(new Error(event.message ?? 'Worker error'));
|
|
84
204
|
};
|
|
85
205
|
worker.addEventListener('message', messageHandler);
|
|
86
206
|
worker.addEventListener('error', errorHandler);
|
|
87
|
-
|
|
207
|
+
if (transferDetection === 'auto') {
|
|
208
|
+
const transferables = detectTransferables(request);
|
|
209
|
+
worker.postMessage({ type: 'request', requestId, payload: request }, transferables);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
worker.postMessage({ type: 'request', requestId, payload: request });
|
|
213
|
+
}
|
|
214
|
+
if (requestTimeout > 0 && Number.isFinite(requestTimeout)) {
|
|
215
|
+
timeoutHandle = setTimeout(() => {
|
|
216
|
+
if (settled)
|
|
217
|
+
return;
|
|
218
|
+
settled = true;
|
|
219
|
+
cleanup();
|
|
220
|
+
// Ask the worker to abort any in-flight work for this id. The
|
|
221
|
+
// cancellation fix wires this through to `fetch()`.
|
|
222
|
+
worker.postMessage({ type: 'cancel', requestId });
|
|
223
|
+
subscriber.error(new WorkerHttpTimeoutError(requestTimeout));
|
|
224
|
+
}, requestTimeout);
|
|
225
|
+
}
|
|
88
226
|
// Teardown: send cancel message on unsubscribe
|
|
89
227
|
return () => {
|
|
90
|
-
|
|
91
|
-
|
|
228
|
+
if (settled)
|
|
229
|
+
return;
|
|
230
|
+
settled = true;
|
|
231
|
+
cleanup();
|
|
92
232
|
worker.postMessage({ type: 'cancel', requestId });
|
|
93
233
|
};
|
|
94
234
|
});
|
|
@@ -116,4 +256,4 @@ function createWorkerTransport(config) {
|
|
|
116
256
|
* Generated bundle index. Do not edit.
|
|
117
257
|
*/
|
|
118
258
|
|
|
119
|
-
export { createWorkerTransport };
|
|
259
|
+
export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @angular-helpers/worker-http
|
|
3
|
-
*
|
|
4
|
-
* Angular HTTP over Web Workers — off-main-thread HTTP pipelines
|
|
5
|
-
* with configurable interceptors, WebCrypto security, and pluggable serialization.
|
|
6
|
-
*
|
|
7
|
-
* Sub-entry points:
|
|
8
|
-
* - @angular-helpers/worker-http/transport (P1: typed RPC bridge)
|
|
9
|
-
* - @angular-helpers/worker-http/serializer (P2: TOON, seroval, auto-detect)
|
|
10
|
-
* - @angular-helpers/worker-http/backend (P3: Angular HttpBackend replacement)
|
|
11
|
-
* - @angular-helpers/worker-http/interceptors (P4: pure-fn interceptors for workers)
|
|
12
|
-
* - @angular-helpers/worker-http/crypto (P5: WebCrypto primitives)
|
|
13
|
-
*/
|
|
14
|
-
const WORKER_HTTP_VERSION = '0.0.1';
|
|
15
|
-
|
|
16
1
|
/**
|
|
17
2
|
* Generated bundle index. Do not edit.
|
|
18
3
|
*/
|
|
19
|
-
|
|
20
|
-
export { WORKER_HTTP_VERSION };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/worker-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Angular HTTP over Web Workers — off-main-thread HTTP pipelines with configurable interceptors, WebCrypto security, and pluggable serialization",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -9,7 +9,7 @@ import { Observable } from 'rxjs';
|
|
|
9
9
|
* Discriminated union for worker HTTP feature kinds.
|
|
10
10
|
* Mirrors Angular's HttpFeatureKind pattern.
|
|
11
11
|
*/
|
|
12
|
-
type WorkerHttpFeatureKind = 'WorkerConfigs' | 'WorkerRoutes' | 'WorkerFallback' | 'WorkerSerialization' | 'WorkerInterceptors';
|
|
12
|
+
type WorkerHttpFeatureKind = 'WorkerConfigs' | 'WorkerRoutes' | 'WorkerFallback' | 'WorkerSerialization' | 'WorkerInterceptors' | 'Telemetry';
|
|
13
13
|
/**
|
|
14
14
|
* Feature object — mirrors Angular's HttpFeature<K> shape.
|
|
15
15
|
*/
|
|
@@ -68,6 +68,82 @@ interface SerializableResponse {
|
|
|
68
68
|
url: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Telemetry hooks for `WorkerHttpBackend`.
|
|
73
|
+
*
|
|
74
|
+
* Extension point for APM integrations (Sentry, Datadog, OpenTelemetry,
|
|
75
|
+
* ad-hoc metrics). Fires on the main thread, synchronously, at three
|
|
76
|
+
* lifecycle points of every HTTP request that reaches
|
|
77
|
+
* `WorkerHttpBackend`:
|
|
78
|
+
*
|
|
79
|
+
* - `onRequest` — after worker resolution / fallback decision, BEFORE
|
|
80
|
+
* dispatch.
|
|
81
|
+
* - `onResponse` — when a successful response is returned from the worker
|
|
82
|
+
* (or the fallback `FetchBackend`).
|
|
83
|
+
* - `onError` — when the request fails in transport or surfaces as a
|
|
84
|
+
* non-2xx `HttpErrorResponse`.
|
|
85
|
+
*
|
|
86
|
+
* Subscribers are invoked inside a try/catch — a throwing or misbehaving
|
|
87
|
+
* subscriber is isolated from the HTTP request and from every other
|
|
88
|
+
* subscriber. Telemetry errors are logged via `console.error` and
|
|
89
|
+
* swallowed.
|
|
90
|
+
*
|
|
91
|
+
* Multiple subscribers are supported via a multi-provider DI token.
|
|
92
|
+
* Register with `withTelemetry(...)`; every call appends one subscriber.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* How a request was dispatched.
|
|
96
|
+
*
|
|
97
|
+
* - `'worker'` — dispatched to a worker from the pool.
|
|
98
|
+
* - `'fallback-fetch'` — dispatched to the main-thread `FetchBackend`
|
|
99
|
+
* (SSR context, no matching route, unknown worker with `'main-thread'`
|
|
100
|
+
* fallback strategy).
|
|
101
|
+
*/
|
|
102
|
+
type WorkerHttpTransportKind = 'worker' | 'fallback-fetch';
|
|
103
|
+
/**
|
|
104
|
+
* Base shape shared by every telemetry event.
|
|
105
|
+
*/
|
|
106
|
+
interface WorkerHttpTelemetryEventBase {
|
|
107
|
+
/** Stable id unique to this request within the process. Correlates all three events. */
|
|
108
|
+
readonly requestId: string;
|
|
109
|
+
/** HTTP method (`'GET'`, `'POST'`, ...). */
|
|
110
|
+
readonly method: string;
|
|
111
|
+
/** URL without query params, as Angular sees it. */
|
|
112
|
+
readonly url: string;
|
|
113
|
+
/** URL with query params baked in. */
|
|
114
|
+
readonly urlWithParams: string;
|
|
115
|
+
/** Worker id that served the request. `null` when routed to fallback fetch. */
|
|
116
|
+
readonly workerId: string | null;
|
|
117
|
+
/** How the request was actually dispatched. */
|
|
118
|
+
readonly transport: WorkerHttpTransportKind;
|
|
119
|
+
/** `performance.now()` value at emission time. */
|
|
120
|
+
readonly timestamp: number;
|
|
121
|
+
}
|
|
122
|
+
/** Fires before the request is dispatched. */
|
|
123
|
+
interface WorkerHttpRequestEvent extends WorkerHttpTelemetryEventBase {
|
|
124
|
+
readonly kind: 'request';
|
|
125
|
+
}
|
|
126
|
+
/** Fires when a successful response is returned. */
|
|
127
|
+
interface WorkerHttpResponseEvent extends WorkerHttpTelemetryEventBase {
|
|
128
|
+
readonly kind: 'response';
|
|
129
|
+
readonly status: number;
|
|
130
|
+
readonly durationMs: number;
|
|
131
|
+
}
|
|
132
|
+
/** Fires when the request fails. */
|
|
133
|
+
interface WorkerHttpErrorEvent extends WorkerHttpTelemetryEventBase {
|
|
134
|
+
readonly kind: 'error';
|
|
135
|
+
readonly error: unknown;
|
|
136
|
+
readonly durationMs: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Telemetry subscriber — all callbacks are optional.
|
|
140
|
+
*/
|
|
141
|
+
interface WorkerHttpTelemetry {
|
|
142
|
+
onRequest?(event: WorkerHttpRequestEvent): void;
|
|
143
|
+
onResponse?(event: WorkerHttpResponseEvent): void;
|
|
144
|
+
onError?(event: WorkerHttpErrorEvent): void;
|
|
145
|
+
}
|
|
146
|
+
|
|
71
147
|
/**
|
|
72
148
|
* Per-worker interceptor specs map. Key is the worker id from
|
|
73
149
|
* `WorkerConfig.id`, plus the special `'*'` wildcard that applies to every
|
|
@@ -232,6 +308,43 @@ declare function withWorkerSerialization(serializer: WorkerSerializer): WorkerHt
|
|
|
232
308
|
* ```
|
|
233
309
|
*/
|
|
234
310
|
declare function withWorkerInterceptors(specs: readonly WorkerInterceptorSpec[] | WorkerInterceptorSpecsMap): WorkerHttpFeature<'WorkerInterceptors'>;
|
|
311
|
+
/**
|
|
312
|
+
* Registers a telemetry subscriber for `WorkerHttpBackend`.
|
|
313
|
+
*
|
|
314
|
+
* Telemetry hooks are a main-thread extension point for APM integrations
|
|
315
|
+
* (Sentry, Datadog, OpenTelemetry, ad-hoc logging). They fire synchronously
|
|
316
|
+
* at three lifecycle points of every HTTP request handled by the backend:
|
|
317
|
+
*
|
|
318
|
+
* - `onRequest` — after worker resolution, before dispatch
|
|
319
|
+
* - `onResponse` — when a successful response is emitted
|
|
320
|
+
* - `onError` — when the request fails
|
|
321
|
+
*
|
|
322
|
+
* The feature is repeatable — call `withTelemetry(...)` multiple times to
|
|
323
|
+
* attach independent subscribers. All of them receive every event, in
|
|
324
|
+
* registration order. A throwing subscriber is isolated: the backend
|
|
325
|
+
* catches and logs the error so telemetry bugs never break the request.
|
|
326
|
+
*
|
|
327
|
+
* @example Basic counter
|
|
328
|
+
* ```typescript
|
|
329
|
+
* const counters = { requests: 0, errors: 0 };
|
|
330
|
+
* provideWorkerHttpClient(
|
|
331
|
+
* withWorkerConfigs([...]),
|
|
332
|
+
* withTelemetry({
|
|
333
|
+
* onRequest: () => counters.requests++,
|
|
334
|
+
* onError: () => counters.errors++,
|
|
335
|
+
* }),
|
|
336
|
+
* );
|
|
337
|
+
* ```
|
|
338
|
+
*
|
|
339
|
+
* @example Latency histogram
|
|
340
|
+
* ```typescript
|
|
341
|
+
* withTelemetry({
|
|
342
|
+
* onResponse: (e) => histogram.record(e.durationMs, { workerId: e.workerId }),
|
|
343
|
+
* onError: (e) => histogram.record(e.durationMs, { workerId: e.workerId, error: true }),
|
|
344
|
+
* });
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
declare function withTelemetry(telemetry: WorkerHttpTelemetry): WorkerHttpFeature<'Telemetry'>;
|
|
235
348
|
|
|
236
349
|
/**
|
|
237
350
|
* Angular `HttpBackend` replacement that routes HTTP requests to web workers.
|
|
@@ -252,12 +365,18 @@ declare class WorkerHttpBackend extends HttpBackend implements OnDestroy {
|
|
|
252
365
|
private readonly serializer;
|
|
253
366
|
private readonly interceptorSpecs;
|
|
254
367
|
private readonly fetchBackend;
|
|
368
|
+
private readonly telemetry;
|
|
255
369
|
private readonly transports;
|
|
256
370
|
handle(req: HttpRequest<unknown>): Observable<HttpEvent<unknown>>;
|
|
257
371
|
ngOnDestroy(): void;
|
|
258
372
|
private getOrCreateTransport;
|
|
259
373
|
private resolveSpecsFor;
|
|
260
374
|
private handleFallback;
|
|
375
|
+
private buildEventBase;
|
|
376
|
+
private emitRequest;
|
|
377
|
+
private emitResponse;
|
|
378
|
+
private emitError;
|
|
379
|
+
private dispatch;
|
|
261
380
|
static ɵfac: i0.ɵɵFactoryDeclaration<WorkerHttpBackend, never>;
|
|
262
381
|
static ɵprov: i0.ɵɵInjectableDeclaration<WorkerHttpBackend>;
|
|
263
382
|
}
|
|
@@ -341,5 +460,5 @@ declare function matchWorkerRoute(url: string, routes: Array<{
|
|
|
341
460
|
priority?: number;
|
|
342
461
|
}>): string | null;
|
|
343
462
|
|
|
344
|
-
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|
|
345
|
-
export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerInterceptorSpecsMap, WorkerRequestOptions, WorkerRoute };
|
|
463
|
+
export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization };
|
|
464
|
+
export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpErrorEvent, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerHttpRequestEvent, WorkerHttpResponseEvent, WorkerHttpTelemetry, WorkerHttpTelemetryEventBase, WorkerHttpTransportKind, WorkerInterceptorSpecsMap, WorkerRequestOptions, WorkerRoute };
|
|
@@ -34,9 +34,27 @@ interface WorkerTransportConfig {
|
|
|
34
34
|
workerUrl?: string | URL;
|
|
35
35
|
/** Maximum number of worker instances in the pool (default: 1) */
|
|
36
36
|
maxInstances?: number;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Transfer strategy for `postMessage` payloads.
|
|
39
|
+
*
|
|
40
|
+
* - `'none'` (default) — payloads are always structured-cloned, preserving
|
|
41
|
+
* the caller's access to the original data after post.
|
|
42
|
+
* - `'auto'` — shallowly walks the payload and passes every detected
|
|
43
|
+
* `Transferable` (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas,
|
|
44
|
+
* ReadableStream, WritableStream, TransformStream) in the transfer list
|
|
45
|
+
* of `postMessage`. Large buffers move zero-copy; their `byteLength`
|
|
46
|
+
* becomes `0` in the main thread after post.
|
|
47
|
+
*
|
|
48
|
+
* The `'manual'` value is reserved for a future API where callers supply
|
|
49
|
+
* their own transfer list per request. It currently behaves like `'none'`.
|
|
50
|
+
*/
|
|
38
51
|
transferDetection?: 'auto' | 'manual' | 'none';
|
|
39
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Per-request timeout in milliseconds. If the worker does not respond
|
|
54
|
+
* within this window, `execute()` errors with `WorkerHttpTimeoutError`
|
|
55
|
+
* and a cancel message is posted to the worker. Set to `0` or
|
|
56
|
+
* non-finite to disable the timeout entirely. Default: `30000` (30 s).
|
|
57
|
+
*/
|
|
40
58
|
requestTimeout?: number;
|
|
41
59
|
/**
|
|
42
60
|
* Optional handshake message posted to every worker as soon as it is
|
|
@@ -103,10 +121,12 @@ interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
|
|
|
103
121
|
*
|
|
104
122
|
* Features:
|
|
105
123
|
* - Request/response correlation via `requestId`
|
|
106
|
-
* -
|
|
124
|
+
* - Cancellation on Observable unsubscribe (also aborts `fetch()` in the worker)
|
|
125
|
+
* - Per-request timeout (default 30 s) rejecting with `WorkerHttpTimeoutError`
|
|
107
126
|
* - Optional worker pool with round-robin dispatch
|
|
108
127
|
* - Lazy worker creation (default)
|
|
109
|
-
* -
|
|
128
|
+
* - Opt-in transferable detection (`transferDetection: 'auto'`) for zero-copy
|
|
129
|
+
* `ArrayBuffer` / stream payloads
|
|
110
130
|
*
|
|
111
131
|
* @example
|
|
112
132
|
* ```typescript
|
|
@@ -123,5 +143,47 @@ interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
|
|
|
123
143
|
*/
|
|
124
144
|
declare function createWorkerTransport<TRequest = unknown, TResponse = unknown>(config: WorkerTransportConfig): WorkerTransport<TRequest, TResponse>;
|
|
125
145
|
|
|
126
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Thrown by `createWorkerTransport` when a request exceeds its configured
|
|
148
|
+
* `requestTimeout`. Consumers can `instanceof`-check this error to distinguish
|
|
149
|
+
* timeout rejections from transport/worker errors.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* transport.execute(req).subscribe({
|
|
154
|
+
* error: (err) => {
|
|
155
|
+
* if (err instanceof WorkerHttpTimeoutError) {
|
|
156
|
+
* // dedicated timeout handling
|
|
157
|
+
* }
|
|
158
|
+
* },
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
declare class WorkerHttpTimeoutError extends Error {
|
|
163
|
+
readonly name = "WorkerHttpTimeoutError";
|
|
164
|
+
readonly timeoutMs: number;
|
|
165
|
+
constructor(timeoutMs: number);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scans a payload one level deep and collects every `Transferable` instance
|
|
170
|
+
* (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream,
|
|
171
|
+
* WritableStream, TransformStream) found in its own enumerable properties.
|
|
172
|
+
*
|
|
173
|
+
* Used by `createWorkerTransport` when `transferDetection === 'auto'` to build
|
|
174
|
+
* the second argument of `worker.postMessage(data, transfer)` so large buffers
|
|
175
|
+
* move zero-copy instead of being structured-cloned.
|
|
176
|
+
*
|
|
177
|
+
* Design notes:
|
|
178
|
+
* - Only one level deep by design: deep traversal has quadratic cost on heavy
|
|
179
|
+
* graphs and makes the transfer list surprising. Real payloads that care
|
|
180
|
+
* about zero-copy put the buffer at the top level.
|
|
181
|
+
* - Duplicates are filtered — the same buffer referenced twice is transferred
|
|
182
|
+
* only once (required by the structured-clone algorithm).
|
|
183
|
+
* - Returns an empty array for primitives, plain serializable values, or when
|
|
184
|
+
* no transferable is found; `postMessage` accepts `[]` safely.
|
|
185
|
+
*/
|
|
186
|
+
declare function detectTransferables(payload: unknown): Transferable[];
|
|
187
|
+
|
|
188
|
+
export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
|
|
127
189
|
export type { WorkerErrorResponse, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };
|
|
@@ -1,16 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @angular-helpers/worker-http
|
|
3
|
-
*
|
|
4
|
-
* Angular HTTP over Web Workers — off-main-thread HTTP pipelines
|
|
5
|
-
* with configurable interceptors, WebCrypto security, and pluggable serialization.
|
|
6
|
-
*
|
|
7
|
-
* Sub-entry points:
|
|
8
|
-
* - @angular-helpers/worker-http/transport (P1: typed RPC bridge)
|
|
9
|
-
* - @angular-helpers/worker-http/serializer (P2: TOON, seroval, auto-detect)
|
|
10
|
-
* - @angular-helpers/worker-http/backend (P3: Angular HttpBackend replacement)
|
|
11
|
-
* - @angular-helpers/worker-http/interceptors (P4: pure-fn interceptors for workers)
|
|
12
|
-
* - @angular-helpers/worker-http/crypto (P5: WebCrypto primitives)
|
|
13
|
-
*/
|
|
14
|
-
declare const WORKER_HTTP_VERSION = "0.0.1";
|
|
15
1
|
|
|
16
|
-
export {
|
|
2
|
+
export { };
|