synstate 6.2.1 → 7.0.1
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/dist/core/class/child-observable-class.d.mts +16 -14
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +39 -12
- 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 +2 -0
- package/dist/core/utils/index.d.mts.map +1 -1
- package/dist/core/utils/index.mjs +2 -0
- package/dist/core/utils/index.mjs.map +1 -1
- package/dist/core/utils/is-development-build.d.mts +15 -0
- package/dist/core/utils/is-development-build.d.mts.map +1 -0
- package/dist/core/utils/is-development-build.mjs +23 -0
- package/dist/core/utils/is-development-build.mjs.map +1 -0
- 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 +53 -17
- package/src/core/class/circular-dependency-comparison.test.mts +36 -40
- package/src/core/class/circular-dependency.test.mts +6 -37
- 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 +2 -0
- package/src/core/utils/is-development-build.mts +32 -0
- package/src/core/utils/is-development-build.test.mts +44 -0
- package/src/core/utils/rethrow-out-of-promise.mts +16 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this is a development build: `NODE_ENV` is not `production`.
|
|
3
|
+
*
|
|
4
|
+
* `process.env.NODE_ENV` is written out in full because that is the expression
|
|
5
|
+
* bundlers (Vite, webpack, esbuild) replace, in the application's build and in
|
|
6
|
+
* a pre-bundled dependency alike: with the string `"development"` in a
|
|
7
|
+
* development build and `"production"` in a production one. Where nothing
|
|
8
|
+
* replaces it and there is no `process` (an unbundled module in a browser),
|
|
9
|
+
* reading it throws and the answer is `false`.
|
|
10
|
+
*
|
|
11
|
+
* synstate-router reads it the same way
|
|
12
|
+
* (`libs/synstate-router/src/internal/is-development.mts`).
|
|
13
|
+
*/
|
|
14
|
+
export declare const isDevelopmentBuild: () => boolean;
|
|
15
|
+
//# sourceMappingURL=is-development-build.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-development-build.d.mts","sourceRoot":"","sources":["../../../src/core/utils/is-development-build.mts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,QAAO,OAOrC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-development-build.mjs","sourceRoot":"","sources":["../../../src/core/utils/is-development-build.mts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAY,EAAE;IAC9C,uCAAuC;IACvC,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throws `error` again from a task of its own, for a `.catch` after each step
|
|
3
|
+
* of a promise chain that runs subscribers (`fromPromise`): after the step that
|
|
4
|
+
* emits and after the one that completes, so that neither exception replaces
|
|
5
|
+
* the other. A subscriber's exception then surfaces as an uncaught error, as
|
|
6
|
+
* one thrown during an update a timer started does (`debounce`, `timer`),
|
|
7
|
+
* rather than as a rejection or not at all.
|
|
8
|
+
*/
|
|
9
|
+
export declare const rethrowOutOfPromise: (error: unknown) => void;
|
|
10
|
+
//# sourceMappingURL=rethrow-out-of-promise.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rethrow-out-of-promise.d.mts","sourceRoot":"","sources":["../../../src/core/utils/rethrow-out-of-promise.mts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,UAAW,OAAO,KAAG,IAOpD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rethrow-out-of-promise.mjs","sourceRoot":"","sources":["../../../src/core/utils/rethrow-out-of-promise.mts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAc,EAAQ,EAAE;IAC1D,UAAU,CAAC,GAAG,EAAE;QACd,uDAAuD;QACvD,yCAAyC;QACzC,sHAAsH;QACtH,MAAM,KAAK,CAAC;IACd,CAAC,EAAE,CAAC,CAAC,CAAC;AACR,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synstate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
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.7",
|
|
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.5.0",
|
|
45
|
+
"eslint-plugin-ts-data-forge": "0.10.10",
|
|
46
|
+
"eslint-plugin-ts-fortress": "1.3.14",
|
|
47
|
+
"eslint-plugin-ts-type-forge": "0.8.19",
|
|
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.1",
|
|
58
|
+
"synstate-react-hooks-compat": "7.0.1",
|
|
59
|
+
"ts-repo-utils": "10.7.3",
|
|
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 {
|
|
5
|
+
import { 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,11 @@ import type {
|
|
|
20
20
|
WithInitialValueOperator,
|
|
21
21
|
Wrap,
|
|
22
22
|
} from '../types/index.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
isDevelopmentBuild,
|
|
25
|
+
issueUnqueuedToken,
|
|
26
|
+
maxDepth,
|
|
27
|
+
} from '../utils/index.mjs';
|
|
24
28
|
import { ManagerObservableClass } from './manager-observable-class.mjs';
|
|
25
29
|
import { ObservableBaseClass } from './observable-base-class.mjs';
|
|
26
30
|
import { toNode, type ChildNode } from './observable-node.mjs';
|
|
@@ -198,6 +202,45 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
|
|
205
|
+
/**
|
|
206
|
+
* `attach`, then projects the value `parent` holds when `switchMap` /
|
|
207
|
+
* `mergeMap` is created, as `map` and `filter` start from it; a completed
|
|
208
|
+
* parent is not projected. Nothing observes the operator yet, so a value the
|
|
209
|
+
* inner observable emits while it is subscribed is only stored: the token is
|
|
210
|
+
* one no update is queued with.
|
|
211
|
+
*
|
|
212
|
+
* If the projection throws, the operator is disposed, which disposes what the
|
|
213
|
+
* projection made and takes the operator off its parent, and the exception
|
|
214
|
+
* goes on to the caller of `pipe`.
|
|
215
|
+
*/
|
|
216
|
+
protected attachProjectingHeldValue<V>(
|
|
217
|
+
parent: Observable<V>,
|
|
218
|
+
project: (value: V, updateToken: UpdateToken) => void,
|
|
219
|
+
): void {
|
|
220
|
+
this.attach();
|
|
221
|
+
|
|
222
|
+
const sn = parent.getSnapshot();
|
|
223
|
+
|
|
224
|
+
if (this.isCompleted || Optional.isNone(sn)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A flag and `finally` rather than `catch` and rethrow: Sumi bans
|
|
229
|
+
// `try..catch`, and the exception is never caught on its way to `pipe`.
|
|
230
|
+
let mut_projected = false;
|
|
231
|
+
|
|
232
|
+
// @sumi-expect-error exceptions/no-try
|
|
233
|
+
try {
|
|
234
|
+
project(sn.value, issueUnqueuedToken());
|
|
235
|
+
|
|
236
|
+
mut_projected = true;
|
|
237
|
+
} finally {
|
|
238
|
+
if (!mut_projected) {
|
|
239
|
+
this.dispose();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
201
244
|
/**
|
|
202
245
|
* Subscribes to the inner observable that the projection of `switchMap` /
|
|
203
246
|
* `mergeMap` returned.
|
|
@@ -212,10 +255,13 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
212
255
|
* glitch), and would emit twice.
|
|
213
256
|
*
|
|
214
257
|
* Values the inner observable emits later start updates of their own.
|
|
258
|
+
* `onComplete` is called when it completes, which may be while it is being
|
|
259
|
+
* subscribed, before this returns.
|
|
215
260
|
*/
|
|
216
261
|
protected subscribeInner(
|
|
217
262
|
inner: Observable<A>,
|
|
218
263
|
updateToken: UpdateToken,
|
|
264
|
+
onComplete?: () => void,
|
|
219
265
|
): Subscription {
|
|
220
266
|
let mut_isSubscribing = true;
|
|
221
267
|
|
|
@@ -225,7 +271,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
225
271
|
} else {
|
|
226
272
|
this.startUpdate(value);
|
|
227
273
|
}
|
|
228
|
-
});
|
|
274
|
+
}, onComplete);
|
|
229
275
|
|
|
230
276
|
mut_isSubscribing = false;
|
|
231
277
|
|
|
@@ -353,17 +399,6 @@ const unregisterChild = (child: ChildNode): void => {
|
|
|
353
399
|
}
|
|
354
400
|
};
|
|
355
401
|
|
|
356
|
-
const isDevelopmentProcess = (): boolean => {
|
|
357
|
-
const proc: unknown = Reflect.get(globalThis, 'process');
|
|
358
|
-
|
|
359
|
-
return (
|
|
360
|
-
isRecord(proc) &&
|
|
361
|
-
hasKey(proc, 'env') &&
|
|
362
|
-
isRecord(proc.env) &&
|
|
363
|
-
(!hasKey(proc.env, 'NODE_ENV') || proc.env.NODE_ENV !== 'production')
|
|
364
|
-
);
|
|
365
|
-
};
|
|
366
|
-
|
|
367
402
|
/**
|
|
368
403
|
* Whether {@link registerChild} checks for a circular dependency: on in
|
|
369
404
|
* development, off in production. The public API cannot make a cycle — a
|
|
@@ -372,7 +407,8 @@ const isDevelopmentProcess = (): boolean => {
|
|
|
372
407
|
* node) against code that rewrites an observable's readonly `parents`, and
|
|
373
408
|
* development is where that code runs first. It walks every ancestor of each
|
|
374
409
|
* new observable, a cost production does not pay: the check is off when
|
|
375
|
-
* `NODE_ENV` is `production`,
|
|
376
|
-
*
|
|
410
|
+
* `NODE_ENV` is `production`, including a bundle whose bundler replaced
|
|
411
|
+
* `process.env.NODE_ENV`, and where nothing replaced it and there is no
|
|
412
|
+
* `process` (see {@link isDevelopmentBuild}).
|
|
377
413
|
*/
|
|
378
|
-
|
|
414
|
+
const checksCircularDependency: boolean = isDevelopmentBuild();
|
|
@@ -14,7 +14,6 @@ import { Optional } from 'ts-data-forge';
|
|
|
14
14
|
import { combine } from '../combine/index.mjs';
|
|
15
15
|
import { source } from '../create/index.mjs';
|
|
16
16
|
import { map } from '../operators/index.mjs';
|
|
17
|
-
import { checksCircularDependency } from './child-observable-class.mjs';
|
|
18
17
|
import {
|
|
19
18
|
createSyncChildObservable,
|
|
20
19
|
createTryUpdateNotImplemented,
|
|
@@ -23,53 +22,50 @@ import { createRootObservable } from './create-root-observable.mjs';
|
|
|
23
22
|
|
|
24
23
|
describe('circular dependency comparison', () => {
|
|
25
24
|
describe('SynState', () => {
|
|
26
|
-
//
|
|
27
|
-
test
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
() => ({}),
|
|
33
|
-
);
|
|
25
|
+
// The check is on in both test runs: see `circular-dependency.test.mts`.
|
|
26
|
+
test('detects cycle at construction time with a clear error message', () => {
|
|
27
|
+
const root = createRootObservable(
|
|
28
|
+
{ initialValue: Optional.some(0) },
|
|
29
|
+
() => ({}),
|
|
30
|
+
);
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const mut_childA = createSyncChildObservable(
|
|
33
|
+
{
|
|
34
|
+
parents: [root],
|
|
35
|
+
initialValue: Optional.some(0),
|
|
36
|
+
},
|
|
37
|
+
createTryUpdateNotImplemented,
|
|
38
|
+
);
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
const childB = createSyncChildObservable(
|
|
41
|
+
{
|
|
42
|
+
parents: [mut_childA],
|
|
43
|
+
initialValue: Optional.some(0),
|
|
44
|
+
},
|
|
45
|
+
createTryUpdateNotImplemented,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Simulate a cycle: mut_childA → childB → mut_childA
|
|
49
|
+
// In normal usage this is impossible — the check runs in every child
|
|
50
|
+
// constructor before the reference becomes available.
|
|
51
|
+
Object.defineProperty(mut_childA, 'parents', {
|
|
52
|
+
value: [childB],
|
|
53
|
+
writable: false,
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(() => {
|
|
58
|
+
createSyncChildObservable(
|
|
44
59
|
{
|
|
45
60
|
parents: [mut_childA],
|
|
46
61
|
initialValue: Optional.some(0),
|
|
47
62
|
},
|
|
48
63
|
createTryUpdateNotImplemented,
|
|
49
64
|
);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Object.defineProperty(mut_childA, 'parents', {
|
|
55
|
-
value: [childB],
|
|
56
|
-
writable: false,
|
|
57
|
-
configurable: true,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
expect(() => {
|
|
61
|
-
createSyncChildObservable(
|
|
62
|
-
{
|
|
63
|
-
parents: [mut_childA],
|
|
64
|
-
initialValue: Optional.some(0),
|
|
65
|
-
},
|
|
66
|
-
createTryUpdateNotImplemented,
|
|
67
|
-
);
|
|
68
|
-
}).toThrow(
|
|
69
|
-
'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
|
|
70
|
-
);
|
|
71
|
-
},
|
|
72
|
-
);
|
|
65
|
+
}).toThrow(
|
|
66
|
+
'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
73
69
|
|
|
74
70
|
test('accepts valid DAG (diamond dependency)', () => {
|
|
75
71
|
const a$ = source(0);
|
|
@@ -2,7 +2,6 @@ import { Optional } from 'ts-data-forge';
|
|
|
2
2
|
import { combine, merge } from '../combine/index.mjs';
|
|
3
3
|
import { source } from '../create/index.mjs';
|
|
4
4
|
import { map } from '../operators/index.mjs';
|
|
5
|
-
import { checksCircularDependency } from './child-observable-class.mjs';
|
|
6
5
|
import {
|
|
7
6
|
createAsyncChildObservable,
|
|
8
7
|
createSyncChildObservable,
|
|
@@ -11,9 +10,12 @@ import {
|
|
|
11
10
|
import { createRootObservable } from './create-root-observable.mjs';
|
|
12
11
|
|
|
13
12
|
describe('circular dependency detection', () => {
|
|
14
|
-
// The check runs only
|
|
15
|
-
//
|
|
16
|
-
|
|
13
|
+
// The check runs only where `NODE_ENV` is not `production`, which is so in
|
|
14
|
+
// both test runs: in the browser run because Vite replaced
|
|
15
|
+
// `process.env.NODE_ENV`, so these tests also fail if it stops being
|
|
16
|
+
// replaced. `test/circular-dependency-check-off.test.mts` loads a production
|
|
17
|
+
// copy.
|
|
18
|
+
describe('cycle in ancestor graph', () => {
|
|
17
19
|
test('should throw when parent chain contains a cycle (A -> B -> A)', () => {
|
|
18
20
|
const root = createRootObservable(
|
|
19
21
|
{ initialValue: Optional.some(0) },
|
|
@@ -183,39 +185,6 @@ describe('circular dependency detection', () => {
|
|
|
183
185
|
});
|
|
184
186
|
});
|
|
185
187
|
|
|
186
|
-
describe.skipIf(checksCircularDependency)('with the check off', () => {
|
|
187
|
-
test('does not walk the ancestors of a new child', () => {
|
|
188
|
-
const root = createRootObservable(
|
|
189
|
-
{ initialValue: Optional.some(0) },
|
|
190
|
-
() => ({}),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
const mut_childA = createSyncChildObservable(
|
|
194
|
-
{
|
|
195
|
-
parents: [root],
|
|
196
|
-
initialValue: Optional.some(0),
|
|
197
|
-
},
|
|
198
|
-
createTryUpdateNotImplemented,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
Object.defineProperty(mut_childA, 'parents', {
|
|
202
|
-
value: [mut_childA],
|
|
203
|
-
writable: false,
|
|
204
|
-
configurable: true,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
expect(() => {
|
|
208
|
-
createSyncChildObservable(
|
|
209
|
-
{
|
|
210
|
-
parents: [mut_childA],
|
|
211
|
-
initialValue: Optional.some(0),
|
|
212
|
-
},
|
|
213
|
-
createTryUpdateNotImplemented,
|
|
214
|
-
);
|
|
215
|
-
}).not.toThrow();
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
188
|
describe('valid DAG patterns should not throw', () => {
|
|
220
189
|
test('diamond dependency is not a cycle', () => {
|
|
221
190
|
const root = createRootObservable(
|
|
@@ -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);
|