@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 +1 -0
- package/fesm2022/angular-helpers-worker-http-backend.mjs +11 -1
- package/fesm2022/angular-helpers-worker-http-interceptors.mjs +70 -19
- package/fesm2022/angular-helpers-worker-http-serializer.mjs +2 -1
- package/fesm2022/angular-helpers-worker-http-transport.mjs +107 -70
- package/package.json +2 -1
- package/types/angular-helpers-worker-http-backend.d.ts +13 -0
- package/types/angular-helpers-worker-http-interceptors.d.ts +16 -1
- package/types/angular-helpers-worker-http-transport.d.ts +36 -28
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: () =>
|
|
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
|
-
*
|
|
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
|
|
83
|
+
function attachPortLoop(port, chain) {
|
|
91
84
|
const controllers = new Map();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
114
|
+
responseBuffer.push({ type: 'response', requestId, result: response });
|
|
115
|
+
scheduleFlush();
|
|
108
116
|
}
|
|
109
117
|
catch (error) {
|
|
110
|
-
|
|
118
|
+
responseBuffer.push({
|
|
111
119
|
type: 'error',
|
|
112
120
|
requestId,
|
|
113
121
|
error: {
|
|
114
|
-
message: error
|
|
115
|
-
name: error
|
|
116
|
-
stack: error
|
|
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
|
-
|
|
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
|
|
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
|
|
201
|
+
function createInstance(index) {
|
|
202
|
+
let worker;
|
|
175
203
|
if (config.workerFactory) {
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
+
return wrapWorker(worker);
|
|
185
222
|
}
|
|
186
|
-
function
|
|
187
|
-
if (
|
|
188
|
-
const
|
|
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
|
-
|
|
230
|
+
instance.postMessage(config.initMessage);
|
|
191
231
|
}
|
|
192
|
-
|
|
193
|
-
return
|
|
232
|
+
instances.push(instance);
|
|
233
|
+
return instance;
|
|
194
234
|
}
|
|
195
|
-
const
|
|
235
|
+
const instance = instances[roundRobinIndex % instances.length];
|
|
196
236
|
roundRobinIndex++;
|
|
197
|
-
return
|
|
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
|
|
258
|
+
const instance = getOrCreateInstance();
|
|
222
259
|
let settled = false;
|
|
223
260
|
let timeoutHandle;
|
|
224
261
|
let abortListener;
|
|
225
262
|
const cleanup = () => {
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
307
|
+
instance.addEventListener('message', messageHandler);
|
|
308
|
+
instance.addEventListener('error', errorHandler);
|
|
263
309
|
if (transferDetection === 'auto') {
|
|
264
310
|
const transferables = detectTransferables(request);
|
|
265
|
-
|
|
311
|
+
dispatchToWorker(instance, { type: 'request', requestId, payload: request }, transferables);
|
|
266
312
|
}
|
|
267
313
|
else {
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
330
|
-
|
|
364
|
+
for (const instance of instances) {
|
|
365
|
+
if (instance.terminate) {
|
|
366
|
+
instance.terminate();
|
|
367
|
+
}
|
|
331
368
|
}
|
|
332
|
-
|
|
369
|
+
instances.length = 0;
|
|
333
370
|
}
|
|
334
371
|
return {
|
|
335
372
|
execute,
|
|
336
373
|
terminate,
|
|
337
374
|
get isActive() {
|
|
338
|
-
return !terminated &&
|
|
375
|
+
return !terminated && instances.length > 0;
|
|
339
376
|
},
|
|
340
377
|
get activeInstances() {
|
|
341
|
-
return
|
|
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.
|
|
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
|
|
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
|
-
|
|
239
|
-
|
|
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 };
|