@angular-helpers/worker-http 21.2.0 → 21.2.2

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
@@ -110,6 +110,7 @@ transport.terminate();
110
110
 
111
111
  - Round-robin pool (`maxInstances`) for parallel request handling
112
112
  - Lazy worker instantiation — no worker created until first request
113
+ - **Support for SharedWorker** — set `mode: 'shared'` to share worker instances across multiple tabs, reducing memory usage and sharing interceptor state (like cache).
113
114
  - **Cancellation that actually aborts `fetch()`** — unsubscribing posts a
114
115
  cancel message; the worker-side message loop threads an `AbortSignal` all
115
116
  the way into `fetch()` so the in-flight HTTP request is truly aborted
@@ -245,7 +245,17 @@ class WorkerHttpBackend extends HttpBackend {
245
245
  return existing;
246
246
  const specs = this.resolveSpecsFor(config.id);
247
247
  const transport = createWorkerTransport({
248
- workerFactory: () => new Worker(config.workerUrl, { type: 'module' }),
248
+ workerFactory: () => {
249
+ if (config.mode === 'shared') {
250
+ return new SharedWorker(config.workerUrl, {
251
+ type: 'module',
252
+ name: config.name ?? config.id,
253
+ });
254
+ }
255
+ return new Worker(config.workerUrl, { type: 'module' });
256
+ },
257
+ mode: config.mode,
258
+ sharedWorkerName: config.name ?? config.id,
249
259
  maxInstances: config.maxInstances ?? 1,
250
260
  initMessage: specs.length > 0 ? { type: 'init-interceptors', specs } : undefined,
251
261
  streamsPolyfill: this.streamsPolyfill,
@@ -78,20 +78,27 @@ async function executeFetch(req, signal) {
78
78
  }
79
79
 
80
80
  /**
81
- * Wires up the worker's `self.onmessage` handler around a built request chain.
82
- *
83
- * Owns the per-request `AbortController` map for cancellation support. Posts
84
- * `response` / `error` messages back to the main thread.
85
- *
86
- * Returns a disposer that restores `self.onmessage` to whatever it was before
87
- * (mainly useful for the configurable pipeline, which swaps the handler when
88
- * receiving the init message).
81
+ * Common loop logic for handling requests/cancellation on a port.
89
82
  */
90
- function attachRequestLoop(chain) {
83
+ function attachPortLoop(port, chain) {
91
84
  const controllers = new Map();
92
- const previous = self.onmessage;
93
- self.onmessage = async (event) => {
94
- const { type, requestId, payload } = event.data ?? {};
85
+ let responseBuffer = [];
86
+ let flushScheduled = false;
87
+ function scheduleFlush() {
88
+ if (flushScheduled)
89
+ return;
90
+ flushScheduled = true;
91
+ queueMicrotask(() => {
92
+ flushScheduled = false;
93
+ if (!responseBuffer.length)
94
+ return;
95
+ const responses = responseBuffer;
96
+ responseBuffer = [];
97
+ port.postMessage({ type: 'batch-response', responses });
98
+ });
99
+ }
100
+ const processMessage = async (msg) => {
101
+ const { type, requestId, payload } = msg;
95
102
  if (type === 'cancel') {
96
103
  controllers.get(requestId)?.abort();
97
104
  controllers.delete(requestId);
@@ -104,25 +111,39 @@ function attachRequestLoop(chain) {
104
111
  controllers.set(requestId, controller);
105
112
  try {
106
113
  const response = await chain(payload, controller.signal);
107
- self.postMessage({ type: 'response', requestId, result: response });
114
+ responseBuffer.push({ type: 'response', requestId, result: response });
115
+ scheduleFlush();
108
116
  }
109
117
  catch (error) {
110
- self.postMessage({
118
+ responseBuffer.push({
111
119
  type: 'error',
112
120
  requestId,
113
121
  error: {
114
- message: error instanceof Error ? error.message : String(error),
115
- name: error instanceof Error ? error.name : 'UnknownError',
116
- stack: error instanceof Error ? error.stack : undefined,
122
+ message: error?.message ?? String(error),
123
+ name: error?.name ?? 'UnknownError',
124
+ stack: error?.stack,
117
125
  },
118
126
  });
127
+ scheduleFlush();
119
128
  }
120
129
  finally {
121
130
  controllers.delete(requestId);
122
131
  }
123
132
  };
133
+ const messageHandler = (event) => {
134
+ const data = event.data ?? {};
135
+ if (data.type === 'batch') {
136
+ for (const msg of data.messages || []) {
137
+ processMessage(msg).catch(console.error);
138
+ }
139
+ }
140
+ else {
141
+ processMessage(data).catch(console.error);
142
+ }
143
+ };
144
+ port.addEventListener('message', messageHandler);
124
145
  return () => {
125
- self.onmessage = previous;
146
+ port.removeEventListener('message', messageHandler);
126
147
  for (const controller of controllers.values()) {
127
148
  controller.abort();
128
149
  }
@@ -130,6 +151,36 @@ function attachRequestLoop(chain) {
130
151
  };
131
152
  }
132
153
 
154
+ /**
155
+ * Wires up the worker's request handler around a built request chain.
156
+ *
157
+ * Automatically detects if running in a Dedicated Worker or Shared Worker context
158
+ * and attaches the appropriate listeners.
159
+ */
160
+ function attachRequestLoop(chain) {
161
+ const disposers = [];
162
+ // Shared Worker context
163
+ if ('onconnect' in self) {
164
+ const connectHandler = (event) => {
165
+ const port = event.ports[0];
166
+ disposers.push(attachPortLoop(port, chain));
167
+ port.start();
168
+ };
169
+ self.addEventListener('connect', connectHandler);
170
+ disposers.push(() => self.removeEventListener('connect', connectHandler));
171
+ }
172
+ else {
173
+ // Dedicated Worker context
174
+ disposers.push(attachPortLoop(self, chain));
175
+ }
176
+ return () => {
177
+ for (const dispose of disposers) {
178
+ dispose();
179
+ }
180
+ disposers.length = 0;
181
+ };
182
+ }
183
+
133
184
  /**
134
185
  * Creates and registers a worker-side HTTP pipeline.
135
186
  *
@@ -638,4 +689,4 @@ function workerCustom(name, config) {
638
689
  * Generated bundle index. Do not edit.
639
690
  */
640
691
 
641
- export { cacheInterceptor, composeInterceptors, contentIntegrityInterceptor, createConfigurableWorkerPipeline, createWorkerPipeline, hmacSigningInterceptor, loggingInterceptor, rateLimitInterceptor, registerInterceptor, resolveSpec, retryInterceptor, workerCache, workerContentIntegrity, workerCustom, workerHmacSigning, workerLogging, workerRateLimit, workerRetry };
692
+ export { attachPortLoop, attachRequestLoop, cacheInterceptor, composeInterceptors, contentIntegrityInterceptor, createConfigurableWorkerPipeline, createWorkerPipeline, hmacSigningInterceptor, loggingInterceptor, rateLimitInterceptor, registerInterceptor, resolveSpec, retryInterceptor, workerCache, workerContentIntegrity, workerCustom, workerHmacSigning, workerLogging, workerRateLimit, workerRetry };
@@ -298,7 +298,8 @@ async function createAutoSerializer(config) {
298
298
  },
299
299
  deserialize(payload) {
300
300
  let resolved = payload;
301
- if (payload.data instanceof ArrayBuffer) {
301
+ if (payload.data instanceof ArrayBuffer ||
302
+ Object.prototype.toString.call(payload.data) === '[object ArrayBuffer]') {
302
303
  const str = new TextDecoder().decode(payload.data);
303
304
  resolved = { ...payload, data: str };
304
305
  }
@@ -136,65 +136,105 @@ class WorkerHttpTimeoutError extends Error {
136
136
  }
137
137
  }
138
138
 
139
+ /**
140
+ * Wraps a Worker or SharedWorker into a unified TransportPort interface.
141
+ */
142
+ function wrapWorker(worker) {
143
+ if ('port' in worker) {
144
+ const port = worker.port;
145
+ return {
146
+ postMessage: (msg, transfer) => port.postMessage(msg, transfer),
147
+ addEventListener: (type, listener) => port.addEventListener(type, listener),
148
+ removeEventListener: (type, listener) => port.removeEventListener(type, listener),
149
+ start: () => port.start(),
150
+ terminate: () => port.close(),
151
+ };
152
+ }
153
+ return worker;
154
+ }
155
+
139
156
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
140
157
  /**
141
158
  * Creates a typed, Observable-based transport for communicating with a web worker.
142
- *
143
- * Features:
144
- * - Request/response correlation via `requestId`
145
- * - Cancellation on Observable unsubscribe (also aborts `fetch()` in the worker)
146
- * - Per-request timeout (default 30 s) rejecting with `WorkerHttpTimeoutError`
147
- * - Optional worker pool with round-robin dispatch
148
- * - Lazy worker creation (default)
149
- * - Opt-in transferable detection (`transferDetection: 'auto'`) for zero-copy
150
- * `ArrayBuffer` / stream payloads
151
- *
152
- * @example
153
- * ```typescript
154
- * const transport = createWorkerTransport<MyRequest, MyResponse>({
155
- * workerFactory: () => new Worker(new URL('./my.worker.ts', import.meta.url), { type: 'module' }),
156
- * maxInstances: 2,
157
- * });
158
- *
159
- * transport.execute(request).subscribe({
160
- * next: (response) => console.log(response),
161
- * error: (err) => console.error(err),
162
- * });
163
- * ```
164
159
  */
165
160
  function createWorkerTransport(config) {
166
- const workers = [];
161
+ const instances = [];
167
162
  let roundRobinIndex = 0;
168
163
  let terminated = false;
164
+ const batchBuffer = new Map();
165
+ const batchTransferables = new Map();
166
+ let flushScheduled = false;
167
+ function scheduleFlush() {
168
+ if (flushScheduled)
169
+ return;
170
+ flushScheduled = true;
171
+ queueMicrotask(() => {
172
+ flushScheduled = false;
173
+ for (const [instance, messages] of batchBuffer.entries()) {
174
+ const transferables = batchTransferables.get(instance) || [];
175
+ instance.postMessage({ type: 'batch', messages }, transferables);
176
+ }
177
+ batchBuffer.clear();
178
+ batchTransferables.clear();
179
+ });
180
+ }
181
+ function dispatchToWorker(instance, msg, transferables) {
182
+ if (!batchBuffer.has(instance)) {
183
+ batchBuffer.set(instance, []);
184
+ }
185
+ batchBuffer.get(instance).push(msg);
186
+ if (transferables?.length) {
187
+ if (!batchTransferables.has(instance)) {
188
+ batchTransferables.set(instance, []);
189
+ }
190
+ batchTransferables.get(instance).push(...transferables);
191
+ }
192
+ scheduleFlush();
193
+ }
194
+ const mode = config.mode ?? 'worker';
195
+ const sharedWorkerName = config.sharedWorkerName ?? 'worker-http';
169
196
  const maxInstances = Math.min(config.maxInstances ?? 1, typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 1);
170
197
  const requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
171
198
  const transferDetection = config.transferDetection ?? 'none';
172
199
  const streamsPolyfill = config.streamsPolyfill ?? false;
173
200
  let polyfillLoaded = false;
174
- function createWorker() {
201
+ function createInstance(index) {
202
+ let worker;
175
203
  if (config.workerFactory) {
176
- return config.workerFactory();
204
+ worker = config.workerFactory();
177
205
  }
178
- if (config.workerUrl) {
206
+ else if (config.workerUrl) {
179
207
  const url = typeof config.workerUrl === 'string'
180
208
  ? new URL(config.workerUrl, document.baseURI)
181
209
  : config.workerUrl;
182
- return new Worker(url, { type: 'module' });
210
+ if (mode === 'shared') {
211
+ const name = maxInstances > 1 ? `${sharedWorkerName}-${index}` : sharedWorkerName;
212
+ worker = new SharedWorker(url, { type: 'module', name });
213
+ }
214
+ else {
215
+ worker = new Worker(url, { type: 'module' });
216
+ }
217
+ }
218
+ else {
219
+ throw new Error('Either workerFactory or workerUrl must be provided');
183
220
  }
184
- throw new Error('Either workerFactory or workerUrl must be provided');
221
+ return wrapWorker(worker);
185
222
  }
186
- function getOrCreateWorker() {
187
- if (workers.length < maxInstances) {
188
- const worker = createWorker();
223
+ function getOrCreateInstance() {
224
+ if (instances.length < maxInstances) {
225
+ const instance = createInstance(instances.length);
226
+ if (instance.start) {
227
+ instance.start();
228
+ }
189
229
  if (config.initMessage) {
190
- worker.postMessage(config.initMessage);
230
+ instance.postMessage(config.initMessage);
191
231
  }
192
- workers.push(worker);
193
- return worker;
232
+ instances.push(instance);
233
+ return instance;
194
234
  }
195
- const worker = workers[roundRobinIndex % workers.length];
235
+ const instance = instances[roundRobinIndex % instances.length];
196
236
  roundRobinIndex++;
197
- return worker;
237
+ return instance;
198
238
  }
199
239
  function execute(request, options) {
200
240
  if (terminated) {
@@ -206,25 +246,22 @@ function createWorkerTransport(config) {
206
246
  const externalSignal = options?.signal;
207
247
  const effectiveTimeout = options?.timeout ?? requestTimeout;
208
248
  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
249
  if (externalSignal?.aborted) {
212
250
  subscriber.error(new WorkerHttpAbortError(externalSignal.reason));
213
251
  return () => undefined;
214
252
  }
215
- // Lazy-load polyfill on first request if enabled
216
253
  if (streamsPolyfill && !polyfillLoaded) {
217
254
  loadStreamsPolyfill().catch((err) => {
218
255
  console.warn('[worker-http] Streams polyfill failed to load:', err);
219
256
  });
220
257
  }
221
- const worker = getOrCreateWorker();
258
+ const instance = getOrCreateInstance();
222
259
  let settled = false;
223
260
  let timeoutHandle;
224
261
  let abortListener;
225
262
  const cleanup = () => {
226
- worker.removeEventListener('message', messageHandler);
227
- worker.removeEventListener('error', errorHandler);
263
+ instance.removeEventListener('message', messageHandler);
264
+ instance.removeEventListener('error', errorHandler);
228
265
  if (timeoutHandle !== undefined) {
229
266
  clearTimeout(timeoutHandle);
230
267
  timeoutHandle = undefined;
@@ -234,8 +271,7 @@ function createWorkerTransport(config) {
234
271
  abortListener = undefined;
235
272
  }
236
273
  };
237
- const messageHandler = (event) => {
238
- const data = event.data;
274
+ const handleResponse = (data) => {
239
275
  if (data.requestId !== requestId)
240
276
  return;
241
277
  if (settled)
@@ -243,14 +279,24 @@ function createWorkerTransport(config) {
243
279
  settled = true;
244
280
  cleanup();
245
281
  if (data.type === 'error') {
246
- const err = data.error;
247
- subscriber.error(new Error(err.message));
282
+ subscriber.error(new Error(data.error.message));
248
283
  }
249
284
  else {
250
285
  subscriber.next(data.result);
251
286
  subscriber.complete();
252
287
  }
253
288
  };
289
+ const messageHandler = (event) => {
290
+ const data = event.data;
291
+ if (data.type === 'batch-response') {
292
+ const match = data.responses.find((r) => r.requestId === requestId);
293
+ if (match)
294
+ handleResponse(match);
295
+ }
296
+ else {
297
+ handleResponse(data);
298
+ }
299
+ };
254
300
  const errorHandler = (event) => {
255
301
  if (settled)
256
302
  return;
@@ -258,14 +304,14 @@ function createWorkerTransport(config) {
258
304
  cleanup();
259
305
  subscriber.error(new Error(event.message ?? 'Worker error'));
260
306
  };
261
- worker.addEventListener('message', messageHandler);
262
- worker.addEventListener('error', errorHandler);
307
+ instance.addEventListener('message', messageHandler);
308
+ instance.addEventListener('error', errorHandler);
263
309
  if (transferDetection === 'auto') {
264
310
  const transferables = detectTransferables(request);
265
- worker.postMessage({ type: 'request', requestId, payload: request }, transferables);
311
+ dispatchToWorker(instance, { type: 'request', requestId, payload: request }, transferables);
266
312
  }
267
313
  else {
268
- worker.postMessage({ type: 'request', requestId, payload: request });
314
+ dispatchToWorker(instance, { type: 'request', requestId, payload: request });
269
315
  }
270
316
  if (effectiveTimeout > 0 && Number.isFinite(effectiveTimeout)) {
271
317
  timeoutHandle = setTimeout(() => {
@@ -273,15 +319,10 @@ function createWorkerTransport(config) {
273
319
  return;
274
320
  settled = true;
275
321
  cleanup();
276
- // Ask the worker to abort any in-flight work for this id. The
277
- // cancellation fix wires this through to `fetch()`.
278
- worker.postMessage({ type: 'cancel', requestId });
322
+ dispatchToWorker(instance, { type: 'cancel', requestId });
279
323
  subscriber.error(new WorkerHttpTimeoutError(effectiveTimeout));
280
324
  }, effectiveTimeout);
281
325
  }
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
326
  if (externalSignal) {
286
327
  abortListener = () => {
287
328
  if (settled)
@@ -289,25 +330,20 @@ function createWorkerTransport(config) {
289
330
  settled = true;
290
331
  const reason = externalSignal.reason;
291
332
  cleanup();
292
- worker.postMessage({ type: 'cancel', requestId });
333
+ dispatchToWorker(instance, { type: 'cancel', requestId });
293
334
  subscriber.error(new WorkerHttpAbortError(reason));
294
335
  };
295
336
  externalSignal.addEventListener('abort', abortListener, { once: true });
296
337
  }
297
- // Teardown: send cancel message on unsubscribe
298
338
  return () => {
299
339
  if (settled)
300
340
  return;
301
341
  settled = true;
302
342
  cleanup();
303
- worker.postMessage({ type: 'cancel', requestId });
343
+ dispatchToWorker(instance, { type: 'cancel', requestId });
304
344
  };
305
345
  });
306
346
  }
307
- /**
308
- * Lazy-loads the streams polyfill if enabled and not already loaded.
309
- * Called before operations that might involve stream transfer.
310
- */
311
347
  async function loadStreamsPolyfill() {
312
348
  if (!streamsPolyfill || polyfillLoaded) {
313
349
  return;
@@ -320,25 +356,26 @@ function createWorkerTransport(config) {
320
356
  polyfillLoaded = true;
321
357
  }
322
358
  catch (err) {
323
- // Log warning but don't block request — streams will fail naturally
324
359
  console.warn('[worker-http] Failed to load streams polyfill:', err);
325
360
  }
326
361
  }
327
362
  function terminate() {
328
363
  terminated = true;
329
- for (const worker of workers) {
330
- worker.terminate();
364
+ for (const instance of instances) {
365
+ if (instance.terminate) {
366
+ instance.terminate();
367
+ }
331
368
  }
332
- workers.length = 0;
369
+ instances.length = 0;
333
370
  }
334
371
  return {
335
372
  execute,
336
373
  terminate,
337
374
  get isActive() {
338
- return !terminated && workers.length > 0;
375
+ return !terminated && instances.length > 0;
339
376
  },
340
377
  get activeInstances() {
341
- return workers.length;
378
+ return instances.length;
342
379
  },
343
380
  };
344
381
  }
@@ -347,4 +384,4 @@ function createWorkerTransport(config) {
347
384
  * Generated bundle index. Do not edit.
348
385
  */
349
386
 
350
- export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
387
+ export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables, wrapWorker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/worker-http",
3
- "version": "21.2.0",
3
+ "version": "21.2.2",
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": {
@@ -102,6 +102,7 @@
102
102
  "module": "fesm2022/angular-helpers-worker-http.mjs",
103
103
  "typings": "types/angular-helpers-worker-http.d.ts",
104
104
  "sideEffects": false,
105
+ "type": "module",
105
106
  "dependencies": {
106
107
  "tslib": "^2.3.0"
107
108
  }
@@ -27,6 +27,19 @@ interface WorkerConfig {
27
27
  workerUrl: URL;
28
28
  /** Maximum worker instances in the pool (default: 1) */
29
29
  maxInstances?: number;
30
+ /**
31
+ * Execution mode for the worker.
32
+ *
33
+ * - `'worker'` (default) — Dedicated Web Worker. Each tab has its own worker instance.
34
+ * - `'shared'` — Shared Web Worker. Multiple tabs share the same worker instances.
35
+ */
36
+ mode?: 'worker' | 'shared';
37
+ /**
38
+ * Name for the SharedWorker. Required when `mode: 'shared'` to ensure multiple
39
+ * tabs connect to the same worker instance. If `maxInstances > 1`, names are
40
+ * suffixed with the instance index (e.g. `api-1`, `api-2`).
41
+ */
42
+ name?: string;
30
43
  }
31
44
  /**
32
45
  * URL-pattern to worker auto-routing rule.
@@ -223,6 +223,21 @@ declare function resolveSpec(spec: WorkerInterceptorSpec): WorkerInterceptorFn;
223
223
  */
224
224
  declare function createConfigurableWorkerPipeline(): void;
225
225
 
226
+ type RequestHandler = (req: SerializableRequest, signal?: AbortSignal) => Promise<SerializableResponse>;
227
+
228
+ /**
229
+ * Wires up the worker's request handler around a built request chain.
230
+ *
231
+ * Automatically detects if running in a Dedicated Worker or Shared Worker context
232
+ * and attaches the appropriate listeners.
233
+ */
234
+ declare function attachRequestLoop(chain: RequestHandler): () => void;
235
+
236
+ /**
237
+ * Common loop logic for handling requests/cancellation on a port.
238
+ */
239
+ declare function attachPortLoop(port: MessagePort | any, chain: RequestHandler): () => void;
240
+
226
241
  /**
227
242
  * Creates a retry interceptor with exponential backoff.
228
243
  *
@@ -362,5 +377,5 @@ declare function workerContentIntegrity(config?: ContentIntegrityConfig): Worker
362
377
  */
363
378
  declare function workerCustom(name: string, config?: unknown): WorkerInterceptorSpec;
364
379
 
365
- export { cacheInterceptor, composeInterceptors, contentIntegrityInterceptor, createConfigurableWorkerPipeline, createWorkerPipeline, hmacSigningInterceptor, loggingInterceptor, rateLimitInterceptor, registerInterceptor, resolveSpec, retryInterceptor, workerCache, workerContentIntegrity, workerCustom, workerHmacSigning, workerLogging, workerRateLimit, workerRetry };
380
+ export { attachPortLoop, attachRequestLoop, cacheInterceptor, composeInterceptors, contentIntegrityInterceptor, createConfigurableWorkerPipeline, createWorkerPipeline, hmacSigningInterceptor, loggingInterceptor, rateLimitInterceptor, registerInterceptor, resolveSpec, retryInterceptor, workerCache, workerContentIntegrity, workerCustom, workerHmacSigning, workerLogging, workerRateLimit, workerRetry };
366
381
  export type { CacheConfig, ContentIntegrityConfig, HmacInterceptorConfig, LoggingConfig, RateLimitConfig, RetryConfig, SerializableHmacConfig, SerializableLoggingConfig, SerializableRequest, SerializableResponse, WorkerInterceptorFn, WorkerInterceptorInitMessage, WorkerInterceptorSpec };
@@ -9,16 +9,16 @@ import { Observable } from 'rxjs';
9
9
  */
10
10
  interface WorkerTransportConfig {
11
11
  /**
12
- * Factory function that creates a new Worker instance.
13
- * The `new Worker(new URL(...))` call MUST be in your app code (not a library)
14
- * for Angular CLI to bundle the worker correctly.
12
+ * Factory function that creates a new Worker or SharedWorker instance.
13
+ * The `new Worker(...)` or `new SharedWorker(...)` call MUST be in your app code
14
+ * (not a library) for Angular CLI to bundle the worker correctly.
15
15
  *
16
16
  * @example
17
17
  * ```typescript
18
18
  * workerFactory: () => new Worker(new URL('./echo.worker.ts', import.meta.url), { type: 'module' })
19
19
  * ```
20
20
  */
21
- workerFactory?: () => Worker;
21
+ workerFactory?: () => Worker | SharedWorker;
22
22
  /**
23
23
  * URL to a pre-transpiled worker file.
24
24
  * Use this when workers are built separately (e.g., with Vite) and distributed
@@ -32,6 +32,19 @@ interface WorkerTransportConfig {
32
32
  * ```
33
33
  */
34
34
  workerUrl?: string | URL;
35
+ /**
36
+ * Execution mode for the worker.
37
+ *
38
+ * - `'worker'` (default) — Dedicated Web Worker. Each tab has its own worker instance.
39
+ * - `'shared'` — Shared Web Worker. Multiple tabs share the same worker instances.
40
+ */
41
+ mode?: 'worker' | 'shared';
42
+ /**
43
+ * Name for the SharedWorker. Required when `mode: 'shared'` to ensure multiple
44
+ * tabs connect to the same worker instance. If `maxInstances > 1`, names are
45
+ * suffixed with the instance index (e.g. `api-1`, `api-2`).
46
+ */
47
+ sharedWorkerName?: string;
35
48
  /** Maximum number of worker instances in the pool (default: 1) */
36
49
  maxInstances?: number;
37
50
  /**
@@ -139,28 +152,6 @@ interface WorkerTransport<TRequest = unknown, TResponse = unknown> {
139
152
 
140
153
  /**
141
154
  * Creates a typed, Observable-based transport for communicating with a web worker.
142
- *
143
- * Features:
144
- * - Request/response correlation via `requestId`
145
- * - Cancellation on Observable unsubscribe (also aborts `fetch()` in the worker)
146
- * - Per-request timeout (default 30 s) rejecting with `WorkerHttpTimeoutError`
147
- * - Optional worker pool with round-robin dispatch
148
- * - Lazy worker creation (default)
149
- * - Opt-in transferable detection (`transferDetection: 'auto'`) for zero-copy
150
- * `ArrayBuffer` / stream payloads
151
- *
152
- * @example
153
- * ```typescript
154
- * const transport = createWorkerTransport<MyRequest, MyResponse>({
155
- * workerFactory: () => new Worker(new URL('./my.worker.ts', import.meta.url), { type: 'module' }),
156
- * maxInstances: 2,
157
- * });
158
- *
159
- * transport.execute(request).subscribe({
160
- * next: (response) => console.log(response),
161
- * error: (err) => console.error(err),
162
- * });
163
- * ```
164
155
  */
165
156
  declare function createWorkerTransport<TRequest = unknown, TResponse = unknown>(config: WorkerTransportConfig): WorkerTransport<TRequest, TResponse>;
166
157
 
@@ -235,5 +226,22 @@ declare class WorkerHttpAbortError extends Error {
235
226
  */
236
227
  declare function detectTransferables(payload: unknown): Transferable[];
237
228
 
238
- export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables };
239
- export type { WorkerErrorResponse, WorkerExecuteOptions, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };
229
+ /**
230
+ * Common interface for communication ports.
231
+ * Unifies Worker and SharedWorker (MessagePort).
232
+ */
233
+ interface TransportPort {
234
+ postMessage(message: any, transfer?: Transferable[]): void;
235
+ addEventListener(type: string, listener: any): void;
236
+ removeEventListener(type: string, listener: any): void;
237
+ terminate?(): void;
238
+ start?(): void;
239
+ }
240
+
241
+ /**
242
+ * Wraps a Worker or SharedWorker into a unified TransportPort interface.
243
+ */
244
+ declare function wrapWorker(worker: Worker | SharedWorker): TransportPort;
245
+
246
+ export { WorkerHttpAbortError, WorkerHttpTimeoutError, createWorkerTransport, detectTransferables, wrapWorker };
247
+ export type { TransportPort, WorkerErrorResponse, WorkerExecuteOptions, WorkerMessage, WorkerResponse, WorkerTransport, WorkerTransportConfig };