@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.
@@ -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
- // Initialize state
150
+ errorMode: "pass-through",
75
151
  createState: () => ({
76
- activeSubscriptions: new Map(),
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
- // Process each incoming chunk
83
- async transform(chunk, state, controller) {
84
- // If we're under the concurrency limit, process immediately
85
- if (state.activeCount < concurrent) {
86
- await subscribeToProjection(chunk, state.index++);
87
- }
88
- else {
89
- // Otherwise, buffer for later
90
- state.buffer.push([chunk, state.index++]);
91
- }
92
- // Helper function to subscribe to inner Observable
93
- async function subscribeToProjection(value, innerIndex) {
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
- // Apply projection function to get inner Observable
97
- innerObservable = project(value, innerIndex);
194
+ innerObservable = project(innerValue, innerIndex);
98
195
  }
99
196
  catch (err) {
100
- // Forward any errors from the projection function
101
- controller.enqueue(error_js_1.ObservableError.from(err, "operator:stateful:mergeMap:project", value));
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
- // Use pull to iterate asynchronously
106
- try {
107
- for await (const innerValue of (0, observable_js_1.pull)(innerObservable, { throwError: false })) {
108
- controller.enqueue(innerValue);
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
- catch (err) {
112
- controller.enqueue(error_js_1.ObservableError.from(err, "operator:stateful:mergeMap:innerObservable", value));
113
- }
114
- finally {
115
- // Clean up after inner Observable completes
116
- state.activeSubscriptions.delete(innerIndex);
117
- state.activeCount--;
118
- // Process the buffer if we have space
119
- processBuffer();
120
- }
121
- }
122
- // Helper function to process the buffer
123
- async function processBuffer() {
124
- // While we have room for more inner Observables and
125
- // there are items in the buffer, subscribe to inner Observables
126
- while (state.activeCount < concurrent && state.buffer.length > 0) {
127
- const [value, bufferIndex] = state.buffer.shift();
128
- await subscribeToProjection(value, bufferIndex);
129
- }
130
- // If the source is completed and we have no active inner
131
- // subscriptions, complete the output stream
132
- // Nothing more to do, transformation is complete
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
- // Handle the end of the source stream
136
- flush(state) {
137
- // Mark the source as completed
253
+ async flush(state) {
138
254
  state.sourceCompleted = true;
139
- // If no active inner subscriptions, we're done
140
- // Stream will close naturally
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
- // Clear the buffer and state
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.activeSubscriptions.clear();
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
- // Initialize state
377
+ errorMode: "pass-through",
257
378
  createState: () => ({
258
- currentController: null,
379
+ currentIterator: null,
259
380
  currentTask: null,
260
381
  currentTaskToken: null,
261
- sourceCompleted: false,
382
+ isCancelled: false,
262
383
  index: 0,
263
384
  }),
264
- // Process each incoming chunk
265
- transform(chunk, state, controller) {
266
- if ((0, error_js_1.isObservableError)(chunk)) {
267
- // If the chunk is an error, we can immediately enqueue it
268
- controller.enqueue(chunk);
269
- return;
270
- }
271
- // Cancel any existing inner subscription
272
- if (state.currentController) {
273
- state.currentController.abort();
274
- state.currentController = null;
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 enqueueIfActive = (value) => {
293
- if (abortController.signal.aborted ||
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
- const iterator = (0, observable_js_1.pull)(innerObservable, { throwError: false })[Symbol.asyncIterator]();
306
- while (!abortController.signal.aborted) {
307
- const { value, done } = await iterator.next();
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
- // Forward the value
312
- enqueueIfActive(value);
434
+ }
435
+ try {
436
+ controller.enqueue(emittedValue);
437
+ }
438
+ catch {
439
+ break;
440
+ }
313
441
  }
314
442
  }
315
443
  catch (err) {
316
- if (!abortController.signal.aborted) {
317
- enqueueIfActive(error_js_1.ObservableError.from(err, "operator:stateful:switchMap:innerObservable", chunk));
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.currentTaskToken === currentTaskToken) {
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
- // Only handle completion if this is still the current controller
326
- if (state.currentController === abortController) {
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
- // Handle cancellation
345
- cancel(state) {
346
- // Cancel the current inner subscription
347
- if (state.currentController) {
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
+ }