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.
Files changed (101) hide show
  1. package/README.md +60 -2
  2. package/dist/core/base/create-child-observable.d.mts.map +1 -1
  3. package/dist/core/base/create-child-observable.mjs +60 -3
  4. package/dist/core/base/create-child-observable.mjs.map +1 -1
  5. package/dist/core/base/create-observable-base.d.mts +21 -4
  6. package/dist/core/base/create-observable-base.d.mts.map +1 -1
  7. package/dist/core/base/create-observable-base.mjs +69 -13
  8. package/dist/core/base/create-observable-base.mjs.map +1 -1
  9. package/dist/core/base/create-root-observable.d.mts.map +1 -1
  10. package/dist/core/base/create-root-observable.mjs +2 -1
  11. package/dist/core/base/create-root-observable.mjs.map +1 -1
  12. package/dist/core/combine/combine.d.mts.map +1 -1
  13. package/dist/core/combine/combine.mjs +0 -1
  14. package/dist/core/combine/combine.mjs.map +1 -1
  15. package/dist/core/combine/merge.d.mts.map +1 -1
  16. package/dist/core/combine/merge.mjs +0 -2
  17. package/dist/core/combine/merge.mjs.map +1 -1
  18. package/dist/core/combine/zip.d.mts.map +1 -1
  19. package/dist/core/combine/zip.mjs +0 -2
  20. package/dist/core/combine/zip.mjs.map +1 -1
  21. package/dist/core/create/counter.d.mts +10 -2
  22. package/dist/core/create/counter.d.mts.map +1 -1
  23. package/dist/core/create/counter.mjs +17 -7
  24. package/dist/core/create/counter.mjs.map +1 -1
  25. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  26. package/dist/core/create/from-abortable-promise.mjs +0 -2
  27. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  28. package/dist/core/create/from-promise.d.mts.map +1 -1
  29. package/dist/core/create/from-promise.mjs +0 -1
  30. package/dist/core/create/from-promise.mjs.map +1 -1
  31. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  32. package/dist/core/create/from-subscribable.mjs +0 -1
  33. package/dist/core/create/from-subscribable.mjs.map +1 -1
  34. package/dist/core/create/just.d.mts.map +1 -1
  35. package/dist/core/create/just.mjs +0 -1
  36. package/dist/core/create/just.mjs.map +1 -1
  37. package/dist/core/operators/audit.d.mts.map +1 -1
  38. package/dist/core/operators/audit.mjs +0 -1
  39. package/dist/core/operators/audit.mjs.map +1 -1
  40. package/dist/core/operators/debounce.d.mts.map +1 -1
  41. package/dist/core/operators/debounce.mjs +0 -1
  42. package/dist/core/operators/debounce.mjs.map +1 -1
  43. package/dist/core/operators/filter.d.mts.map +1 -1
  44. package/dist/core/operators/filter.mjs +19 -0
  45. package/dist/core/operators/filter.mjs.map +1 -1
  46. package/dist/core/operators/map.d.mts.map +1 -1
  47. package/dist/core/operators/map.mjs +0 -1
  48. package/dist/core/operators/map.mjs.map +1 -1
  49. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  50. package/dist/core/operators/skip-if-no-change.mjs +0 -1
  51. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  52. package/dist/core/operators/take-until.d.mts.map +1 -1
  53. package/dist/core/operators/take-until.mjs +0 -1
  54. package/dist/core/operators/take-until.mjs.map +1 -1
  55. package/dist/core/operators/throttle.d.mts.map +1 -1
  56. package/dist/core/operators/throttle.mjs +0 -1
  57. package/dist/core/operators/throttle.mjs.map +1 -1
  58. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  59. package/dist/core/operators/with-buffered-from.mjs +0 -1
  60. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  61. package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -1
  62. package/dist/core/predefined/operators/unwrap-optional.mjs +0 -1
  63. package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -1
  64. package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -1
  65. package/dist/core/predefined/operators/unwrap-result-ok.mjs +0 -1
  66. package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -1
  67. package/dist/core/types/observable.d.mts +2 -0
  68. package/dist/core/types/observable.d.mts.map +1 -1
  69. package/dist/core/types/observable.mjs.map +1 -1
  70. package/dist/core/utils/id-maker.d.mts.map +1 -1
  71. package/dist/core/utils/id-maker.mjs +0 -1
  72. package/dist/core/utils/id-maker.mjs.map +1 -1
  73. package/dist/core/utils/utils.d.mts.map +1 -1
  74. package/dist/core/utils/utils.mjs +0 -1
  75. package/dist/core/utils/utils.mjs.map +1 -1
  76. package/package.json +14 -14
  77. package/src/core/base/create-child-observable.mts +78 -3
  78. package/src/core/base/create-observable-base.mts +111 -31
  79. package/src/core/base/create-root-observable.mts +3 -1
  80. package/src/core/combine/combine.mts +0 -1
  81. package/src/core/combine/merge.mts +0 -2
  82. package/src/core/combine/zip.mts +0 -2
  83. package/src/core/create/counter.mts +20 -10
  84. package/src/core/create/counter.test.mts +183 -0
  85. package/src/core/create/from-abortable-promise.mts +0 -2
  86. package/src/core/create/from-promise.mts +0 -1
  87. package/src/core/create/from-subscribable.mts +0 -1
  88. package/src/core/create/just.mts +0 -1
  89. package/src/core/operators/audit.mts +0 -1
  90. package/src/core/operators/debounce.mts +0 -1
  91. package/src/core/operators/filter.mts +41 -0
  92. package/src/core/operators/map.mts +0 -1
  93. package/src/core/operators/skip-if-no-change.mts +0 -1
  94. package/src/core/operators/take-until.mts +0 -1
  95. package/src/core/operators/throttle.mts +0 -1
  96. package/src/core/operators/with-buffered-from.mts +0 -1
  97. package/src/core/predefined/operators/unwrap-optional.mts +0 -1
  98. package/src/core/predefined/operators/unwrap-result-ok.mts +0 -1
  99. package/src/core/types/observable.mts +2 -0
  100. package/src/core/utils/id-maker.mts +0 -1
  101. 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.0",
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.12.0",
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.2",
40
+ "@vitest/browser-playwright": "5.0.3",
41
41
  "argparse": "3.0.2",
