synstate 3.3.0 → 3.4.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.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/dist/core/base/create-child-observable.d.mts +9 -2
  3. package/dist/core/base/create-child-observable.d.mts.map +1 -1
  4. package/dist/core/base/create-child-observable.mjs +26 -8
  5. package/dist/core/base/create-child-observable.mjs.map +1 -1
  6. package/dist/core/base/create-observable-base.d.mts +2 -0
  7. package/dist/core/base/create-observable-base.d.mts.map +1 -1
  8. package/dist/core/base/create-observable-base.mjs +21 -3
  9. package/dist/core/base/create-observable-base.mjs.map +1 -1
  10. package/dist/core/base/create-root-observable.d.mts +9 -0
  11. package/dist/core/base/create-root-observable.d.mts.map +1 -1
  12. package/dist/core/base/create-root-observable.mjs +25 -2
  13. package/dist/core/base/create-root-observable.mjs.map +1 -1
  14. package/dist/core/combine/merge.d.mts +0 -3
  15. package/dist/core/combine/merge.d.mts.map +1 -1
  16. package/dist/core/combine/merge.mjs +0 -3
  17. package/dist/core/combine/merge.mjs.map +1 -1
  18. package/dist/core/combine/zip.d.mts +4 -2
  19. package/dist/core/combine/zip.d.mts.map +1 -1
  20. package/dist/core/combine/zip.mjs +21 -3
  21. package/dist/core/combine/zip.mjs.map +1 -1
  22. package/dist/core/create/counter.d.mts +9 -3
  23. package/dist/core/create/counter.d.mts.map +1 -1
  24. package/dist/core/create/counter.mjs +16 -3
  25. package/dist/core/create/counter.mjs.map +1 -1
  26. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  27. package/dist/core/create/from-subscribable.mjs +34 -11
  28. package/dist/core/create/from-subscribable.mjs.map +1 -1
  29. package/dist/core/create/source.d.mts +7 -0
  30. package/dist/core/create/source.d.mts.map +1 -1
  31. package/dist/core/create/source.mjs +6 -2
  32. package/dist/core/create/source.mjs.map +1 -1
  33. package/dist/core/operators/merge-map.d.mts +0 -3
  34. package/dist/core/operators/merge-map.d.mts.map +1 -1
  35. package/dist/core/operators/merge-map.mjs +0 -3
  36. package/dist/core/operators/merge-map.mjs.map +1 -1
  37. package/dist/core/operators/skip-until.d.mts.map +1 -1
  38. package/dist/core/operators/skip-until.mjs +15 -6
  39. package/dist/core/operators/skip-until.mjs.map +1 -1
  40. package/dist/core/operators/switch-map.d.mts +0 -3
  41. package/dist/core/operators/switch-map.d.mts.map +1 -1
  42. package/dist/core/operators/switch-map.mjs +0 -3
  43. package/dist/core/operators/switch-map.mjs.map +1 -1
  44. package/dist/core/operators/take-until.d.mts.map +1 -1
  45. package/dist/core/operators/take-until.mjs +13 -1
  46. package/dist/core/operators/take-until.mjs.map +1 -1
  47. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  48. package/dist/core/operators/with-buffered-from.mjs +12 -3
  49. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  50. package/dist/core/types/observable-family.d.mts +5 -1
  51. package/dist/core/types/observable-family.d.mts.map +1 -1
  52. package/dist/core/types/observable-family.mjs.map +1 -1
  53. package/dist/utils/create-event-emitter.d.mts +2 -2
  54. package/dist/utils/create-reducer.d.mts +10 -1
  55. package/dist/utils/create-reducer.d.mts.map +1 -1
  56. package/dist/utils/create-reducer.mjs +13 -5
  57. package/dist/utils/create-reducer.mjs.map +1 -1
  58. package/dist/utils/create-state.d.mts +7 -0
  59. package/dist/utils/create-state.d.mts.map +1 -1
  60. package/dist/utils/create-state.mjs +7 -0
  61. package/dist/utils/create-state.mjs.map +1 -1
  62. package/package.json +4 -4
  63. package/src/core/base/create-child-observable.mts +34 -6
  64. package/src/core/base/create-observable-base.mts +23 -3
  65. package/src/core/base/create-root-observable.mts +35 -1
  66. package/src/core/base/write-during-propagation.test.mts +234 -0
  67. package/src/core/combine/merge.mts +0 -3
  68. package/src/core/combine/zip.mts +36 -5
  69. package/src/core/create/counter.mts +27 -6
  70. package/src/core/create/counter.test.mts +82 -0
  71. package/src/core/create/from-subscribable.mts +33 -5
  72. package/src/core/create/from-subscribable.test.mts +129 -0
  73. package/src/core/create/source.mts +18 -3
  74. package/src/core/create/source.test.mts +41 -0
  75. package/src/core/operators/merge-map.mts +0 -3
  76. package/src/core/operators/skip-until.mts +21 -9
  77. package/src/core/operators/switch-map.mts +0 -3
  78. package/src/core/operators/take-until-skip-until-teardown.test.mts +127 -0
  79. package/src/core/operators/take-until.mts +15 -1
  80. package/src/core/operators/with-buffered-from.mts +15 -3
  81. package/src/core/operators/with-buffered-from.test.mts +56 -0
  82. package/src/core/types/observable-family.mts +5 -1
  83. package/src/utils/create-event-emitter.mts +2 -2
  84. package/src/utils/create-reducer.mts +14 -8
  85. package/src/utils/create-reducer.test.mts +12 -0
  86. package/src/utils/create-state.mts +7 -0
  87. package/src/utils/create-state.test.mts +20 -0
