@angular-helpers/worker-http 1.0.0 → 21.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,6 +113,11 @@ transport.terminate();
113
113
  - **Per-request timeout** (default `30_000` ms) via `requestTimeout`; errors
114
114
  with `WorkerHttpTimeoutError` and sends a cancel message to the worker.
115
115
  Set to `0` to disable.
116
+ - **Per-call `AbortSignal` and `timeout` overrides** via the second argument
117
+ to `execute(request, { signal, timeout })`. The signal triggers a typed
118
+ `WorkerHttpAbortError`; an aborted-at-call-time signal fails fast with no
119
+ postMessage round-trip. Distinct error class from `WorkerHttpTimeoutError`
120
+ so consumers can branch on `instanceof`.
116
121
  - **Opt-in transferable detection** via `transferDetection: 'auto'` — passes
117
122
  detected `ArrayBuffer` / `MessagePort` / `ImageBitmap` /
118
123
  `OffscreenCanvas` / streams as the transfer list of `postMessage`, enabling
@@ -470,9 +475,10 @@ manual control.
470
475
  - `withWorkerSerialization(serializer)` — plug in `createSerovalSerializer()` for complex request bodies (`Date`, `Map`, `Set`)
471
476
  - `withWorkerInterceptors(specs | specsByWorker)` — configure the worker-side pipeline from Angular DI; pairs with `createConfigurableWorkerPipeline()` in the worker file
472
477
  - `WORKER_TARGET` — `HttpContextToken<string | null>` for per-request worker routing via `HttpContext`
473
- - `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field
478
+ - `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field, plus per-request `signal?: AbortSignal` and `timeout?: number` for cancellation. Aborted requests error with `WorkerHttpAbortError`; expired timeouts with `WorkerHttpTimeoutError`.
474
479
  - `WorkerHttpBackend` — the `HttpBackend` implementation (injectable for advanced use)
475
480
  - `matchWorkerRoute(url, routes)` — pure utility to test routing rules
481
+ - `WORKER_HTTP_SIGNAL`, `WORKER_HTTP_TIMEOUT` — `HttpContextToken`s used internally by `WorkerHttpClient`; set them directly on the `HttpContext` if you're using `HttpClient` rather than the wrapper.
476
482
 
477
483
  ---
478
484
 
@@ -22,6 +22,31 @@ import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
22
22
  * ```
23
23
  */
24
24
  const WORKER_TARGET = new HttpContextToken(() => null);
25
+ /**
26
+ * Per-request HttpContextToken carrying an external `AbortSignal`.
27
+ *
28
+ * When the signal fires, the backend posts a `cancel` message to the worker
29
+ * and the request errors with `WorkerHttpAbortError` (wrapped by Angular into
30
+ * `HttpErrorResponse`).
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const ac = new AbortController();
35
+ * this.http.get('/api/users', { signal: ac.signal });
36
+ * // later
37
+ * ac.abort();
38
+ * ```
39
+ */
40
+ const WORKER_HTTP_SIGNAL = new HttpContextToken(() => null);
41
+ /**
42
+ * Per-request HttpContextToken carrying a timeout in milliseconds.
43
+ *
44
+ * When set, overrides the transport-level `requestTimeout` for this single
45
+ * call. `0` or non-finite disables the timeout for this request only.
46
+ *
47
+ * On expiry the request errors with `WorkerHttpTimeoutError`.
48
+ */
49
+ const WORKER_HTTP_TIMEOUT = new HttpContextToken(() => null);
25
50
  /**
26
51
  * Registered worker definitions provided via `withWorkerConfigs()`.
27
52
  */