42
- "eslint-config-typed": "5.17.0",
43
- "eslint-plugin-ts-data-forge": "0.8.3",
44
- "eslint-plugin-ts-fortress": "1.3.0",
45
- "eslint-plugin-ts-type-forge": "0.8.5",
46
- "jotai": "3.0.0",
47
- "mobx": "7.0.5",
48
- "playwright": "1.63.0",
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.0",
55
- "synstate-react-hooks-compat": "3.1.0",
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 } = createManagerObservableParts(handle);
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
- let mut_children: readonly ChildObservable<unknown>[] = [];
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(Arr.isNonEmpty(mut_children));
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 = Arr.toPushed(
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
- mut_children.some((c) => !c.isCompleted);
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
- // propagate to children
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 descendant set and a depth-ordered
283
- * propagation list; this is the single implementation of that pair (the class
284
- * version duplicated it across `RootObservableClass` and
285
- * `AsyncChildObservableClass`).
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
- let mut_propagationOrder: readonly ChildObservable<unknown>[] = [];
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
- const mut_descendantsIdSet: MutableSet<ObservableId> = new Set();
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
- if (mut_descendantsIdSet.has(child.id)) {
296
- return;
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
- mut_descendantsIdSet.add(child.id);
353
+ mut_propagationOrder = undefined;
354
+ };
300
355
 
301
- const insertPos = binarySearch(
302
- mut_propagationOrder.map((a) => a.depth),
303
- child.depth,
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
- mut_propagationOrder = Arr.toInserted(
307
- mut_propagationOrder,
308
- insertPos,
309
- child,
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 mut_propagationOrder) {
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 } = createManagerObservableParts(handle);
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);
@@ -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
- * Starts with 0 immediately after subscription, then emits 1, 2, 3, ... every interval.
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 startManually - If true, waits for manual start (default: false)
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 === undefined || mut_timerId === undefined) {
64
- return;
71
+ if (mut_timerId0 !== undefined) {
72
+ clearTimeout(mut_timerId0);
65
73
  }
66
74
 
67
- clearInterval(mut_timerId0);
68
-
69
- clearInterval(mut_timerId);
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
- mut_timerId0 = setTimeout(() => {
94
- startUpdate(mut_counter);
95
- }, 0);
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);