@angular-helpers/worker-http 0.2.0 → 0.4.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.
@@ -1,180 +1,152 @@
1
1
  /**
2
- * Creates and registers a worker-side HTTP pipeline.
3
- *
4
- * Call this inside a worker file to set up the interceptor chain.
5
- * The pipeline listens for incoming requests via `postMessage`,
6
- * runs them through the interceptor chain, executes `fetch()`,
7
- * and sends the response back.
8
- *
9
- * @example
10
- * ```typescript
11
- * // workers/secure.worker.ts
12
- * import { createWorkerPipeline } from '@angular-helpers/worker-http/interceptors';
13
- * import { hmacSigningInterceptor } from './my-interceptors';
2
+ * Composes interceptor functions around a final handler, producing a single
3
+ * `(req) => Promise<resp>` chain. Pure — no side effects.
4
+ */
5
+ function buildChain(fns, finalHandler) {
6
+ return fns.reduceRight((next, interceptor) => (req) => interceptor(req, next), finalHandler);
7
+ }
8
+ /**
9
+ * Performs the actual `fetch()` call inside the worker, translating the
10
+ * structured-clone-safe `SerializableRequest` into a `Request` and the
11
+ * `Response` back into a `SerializableResponse`.
14
12
  *
15
- * createWorkerPipeline([hmacSigningInterceptor]);
16
- * ```
13
+ * The optional `signal` allows the caller to wire up cancellation via an
14
+ * `AbortController` owned outside of this function.
17
15
  */
