synstate 6.1.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 +3 -3
- package/dist/core/authoring.d.mts +3 -3
- package/dist/core/class/child-observable-class.d.mts +40 -27
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +78 -46
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/create-child-observable.d.mts +28 -30
- 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 +12 -12
- 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/manager-observable-class.d.mts +6 -6
- package/dist/core/class/manager-observable-class.mjs.map +1 -1
- package/dist/core/class/observable-base-class.d.mts +29 -31
- package/dist/core/class/observable-base-class.d.mts.map +1 -1
- package/dist/core/class/observable-base-class.mjs +0 -2
- package/dist/core/class/observable-base-class.mjs.map +1 -1
- package/dist/core/class/observable-node.d.mts +3 -4
- package/dist/core/class/observable-node.d.mts.map +1 -1
- package/dist/core/class/observable-node.mjs +0 -1
- package/dist/core/class/observable-node.mjs.map +1 -1
- package/dist/core/class/propagation-state.d.mts +6 -8
- package/dist/core/class/propagation-state.d.mts.map +1 -1
- package/dist/core/class/propagation-state.mjs +1 -3
- package/dist/core/class/propagation-state.mjs.map +1 -1
- package/dist/core/class/root-observable-class.d.mts +4 -4
- package/dist/core/combine/derive.d.mts +7 -8
- package/dist/core/combine/derive.d.mts.map +1 -1
- package/dist/core/combine/derive.mjs +2 -2
- package/dist/core/combine/derive.mjs.map +1 -1
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +3 -3
- 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.d.mts +3 -3
- 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/from-subscribable.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/create/timer.mjs.map +1 -1
- package/dist/core/operators/audit.mjs.map +1 -1
- package/dist/core/operators/debounce.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/keep-equal-parts.d.mts +14 -15
- package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
- package/dist/core/operators/keep-equal-parts.mjs +0 -1
- package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
- package/dist/core/operators/map.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +7 -3
- 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/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.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-until.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/take.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/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.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/map-to.d.mts +3 -4
- package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-to.mjs +0 -1
- package/dist/core/predefined/operators/map-to.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/scope/scope.d.mts +4 -4
- package/dist/core/types/observable-family.d.mts.map +1 -1
- package/dist/core/types/observable-family.mjs.map +1 -1
- package/dist/core/types/observable.d.mts +5 -5
- package/dist/core/types/observable.mjs.map +1 -1
- package/dist/core/utils/current-scope.d.mts +6 -6
- package/dist/core/utils/id-maker.d.mts +2 -2
- 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/observable-brand.d.mts +5 -5
- 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/dist/utils/create-boolean-state.d.mts +2 -2
- package/dist/utils/create-reducer.d.mts +18 -18
- package/dist/utils/create-state.d.mts +2 -2
- package/dist/utils/create-state.mjs.map +1 -1
- package/dist/utils/state-source.d.mts +3 -3
- package/package.json +12 -11
- package/src/core/authoring.mts +3 -3
- package/src/core/class/child-observable-class.mts +131 -90
- package/src/core/class/circular-dependency-comparison.test.mts +2 -2
- package/src/core/class/create-child-observable.mts +35 -34
- package/src/core/class/create-root-observable.mts +13 -12
- package/src/core/class/manager-observable-class.mts +12 -12
- package/src/core/class/observable-base-class.mts +33 -35
- package/src/core/class/observable-node.mts +3 -4
- package/src/core/class/propagation-state.mts +27 -29
- package/src/core/class/remove-completed.test.mts +2 -2
- package/src/core/class/root-observable-class.mts +9 -9
- package/src/core/class/unobserved-write.test.mts +4 -4
- package/src/core/class/write-during-propagation.test.mts +75 -4
- package/src/core/combine/derive.mts +12 -12
- package/src/core/combine/merge.mts +2 -2
- package/src/core/combine/zip.mts +12 -9
- package/src/core/create/counter.mts +7 -7
- package/src/core/create/from-abortable-promise.mts +35 -25
- package/src/core/create/from-promise.mts +30 -20
- package/src/core/create/from-promise.test.mts +97 -0
- package/src/core/create/from-subscribable.mts +2 -2
- package/src/core/create/source.mts +7 -6
- package/src/core/create/timer.mts +2 -2
- package/src/core/operators/audit.mts +2 -2
- package/src/core/operators/debounce.mts +2 -2
- package/src/core/operators/filter.mts +6 -7
- 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/keep-equal-parts.mts +16 -17
- package/src/core/operators/map.mts +2 -2
- package/src/core/operators/merge-map.mts +59 -29
- package/src/core/operators/pairwise.mts +4 -4
- package/src/core/operators/scan.mts +2 -2
- package/src/core/operators/skip-if-no-change.mts +4 -4
- package/src/core/operators/skip-until.mts +4 -4
- package/src/core/operators/skip-while.mts +14 -10
- package/src/core/operators/skip-while.test.mts +59 -0
- package/src/core/operators/switch-map.mts +26 -15
- package/src/core/operators/take-until.mts +2 -2
- package/src/core/operators/take-while.mts +17 -19
- package/src/core/operators/take-while.test.mts +51 -0
- package/src/core/operators/take.mts +2 -2
- package/src/core/operators/throttle.mts +6 -5
- package/src/core/operators/with-buffered-from.mts +18 -5
- package/src/core/operators/with-buffered-from.test.mts +27 -0
- package/src/core/operators/with-current-value-from.mts +2 -2
- package/src/core/operators/with-initial-value.mts +2 -2
- package/src/core/predefined/operators/attach-index.mts +4 -5
- package/src/core/predefined/operators/map-to.mts +3 -4
- package/src/core/predefined/operators/skip.mts +2 -2
- package/src/core/scope/scope.mts +6 -6
- package/src/core/types/observable-family.mts +4 -5
- package/src/core/types/observable.mts +7 -7
- package/src/core/utils/current-scope.mts +8 -8
- package/src/core/utils/id-maker.mts +2 -2
- package/src/core/utils/index.mts +1 -0
- package/src/core/utils/observable-brand.mts +5 -5
- package/src/core/utils/rethrow-out-of-promise.mts +16 -0
- package/src/utils/create-boolean-state.mts +2 -2
- package/src/utils/create-reducer.mts +18 -18
- package/src/utils/create-state.mts +4 -4
- package/src/utils/create-state.test.mts +2 -2
- package/src/utils/state-source.mts +3 -3
|
@@ -87,18 +87,18 @@ export declare const createReducer: <S, A>(reducer: Reducer<S, A>, initialState:
|
|
|
87
87
|
*/
|
|
88
88
|
export type StateOptions<S> = Readonly<{
|
|
89
89
|
/**
|
|
90
|
-
* Whether the next state is the same as the current one. When it says so,
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* identity: `setState` and the like return the current state rather than
|
|
94
|
-
*
|
|
90
|
+
* Whether the next state is the same as the current one. When it says so, the
|
|
91
|
+
* update is not passed on — no subscriber, and nothing derived from the
|
|
92
|
+
* state, is told of it — and the current state is kept, along with its
|
|
93
|
+
* identity: `setState` and the like return the current state rather than the
|
|
94
|
+
* next.
|
|
95
95
|
*
|
|
96
96
|
* Decide here, where the state is held, rather than downstream with
|
|
97
|
-
* `skipIfNoChange`: an update passed on here reaches whatever reads the
|
|
98
|
-
*
|
|
97
|
+
* `skipIfNoChange`: an update passed on here reaches whatever reads the state
|
|
98
|
+
* directly before any operator can thin it out.
|
|
99
99
|
*
|
|
100
|
-
* Defaults to `Object.is`, so writing the value already held passes
|
|
101
|
-
*
|
|
100
|
+
* Defaults to `Object.is`, so writing the value already held passes nothing
|
|
101
|
+
* on. Pass `() => false` to pass on every update, equal or not.
|
|
102
102
|
*
|
|
103
103
|
* @param current - The state held now
|
|
104
104
|
* @param next - The state the update would set
|
|
@@ -107,8 +107,8 @@ export type StateOptions<S> = Readonly<{
|
|
|
107
107
|
/**
|
|
108
108
|
* Whether an update keeps the parts of the state it did not change. With
|
|
109
109
|
* `true`, the next state is compared with the current one before it is
|
|
110
|
-
* written, and every part of it deeply equal to the same part of the
|
|
111
|
-
*
|
|
110
|
+
* written, and every part of it deeply equal to the same part of the current
|
|
111
|
+
* state is replaced by that part (`replaceEqualDeep` of
|
|
112
112
|
* ts-data-forge): a reader of an unchanged part keeps the object it has, so
|
|
113
113
|
* `map`, comparing with `Object.is`, stops there. When the whole next state is
|
|
114
114
|
* deeply equal, the current state is kept and nothing is passed on, so
|
|
@@ -121,14 +121,14 @@ export type StateOptions<S> = Readonly<{
|
|
|
121
121
|
* arrays are walked into; a `Map`, a `Set`, a `Date` or a class instance is
|
|
122
122
|
* compared and kept whole.
|
|
123
123
|
*
|
|
124
|
-
* Prefer this over `keepEqualParts()` downstream: keeping the parts where
|
|
125
|
-
*
|
|
126
|
-
*
|
|
124
|
+
* Prefer this over `keepEqualParts()` downstream: keeping the parts where the
|
|
125
|
+
* state is written means no update with nothing in it is passed on at all,
|
|
126
|
+
* and nothing downstream has anything to undo.
|
|
127
127
|
*
|
|
128
|
-
* Defaults to `false`: the next state is written as given. With it on,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
128
|
+
* Defaults to `false`: the next state is written as given. With it on, each
|
|
129
|
+
* update compares deeply every part of the next state that is not the very
|
|
130
|
+
* object of the current one; a state whose updates already keep what they do
|
|
131
|
+
* not change, as `{ ...prev, x }` does, gains nothing from it.
|
|
132
132
|
*/
|
|
133
133
|
keepEqualParts?: boolean | ReplaceEqualDeepOptions;
|
|
134
134
|
}>;
|
|
@@ -105,8 +105,8 @@ import type { StateOptions } from './create-reducer.mjs';
|
|
|
105
105
|
*
|
|
106
106
|
* assert.strictEqual(report.getSnapshot().value, held);
|
|
107
107
|
*
|
|
108
|
-
* // An edited title: a new report and a new array, while the summary is
|
|
109
|
-
* //
|
|
108
|
+
* // An edited title: a new report and a new array, while the summary is the
|
|
109
|
+
* // one held, so `summary` is not told of it.
|
|
110
110
|
* setReport({
|
|
111
111
|
* summary: { open: 1 },
|
|
112
112
|
* pulls: [{ number: 1, title: 'one (edited)' }],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,iBAAiB,CACtD,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,
|
|
1
|
+
{"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,iBAAiB,CACtD,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,2EAA2E;IAC3E,iCAAiC;IACjC,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE;QAClD,MAAM,YAAY,GAAG,WAAW,EAAE,CAAC;QAEnC,OAAO,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,GAAM,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,CAAC;IAEhE,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type InitializedSourceObservable } from '../core/index.mjs';
|
|
2
2
|
import type { StateOptions } from './create-reducer.mjs';
|
|
3
3
|
/**
|
|
4
|
-
* What `createState` and `createReducer` are both built on: the source
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* What `createState` and `createReducer` are both built on: the source holding
|
|
5
|
+
* the state, its current value, and `commit`, the one way either writes it.
|
|
6
|
+
* `commit` writes `reduced` unless `equals` finds it the same as
|
|
7
7
|
* `currentState`, after keeping the parts of `currentState` it did not change
|
|
8
8
|
* when `keepEqualParts` asks for that, and returns the state written, or
|
|
9
9
|
* `currentState` when nothing was.
|
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
|
},
|
package/src/core/authoring.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What custom creators and operators are written with:
|
|
3
|
-
* built-in
|
|
4
|
-
* internal.
|
|
2
|
+
* What custom creators and operators are written with: factories that wrap the
|
|
3
|
+
* base classes the built-in observables extend (#2117). The rest of `core/base`
|
|
4
|
+
* stays internal.
|
|
5
5
|
*/
|
|
6
6
|
export {
|
|
7
7
|
createAsyncChildObservable,
|
|
@@ -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';
|
|
@@ -30,11 +30,11 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
|
|
|
30
30
|
parents: Wrap<P>;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Defaults to one more than the deepest parent. An update reaches
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Defaults to one more than the deepest parent. An update reaches observables
|
|
34
|
+
* in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
|
|
35
|
+
* without being its child (`withCurrentValueFrom`'s second source) has to be
|
|
36
|
+
* shallower than this one too, or this one reads its value from before the
|
|
37
|
+
* update: pass `1 + Math.max(...)` over both then.
|
|
38
38
|
*/
|
|
39
39
|
depth?: number;
|
|
40
40
|
|
|
@@ -45,9 +45,9 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
|
|
|
45
45
|
* An observable derived from `parents` that emits, if at all, in the same
|
|
46
46
|
* update as they do (`map`, `filter`, `combine`, …).
|
|
47
47
|
*
|
|
48
|
-
* A subclass overrides `tryUpdate`, and, when it holds something to
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* A subclass overrides `tryUpdate`, and, when it holds something to release,
|
|
49
|
+
* `teardown`; its constructor ends with `this.attach()`, once everything
|
|
50
|
+
* `tryUpdate` and `teardown` read is in place.
|
|
51
51
|
*/
|
|
52
52
|
export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
53
53
|
extends ObservableBaseClass<A, 'sync child', number>
|
|
@@ -107,10 +107,9 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Whether this observable can emit nothing more although some parent is
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* empty).
|
|
110
|
+
* Whether this observable can emit nothing more although some parent is still
|
|
111
|
+
* live, asked whenever a parent completes. `true` completes it then, as all
|
|
112
|
+
* parents completing does (`zip` once a completed source's queue is empty).
|
|
114
113
|
*/
|
|
115
114
|
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
116
115
|
canEmitNoMore(): boolean {
|
|
@@ -118,8 +117,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
|
-
* Completes this observable once the update now propagating has reached
|
|
122
|
-
*
|
|
120
|
+
* Completes this observable once the update now propagating has reached every
|
|
121
|
+
* descendant, so that they see the value just passed on first.
|
|
123
122
|
*/
|
|
124
123
|
protected completeAfterPropagation(): void {
|
|
125
124
|
runAfterPropagation(() => {
|
|
@@ -129,8 +128,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
/**
|
|
132
|
-
* An observable derived from `parents` that may emit later, in an update of
|
|
133
|
-
*
|
|
131
|
+
* An observable derived from `parents` that may emit later, in an update of its
|
|
132
|
+
* own, through `startUpdate` (`debounce`, `switchMap`). It may also emit
|
|
134
133
|
* synchronously with `setNext`, as a sync child does. A subclass may also
|
|
135
134
|
* override `flush`, to pass on at completion what it still holds.
|
|
136
135
|
*/
|
|
@@ -199,24 +198,66 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
199
198
|
});
|
|
200
199
|
}
|
|
201
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
|
+
|
|
202
240
|
/**
|
|
203
241
|
* Subscribes to the inner observable that the projection of `switchMap` /
|
|
204
242
|
* `mergeMap` returned.
|
|
205
243
|
*
|
|
206
244
|
* If the inner observable already holds a value (`just(x)`, a state), it
|
|
207
|
-
* emits that value synchronously while being subscribed. That first value
|
|
208
|
-
*
|
|
209
|
-
* projection, so it joins the propagation in progress like a sync
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
245
|
+
* emits that value synchronously while being subscribed. That first value is
|
|
246
|
+
* emitted with `updateToken`, the token of the update that ran the
|
|
247
|
+
* projection, so it joins the propagation in progress like a sync operator's
|
|
248
|
+
* value would. Starting an update of its own instead would reach the nodes
|
|
249
|
+
* below before the current update reached this observable's siblings: a node
|
|
250
|
+
* reading both would see the new inner value beside stale sibling values (a
|
|
251
|
+
* glitch), and would emit twice.
|
|
214
252
|
*
|
|
215
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.
|
|
216
256
|
*/
|
|
217
257
|
protected subscribeInner(
|
|
218
258
|
inner: Observable<A>,
|
|
219
259
|
updateToken: UpdateToken,
|
|
260
|
+
onComplete?: () => void,
|
|
220
261
|
): Subscription {
|
|
221
262
|
let mut_isSubscribing = true;
|
|
222
263
|
|
|
@@ -226,7 +267,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
226
267
|
} else {
|
|
227
268
|
this.startUpdate(value);
|
|
228
269
|
}
|
|
229
|
-
});
|
|
270
|
+
}, onComplete);
|
|
230
271
|
|
|
231
272
|
mut_isSubscribing = false;
|
|
232
273
|
|
|
@@ -236,8 +277,8 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
236
277
|
|
|
237
278
|
/**
|
|
238
279
|
* A sync child that always holds a value: a subclass passes a `Some` as its
|
|
239
|
-
* `initialValue`. It differs from its base only in the types of
|
|
240
|
-
*
|
|
280
|
+
* `initialValue`. It differs from its base only in the types of `getSnapshot`
|
|
281
|
+
* and `pipe`.
|
|
241
282
|
*/
|
|
242
283
|
export class InitializedSyncChildObservableClass<
|
|
243
284
|
A,
|
|
@@ -266,8 +307,8 @@ export class InitializedSyncChildObservableClass<
|
|
|
266
307
|
/* helpers */
|
|
267
308
|
|
|
268
309
|
/**
|
|
269
|
-
* Called by a parent that has completed: whether no live parent is left, or
|
|
270
|
-
*
|
|
310
|
+
* Called by a parent that has completed: whether no live parent is left, or no
|
|
311
|
+
* value can come from the live ones.
|
|
271
312
|
*/
|
|
272
313
|
const shouldComplete = (
|
|
273
314
|
child: ChildNode & Readonly<{ canEmitNoMore: () => boolean }>,
|
|
@@ -275,6 +316,63 @@ const shouldComplete = (
|
|
|
275
316
|
!child.isCompleted &&
|
|
276
317
|
(child.parents.every((p) => p.isCompleted) || child.canEmitNoMore());
|
|
277
318
|
|
|
319
|
+
const hasCircularDependencyFrom = (
|
|
320
|
+
node: Observable<unknown>,
|
|
321
|
+
mut_visited: MutableSet<Observable<unknown>>,
|
|
322
|
+
mut_inPath: MutableSet<Observable<unknown>>,
|
|
323
|
+
): boolean => {
|
|
324
|
+
if (mut_inPath.has(node)) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (mut_visited.has(node)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
mut_visited.add(node);
|
|
333
|
+
|
|
334
|
+
mut_inPath.add(node);
|
|
335
|
+
|
|
336
|
+
if (node.kind !== 'root') {
|
|
337
|
+
for (const parent of node.parents) {
|
|
338
|
+
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
mut_inPath.delete(node);
|
|
345
|
+
|
|
346
|
+
return false;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Detects circular dependencies by walking the full ancestor chain of the
|
|
351
|
+
* child's parents and checking whether the child already appears among them.
|
|
352
|
+
*
|
|
353
|
+
* @throws {Error} if a circular dependency is detected
|
|
354
|
+
*/
|
|
355
|
+
const detectCircularDependency = (child: ChildNode): void => {
|
|
356
|
+
const mut_visited = new Set<Observable<unknown>>();
|
|
357
|
+
|
|
358
|
+
const mut_inPath = new Set<Observable<unknown>>([
|
|
359
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
360
|
+
child as Observable<unknown>,
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
for (const parent of child.parents) {
|
|
364
|
+
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
365
|
+
// A programming error (invariant violation), not a recoverable failure.
|
|
366
|
+
// `panic` marks the error it throws, so the `Result` / `AsyncResult`
|
|
367
|
+
// boundaries rethrow it rather than turning an invariant violation into
|
|
368
|
+
// an `Err` a caller might handle (Sumi D-48 / D-53).
|
|
369
|
+
panic(
|
|
370
|
+
'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
278
376
|
/** Registers a child to its parents. */
|
|
279
377
|
const registerChild = (child: ChildNode): void => {
|
|
280
378
|
if (checksCircularDependency) {
|
|
@@ -287,9 +385,9 @@ const registerChild = (child: ChildNode): void => {
|
|
|
287
385
|
};
|
|
288
386
|
|
|
289
387
|
/**
|
|
290
|
-
* Undoes {@link registerChild} for a completed child: its parents forget it,
|
|
291
|
-
*
|
|
292
|
-
*
|
|
388
|
+
* Undoes {@link registerChild} for a completed child: its parents forget it, so
|
|
389
|
+
* that no update puts it in its queue again. Without this a completed node
|
|
390
|
+
* stayed reachable for as long as its root lived (#2134).
|
|
293
391
|
*/
|
|
294
392
|
const unregisterChild = (child: ChildNode): void => {
|
|
295
393
|
for (const p of child.parents) {
|
|
@@ -320,60 +418,3 @@ const isDevelopmentProcess = (): boolean => {
|
|
|
320
418
|
* bundle).
|
|
321
419
|
*/
|
|
322
420
|
export const checksCircularDependency: boolean = isDevelopmentProcess();
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Detects circular dependencies by walking the full ancestor chain of the
|
|
326
|
-
* child's parents and checking whether the child already appears among them.
|
|
327
|
-
*
|
|
328
|
-
* @throws {Error} if a circular dependency is detected
|
|
329
|
-
*/
|
|
330
|
-
const detectCircularDependency = (child: ChildNode): void => {
|
|
331
|
-
const mut_visited = new Set<Observable<unknown>>();
|
|
332
|
-
|
|
333
|
-
const mut_inPath = new Set<Observable<unknown>>([
|
|
334
|
-
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
335
|
-
child as Observable<unknown>,
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
for (const parent of child.parents) {
|
|
339
|
-
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
340
|
-
// A programming error (invariant violation), not a recoverable
|
|
341
|
-
// failure. `panic` marks the error it throws, so the `Result` /
|
|
342
|
-
// `AsyncResult` boundaries rethrow it rather than turning an invariant
|
|
343
|
-
// violation into an `Err` a caller might handle (Sumi D-48 / D-53).
|
|
344
|
-
panic(
|
|
345
|
-
'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const hasCircularDependencyFrom = (
|
|
352
|
-
node: Observable<unknown>,
|
|
353
|
-
mut_visited: MutableSet<Observable<unknown>>,
|
|
354
|
-
mut_inPath: MutableSet<Observable<unknown>>,
|
|
355
|
-
): boolean => {
|
|
356
|
-
if (mut_inPath.has(node)) {
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (mut_visited.has(node)) {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
mut_visited.add(node);
|
|
365
|
-
|
|
366
|
-
mut_inPath.add(node);
|
|
367
|
-
|
|
368
|
-
if (node.kind !== 'root') {
|
|
369
|
-
for (const parent of node.parents) {
|
|
370
|
-
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
mut_inPath.delete(node);
|
|
377
|
-
|
|
378
|
-
return false;
|
|
379
|
-
};
|
|
@@ -49,8 +49,8 @@ describe('circular dependency comparison', () => {
|
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
// Simulate a cycle: mut_childA → childB → mut_childA
|
|
52
|
-
// In normal usage this is impossible — the check runs in every
|
|
53
|
-
//
|
|
52
|
+
// In normal usage this is impossible — the check runs in every child
|
|
53
|
+
// constructor before the reference becomes available.
|
|
54
54
|
Object.defineProperty(mut_childA, 'parents', {
|
|
55
55
|
value: [childB],
|
|
56
56
|
writable: false,
|
|
@@ -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,
|
|
@@ -31,17 +32,16 @@ export type SyncChildObservableTools<A> = Readonly<{
|
|
|
31
32
|
complete: () => void;
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
* Completes this observable once the update now propagating has reached
|
|
35
|
-
*
|
|
35
|
+
* Completes this observable once the update now propagating has reached every
|
|
36
|
+
* descendant, so that they see the value just passed on first.
|
|
36
37
|
*/
|
|
37
38
|
completeAfterPropagation: () => void;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Registers a teardown (clear timers, unsubscribe inner observables, …) to
|
|
41
|
-
* run when this observable completes or is disposed, in the order
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* teardown runs at once.
|
|
42
|
+
* run when this observable completes or is disposed, in the order registered.
|
|
43
|
+
* Called from `init`, it lets the teardown close over what `init` creates.
|
|
44
|
+
* Registered once the observable has completed, the teardown runs at once.
|
|
45
45
|
*/
|
|
46
46
|
onComplete: (teardown: () => void) => void;
|
|
47
47
|
}>;
|
|
@@ -57,21 +57,20 @@ export type AsyncChildObservableTools<A> = Readonly<{
|
|
|
57
57
|
SyncChildObservableTools<A>;
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* What a sync child's `init` may return instead of `tryUpdate` alone: the
|
|
61
|
-
*
|
|
60
|
+
* What a sync child's `init` may return instead of `tryUpdate` alone: the hooks
|
|
61
|
+
* the factory calls, which read the state `init` creates.
|
|
62
62
|
*/
|
|
63
63
|
export type SyncChildObservableHooks = Readonly<{
|
|
64
64
|
/**
|
|
65
|
-
* Called with the token of every update in which a parent of this
|
|
66
|
-
*
|
|
65
|
+
* Called with the token of every update in which a parent of this observable
|
|
66
|
+
* emitted.
|
|
67
67
|
*/
|
|
68
68
|
tryUpdate: (updateToken: UpdateToken) => void;
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Whether this observable can emit nothing more although some parent is
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* empty).
|
|
71
|
+
* Whether this observable can emit nothing more although some parent is still
|
|
72
|
+
* live, asked whenever a parent completes. `true` completes it then, as all
|
|
73
|
+
* parents completing does (`zip` once a completed source's queue is empty).
|
|
75
74
|
*/
|
|
76
75
|
canEmitNoMore?: () => boolean;
|
|
77
76
|
}>;
|
|
@@ -84,8 +83,8 @@ export type SyncChildObservableHooks = Readonly<{
|
|
|
84
83
|
export type AsyncChildObservableHooks = SyncChildObservableHooks &
|
|
85
84
|
Readonly<{
|
|
86
85
|
/**
|
|
87
|
-
* Run at the beginning of `complete()`, before the teardowns, and never
|
|
88
|
-
*
|
|
86
|
+
* Run at the beginning of `complete()`, before the teardowns, and never by
|
|
87
|
+
* `dispose()`: passes on what this observable still holds through
|
|
89
88
|
* `startUpdate`, so that its subscribers see it before they see the
|
|
90
89
|
* completion. A disposed observable has no one left to tell.
|
|
91
90
|
*/
|
|
@@ -96,11 +95,11 @@ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
|
|
|
96
95
|
parents: Wrap<P>;
|
|
97
96
|
|
|
98
97
|
/**
|
|
99
|
-
* Defaults to one more than the deepest parent. An update reaches
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
98
|
+
* Defaults to one more than the deepest parent. An update reaches observables
|
|
99
|
+
* in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
|
|
100
|
+
* without being its child (`withCurrentValueFrom`'s second source) has to be
|
|
101
|
+
* shallower than this one too, or this one reads its value from before the
|
|
102
|
+
* update: pass `1 + Math.max(...)` over both then.
|
|
104
103
|
*/
|
|
105
104
|
depth?: number;
|
|
106
105
|
}>;
|
|
@@ -122,15 +121,15 @@ export type InitializedChildObservableConfig<
|
|
|
122
121
|
}>;
|
|
123
122
|
|
|
124
123
|
/**
|
|
125
|
-
* Creates a sync-child observable: one derived from `parents` that emits, if
|
|
126
|
-
*
|
|
127
|
-
*
|
|
124
|
+
* Creates a sync-child observable: one derived from `parents` that emits, if at
|
|
125
|
+
* all, in the same update as they do. `map`, `filter`, `combine` and the other
|
|
126
|
+
* synchronous operators are built with it.
|
|
128
127
|
*
|
|
129
128
|
* `init` runs during construction and receives the tools. It returns
|
|
130
129
|
* `tryUpdate`, which is called with the token of every update in which some
|
|
131
|
-
* parent of this observable emitted — a parent that did not emit in that
|
|
132
|
-
*
|
|
133
|
-
*
|
|
130
|
+
* parent of this observable emitted — a parent that did not emit in that update
|
|
131
|
+
* has a different `updateToken`; to emit, call `setNext` with the token
|
|
132
|
+
* received — or an object holding `tryUpdate` and the other hooks
|
|
134
133
|
* ({@link SyncChildObservableHooks}). State the hooks share can live inside
|
|
135
134
|
* `init`, next to the teardown it registers with the `onComplete` tool.
|
|
136
135
|
* `init` may complete the observable; it is then never registered to its
|
|
@@ -211,8 +210,8 @@ export const createInitializedSyncChildObservable = <
|
|
|
211
210
|
* Creates an async-child observable: one derived from `parents` that may emit
|
|
212
211
|
* later, in an update of its own, through `startUpdate` (`debounce`,
|
|
213
212
|
* `switchMap`). It may also emit synchronously with `setNext`, as a sync child
|
|
214
|
-
* does. See {@link createSyncChildObservable} for the `init` contract; an
|
|
215
|
-
*
|
|
213
|
+
* does. See {@link createSyncChildObservable} for the `init` contract; an async
|
|
214
|
+
* child's `init` may also return a `flush` hook
|
|
216
215
|
* ({@link AsyncChildObservableHooks}).
|
|
217
216
|
*/
|
|
218
217
|
export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
@@ -302,7 +301,9 @@ class CustomAsyncChildObservableClass<
|
|
|
302
301
|
const P extends NonEmptyUnknownList,
|
|
303
302
|
> extends AsyncChildObservableClass<A, P> {
|
|
304
303
|
#mut_teardowns: readonly (() => void)[] | undefined;
|
|
305
|
-
|
|
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;
|
|
306
307
|
|
|
307
308
|
constructor(
|
|
308
309
|
config: ChildObservableConfig<A, P>,
|
|
@@ -346,15 +347,15 @@ class CustomAsyncChildObservableClass<
|
|
|
346
347
|
}
|
|
347
348
|
|
|
348
349
|
override tryUpdate(updateToken: UpdateToken): void {
|
|
349
|
-
this.#hooks
|
|
350
|
+
this.#hooks?.tryUpdate(updateToken);
|
|
350
351
|
}
|
|
351
352
|
|
|
352
353
|
override canEmitNoMore(): boolean {
|
|
353
|
-
return this.#hooks
|
|
354
|
+
return this.#hooks?.canEmitNoMore?.() === true;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
protected override flush(): void {
|
|
357
|
-
this.#hooks
|
|
358
|
+
this.#hooks?.flush?.();
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
protected override teardown(): void {
|