@@ -1,4 +1,5 @@
1
1
  import { Arr } from 'ts-data-forge';
2
+ import { map } from '../operators/index.mjs';
2
3
  import { counter } from './counter.mjs';
3
4
 
4
5
  describe(counter, () => {
@@ -191,4 +192,85 @@ describe(counter, () => {
191
192
 
192
193
  vi.useRealTimers();
193
194
  });
195
+
196
+ test.each([
197
+ { emitInitial: true, expected: [0, 1, 2] },
198
+ { emitInitial: false, expected: [1, 2, 3] },
199
+ ] as const)(
200
+ 'times: 3, emitInitial: $emitInitial completes right after the third emission',
201
+ ({ emitInitial, expected }) => {
202
+ vi.useFakeTimers();
203
+
204
+ const counter$ = counter(100, { times: 3, emitInitial });
205
+
206
+ const mut_history: number[] = [];
207
+
208
+ counter$.subscribe((n) => {
209
+ mut_history.push(n);
210
+ });
211
+
212
+ vi.advanceTimersToNextTimer().advanceTimersToNextTimer();
213
+
214
+ assert.deepStrictEqual<readonly number[]>(
215
+ mut_history,
216
+ Arr.take(expected, 2),
217
+ );
218
+
219
+ assert.isFalse(counter$.isCompleted);
220
+
221
+ vi.advanceTimersToNextTimer();
222
+
223
+ assert.deepStrictEqual<readonly number[]>(mut_history, expected);
224
+
225
+ assert.isTrue(counter$.isCompleted);
226
+
227
+ assert.strictEqual(vi.getTimerCount(), 0);
228
+
229
+ vi.useRealTimers();
230
+ },
231
+ );
232
+
233
+ test('times: 1 emits only the 0 and clears the interval', () => {
234
+ vi.useFakeTimers();
235
+
236
+ const counter$ = counter(100, { times: 1 });
237
+
238
+ const mut_history: number[] = [];
239
+
240
+ counter$.subscribe((n) => {
241
+ mut_history.push(n);
242
+ });
243
+
244
+ vi.advanceTimersToNextTimer();
245
+
246
+ assert.deepStrictEqual(mut_history, [0]);
247
+
248
+ assert.isTrue(counter$.isCompleted);
249
+
250
+ assert.strictEqual(vi.getTimerCount(), 0);
251
+
252
+ vi.useRealTimers();
253
+ });
254
+
255
+ test('times completes the downstream after the last value reaches it', () => {
256
+ vi.useFakeTimers();
257
+
258
+ const counter$ = counter(100, { times: 2 });
259
+
260
+ const doubled$ = counter$.pipe(map((n) => n * 2));
261
+
262
+ const mut_history: number[] = [];
263
+
264
+ doubled$.subscribe((n) => {
265
+ mut_history.push(n);
266
+ });
267
+
268
+ vi.advanceTimersToNextTimer().advanceTimersToNextTimer();
269
+
270
+ assert.deepStrictEqual(mut_history, [0, 2]);
271
+
272
+ assert.isTrue(doubled$.isCompleted);
273
+
274
+ vi.useRealTimers();
275
+ });
194
276
  });
