@angular-helpers/worker-http 0.2.0 → 0.3.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
|
@@ -16,13 +16,13 @@ On top of that, workers provide a natural isolation boundary for security-sensit
|
|
|
16
16
|
|
|
17
17
|
## Package map
|
|
18
18
|
|
|
19
|
-
| Entry point | Description | Status
|
|
20
|
-
| ------------------------------------------- | ---------------------------------------------------------------- |
|
|
21
|
-
| `@angular-helpers/worker-http/transport` | Typed RPC bridge, round-robin pool, cancellation | ✅ Available
|
|
22
|
-
| `@angular-helpers/worker-http/serializer` | Pluggable serialization (structured clone, seroval, auto-detect) | ✅ Available
|
|
23
|
-
| `@angular-helpers/worker-http/interceptors` | Pure-function interceptor pipeline for workers | ✅ Available
|
|
24
|
-
| `@angular-helpers/worker-http/crypto` | WebCrypto primitives (HMAC, AES-GCM, SHA hashing) | ✅ Available
|
|
25
|
-
| `@angular-helpers/worker-http/backend` | Angular `HttpBackend` replacement — `provideWorkerHttpClient()` |
|
|
19
|
+
| Entry point | Description | Status |
|
|
20
|
+
| ------------------------------------------- | ---------------------------------------------------------------- | ------------ |
|
|
21
|
+
| `@angular-helpers/worker-http/transport` | Typed RPC bridge, round-robin pool, cancellation | ✅ Available |
|
|
22
|
+
| `@angular-helpers/worker-http/serializer` | Pluggable serialization (structured clone, seroval, auto-detect) | ✅ Available |
|
|
23
|
+
| `@angular-helpers/worker-http/interceptors` | Pure-function interceptor pipeline for workers | ✅ Available |
|
|
24
|
+
| `@angular-helpers/worker-http/crypto` | WebCrypto primitives (HMAC, AES-GCM, SHA hashing) | ✅ Available |
|
|
25
|
+
| `@angular-helpers/worker-http/backend` | Angular `HttpBackend` replacement — `provideWorkerHttpClient()` | ✅ Available |
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -313,13 +313,9 @@ const hash = await hasher.hash('SHA-256', data); // → hex string
|
|
|
313
313
|
|
|
314
314
|
---
|
|
315
315
|
|
|
316
|
-
### `/backend` — Angular `HttpBackend` replacement
|
|
316
|
+
### `/backend` — Angular `HttpBackend` replacement
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
The goal is a drop-in replacement for Angular's `HttpBackend` that transparently routes requests to the appropriate worker.
|
|
321
|
-
|
|
322
|
-
**Planned API:**
|
|
318
|
+
Drop-in replacement for Angular's `HttpBackend` that transparently routes `HttpClient` requests to Web Workers. Use `WorkerHttpClient` exactly like `HttpClient` — the routing is invisible to application code.
|
|
323
319
|
|
|
324
320
|
```typescript
|
|
325
321
|
// app.config.ts
|
|
@@ -328,34 +324,75 @@ import {
|
|
|
328
324
|
withWorkerConfigs,
|
|
329
325
|
withWorkerRoutes,
|
|
330
326
|
withWorkerFallback,
|
|
327
|
+
withWorkerSerialization,
|
|
331
328
|
} from '@angular-helpers/worker-http/backend';
|
|
329
|
+
import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
|
|
332
330
|
|
|
333
|
-
|
|
331
|
+
export const appConfig: ApplicationConfig = {
|
|
334
332
|
providers: [
|
|
335
333
|
provideWorkerHttpClient(
|
|
336
334
|
withWorkerConfigs([
|
|
335
|
+
{
|
|
336
|
+
id: 'api',
|
|
337
|
+
workerUrl: new URL('./workers/api.worker', import.meta.url),
|
|
338
|
+
maxInstances: 2, // round-robin pool
|
|
339
|
+
},
|
|
337
340
|
{
|
|
338
341
|
id: 'secure',
|
|
339
342
|
workerUrl: new URL('./workers/secure.worker', import.meta.url),
|
|
340
|
-
maxInstances: 2,
|
|
341
343
|
},
|
|
342
344
|
]),
|
|
343
|
-
withWorkerRoutes([
|
|
344
|
-
|
|
345
|
+
withWorkerRoutes([
|
|
346
|
+
{ pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
|
|
347
|
+
{ pattern: /\/api\//, worker: 'api', priority: 5 },
|
|
348
|
+
]),
|
|
349
|
+
withWorkerFallback('main-thread'), // SSR-safe
|
|
350
|
+
withWorkerSerialization(createSerovalSerializer()), // optional: complex bodies
|
|
345
351
|
),
|
|
346
352
|
],
|
|
347
|
-
}
|
|
353
|
+
};
|
|
348
354
|
|
|
349
|
-
// data.service.ts —
|
|
355
|
+
// data.service.ts — WorkerHttpClient is a drop-in for HttpClient
|
|
350
356
|
export class DataService {
|
|
351
|
-
private http = inject(
|
|
357
|
+
private http = inject(WorkerHttpClient);
|
|
352
358
|
|
|
353
|
-
|
|
354
|
-
return this.http.get<
|
|
359
|
+
getUsers() {
|
|
360
|
+
return this.http.get<User[]>('/api/users'); // auto-routed to 'api' worker
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
getSecureData() {
|
|
364
|
+
// per-request override via { worker } option or WORKER_TARGET context token
|
|
365
|
+
return this.http.get('/api/secure/payments', { worker: 'secure' });
|
|
355
366
|
}
|
|
356
367
|
}
|
|
368
|
+
|
|
369
|
+
// workers/api.worker.ts — runs on a separate OS thread
|
|
370
|
+
import {
|
|
371
|
+
createWorkerPipeline,
|
|
372
|
+
loggingInterceptor,
|
|
373
|
+
retryInterceptor,
|
|
374
|
+
cacheInterceptor,
|
|
375
|
+
} from '@angular-helpers/worker-http/interceptors';
|
|
376
|
+
|
|
377
|
+
createWorkerPipeline([
|
|
378
|
+
loggingInterceptor(),
|
|
379
|
+
retryInterceptor({ maxRetries: 3 }),
|
|
380
|
+
cacheInterceptor({ ttl: 60000 }),
|
|
381
|
+
]);
|
|
357
382
|
```
|
|
358
383
|
|
|
384
|
+
**Features:**
|
|
385
|
+
|
|
386
|
+
- `provideWorkerHttpClient(...features)` — replaces `provideHttpClient()`; do not use both
|
|
387
|
+
- `withWorkerConfigs(configs)` — register named workers with optional pool size
|
|
388
|
+
- `withWorkerRoutes(routes)` — URL-pattern routing with priority ordering
|
|
389
|
+
- `withWorkerFallback(strategy)` — `'main-thread'` (SSR-safe) or `'error'`
|
|
390
|
+
- `withWorkerSerialization(serializer)` — plug in `createSerovalSerializer()` for complex request bodies (`Date`, `Map`, `Set`)
|
|
391
|
+
- `WORKER_TARGET` — `HttpContextToken<string | null>` for per-request worker routing via `HttpContext`
|
|
392
|
+
- `WorkerHttpClient` — `HttpClient` wrapper with optional `{ worker: string }` routing field
|
|
393
|
+
- `WorkerHttpBackend` — the `HttpBackend` implementation (injectable for advanced use)
|
|
394
|
+
- `matchWorkerRoute(url, routes)` — pure utility to test routing rules
|
|
395
|
+
|
|
359
396
|
---
|
|
360
397
|
|
|
361
398
|
## Design principles
|
|
@@ -1,3 +1,372 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injectable, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
+
import { HttpContextToken, HttpHeaders, HttpResponse, HttpBackend, FetchBackend, HttpErrorResponse, HttpClient, HttpContext, provideHttpClient, withFetch } from '@angular/common/http';
|
|
4
|
+
import { throwError } from 'rxjs';
|
|
5
|
+
import { map, catchError } from 'rxjs/operators';
|
|
6
|
+
import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Per-request HttpContextToken that carries the target worker ID.
|
|
10
|
+
*
|
|
11
|
+
* `null` → use URL-pattern auto-routing (or main-thread fallback if no route matches).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // With WorkerHttpClient (recommended)
|
|
16
|
+
* this.http.get('/api/data', { worker: 'secure' });
|
|
17
|
+
*
|
|
18
|
+
* // With standard HttpClient (power user)
|
|
19
|
+
* this.http.get('/api/data', {
|
|
20
|
+
* context: new HttpContext().set(WORKER_TARGET, 'secure'),
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
const WORKER_TARGET = new HttpContextToken(() => null);
|
|
25
|
+
/**
|
|
26
|
+
* Registered worker definitions provided via `withWorkerConfigs()`.
|
|
27
|
+
*/
|
|
28
|
+
const WORKER_HTTP_CONFIGS_TOKEN = new InjectionToken('WorkerHttpConfigs', {
|
|
29
|
+
factory: () => [],
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* URL-pattern routing rules provided via `withWorkerRoutes()`.
|
|
33
|
+
*/
|
|
34
|
+
const WORKER_HTTP_ROUTES_TOKEN = new InjectionToken('WorkerHttpRoutes', {
|
|
35
|
+
factory: () => [],
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Fallback strategy provided via `withWorkerFallback()`.
|
|
39
|
+
* Defaults to `'main-thread'` (safe for SSR / unsupported environments).
|
|
40
|
+
*/
|
|
41
|
+
const WORKER_HTTP_FALLBACK_TOKEN = new InjectionToken('WorkerHttpFallback', { factory: () => 'main-thread' });
|
|
42
|
+
/**
|
|
43
|
+
* Optional serializer for crossing the worker boundary.
|
|
44
|
+
* Provided via `withWorkerSerialization()`. Defaults to `null` (structured clone).
|
|
45
|
+
*
|
|
46
|
+
* When set, `WorkerHttpBackend` serializes the request body before `postMessage`
|
|
47
|
+
* using this serializer. The worker-side `createWorkerPipeline()` receives the
|
|
48
|
+
* serialized form — add a worker interceptor to deserialize it if needed.
|
|
49
|
+
*/
|
|
50
|
+
const WORKER_HTTP_SERIALIZER_TOKEN = new InjectionToken('WorkerHttpSerializer', { factory: () => null });
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Converts an Angular `HttpRequest` into a structured-clone-safe POJO
|
|
54
|
+
* that can be sent to a web worker via `postMessage`.
|
|
55
|
+
*
|
|
56
|
+
* Notes:
|
|
57
|
+
* - `urlWithParams` is used so query params embedded via `HttpParams` are included.
|
|
58
|
+
* - The `context` field is intentionally left empty: `HttpContext` uses class references
|
|
59
|
+
* as keys which cannot cross the worker boundary.
|
|
60
|
+
*/
|
|
61
|
+
function toSerializableRequest(req) {
|
|
62
|
+
const headers = {};
|
|
63
|
+
for (const name of req.headers.keys()) {
|
|
64
|
+
headers[name.toLowerCase()] = req.headers.getAll(name) ?? [];
|
|
65
|
+
}
|
|
66
|
+
const params = {};
|
|
67
|
+
for (const name of req.params.keys()) {
|
|
68
|
+
params[name] = req.params.getAll(name) ?? [];
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
method: req.method,
|
|
72
|
+
url: req.urlWithParams,
|
|
73
|
+
headers,
|
|
74
|
+
params,
|
|
75
|
+
body: req.body,
|
|
76
|
+
responseType: req.responseType,
|
|
77
|
+
withCredentials: req.withCredentials,
|
|
78
|
+
context: {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Converts a worker `SerializableResponse` back into an Angular `HttpResponse`.
|
|
83
|
+
*/
|
|
84
|
+
function toHttpResponse(res, req) {
|
|
85
|
+
let headers = new HttpHeaders();
|
|
86
|
+
for (const [key, values] of Object.entries(res.headers)) {
|
|
87
|
+
for (const value of values) {
|
|
88
|
+
headers = headers.append(key, value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new HttpResponse({
|
|
92
|
+
body: res.body,
|
|
93
|
+
headers,
|
|
94
|
+
status: res.status,
|
|
95
|
+
statusText: res.statusText,
|
|
96
|
+
url: res.url || req.urlWithParams,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Matches a URL against a sorted list of `WorkerRoute` rules.
|
|
101
|
+
* Rules with higher `priority` are evaluated first.
|
|
102
|
+
* Returns the matched worker ID or `null` if no rule matches.
|
|
103
|
+
*/
|
|
104
|
+
function matchWorkerRoute(url, routes) {
|
|
105
|
+
const sorted = [...routes].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
106
|
+
for (const route of sorted) {
|
|
107
|
+
const pattern = typeof route.pattern === 'string' ? new RegExp(route.pattern) : route.pattern;
|
|
108
|
+
if (pattern.test(url)) {
|
|
109
|
+
return route.worker;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Angular `HttpBackend` replacement that routes HTTP requests to web workers.
|
|
117
|
+
*
|
|
118
|
+
* Registered via `provideWorkerHttpClient()`. Not meant to be used directly.
|
|
119
|
+
*
|
|
120
|
+
* Flow per request:
|
|
121
|
+
* 1. Check SSR: if `Worker` is undefined → fallback strategy
|
|
122
|
+
* 2. Resolve target worker ID from `WORKER_TARGET` context or URL-pattern routing
|
|
123
|
+
* 3. Serialize `HttpRequest` → `SerializableRequest` (structured-clone safe)
|
|
124
|
+
* 4. Dispatch to the worker's `WorkerTransport`
|
|
125
|
+
* 5. Deserialize `SerializableResponse` → `HttpResponse`
|
|
126
|
+
*/
|
|
127
|
+
class WorkerHttpBackend extends HttpBackend {
|
|
128
|
+
configs = inject(WORKER_HTTP_CONFIGS_TOKEN);
|
|
129
|
+
routes = inject(WORKER_HTTP_ROUTES_TOKEN);
|
|
130
|
+
fallback = inject(WORKER_HTTP_FALLBACK_TOKEN);
|
|
131
|
+
serializer = inject(WORKER_HTTP_SERIALIZER_TOKEN);
|
|
132
|
+
fetchBackend = inject(FetchBackend, { optional: true });
|
|
133
|
+
transports = new Map();
|
|
134
|
+
handle(req) {
|
|
135
|
+
if (typeof Worker === 'undefined') {
|
|
136
|
+
return this.handleFallback(req, 'Web Workers are not available in this environment (SSR)');
|
|
137
|
+
}
|
|
138
|
+
const workerId = req.context.get(WORKER_TARGET) ?? matchWorkerRoute(req.url, this.routes);
|
|
139
|
+
if (!workerId) {
|
|
140
|
+
return this.handleFallback(req, `No worker route matched for URL: ${req.url}`);
|
|
141
|
+
}
|
|
142
|
+
const config = this.configs.find((c) => c.id === workerId);
|
|
143
|
+
if (!config) {
|
|
144
|
+
return throwError(() => new Error(`[WorkerHttpBackend] Unknown worker id: "${workerId}". ` +
|
|
145
|
+
`Register it via withWorkerConfigs([{ id: "${workerId}", workerUrl: ... }]).`));
|
|
146
|
+
}
|
|
147
|
+
const transport = this.getOrCreateTransport(config);
|
|
148
|
+
const serializable = toSerializableRequest(req);
|
|
149
|
+
const body = this.serializer !== null && serializable.body !== null && serializable.body !== undefined
|
|
150
|
+
? this.serializer.serialize(serializable.body).data
|
|
151
|
+
: serializable.body;
|
|
152
|
+
const payload = body !== serializable.body ? { ...serializable, body } : serializable;
|
|
153
|
+
return transport.execute(payload).pipe(map((res) => toHttpResponse(res, req)), catchError((err) => throwError(() => new HttpErrorResponse({
|
|
154
|
+
error: err,
|
|
155
|
+
status: 0,
|
|
156
|
+
statusText: 'Worker Error',
|
|
157
|
+
url: req.urlWithParams,
|
|
158
|
+
}))));
|
|
159
|
+
}
|
|
160
|
+
ngOnDestroy() {
|
|
161
|
+
for (const transport of this.transports.values()) {
|
|
162
|
+
transport.terminate();
|
|
163
|
+
}
|
|
164
|
+
this.transports.clear();
|
|
165
|
+
}
|
|
166
|
+
getOrCreateTransport(config) {
|
|
167
|
+
const existing = this.transports.get(config.id);
|
|
168
|
+
if (existing)
|
|
169
|
+
return existing;
|
|
170
|
+
const transport = createWorkerTransport({
|
|
171
|
+
workerFactory: () => new Worker(config.workerUrl, { type: 'module' }),
|
|
172
|
+
maxInstances: config.maxInstances ?? 1,
|
|
173
|
+
});
|
|
174
|
+
this.transports.set(config.id, transport);
|
|
175
|
+
return transport;
|
|
176
|
+
}
|
|
177
|
+
handleFallback(req, reason) {
|
|
178
|
+
if (this.fallback === 'error' || !this.fetchBackend) {
|
|
179
|
+
return throwError(() => new Error(`[WorkerHttpBackend] ${reason}`));
|
|
180
|
+
}
|
|
181
|
+
return this.fetchBackend.handle(req);
|
|
182
|
+
}
|
|
183
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
184
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend });
|
|
185
|
+
}
|
|
186
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpBackend, decorators: [{
|
|
187
|
+
type: Injectable
|
|
188
|
+
}] });
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Convenience wrapper over `HttpClient` that adds an optional `{ worker }` field
|
|
192
|
+
* to every method. Under the hood it sets `WORKER_TARGET` on the `HttpContext` —
|
|
193
|
+
* the caller never has to touch the context manually.
|
|
194
|
+
*
|
|
195
|
+
* Usage is identical to `HttpClient` — just inject `WorkerHttpClient` instead.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* @Injectable({ providedIn: 'root' })
|
|
200
|
+
* export class DataService {
|
|
201
|
+
* private readonly http = inject(WorkerHttpClient);
|
|
202
|
+
*
|
|
203
|
+
* getUsers(): Observable<User[]> {
|
|
204
|
+
* return this.http.get<User[]>('/api/users'); // auto-routed by URL pattern
|
|
205
|
+
* }
|
|
206
|
+
*
|
|
207
|
+
* getSensitiveReport(): Observable<Report> {
|
|
208
|
+
* return this.http.get<Report>('/api/secure/reports', { worker: 'secure' });
|
|
209
|
+
* }
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
class WorkerHttpClient {
|
|
214
|
+
http = inject(HttpClient);
|
|
215
|
+
get(url, options) {
|
|
216
|
+
return this.http.get(url, this.withWorker(options));
|
|
217
|
+
}
|
|
218
|
+
post(url, body, options) {
|
|
219
|
+
return this.http.post(url, body, this.withWorker(options));
|
|
220
|
+
}
|
|
221
|
+
put(url, body, options) {
|
|
222
|
+
return this.http.put(url, body, this.withWorker(options));
|
|
223
|
+
}
|
|
224
|
+
patch(url, body, options) {
|
|
225
|
+
return this.http.patch(url, body, this.withWorker(options));
|
|
226
|
+
}
|
|
227
|
+
delete(url, options) {
|
|
228
|
+
return this.http.delete(url, this.withWorker(options));
|
|
229
|
+
}
|
|
230
|
+
head(url, options) {
|
|
231
|
+
return this.http.head(url, this.withWorker(options));
|
|
232
|
+
}
|
|
233
|
+
withWorker(options) {
|
|
234
|
+
const { worker = null, context, ...rest } = options ?? {};
|
|
235
|
+
return {
|
|
236
|
+
...rest,
|
|
237
|
+
context: (context ?? new HttpContext()).set(WORKER_TARGET, worker),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
241
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient });
|
|
242
|
+
}
|
|
243
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WorkerHttpClient, decorators: [{
|
|
244
|
+
type: Injectable
|
|
245
|
+
}] });
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Sets up the worker HTTP infrastructure and replaces Angular's `HttpBackend`
|
|
249
|
+
* with `WorkerHttpBackend`.
|
|
250
|
+
*
|
|
251
|
+
* Drop-in companion to `provideHttpClient()`. Can be used INSTEAD of it —
|
|
252
|
+
* `HttpClient` and the full interceptor chain are included automatically.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* // app.config.ts
|
|
257
|
+
* export const appConfig: ApplicationConfig = {
|
|
258
|
+
* providers: [
|
|
259
|
+
* provideWorkerHttpClient(
|
|
260
|
+
* withWorkerConfigs([
|
|
261
|
+
* { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
|
|
262
|
+
* ]),
|
|
263
|
+
* withWorkerRoutes([
|
|
264
|
+
* { pattern: /\/api\//, worker: 'public', priority: 1 },
|
|
265
|
+
* ]),
|
|
266
|
+
* withWorkerFallback('main-thread'),
|
|
267
|
+
* ),
|
|
268
|
+
* ],
|
|
269
|
+
* };
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
function provideWorkerHttpClient(...features) {
|
|
273
|
+
const featureProviders = features.flatMap((f) => f.providers);
|
|
274
|
+
return makeEnvironmentProviders([
|
|
275
|
+
provideHttpClient(withFetch()),
|
|
276
|
+
FetchBackend,
|
|
277
|
+
{ provide: HttpBackend, useClass: WorkerHttpBackend },
|
|
278
|
+
WorkerHttpClient,
|
|
279
|
+
...featureProviders,
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Registers worker definitions (id + workerUrl + optional pool size).
|
|
284
|
+
*
|
|
285
|
+
* At least one config is required for any request to reach a worker.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* withWorkerConfigs([
|
|
290
|
+
* { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
|
|
291
|
+
* { id: 'secure', workerUrl: new URL('./workers/secure.worker', import.meta.url), maxInstances: 2 },
|
|
292
|
+
* ])
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
function withWorkerConfigs(configs) {
|
|
296
|
+
return {
|
|
297
|
+
kind: 'WorkerConfigs',
|
|
298
|
+
providers: [{ provide: WORKER_HTTP_CONFIGS_TOKEN, useValue: configs }],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Declares URL-pattern → worker routing rules evaluated in priority order.
|
|
303
|
+
*
|
|
304
|
+
* When a request URL matches a pattern, the associated worker handles it.
|
|
305
|
+
* Explicit `WORKER_TARGET` context always takes precedence over routes.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* withWorkerRoutes([
|
|
310
|
+
* { pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
|
|
311
|
+
* { pattern: /\/api\//, worker: 'public', priority: 1 },
|
|
312
|
+
* ])
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
function withWorkerRoutes(routes) {
|
|
316
|
+
return {
|
|
317
|
+
kind: 'WorkerRoutes',
|
|
318
|
+
providers: [{ provide: WORKER_HTTP_ROUTES_TOKEN, useValue: routes }],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Sets the fallback strategy when workers are unavailable (SSR, old browsers,
|
|
323
|
+
* or when no route matches).
|
|
324
|
+
*
|
|
325
|
+
* - `'main-thread'` (default) — silently delegates to `FetchBackend`
|
|
326
|
+
* - `'error'` — throws, forcing explicit handling in the application
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* withWorkerFallback('main-thread') // SSR-safe
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
function withWorkerFallback(strategy) {
|
|
334
|
+
return {
|
|
335
|
+
kind: 'WorkerFallback',
|
|
336
|
+
providers: [{ provide: WORKER_HTTP_FALLBACK_TOKEN, useValue: strategy }],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Configures a custom serializer for crossing the worker boundary.
|
|
341
|
+
*
|
|
342
|
+
* By default `WorkerHttpBackend` relies on the browser's structured clone algorithm
|
|
343
|
+
* (safe for plain objects, arrays, primitives, `Date`, `ArrayBuffer`).
|
|
344
|
+
* Use `withWorkerSerialization` when your request bodies contain types that
|
|
345
|
+
* structured clone cannot handle (e.g. class instances, circular references, `Map`, `Set`).
|
|
346
|
+
*
|
|
347
|
+
* **Worker-side note:** The serialized form is what the worker receives as `req.body`.
|
|
348
|
+
* If you use `createSerovalSerializer` or similar, add a worker-side interceptor
|
|
349
|
+
* to deserialize the body before calling `fetch()`.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
|
|
354
|
+
*
|
|
355
|
+
* provideWorkerHttpClient(
|
|
356
|
+
* withWorkerConfigs([...]),
|
|
357
|
+
* withWorkerSerialization(createSerovalSerializer()),
|
|
358
|
+
* )
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
function withWorkerSerialization(serializer) {
|
|
362
|
+
return {
|
|
363
|
+
kind: 'WorkerSerialization',
|
|
364
|
+
providers: [{ provide: WORKER_HTTP_SERIALIZER_TOKEN, useValue: serializer }],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
1
368
|
/**
|
|
2
369
|
* Generated bundle index. Do not edit.
|
|
3
370
|
*/
|
|
371
|
+
|
|
372
|
+
export { WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerRoutes, withWorkerSerialization };
|
|
@@ -228,7 +228,7 @@ function arrayBufferToHex$1(buffer) {
|
|
|
228
228
|
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
229
229
|
}
|
|
230
230
|
function defaultPayloadBuilder(req) {
|
|
231
|
-
const body = req.body
|
|
231
|
+
const body = req.body !== null && req.body !== undefined ? JSON.stringify(req.body) : '';
|
|
232
232
|
return `${req.method}:${req.url}:${body}`;
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
@@ -315,7 +315,7 @@ function loggingInterceptor(config) {
|
|
|
315
315
|
catch (error) {
|
|
316
316
|
const elapsedMs = Date.now() - startMs;
|
|
317
317
|
const status = error.status;
|
|
318
|
-
const label = status
|
|
318
|
+
const label = status !== null && status !== undefined ? String(status) : 'NETWORK_ERROR';
|
|
319
319
|
safeLog(`[worker] ✕ ${label} ${req.url} (${elapsedMs}ms)`, error);
|
|
320
320
|
throw error;
|
|
321
321
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/worker-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Provider, InjectionToken, EnvironmentProviders, OnDestroy } from '@angular/core';
|
|
3
|
+
import { HttpContextToken, HttpBackend, HttpRequest, HttpEvent, HttpContext, HttpResponse } from '@angular/common/http';
|
|
4
|
+
import { WorkerSerializer } from '@angular-helpers/worker-http/serializer';
|
|
5
|
+
import { Observable } from 'rxjs';
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Discriminated union for worker HTTP feature kinds.
|
|
@@ -38,5 +42,255 @@ interface WorkerRoute {
|
|
|
38
42
|
* Fallback strategy when workers are unavailable (SSR, old browsers).
|
|
39
43
|
*/
|
|
40
44
|
type WorkerFallbackStrategy = 'main-thread' | 'error';
|
|
45
|
+
/**
|
|
46
|
+
* Serializable HTTP request — POJO version of Angular's HttpRequest.
|
|
47
|
+
* Structured-clone safe: no classes, no functions, no prototype chains.
|
|
48
|
+
*/
|
|
49
|
+
interface SerializableRequest {
|
|
50
|
+
method: string;
|
|
51
|
+
url: string;
|
|
52
|
+
headers: Record<string, string[]>;
|
|
53
|
+
params: Record<string, string[]>;
|
|
54
|
+
body: unknown;
|
|
55
|
+
responseType: 'json' | 'text' | 'blob' | 'arraybuffer';
|
|
56
|
+
withCredentials: boolean;
|
|
57
|
+
context: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Serializable HTTP response — POJO version of Angular's HttpResponse.
|
|
61
|
+
*/
|
|
62
|
+
interface SerializableResponse {
|
|
63
|
+
status: number;
|
|
64
|
+
statusText: string;
|
|
65
|
+
headers: Record<string, string[]>;
|
|
66
|
+
body: unknown;
|
|
67
|
+
url: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Per-request HttpContextToken that carries the target worker ID.
|
|
72
|
+
*
|
|
73
|
+
* `null` → use URL-pattern auto-routing (or main-thread fallback if no route matches).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* // With WorkerHttpClient (recommended)
|
|
78
|
+
* this.http.get('/api/data', { worker: 'secure' });
|
|
79
|
+
*
|
|
80
|
+
* // With standard HttpClient (power user)
|
|
81
|
+
* this.http.get('/api/data', {
|
|
82
|
+
* context: new HttpContext().set(WORKER_TARGET, 'secure'),
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare const WORKER_TARGET: HttpContextToken<string>;
|
|
87
|
+
/**
|
|
88
|
+
* Optional serializer for crossing the worker boundary.
|
|
89
|
+
* Provided via `withWorkerSerialization()`. Defaults to `null` (structured clone).
|
|
90
|
+
*
|
|
91
|
+
* When set, `WorkerHttpBackend` serializes the request body before `postMessage`
|
|
92
|
+
* using this serializer. The worker-side `createWorkerPipeline()` receives the
|
|
93
|
+
* serialized form — add a worker interceptor to deserialize it if needed.
|
|
94
|
+
*/
|
|
95
|
+
declare const WORKER_HTTP_SERIALIZER_TOKEN: InjectionToken<WorkerSerializer>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sets up the worker HTTP infrastructure and replaces Angular's `HttpBackend`
|
|
99
|
+
* with `WorkerHttpBackend`.
|
|
100
|
+
*
|
|
101
|
+
* Drop-in companion to `provideHttpClient()`. Can be used INSTEAD of it —
|
|
102
|
+
* `HttpClient` and the full interceptor chain are included automatically.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // app.config.ts
|
|
107
|
+
* export const appConfig: ApplicationConfig = {
|
|
108
|
+
* providers: [
|
|
109
|
+
* provideWorkerHttpClient(
|
|
110
|
+
* withWorkerConfigs([
|
|
111
|
+
* { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
|
|
112
|
+
* ]),
|
|
113
|
+
* withWorkerRoutes([
|
|
114
|
+
* { pattern: /\/api\//, worker: 'public', priority: 1 },
|
|
115
|
+
* ]),
|
|
116
|
+
* withWorkerFallback('main-thread'),
|
|
117
|
+
* ),
|
|
118
|
+
* ],
|
|
119
|
+
* };
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function provideWorkerHttpClient(...features: WorkerHttpFeature<WorkerHttpFeatureKind>[]): EnvironmentProviders;
|
|
123
|
+
/**
|
|
124
|
+
* Registers worker definitions (id + workerUrl + optional pool size).
|
|
125
|
+
*
|
|
126
|
+
* At least one config is required for any request to reach a worker.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* withWorkerConfigs([
|
|
131
|
+
* { id: 'public', workerUrl: new URL('./workers/public.worker', import.meta.url) },
|
|
132
|
+
* { id: 'secure', workerUrl: new URL('./workers/secure.worker', import.meta.url), maxInstances: 2 },
|
|
133
|
+
* ])
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
declare function withWorkerConfigs(configs: WorkerConfig[]): WorkerHttpFeature<'WorkerConfigs'>;
|
|
137
|
+
/**
|
|
138
|
+
* Declares URL-pattern → worker routing rules evaluated in priority order.
|
|
139
|
+
*
|
|
140
|
+
* When a request URL matches a pattern, the associated worker handles it.
|
|
141
|
+
* Explicit `WORKER_TARGET` context always takes precedence over routes.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* withWorkerRoutes([
|
|
146
|
+
* { pattern: /\/api\/secure\//, worker: 'secure', priority: 10 },
|
|
147
|
+
* { pattern: /\/api\//, worker: 'public', priority: 1 },
|
|
148
|
+
* ])
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
declare function withWorkerRoutes(routes: WorkerRoute[]): WorkerHttpFeature<'WorkerRoutes'>;
|
|
152
|
+
/**
|
|
153
|
+
* Sets the fallback strategy when workers are unavailable (SSR, old browsers,
|
|
154
|
+
* or when no route matches).
|
|
155
|
+
*
|
|
156
|
+
* - `'main-thread'` (default) — silently delegates to `FetchBackend`
|
|
157
|
+
* - `'error'` — throws, forcing explicit handling in the application
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* withWorkerFallback('main-thread') // SSR-safe
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
declare function withWorkerFallback(strategy: WorkerFallbackStrategy): WorkerHttpFeature<'WorkerFallback'>;
|
|
165
|
+
/**
|
|
166
|
+
* Configures a custom serializer for crossing the worker boundary.
|
|
167
|
+
*
|
|
168
|
+
* By default `WorkerHttpBackend` relies on the browser's structured clone algorithm
|
|
169
|
+
* (safe for plain objects, arrays, primitives, `Date`, `ArrayBuffer`).
|
|
170
|
+
* Use `withWorkerSerialization` when your request bodies contain types that
|
|
171
|
+
* structured clone cannot handle (e.g. class instances, circular references, `Map`, `Set`).
|
|
172
|
+
*
|
|
173
|
+
* **Worker-side note:** The serialized form is what the worker receives as `req.body`.
|
|
174
|
+
* If you use `createSerovalSerializer` or similar, add a worker-side interceptor
|
|
175
|
+
* to deserialize the body before calling `fetch()`.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
|
|
180
|
+
*
|
|
181
|
+
* provideWorkerHttpClient(
|
|
182
|
+
* withWorkerConfigs([...]),
|
|
183
|
+
* withWorkerSerialization(createSerovalSerializer()),
|
|
184
|
+
* )
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
declare function withWorkerSerialization(serializer: WorkerSerializer): WorkerHttpFeature<'WorkerSerialization'>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Angular `HttpBackend` replacement that routes HTTP requests to web workers.
|
|
191
|
+
*
|
|
192
|
+
* Registered via `provideWorkerHttpClient()`. Not meant to be used directly.
|
|
193
|
+
*
|
|
194
|
+
* Flow per request:
|
|
195
|
+
* 1. Check SSR: if `Worker` is undefined → fallback strategy
|
|
196
|
+
* 2. Resolve target worker ID from `WORKER_TARGET` context or URL-pattern routing
|
|
197
|
+
* 3. Serialize `HttpRequest` → `SerializableRequest` (structured-clone safe)
|
|
198
|
+
* 4. Dispatch to the worker's `WorkerTransport`
|
|
199
|
+
* 5. Deserialize `SerializableResponse` → `HttpResponse`
|
|
200
|
+
*/
|
|
201
|
+
declare class WorkerHttpBackend extends HttpBackend implements OnDestroy {
|
|
202
|
+
private readonly configs;
|
|
203
|
+
private readonly routes;
|
|
204
|
+
private readonly fallback;
|
|
205
|
+
private readonly serializer;
|
|
206
|
+
private readonly fetchBackend;
|
|
207
|
+
private readonly transports;
|
|
208
|
+
handle(req: HttpRequest<unknown>): Observable<HttpEvent<unknown>>;
|
|
209
|
+
ngOnDestroy(): void;
|
|
210
|
+
private getOrCreateTransport;
|
|
211
|
+
private handleFallback;
|
|
212
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<WorkerHttpBackend, never>;
|
|
213
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<WorkerHttpBackend>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Options accepted by `WorkerHttpClient` methods.
|
|
218
|
+
* Identical to `HttpClient` options with an optional `worker` field added.
|
|
219
|
+
*/
|
|
220
|
+
interface WorkerRequestOptions {
|
|
221
|
+
/** Target worker ID. Overrides URL-pattern routing for this specific request. */
|
|
222
|
+
worker?: string | null;
|
|
223
|
+
context?: HttpContext;
|
|
224
|
+
headers?: Record<string, string | string[]>;
|
|
225
|
+
params?: Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>;
|
|
226
|
+
responseType?: 'json';
|
|
227
|
+
withCredentials?: boolean;
|
|
228
|
+
observe?: 'body';
|
|
229
|
+
reportProgress?: boolean;
|
|
230
|
+
transferCache?: {
|
|
231
|
+
includeHeaders?: string[];
|
|
232
|
+
} | boolean;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Convenience wrapper over `HttpClient` that adds an optional `{ worker }` field
|
|
236
|
+
* to every method. Under the hood it sets `WORKER_TARGET` on the `HttpContext` —
|
|
237
|
+
* the caller never has to touch the context manually.
|
|
238
|
+
*
|
|
239
|
+
* Usage is identical to `HttpClient` — just inject `WorkerHttpClient` instead.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* @Injectable({ providedIn: 'root' })
|
|
244
|
+
* export class DataService {
|
|
245
|
+
* private readonly http = inject(WorkerHttpClient);
|
|
246
|
+
*
|
|
247
|
+
* getUsers(): Observable<User[]> {
|
|
248
|
+
* return this.http.get<User[]>('/api/users'); // auto-routed by URL pattern
|
|
249
|
+
* }
|
|
250
|
+
*
|
|
251
|
+
* getSensitiveReport(): Observable<Report> {
|
|
252
|
+
* return this.http.get<Report>('/api/secure/reports', { worker: 'secure' });
|
|
253
|
+
* }
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
declare class WorkerHttpClient {
|
|
258
|
+
private readonly http;
|
|
259
|
+
get<T>(url: string, options?: WorkerRequestOptions): Observable<T>;
|
|
260
|
+
post<T>(url: string, body: unknown, options?: WorkerRequestOptions): Observable<T>;
|
|
261
|
+
put<T>(url: string, body: unknown, options?: WorkerRequestOptions): Observable<T>;
|
|
262
|
+
patch<T>(url: string, body: unknown, options?: WorkerRequestOptions): Observable<T>;
|
|
263
|
+
delete<T>(url: string, options?: WorkerRequestOptions): Observable<T>;
|
|
264
|
+
head<T>(url: string, options?: WorkerRequestOptions): Observable<T>;
|
|
265
|
+
private withWorker;
|
|
266
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<WorkerHttpClient, never>;
|
|
267
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<WorkerHttpClient>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Converts an Angular `HttpRequest` into a structured-clone-safe POJO
|
|
272
|
+
* that can be sent to a web worker via `postMessage`.
|
|
273
|
+
*
|
|
274
|
+
* Notes:
|
|
275
|
+
* - `urlWithParams` is used so query params embedded via `HttpParams` are included.
|
|
276
|
+
* - The `context` field is intentionally left empty: `HttpContext` uses class references
|
|
277
|
+
* as keys which cannot cross the worker boundary.
|
|
278
|
+
*/
|
|
279
|
+
declare function toSerializableRequest(req: HttpRequest<unknown>): SerializableRequest;
|
|
280
|
+
/**
|
|
281
|
+
* Converts a worker `SerializableResponse` back into an Angular `HttpResponse`.
|
|
282
|
+
*/
|
|
283
|
+
declare function toHttpResponse(res: SerializableResponse, req: HttpRequest<unknown>): HttpResponse<unknown>;
|
|
284
|
+
/**
|
|
285
|
+
* Matches a URL against a sorted list of `WorkerRoute` rules.
|
|
286
|
+
* Rules with higher `priority` are evaluated first.
|
|
287
|
+
* Returns the matched worker ID or `null` if no rule matches.
|
|
288
|
+
*/
|
|
289
|
+
declare function matchWorkerRoute(url: string, routes: Array<{
|
|
290
|
+
pattern: RegExp | string;
|
|
291
|
+
worker: string;
|
|
292
|
+
priority?: number;
|
|
293
|
+
}>): string | null;
|
|
41
294
|
|
|
42
|
-
export
|
|
295
|
+
export { WORKER_HTTP_SERIALIZER_TOKEN, WORKER_TARGET, WorkerHttpBackend, WorkerHttpClient, matchWorkerRoute, provideWorkerHttpClient, toHttpResponse, toSerializableRequest, withWorkerConfigs, withWorkerFallback, withWorkerRoutes, withWorkerSerialization };
|
|
296
|
+
export type { SerializableRequest, SerializableResponse, WorkerConfig, WorkerFallbackStrategy, WorkerHttpFeature, WorkerHttpFeatureKind, WorkerRequestOptions, WorkerRoute };
|