synstate 3.1.0 → 3.2.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/README.md +60 -2
- package/dist/core/base/create-child-observable.d.mts.map +1 -1
- package/dist/core/base/create-child-observable.mjs +60 -3
- package/dist/core/base/create-child-observable.mjs.map +1 -1
- package/dist/core/base/create-observable-base.d.mts +21 -4
- package/dist/core/base/create-observable-base.d.mts.map +1 -1
- package/dist/core/base/create-observable-base.mjs +69 -13
- 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 +2 -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 +0 -1
- 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 +0 -2
- 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 +0 -2
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.d.mts +10 -2
- package/dist/core/create/counter.d.mts.map +1 -1
- package/dist/core/create/counter.mjs +17 -7
- 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 +0 -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 +0 -1
- 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 +0 -1
- 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 +0 -1
- package/dist/core/create/just.mjs.map +1 -1
- package/dist/core/operators/audit.d.mts.map +1 -1
- package/dist/core/operators/audit.mjs +0 -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 +0 -1
- package/dist/core/operators/debounce.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs +19 -0
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/map.d.mts.map +1 -1
- package/dist/core/operators/map.mjs +0 -1
- 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 +0 -1
- 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 +0 -1
- 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 +0 -1
- 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 +0 -1
- package/dist/core/operators/with-buffered-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 +0 -1
- 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 +0 -1
- package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -1
- package/dist/core/types/observable.d.mts +2 -0
- package/dist/core/types/observable.d.mts.map +1 -1
- 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 +0 -1
- 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 +0 -1
- package/dist/core/utils/utils.mjs.map +1 -1
- package/package.json +14 -14
- package/src/core/base/create-child-observable.mts +78 -3
- package/src/core/base/create-observable-base.mts +111 -31
- package/src/core/base/create-root-observable.mts +3 -1
- package/src/core/combine/combine.mts +0 -1
- package/src/core/combine/merge.mts +0 -2
- package/src/core/combine/zip.mts +0 -2
- package/src/core/create/counter.mts +20 -10
- package/src/core/create/counter.test.mts +183 -0
- package/src/core/create/from-abortable-promise.mts +0 -2
- package/src/core/create/from-promise.mts +0 -1
- package/src/core/create/from-subscribable.mts +0 -1
- package/src/core/create/just.mts +0 -1
- package/src/core/operators/audit.mts +0 -1
- package/src/core/operators/debounce.mts +0 -1
- package/src/core/operators/filter.mts +41 -0
- package/src/core/operators/map.mts +0 -1
- package/src/core/operators/skip-if-no-change.mts +0 -1
- package/src/core/operators/take-until.mts +0 -1
- package/src/core/operators/throttle.mts +0 -1
- package/src/core/operators/with-buffered-from.mts +0 -1
- package/src/core/predefined/operators/unwrap-optional.mts +0 -1
- package/src/core/predefined/operators/unwrap-result-ok.mts +0 -1
- package/src/core/types/observable.mts +2 -0
- package/src/core/utils/id-maker.mts +0 -1
- package/src/core/utils/utils.mts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synstate",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Type-safe State Management Library for TypeScript/JavaScript",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,25 +34,25 @@
|
|
|
34
34
|
"ts-type-forge": "^9.2.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@reduxjs/toolkit": "2.
|
|
37
|
+
"@reduxjs/toolkit": "2.13.0",
|
|
38
38
|
"@sumi-lang/cli": "0.0.5",
|
|
39
39
|
"@types/react": "19.3.0",
|
|
40
|
-
"@vitest/browser-playwright": "5.0.
|
|
40
|
+
"@vitest/browser-playwright": "5.0.3",
|
|
41
41
|
"argparse": "3.0.2",
|
|
42
|
-
"eslint-config-typed": "5.17.
|
|
43
|
-
"eslint-plugin-ts-data-forge": "0.8.
|
|
44
|
-
"eslint-plugin-ts-fortress": "1.3.
|
|
45
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
46
|
-
"jotai": "3.0.
|
|
47
|
-
"mobx": "7.0.
|
|
48
|
-
"playwright": "1.
|
|
42
|
+
"eslint-config-typed": "5.17.4",
|
|
43
|
+
"eslint-plugin-ts-data-forge": "0.8.6",
|
|
44
|
+
"eslint-plugin-ts-fortress": "1.3.3",
|
|
45
|
+
"eslint-plugin-ts-type-forge": "0.8.8",
|
|
46
|
+
"jotai": "3.0.1",
|
|
47
|
+
"mobx": "7.0.6",
|
|
48
|
+
"playwright": "1.64.0",
|
|
49
49
|
"prettier": "3.9.9",
|
|
50
50
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
51
51
|
"prettier-plugin-packagejson": "3.0.2",
|
|
52
52
|
"react": "19.3.0",
|
|
53
53
|
"rxjs": "7.8.2",
|
|
54
|
-
"synstate-react-hooks": "3.1
|
|
55
|
-
"synstate-react-hooks-compat": "3.1
|
|
54
|
+
"synstate-react-hooks": "3.2.1",
|
|
55
|
+
"synstate-react-hooks-compat": "3.2.1",
|
|
56
56
|
"ts-repo-utils": "10.7.1",
|
|
57
57
|
"valtio": "2.3.2",
|
|
58
58
|
"zustand": "5.0.15"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
|
|
70
70
|
"check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
|
|
71
|
-
"check:lint": "eslint .",
|
|
71
|
+
"check:lint": "oxlint . && eslint .",
|
|
72
72
|
"check:sumi": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../languages/sumi/cli/src/cmd/sumi.mts check ./tsconfig.sumi.json",
|
|
73
73
|
"check:test": "pnpm run z:vitest:node run",
|
|
74
74
|
"check:test:browser": "pnpm run z:vitest --project='Browser' run",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
|
|
80
80
|
"fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
|
|
81
81
|
"fix:fmt:full": "pnpm run z:prettier .",
|
|
82
|
-
"fix:lint": "eslint . --fix",
|
|
82
|
+
"fix:lint": "oxlint . --fix && eslint . --fix",
|
|
83
83
|
"fmt": "pnpm run fix:fmt",
|
|
84
84
|
"gen": "run-s gen:index gen:readme gen:jsdoc gen:sample-diffs",
|
|
85
85
|
"gen:index": "run-s gen:index:src fix:fmt",
|
|
@@ -98,11 +98,25 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
|
98
98
|
): SyncChildObservable<A, P> => {
|
|
99
99
|
const handle = createObservableBaseHandle<A>(initialValue);
|
|
100
100
|
|
|
101
|
+
/** Set once assembled; see {@link unregisterChild}. */
|
|
102
|
+
let mut_self: ChildObservable<A> | undefined = undefined;
|
|
103
|
+
|
|
101
104
|
const complete = (): void => {
|
|
105
|
+
// A completed node is reached again through every parent and child that
|
|
106
|
+
// completes after it; running again would repeat the teardown and the
|
|
107
|
+
// ancestor walk in `unregisterChild` for each of them.
|
|
108
|
+
if (handle.isCompleted()) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
onComplete?.();
|
|
103
113
|
|
|
104
114
|
handle.completeBase();
|
|
105
115
|
|
|
116
|
+
if (mut_self !== undefined) {
|
|
117
|
+
unregisterChild(mut_self, parents);
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
// propagate to parents
|
|
107
121
|
for (const par of parents) {
|
|
108
122
|
par.tryComplete();
|
|
@@ -110,6 +124,10 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
|
110
124
|
};
|
|
111
125
|
|
|
112
126
|
const tryComplete = (): void => {
|
|
127
|
+
if (handle.isCompleted()) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
tryCompleteChild({
|
|
114
132
|
hasSubscriber: handle.hasSubscriber(),
|
|
115
133
|
hasActiveChild: handle.hasActiveChild(),
|
|
@@ -136,6 +154,8 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
|
136
154
|
|
|
137
155
|
registerChild(observable, parents);
|
|
138
156
|
|
|
157
|
+
mut_self = observable;
|
|
158
|
+
|
|
139
159
|
return observable;
|
|
140
160
|
};
|
|
141
161
|
|
|
@@ -161,7 +181,6 @@ export const createInitializedSyncChildObservable = <
|
|
|
161
181
|
// `getSnapshot` always returns `Some` here: `initialValue` is a `Some` and
|
|
162
182
|
// `setNext` only ever stores a `Some`.
|
|
163
183
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
164
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
165
184
|
observable as InitializedSyncChildObservable<A, P>
|
|
166
185
|
);
|
|
167
186
|
};
|
|
@@ -184,13 +203,28 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
|
184
203
|
): AsyncChildObservable<A, P> => {
|
|
185
204
|
const handle = createObservableBaseHandle<A>(initialValue);
|
|
186
205
|
|
|
187
|
-
const { addDescendant, startUpdate } =
|
|
206
|
+
const { addDescendant, deleteDescendant, startUpdate } =
|
|
207
|
+
createManagerObservableParts(handle);
|
|
208
|
+
|
|
209
|
+
/** Set once assembled; see {@link unregisterChild}. */
|
|
210
|
+
let mut_self: ChildObservable<A> | undefined = undefined;
|
|
188
211
|
|
|
189
212
|
const complete = (): void => {
|
|
213
|
+
// A completed node is reached again through every parent and child that
|
|
214
|
+
// completes after it; running again would repeat the teardown and the
|
|
215
|
+
// ancestor walk in `unregisterChild` for each of them.
|
|
216
|
+
if (handle.isCompleted()) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
190
220
|
onComplete?.();
|
|
191
221
|
|
|
192
222
|
handle.completeBase();
|
|
193
223
|
|
|
224
|
+
if (mut_self !== undefined) {
|
|
225
|
+
unregisterChild(mut_self, parents);
|
|
226
|
+
}
|
|
227
|
+
|
|
194
228
|
// propagate to parents
|
|
195
229
|
for (const par of parents) {
|
|
196
230
|
par.tryComplete();
|
|
@@ -198,6 +232,10 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
|
198
232
|
};
|
|
199
233
|
|
|
200
234
|
const tryComplete = (): void => {
|
|
235
|
+
if (handle.isCompleted()) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
201
239
|
tryCompleteChild({
|
|
202
240
|
hasSubscriber: handle.hasSubscriber(),
|
|
203
241
|
hasActiveChild: handle.hasActiveChild(),
|
|
@@ -220,11 +258,13 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
|
220
258
|
tryUpdate,
|
|
221
259
|
tryComplete,
|
|
222
260
|
complete,
|
|
223
|
-
extra: { parents, addDescendant },
|
|
261
|
+
extra: { parents, addDescendant, deleteDescendant },
|
|
224
262
|
});
|
|
225
263
|
|
|
226
264
|
registerChild(observable, parents);
|
|
227
265
|
|
|
266
|
+
mut_self = observable;
|
|
267
|
+
|
|
228
268
|
return observable;
|
|
229
269
|
};
|
|
230
270
|
|
|
@@ -322,6 +362,41 @@ const registerChild = <A,>(
|
|
|
322
362
|
}
|
|
323
363
|
};
|
|
324
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Undoes {@link registerChild} for a completed child: its parents forget it, and
|
|
367
|
+
* every manager it registered to stops propagating updates to it. Without this
|
|
368
|
+
* a completed node stayed reachable for as long as its root lived, and ran its
|
|
369
|
+
* `tryUpdate` on every update (#2134).
|
|
370
|
+
*
|
|
371
|
+
* A completion that happens while the child is still being constructed (from
|
|
372
|
+
* a leaf factory's `createTryUpdate`) precedes its registration, so there is
|
|
373
|
+
* nothing to undo; `mut_self` is still `undefined` then.
|
|
374
|
+
*/
|
|
375
|
+
const unregisterChild = <A,>(
|
|
376
|
+
child: ChildObservable<A>,
|
|
377
|
+
parents: ChildObservable<A>['parents'],
|
|
378
|
+
): void => {
|
|
379
|
+
for (const p of parents) {
|
|
380
|
+
p.deleteChild(child);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const mut_rest = Array.from(parents);
|
|
384
|
+
|
|
385
|
+
while (Arr.isNonEmpty(mut_rest)) {
|
|
386
|
+
const p = mut_rest.pop();
|
|
387
|
+
|
|
388
|
+
if (p === undefined) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (isManagerObservable(p)) {
|
|
393
|
+
p.deleteDescendant(child);
|
|
394
|
+
} else {
|
|
395
|
+
mut_rest.push(...p.parents);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
325
400
|
const tryCompleteChild = <A,>({
|
|
326
401
|
hasSubscriber,
|
|
327
402
|
hasActiveChild,
|
|
@@ -72,6 +72,12 @@ export type ObservableBaseHandle<A> = Readonly<{
|
|
|
72
72
|
|
|
73
73
|
addChild: <B>(child: ChildObservable<B>) => void;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Forgets `child`, which a completed child calls on each of its parents so
|
|
77
|
+
* that nothing keeps a reference to it (#2134).
|
|
78
|
+
*/
|
|
79
|
+
deleteChild: <B>(child: ChildObservable<B>) => void;
|
|
80
|
+
|
|
75
81
|
getSnapshot: () => Optional<A>;
|
|
76
82
|
|
|
77
83
|
isCompleted: () => boolean;
|
|
@@ -115,7 +121,9 @@ export const createObservableBaseHandle = <A,>(
|
|
|
115
121
|
): ObservableBaseHandle<A> => {
|
|
116
122
|
const id = issueObservableId();
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
// A set, not an array: a completed child removes itself, and that has to be
|
|
125
|
+
// O(1) for a UI that creates and completes derived nodes on every mount.
|
|
126
|
+
const mut_children: MutableSet<ChildObservable<unknown>> = new Set();
|
|
119
127
|
|
|
120
128
|
const mut_subscribers: MutableMap<SubscriberId, Subscriber<A>> = new Map();
|
|
121
129
|
|
|
@@ -142,7 +150,7 @@ export const createObservableBaseHandle = <A,>(
|
|
|
142
150
|
|
|
143
151
|
writers.setHasSubscriber(mut_subscribers.size > 0);
|
|
144
152
|
|
|
145
|
-
writers.setHasChild(
|
|
153
|
+
writers.setHasChild(mut_children.size > 0);
|
|
146
154
|
};
|
|
147
155
|
|
|
148
156
|
const addSubscriber = (s: Subscriber<A>): SubscriberId => {
|
|
@@ -163,15 +171,17 @@ export const createObservableBaseHandle = <A,>(
|
|
|
163
171
|
};
|
|
164
172
|
|
|
165
173
|
const addChild = <B,>(child: ChildObservable<B>): void => {
|
|
166
|
-
mut_children
|
|
167
|
-
mut_children,
|
|
168
|
-
|
|
169
|
-
child as ChildObservable<unknown>,
|
|
170
|
-
);
|
|
174
|
+
mut_children.add(child);
|
|
171
175
|
|
|
172
176
|
mut_writers?.setHasChild(true);
|
|
173
177
|
};
|
|
174
178
|
|
|
179
|
+
const deleteChild = <B,>(child: ChildObservable<B>): void => {
|
|
180
|
+
mut_children.delete(child);
|
|
181
|
+
|
|
182
|
+
mut_writers?.setHasChild(mut_children.size > 0);
|
|
183
|
+
};
|
|
184
|
+
|
|
175
185
|
const getSnapshot = (): Optional<A> => mut_currentValue;
|
|
176
186
|
|
|
177
187
|
const isCompleted = (): boolean => mut_isCompleted;
|
|
@@ -180,8 +190,15 @@ export const createObservableBaseHandle = <A,>(
|
|
|
180
190
|
|
|
181
191
|
const hasSubscriber = (): boolean => mut_subscribers.size > 0;
|
|
182
192
|
|
|
183
|
-
const hasActiveChild = (): boolean =>
|
|
184
|
-
|
|
193
|
+
const hasActiveChild = (): boolean => {
|
|
194
|
+
for (const c of mut_children) {
|
|
195
|
+
if (!c.isCompleted) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return false;
|
|
201
|
+
};
|
|
185
202
|
|
|
186
203
|
const setNext = (nextValue: A, nextUpdateToken: UpdateToken): void => {
|
|
187
204
|
mut_updateToken = nextUpdateToken;
|
|
@@ -215,7 +232,8 @@ export const createObservableBaseHandle = <A,>(
|
|
|
215
232
|
|
|
216
233
|
mut_writers?.setHasSubscriber(false);
|
|
217
234
|
|
|
218
|
-
//
|
|
235
|
+
// Propagate to children. A child that completes removes itself from
|
|
236
|
+
// `mut_children` during this loop, which iterating a `Set` tolerates.
|
|
219
237
|
for (const o of mut_children) {
|
|
220
238
|
o.tryComplete();
|
|
221
239
|
}
|
|
@@ -255,6 +273,7 @@ export const createObservableBaseHandle = <A,>(
|
|
|
255
273
|
id,
|
|
256
274
|
attach,
|
|
257
275
|
addChild,
|
|
276
|
+
deleteChild,
|
|
258
277
|
getSnapshot,
|
|
259
278
|
isCompleted,
|
|
260
279
|
updateToken,
|
|
@@ -273,41 +292,98 @@ export const createObservableBaseHandle = <A,>(
|
|
|
273
292
|
export type ManagerObservableParts<A> = Readonly<{
|
|
274
293
|
addDescendant: <B>(child: ChildObservable<B>) => void;
|
|
275
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Takes `child` out of the propagation order, which a completed child calls
|
|
297
|
+
* on every manager it registered to (#2134).
|
|
298
|
+
*/
|
|
299
|
+
deleteDescendant: <B>(child: ChildObservable<B>) => void;
|
|
300
|
+
|
|
276
301
|
/** Emits `nextValue` and propagates the update to all descendants. */
|
|
277
302
|
startUpdate: (nextValue: A) => void;
|
|
278
303
|
}>;
|
|
279
304
|
|
|
280
305
|
/**
|
|
281
306
|
* Builds the manager half of a root or async-child observable. Both start their
|
|
282
|
-
* own update propagation, so both keep a
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
307
|
+
* own update propagation, so both keep a depth-ordered propagation list; this
|
|
308
|
+
* is the single implementation of it (the class version duplicated it across
|
|
309
|
+
* `RootObservableClass` and `AsyncChildObservableClass`).
|
|
310
|
+
*
|
|
311
|
+
* The descendants are kept in one insertion-ordered set per depth, so adding
|
|
312
|
+
* and removing one is O(1) apart from the first node of a new depth. The flat
|
|
313
|
+
* array `startUpdate` walks is rebuilt only when an update finds it stale: it
|
|
314
|
+
* used to be copied on every `addDescendant`, which made creating n nodes under
|
|
315
|
+
* one root O(n²). Rebuilding at the start of an update also keeps the walk a
|
|
316
|
+
* snapshot, as the copy-on-write array was — a node created or completed while
|
|
317
|
+
* an update is propagating does not change which nodes that update visits.
|
|
286
318
|
*/
|
|
287
319
|
export const createManagerObservableParts = <A,>(
|
|
288
320
|
handle: ObservableBaseHandle<A>,
|
|
289
321
|
): ManagerObservableParts<A> => {
|
|
290
|
-
|
|
322
|
+
const mut_buckets: MutableMap<
|
|
323
|
+
number,
|
|
324
|
+
MutableSet<ChildObservable<unknown>>
|
|
325
|
+
> = new Map();
|
|
326
|
+
|
|
327
|
+
/** The keys of `mut_buckets`, ascending. */
|
|
328
|
+
let mut_depths: readonly number[] = [];
|
|
291
329
|
|
|
292
|
-
|
|
330
|
+
/** `undefined` while stale. */
|
|
331
|
+
let mut_propagationOrder: readonly ChildObservable<unknown>[] | undefined =
|
|
332
|
+
[];
|
|
293
333
|
|
|
294
334
|
const addDescendant = <B,>(child: ChildObservable<B>): void => {
|
|
295
|
-
|
|
296
|
-
|
|
335
|
+
const mut_bucket = mut_buckets.get(child.depth);
|
|
336
|
+
|
|
337
|
+
if (mut_bucket === undefined) {
|
|
338
|
+
mut_buckets.set(child.depth, new Set<ChildObservable<unknown>>([child]));
|
|
339
|
+
|
|
340
|
+
mut_depths = Arr.toInserted(
|
|
341
|
+
mut_depths,
|
|
342
|
+
binarySearch(mut_depths, child.depth),
|
|
343
|
+
child.depth,
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
if (mut_bucket.has(child)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
mut_bucket.add(child);
|
|
297
351
|
}
|
|
298
352
|
|
|
299
|
-
|
|
353
|
+
mut_propagationOrder = undefined;
|
|
354
|
+
};
|
|
300
355
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
);
|
|
356
|
+
const deleteDescendant = <B,>(child: ChildObservable<B>): void => {
|
|
357
|
+
// An emptied bucket stays, so that its depth is not inserted again.
|
|
358
|
+
const mut_bucket = mut_buckets.get(child.depth);
|
|
305
359
|
|
|
306
|
-
|
|
307
|
-
mut_propagationOrder
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
360
|
+
if (mut_bucket?.delete(child) === true) {
|
|
361
|
+
mut_propagationOrder = undefined;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const getPropagationOrder = (): readonly ChildObservable<unknown>[] => {
|
|
366
|
+
if (mut_propagationOrder !== undefined) {
|
|
367
|
+
return mut_propagationOrder;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const mut_order: ChildObservable<unknown>[] = [];
|
|
371
|
+
|
|
372
|
+
for (const depth of mut_depths) {
|
|
373
|
+
const bucket = mut_buckets.get(depth);
|
|
374
|
+
|
|
375
|
+
if (bucket !== undefined) {
|
|
376
|
+
// Not `push(...bucket)`: a spread passes every element as an argument,
|
|
377
|
+
// which overflows the stack for a bucket of a few hundred thousand.
|
|
378
|
+
for (const c of bucket) {
|
|
379
|
+
mut_order.push(c);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
mut_propagationOrder = mut_order;
|
|
385
|
+
|
|
386
|
+
return mut_order;
|
|
311
387
|
};
|
|
312
388
|
|
|
313
389
|
const startUpdate = (nextValue: A): void => {
|
|
@@ -315,12 +391,12 @@ export const createManagerObservableParts = <A,>(
|
|
|
315
391
|
|
|
316
392
|
handle.setNext(nextValue, updateToken);
|
|
317
393
|
|
|
318
|
-
for (const p of
|
|
394
|
+
for (const p of getPropagationOrder()) {
|
|
319
395
|
p.tryUpdate(updateToken);
|
|
320
396
|
}
|
|
321
397
|
};
|
|
322
398
|
|
|
323
|
-
return { addDescendant, startUpdate };
|
|
399
|
+
return { addDescendant, deleteDescendant, startUpdate };
|
|
324
400
|
};
|
|
325
401
|
|
|
326
402
|
/** Default `tryUpdate` for observables that never receive parent updates. */
|
|
@@ -389,12 +465,14 @@ export const assembleObservable = <
|
|
|
389
465
|
operator: WithInitialValueOperator<A, B>,
|
|
390
466
|
): InitializedObservable<B>;
|
|
391
467
|
|
|
468
|
+
// A `WithInitialValueOperator` is also an `Operator`: nothing at run time
|
|
469
|
+
// tells the two apart, so this set only refines the return type (#1952).
|
|
470
|
+
// @sumi-expect-error functions/no-refinement-overload
|
|
392
471
|
function pipe<B>(operator: Operator<A, B>): Observable<B>;
|
|
393
472
|
|
|
394
473
|
function pipe<B>(operator: Operator<A, B>): Observable<B> {
|
|
395
474
|
return operator(
|
|
396
475
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
397
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
398
476
|
mut_observable as unknown as InitializedObservable<A>,
|
|
399
477
|
);
|
|
400
478
|
}
|
|
@@ -415,6 +493,8 @@ export const assembleObservable = <
|
|
|
415
493
|
|
|
416
494
|
addChild: handle.addChild,
|
|
417
495
|
|
|
496
|
+
deleteChild: handle.deleteChild,
|
|
497
|
+
|
|
418
498
|
getSnapshot: handle.getSnapshot,
|
|
419
499
|
|
|
420
500
|
isCompleted: false,
|
|
@@ -59,7 +59,8 @@ export const createRootObservable = <
|
|
|
59
59
|
): Extension & RootObservable<A> => {
|
|
60
60
|
const handle = createObservableBaseHandle<A>(initialValue);
|
|
61
61
|
|
|
62
|
-
const { addDescendant, startUpdate } =
|
|
62
|
+
const { addDescendant, deleteDescendant, startUpdate } =
|
|
63
|
+
createManagerObservableParts(handle);
|
|
63
64
|
|
|
64
65
|
const complete = (): void => {
|
|
65
66
|
onComplete?.();
|
|
@@ -89,6 +90,7 @@ export const createRootObservable = <
|
|
|
89
90
|
extra: {
|
|
90
91
|
...extension,
|
|
91
92
|
addDescendant,
|
|
93
|
+
deleteDescendant,
|
|
92
94
|
},
|
|
93
95
|
});
|
|
94
96
|
};
|
|
@@ -75,7 +75,6 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
|
|
|
75
75
|
parents: OS,
|
|
76
76
|
): CombineObservableRefined<OS> =>
|
|
77
77
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
78
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
79
78
|
createCombineObservable(parents) as unknown as CombineObservableRefined<OS>;
|
|
80
79
|
|
|
81
80
|
/**
|
|
@@ -66,7 +66,6 @@ export const merge = <const OS extends NonEmptyTuple<Observable<unknown>>>(
|
|
|
66
66
|
parents: OS,
|
|
67
67
|
): MergeObservableRefined<OS> =>
|
|
68
68
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
69
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
70
69
|
createMergeObservable(parents) as MergeObservableRefined<OS>;
|
|
71
70
|
|
|
72
71
|
const createMergeObservable = <const P extends NonEmptyUnknownList>(
|
|
@@ -90,7 +89,6 @@ const createMergeObservable = <const P extends NonEmptyUnknownList>(
|
|
|
90
89
|
|
|
91
90
|
const nextValue =
|
|
92
91
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
93
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
94
92
|
Optional.unwrap(parentToUse.getSnapshot()) as ArrayElement<P>;
|
|
95
93
|
|
|
96
94
|
setNext(nextValue, updateToken);
|
package/src/core/combine/zip.mts
CHANGED
|
@@ -68,7 +68,6 @@ export const zip = <const OS extends NonEmptyTuple<Observable<unknown>>>(
|
|
|
68
68
|
parents: OS,
|
|
69
69
|
): ZipObservableRefined<OS> =>
|
|
70
70
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
71
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
72
71
|
createZipObservable(parents) as unknown as ZipObservableRefined<OS>;
|
|
73
72
|
|
|
74
73
|
const createZipObservable = <const A extends NonEmptyUnknownList>(
|
|
@@ -119,7 +118,6 @@ const createZipObservable = <const A extends NonEmptyUnknownList>(
|
|
|
119
118
|
// `Arr.map` has a single result element type, so it reports
|
|
120
119
|
// `A[number] | undefined` per position rather than `A[P]`.
|
|
121
120
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
122
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
123
121
|
Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
|
|
124
122
|
|
|
125
123
|
setNext(nextValue, updateToken);
|
|
@@ -4,10 +4,17 @@ import type { CounterObservable, TimerId } from '../types/index.mjs';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Creates an observable that emits incremental numbers at a specified interval.
|
|
7
|
-
*
|
|
7
|
+
* Emits 0 in the macrotask right after `start()` (which runs at creation
|
|
8
|
+
* unless `startManually` is set), then 1, 2, 3, ... every interval. A
|
|
9
|
+
* subscriber added after that 0 has been emitted does not receive it.
|
|
8
10
|
*
|
|
9
11
|
* @param intervalMilliSeconds - The interval duration in milliseconds
|
|
10
|
-
* @param
|
|
12
|
+
* @param options - Optional settings
|
|
13
|
+
* @param options.startManually - If true, waits for `start()` instead of
|
|
14
|
+
* starting at creation (default: false)
|
|
15
|
+
* @param options.emitInitial - If false, skips the 0 emitted right after
|
|
16
|
+
* `start()`, so the first value is 1, one interval later — like RxJS
|
|
17
|
+
* `interval` rather than `timer(0, ms)` (default: true)
|
|
11
18
|
* @returns An observable that emits sequential numbers
|
|
12
19
|
*
|
|
13
20
|
* @example
|
|
@@ -49,6 +56,7 @@ export const counter = (
|
|
|
49
56
|
intervalMilliSeconds: number,
|
|
50
57
|
options?: Readonly<{
|
|
51
58
|
startManually?: boolean;
|
|
59
|
+
emitInitial?: boolean;
|
|
52
60
|
}>,
|
|
53
61
|
): CounterObservable => {
|
|
54
62
|
let mut_counter: SafeUint = asSafeUint(0);
|
|
@@ -60,13 +68,13 @@ export const counter = (
|
|
|
60
68
|
let mut_isStarted = false;
|
|
61
69
|
|
|
62
70
|
const resetTimer = (): void => {
|
|
63
|
-
if (mut_timerId0
|
|
64
|
-
|
|
71
|
+
if (mut_timerId0 !== undefined) {
|
|
72
|
+
clearTimeout(mut_timerId0);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
if (mut_timerId !== undefined) {
|
|
76
|
+
clearInterval(mut_timerId);
|
|
77
|
+
}
|
|
70
78
|
};
|
|
71
79
|
|
|
72
80
|
const observable = createRootObservable<
|
|
@@ -90,9 +98,11 @@ export const counter = (
|
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
if (options?.emitInitial ?? true) {
|
|
102
|
+
mut_timerId0 = setTimeout(() => {
|
|
103
|
+
startUpdate(mut_counter);
|
|
104
|
+
}, 0);
|
|
105
|
+
}
|
|
96
106
|
|
|
97
107
|
mut_timerId = setInterval(() => {
|
|
98
108
|
mut_counter = SafeUint.add(1, mut_counter);
|