@@ -188,7 +213,12 @@ class WorkerHttpBackend extends HttpBackend {
188
213
  const payload = body !== serializable.body ? { ...serializable, body } : serializable;
189
214
  const base = this.buildEventBase(req, workerId, 'worker');
190
215
  this.emitRequest(base);
191
- return transport.execute(payload).pipe(map((res) => toHttpResponse(res, req)), tap((event) => {
216
+ const signal = req.context.get(WORKER_HTTP_SIGNAL) ?? undefined;
217
+ const timeout = req.context.get(WORKER_HTTP_TIMEOUT);
218
+ const executeOptions = signal !== undefined || timeout !== null
219
+ ? { signal, timeout: timeout ?? undefined }
220
+ : undefined;
221
+ return transport.execute(payload, executeOptions).pipe(map((res) => toHttpResponse(res, req)), tap((event) => {
192
222
  if (event instanceof HttpResponse) {
193
223
  this.emitResponse(base, event.status);
194
224
  }
@@ -352,11 +382,15 @@ class WorkerHttpClient {
352
382
  return this.http.head(url, this.withWorker(options));
353
383
  }
354
384
  withWorker(options) {
355
- const { worker = null, context, ...rest } = options ?? {};
356
- return {
357
- ...rest,
358
- context: (context ?? new HttpContext()).set(WORKER_TARGET, worker),
359
- };
385
+ const { worker = null, signal, timeout, context, ...rest } = options ?? {};
386
+ let ctx = (context ?? new HttpContext()).set(WORKER_TARGET, worker);
387
+ if (signal !== undefined) {
388
+ ctx = ctx.set(WORKER_HTTP_SIGNAL, signal);
389
+ }
390
+ if (timeout !== undefined) {
391
+ ctx = ctx.set(WORKER_HTTP_TIMEOUT, timeout);
392
+ }
393
+ return { ...rest, context: ctx };
360
394
  }
361
395
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
362
396
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient });
@@ -603,4 +637,4 @@ function withWorkerStreamsPolyfill() {
603
637
  * Generated bundle index. Do not edit.
604
638
  */
605
639
 
606
- export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
640
+ export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_SIGNAL, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_HTTP_TIMEOUT, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
@@ -74,6 +74,41 @@ function isTransferable(value) {
74
74
  return false;
75
75
  }
76
76
 
77
+ /**
78
+ * Thrown by `createWorkerTransport` when a request is aborted via an external
79
+ * `AbortSignal` passed to `execute(request, { signal })`.
80
+ *
81
+ * Distinct from `WorkerHttpTimeoutError` (which fires when the per-request
82
+ * `timeout` elapses) and from a silent unsubscribe (which sends a `cancel`
83
+ * message but does not surface an error to the subscriber, since RxJS already
84
+ * tore down the stream).
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const ac = new AbortController();
89
+ * transport.execute(req, { signal: ac.signal }).subscribe({
90
+ * error: (err) => {
91
+ * if (err instanceof WorkerHttpAbortError) {
92
+ * // user-driven cancellation; usually safe to ignore in UI
93
+ * }
94
+ * },
95
+ * });
96
+ * ac.abort('user navigated away');
97
+ * ```
98
+ */
99
+ class WorkerHttpAbortError extends Error {
100
+ name = 'WorkerHttpAbortError';
101
+ /** The reason passed to `AbortController.abort(reason)`, if any. */
102
+ reason;
103
+ constructor(reason) {
104
+ super(reason === undefined
105
+ ? 'Worker request aborted'
106
+ : `Worker request aborted: ${reason instanceof Error ? reason.message : String(reason)}`);
107
+ this.reason = reason;
108
+ Object.setPrototypeOf(this, new.target.prototype);
109
+ }
110
+ }
111
+
77
112
  /**
78
113
  * Thrown by `createWorkerTransport` when a request exceeds its configured
79
114
  * `requestTimeout`. Consumers can `instanceof`-check this error to distinguish
@@ -161,14 +196,22 @@ function createWorkerTransport(config) {
161
196
  roundRobinIndex++;
162
197
  return worker;
163
198
  }
164
- function execute(request) {
199
+ function execute(request, options) {
165
200
  if (terminated) {
166
201
  return new Observable((subscriber) => {
167
202
  subscriber.error(new Error('WorkerTransport has been terminated'));
168
203
  });
169
204
  }
170
205
  const requestId = crypto.randomUUID();
206
+ const externalSignal = options?.signal;
207
+ const effectiveTimeout = options?.timeout ?? requestTimeout;
171
208
  return new Observable((subscriber) => {
209
+ // Fail-fast: if the caller's signal was already aborted before we even
210
+ // touched a worker, surface it immediately with no postMessage roundtrip.
211
+ if (externalSignal?.aborted) {
212
+ subscriber.error(new WorkerHttpAbortError(externalSignal.reason));
213
+ return () => undefined;
214
+ }
172
215
  // Lazy-load polyfill on first request if enabled
173
216
  if (streamsPolyfill && !polyfillLoaded) {
174
217
  loadStreamsPolyfill().catch((err) => {
@@ -178,6 +221,7 @@ function createWorkerTransport(config) {
178
221
  const worker = getOrCreateWorker();
179
222
  let settled = false;
180
223
  let timeoutHandle;
224
+ let abortListener;
181
225
  const cleanup = () => {
182
226
  worker.removeEventListener('message', messageHandler);
183
227
  worker.removeEventListener('error', errorHandler);
@@ -185,6 +229,10 @@ function createWorkerTransport(config) {
185
229
  clearTimeout(timeoutHandle);
186
230
  timeoutHandle = undefined;
187
231
  }
232
+ if (abortListener && externalSignal) {
233
+ externalSignal.removeEventListener('abort', abortListener);
234
+ abortListener = undefined;
235
+ }
188
236
  };
189
237
  const messageHandler = (event) => {
190
238
  const data = event.data;
@@ -219,7 +267,7 @@ function createWorkerTransport(config) {
219
267
  else {
220
268
  worker.postMessage({ type: 'request', requestId, payload: request });
221
269
  }
222
- if (requestTimeout > 0 && Number.isFinite(requestTimeout)) {
270
+ if (effectiveTimeout > 0 && Number.isFinite(effectiveTimeout)) {
223
271
  timeoutHandle = setTimeout(() => {
224
272
  if (settled)
225
273
  return;
@@ -228,8 +276,23 @@ function createWorkerTransport(config) {
228
276
  // Ask the worker to abort any in-flight work for this id. The
229
277
  // cancellation fix wires this through to `fetch()`.
230
278
  worker.postMessage({ type: 'cancel', requestId });
231
- subscriber.error(new WorkerHttpTimeoutError(requestTimeout));
232
- }, requestTimeout);
279
+ subscriber.error(new WorkerHttpTimeoutError(effectiveTimeout));
280
+ }, effectiveTimeout);
281
+ }
282
+ // External AbortSignal: surface a typed abort error and cancel the
283
+ // worker-side fetch. Distinct from a silent unsubscribe (no error) and
284
+ // from a timeout (different error type).
285
+ if (externalSignal) {
286
+ abortListener = () => {
287
+ if (settled)
288
+ return;
289
+ settled = true;
290
+ const reason = externalSignal.reason;
291
+ cleanup();
292
+ worker.postMessage({ type: 'cancel', requestId });
293
+ subscriber.error(new WorkerHttpAbortError(reason));
294
+ };
295
+ externalSignal.addEventListener('abort', abortListener, { once: true });
233
296
  }
234
297
  // Teardown: send cancel message on unsubscribe
235
298
  return () => {
@@ -284,4 +347,4 @@ function createWorkerTransport(config) {
284
347
  * Generated bundle index. Do not edit.
285
348
  */
286
349
 
287
- export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
350
+ export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/worker-http",
3
- "version": "1.0.0",
3
+ "version": "21.1.0",
4
4
  "description": "Angular HTTP over Web Workers — off-main-thread HTTP pipelines with configurable interceptors, WebCrypto security, and pluggable serialization",
5
5
  "schematics": "./schematics/collection.json",
6
6
  "exports": {
@@ -168,6 +168,31 @@ type WorkerInterceptorSpecsMap = Readonly<Record<string, readonly WorkerIntercep
168
168
  * ```
169
169
  */
170
170
  declare const WORKER_TARGET: HttpContextToken<string>;
171
+ /**
172
+ * Per-request HttpContextToken carrying an external `AbortSignal`.
173
+ *
174
+ * When the signal fires, the backend posts a `cancel` message to the worker
175
+ * and the request errors with `WorkerHttpAbortError` (wrapped by Angular into
176
+ * `HttpErrorResponse`).
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * const ac = new AbortController();
181
+ * this.http.get('/api/users', { signal: ac.signal });
182
+ * // later
183
+ * ac.abort();
184
+ * ```
185
+ */
186
+ declare const WORKER_HTTP_SIGNAL: HttpContextToken<AbortSignal>;
187
+ /**
188
+ * Per-request HttpContextToken carrying a timeout in milliseconds.
189
+ *
190
+ * When set, overrides the transport-level `requestTimeout` for this single
191
+ * call. `0` or non-finite disables the timeout for this request only.
192
+ *
193
+ * On expiry the request errors with `WorkerHttpTimeoutError`.
194
+ */
195
+ declare const WORKER_HTTP_TIMEOUT: HttpContextToken<number>;
171
196
  /**
172
197
  * Optional serializer for crossing the worker boundary.
173
198
  * Provided via `withWorkerSerialization()`. Defaults to `null` (structured clone).
@@ -422,6 +447,20 @@ declare class WorkerHttpBackend extends HttpBackend implements OnDestroy {
422
447
  interface WorkerRequestOptions {
423
448
  /** Target worker ID. Overrides URL-pattern routing for this specific request. */
424
449
  worker?: string | null;
450
+ /**
451
+ * External `AbortSignal`. When it fires, the backend posts a `cancel` to
452
+ * the worker and the request errors with `WorkerHttpAbortError` (wrapped in
453
+ * `HttpErrorResponse`). Useful with `AbortController` or
454
+ * `takeUntilDestroyed()`.
455
+ */
456
+ signal?: AbortSignal;
457
+ /**
458
+ * Per-request timeout in milliseconds. Overrides the transport-level
459
+ * `requestTimeout` for this single call. On expiry the request errors with
460
+ * `WorkerHttpTimeoutError`. `0` or non-finite disables the timeout for this
461
+ * request only.
462
+ */
463
+ timeout?: number;
425
464
  context?: HttpContext;
426
465
  headers?: Record<string, string | string[]>;
427
466
  params?: Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>;
@@ -494,5 +533,5 @@ declare function matchWorkerRoute(url: string, routes: Array<{
494
533
  priority?: number;
495
534
  }>): string | null;
496
535
 
497
- export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
536
+ export { WORKER_HTTP_INTERCEPTORS_TOKEN, WORKER_HTTP_SERIALIZER_TOKEN, WORKER_HTTP_SIGNAL, WORKER_HTTP_STREAMS_POLYFILL_TOKEN, WORKER_HTTP_TIMEOUT, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withTelemetry, withWorkerConfigs, withWorkerFallback, withWorkerInterceptors, withWorkerRoutes, withWorkerSerialization, withWorkerStreamsPolyfill };
498
537
  export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpErrorEvent, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerHttpRequestEvent, WorkerHttpResponseEvent, WorkerHttpTelemetry, WorkerHttpTelemetryEventBase, WorkerHttpTransportKind, WorkerInterceptorSpecsMap, WorkerRequestOptions, WorkerRoute };
@@ -109,13 +109,26 @@ interface WorkerErrorResponse {
109
109
  statusText?: string;
110
110
  };
111
111
  }
112
+ /**
113
+ * Per-request options accepted by `WorkerTransport.execute()`.
114
+ *
115
+ * - `signal` — external `AbortSignal`. When it fires, the transport posts a
116
+ * `cancel` message to the worker and rejects the Observable with
117
+ * `WorkerHttpAbortError`.
118
+ * - `timeout` — overrides the transport-level `requestTimeout` for this single
119
+ * call. `0` or non-finite disables the timeout for this request.
120
+ */
121
+ interface WorkerExecuteOptions {
122
+ signal?: AbortSignal;
123
+ timeout?: number;
124
+ }
112
125
  /**
113
126
  * Typed transport interface for communicating with a web worker.
114
127
  * Observable-based: unsubscribing sends a cancel message to the worker.
115
128
  */
116
129
  interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
117
130
  /** Send a request to the worker and get an Observable response */
118
- execute(request: TRequest): Observable<TResponse>;
131
+ execute(request: TRequest, options?: WorkerExecuteOptions): Observable<TResponse>;
119
132
  /** Terminate all workers and release resources */
120
133
  terminate(): void;
121
134
  /** Whether the transport has active workers */
@@ -173,6 +186,35 @@ declare class WorkerHttpTimeoutError extends Error {
173
186
  constructor(timeoutMs: number);
174
187
  }
175
188
 
189
+ /**
190
+ * Thrown by `createWorkerTransport` when a request is aborted via an external
191
+ * `AbortSignal` passed to `execute(request, { signal })`.
192
+ *
193
+ * Distinct from `WorkerHttpTimeoutError` (which fires when the per-request
194
+ * `timeout` elapses) and from a silent unsubscribe (which sends a `cancel`
195
+ * message but does not surface an error to the subscriber, since RxJS already
196
+ * tore down the stream).
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * const ac = new AbortController();
201
+ * transport.execute(req, { signal: ac.signal }).subscribe({
202
+ * error: (err) => {
203
+ * if (err instanceof WorkerHttpAbortError) {
204
+ * // user-driven cancellation; usually safe to ignore in UI
205
+ * }
206
+ * },
207
+ * });
208
+ * ac.abort('user navigated away');
209
+ * ```
210
+ */
211
+ declare class WorkerHttpAbortError extends Error {
212
+ readonly name = "WorkerHttpAbortError";
213
+ /** The reason passed to `AbortController.abort(reason)`, if any. */
214
+ readonly reason: unknown;
215
+ constructor(reason?: unknown);
216
+ }
217
+
176
218
  /**
177
219
  * Scans a payload one level deep and collects every `Transferable` instance
178
220
  * (ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream,
@@ -193,5 +235,5 @@ declare class WorkerHttpTimeoutError extends Error {
193
235
  */
194
236
  declare function detectTransferables(payload: unknown): Transferable[];
195
237
 
196
- export { WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
197
- export type { WorkerErrorResponse, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };
238
+ export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
239
+ export type { WorkerErrorResponse, WorkerExecuteOptions, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };