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