18
- function createWorkerPipeline(interceptors) {
19
- const controllers = new Map();
20
- async function executeFetch(req) {
21
- const controller = controllers.get(req.url) ?? new AbortController();
22
- const headers = new Headers();
23
- for (const [key, values] of Object.entries(req.headers)) {
24
- for (const value of values) {
25
- headers.append(key, value);
26
- }
16
+ async function executeFetch(req, signal) {
17
+ const headers = new Headers();
18
+ for (const [key, values] of Object.entries(req.headers)) {
19
+ for (const value of values) {
20
+ headers.append(key, value);
27
21
  }
28
- let url = req.url;
29
- const paramEntries = Object.entries(req.params);
30
- if (paramEntries.length > 0) {
31
- const searchParams = new URLSearchParams();
32
- for (const [key, values] of paramEntries) {
33
- for (const value of values) {
34
- searchParams.append(key, value);
35
- }
22
+ }
23
+ let url = req.url;
24
+ const paramEntries = Object.entries(req.params);
25
+ if (paramEntries.length > 0) {
26
+ const searchParams = new URLSearchParams();
27
+ for (const [key, values] of paramEntries) {
28
+ for (const value of values) {
29
+ searchParams.append(key, value);
36
30
  }
37
- url += (url.includes('?') ? '&' : '?') + searchParams.toString();
38
- }
39
- const fetchInit = {
40
- method: req.method,
41
- headers,
42
- credentials: req.withCredentials ? 'include' : 'same-origin',
43
- signal: controller.signal,
44
- };
45
- if (req.body !== null &&
46
- req.body !== undefined &&
47
- req.method !== 'GET' &&
48
- req.method !== 'HEAD') {
49
- fetchInit.body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
50
- }
51
- const response = await fetch(url, fetchInit);
52
- const responseHeaders = {};
53
- response.headers.forEach((value, key) => {
54
- responseHeaders[key] = responseHeaders[key] ?? [];
55
- responseHeaders[key].push(value);
56
- });
57
- let body;
58
- switch (req.responseType) {
59
- case 'text':
60
- body = await response.text();
61
- break;
62
- case 'arraybuffer':
63
- body = await response.arrayBuffer();
64
- break;
65
- case 'blob':
66
- body = await response.blob();
67
- break;
68
- default:
69
- body = await response.json();
70
31
  }
71
- return {
72
- status: response.status,
73
- statusText: response.statusText,
74
- headers: responseHeaders,
75
- body,
76
- url: response.url,
77
- };
32
+ url += (url.includes('?') ? '&' : '?') + searchParams.toString();
33
+ }
34
+ const fetchInit = {
35
+ method: req.method,
36
+ headers,
37
+ credentials: req.withCredentials ? 'include' : 'same-origin',
38
+ signal,
39
+ };
40
+ if (req.body !== null &&
41
+ req.body !== undefined &&
42
+ req.method !== 'GET' &&
43
+ req.method !== 'HEAD') {
44
+ fetchInit.body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
78
45
  }
79
- function buildChain(fns, finalHandler) {
80
- return fns.reduceRight((next, interceptor) => (req) => interceptor(req, next), finalHandler);
46
+ const response = await fetch(url, fetchInit);
47
+ const responseHeaders = {};
48
+ response.headers.forEach((value, key) => {
49
+ responseHeaders[key] = responseHeaders[key] ?? [];
50
+ responseHeaders[key].push(value);
51
+ });
52
+ let body;
53
+ switch (req.responseType) {
54
+ case 'text':
55
+ body = await response.text();
56
+ break;
57
+ case 'arraybuffer':
58
+ body = await response.arrayBuffer();
59
+ break;
60
+ case 'blob':
61
+ body = await response.blob();
62
+ break;
63
+ default:
64
+ body = await response.json();
81
65
  }
82
- const chain = buildChain(interceptors, executeFetch);
66
+ return {
67
+ status: response.status,
68
+ statusText: response.statusText,
69
+ headers: responseHeaders,
70
+ body,
71
+ url: response.url,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Wires up the worker's `self.onmessage` handler around a built request chain.
77
+ *
78
+ * Owns the per-request `AbortController` map for cancellation support. Posts
79
+ * `response` / `error` messages back to the main thread.
80
+ *
81
+ * Returns a disposer that restores `self.onmessage` to whatever it was before
82
+ * (mainly useful for the configurable pipeline, which swaps the handler when
83
+ * receiving the init message).
84
+ */
85
+ function attachRequestLoop(chain) {
86
+ const controllers = new Map();
87
+ const previous = self.onmessage;
83
88
  self.onmessage = async (event) => {
84
- const { type, requestId, payload } = event.data;
89
+ const { type, requestId, payload } = event.data ?? {};
85
90
  if (type === 'cancel') {
86
91
  controllers.get(requestId)?.abort();
87
92
  controllers.delete(requestId);
88
93
  return;
89
94
  }
90
- if (type === 'request') {
91
- const controller = new AbortController();
92
- controllers.set(requestId, controller);
93
- try {
94
- const response = await chain(payload);
95
- self.postMessage({ type: 'response', requestId, result: response });
96
- }
97
- catch (error) {
98
- self.postMessage({
99
- type: 'error',
100
- requestId,
101
- error: {
102
- message: error instanceof Error ? error.message : String(error),
103
- name: error instanceof Error ? error.name : 'UnknownError',
104
- stack: error instanceof Error ? error.stack : undefined,
105
- },
106
- });
107
- }
108
- finally {
109
- controllers.delete(requestId);
110
- }
95
+ if (type !== 'request') {
96
+ return;
97
+ }
98
+ const controller = new AbortController();
99
+ controllers.set(requestId, controller);
100
+ try {
101
+ const response = await chain(payload);
102
+ self.postMessage({ type: 'response', requestId, result: response });
103
+ }
104
+ catch (error) {
105
+ self.postMessage({
106
+ type: 'error',
107
+ requestId,
108
+ error: {
109
+ message: error instanceof Error ? error.message : String(error),
110
+ name: error instanceof Error ? error.name : 'UnknownError',
111
+ stack: error instanceof Error ? error.stack : undefined,
112
+ },
113
+ });
114
+ }
115
+ finally {
116
+ controllers.delete(requestId);
111
117
  }
112
118
  };
119
+ return () => {
120
+ self.onmessage = previous;
121
+ for (const controller of controllers.values()) {
122
+ controller.abort();
123
+ }
124
+ controllers.clear();
125
+ };
113
126
  }
114
127
 
115
- function parseRetryAfterMs(headerValue) {
116
- const seconds = parseFloat(headerValue);
117
- if (!isNaN(seconds)) {
118
- return Math.max(0, seconds * 1000);
119
- }
120
- const date = new Date(headerValue).getTime();
121
- if (!isNaN(date)) {
122
- return Math.max(0, date - Date.now());
123
- }
124
- return 0;
125
- }
126
- function delay(ms) {
127
- return new Promise((resolve) => setTimeout(resolve, ms));
128
- }
129
128
  /**
130
- * Creates a retry interceptor with exponential backoff.
129
+ * Creates and registers a worker-side HTTP pipeline.
131
130
  *
132
- * Retries requests that fail with specific HTTP status codes.
133
- * Respects the `Retry-After` response header when present.
131
+ * Call this inside a worker file to set up the interceptor chain.
132
+ * The pipeline listens for incoming requests via `postMessage`,
133
+ * runs them through the interceptor chain, executes `fetch()`,
134
+ * and sends the response back.
135
+ *
136
+ * For a runtime-configurable variant whose chain is built from specs sent
137
+ * from the main thread, use `createConfigurableWorkerPipeline()` instead.
134
138
  *
135
139
  * @example
136
140
  * ```typescript
137
- * createWorkerPipeline([
138
- * retryInterceptor({ maxRetries: 3, initialDelay: 500 }),
139
- * ]);
141
+ * // workers/secure.worker.ts
142
+ * import { createWorkerPipeline, hmacSigningInterceptor } from '@angular-helpers/worker-http/interceptors';
143
+ *
144
+ * createWorkerPipeline([hmacSigningInterceptor({ keyMaterial })]);
140
145
  * ```
141
146
  */
142
- function retryInterceptor(config) {
143
- const maxRetries = config?.maxRetries ?? 3;
144
- const initialDelay = config?.initialDelay ?? 1000;
145
- const backoffMultiplier = config?.backoffMultiplier ?? 2;
146
- const retryStatusCodes = config?.retryStatusCodes ?? [408, 429, 500, 502, 503, 504];
147
- const retryOnNetworkError = config?.retryOnNetworkError ?? true;
148
- return async (req, next) => {
149
- let attempt = 0;
150
- while (true) {
151
- try {
152
- const response = await next(req);
153
- if (maxRetries > 0 && retryStatusCodes.includes(response.status)) {
154
- if (attempt < maxRetries) {
155
- const retryAfterHeader = response.headers['retry-after']?.[0] ?? response.headers['Retry-After']?.[0];
156
- const waitMs = retryAfterHeader
157
- ? parseRetryAfterMs(retryAfterHeader)
158
- : initialDelay * Math.pow(backoffMultiplier, attempt);
159
- await delay(waitMs);
160
- attempt++;
161
- continue;
162
- }
163
- throw Object.assign(new Error(`Max retries exceeded after ${maxRetries} attempt(s) (status: ${response.status})`), { status: response.status });
164
- }
165
- return response;
166
- }
167
- catch (error) {
168
- if (retryOnNetworkError && attempt < maxRetries) {
169
- const waitMs = initialDelay * Math.pow(backoffMultiplier, attempt);
170
- await delay(waitMs);
171
- attempt++;
172
- continue;
173
- }
174
- throw error;
175
- }
176
- }
177
- };
147
+ function createWorkerPipeline(interceptors) {
148
+ const chain = buildChain(interceptors, (req) => executeFetch(req));
149
+ attachRequestLoop(chain);
178
150
  }
179
151
 
180
152
  /**
@@ -227,8 +199,68 @@ function cacheInterceptor(config) {
227
199
  function arrayBufferToHex$1(buffer) {
228
200
  return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
229
201
  }
202
+ async function hashBody(body, algorithm) {
203
+ let data;
204
+ if (body instanceof ArrayBuffer) {
205
+ data = body;
206
+ }
207
+ else if (body instanceof Blob) {
208
+ data = await body.arrayBuffer();
209
+ }
210
+ else {
211
+ const str = typeof body === 'string' ? body : (JSON.stringify(body) ?? '');
212
+ data = new TextEncoder().encode(str).buffer;
213
+ }
214
+ const hash = await crypto.subtle.digest(algorithm, data);
215
+ return arrayBufferToHex$1(hash);
216
+ }
217
+ function getHeaderValue(response, headerName) {
218
+ const lower = headerName.toLowerCase();
219
+ return response.headers[lower]?.[0] ?? response.headers[headerName]?.[0];
220
+ }
221
+ /**
222
+ * Creates a content integrity interceptor that verifies response body integrity
223
+ * against a hash provided in a response header.
224
+ *
225
+ * Uses WebCrypto `SubtleCrypto` (native in web workers).
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * createWorkerPipeline([
230
+ * contentIntegrityInterceptor({
231
+ * algorithm: 'SHA-256',
232
+ * headerName: 'X-Content-Hash',
233
+ * requireHash: true,
234
+ * }),
235
+ * ]);
236
+ * ```
237
+ */
238
+ function contentIntegrityInterceptor(config) {
239
+ const algorithm = config?.algorithm ?? 'SHA-256';
240
+ const headerName = config?.headerName ?? 'X-Content-Hash';
241
+ const requireHash = config?.requireHash ?? false;
242
+ return async (req, next) => {
243
+ const response = await next(req);
244
+ const expectedHash = getHeaderValue(response, headerName);
245
+ if (!expectedHash) {
246
+ if (requireHash) {
247
+ throw Object.assign(new Error(`Content integrity header '${headerName}' is required but missing`), { status: 0 });
248
+ }
249
+ return response;
250
+ }
251
+ const actualHash = await hashBody(response.body, algorithm);
252
+ if (actualHash !== expectedHash.toLowerCase()) {
253
+ throw Object.assign(new Error('Content integrity check failed'), { status: 0 });
254
+ }
255
+ return response;
256
+ };
257
+ }
258
+
259
+ function arrayBufferToHex(buffer) {
260
+ return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
261
+ }
230
262
  function defaultPayloadBuilder(req) {
231
- const body = req.body != null ? JSON.stringify(req.body) : '';
263
+ const body = req.body !== null && req.body !== undefined ? JSON.stringify(req.body) : '';
232
264
  return `${req.method}:${req.url}:${body}`;
233
265
  }
234
266
  /**
@@ -267,7 +299,7 @@ function hmacSigningInterceptor(config) {
267
299
  const key = await getCryptoKey();
268
300
  const payload = payloadBuilder(req);
269
301
  const signatureBuffer = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
270
- const signatureHex = arrayBufferToHex$1(signatureBuffer);
302
+ const signatureHex = arrayBufferToHex(signatureBuffer);
271
303
  const signedReq = {
272
304
  ...req,
273
305
  headers: {
@@ -315,7 +347,7 @@ function loggingInterceptor(config) {
315
347
  catch (error) {
316
348
  const elapsedMs = Date.now() - startMs;
317
349
  const status = error.status;
318
- const label = status != null ? String(status) : 'NETWORK_ERROR';
350
+ const label = status !== null && status !== undefined ? String(status) : 'NETWORK_ERROR';
319
351
  safeLog(`[worker] ✕ ${label} ${req.url} (${elapsedMs}ms)`, error);
320
352
  throw error;
321
353
  }
@@ -356,64 +388,180 @@ function rateLimitInterceptor(config) {
356
388
  };
357
389
  }
358
390
 
359
- function arrayBufferToHex(buffer) {
360
- return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
361
- }
362
- async function hashBody(body, algorithm) {
363
- let data;
364
- if (body instanceof ArrayBuffer) {
365
- data = body;
366
- }
367
- else if (body instanceof Blob) {
368
- data = await body.arrayBuffer();
391
+ function parseRetryAfterMs(headerValue) {
392
+ const seconds = parseFloat(headerValue);
393
+ if (!isNaN(seconds)) {
394
+ return Math.max(0, seconds * 1000);
369
395
  }
370
- else {
371
- const str = typeof body === 'string' ? body : (JSON.stringify(body) ?? '');
372
- data = new TextEncoder().encode(str).buffer;
396
+ const date = new Date(headerValue).getTime();
397
+ if (!isNaN(date)) {
398
+ return Math.max(0, date - Date.now());
373
399
  }
374
- const hash = await crypto.subtle.digest(algorithm, data);
375
- return arrayBufferToHex(hash);
400
+ return 0;
376
401
  }
377
- function getHeaderValue(response, headerName) {
378
- const lower = headerName.toLowerCase();
379
- return response.headers[lower]?.[0] ?? response.headers[headerName]?.[0];
402
+ function delay(ms) {
403
+ return new Promise((resolve) => setTimeout(resolve, ms));
380
404
  }
381
405
  /**
382
- * Creates a content integrity interceptor that verifies response body integrity
383
- * against a hash provided in a response header.
406
+ * Creates a retry interceptor with exponential backoff.
384
407
  *
385
- * Uses WebCrypto `SubtleCrypto` (native in web workers).
408
+ * Retries requests that fail with specific HTTP status codes.
409
+ * Respects the `Retry-After` response header when present.
386
410
  *
387
411
  * @example
388
412
  * ```typescript
389
413
  * createWorkerPipeline([
390
- * contentIntegrityInterceptor({
391
- * algorithm: 'SHA-256',
392
- * headerName: 'X-Content-Hash',
393
- * requireHash: true,
394
- * }),
414
+ * retryInterceptor({ maxRetries: 3, initialDelay: 500 }),
395
415
  * ]);
396
416
  * ```
397
417
  */
398
- function contentIntegrityInterceptor(config) {
399
- const algorithm = config?.algorithm ?? 'SHA-256';
400
- const headerName = config?.headerName ?? 'X-Content-Hash';
401
- const requireHash = config?.requireHash ?? false;
418
+ function retryInterceptor(config) {
419
+ const maxRetries = config?.maxRetries ?? 3;
420
+ const initialDelay = config?.initialDelay ?? 1000;
421
+ const backoffMultiplier = config?.backoffMultiplier ?? 2;
422
+ const retryStatusCodes = config?.retryStatusCodes ?? [408, 429, 500, 502, 503, 504];
423
+ const retryOnNetworkError = config?.retryOnNetworkError ?? true;
402
424
  return async (req, next) => {
403
- const response = await next(req);
404
- const expectedHash = getHeaderValue(response, headerName);
405
- if (!expectedHash) {
406
- if (requireHash) {
407
- throw Object.assign(new Error(`Content integrity header '${headerName}' is required but missing`), { status: 0 });
425
+ let attempt = 0;
426
+ while (true) {
427
+ try {
428
+ const response = await next(req);
429
+ if (maxRetries > 0 && retryStatusCodes.includes(response.status)) {
430
+ if (attempt < maxRetries) {
431
+ const retryAfterHeader = response.headers['retry-after']?.[0] ?? response.headers['Retry-After']?.[0];
432
+ const waitMs = retryAfterHeader
433
+ ? parseRetryAfterMs(retryAfterHeader)
434
+ : initialDelay * Math.pow(backoffMultiplier, attempt);
435
+ await delay(waitMs);
436
+ attempt++;
437
+ continue;
438
+ }
439
+ throw Object.assign(new Error(`Max retries exceeded after ${maxRetries} attempt(s) (status: ${response.status})`), { status: response.status });
440
+ }
441
+ return response;
442
+ }
443
+ catch (error) {
444
+ if (retryOnNetworkError && attempt < maxRetries) {
445
+ const waitMs = initialDelay * Math.pow(backoffMultiplier, attempt);
446
+ await delay(waitMs);
447
+ attempt++;
448
+ continue;
449
+ }
450
+ throw error;
408
451
  }
409
- return response;
410
452
  }
411
- const actualHash = await hashBody(response.body, algorithm);
412
- if (actualHash !== expectedHash.toLowerCase()) {
413
- throw Object.assign(new Error('Content integrity check failed'), { status: 0 });
453
+ };
454
+ }
455
+
456
+ const INIT_MESSAGE_TYPE = 'init-interceptors';
457
+ const customRegistry = new Map();
458
+ /**
459
+ * Registers a custom interceptor factory that can be referenced from
460
+ * `withWorkerInterceptors([workerCustom('my-name', config)])`.
461
+ *
462
+ * Must be called inside the worker file BEFORE `createConfigurableWorkerPipeline()`.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * // app.worker.ts
467
+ * import { createConfigurableWorkerPipeline, registerInterceptor } from '@angular-helpers/worker-http/interceptors';
468
+ *
469
+ * registerInterceptor('auth-token', (config: { token: string }) => async (req, next) => {
470
+ * const headers = { ...req.headers, authorization: [`Bearer ${config.token}`] };
471
+ * return next({ ...req, headers });
472
+ * });
473
+ *
474
+ * createConfigurableWorkerPipeline();
475
+ * ```
476
+ */
477
+ function registerInterceptor(name, factory) {
478
+ customRegistry.set(name, factory);
479
+ }
480
+ /** Test-only: clears the custom registry. */
481
+ function __clearCustomRegistry() {
482
+ customRegistry.clear();
483
+ }
484
+ /**
485
+ * Resolves a single spec to its concrete `WorkerInterceptorFn`.
486
+ *
487
+ * Exported for test use. Throws on unknown `kind` or unregistered custom name
488
+ * so misconfiguration fails loudly at init time, not on the first request.
489
+ */
490
+ function resolveSpec(spec) {
491
+ switch (spec.kind) {
492
+ case 'logging':
493
+ return loggingInterceptor(spec.config);
494
+ case 'retry':
495
+ return retryInterceptor(spec.config);
496
+ case 'cache':
497
+ return cacheInterceptor(spec.config);
498
+ case 'hmac-signing':
499
+ return hmacSigningInterceptor(spec.config);
500
+ case 'rate-limit':
501
+ return rateLimitInterceptor(spec.config);
502
+ case 'content-integrity':
503
+ return contentIntegrityInterceptor(spec.config);
504
+ case 'custom': {
505
+ const factory = customRegistry.get(spec.name);
506
+ if (!factory) {
507
+ throw new Error(`[worker-http] Custom interceptor "${spec.name}" was not registered. ` +
508
+ `Call registerInterceptor("${spec.name}", factory) in your worker file before createConfigurableWorkerPipeline().`);
509
+ }
510
+ return factory(spec.config);
511
+ }
512
+ default: {
513
+ const exhaustive = spec;
514
+ throw new Error(`[worker-http] Unknown interceptor spec kind: ${JSON.stringify(exhaustive)}`);
515
+ }
516
+ }
517
+ }
518
+ /**
519
+ * Creates a worker-side pipeline whose interceptor chain is supplied at
520
+ * runtime via the init handshake message sent by `WorkerHttpBackend`.
521
+ *
522
+ * Behavior:
523
+ * - Listens for the first `init-interceptors` message and builds the chain
524
+ * from the received specs.
525
+ * - Until init arrives, incoming `request` messages are buffered (they will
526
+ * be flushed once the chain is ready).
527
+ * - If no init arrives (e.g. worker used standalone without
528
+ * `withWorkerInterceptors`), the pipeline runs with an empty chain so
529
+ * every request goes straight to `fetch()`.
530
+ *
531
+ * Custom interceptor factories must be registered with
532
+ * `registerInterceptor(name, factory)` before this call.
533
+ */
534
+ function createConfigurableWorkerPipeline() {
535
+ const pending = [];
536
+ const initialHandler = (event) => {
537
+ const data = event.data ?? {};
538
+ if (data.type === INIT_MESSAGE_TYPE) {
539
+ const specs = data.specs ?? [];
540
+ const fns = specs.map((spec) => resolveSpec(spec));
541
+ const chain = buildChain(fns, (req) => executeFetch(req));
542
+ // Swap to the regular request loop and replay any buffered messages.
543
+ attachRequestLoop(chain);
544
+ const handler = self.onmessage;
545
+ if (handler) {
546
+ for (const buffered of pending) {
547
+ handler.call(self, buffered);
548
+ }
549
+ }
550
+ pending.length = 0;
551
+ return;
552
+ }
553
+ if (data.type === 'request' || data.type === 'cancel') {
554
+ pending.push(event);
414
555
  }
415
- return response;
416
556
  };
557
+ self.onmessage = initialHandler;
558
+ // Safety net: if the main thread never sends init within a microtask,
559
+ // the worker still works as a no-interceptor pipeline. We DO NOT trigger
560
+ // this immediately — the init arrives via postMessage which is queued, so
561
+ // we let the buffered messages accumulate until init or the first non-init
562
+ // message after a tick. The transport guarantees init is posted first, so
563
+ // in practice the empty-chain path is only hit when the worker is used
564
+ // without withWorkerInterceptors.
417
565
  }
418
566
 
419
567
  /**
@@ -446,8 +594,41 @@ function composeInterceptors(...fns) {
446
594
  };
447
595
  }
448
596
 
597
+ /**
598
+ * Spec builders — pure factories that return POJO `WorkerInterceptorSpec`
599
+ * objects suitable for `withWorkerInterceptors([...])`.
600
+ *
601
+ * Each builder mirrors the corresponding worker-side interceptor factory but
602
+ * accepts only serializable config (no function fields).
603
+ */
604
+ function workerLogging(config) {
605
+ return { kind: 'logging', config };
606
+ }
607
+ function workerRetry(config) {
608
+ return { kind: 'retry', config };
609
+ }
610
+ function workerCache(config) {
611
+ return { kind: 'cache', config };
612
+ }
613
+ function workerHmacSigning(config) {
614
+ return { kind: 'hmac-signing', config };
615
+ }
616
+ function workerRateLimit(config) {
617
+ return { kind: 'rate-limit', config };
618
+ }
619
+ function workerContentIntegrity(config) {
620
+ return { kind: 'content-integrity', config };
621
+ }
622
+ /**
623
+ * Reference a custom interceptor that has been registered on the worker side
624
+ * via `registerInterceptor(name, factory)`.
625
+ */
626
+ function workerCustom(name, config) {
627
+ return { kind: 'custom', name, config };
628
+ }
629
+
449
630
  /**
450
631
  * Generated bundle index. Do not edit.
451
632
  */
452
633
 
453
- export { cacheInterceptor, composeInterceptors, contentIntegrityInterceptor, createWorkerPipeline, hmacSigningInterceptor, loggingInterceptor, rateLimitInterceptor, retryInterceptor };
634
+ export { cacheInterceptor, composeInterceptors, contentIntegrityInterceptor, createConfigurableWorkerPipeline, createWorkerPipeline, hmacSigningInterceptor, loggingInterceptor, rateLimitInterceptor, registerInterceptor, resolveSpec, retryInterceptor, workerCache, workerContentIntegrity, workerCustom, workerHmacSigning, workerLogging, workerRateLimit, workerRetry };
@@ -43,6 +43,9 @@ function createWorkerTransport(config) {
43
43
  function getOrCreateWorker() {
44
44
  if (workers.length < maxInstances) {
45
45
  const worker = createWorker();
46
+ if (config.initMessage) {
47
+ worker.postMessage(config.initMessage);
48
+ }
46
49
  workers.push(worker);
47
50
  return worker;
48
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/worker-http",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",