synstate 3.0.4 → 3.0.6
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/base/create-child-observable.d.mts.map +1 -1
- package/dist/core/base/create-child-observable.mjs +7 -3
- package/dist/core/base/create-child-observable.mjs.map +1 -1
- package/dist/core/base/create-observable-base.d.mts +53 -7
- package/dist/core/base/create-observable-base.d.mts.map +1 -1
- package/dist/core/base/create-observable-base.mjs +53 -19
- package/dist/core/base/create-observable-base.mjs.map +1 -1
- package/dist/core/base/create-root-observable.d.mts.map +1 -1
- package/dist/core/base/create-root-observable.mjs.map +1 -1
- package/dist/core/combine/combine.d.mts.map +1 -1
- package/dist/core/combine/combine.mjs +7 -4
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts.map +1 -1
- package/dist/core/combine/merge.mjs +4 -1
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +8 -5
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs +6 -2
- 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 +5 -2
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts.map +1 -1
- package/dist/core/create/from-subscribable.mjs +1 -0
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/just.d.mts.map +1 -1
- package/dist/core/create/just.mjs +1 -0
- package/dist/core/create/just.mjs.map +1 -1
- package/dist/core/create/source.mjs +2 -1
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/operators/audit.d.mts.map +1 -1
- package/dist/core/operators/audit.mjs +3 -1
- package/dist/core/operators/audit.mjs.map +1 -1
- package/dist/core/operators/debounce.d.mts.map +1 -1
- package/dist/core/operators/debounce.mjs +3 -1
- package/dist/core/operators/debounce.mjs.map +1 -1
- package/dist/core/operators/map.d.mts.map +1 -1
- package/dist/core/operators/map.mjs +1 -0
- package/dist/core/operators/map.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +1 -0
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/take-until.d.mts.map +1 -1
- package/dist/core/operators/take-until.mjs +1 -0
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/throttle.d.mts.map +1 -1
- package/dist/core/operators/throttle.mjs +1 -0
- 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 +1 -0
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs +2 -1
- package/dist/core/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -1
- package/dist/core/predefined/operators/unwrap-optional.mjs +1 -0
- package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -1
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -1
- package/dist/core/predefined/operators/unwrap-result-ok.mjs +1 -0
- package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -1
- package/dist/core/types/observable.d.mts +1 -1
- package/dist/core/types/observable.d.mts.map +1 -1
- package/dist/core/types/observable.mjs +5 -0
- package/dist/core/types/observable.mjs.map +1 -1
- package/dist/core/utils/id-maker.d.mts.map +1 -1
- package/dist/core/utils/id-maker.mjs +1 -0
- package/dist/core/utils/id-maker.mjs.map +1 -1
- package/dist/core/utils/utils.d.mts.map +1 -1
- package/dist/core/utils/utils.mjs +5 -2
- package/dist/core/utils/utils.mjs.map +1 -1
- package/package.json +36 -38
- package/src/core/base/circular-dependency-comparison.test.mts +5 -6
- package/src/core/base/circular-dependency.test.mts +20 -21
- package/src/core/base/create-child-observable.mts +10 -3
- package/src/core/base/create-observable-base.mts +129 -30
- package/src/core/base/create-root-observable.mts +2 -0
- package/src/core/combine/combine.mts +13 -8
- package/src/core/combine/merge.mts +5 -1
- package/src/core/combine/zip.mts +12 -8
- package/src/core/create/from-abortable-promise.mts +8 -2
- package/src/core/create/from-promise.mts +7 -2
- package/src/core/create/from-subscribable.mts +1 -0
- package/src/core/create/just.mts +1 -0
- package/src/core/create/source.mts +3 -1
- package/src/core/operators/audit.mts +4 -1
- package/src/core/operators/debounce.mts +4 -1
- package/src/core/operators/map.mts +1 -0
- package/src/core/operators/skip-if-no-change.mts +1 -0
- package/src/core/operators/take-until.mts +1 -0
- package/src/core/operators/throttle.mts +1 -0
- package/src/core/operators/with-buffered-from.mts +1 -0
- package/src/core/operators/with-current-value-from.mts +3 -1
- package/src/core/predefined/operators/unwrap-optional.mts +1 -0
- package/src/core/predefined/operators/unwrap-result-ok.mts +1 -0
- package/src/core/types/observable.mts +11 -1
- package/src/core/utils/id-maker.mts +1 -0
- package/src/core/utils/utils.mts +7 -2
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* - Jotai: circular atom definitions are silently accepted. A cycle manifests
|
|
8
8
|
* only at read time as a `Maximum call stack size exceeded` error.
|
|
9
9
|
*/
|
|
10
|
-
/* eslint-disable functional/immutable-data */
|
|
11
10
|
import { type Atom, atom, createStore } from 'jotai';
|
|
12
11
|
import { BehaviorSubject, combineLatest, map as rxMap } from 'rxjs';
|
|
13
12
|
import { Optional } from 'ts-data-forge';
|
|
@@ -26,7 +25,7 @@ describe('circular dependency comparison', () => {
|
|
|
26
25
|
() => ({}),
|
|
27
26
|
);
|
|
28
27
|
|
|
29
|
-
const
|
|
28
|
+
const mut_childA = createSyncChildObservable(
|
|
30
29
|
{
|
|
31
30
|
parents: [root],
|
|
32
31
|
initialValue: Optional.some(0),
|
|
@@ -36,16 +35,16 @@ describe('circular dependency comparison', () => {
|
|
|
36
35
|
|
|
37
36
|
const childB = createSyncChildObservable(
|
|
38
37
|
{
|
|
39
|
-
parents: [
|
|
38
|
+
parents: [mut_childA],
|
|
40
39
|
initialValue: Optional.some(0),
|
|
41
40
|
},
|
|
42
41
|
createTryUpdateNotImplemented,
|
|
43
42
|
);
|
|
44
43
|
|
|
45
|
-
// Simulate a cycle:
|
|
44
|
+
// Simulate a cycle: mut_childA → childB → mut_childA
|
|
46
45
|
// In normal usage this is impossible — the check runs in every
|
|
47
46
|
// child constructor before the reference becomes available.
|
|
48
|
-
Object.defineProperty(
|
|
47
|
+
Object.defineProperty(mut_childA, 'parents', {
|
|
49
48
|
value: [childB],
|
|
50
49
|
writable: false,
|
|
51
50
|
configurable: true,
|
|
@@ -54,7 +53,7 @@ describe('circular dependency comparison', () => {
|
|
|
54
53
|
expect(() => {
|
|
55
54
|
createSyncChildObservable(
|
|
56
55
|
{
|
|
57
|
-
parents: [
|
|
56
|
+
parents: [mut_childA],
|
|
58
57
|
initialValue: Optional.some(0),
|
|
59
58
|
},
|
|
60
59
|
createTryUpdateNotImplemented,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable functional/immutable-data */
|
|
2
1
|
import { Optional } from 'ts-data-forge';
|
|
3
2
|
import { combine, merge } from '../combine/index.mjs';
|
|
4
3
|
import { source } from '../create/index.mjs';
|
|
@@ -18,7 +17,7 @@ describe('circular dependency detection', () => {
|
|
|
18
17
|
() => ({}),
|
|
19
18
|
);
|
|
20
19
|
|
|
21
|
-
const
|
|
20
|
+
const mut_childA = createSyncChildObservable(
|
|
22
21
|
{
|
|
23
22
|
parents: [root],
|
|
24
23
|
initialValue: Optional.some(0),
|
|
@@ -28,14 +27,14 @@ describe('circular dependency detection', () => {
|
|
|
28
27
|
|
|
29
28
|
const childB = createSyncChildObservable(
|
|
30
29
|
{
|
|
31
|
-
parents: [
|
|
30
|
+
parents: [mut_childA],
|
|
32
31
|
initialValue: Optional.some(0),
|
|
33
32
|
},
|
|
34
33
|
createTryUpdateNotImplemented,
|
|
35
34
|
);
|
|
36
35
|
|
|
37
|
-
// Mutate
|
|
38
|
-
Object.defineProperty(
|
|
36
|
+
// Mutate mut_childA.parents to create a cycle: mut_childA -> childB -> mut_childA
|
|
37
|
+
Object.defineProperty(mut_childA, 'parents', {
|
|
39
38
|
value: [childB],
|
|
40
39
|
writable: false,
|
|
41
40
|
configurable: true,
|
|
@@ -44,7 +43,7 @@ describe('circular dependency detection', () => {
|
|
|
44
43
|
expect(() => {
|
|
45
44
|
createSyncChildObservable(
|
|
46
45
|
{
|
|
47
|
-
parents: [
|
|
46
|
+
parents: [mut_childA],
|
|
48
47
|
initialValue: Optional.some(0),
|
|
49
48
|
},
|
|
50
49
|
createTryUpdateNotImplemented,
|
|
@@ -60,7 +59,7 @@ describe('circular dependency detection', () => {
|
|
|
60
59
|
() => ({}),
|
|
61
60
|
);
|
|
62
61
|
|
|
63
|
-
const
|
|
62
|
+
const mut_childA = createAsyncChildObservable(
|
|
64
63
|
{
|
|
65
64
|
parents: [root],
|
|
66
65
|
initialValue: Optional.some(0),
|
|
@@ -70,14 +69,14 @@ describe('circular dependency detection', () => {
|
|
|
70
69
|
|
|
71
70
|
const childB = createSyncChildObservable(
|
|
72
71
|
{
|
|
73
|
-
parents: [
|
|
72
|
+
parents: [mut_childA],
|
|
74
73
|
initialValue: Optional.some(0),
|
|
75
74
|
},
|
|
76
75
|
createTryUpdateNotImplemented,
|
|
77
76
|
);
|
|
78
77
|
|
|
79
|
-
// Create cycle:
|
|
80
|
-
Object.defineProperty(
|
|
78
|
+
// Create cycle: mut_childA -> childB -> mut_childA
|
|
79
|
+
Object.defineProperty(mut_childA, 'parents', {
|
|
81
80
|
value: [childB],
|
|
82
81
|
writable: false,
|
|
83
82
|
configurable: true,
|
|
@@ -86,7 +85,7 @@ describe('circular dependency detection', () => {
|
|
|
86
85
|
expect(() => {
|
|
87
86
|
createAsyncChildObservable(
|
|
88
87
|
{
|
|
89
|
-
parents: [
|
|
88
|
+
parents: [mut_childA],
|
|
90
89
|
initialValue: Optional.some(0),
|
|
91
90
|
},
|
|
92
91
|
createTryUpdateNotImplemented,
|
|
@@ -102,7 +101,7 @@ describe('circular dependency detection', () => {
|
|
|
102
101
|
() => ({}),
|
|
103
102
|
);
|
|
104
103
|
|
|
105
|
-
const
|
|
104
|
+
const mut_childA = createSyncChildObservable(
|
|
106
105
|
{
|
|
107
106
|
parents: [root],
|
|
108
107
|
initialValue: Optional.some(0),
|
|
@@ -112,7 +111,7 @@ describe('circular dependency detection', () => {
|
|
|
112
111
|
|
|
113
112
|
const childB = createSyncChildObservable(
|
|
114
113
|
{
|
|
115
|
-
parents: [
|
|
114
|
+
parents: [mut_childA],
|
|
116
115
|
initialValue: Optional.some(0),
|
|
117
116
|
},
|
|
118
117
|
createTryUpdateNotImplemented,
|
|
@@ -126,8 +125,8 @@ describe('circular dependency detection', () => {
|
|
|
126
125
|
createTryUpdateNotImplemented,
|
|
127
126
|
);
|
|
128
127
|
|
|
129
|
-
// Create cycle:
|
|
130
|
-
Object.defineProperty(
|
|
128
|
+
// Create cycle: mut_childA -> childC -> childB -> mut_childA
|
|
129
|
+
Object.defineProperty(mut_childA, 'parents', {
|
|
131
130
|
value: [childC],
|
|
132
131
|
writable: false,
|
|
133
132
|
configurable: true,
|
|
@@ -136,7 +135,7 @@ describe('circular dependency detection', () => {
|
|
|
136
135
|
expect(() => {
|
|
137
136
|
createSyncChildObservable(
|
|
138
137
|
{
|
|
139
|
-
parents: [
|
|
138
|
+
parents: [mut_childA],
|
|
140
139
|
initialValue: Optional.some(0),
|
|
141
140
|
},
|
|
142
141
|
createTryUpdateNotImplemented,
|
|
@@ -152,7 +151,7 @@ describe('circular dependency detection', () => {
|
|
|
152
151
|
() => ({}),
|
|
153
152
|
);
|
|
154
153
|
|
|
155
|
-
const
|
|
154
|
+
const mut_childA = createSyncChildObservable(
|
|
156
155
|
{
|
|
157
156
|
parents: [root],
|
|
158
157
|
initialValue: Optional.some(0),
|
|
@@ -160,9 +159,9 @@ describe('circular dependency detection', () => {
|
|
|
160
159
|
createTryUpdateNotImplemented,
|
|
161
160
|
);
|
|
162
161
|
|
|
163
|
-
// Make
|
|
164
|
-
Object.defineProperty(
|
|
165
|
-
value: [
|
|
162
|
+
// Make mut_childA's parents reference mut_childA itself (self-loop)
|
|
163
|
+
Object.defineProperty(mut_childA, 'parents', {
|
|
164
|
+
value: [mut_childA],
|
|
166
165
|
writable: false,
|
|
167
166
|
configurable: true,
|
|
168
167
|
});
|
|
@@ -170,7 +169,7 @@ describe('circular dependency detection', () => {
|
|
|
170
169
|
expect(() => {
|
|
171
170
|
createSyncChildObservable(
|
|
172
171
|
{
|
|
173
|
-
parents: [
|
|
172
|
+
parents: [mut_childA],
|
|
174
173
|
initialValue: Optional.some(0),
|
|
175
174
|
},
|
|
176
175
|
createTryUpdateNotImplemented,
|
|
@@ -160,6 +160,7 @@ export const createInitializedSyncChildObservable = <
|
|
|
160
160
|
return (
|
|
161
161
|
// `getSnapshot` always returns `Some` here: `initialValue` is a `Some` and
|
|
162
162
|
// `setNext` only ever stores a `Some`.
|
|
163
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
163
164
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
164
165
|
observable as InitializedSyncChildObservable<A, P>
|
|
165
166
|
);
|
|
@@ -240,9 +241,13 @@ const hasCircularDependencyFrom = (
|
|
|
240
241
|
mut_visited: MutableSet<ObservableId>,
|
|
241
242
|
mut_inPath: MutableSet<ObservableId>,
|
|
242
243
|
): boolean => {
|
|
243
|
-
if (mut_inPath.has(node.id))
|
|
244
|
+
if (mut_inPath.has(node.id)) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
244
247
|
|
|
245
|
-
if (mut_visited.has(node.id))
|
|
248
|
+
if (mut_visited.has(node.id)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
246
251
|
|
|
247
252
|
mut_visited.add(node.id);
|
|
248
253
|
|
|
@@ -304,7 +309,9 @@ const registerChild = <A,>(
|
|
|
304
309
|
while (Arr.isNonEmpty(mut_rest)) {
|
|
305
310
|
const p = mut_rest.pop();
|
|
306
311
|
|
|
307
|
-
if (p === undefined)
|
|
312
|
+
if (p === undefined) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
308
315
|
|
|
309
316
|
if (isManagerObservable(p)) {
|
|
310
317
|
p.addDescendant(child);
|
|
@@ -42,6 +42,38 @@ import {
|
|
|
42
42
|
export type ObservableBaseHandle<A> = Readonly<{
|
|
43
43
|
id: ObservableId;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Registers the public object {@link assembleObservable} built from this
|
|
47
|
+
* handle, so that its four state flags (`isCompleted`, `updateToken`,
|
|
48
|
+
* `hasSubscriber`, `hasChild`) can be mirrored onto it as plain data
|
|
49
|
+
* properties.
|
|
50
|
+
*
|
|
51
|
+
* They used to be getters delegating to this handle's closures, and that
|
|
52
|
+
* costs more than it looks. Reading one was an accessor call plus a closure
|
|
53
|
+
* call where a field read would do — `updateToken` is read once per child
|
|
54
|
+
* per update, so the propagation path paid it on every step — and an object
|
|
55
|
+
* literal carrying accessors is far more expensive to create than one
|
|
56
|
+
* carrying data, which is most of what building a graph costs. Measured on
|
|
57
|
+
* one machine with `pnpm run benchmark`: the derived chain's 100,000 updates
|
|
58
|
+
* 16.7 ms -> 13.6 ms, and the cascaded diamond at N=20, whose measurement is
|
|
59
|
+
* dominated by graph construction, 587 ms -> 362 ms.
|
|
60
|
+
*
|
|
61
|
+
* Every write below therefore has to keep the mirror in step; that is the
|
|
62
|
+
* price of not reading through a closure on the hot path. It is paid in
|
|
63
|
+
* clarity too, and on purpose: `hasSubscriber` and `hasChild` are derived
|
|
64
|
+
* from `mut_subscribers` and `mut_children`, which a getter stated once and
|
|
65
|
+
* declaratively, whereas the mirror restates it at every write.
|
|
66
|
+
*
|
|
67
|
+
* Nor can the rarely read flags alone go back to getters while `updateToken`
|
|
68
|
+
* stays data. A single accessor in the literal is enough to lose nearly all
|
|
69
|
+
* of the gain: measured on Node 26 with the `...extra` spread followed by
|
|
70
|
+
* these properties, one getter among data properties made the literal about
|
|
71
|
+
* 10x slower to create than all data (four getters: about 15x) and gave
|
|
72
|
+
* every instance its own hidden class (`%HaveSameMap` false). Keep all four
|
|
73
|
+
* as data, or measure again before changing that.
|
|
74
|
+
*/
|
|
75
|
+
attach: (writers: ObservableStateWriters) => void;
|
|
76
|
+
|
|
45
77
|
addChild: <B>(child: ChildObservable<B>) => void;
|
|
46
78
|
|
|
47
79
|
getSnapshot: () => Optional<A>;
|
|
@@ -52,8 +84,6 @@ export type ObservableBaseHandle<A> = Readonly<{
|
|
|
52
84
|
|
|
53
85
|
hasSubscriber: () => boolean;
|
|
54
86
|
|
|
55
|
-
hasChild: () => boolean;
|
|
56
|
-
|
|
57
87
|
hasActiveChild: () => boolean;
|
|
58
88
|
|
|
59
89
|
subscribe: (onNext: (v: A) => void, onComplete?: () => void) => Subscription;
|
|
@@ -70,6 +100,20 @@ export type ObservableBaseHandle<A> = Readonly<{
|
|
|
70
100
|
completeBase: () => void;
|
|
71
101
|
}>;
|
|
72
102
|
|
|
103
|
+
/**
|
|
104
|
+
* How the handle writes the four state flags onto the public object. The
|
|
105
|
+
* writers are closures over that object rather than the object itself, because
|
|
106
|
+
* a parameter this side could mutate has to be a mutable type, which
|
|
107
|
+
* `@typescript-eslint/prefer-readonly-parameter-types` rejects — and the object
|
|
108
|
+
* is `Readonly` to everyone else.
|
|
109
|
+
*/
|
|
110
|
+
type ObservableStateWriters = Readonly<{
|
|
111
|
+
setIsCompleted: (value: boolean) => void;
|
|
112
|
+
setUpdateToken: (value: UpdateToken) => void;
|
|
113
|
+
setHasSubscriber: (value: boolean) => void;
|
|
114
|
+
setHasChild: (value: boolean) => void;
|
|
115
|
+
}>;
|
|
116
|
+
|
|
73
117
|
export const createObservableBaseHandle = <A,>(
|
|
74
118
|
initialValue: Optional<A>,
|
|
75
119
|
): ObservableBaseHandle<A> => {
|
|
@@ -85,17 +129,41 @@ export const createObservableBaseHandle = <A,>(
|
|
|
85
129
|
|
|
86
130
|
let mut_updateToken: UpdateToken = issueUpdateToken();
|
|
87
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Set once {@link assembleObservable} has built the public object. It does
|
|
134
|
+
* not exist while a leaf factory's `init` callback runs — that callback may
|
|
135
|
+
* already subscribe or complete — so every write below is optional, and
|
|
136
|
+
* `attach` copies the state as it stands rather than assuming defaults.
|
|
137
|
+
*/
|
|
138
|
+
let mut_writers: ObservableStateWriters | undefined;
|
|
139
|
+
|
|
140
|
+
const attach = (writers: ObservableStateWriters): void => {
|
|
141
|
+
mut_writers = writers;
|
|
142
|
+
|
|
143
|
+
writers.setIsCompleted(mut_isCompleted);
|
|
144
|
+
|
|
145
|
+
writers.setUpdateToken(mut_updateToken);
|
|
146
|
+
|
|
147
|
+
writers.setHasSubscriber(mut_subscribers.size > 0);
|
|
148
|
+
|
|
149
|
+
writers.setHasChild(Arr.isNonEmpty(mut_children));
|
|
150
|
+
};
|
|
151
|
+
|
|
88
152
|
const addSubscriber = (s: Subscriber<A>): SubscriberId => {
|
|
89
153
|
// return the id of added subscriber
|
|
90
154
|
const subscriberId = issueSubscriberId();
|
|
91
155
|
|
|
92
156
|
mut_subscribers.set(subscriberId, s);
|
|
93
157
|
|
|
158
|
+
mut_writers?.setHasSubscriber(true);
|
|
159
|
+
|
|
94
160
|
return subscriberId;
|
|
95
161
|
};
|
|
96
162
|
|
|
97
163
|
const removeSubscriber = (subscriberId: SubscriberId): void => {
|
|
98
164
|
mut_subscribers.delete(subscriberId);
|
|
165
|
+
|
|
166
|
+
mut_writers?.setHasSubscriber(mut_subscribers.size > 0);
|
|
99
167
|
};
|
|
100
168
|
|
|
101
169
|
const addChild = <B,>(child: ChildObservable<B>): void => {
|
|
@@ -104,6 +172,8 @@ export const createObservableBaseHandle = <A,>(
|
|
|
104
172
|
|
|
105
173
|
child as ChildObservable<unknown>,
|
|
106
174
|
);
|
|
175
|
+
|
|
176
|
+
mut_writers?.setHasChild(true);
|
|
107
177
|
};
|
|
108
178
|
|
|
109
179
|
const getSnapshot = (): Optional<A> => mut_currentValue;
|
|
@@ -114,14 +184,14 @@ export const createObservableBaseHandle = <A,>(
|
|
|
114
184
|
|
|
115
185
|
const hasSubscriber = (): boolean => mut_subscribers.size > 0;
|
|
116
186
|
|
|
117
|
-
const hasChild = (): boolean => Arr.isNonEmpty(mut_children);
|
|
118
|
-
|
|
119
187
|
const hasActiveChild = (): boolean =>
|
|
120
188
|
mut_children.some((c) => !c.isCompleted);
|
|
121
189
|
|
|
122
190
|
const setNext = (nextValue: A, nextUpdateToken: UpdateToken): void => {
|
|
123
191
|
mut_updateToken = nextUpdateToken;
|
|
124
192
|
|
|
193
|
+
mut_writers?.setUpdateToken(nextUpdateToken);
|
|
194
|
+
|
|
125
195
|
mut_currentValue = Optional.some(nextValue);
|
|
126
196
|
|
|
127
197
|
for (const s of mut_subscribers.values()) {
|
|
@@ -130,11 +200,15 @@ export const createObservableBaseHandle = <A,>(
|
|
|
130
200
|
};
|
|
131
201
|
|
|
132
202
|
const completeBase = (): void => {
|
|
133
|
-
if (mut_isCompleted)
|
|
203
|
+
if (mut_isCompleted) {
|
|
204
|
+
return; // terminate only once
|
|
205
|
+
}
|
|
134
206
|
|
|
135
207
|
// change state
|
|
136
208
|
mut_isCompleted = true;
|
|
137
209
|
|
|
210
|
+
mut_writers?.setIsCompleted(true);
|
|
211
|
+
|
|
138
212
|
// run subscribers for the current value
|
|
139
213
|
for (const s of mut_subscribers.values()) {
|
|
140
214
|
s.onComplete();
|
|
@@ -143,6 +217,8 @@ export const createObservableBaseHandle = <A,>(
|
|
|
143
217
|
// remove all subscribers
|
|
144
218
|
mut_subscribers.clear();
|
|
145
219
|
|
|
220
|
+
mut_writers?.setHasSubscriber(false);
|
|
221
|
+
|
|
146
222
|
// propagate to children
|
|
147
223
|
for (const o of mut_children) {
|
|
148
224
|
o.tryComplete();
|
|
@@ -181,12 +257,12 @@ export const createObservableBaseHandle = <A,>(
|
|
|
181
257
|
|
|
182
258
|
return {
|
|
183
259
|
id,
|
|
260
|
+
attach,
|
|
184
261
|
addChild,
|
|
185
262
|
getSnapshot,
|
|
186
263
|
isCompleted,
|
|
187
264
|
updateToken,
|
|
188
265
|
hasSubscriber,
|
|
189
|
-
hasChild,
|
|
190
266
|
hasActiveChild,
|
|
191
267
|
subscribe,
|
|
192
268
|
setNext,
|
|
@@ -220,7 +296,9 @@ export const createManagerObservableParts = <A,>(
|
|
|
220
296
|
const mut_descendantsIdSet: MutableSet<ObservableId> = new Set();
|
|
221
297
|
|
|
222
298
|
const addDescendant = <B,>(child: ChildObservable<B>): void => {
|
|
223
|
-
if (mut_descendantsIdSet.has(child.id))
|
|
299
|
+
if (mut_descendantsIdSet.has(child.id)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
224
302
|
|
|
225
303
|
mut_descendantsIdSet.add(child.id);
|
|
226
304
|
|
|
@@ -270,7 +348,7 @@ type AssembleObservableArgs<
|
|
|
270
348
|
Extra extends ReadonlyRecord<string, unknown>,
|
|
271
349
|
> = Readonly<{
|
|
272
350
|
kind: Kind;
|
|
273
|
-
depth: number;
|
|
351
|
+
depth: Kind extends 'root' ? 0 : number;
|
|
274
352
|
handle: ObservableBaseHandle<A>;
|
|
275
353
|
tryUpdate: (updateToken: UpdateToken) => void;
|
|
276
354
|
tryComplete: () => void;
|
|
@@ -278,8 +356,9 @@ type AssembleObservableArgs<
|
|
|
278
356
|
/**
|
|
279
357
|
* Kind-specific public members (`parents`, `addDescendant`) and leaf
|
|
280
358
|
* extensions (`next`, `start`). They are merged here, in the one place the
|
|
281
|
-
* final object literal is created, because the
|
|
282
|
-
*
|
|
359
|
+
* final object literal is created, because the handle keeps the state flags
|
|
360
|
+
* up to date on that object alone: a copy made by spreading it afterwards
|
|
361
|
+
* would freeze their current values.
|
|
283
362
|
*/
|
|
284
363
|
extra: Extra;
|
|
285
364
|
}>;
|
|
@@ -287,9 +366,10 @@ type AssembleObservableArgs<
|
|
|
287
366
|
/**
|
|
288
367
|
* Builds the public observable object from a base handle. This replaces the
|
|
289
368
|
* implicit assembly a `class` performs: state stays in the handle's closures,
|
|
290
|
-
* `isCompleted` / `updateToken` / `hasSubscriber` / `hasChild` become
|
|
291
|
-
*
|
|
292
|
-
* `tryComplete`, `complete`) is passed in already
|
|
369
|
+
* `isCompleted` / `updateToken` / `hasSubscriber` / `hasChild` become data
|
|
370
|
+
* properties the handle keeps in step through `attach`, and the kind-specific
|
|
371
|
+
* behavior (`tryUpdate`, `tryComplete`, `complete`) is passed in already
|
|
372
|
+
* composed.
|
|
293
373
|
*/
|
|
294
374
|
export const assembleObservable = <
|
|
295
375
|
A,
|
|
@@ -305,7 +385,10 @@ export const assembleObservable = <
|
|
|
305
385
|
extra,
|
|
306
386
|
}: AssembleObservableArgs<A, Kind, Extra>): Extra &
|
|
307
387
|
ObservableBase<A> &
|
|
308
|
-
Readonly<{
|
|
388
|
+
Readonly<{
|
|
389
|
+
kind: Kind;
|
|
390
|
+
depth: AssembleObservableArgs<A, Kind, Extra>['depth'];
|
|
391
|
+
}> => {
|
|
309
392
|
function pipe<B>(
|
|
310
393
|
operator: WithInitialValueOperator<A, B>,
|
|
311
394
|
): InitializedObservable<B>;
|
|
@@ -314,12 +397,18 @@ export const assembleObservable = <
|
|
|
314
397
|
|
|
315
398
|
function pipe<B>(operator: Operator<A, B>): Observable<B> {
|
|
316
399
|
return operator(
|
|
400
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
317
401
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
318
|
-
|
|
402
|
+
mut_observable as unknown as InitializedObservable<A>,
|
|
319
403
|
);
|
|
320
404
|
}
|
|
321
405
|
|
|
322
|
-
|
|
406
|
+
// The four state flags are plain data properties kept in step by the handle,
|
|
407
|
+
// not getters delegating to it — see `ObservableBaseHandle.attach` for the
|
|
408
|
+
// measurements that decided this. `updateToken` in particular is read once
|
|
409
|
+
// per child per update, and `tryUpdate` reading a field rather than calling
|
|
410
|
+
// through an accessor is most of what the change buys.
|
|
411
|
+
const mut_observable = {
|
|
323
412
|
...extra,
|
|
324
413
|
|
|
325
414
|
id: handle.id,
|
|
@@ -332,21 +421,13 @@ export const assembleObservable = <
|
|
|
332
421
|
|
|
333
422
|
getSnapshot: handle.getSnapshot,
|
|
334
423
|
|
|
335
|
-
|
|
336
|
-
return handle.isCompleted();
|
|
337
|
-
},
|
|
424
|
+
isCompleted: false,
|
|
338
425
|
|
|
339
|
-
|
|
340
|
-
return handle.updateToken();
|
|
341
|
-
},
|
|
426
|
+
updateToken: handle.updateToken(),
|
|
342
427
|
|
|
343
|
-
|
|
344
|
-
return handle.hasSubscriber();
|
|
345
|
-
},
|
|
428
|
+
hasSubscriber: false,
|
|
346
429
|
|
|
347
|
-
|
|
348
|
-
return handle.hasChild();
|
|
349
|
-
},
|
|
430
|
+
hasChild: false,
|
|
350
431
|
|
|
351
432
|
hasActiveChild: handle.hasActiveChild,
|
|
352
433
|
|
|
@@ -359,7 +440,25 @@ export const assembleObservable = <
|
|
|
359
440
|
subscribe: handle.subscribe,
|
|
360
441
|
|
|
361
442
|
pipe,
|
|
362
|
-
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
handle.attach({
|
|
446
|
+
setIsCompleted: (value) => {
|
|
447
|
+
mut_observable.isCompleted = value;
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
setUpdateToken: (value) => {
|
|
451
|
+
mut_observable.updateToken = value;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
setHasSubscriber: (value) => {
|
|
455
|
+
mut_observable.hasSubscriber = value;
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
setHasChild: (value) => {
|
|
459
|
+
mut_observable.hasChild = value;
|
|
460
|
+
},
|
|
461
|
+
});
|
|
363
462
|
|
|
364
|
-
return
|
|
463
|
+
return mut_observable;
|
|
365
464
|
};
|
|
@@ -47,6 +47,8 @@ export type RootObservableConfig<A> = Readonly<{
|
|
|
47
47
|
*/
|
|
48
48
|
export const createRootObservable = <
|
|
49
49
|
A,
|
|
50
|
+
// The default is deliberately empty: a root leaf without `init` adds no members.
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-generated-empty-object-type
|
|
50
52
|
Extension extends ReadonlyRecord<string, unknown> = ReadonlyRecord<
|
|
51
53
|
never,
|
|
52
54
|
never
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
|
|
75
75
|
parents: OS,
|
|
76
76
|
): CombineObservableRefined<OS> =>
|
|
77
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
77
78
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
78
79
|
createCombineObservable(parents) as unknown as CombineObservableRefined<OS>;
|
|
79
80
|
|
|
@@ -113,7 +114,9 @@ const createCombineObservable = <const A extends NonEmptyUnknownList>(
|
|
|
113
114
|
},
|
|
114
115
|
({ setNext }) =>
|
|
115
116
|
(updateToken) => {
|
|
116
|
-
if (parents.every((o) => o.updateToken !== updateToken))
|
|
117
|
+
if (parents.every((o) => o.updateToken !== updateToken)) {
|
|
118
|
+
return; // all parents are skipped
|
|
119
|
+
}
|
|
117
120
|
|
|
118
121
|
// Same shape-preserving chain as the initial value: `Arr.map` (not the
|
|
119
122
|
// native one, which drops the tuple shape) into the uniform
|
|
@@ -123,14 +126,16 @@ const createCombineObservable = <const A extends NonEmptyUnknownList>(
|
|
|
123
126
|
(a) => a.getSnapshot(),
|
|
124
127
|
);
|
|
125
128
|
|
|
126
|
-
if (Arr.every(parentValues, Optional.isSome)) {
|
|
127
|
-
|
|
128
|
-
parentValues,
|
|
129
|
-
(a) => a.value,
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
setNext(nextValue, updateToken);
|
|
129
|
+
if (!Arr.every(parentValues, Optional.isSome)) {
|
|
130
|
+
return;
|
|
133
131
|
}
|
|
132
|
+
|
|
133
|
+
const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
|
|
134
|
+
parentValues,
|
|
135
|
+
(a) => a.value,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
setNext(nextValue, updateToken);
|
|
134
139
|
},
|
|
135
140
|
);
|
|
136
141
|
};
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
export const merge = <const OS extends NonEmptyTuple<Observable<unknown>>>(
|
|
66
66
|
parents: OS,
|
|
67
67
|
): MergeObservableRefined<OS> =>
|
|
68
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
68
69
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
69
70
|
createMergeObservable(parents) as MergeObservableRefined<OS>;
|
|
70
71
|
|
|
@@ -83,9 +84,12 @@ const createMergeObservable = <const P extends NonEmptyUnknownList>(
|
|
|
83
84
|
o.updateToken === updateToken && Optional.isSome(o.getSnapshot()),
|
|
84
85
|
);
|
|
85
86
|
|
|
86
|
-
if (parentToUse === undefined)
|
|
87
|
+
if (parentToUse === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
87
90
|
|
|
88
91
|
const nextValue =
|
|
92
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
89
93
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
90
94
|
Optional.unwrap(parentToUse.getSnapshot()) as ArrayElement<P>;
|
|
91
95
|
|
package/src/core/combine/zip.mts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
export const zip = <const OS extends NonEmptyTuple<Observable<unknown>>>(
|
|
68
68
|
parents: OS,
|
|
69
69
|
): ZipObservableRefined<OS> =>
|
|
70
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
70
71
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
71
72
|
createZipObservable(parents) as unknown as ZipObservableRefined<OS>;
|
|
72
73
|
|
|
@@ -110,15 +111,18 @@ const createZipObservable = <const A extends NonEmptyUnknownList>(
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
if (queues.
|
|
114
|
-
|
|
115
|
-
// `Arr.map` has a single result element type, so it reports
|
|
116
|
-
// `A[number] | undefined` per position rather than `A[P]`.
|
|
117
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
118
|
-
Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
|
|
119
|
-
|
|
120
|
-
setNext(nextValue, updateToken);
|
|
114
|
+
if (queues.some((list) => list.isEmpty)) {
|
|
115
|
+
return;
|
|
121
116
|
}
|
|
117
|
+
|
|
118
|
+
const nextValue =
|
|
119
|
+
// `Arr.map` has a single result element type, so it reports
|
|
120
|
+
// `A[number] | undefined` per position rather than `A[P]`.
|
|
121
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
122
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
123
|
+
Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
|
|
124
|
+
|
|
125
|
+
setNext(nextValue, updateToken);
|
|
122
126
|
},
|
|
123
127
|
);
|
|
124
128
|
};
|
|
@@ -70,6 +70,7 @@ import { type FromPromiseObservable } from '../types/index.mjs';
|
|
|
70
70
|
* ```
|
|
71
71
|
*/
|
|
72
72
|
export const fromAbortablePromise = <A, E = unknown>(
|
|
73
|
+
// @sumi-expect-error readonly/require-readonly-parameter
|
|
73
74
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
74
75
|
factory: (signal: AbortSignal) => Promise<A>,
|
|
75
76
|
): FromPromiseObservable<A, E> => {
|
|
@@ -87,12 +88,16 @@ export const fromAbortablePromise = <A, E = unknown>(
|
|
|
87
88
|
|
|
88
89
|
promise
|
|
89
90
|
.then((value) => {
|
|
90
|
-
if (isCompleted())
|
|
91
|
+
if (isCompleted()) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
91
94
|
|
|
92
95
|
startUpdate(Result.ok(value));
|
|
93
96
|
})
|
|
94
97
|
.catch((error: unknown) => {
|
|
95
|
-
if (isCompleted())
|
|
98
|
+
if (isCompleted()) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
96
101
|
|
|
97
102
|
// Silently ignore AbortError — it means the observable was
|
|
98
103
|
// intentionally completed (e.g., by switchMap).
|
|
@@ -102,6 +107,7 @@ export const fromAbortablePromise = <A, E = unknown>(
|
|
|
102
107
|
|
|
103
108
|
startUpdate(
|
|
104
109
|
Result.err(
|
|
110
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
105
111
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
106
112
|
error as E,
|
|
107
113
|
),
|