synstate 6.2.1 → 7.0.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 +1 -1
- package/dist/core/class/child-observable-class.d.mts +16 -2
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +37 -4
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/create-child-observable.d.mts +1 -1
- package/dist/core/class/create-child-observable.d.mts.map +1 -1
- package/dist/core/class/create-child-observable.mjs +6 -8
- package/dist/core/class/create-child-observable.mjs.map +1 -1
- package/dist/core/class/create-root-observable.d.mts +1 -1
- package/dist/core/class/create-root-observable.d.mts.map +1 -1
- package/dist/core/class/create-root-observable.mjs +1 -5
- package/dist/core/class/create-root-observable.mjs.map +1 -1
- package/dist/core/class/propagation-state.mjs +1 -1
- package/dist/core/class/propagation-state.mjs.map +1 -1
- package/dist/core/combine/derive.d.mts.map +1 -1
- package/dist/core/combine/derive.mjs +2 -1
- package/dist/core/combine/derive.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +5 -2
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.mjs +2 -2
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs +10 -3
- package/dist/core/create/from-abortable-promise.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +10 -3
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs +2 -1
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts +1 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs +2 -3
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +6 -2
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +47 -16
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts +1 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +9 -6
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +4 -0
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +16 -6
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +2 -2
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +10 -8
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/throttle.mjs +3 -2
- package/dist/core/operators/throttle.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +13 -3
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/predefined/operators/attach-index.d.mts +1 -1
- package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
- package/dist/core/predefined/operators/attach-index.mjs +2 -3
- package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
- package/dist/core/predefined/operators/skip.d.mts.map +1 -1
- package/dist/core/predefined/operators/skip.mjs +2 -2
- package/dist/core/predefined/operators/skip.mjs.map +1 -1
- package/dist/core/utils/index.d.mts +1 -0
- package/dist/core/utils/index.d.mts.map +1 -1
- package/dist/core/utils/index.mjs +1 -0
- package/dist/core/utils/index.mjs.map +1 -1
- package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
- package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
- package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
- package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
- package/package.json +12 -11
- package/src/core/class/child-observable-class.mts +45 -3
- package/src/core/class/create-child-observable.mts +8 -5
- package/src/core/class/create-root-observable.mts +2 -1
- package/src/core/class/propagation-state.mts +1 -1
- package/src/core/class/write-during-propagation.test.mts +75 -4
- package/src/core/combine/derive.mts +2 -1
- package/src/core/combine/zip.mts +5 -2
- package/src/core/create/counter.mts +2 -2
- package/src/core/create/from-abortable-promise.mts +33 -23
- package/src/core/create/from-promise.mts +28 -18
- package/src/core/create/from-promise.test.mts +97 -0
- package/src/core/create/source.mts +2 -1
- package/src/core/operators/filter.mts +2 -3
- package/src/core/operators/flat-map-initial-value.test.mts +269 -0
- package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
- package/src/core/operators/inner-ownership.test.mts +145 -2
- package/src/core/operators/merge-map.mts +56 -26
- package/src/core/operators/skip-while.mts +12 -8
- package/src/core/operators/skip-while.test.mts +59 -0
- package/src/core/operators/switch-map.mts +21 -10
- package/src/core/operators/take-while.mts +15 -17
- package/src/core/operators/take-while.test.mts +51 -0
- package/src/core/operators/throttle.mts +4 -3
- package/src/core/operators/with-buffered-from.mts +16 -3
- package/src/core/operators/with-buffered-from.test.mts +27 -0
- package/src/core/predefined/operators/attach-index.mts +2 -3
- package/src/core/predefined/operators/skip.mts +2 -2
- package/src/core/utils/index.mts +1 -0
- package/src/core/utils/rethrow-out-of-promise.mts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synstate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Type-safe State Management Library for TypeScript/JavaScript",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,21 +29,22 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"ts-data-forge": "^14.
|
|
33
|
-
"ts-std-forge": "^0.5.
|
|
32
|
+
"ts-data-forge": "^14.11.0",
|
|
33
|
+
"ts-std-forge": "^0.5.3",
|
|
34
34
|
"ts-type-forge": "^9.2.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@preact/signals-core": "1.14.4",
|
|
38
38
|
"@reduxjs/toolkit": "2.13.0",
|
|
39
|
-
"@sumi-lang/cli": "0.1.
|
|
39
|
+
"@sumi-lang/cli": "0.1.6",
|
|
40
40
|
"@types/react": "19.3.0",
|
|
41
41
|
"@vitest/browser-playwright": "5.0.5",
|
|
42
42
|
"argparse": "3.0.3",
|
|
43
|
-
"eslint
|
|
44
|
-
"eslint-
|
|
45
|
-
"eslint-plugin-ts-
|
|
46
|
-
"eslint-plugin-ts-
|
|
43
|
+
"eslint": "10.13.0",
|
|
44
|
+
"eslint-config-typed": "7.4.0",
|
|
45
|
+
"eslint-plugin-ts-data-forge": "0.10.9",
|
|
46
|
+
"eslint-plugin-ts-fortress": "1.3.13",
|
|
47
|
+
"eslint-plugin-ts-type-forge": "0.8.18",
|
|
47
48
|
"jotai": "3.0.1",
|
|
48
49
|
"mobx": "7.1.0",
|
|
49
50
|
"playwright": "1.64.1",
|
|
@@ -53,9 +54,9 @@
|
|
|
53
54
|
"react": "19.3.1",
|
|
54
55
|
"rxjs": "7.8.2",
|
|
55
56
|
"solid-js": "1.9.17",
|
|
56
|
-
"synstate-react-hooks": "
|
|
57
|
-
"synstate-react-hooks-compat": "
|
|
58
|
-
"ts-repo-utils": "10.7.
|
|
57
|
+
"synstate-react-hooks": "7.0.0",
|
|
58
|
+
"synstate-react-hooks-compat": "7.0.0",
|
|
59
|
+
"ts-repo-utils": "10.7.2",
|
|
59
60
|
"valtio": "2.3.2",
|
|
60
61
|
"zustand": "5.0.15"
|
|
61
62
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
3
3
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
4
4
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
5
|
-
import { hasKey, isRecord,
|
|
5
|
+
import { hasKey, isRecord, Optional, type Some } from 'ts-data-forge';
|
|
6
6
|
import { panic } from 'ts-std-forge';
|
|
7
7
|
import type { MutableSet } from 'ts-type-forge';
|
|
8
8
|
import type {
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
WithInitialValueOperator,
|
|
21
21
|
Wrap,
|
|
22
22
|
} from '../types/index.mjs';
|
|
23
|
-
import { maxDepth } from '../utils/index.mjs';
|
|
23
|
+
import { issueUnqueuedToken, maxDepth } from '../utils/index.mjs';
|
|
24
24
|
import { ManagerObservableClass } from './manager-observable-class.mjs';
|
|
25
25
|
import { ObservableBaseClass } from './observable-base-class.mjs';
|
|
26
26
|
import { toNode, type ChildNode } from './observable-node.mjs';
|
|
@@ -198,6 +198,45 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* `attach`, then projects the value `parent` holds when `switchMap` /
|
|
203
|
+
* `mergeMap` is created, as `map` and `filter` start from it; a completed
|
|
204
|
+
* parent is not projected. Nothing observes the operator yet, so a value the
|
|
205
|
+
* inner observable emits while it is subscribed is only stored: the token is
|
|
206
|
+
* one no update is queued with.
|
|
207
|
+
*
|
|
208
|
+
* If the projection throws, the operator is disposed, which disposes what the
|
|
209
|
+
* projection made and takes the operator off its parent, and the exception
|
|
210
|
+
* goes on to the caller of `pipe`.
|
|
211
|
+
*/
|
|
212
|
+
protected attachProjectingHeldValue<V>(
|
|
213
|
+
parent: Observable<V>,
|
|
214
|
+
project: (value: V, updateToken: UpdateToken) => void,
|
|
215
|
+
): void {
|
|
216
|
+
this.attach();
|
|
217
|
+
|
|
218
|
+
const sn = parent.getSnapshot();
|
|
219
|
+
|
|
220
|
+
if (this.isCompleted || Optional.isNone(sn)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A flag and `finally` rather than `catch` and rethrow: Sumi bans
|
|
225
|
+
// `try..catch`, and the exception is never caught on its way to `pipe`.
|
|
226
|
+
let mut_projected = false;
|
|
227
|
+
|
|
228
|
+
// @sumi-expect-error exceptions/no-try
|
|
229
|
+
try {
|
|
230
|
+
project(sn.value, issueUnqueuedToken());
|
|
231
|
+
|
|
232
|
+
mut_projected = true;
|
|
233
|
+
} finally {
|
|
234
|
+
if (!mut_projected) {
|
|
235
|
+
this.dispose();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
201
240
|
/**
|
|
202
241
|
* Subscribes to the inner observable that the projection of `switchMap` /
|
|
203
242
|
* `mergeMap` returned.
|
|
@@ -212,10 +251,13 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
212
251
|
* glitch), and would emit twice.
|
|
213
252
|
*
|
|
214
253
|
* Values the inner observable emits later start updates of their own.
|
|
254
|
+
* `onComplete` is called when it completes, which may be while it is being
|
|
255
|
+
* subscribed, before this returns.
|
|
215
256
|
*/
|
|
216
257
|
protected subscribeInner(
|
|
217
258
|
inner: Observable<A>,
|
|
218
259
|
updateToken: UpdateToken,
|
|
260
|
+
onComplete?: () => void,
|
|
219
261
|
): Subscription {
|
|
220
262
|
let mut_isSubscribing = true;
|
|
221
263
|
|
|
@@ -225,7 +267,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
225
267
|
} else {
|
|
226
268
|
this.startUpdate(value);
|
|
227
269
|
}
|
|
228
|
-
});
|
|
270
|
+
}, onComplete);
|
|
229
271
|
|
|
230
272
|
mut_isSubscribing = false;
|
|
231
273
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
3
3
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
4
4
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
5
|
-
import {
|
|
5
|
+
import type { Optional, Some } from 'ts-data-forge';
|
|
6
|
+
import * as Arr from 'ts-data-forge/array';
|
|
6
7
|
import { unreachable } from 'ts-std-forge';
|
|
7
8
|
import type {
|
|
8
9
|
AsyncChildObservable,
|
|
@@ -300,7 +301,9 @@ class CustomAsyncChildObservableClass<
|
|
|
300
301
|
const P extends NonEmptyUnknownList,
|
|
301
302
|
> extends AsyncChildObservableClass<A, P> {
|
|
302
303
|
#mut_teardowns: readonly (() => void)[] | undefined;
|
|
303
|
-
|
|
304
|
+
// Undefined until `init` returns, which may complete the observable first;
|
|
305
|
+
// `complete` then calls `flush` before there is a hook to call.
|
|
306
|
+
readonly #hooks: AsyncChildObservableHooks | undefined;
|
|
304
307
|
|
|
305
308
|
constructor(
|
|
306
309
|
config: ChildObservableConfig<A, P>,
|
|
@@ -344,15 +347,15 @@ class CustomAsyncChildObservableClass<
|
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
override tryUpdate(updateToken: UpdateToken): void {
|
|
347
|
-
this.#hooks
|
|
350
|
+
this.#hooks?.tryUpdate(updateToken);
|
|
348
351
|
}
|
|
349
352
|
|
|
350
353
|
override canEmitNoMore(): boolean {
|
|
351
|
-
return this.#hooks
|
|
354
|
+
return this.#hooks?.canEmitNoMore?.() === true;
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
protected override flush(): void {
|
|
355
|
-
this.#hooks
|
|
358
|
+
this.#hooks?.flush?.();
|
|
356
359
|
}
|
|
357
360
|
|
|
358
361
|
protected override teardown(): void {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
3
3
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
4
4
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
5
|
-
import {
|
|
5
|
+
import type { Optional } from 'ts-data-forge';
|
|
6
|
+
import * as Arr from 'ts-data-forge/array';
|
|
6
7
|
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
7
8
|
import type { RootObservable } from '../types/index.mjs';
|
|
8
9
|
import { RootObservableClass } from './root-observable-class.mjs';
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* costs about twice as much per access as a module binding, on a path every
|
|
12
12
|
* update takes.
|
|
13
13
|
*/
|
|
14
|
-
import
|
|
14
|
+
import * as Arr from 'ts-data-forge/array';
|
|
15
15
|
import { panic } from 'ts-std-forge';
|
|
16
16
|
import type { MutableSet } from 'ts-type-forge';
|
|
17
17
|
import type { UpdateToken } from '../types/index.mjs';
|
|
@@ -4,7 +4,12 @@ import type { FixedLengthTuple } from 'ts-type-forge';
|
|
|
4
4
|
import { createState } from '../../utils/index.mjs';
|
|
5
5
|
import { combine } from '../combine/index.mjs';
|
|
6
6
|
import { fromSubscribable, source } from '../create/index.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
map,
|
|
9
|
+
switchMap,
|
|
10
|
+
throttle,
|
|
11
|
+
withBufferedFrom,
|
|
12
|
+
} from '../operators/index.mjs';
|
|
8
13
|
import { writeDuringPropagationWarning } from './root-observable-class.mjs';
|
|
9
14
|
|
|
10
15
|
describe('a write to a root observable while an update is propagating (#2336)', () => {
|
|
@@ -156,7 +161,8 @@ describe('a write to a root observable while an update is propagating (#2336)',
|
|
|
156
161
|
|
|
157
162
|
assert.deepStrictEqual(warn, []);
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
// 0 from the value `outer$` held when `switchMap` was created.
|
|
165
|
+
assert.deepStrictEqual(mut_history, [0, 10]);
|
|
160
166
|
});
|
|
161
167
|
|
|
162
168
|
test('does not warn when a root created during propagation emits while it is created', async () => {
|
|
@@ -188,7 +194,8 @@ describe('a write to a root observable while an update is propagating (#2336)',
|
|
|
188
194
|
|
|
189
195
|
assert.deepStrictEqual(warn, []);
|
|
190
196
|
|
|
191
|
-
|
|
197
|
+
// 0 from the value `outer$` held when `switchMap` was created.
|
|
198
|
+
assert.deepStrictEqual(mut_history, [0, 10]);
|
|
192
199
|
});
|
|
193
200
|
|
|
194
201
|
test('a subscriber that throws does not leave the propagation flag set', async () => {
|
|
@@ -214,12 +221,76 @@ describe('a write to a root observable while an update is propagating (#2336)',
|
|
|
214
221
|
});
|
|
215
222
|
});
|
|
216
223
|
|
|
224
|
+
describe('a stateful operator updates its state before setNext, which a write from a subscriber re-enters', () => {
|
|
225
|
+
test('withBufferedFrom keeps a value written to the buffered observable for the next batch', async () => {
|
|
226
|
+
const [data$, setData] = createState('a');
|
|
227
|
+
|
|
228
|
+
const [trigger$, setTrigger] = createState(0);
|
|
229
|
+
|
|
230
|
+
const result$ = trigger$.pipe(withBufferedFrom(data$));
|
|
231
|
+
|
|
232
|
+
const mut_history: (readonly [number, readonly string[]])[] = [];
|
|
233
|
+
|
|
234
|
+
result$.subscribe((v) => {
|
|
235
|
+
mut_history.push(v);
|
|
236
|
+
|
|
237
|
+
if (v[0] === 1) {
|
|
238
|
+
setData('b');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await captureWarnings(() => {
|
|
243
|
+
setTrigger(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
setTrigger(2);
|
|
247
|
+
|
|
248
|
+
assert.deepStrictEqual(mut_history, [
|
|
249
|
+
[0, ['a']],
|
|
250
|
+
[1, []],
|
|
251
|
+
[2, ['b']],
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('throttle drops a value a subscriber writes to the parent inside the window, and keeps one timer', async () => {
|
|
256
|
+
vi.useFakeTimers();
|
|
257
|
+
|
|
258
|
+
onTestFinished(() => {
|
|
259
|
+
vi.useRealTimers();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const [src$, setSrc] = createState(0);
|
|
263
|
+
|
|
264
|
+
const throttled$ = src$.pipe(throttle(1000));
|
|
265
|
+
|
|
266
|
+
const mut_history: number[] = [];
|
|
267
|
+
|
|
268
|
+
throttled$.subscribe((v) => {
|
|
269
|
+
mut_history.push(v);
|
|
270
|
+
|
|
271
|
+
if (v === 1) {
|
|
272
|
+
setSrc(2);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await captureWarnings(() => {
|
|
277
|
+
setSrc(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
assert.deepStrictEqual(mut_history, [0, 1]);
|
|
281
|
+
|
|
282
|
+
throttled$.complete();
|
|
283
|
+
|
|
284
|
+
assert.strictEqual(vi.getTimerCount(), 0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
217
288
|
/**
|
|
218
289
|
* Runs `run` with `console.warn` replaced by a mock, and returns the messages
|
|
219
290
|
* it was called with.
|
|
220
291
|
*/
|
|
221
292
|
const captureWarnings = async (
|
|
222
|
-
run: () => Promise<void> |
|
|
293
|
+
run: () => Promise<void> | undefined,
|
|
223
294
|
): Promise<readonly unknown[]> => {
|
|
224
295
|
const warn = vi.fn();
|
|
225
296
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import {
|
|
8
|
+
import { Optional } from 'ts-data-forge';
|
|
9
|
+
import * as Arr from 'ts-data-forge/array';
|
|
9
10
|
import type { NonEmptyTuple } from 'ts-type-forge';
|
|
10
11
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
11
12
|
import type {
|
package/src/core/combine/zip.mts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import {
|
|
8
|
+
import { Optional, createQueue, expectType } from 'ts-data-forge';
|
|
9
|
+
import * as Arr from 'ts-data-forge/array';
|
|
9
10
|
import type { NonEmptyTuple, Tuple } from 'ts-type-forge';
|
|
10
11
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
11
12
|
import { source } from '../create/index.mjs';
|
|
@@ -150,8 +151,10 @@ class ZipObservableClass<const A extends NonEmptyUnknownList>
|
|
|
150
151
|
|
|
151
152
|
this.setNext(nextValue, updateToken);
|
|
152
153
|
|
|
154
|
+
// After the update has reached what is derived from it, as in `take`
|
|
155
|
+
// (#2371).
|
|
153
156
|
if (this.canEmitNoMore()) {
|
|
154
|
-
this.
|
|
157
|
+
this.completeAfterPropagation();
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import { asSafeUint, Optional, SafeUint } from 'ts-data-forge';
|
|
8
|
+
import { asSafeUint, Optional, type SafeUint } from 'ts-data-forge';
|
|
9
9
|
import type { PositiveSafeIntWithSmallInt } from 'ts-type-forge';
|
|
10
10
|
import { RootObservableClass } from '../class/index.mjs';
|
|
11
11
|
import type { CounterObservable, TimerId } from '../types/index.mjs';
|
|
@@ -154,7 +154,7 @@ class CounterObservableClass
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
this.#mut_timerId = setInterval(() => {
|
|
157
|
-
this.#mut_counter =
|
|
157
|
+
this.#mut_counter = asSafeUint(this.#mut_counter + 1);
|
|
158
158
|
|
|
159
159
|
this.#emit(this.#mut_counter);
|
|
160
160
|
}, this.#intervalMilliSeconds);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { Optional, Result } from 'ts-data-forge';
|
|
9
9
|
import { RootObservableClass } from '../class/index.mjs';
|
|
10
10
|
import type { FromPromiseObservable } from '../types/index.mjs';
|
|
11
|
+
import { rethrowOutOfPromise } from '../utils/index.mjs';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Creates an observable from a Promise factory that receives an `AbortSignal`.
|
|
@@ -96,35 +97,44 @@ class FromAbortablePromiseObservableClass<A, E = unknown>
|
|
|
96
97
|
|
|
97
98
|
const promise = factory(this.#abortController.signal);
|
|
98
99
|
|
|
100
|
+
// `onRejected` beside `onFulfilled`, so that only the promise's own
|
|
101
|
+
// rejection becomes an Err: what a subscriber throws on the Ok value is not
|
|
102
|
+
// the promise's (#2931). A `.catch` after `.then` would take both, and is
|
|
103
|
+
// also a step later.
|
|
99
104
|
promise
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
// eslint-disable-next-line unicorn/prefer-then-catch -- its suggestion, `.then(onFulfilled).catch(onRejected)`, would take what a subscriber throws too
|
|
106
|
+
.then(
|
|
107
|
+
(value) => {
|
|
108
|
+
if (this.isCompleted) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
this.startUpdate(Result.ok(value));
|
|
113
|
+
},
|
|
114
|
+
(error: unknown) => {
|
|
115
|
+
if (this.isCompleted) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
// Silently ignore AbortError — it means the observable was
|
|
120
|
+
// intentionally completed (e.g., by switchMap).
|
|
121
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
this.startUpdate(
|
|
126
|
+
Result.err(
|
|
127
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
128
|
+
error as E,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
.catch(rethrowOutOfPromise)
|
|
125
134
|
.finally(() => {
|
|
126
135
|
this.complete();
|
|
127
|
-
})
|
|
136
|
+
})
|
|
137
|
+
.catch(rethrowOutOfPromise);
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
protected override teardown(): void {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { Optional, Result } from 'ts-data-forge';
|
|
8
8
|
import { RootObservableClass } from '../class/index.mjs';
|
|
9
9
|
import type { FromPromiseObservable } from '../types/index.mjs';
|
|
10
|
+
import { rethrowOutOfPromise } from '../utils/index.mjs';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Creates an observable from a Promise.
|
|
@@ -66,28 +67,37 @@ class FromPromiseObservableClass<A, E = unknown>
|
|
|
66
67
|
constructor(promise: Promise<A>) {
|
|
67
68
|
super({ initialValue: Optional.none });
|
|
68
69
|
|
|
70
|
+
// `onRejected` beside `onFulfilled`, so that only the promise's own
|
|
71
|
+
// rejection becomes an Err: what a subscriber throws on the Ok value is not
|
|
72
|
+
// the promise's (#2931). A `.catch` after `.then` would take both, and is
|
|
73
|
+
// also a step later.
|
|
69
74
|
promise
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// eslint-disable-next-line unicorn/prefer-then-catch -- its suggestion, `.then(onFulfilled).catch(onRejected)`, would take what a subscriber throws too
|
|
76
|
+
.then(
|
|
77
|
+
(value) => {
|
|
78
|
+
if (this.isCompleted) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
this.startUpdate(Result.ok(value));
|
|
83
|
+
},
|
|
84
|
+
(error: unknown) => {
|
|
85
|
+
if (this.isCompleted) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
this.startUpdate(
|
|
90
|
+
Result.err(
|
|
91
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
92
|
+
error as E,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
.catch(rethrowOutOfPromise)
|
|
89
98
|
.finally(() => {
|
|
90
99
|
this.complete();
|
|
91
|
-
})
|
|
100
|
+
})
|
|
101
|
+
.catch(rethrowOutOfPromise);
|
|
92
102
|
}
|
|
93
103
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Optional, Result } from 'ts-data-forge';
|
|
2
|
+
import { panic } from 'ts-std-forge';
|
|
3
|
+
import { fromAbortablePromise } from './from-abortable-promise.mjs';
|
|
4
|
+
import { fromPromise } from './from-promise.mjs';
|
|
5
|
+
|
|
6
|
+
describe('fromPromise and fromAbortablePromise report only the promise’s own rejection as an Err (#2931)', () => {
|
|
7
|
+
test.each([
|
|
8
|
+
['fromPromise', () => fromPromise(Promise.resolve(1))],
|
|
9
|
+
[
|
|
10
|
+
'fromAbortablePromise',
|
|
11
|
+
() => fromAbortablePromise(() => Promise.resolve(1)),
|
|
12
|
+
],
|
|
13
|
+
] as const)(
|
|
14
|
+
'%s: a subscriber that throws on the Ok value gets no Err',
|
|
15
|
+
async (_, create) => {
|
|
16
|
+
vi.useFakeTimers();
|
|
17
|
+
|
|
18
|
+
onTestFinished(() => {
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const mut_events: string[] = [];
|
|
23
|
+
|
|
24
|
+
const result$ = create();
|
|
25
|
+
|
|
26
|
+
result$.subscribe(
|
|
27
|
+
(r) => {
|
|
28
|
+
mut_events.push(Result.isOk(r) ? `ok ${r.value}` : 'err');
|
|
29
|
+
|
|
30
|
+
if (Result.isOk(r)) {
|
|
31
|
+
panic('bug in a subscriber');
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
() => {
|
|
35
|
+
mut_events.push('complete');
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await settlePromises();
|
|
40
|
+
|
|
41
|
+
assert.deepStrictEqual(mut_events, ['ok 1', 'complete']);
|
|
42
|
+
|
|
43
|
+
// The subscriber's exception is not swallowed: it is thrown again from a
|
|
44
|
+
// task of its own.
|
|
45
|
+
expect(() => {
|
|
46
|
+
vi.runOnlyPendingTimers();
|
|
47
|
+
}).toThrow('bug in a subscriber');
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const rejection = new Error('rejected');
|
|
52
|
+
|
|
53
|
+
test.each([
|
|
54
|
+
['fromPromise', () => fromPromise(Promise.reject(rejection))],
|
|
55
|
+
[
|
|
56
|
+
'fromAbortablePromise',
|
|
57
|
+
() => fromAbortablePromise(() => Promise.reject(rejection)),
|
|
58
|
+
],
|
|
59
|
+
] as const)('%s: a rejection is an Err', async (_, create) => {
|
|
60
|
+
const result$ = create();
|
|
61
|
+
|
|
62
|
+
const mut_events: string[] = [];
|
|
63
|
+
|
|
64
|
+
result$.subscribe(
|
|
65
|
+
(r) => {
|
|
66
|
+
mut_events.push(
|
|
67
|
+
Result.isErr(r) && r.value === rejection ? 'err rejected' : 'ok',
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
() => {
|
|
71
|
+
mut_events.push('complete');
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await settlePromises();
|
|
76
|
+
|
|
77
|
+
assert.deepStrictEqual(mut_events, ['err rejected', 'complete']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('fromPromise holds the value by the time an await of the same promise resumes', async () => {
|
|
81
|
+
const promise = Promise.resolve(1);
|
|
82
|
+
|
|
83
|
+
const result$ = fromPromise(promise);
|
|
84
|
+
|
|
85
|
+
await promise;
|
|
86
|
+
|
|
87
|
+
assert.deepStrictEqual(result$.getSnapshot(), Optional.some(Result.ok(1)));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Lets the promise chains already started run to their end. */
|
|
92
|
+
const settlePromises = async (): Promise<void> => {
|
|
93
|
+
for (const _ of Array.from({ length: 10 })) {
|
|
94
|
+
// eslint-disable-next-line no-await-in-loop -- each await lets the chains take one more step
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// @sumi-expect-error-file classes/no-class
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
|
-
import {
|
|
7
|
+
import { Optional } from 'ts-data-forge';
|
|
8
|
+
import * as Arr from 'ts-data-forge/array';
|
|
8
9
|
import { RootObservableClass } from '../class/index.mjs';
|
|
9
10
|
import type {
|
|
10
11
|
InitializedSourceObservable,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
isString,
|
|
13
13
|
Optional,
|
|
14
14
|
pipe,
|
|
15
|
-
SafeUint,
|
|
15
|
+
type SafeUint,
|
|
16
16
|
} from 'ts-data-forge';
|
|
17
17
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
18
18
|
import { source } from '../create/index.mjs';
|
|
@@ -132,8 +132,7 @@ class FilterObservableClass<A>
|
|
|
132
132
|
return; // skip update
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
this.#mut_index =
|
|
136
|
-
this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
|
|
135
|
+
this.#mut_index = asSafeUint(this.#mut_index + 1);
|
|
137
136
|
|
|
138
137
|
if (this.#predicate(sn.value, this.#mut_index)) {
|
|
139
138
|
this.setNext(sn.value, updateToken);
|