@okikio/observables 1.0.2 → 1.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 +117 -6
- package/esm/helpers/_types.d.ts +62 -0
- package/esm/helpers/_types.d.ts.map +1 -1
- package/esm/helpers/operations/combination.d.ts +112 -3
- package/esm/helpers/operations/combination.d.ts.map +1 -1
- package/esm/helpers/operations/combination.js +682 -115
- package/esm/helpers/operations/conditional.d.ts +77 -1
- package/esm/helpers/operations/conditional.d.ts.map +1 -1
- package/esm/helpers/operations/conditional.js +139 -0
- package/esm/helpers/operators.d.ts +57 -0
- package/esm/helpers/operators.d.ts.map +1 -1
- package/esm/helpers/operators.js +139 -4
- package/esm/helpers/utils.d.ts +73 -1
- package/esm/helpers/utils.d.ts.map +1 -1
- package/esm/helpers/utils.js +168 -0
- package/package.json +1 -1
- package/script/helpers/_types.d.ts +62 -0
- package/script/helpers/_types.d.ts.map +1 -1
- package/script/helpers/operations/combination.d.ts +112 -3
- package/script/helpers/operations/combination.d.ts.map +1 -1
- package/script/helpers/operations/combination.js +686 -115
- package/script/helpers/operations/conditional.d.ts +77 -1
- package/script/helpers/operations/conditional.d.ts.map +1 -1
- package/script/helpers/operations/conditional.js +143 -0
- package/script/helpers/operators.d.ts +57 -0
- package/script/helpers/operators.d.ts.map +1 -1
- package/script/helpers/operators.js +141 -4
- package/script/helpers/utils.d.ts +73 -1
- package/script/helpers/utils.d.ts.map +1 -1
- package/script/helpers/utils.js +172 -0
|
@@ -18,6 +18,77 @@ import "../../_dnt.polyfills.js";
|
|
|
18
18
|
import { createStatefulOperator } from "../operators.js";
|
|
19
19
|
import { isObservableError, ObservableError } from "../../error.js";
|
|
20
20
|
import { pull } from "../../observable.js";
|
|
21
|
+
import { createQueue, dequeue, enqueue, getSize, isFull, toArray, } from "../../queue.js";
|
|
22
|
+
/**
|
|
23
|
+
* Cancels an iterator if it exposes `return()`.
|
|
24
|
+
*
|
|
25
|
+
* Most iterators created through `pull()` do expose `return()`, and calling it
|
|
26
|
+
* is the Observable equivalent of saying "the consumer is done, release any
|
|
27
|
+
* held resources now".
|
|
28
|
+
*/
|
|
29
|
+
async function cancelIterator(iterator) {
|
|
30
|
+
if (iterator?.return) {
|
|
31
|
+
await iterator.return();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rebuilds the strongly typed tuple returned by the combination operators.
|
|
36
|
+
*
|
|
37
|
+
* The runtime data is stored in mutable `unknown[]` buffers because values are
|
|
38
|
+
* filled over time. This helper isolates the final cast to one place, and the
|
|
39
|
+
* cast is sound because every caller preserves the exact length and ordering of
|
|
40
|
+
* the original companion Observable tuple.
|
|
41
|
+
*/
|
|
42
|
+
function toCombinationTuple(sourceValue, otherValues) {
|
|
43
|
+
return [
|
|
44
|
+
sourceValue,
|
|
45
|
+
...otherValues,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Removes one queued value from each companion queue in order.
|
|
50
|
+
*
|
|
51
|
+
* Callers only use this after checking that every queue has at least one value.
|
|
52
|
+
* That precondition is what makes the returned tuple complete instead of partly
|
|
53
|
+
* `undefined`.
|
|
54
|
+
*/
|
|
55
|
+
function shiftQueuedValues(queues) {
|
|
56
|
+
return queues.map((queue) => dequeue(queue));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Starts zip queues small and lets them grow only when buffering actually
|
|
60
|
+
* happens.
|
|
61
|
+
*
|
|
62
|
+
* `zipWith()` can run for a long time, so this avoids paying for large backing
|
|
63
|
+
* arrays up front while still keeping steady-state dequeue work O(1).
|
|
64
|
+
*/
|
|
65
|
+
const INITIAL_ZIP_QUEUE_CAPACITY = 16;
|
|
66
|
+
/**
|
|
67
|
+
* Returns whether a queue currently has at least one buffered value.
|
|
68
|
+
*/
|
|
69
|
+
function hasQueuedValue(queue) {
|
|
70
|
+
return getSize(queue) > 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Appends a value to a queue, growing the underlying circular buffer when the
|
|
74
|
+
* current capacity is exhausted.
|
|
75
|
+
*
|
|
76
|
+
* The shared queue utilities are fixed-capacity by design. `zipWith()` needs
|
|
77
|
+
* unbounded logical buffering, so this helper preserves the queue API while
|
|
78
|
+
* replacing the backing queue with a larger one only when necessary.
|
|
79
|
+
*/
|
|
80
|
+
function enqueueQueuedValue(queue, value) {
|
|
81
|
+
if (!isFull(queue)) {
|
|
82
|
+
enqueue(queue, value);
|
|
83
|
+
return queue;
|
|
84
|
+
}
|
|
85
|
+
const grownQueue = createQueue(Math.max(queue.capacity * 2, 1));
|
|
86
|
+
for (const item of toArray(queue)) {
|
|
87
|
+
enqueue(grownQueue, item);
|
|
88
|
+
}
|
|
89
|
+
enqueue(grownQueue, value);
|
|
90
|
+
return grownQueue;
|
|
91
|
+
}
|
|
21
92
|
/**
|
|
22
93
|
* Transforms each item into a new stream and merges their outputs, running
|
|
23
94
|
* them in parallel.
|
|
@@ -64,87 +135,133 @@ import { pull } from "../../observable.js";
|
|
|
64
135
|
* @returns An operator function that maps and flattens values
|
|
65
136
|
*/
|
|
66
137
|
export function mergeMap(project, concurrent = Infinity) {
|
|
138
|
+
const maxConcurrent = concurrent > 0 ? concurrent : 1;
|
|
67
139
|
return createStatefulOperator({
|
|
68
140
|
name: "mergeMap",
|
|
69
|
-
|
|
141
|
+
errorMode: "pass-through",
|
|
70
142
|
createState: () => ({
|
|
71
|
-
|
|
143
|
+
activeIterators: new Map(),
|
|
144
|
+
activeTasks: new Set(),
|
|
72
145
|
buffer: [],
|
|
73
|
-
sourceCompleted: false,
|
|
74
146
|
index: 0,
|
|
75
147
|
activeCount: 0,
|
|
148
|
+
idleDeferred: null,
|
|
149
|
+
isCancelled: false,
|
|
150
|
+
sourceCompleted: false,
|
|
76
151
|
}),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
152
|
+
transform(chunk, state, controller) {
|
|
153
|
+
const value = chunk;
|
|
154
|
+
const sourceIndex = state.index++;
|
|
155
|
+
// `flush()` must wait for both active inners and buffered outers.
|
|
156
|
+
// Resolving this gate too early would let the operator finish while inner
|
|
157
|
+
// work is still producing values.
|
|
158
|
+
const resolveIfIdle = () => {
|
|
159
|
+
if (state.sourceCompleted &&
|
|
160
|
+
state.activeCount === 0 &&
|
|
161
|
+
state.buffer.length === 0 &&
|
|
162
|
+
state.idleDeferred) {
|
|
163
|
+
state.idleDeferred.resolve();
|
|
164
|
+
state.idleDeferred = null;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Keep filling free concurrency slots until either the buffer is empty or
|
|
168
|
+
// the operator reaches its configured parallelism limit.
|
|
169
|
+
const drainBuffer = () => {
|
|
170
|
+
while (!state.isCancelled &&
|
|
171
|
+
state.activeCount < maxConcurrent &&
|
|
172
|
+
state.buffer.length > 0) {
|
|
173
|
+
const next = state.buffer.shift();
|
|
174
|
+
if (!next)
|
|
175
|
+
break;
|
|
176
|
+
launchProjection(next[0], next[1]);
|
|
177
|
+
}
|
|
178
|
+
resolveIfIdle();
|
|
179
|
+
};
|
|
180
|
+
// Start draining one projected inner Observable and keep enough metadata
|
|
181
|
+
// around to cancel or await it later.
|
|
182
|
+
const launchProjection = (innerValue, innerIndex) => {
|
|
89
183
|
let innerObservable;
|
|
90
184
|
try {
|
|
91
|
-
|
|
92
|
-
innerObservable = project(value, innerIndex);
|
|
185
|
+
innerObservable = project(innerValue, innerIndex);
|
|
93
186
|
}
|
|
94
187
|
catch (err) {
|
|
95
|
-
|
|
96
|
-
|
|
188
|
+
try {
|
|
189
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:mergeMap:project", innerValue));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Downstream cancellation already decided the stream outcome.
|
|
193
|
+
}
|
|
194
|
+
drainBuffer();
|
|
97
195
|
return;
|
|
98
196
|
}
|
|
99
197
|
state.activeCount++;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
198
|
+
const iterator = pull(innerObservable, { throwError: false })[Symbol.asyncIterator]();
|
|
199
|
+
state.activeIterators.set(innerIndex, iterator);
|
|
200
|
+
const taskRef = { current: null };
|
|
201
|
+
const task = (async () => {
|
|
202
|
+
try {
|
|
203
|
+
while (!state.isCancelled) {
|
|
204
|
+
const { value: emittedValue, done } = await iterator.next();
|
|
205
|
+
if (done || state.isCancelled) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
controller.enqueue(emittedValue);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
104
215
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (!state.isCancelled) {
|
|
218
|
+
try {
|
|
219
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:mergeMap:innerObservable", innerValue));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Downstream cancellation already decided the stream outcome.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
state.activeIterators.delete(innerIndex);
|
|
228
|
+
if (taskRef.current) {
|
|
229
|
+
state.activeTasks.delete(taskRef.current);
|
|
230
|
+
}
|
|
231
|
+
state.activeCount--;
|
|
232
|
+
drainBuffer();
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
taskRef.current = task;
|
|
236
|
+
state.activeTasks.add(task);
|
|
237
|
+
};
|
|
238
|
+
if (state.activeCount < maxConcurrent) {
|
|
239
|
+
launchProjection(value, sourceIndex);
|
|
240
|
+
return;
|
|
128
241
|
}
|
|
242
|
+
state.buffer.push([value, sourceIndex]);
|
|
129
243
|
},
|
|
130
|
-
|
|
131
|
-
flush(state) {
|
|
132
|
-
// Mark the source as completed
|
|
244
|
+
async flush(state) {
|
|
133
245
|
state.sourceCompleted = true;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Otherwise, let the active subscriptions complete
|
|
137
|
-
},
|
|
138
|
-
// Handle cancellation
|
|
139
|
-
cancel(state) {
|
|
140
|
-
// Clean up all inner subscriptions
|
|
141
|
-
for (const subscription of state.activeSubscriptions.values()) {
|
|
142
|
-
subscription.unsubscribe();
|
|
246
|
+
if (state.activeCount === 0 && state.buffer.length === 0) {
|
|
247
|
+
return;
|
|
143
248
|
}
|
|
144
|
-
|
|
249
|
+
state.idleDeferred ??= Promise.withResolvers();
|
|
250
|
+
await state.idleDeferred.promise;
|
|
251
|
+
},
|
|
252
|
+
async cancel(state) {
|
|
253
|
+
state.isCancelled = true;
|
|
145
254
|
state.buffer.length = 0;
|
|
146
|
-
state.
|
|
255
|
+
const iterators = [...state.activeIterators.values()];
|
|
256
|
+
state.activeIterators.clear();
|
|
257
|
+
await Promise.allSettled(iterators.map((iterator) => iterator.return?.()));
|
|
258
|
+
await Promise.allSettled([...state.activeTasks]);
|
|
259
|
+
state.activeTasks.clear();
|
|
147
260
|
state.activeCount = 0;
|
|
261
|
+
if (state.idleDeferred) {
|
|
262
|
+
state.idleDeferred.resolve();
|
|
263
|
+
state.idleDeferred = null;
|
|
264
|
+
}
|
|
148
265
|
},
|
|
149
266
|
});
|
|
150
267
|
}
|
|
@@ -248,25 +365,36 @@ export function concatMap(project) {
|
|
|
248
365
|
export function switchMap(project) {
|
|
249
366
|
return createStatefulOperator({
|
|
250
367
|
name: "switchMap",
|
|
251
|
-
|
|
368
|
+
errorMode: "pass-through",
|
|
252
369
|
createState: () => ({
|
|
253
|
-
|
|
370
|
+
currentIterator: null,
|
|
254
371
|
currentTask: null,
|
|
255
372
|
currentTaskToken: null,
|
|
256
|
-
|
|
373
|
+
isCancelled: false,
|
|
257
374
|
index: 0,
|
|
258
375
|
}),
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
state.
|
|
269
|
-
state.
|
|
376
|
+
async transform(chunk, state, controller) {
|
|
377
|
+
/**
|
|
378
|
+
* Stops the current inner Observable before a newer one replaces it.
|
|
379
|
+
*
|
|
380
|
+
* The order matters: `return()` asks the iterator to clean up, then we
|
|
381
|
+
* await the draining task so no stale emissions race with the next inner.
|
|
382
|
+
*/
|
|
383
|
+
const stopCurrent = async () => {
|
|
384
|
+
const iterator = state.currentIterator;
|
|
385
|
+
const task = state.currentTask;
|
|
386
|
+
state.currentIterator = null;
|
|
387
|
+
state.currentTask = null;
|
|
388
|
+
state.currentTaskToken = null;
|
|
389
|
+
if (iterator?.return) {
|
|
390
|
+
await iterator.return();
|
|
391
|
+
}
|
|
392
|
+
if (task) {
|
|
393
|
+
await task;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
if (state.currentTaskToken || state.currentIterator || state.currentTask) {
|
|
397
|
+
await stopCurrent();
|
|
270
398
|
}
|
|
271
399
|
let innerObservable;
|
|
272
400
|
try {
|
|
@@ -278,73 +406,512 @@ export function switchMap(project) {
|
|
|
278
406
|
controller.enqueue(ObservableError.from(err, "operator:stateful:switchMap:project", chunk));
|
|
279
407
|
return;
|
|
280
408
|
}
|
|
281
|
-
// Create a new abort controller for this inner subscription
|
|
282
|
-
const abortController = new AbortController();
|
|
283
|
-
state.currentController = abortController;
|
|
284
|
-
// Subscribe to the new inner Observable
|
|
285
409
|
const currentTaskToken = {};
|
|
410
|
+
const iterator = pull(innerObservable, { throwError: false })[Symbol.asyncIterator]();
|
|
411
|
+
state.currentTaskToken = currentTaskToken;
|
|
412
|
+
state.currentIterator = iterator;
|
|
413
|
+
const currentTaskRef = {
|
|
414
|
+
current: null,
|
|
415
|
+
};
|
|
286
416
|
const currentTask = (async () => {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
state.currentController !== abortController) {
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
try {
|
|
293
|
-
controller.enqueue(value);
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
abortController.abort();
|
|
297
|
-
}
|
|
417
|
+
const isActive = () => {
|
|
418
|
+
return !state.isCancelled && state.currentTaskToken === currentTaskToken;
|
|
298
419
|
};
|
|
299
420
|
try {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// If the controller is aborted or we're done, exit the loop
|
|
304
|
-
if (abortController.signal.aborted || done)
|
|
421
|
+
while (isActive()) {
|
|
422
|
+
const { value: emittedValue, done } = await iterator.next();
|
|
423
|
+
if (done || !isActive()) {
|
|
305
424
|
break;
|
|
306
|
-
|
|
307
|
-
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
controller.enqueue(emittedValue);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
308
432
|
}
|
|
309
433
|
}
|
|
310
434
|
catch (err) {
|
|
311
|
-
if (
|
|
312
|
-
|
|
435
|
+
if (isActive()) {
|
|
436
|
+
try {
|
|
437
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:switchMap:innerObservable", chunk));
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Downstream cancellation already decided the stream outcome.
|
|
441
|
+
}
|
|
313
442
|
}
|
|
314
443
|
}
|
|
315
444
|
finally {
|
|
316
|
-
if (state.
|
|
445
|
+
if (state.currentIterator === iterator) {
|
|
446
|
+
state.currentIterator = null;
|
|
447
|
+
}
|
|
448
|
+
if (state.currentTask === currentTaskRef.current) {
|
|
317
449
|
state.currentTask = null;
|
|
318
|
-
state.currentTaskToken = null;
|
|
319
450
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
state.currentController = null;
|
|
323
|
-
// If source is completed and we have no active inner, we're done
|
|
324
|
-
// Stream will close naturally
|
|
451
|
+
if (state.currentTaskToken === currentTaskToken) {
|
|
452
|
+
state.currentTaskToken = null;
|
|
325
453
|
}
|
|
326
454
|
}
|
|
327
455
|
})();
|
|
456
|
+
currentTaskRef.current = currentTask;
|
|
328
457
|
state.currentTask = currentTask;
|
|
329
|
-
state.currentTaskToken = currentTaskToken;
|
|
330
458
|
},
|
|
331
|
-
// Handle the end of the source stream
|
|
332
459
|
async flush(state) {
|
|
333
|
-
// Mark the source as completed
|
|
334
|
-
state.sourceCompleted = true;
|
|
335
460
|
if (state.currentTask) {
|
|
336
461
|
await state.currentTask;
|
|
337
462
|
}
|
|
338
463
|
},
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
state.currentController.abort();
|
|
344
|
-
state.currentController = null;
|
|
464
|
+
async cancel(state) {
|
|
465
|
+
state.isCancelled = true;
|
|
466
|
+
if (state.currentIterator?.return) {
|
|
467
|
+
await state.currentIterator.return();
|
|
345
468
|
}
|
|
469
|
+
if (state.currentTask) {
|
|
470
|
+
await state.currentTask;
|
|
471
|
+
}
|
|
472
|
+
state.currentIterator = null;
|
|
346
473
|
state.currentTask = null;
|
|
347
474
|
state.currentTaskToken = null;
|
|
348
475
|
},
|
|
349
476
|
});
|
|
350
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Emits each source value together with the latest value from every companion
|
|
480
|
+
* Observable.
|
|
481
|
+
*
|
|
482
|
+
* This operator waits until every companion Observable has produced at least
|
|
483
|
+
* one value. After that gate opens, each new source value emits a tuple that
|
|
484
|
+
* includes the source value followed by the most recent companion values.
|
|
485
|
+
*
|
|
486
|
+
* ```text
|
|
487
|
+
* companion A: ---a----b-------c--
|
|
488
|
+
* companion B: -----x-------y-----
|
|
489
|
+
* source: ------1--2------3--
|
|
490
|
+
* output: ------[1,a,x][2,b,x][3,c,y]
|
|
491
|
+
* ```
|
|
492
|
+
*
|
|
493
|
+
* The source still decides when outputs happen. Companion Observables only
|
|
494
|
+
* update the remembered latest values.
|
|
495
|
+
*
|
|
496
|
+
* @typeParam T - Value type from the primary source
|
|
497
|
+
* @typeParam TOthers - Value types from the companion Observables
|
|
498
|
+
* @param others - Companion Observables whose latest values should be attached
|
|
499
|
+
* @returns An operator that emits tuples of the source value and latest others
|
|
500
|
+
*/
|
|
501
|
+
export function withLatestFrom(...others) {
|
|
502
|
+
return createStatefulOperator({
|
|
503
|
+
name: "withLatestFrom",
|
|
504
|
+
errorMode: "pass-through",
|
|
505
|
+
createState: () => ({
|
|
506
|
+
latestValues: new Array(others.length),
|
|
507
|
+
seenLatest: others.map(() => false),
|
|
508
|
+
iterators: [],
|
|
509
|
+
tasks: new Set(),
|
|
510
|
+
isCancelled: false,
|
|
511
|
+
}),
|
|
512
|
+
start(state, controller) {
|
|
513
|
+
// Companion Observables run in the background and only update cached
|
|
514
|
+
// latest values. They never decide when to emit tuples themselves.
|
|
515
|
+
others.forEach((observable, index) => {
|
|
516
|
+
const iterator = pull(observable, { throwError: false })[Symbol.asyncIterator]();
|
|
517
|
+
state.iterators[index] = iterator;
|
|
518
|
+
const taskRef = { current: null };
|
|
519
|
+
const task = (async () => {
|
|
520
|
+
try {
|
|
521
|
+
while (!state.isCancelled) {
|
|
522
|
+
const { value, done } = await iterator.next();
|
|
523
|
+
if (done || state.isCancelled) {
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
if (isObservableError(value)) {
|
|
527
|
+
try {
|
|
528
|
+
controller.enqueue(value);
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
state.latestValues[index] = value;
|
|
536
|
+
state.seenLatest[index] = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
if (!state.isCancelled) {
|
|
541
|
+
try {
|
|
542
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:withLatestFrom:companion", { companionIndex: index }));
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// Downstream cancellation already decided the stream outcome.
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
if (taskRef.current) {
|
|
551
|
+
state.tasks.delete(taskRef.current);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
})();
|
|
555
|
+
taskRef.current = task;
|
|
556
|
+
state.tasks.add(task);
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
transform(chunk, state, controller) {
|
|
560
|
+
if (!state.seenLatest.every(Boolean)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
controller.enqueue(toCombinationTuple(chunk, state.latestValues));
|
|
564
|
+
},
|
|
565
|
+
async flush(state) {
|
|
566
|
+
await Promise.allSettled([...state.tasks]);
|
|
567
|
+
},
|
|
568
|
+
async cancel(state) {
|
|
569
|
+
state.isCancelled = true;
|
|
570
|
+
await Promise.allSettled(state.iterators.map((iterator) => cancelIterator(iterator)));
|
|
571
|
+
await Promise.allSettled([...state.tasks]);
|
|
572
|
+
state.tasks.clear();
|
|
573
|
+
state.iterators.length = 0;
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Combines the source with companion Observables using combine-latest timing.
|
|
579
|
+
*
|
|
580
|
+
* Once every source has emitted at least one value, any new value from the
|
|
581
|
+
* primary source or a companion Observable emits a new tuple containing the
|
|
582
|
+
* latest value from each source.
|
|
583
|
+
*
|
|
584
|
+
* @typeParam T - Value type from the primary source
|
|
585
|
+
* @typeParam TOthers - Value types from the companion Observables
|
|
586
|
+
* @param others - Companion Observables that participate in the latest-value set
|
|
587
|
+
* @returns An operator that emits tuples of the latest value from every source
|
|
588
|
+
*/
|
|
589
|
+
export function combineLatestWith(...others) {
|
|
590
|
+
return createStatefulOperator({
|
|
591
|
+
name: "combineLatestWith",
|
|
592
|
+
errorMode: "pass-through",
|
|
593
|
+
createState: () => ({
|
|
594
|
+
sourceValue: null,
|
|
595
|
+
hasSourceValue: false,
|
|
596
|
+
latestValues: new Array(others.length),
|
|
597
|
+
seenLatest: others.map(() => false),
|
|
598
|
+
iterators: [],
|
|
599
|
+
tasks: new Set(),
|
|
600
|
+
sourceCompleted: false,
|
|
601
|
+
isCancelled: false,
|
|
602
|
+
}),
|
|
603
|
+
start(state, controller) {
|
|
604
|
+
/**
|
|
605
|
+
* Emits the full latest tuple only after the source and every companion
|
|
606
|
+
* have contributed at least one value.
|
|
607
|
+
*/
|
|
608
|
+
const emitLatest = () => {
|
|
609
|
+
if (!state.hasSourceValue || !state.seenLatest.every(Boolean)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
controller.enqueue(toCombinationTuple(state.sourceValue, state.latestValues));
|
|
613
|
+
};
|
|
614
|
+
others.forEach((observable, index) => {
|
|
615
|
+
const iterator = pull(observable, { throwError: false })[Symbol.asyncIterator]();
|
|
616
|
+
state.iterators[index] = iterator;
|
|
617
|
+
const taskRef = { current: null };
|
|
618
|
+
const task = (async () => {
|
|
619
|
+
try {
|
|
620
|
+
while (!state.isCancelled) {
|
|
621
|
+
const { value, done } = await iterator.next();
|
|
622
|
+
if (done || state.isCancelled) {
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
if (isObservableError(value)) {
|
|
626
|
+
try {
|
|
627
|
+
controller.enqueue(value);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
state.latestValues[index] = value;
|
|
635
|
+
state.seenLatest[index] = true;
|
|
636
|
+
try {
|
|
637
|
+
emitLatest();
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
if (!state.isCancelled) {
|
|
646
|
+
try {
|
|
647
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:combineLatestWith:companion", { companionIndex: index }));
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Downstream cancellation already decided the stream outcome.
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
if (taskRef.current) {
|
|
656
|
+
state.tasks.delete(taskRef.current);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
})();
|
|
660
|
+
taskRef.current = task;
|
|
661
|
+
state.tasks.add(task);
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
transform(chunk, state, controller) {
|
|
665
|
+
state.sourceValue = chunk;
|
|
666
|
+
state.hasSourceValue = true;
|
|
667
|
+
if (!state.seenLatest.every(Boolean)) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
controller.enqueue(toCombinationTuple(state.sourceValue, state.latestValues));
|
|
671
|
+
},
|
|
672
|
+
async flush(state) {
|
|
673
|
+
state.sourceCompleted = true;
|
|
674
|
+
await Promise.allSettled([...state.tasks]);
|
|
675
|
+
},
|
|
676
|
+
async cancel(state) {
|
|
677
|
+
state.isCancelled = true;
|
|
678
|
+
await Promise.allSettled(state.iterators.map((iterator) => cancelIterator(iterator)));
|
|
679
|
+
await Promise.allSettled([...state.tasks]);
|
|
680
|
+
state.tasks.clear();
|
|
681
|
+
state.iterators.length = 0;
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Pairs source values with companion values in strict arrival order.
|
|
687
|
+
*
|
|
688
|
+
* `zipWith` waits until it has one value from the source and one value from
|
|
689
|
+
* each companion. It then emits a tuple and removes those values from the
|
|
690
|
+
* queues before waiting for the next full set.
|
|
691
|
+
*
|
|
692
|
+
* @typeParam T - Value type from the primary source
|
|
693
|
+
* @typeParam TOthers - Value types from the companion Observables
|
|
694
|
+
* @param others - Companion Observables to align by arrival order
|
|
695
|
+
* @returns An operator that emits tuples aligned by position instead of time
|
|
696
|
+
*/
|
|
697
|
+
export function zipWith(...others) {
|
|
698
|
+
return createStatefulOperator({
|
|
699
|
+
name: "zipWith",
|
|
700
|
+
errorMode: "pass-through",
|
|
701
|
+
createState: () => ({
|
|
702
|
+
sourceQueue: createQueue(INITIAL_ZIP_QUEUE_CAPACITY),
|
|
703
|
+
otherQueues: others.map(() => createQueue(INITIAL_ZIP_QUEUE_CAPACITY)),
|
|
704
|
+
sourceCompleted: false,
|
|
705
|
+
otherCompleted: others.map(() => false),
|
|
706
|
+
iterators: [],
|
|
707
|
+
tasks: new Set(),
|
|
708
|
+
isCancelled: false,
|
|
709
|
+
isTerminated: false,
|
|
710
|
+
}),
|
|
711
|
+
start(state, controller) {
|
|
712
|
+
/**
|
|
713
|
+
* Emits as many full zip tuples as are currently available.
|
|
714
|
+
*
|
|
715
|
+
* The termination logic is important: once a completed companion has no
|
|
716
|
+
* queued value left, zip can never produce another full tuple, so the
|
|
717
|
+
* operator terminates immediately instead of waiting for the source to end.
|
|
718
|
+
*/
|
|
719
|
+
const emitAvailable = () => {
|
|
720
|
+
while (!state.isTerminated &&
|
|
721
|
+
hasQueuedValue(state.sourceQueue) &&
|
|
722
|
+
state.otherQueues.every((queue) => hasQueuedValue(queue))) {
|
|
723
|
+
controller.enqueue(toCombinationTuple(dequeue(state.sourceQueue), shiftQueuedValues(state.otherQueues)));
|
|
724
|
+
}
|
|
725
|
+
const blockedByCompletedCompanion = state.otherCompleted.some((completed, index) => completed && !hasQueuedValue(state.otherQueues[index]));
|
|
726
|
+
if (!state.isTerminated &&
|
|
727
|
+
(blockedByCompletedCompanion ||
|
|
728
|
+
(state.sourceCompleted && !hasQueuedValue(state.sourceQueue)))) {
|
|
729
|
+
state.isTerminated = true;
|
|
730
|
+
controller.terminate();
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
others.forEach((observable, index) => {
|
|
734
|
+
const iterator = pull(observable, { throwError: false })[Symbol.asyncIterator]();
|
|
735
|
+
state.iterators[index] = iterator;
|
|
736
|
+
const taskRef = { current: null };
|
|
737
|
+
const task = (async () => {
|
|
738
|
+
try {
|
|
739
|
+
while (!state.isCancelled && !state.isTerminated) {
|
|
740
|
+
const { value, done } = await iterator.next();
|
|
741
|
+
if (done || state.isCancelled || state.isTerminated) {
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
if (isObservableError(value)) {
|
|
745
|
+
try {
|
|
746
|
+
controller.enqueue(value);
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
state.otherQueues[index] = enqueueQueuedValue(state.otherQueues[index], value);
|
|
754
|
+
emitAvailable();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
if (!state.isCancelled && !state.isTerminated) {
|
|
759
|
+
try {
|
|
760
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:zipWith:companion", { companionIndex: index }));
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// Downstream cancellation already decided the stream outcome.
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
state.otherCompleted[index] = true;
|
|
769
|
+
if (taskRef.current) {
|
|
770
|
+
state.tasks.delete(taskRef.current);
|
|
771
|
+
}
|
|
772
|
+
if (!state.isCancelled && !state.isTerminated) {
|
|
773
|
+
emitAvailable();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
taskRef.current = task;
|
|
778
|
+
state.tasks.add(task);
|
|
779
|
+
});
|
|
780
|
+
},
|
|
781
|
+
transform(chunk, state, controller) {
|
|
782
|
+
state.sourceQueue = enqueueQueuedValue(state.sourceQueue, chunk);
|
|
783
|
+
while (!state.isTerminated &&
|
|
784
|
+
hasQueuedValue(state.sourceQueue) &&
|
|
785
|
+
state.otherQueues.every((queue) => hasQueuedValue(queue))) {
|
|
786
|
+
controller.enqueue(toCombinationTuple(dequeue(state.sourceQueue), shiftQueuedValues(state.otherQueues)));
|
|
787
|
+
}
|
|
788
|
+
const blockedByCompletedCompanion = state.otherCompleted.some((completed, index) => completed && !hasQueuedValue(state.otherQueues[index]));
|
|
789
|
+
if (!state.isTerminated && blockedByCompletedCompanion) {
|
|
790
|
+
state.isTerminated = true;
|
|
791
|
+
controller.terminate();
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
async flush(state, controller) {
|
|
795
|
+
state.sourceCompleted = true;
|
|
796
|
+
if (!state.isTerminated && !hasQueuedValue(state.sourceQueue)) {
|
|
797
|
+
state.isTerminated = true;
|
|
798
|
+
controller.terminate();
|
|
799
|
+
}
|
|
800
|
+
await Promise.allSettled([...state.tasks]);
|
|
801
|
+
},
|
|
802
|
+
async cancel(state) {
|
|
803
|
+
state.isCancelled = true;
|
|
804
|
+
await Promise.allSettled(state.iterators.map((iterator) => cancelIterator(iterator)));
|
|
805
|
+
await Promise.allSettled([...state.tasks]);
|
|
806
|
+
state.tasks.clear();
|
|
807
|
+
state.iterators.length = 0;
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Mirrors whichever Observable emits first: the source or one of the
|
|
813
|
+
* companions.
|
|
814
|
+
*
|
|
815
|
+
* If the primary source wins, future source values keep flowing and companion
|
|
816
|
+
* Observables are cancelled. If a companion wins, the source is ignored from
|
|
817
|
+
* that point on and the winning companion continues to drive the output.
|
|
818
|
+
*
|
|
819
|
+
* @typeParam T - Value type from the primary source
|
|
820
|
+
* @typeParam TOthers - Value types from the companion Observables
|
|
821
|
+
* @param others - Companion Observables racing against the source
|
|
822
|
+
* @returns An operator that mirrors the winning Observable
|
|
823
|
+
*/
|
|
824
|
+
export function raceWith(...others) {
|
|
825
|
+
return createStatefulOperator({
|
|
826
|
+
name: "raceWith",
|
|
827
|
+
errorMode: "pass-through",
|
|
828
|
+
createState: () => ({
|
|
829
|
+
winner: null,
|
|
830
|
+
iterators: [],
|
|
831
|
+
tasks: new Set(),
|
|
832
|
+
sourceCompleted: false,
|
|
833
|
+
isCancelled: false,
|
|
834
|
+
}),
|
|
835
|
+
start(state, controller) {
|
|
836
|
+
/**
|
|
837
|
+
* Cancels every companion except the winning one.
|
|
838
|
+
*
|
|
839
|
+
* `raceWith()` is intentionally ruthless: once the first value arrives,
|
|
840
|
+
* every loser is stopped so only one Observable continues to drive output.
|
|
841
|
+
*/
|
|
842
|
+
const cancelLosers = async (winnerIndex) => {
|
|
843
|
+
const loserIterators = state.iterators.filter((_, index) => index !== winnerIndex);
|
|
844
|
+
await Promise.allSettled(loserIterators.map((iterator) => cancelIterator(iterator)));
|
|
845
|
+
};
|
|
846
|
+
others.forEach((observable, index) => {
|
|
847
|
+
const iterator = pull(observable, { throwError: false })[Symbol.asyncIterator]();
|
|
848
|
+
state.iterators[index] = iterator;
|
|
849
|
+
const taskRef = { current: null };
|
|
850
|
+
const task = (async () => {
|
|
851
|
+
try {
|
|
852
|
+
while (!state.isCancelled) {
|
|
853
|
+
const { value, done } = await iterator.next();
|
|
854
|
+
if (done || state.isCancelled) {
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
if (state.winner === null) {
|
|
858
|
+
state.winner = index;
|
|
859
|
+
await cancelLosers(index);
|
|
860
|
+
}
|
|
861
|
+
if (state.winner !== index) {
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
controller.enqueue(value);
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
if (!state.isCancelled && state.winner === index) {
|
|
874
|
+
try {
|
|
875
|
+
controller.enqueue(ObservableError.from(err, "operator:stateful:raceWith:companion", { companionIndex: index }));
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
// Downstream cancellation already decided the stream outcome.
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
finally {
|
|
883
|
+
if (taskRef.current) {
|
|
884
|
+
state.tasks.delete(taskRef.current);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
})();
|
|
888
|
+
taskRef.current = task;
|
|
889
|
+
state.tasks.add(task);
|
|
890
|
+
});
|
|
891
|
+
},
|
|
892
|
+
async transform(chunk, state, controller) {
|
|
893
|
+
if (state.winner === null) {
|
|
894
|
+
state.winner = "source";
|
|
895
|
+
await Promise.allSettled(state.iterators.map((iterator) => cancelIterator(iterator)));
|
|
896
|
+
}
|
|
897
|
+
if (state.winner !== "source") {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
controller.enqueue(chunk);
|
|
901
|
+
},
|
|
902
|
+
async flush(state) {
|
|
903
|
+
state.sourceCompleted = true;
|
|
904
|
+
if (state.winner === "source") {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
await Promise.allSettled([...state.tasks]);
|
|
908
|
+
},
|
|
909
|
+
async cancel(state) {
|
|
910
|
+
state.isCancelled = true;
|
|
911
|
+
await Promise.allSettled(state.iterators.map((iterator) => cancelIterator(iterator)));
|
|
912
|
+
await Promise.allSettled([...state.tasks]);
|
|
913
|
+
state.tasks.clear();
|
|
914
|
+
state.iterators.length = 0;
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
}
|