@@ -3,6 +3,7 @@ import { createRootObservable } from '../base/index.mjs';
3
3
  import type {
4
4
  FromSubscribableObservable,
5
5
  Subscribable,
6
+ Subscription,
6
7
  } from '../types/index.mjs';
7
8
 
8
9
  /**
@@ -65,11 +66,25 @@ import type {
65
66
  */
66
67
  export const fromSubscribable = <A, E = unknown>(
67
68
  subscribable: Subscribable<A>,
68
- ): FromSubscribableObservable<A, E> =>
69
- createRootObservable<Result<A, E>>(
70
- { initialValue: Optional.none },
71
- ({ startUpdate, complete }) => {
72
- subscribable.subscribe(
69
+ ): FromSubscribableObservable<A, E> => {
70
+ let mut_subscription: Subscription | undefined = undefined;
71
+
72
+ // Set once the subscribable has completed: its subscription is over, so
73
+ // completing this observable must not unsubscribe from it again.
74
+ let mut_isSourceCompleted = false;
75
+
76
+ const unsubscribe = (): void => {
77
+ if (!mut_isSourceCompleted) {
78
+ mut_subscription?.unsubscribe();
79
+ }
80
+
81
+ mut_subscription = undefined;
82
+ };
83
+
84
+ return createRootObservable<Result<A, E>>(
85
+ { initialValue: Optional.none, onComplete: unsubscribe },
86
+ ({ startUpdate, isCompleted, complete }) => {
87
+ const subscription = subscribable.subscribe(
73
88
  (nextValue) => {
74
89
  startUpdate(Result.ok(nextValue));
75
90
  },
@@ -82,10 +97,23 @@ export const fromSubscribable = <A, E = unknown>(
82
97
  );
83
98
  },
84
99
  () => {
100
+ mut_isSourceCompleted = true;
101
+
85
102
  complete();
86
103
  },
87
104
  );
88
105
 
106
+ // `subscribe` may have completed this observable before returning
107
+ // the subscription, so `unsubscribe` above had nothing to release.
108
+ if (isCompleted()) {
109
+ if (!mut_isSourceCompleted) {
110
+ subscription.unsubscribe();
111
+ }
112
+ } else {
113
+ mut_subscription = subscription;
114
+ }
115
+
89
116
  return {};
90
117
  },
91
118
  );
119
+ };
@@ -0,0 +1,129 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import { take } from '../predefined/index.mjs';
3
+ import type { Subscribable } from '../types/index.mjs';
4
+ import { fromSubscribable } from './from-subscribable.mjs';
5
+
6
+ describe(fromSubscribable, () => {
7
+ test('complete() unsubscribes from the subscribable once', () => {
8
+ const { subscribable, emit, unsubscribe } = createManualSubscribable();
9
+
10
+ const observable$ = fromSubscribable(subscribable);
11
+
12
+ observable$.complete();
13
+
14
+ observable$.complete();
15
+
16
+ expect(unsubscribe).toHaveBeenCalledExactlyOnceWith();
17
+
18
+ emit(1);
19
+
20
+ assert.isTrue(observable$.isCompleted);
21
+ });
22
+
23
+ test('dispose() unsubscribes from the subscribable', () => {
24
+ const { subscribable, unsubscribe } = createManualSubscribable();
25
+
26
+ fromSubscribable(subscribable).dispose();
27
+
28
+ expect(unsubscribe).toHaveBeenCalledExactlyOnceWith();
29
+ });
30
+
31
+ test('does not unsubscribe when the subscribable completes first', () => {
32
+ const { subscribable, emit, completeSource, unsubscribe } =
33
+ createManualSubscribable();
34
+
35
+ const observable$ = fromSubscribable(subscribable);
36
+
37
+ const mut_history: number[] = [];
38
+
39
+ observable$.subscribe((result) => {
40
+ if (Result.isOk(result)) {
41
+ mut_history.push(result.value);
42
+ }
43
+ });
44
+
45
+ emit(1);
46
+
47
+ completeSource();
48
+
49
+ assert.isTrue(observable$.isCompleted);
50
+
51
+ observable$.complete();
52
+
53
+ expect(unsubscribe).not.toHaveBeenCalled();
54
+
55
+ assert.deepStrictEqual(mut_history, [1]);
56
+ });
57
+
58
+ test('does not unsubscribe when the subscribable completes synchronously during subscribe', () => {
59
+ const unsubscribe = vi.fn<() => void>();
60
+
61
+ const observable$ = fromSubscribable<number>({
62
+ subscribe: (_onNext, _onError, onComplete) => {
63
+ onComplete?.();
64
+
65
+ return { unsubscribe };
66
+ },
67
+ });
68
+
69
+ assert.isTrue(observable$.isCompleted);
70
+
71
+ expect(unsubscribe).not.toHaveBeenCalled();
72
+ });
73
+
74
+ test('a child that completes with nothing else using it unsubscribes from the subscribable', () => {
75
+ const { subscribable, emit, unsubscribe } = createManualSubscribable();
76
+
77
+ const observable$ = fromSubscribable(subscribable);
78
+
79
+ const taken$ = observable$.pipe(take(1));
80
+
81
+ taken$.subscribe(() => {});
82
+
83
+ emit(1);
84
+
85
+ emit(2);
86
+
87
+ assert.isTrue(taken$.isCompleted);
88
+
89
+ assert.isTrue(observable$.isCompleted);
90
+
91
+ expect(unsubscribe).toHaveBeenCalledExactlyOnceWith();
92
+ });
93
+ });
94
+
95
+ const createManualSubscribable = (): Readonly<{
96
+ subscribable: Subscribable<number>;
97
+ emit: (value: number) => void;
98
+ completeSource: () => void;
99
+ unsubscribe: () => void;
100
+ }> => {
101
+ let mut_onNext: ((value: number) => void) | undefined = undefined;
102
+
103
+ let mut_onComplete: (() => void) | undefined = undefined;
104
+
105
+ const unsubscribe = vi.fn<() => void>(() => {
106
+ mut_onNext = undefined;
107
+
108
+ mut_onComplete = undefined;
109
+ });
110
+
111
+ return {
112
+ subscribable: {
113
+ subscribe: (onNext, _onError, onComplete) => {
114
+ mut_onNext = onNext;
115
+
116
+ mut_onComplete = onComplete;
117
+
118
+ return { unsubscribe };
119
+ },
120
+ },
121
+ emit: (value) => {
122
+ mut_onNext?.(value);
123
+ },
124
+ completeSource: () => {
125
+ mut_onComplete?.();
126
+ },
127
+ unsubscribe,
128
+ };
129
+ };
@@ -43,6 +43,13 @@ import type {
43
43
  *
44
44
  * assert.deepStrictEqual(valueHistory, [1, 2, 3]);
45
45
  * ```
46
+ *
47
+ * @note Do not call `next` from a subscriber or a projection while another
48
+ * update is propagating: the write propagates immediately, so subscribers can
49
+ * see an intermediate state, and it logs a `console.warn` (once per
50
+ * observable). Derive the value from its source instead —
51
+ * `const b$ = a$.pipe(map((a) => a * 2))` rather than
52
+ * `a$.subscribe((a) => { setB(a * 2); })`.
46
53
  */
47
54
  export function source<const A>(
48
55
  initialValue: A,
@@ -51,19 +58,27 @@ export function source<const A>(
51
58
  export function source<const A>(): SourceObservable<A>;
52
59
 
53
60
  export function source<const A>(...args: readonly A[]): SourceObservable<A> {
54
- return createRootObservable<A, Readonly<{ next: (nextValue: A) => void }>>(
61
+ return createRootObservable<
62
+ A,
63
+ Readonly<{ next: (nextValue: A) => A | undefined }>
64
+ >(
55
65
  {
56
66
  initialValue: Arr.isNonEmpty(args)
57
67
  ? Optional.some(args[0])
58
68
  : Optional.none,
59
69
  },
60
- ({ startUpdate, isCompleted }) => ({
70
+ ({ startUpdate, getSnapshot, isCompleted }) => ({
71
+ // Returns the value held after the call: `nextValue`, or, once
72
+ // completed, the value already there — `undefined` only on a source
73
+ // that never had one, whose `next` is typed `void`.
61
74
  next: (nextValue: A) => {
62
75
  if (isCompleted()) {
63
- return;
76
+ return Optional.unwrap(getSnapshot());
64
77
  }
65
78
 
66
79
  startUpdate(nextValue);
80
+
81
+ return nextValue;
67
82
  },
68
83
  }),
69
84
  );
@@ -0,0 +1,41 @@
1
+ import { expectType } from 'ts-data-forge';
2
+ import type { SourceObservable } from '../types/index.mjs';
3
+ import { source } from './source.mjs';
4
+
5
+ describe(source, () => {
6
+ test('`next` returns the value it passed on', () => {
7
+ const count$ = source<number>(0);
8
+
9
+ assert.strictEqual(count$.next(1), 1);
10
+
11
+ assert.strictEqual(count$.getSnapshot().value, 1);
12
+ });
13
+
14
+ test('once completed, `next` passes nothing on and returns the value held', () => {
15
+ const count$ = source<number>(0);
16
+
17
+ const mut_seen: number[] = [];
18
+
19
+ count$.subscribe((value) => {
20
+ mut_seen.push(value);
21
+ });
22
+
23
+ count$.next(1);
24
+
25
+ count$.complete();
26
+
27
+ assert.strictEqual(count$.next(2), 1);
28
+
29
+ assert.deepStrictEqual(mut_seen, [0, 1]);
30
+ });
31
+
32
+ test('`next` returns the value only on a source with an initial value', () => {
33
+ const passed = source<number>(0).next(1);
34
+
35
+ expectType<typeof passed, number>('=');
36
+
37
+ assert.strictEqual(passed, 1);
38
+
39
+ expectType<SourceObservable<number>['next'], (value: number) => void>('=');
40
+ });
41
+ });
@@ -98,9 +98,6 @@ import type {
98
98
  * 'C3',
99
99
  * ]);
100
100
  * ```
101
- *
102
- * @note To improve code readability, consider using `createState` instead of `mergeMap`,
103
- * subscribing to `parentObservable` and calling `setState` within it.
104
101
  */
105
102
  export const mergeMap =
106
103
  <A, B>(
@@ -4,6 +4,7 @@ import type {
4
4
  DropInitialValueOperator,
5
5
  Observable,
6
6
  SkipUntilOperatorObservable,
7
+ Subscription,
7
8
  } from '../types/index.mjs';
8
9
 
9
10
  /**
@@ -65,12 +66,28 @@ const createSkipUntilObservable = <A,>(
65
66
  parentObservable: Observable<A>,
66
67
  notifier: Observable<unknown>,
67
68
  ): SkipUntilOperatorObservable<A> => {
68
- let mut_isSkipping = true;
69
+ // A notifier that already holds a value, or has completed, would call
70
+ // `stopSkipping` inside `subscribe`, before the subscription is stored.
71
+ let mut_isSkipping =
72
+ Optional.isNone(notifier.getSnapshot()) && !notifier.isCompleted;
73
+
74
+ let mut_subscription: Subscription | undefined = undefined;
75
+
76
+ const stopSkipping = (): void => {
77
+ mut_isSkipping = false;
78
+
79
+ mut_subscription?.unsubscribe();
80
+
81
+ mut_subscription = undefined;
82
+ };
69
83
 
70
84
  const observable = createSyncChildObservable<A, readonly [A]>(
71
85
  {
72
86
  parents: [parentObservable],
73
87
  initialValue: Optional.none,
88
+ onComplete: () => {
89
+ mut_subscription?.unsubscribe();
90
+ },
74
91
  },
75
92
  ({ setNext }) =>
76
93
  (updateToken) => {
@@ -88,14 +105,9 @@ const createSkipUntilObservable = <A,>(
88
105
  },
89
106
  );
90
107
 
91
- notifier.subscribe(
92
- () => {
93
- mut_isSkipping = false;
94
- },
95
- () => {
96
- mut_isSkipping = false;
97
- },
98
- );
108
+ if (mut_isSkipping) {
109
+ mut_subscription = notifier.subscribe(stopSkipping, stopSkipping);
110
+ }
99
111
 
100
112
  return observable;
101
113
  };
@@ -96,9 +96,6 @@ import type {
96
96
  * 'C3',
97
97
  * ]);
98
98
  * ```
99
- *
100
- * @note To improve code readability, consider using `createState` instead of `switchMap`,
101
- * subscribe to `parentObservable` and call `setState` within it.
102
99
  */
103
100
  export const switchMap =
104
101
  <A, B>(
@@ -0,0 +1,127 @@
1
+ import { source } from '../create/index.mjs';
2
+ import { take } from '../predefined/index.mjs';
3
+ import { skipUntil } from './skip-until.mjs';
4
+ import { takeUntil } from './take-until.mjs';
5
+
6
+ describe('takeUntil releases the notifier', () => {
7
+ test('when the result is completed before the notifier emits', () => {
8
+ const notifier$ = source<undefined>();
9
+
10
+ const result$ = source<number>().pipe(takeUntil(notifier$));
11
+
12
+ assert.isTrue(notifier$.hasSubscriber);
13
+
14
+ result$.complete();
15
+
16
+ assert.isFalse(notifier$.hasSubscriber);
17
+ });
18
+
19
+ test('when the parent completes', () => {
20
+ const notifier$ = source<undefined>();
21
+
22
+ const num$ = source<number>();
23
+
24
+ num$.pipe(takeUntil(notifier$));
25
+
26
+ num$.complete();
27
+
28
+ assert.isFalse(notifier$.hasSubscriber);
29
+ });
30
+
31
+ test('when the downstream completes', () => {
32
+ const notifier$ = source<undefined>();
33
+
34
+ const num$ = source<number>();
35
+
36
+ const taken$ = num$.pipe(takeUntil(notifier$)).pipe(take(1));
37
+
38
+ taken$.subscribe(() => {});
39
+
40
+ // `take(n)` completes on the emission after its `n`th.
41
+ num$.next(1);
42
+
43
+ num$.next(2);
44
+
45
+ assert.isTrue(taken$.isCompleted);
46
+
47
+ assert.isFalse(notifier$.hasSubscriber);
48
+ });
49
+
50
+ test('when the notifier emits', () => {
51
+ const notifier$ = source<undefined>();
52
+
53
+ const result$ = source<number>().pipe(takeUntil(notifier$));
54
+
55
+ notifier$.next(undefined);
56
+
57
+ assert.isTrue(result$.isCompleted);
58
+
59
+ assert.isFalse(notifier$.hasSubscriber);
60
+ });
61
+
62
+ test('when the notifier has already emitted', () => {
63
+ const notifier$ = source<number>(0);
64
+
65
+ const result$ = source<number>().pipe(takeUntil(notifier$));
66
+
67
+ assert.isTrue(result$.isCompleted);
68
+
69
+ assert.isFalse(notifier$.hasSubscriber);
70
+ });
71
+ });
72
+
73
+ describe('skipUntil releases the notifier', () => {
74
+ test('when the notifier emits', () => {
75
+ const notifier$ = source<undefined>();
76
+
77
+ const num$ = source<number>();
78
+
79
+ const result$ = num$.pipe(skipUntil(notifier$));
80
+
81
+ const mut_history: number[] = [];
82
+
83
+ result$.subscribe((x) => {
84
+ mut_history.push(x);
85
+ });
86
+
87
+ num$.next(1);
88
+
89
+ notifier$.next(undefined);
90
+
91
+ assert.isFalse(notifier$.hasSubscriber);
92
+
93
+ num$.next(2);
94
+
95
+ assert.deepStrictEqual(mut_history, [2]);
96
+ });
97
+
98
+ test('when the notifier has already emitted', () => {
99
+ const notifier$ = source<number>(0);
100
+
101
+ const num$ = source<number>();
102
+
103
+ const result$ = num$.pipe(skipUntil(notifier$));
104
+
105
+ const mut_history: number[] = [];
106
+
107
+ result$.subscribe((x) => {
108
+ mut_history.push(x);
109
+ });
110
+
111
+ assert.isFalse(notifier$.hasSubscriber);
112
+
113
+ num$.next(1);
114
+
115
+ assert.deepStrictEqual(mut_history, [1]);
116
+ });
117
+
118
+ test('when the result is completed before the notifier emits', () => {
119
+ const notifier$ = source<undefined>();
120
+
121
+ const result$ = source<number>().pipe(skipUntil(notifier$));
122
+
123
+ result$.complete();
124
+
125
+ assert.isFalse(notifier$.hasSubscriber);
126
+ });
127
+ });
@@ -3,6 +3,7 @@ import { createSyncChildObservable } from '../base/index.mjs';
3
3
  import type {
4
4
  KeepInitialValueOperator,
5
5
  Observable,
6
+ Subscription,
6
7
  TakeUntilOperatorObservable,
7
8
  } from '../types/index.mjs';
8
9
 
@@ -68,10 +69,15 @@ const createTakeUntilObservable = <A,>(
68
69
  parentObservable: Observable<A>,
69
70
  notifier: Observable<unknown>,
70
71
  ): TakeUntilOperatorObservable<A> => {
72
+ let mut_subscription: Subscription | undefined = undefined;
73
+
71
74
  const observable = createSyncChildObservable<A, readonly [A]>(
72
75
  {
73
76
  parents: [parentObservable],
74
77
  initialValue: parentObservable.getSnapshot(),
78
+ onComplete: () => {
79
+ mut_subscription?.unsubscribe();
80
+ },
75
81
  },
76
82
  ({ setNext }) =>
77
83
  (updateToken) => {
@@ -88,7 +94,7 @@ const createTakeUntilObservable = <A,>(
88
94
  },
89
95
  );
90
96
 
91
- notifier.subscribe(
97
+ const subscription = notifier.subscribe(
92
98
  () => {
93
99
  observable.complete();
94
100
  },
@@ -97,5 +103,13 @@ const createTakeUntilObservable = <A,>(
97
103
  },
98
104
  );
99
105
 
106
+ // A notifier that already holds a value completes `observable` inside
107
+ // `subscribe`, before `onComplete` can see the subscription.
108
+ if (observable.isCompleted) {
109
+ subscription.unsubscribe();
110
+ } else {
111
+ mut_subscription = subscription;
112
+ }
113
+
100
114
  return observable;
101
115
  };
@@ -3,6 +3,7 @@ import { createSyncChildObservable } from '../base/index.mjs';
3
3
  import type {
4
4
  KeepInitialValueOperator,
5
5
  Observable,
6
+ Subscription,
6
7
  WithBufferedFromOperatorObservable,
7
8
  } from '../types/index.mjs';
8
9
  import { maxDepth } from '../utils/index.mjs';
@@ -86,6 +87,8 @@ const createWithBufferedFromObservable = <A, B>(
86
87
  mut_bufferedValues = [];
87
88
  };
88
89
 
90
+ let mut_subscription: Subscription | undefined = undefined;
91
+
89
92
  const result = createSyncChildObservable<
90
93
  readonly [A, readonly B[]],
91
94
  readonly [A]
@@ -104,6 +107,11 @@ const createWithBufferedFromObservable = <A, B>(
104
107
  Optional.isNone(me) ? [] : [me.value],
105
108
  ] as const),
106
109
  ).value,
110
+ onComplete: () => {
111
+ mut_subscription?.unsubscribe();
112
+
113
+ clearBuffer();
114
+ },
107
115
  },
108
116
  ({ setNext }) =>
109
117
  (updateToken) => {
@@ -122,9 +130,13 @@ const createWithBufferedFromObservable = <A, B>(
122
130
  },
123
131
  );
124
132
 
125
- observable.subscribe((value) => {
126
- mut_bufferedValues = Arr.toPushed(mut_bufferedValues, value);
127
- });
133
+ // A child completed at creation has already run `onComplete`; subscribing
134
+ // after it would leave a subscription nothing releases.
135
+ if (!result.isCompleted) {
136
+ mut_subscription = observable.subscribe((value) => {
137
+ mut_bufferedValues = Arr.toPushed(mut_bufferedValues, value);
138
+ });
139
+ }
128
140
 
129
141
  return result;
130
142
  };