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.
Files changed (99) hide show
  1. package/README.md +1 -1
  2. package/dist/core/base/create-child-observable.d.mts.map +1 -1
  3. package/dist/core/base/create-child-observable.mjs +7 -3
  4. package/dist/core/base/create-child-observable.mjs.map +1 -1
  5. package/dist/core/base/create-observable-base.d.mts +53 -7
  6. package/dist/core/base/create-observable-base.d.mts.map +1 -1
  7. package/dist/core/base/create-observable-base.mjs +53 -19
  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.map +1 -1
  11. package/dist/core/combine/combine.d.mts.map +1 -1
  12. package/dist/core/combine/combine.mjs +7 -4
  13. package/dist/core/combine/combine.mjs.map +1 -1
  14. package/dist/core/combine/merge.d.mts.map +1 -1
  15. package/dist/core/combine/merge.mjs +4 -1
  16. package/dist/core/combine/merge.mjs.map +1 -1
  17. package/dist/core/combine/zip.d.mts.map +1 -1
  18. package/dist/core/combine/zip.mjs +8 -5
  19. package/dist/core/combine/zip.mjs.map +1 -1
  20. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  21. package/dist/core/create/from-abortable-promise.mjs +6 -2
  22. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  23. package/dist/core/create/from-promise.d.mts.map +1 -1
  24. package/dist/core/create/from-promise.mjs +5 -2
  25. package/dist/core/create/from-promise.mjs.map +1 -1
  26. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  27. package/dist/core/create/from-subscribable.mjs +1 -0
  28. package/dist/core/create/from-subscribable.mjs.map +1 -1
  29. package/dist/core/create/just.d.mts.map +1 -1
  30. package/dist/core/create/just.mjs +1 -0
  31. package/dist/core/create/just.mjs.map +1 -1
  32. package/dist/core/create/source.mjs +2 -1
  33. package/dist/core/create/source.mjs.map +1 -1
  34. package/dist/core/operators/audit.d.mts.map +1 -1
  35. package/dist/core/operators/audit.mjs +3 -1
  36. package/dist/core/operators/audit.mjs.map +1 -1
  37. package/dist/core/operators/debounce.d.mts.map +1 -1
  38. package/dist/core/operators/debounce.mjs +3 -1
  39. package/dist/core/operators/debounce.mjs.map +1 -1
  40. package/dist/core/operators/map.d.mts.map +1 -1
  41. package/dist/core/operators/map.mjs +1 -0
  42. package/dist/core/operators/map.mjs.map +1 -1
  43. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  44. package/dist/core/operators/skip-if-no-change.mjs +1 -0
  45. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  46. package/dist/core/operators/take-until.d.mts.map +1 -1
  47. package/dist/core/operators/take-until.mjs +1 -0
  48. package/dist/core/operators/take-until.mjs.map +1 -1
  49. package/dist/core/operators/throttle.d.mts.map +1 -1
  50. package/dist/core/operators/throttle.mjs +1 -0
  51. package/dist/core/operators/throttle.mjs.map +1 -1
  52. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  53. package/dist/core/operators/with-buffered-from.mjs +1 -0
  54. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  55. package/dist/core/operators/with-current-value-from.mjs +2 -1
  56. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  57. package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -1
  58. package/dist/core/predefined/operators/unwrap-optional.mjs +1 -0
  59. package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -1
  60. package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -1
  61. package/dist/core/predefined/operators/unwrap-result-ok.mjs +1 -0
  62. package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -1
  63. package/dist/core/types/observable.d.mts +1 -1
  64. package/dist/core/types/observable.d.mts.map +1 -1
  65. package/dist/core/types/observable.mjs +5 -0
  66. package/dist/core/types/observable.mjs.map +1 -1
  67. package/dist/core/utils/id-maker.d.mts.map +1 -1
  68. package/dist/core/utils/id-maker.mjs +1 -0
  69. package/dist/core/utils/id-maker.mjs.map +1 -1
  70. package/dist/core/utils/utils.d.mts.map +1 -1
  71. package/dist/core/utils/utils.mjs +5 -2
  72. package/dist/core/utils/utils.mjs.map +1 -1
  73. package/package.json +36 -38
  74. package/src/core/base/circular-dependency-comparison.test.mts +5 -6
  75. package/src/core/base/circular-dependency.test.mts +20 -21
  76. package/src/core/base/create-child-observable.mts +10 -3
  77. package/src/core/base/create-observable-base.mts +129 -30
  78. package/src/core/base/create-root-observable.mts +2 -0
  79. package/src/core/combine/combine.mts +13 -8
  80. package/src/core/combine/merge.mts +5 -1
  81. package/src/core/combine/zip.mts +12 -8
  82. package/src/core/create/from-abortable-promise.mts +8 -2
  83. package/src/core/create/from-promise.mts +7 -2
  84. package/src/core/create/from-subscribable.mts +1 -0
  85. package/src/core/create/just.mts +1 -0
  86. package/src/core/create/source.mts +3 -1
  87. package/src/core/operators/audit.mts +4 -1
  88. package/src/core/operators/debounce.mts +4 -1
  89. package/src/core/operators/map.mts +1 -0
  90. package/src/core/operators/skip-if-no-change.mts +1 -0
  91. package/src/core/operators/take-until.mts +1 -0
  92. package/src/core/operators/throttle.mts +1 -0
  93. package/src/core/operators/with-buffered-from.mts +1 -0
  94. package/src/core/operators/with-current-value-from.mts +3 -1
  95. package/src/core/predefined/operators/unwrap-optional.mts +1 -0
  96. package/src/core/predefined/operators/unwrap-result-ok.mts +1 -0
  97. package/src/core/types/observable.mts +11 -1
  98. package/src/core/utils/id-maker.mts +1 -0
  99. 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 childA = createSyncChildObservable(
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: [childA],
38
+ parents: [mut_childA],
40
39
  initialValue: Optional.some(0),
41
40
  },
42
41
  createTryUpdateNotImplemented,
43
42
  );
44
43
 
45
- // Simulate a cycle: childA → childB → childA
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(childA, 'parents', {
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: [childA],
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 childA = createSyncChildObservable(
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: [childA],
30
+ parents: [mut_childA],
32
31
  initialValue: Optional.some(0),
33
32
  },
34
33
  createTryUpdateNotImplemented,
35
34
  );
36
35
 
37
- // Mutate childA.parents to create a cycle: childA -> childB -> childA
38
- Object.defineProperty(childA, 'parents', {
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: [childA],
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 childA = createAsyncChildObservable(
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: [childA],
72
+ parents: [mut_childA],
74
73
  initialValue: Optional.some(0),
75
74
  },
76
75
  createTryUpdateNotImplemented,
77
76
  );
78
77
 
79
- // Create cycle: childA -> childB -> childA
80
- Object.defineProperty(childA, 'parents', {
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: [childA],
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 childA = createSyncChildObservable(
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: [childA],
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: childA -> childC -> childB -> childA
130
- Object.defineProperty(childA, 'parents', {
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: [childA],
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 childA = createSyncChildObservable(
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 childA's parents reference childA itself (self-loop)
164
- Object.defineProperty(childA, 'parents', {
165
- value: [childA],
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: [childA],
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)) return true;
244
+ if (mut_inPath.has(node.id)) {
245
+ return true;
246
+ }
244
247
 
245
- if (mut_visited.has(node.id)) return false;
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) break;
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) return; // terminate only once
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)) return;
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 public getters below must not
282
- * be copied by a spread (spreading would freeze their current values).
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 getters
291
- * delegating to the handle, and the kind-specific behavior (`tryUpdate`,
292
- * `tryComplete`, `complete`) is passed in already composed.
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<{ kind: Kind }> => {
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
- observable as unknown as InitializedObservable<A>,
402
+ mut_observable as unknown as InitializedObservable<A>,
319
403
  );
320
404
  }
321
405
 
322
- const observable = {
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
- get isCompleted(): boolean {
336
- return handle.isCompleted();
337
- },
424
+ isCompleted: false,
338
425
 
339
- get updateToken(): UpdateToken {
340
- return handle.updateToken();
341
- },
426
+ updateToken: handle.updateToken(),
342
427
 
343
- get hasSubscriber(): boolean {
344
- return handle.hasSubscriber();
345
- },
428
+ hasSubscriber: false,
346
429
 
347
- get hasChild(): boolean {
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
- } as const;
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 observable;
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)) return; // all parents are skipped
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
- const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
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) return;
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
 
@@ -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.every((list) => !list.isEmpty)) {
114
- const nextValue =
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()) return;
91
+ if (isCompleted()) {
92
+ return;
93
+ }
91
94
 
92
95
  startUpdate(Result.ok(value));
93
96
  })
94
97
  .catch((error: unknown) => {
95
- if (isCompleted()) return;
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
  ),