@zeix/cause-effect 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
@@ -0,0 +1,139 @@
1
+ import {
2
+ validateCallback,
3
+ validateReadValue,
4
+ validateSignalValue,
5
+ } from '../errors'
6
+ import {
7
+ activeSink,
8
+ type Cleanup,
9
+ type ComputedOptions,
10
+ defaultEquals,
11
+ link,
12
+ type StateNode,
13
+ setState,
14
+ TYPE_SENSOR,
15
+ } from '../graph'
16
+ import { isObjectOfType, isSyncFunction } from '../util'
17
+
18
+ /* === Types === */
19
+
20
+ /**
21
+ * A read-only signal that tracks external input and updates a state value as long as it is active.
22
+ *
23
+ * @template T - The type of value produced by the sensor
24
+ */
25
+ type Sensor<T extends {}> = {
26
+ readonly [Symbol.toStringTag]: 'Sensor'
27
+
28
+ /**
29
+ * Gets the current value of the sensor.
30
+ * Updates its state value if the sensor is active.
31
+ * When called inside another reactive context, creates a dependency.
32
+ * @returns The sensor value
33
+ * @throws UnsetSignalValueError If the sensor value is still unset when read.
34
+ */
35
+ get(): T
36
+ }
37
+
38
+ /**
39
+ * A callback function for sensors when the sensor starts being watched.
40
+ *
41
+ * @template T - The type of value observed
42
+ * @param set - A function to set the observed value
43
+ * @returns A cleanup function when the sensor stops being watched
44
+ */
45
+ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
46
+
47
+ /* === Exported Functions === */
48
+
49
+ /**
50
+ * Creates a sensor that tracks external input and updates a state value as long as it is active.
51
+ * Sensors get activated when they are first accessed by an effect and deactivated when they are
52
+ * no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
53
+ *
54
+ * @since 0.18.0
55
+ * @template T - The type of value stored in the state
56
+ * @param start - The callback function that starts the sensor and returns a cleanup function.
57
+ * @param options - Optional options for the sensor.
58
+ * @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
59
+ * before the start callback fires. Essential for the mutable-object observation pattern.
60
+ * @param options.equals - Optional equality function. Defaults to `Object.is`. Use `SKIP_EQUALITY`
61
+ * for mutable objects where the reference stays the same but internal state changes.
62
+ * @param options.guard - Optional type guard to validate values.
63
+ * @returns A read-only sensor signal.
64
+ *
65
+ * @example Tracking external values
66
+ * ```ts
67
+ * const mousePos = createSensor<{ x: number; y: number }>((set) => {
68
+ * const handler = (e: MouseEvent) => {
69
+ * set({ x: e.clientX, y: e.clientY });
70
+ * };
71
+ * window.addEventListener('mousemove', handler);
72
+ * return () => window.removeEventListener('mousemove', handler);
73
+ * });
74
+ * ```
75
+ *
76
+ * @example Observing a mutable object
77
+ * ```ts
78
+ * import { createSensor, SKIP_EQUALITY } from 'cause-effect';
79
+ *
80
+ * const el = createSensor<HTMLElement>((set) => {
81
+ * const node = document.getElementById('box')!;
82
+ * set(node);
83
+ * const obs = new MutationObserver(() => set(node));
84
+ * obs.observe(node, { attributes: true });
85
+ * return () => obs.disconnect();
86
+ * }, { value: node, equals: SKIP_EQUALITY });
87
+ * ```
88
+ */
89
+ function createSensor<T extends {}>(
90
+ start: SensorCallback<T>,
91
+ options?: ComputedOptions<T>,
92
+ ): Sensor<T> {
93
+ validateCallback(TYPE_SENSOR, start, isSyncFunction)
94
+ if (options?.value !== undefined)
95
+ validateSignalValue(TYPE_SENSOR, options.value, options?.guard)
96
+
97
+ const node: StateNode<T> = {
98
+ value: options?.value as T,
99
+ sinks: null,
100
+ sinksTail: null,
101
+ equals: options?.equals ?? defaultEquals,
102
+ guard: options?.guard,
103
+ stop: undefined,
104
+ }
105
+
106
+ return {
107
+ [Symbol.toStringTag]: TYPE_SENSOR,
108
+ get(): T {
109
+ if (activeSink) {
110
+ // Start fires before link: synchronous set() inside start updates
111
+ // node.value without propagation (no sinks yet). The activating
112
+ // effect reads the updated value directly after link.
113
+ if (!node.sinks)
114
+ node.stop = start((next: T): void => {
115
+ validateSignalValue(TYPE_SENSOR, next, node.guard)
116
+ setState(node, next)
117
+ })
118
+ link(node, activeSink)
119
+ }
120
+ validateReadValue(TYPE_SENSOR, node.value)
121
+ return node.value
122
+ },
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Checks if a value is a Sensor signal.
128
+ *
129
+ * @since 0.18.0
130
+ * @param value - The value to check
131
+ * @returns True if the value is a Sensor
132
+ */
133
+ function isSensor<T extends {} = unknown & {}>(
134
+ value: unknown,
135
+ ): value is Sensor<T> {
136
+ return isObjectOfType(value, TYPE_SENSOR)
137
+ }
138
+
139
+ export { createSensor, isSensor, type Sensor, type SensorCallback }
@@ -0,0 +1,135 @@
1
+ import { validateCallback, validateSignalValue } from '../errors'
2
+ import {
3
+ activeSink,
4
+ defaultEquals,
5
+ link,
6
+ type SignalOptions,
7
+ type StateNode,
8
+ setState,
9
+ TYPE_STATE,
10
+ } from '../graph'
11
+ import { isObjectOfType } from '../util'
12
+
13
+ /* === Types === */
14
+
15
+ /**
16
+ * A callback function for states that updates a value based on the previous value.
17
+ *
18
+ * @template T - The type of value
19
+ * @param prev - The previous state value
20
+ * @returns The new state value
21
+ */
22
+ type UpdateCallback<T extends {}> = (prev: T) => T
23
+
24
+ /**
25
+ * A mutable reactive state container.
26
+ * Changes to the state will automatically propagate to dependent computations and effects.
27
+ *
28
+ * @template T - The type of value stored in the state
29
+ */
30
+ type State<T extends {}> = {
31
+ readonly [Symbol.toStringTag]: 'State'
32
+
33
+ /**
34
+ * Gets the current value of the state.
35
+ * When called inside a memo, task, or effect, creates a dependency.
36
+ * @returns The current value
37
+ */
38
+ get(): T
39
+
40
+ /**
41
+ * Sets a new value for the state.
42
+ * If the new value is different (according to the equality function), all dependents will be notified.
43
+ * @param next - The new value to set
44
+ */
45
+ set(next: T): void
46
+
47
+ /**
48
+ * Updates the state with a new value computed by a callback function.
49
+ * The callback receives the current value as an argument.
50
+ * @param fn - The callback function to compute the new value
51
+ */
52
+ update(fn: UpdateCallback<T>): void
53
+ }
54
+
55
+ /* === Exported Functions === */
56
+
57
+ /**
58
+ * Creates a mutable reactive state container.
59
+ *
60
+ * @since 0.9.0
61
+ * @template T - The type of value stored in the state
62
+ * @param value - The initial value
63
+ * @param options - Optional configuration for the state
64
+ * @returns A State object with get() and set() methods
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const count = createState(0);
69
+ * count.set(1);
70
+ * console.log(count.get()); // 1
71
+ * ```
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // With type guard
76
+ * const count = createState(0, {
77
+ * guard: (v): v is number => typeof v === 'number'
78
+ * });
79
+ * ```
80
+ */
81
+ function createState<T extends {}>(
82
+ value: T,
83
+ options?: SignalOptions<T>,
84
+ ): State<T> {
85
+ validateSignalValue(TYPE_STATE, value, options?.guard)
86
+
87
+ const node: StateNode<T> = {
88
+ value,
89
+ sinks: null,
90
+ sinksTail: null,
91
+ equals: options?.equals ?? defaultEquals,
92
+ guard: options?.guard,
93
+ }
94
+
95
+ return {
96
+ [Symbol.toStringTag]: TYPE_STATE,
97
+ get(): T {
98
+ if (activeSink) link(node, activeSink)
99
+ return node.value
100
+ },
101
+ set(next: T): void {
102
+ validateSignalValue(TYPE_STATE, next, node.guard)
103
+ setState(node, next)
104
+ },
105
+ update(fn: UpdateCallback<T>): void {
106
+ validateCallback(TYPE_STATE, fn)
107
+ const next = fn(node.value)
108
+ validateSignalValue(TYPE_STATE, next, node.guard)
109
+ setState(node, next)
110
+ },
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Checks if a value is a State signal.
116
+ *
117
+ * @since 0.9.0
118
+ * @param value - The value to check
119
+ * @returns True if the value is a State
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const state = createState(0);
124
+ * if (isState(state)) {
125
+ * state.set(1); // TypeScript knows state has set()
126
+ * }
127
+ * ```
128
+ */
129
+ function isState<T extends {} = unknown & {}>(
130
+ value: unknown,
131
+ ): value is State<T> {
132
+ return isObjectOfType(value, TYPE_STATE)
133
+ }
134
+
135
+ export { createState, isState, type State, type UpdateCallback }
@@ -0,0 +1,383 @@
1
+ import { DuplicateKeyError, validateSignalValue } from '../errors'
2
+ import {
3
+ activeSink,
4
+ batch,
5
+ batchDepth,
6
+ type Cleanup,
7
+ FLAG_CLEAN,
8
+ FLAG_DIRTY,
9
+ flush,
10
+ link,
11
+ type MemoNode,
12
+ propagate,
13
+ refresh,
14
+ type SinkNode,
15
+ TYPE_STORE,
16
+ untrack,
17
+ } from '../graph'
18
+ import { isFunction, isObjectOfType, isRecord } from '../util'
19
+ import {
20
+ createList,
21
+ type DiffResult,
22
+ isEqual,
23
+ type List,
24
+ type UnknownRecord,
25
+ } from './list'
26
+ import { createState, type State } from './state'
27
+
28
+ /* === Types === */
29
+
30
+ type StoreOptions = {
31
+ watched?: () => Cleanup
32
+ }
33
+
34
+ type BaseStore<T extends UnknownRecord> = {
35
+ readonly [Symbol.toStringTag]: 'Store'
36
+ readonly [Symbol.isConcatSpreadable]: false
37
+ [Symbol.iterator](): IterableIterator<
38
+ [
39
+ string,
40
+ State<T[keyof T] & {}> | Store<UnknownRecord> | List<unknown & {}>,
41
+ ]
42
+ >
43
+ keys(): IterableIterator<string>
44
+ byKey<K extends keyof T & string>(
45
+ key: K,
46
+ ): T[K] extends readonly (infer U extends {})[]
47
+ ? List<U>
48
+ : T[K] extends UnknownRecord
49
+ ? Store<T[K]>
50
+ : T[K] extends unknown & {}
51
+ ? State<T[K] & {}>
52
+ : State<T[K] & {}> | undefined
53
+ get(): T
54
+ set(newValue: T): void
55
+ update(fn: (oldValue: T) => T): void
56
+ add<K extends keyof T & string>(key: K, value: T[K]): K
57
+ remove(key: string): void
58
+ }
59
+
60
+ type Store<T extends UnknownRecord> = BaseStore<T> & {
61
+ [K in keyof T]: T[K] extends readonly (infer U extends {})[]
62
+ ? List<U>
63
+ : T[K] extends UnknownRecord
64
+ ? Store<T[K]>
65
+ : T[K] extends unknown & {}
66
+ ? State<T[K] & {}>
67
+ : State<T[K] & {}> | undefined
68
+ }
69
+
70
+ /* === Functions === */
71
+
72
+ /** Diff two records and return granular changes */
73
+ function diffRecords<T extends UnknownRecord>(
74
+ oldObj: T,
75
+ newObj: T,
76
+ ): DiffResult {
77
+ // Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
78
+ const oldValid = isRecord(oldObj) || Array.isArray(oldObj)
79
+ const newValid = isRecord(newObj) || Array.isArray(newObj)
80
+ if (!oldValid || !newValid) {
81
+ // For non-objects or non-plain objects, treat as complete change if different
82
+ const changed = !Object.is(oldObj, newObj)
83
+ return {
84
+ changed,
85
+ add: changed && newValid ? newObj : {},
86
+ change: {},
87
+ remove: changed && oldValid ? oldObj : {},
88
+ }
89
+ }
90
+
91
+ const visited = new WeakSet()
92
+
93
+ const add = {} as UnknownRecord
94
+ const change = {} as UnknownRecord
95
+ const remove = {} as UnknownRecord
96
+ let changed = false
97
+
98
+ const oldKeys = Object.keys(oldObj)
99
+ const newKeys = Object.keys(newObj)
100
+
101
+ // Pass 1: iterate new keys — find additions and changes
102
+ for (const key of newKeys) {
103
+ if (key in oldObj) {
104
+ if (!isEqual(oldObj[key], newObj[key], visited)) {
105
+ change[key] = newObj[key]
106
+ changed = true
107
+ }
108
+ } else {
109
+ add[key] = newObj[key]
110
+ changed = true
111
+ }
112
+ }
113
+
114
+ // Pass 2: iterate old keys — find removals
115
+ for (const key of oldKeys) {
116
+ if (!(key in newObj)) {
117
+ remove[key] = undefined
118
+ changed = true
119
+ }
120
+ }
121
+
122
+ return { add, change, remove, changed }
123
+ }
124
+
125
+ /**
126
+ * Creates a reactive store with deeply nested reactive properties.
127
+ * Each property becomes its own signal (State for primitives, nested Store for objects, List for arrays).
128
+ * Properties are accessible directly via proxy.
129
+ *
130
+ * @since 0.15.0
131
+ * @param initialValue - Initial object value of the store
132
+ * @param options - Optional configuration for watch lifecycle
133
+ * @returns A Store with reactive properties
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const user = createStore({ name: 'Alice', age: 30 });
138
+ * user.name.set('Bob'); // Only name subscribers react
139
+ * console.log(user.get()); // { name: 'Bob', age: 30 }
140
+ * ```
141
+ */
142
+ function createStore<T extends UnknownRecord>(
143
+ initialValue: T,
144
+ options?: StoreOptions,
145
+ ): Store<T> {
146
+ validateSignalValue(TYPE_STORE, initialValue, isRecord)
147
+
148
+ const signals = new Map<
149
+ string,
150
+ State<unknown & {}> | Store<UnknownRecord> | List<unknown & {}>
151
+ >()
152
+
153
+ // --- Internal helpers ---
154
+
155
+ const addSignal = (key: string, value: unknown): void => {
156
+ validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
157
+ if (Array.isArray(value)) signals.set(key, createList(value))
158
+ else if (isRecord(value)) signals.set(key, createStore(value))
159
+ else signals.set(key, createState(value as unknown & {}))
160
+ }
161
+
162
+ // Build current value from child signals
163
+ const buildValue = (): T => {
164
+ const record = {} as UnknownRecord
165
+ signals.forEach((signal, key) => {
166
+ record[key] = signal.get()
167
+ })
168
+ return record as T
169
+ }
170
+
171
+ // Structural tracking node — not a general-purpose Memo.
172
+ // On first get(): refresh() establishes edges from child signals.
173
+ // On subsequent get(): untrack(buildValue) rebuilds without re-linking.
174
+ // Mutation methods (add/remove/set) null out sources to force re-establishment.
175
+ const node: MemoNode<T> = {
176
+ fn: buildValue,
177
+ value: initialValue,
178
+ flags: FLAG_DIRTY,
179
+ sources: null,
180
+ sourcesTail: null,
181
+ sinks: null,
182
+ sinksTail: null,
183
+ equals: isEqual,
184
+ error: undefined,
185
+ }
186
+
187
+ const applyChanges = (changes: DiffResult): boolean => {
188
+ let structural = false
189
+
190
+ // Additions
191
+ for (const key in changes.add) {
192
+ addSignal(key, changes.add[key])
193
+ structural = true
194
+ }
195
+
196
+ // Changes
197
+ if (Object.keys(changes.change).length) {
198
+ batch(() => {
199
+ for (const key in changes.change) {
200
+ const value = changes.change[key]
201
+ validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
202
+ const signal = signals.get(key)
203
+ if (signal) {
204
+ // Type changed (e.g. primitive → object or vice versa): replace signal
205
+ if (isRecord(value) !== isStore(signal)) {
206
+ addSignal(key, value)
207
+ structural = true
208
+ } else signal.set(value as never)
209
+ }
210
+ }
211
+ })
212
+ }
213
+
214
+ // Removals
215
+ for (const key in changes.remove) {
216
+ signals.delete(key)
217
+ structural = true
218
+ }
219
+
220
+ if (structural) {
221
+ node.sources = null
222
+ node.sourcesTail = null
223
+ }
224
+
225
+ return changes.changed
226
+ }
227
+
228
+ // --- Initialize ---
229
+ for (const key of Object.keys(initialValue))
230
+ addSignal(key, initialValue[key])
231
+
232
+ // --- Store object ---
233
+ const store: BaseStore<T> = {
234
+ [Symbol.toStringTag]: TYPE_STORE,
235
+ [Symbol.isConcatSpreadable]: false as const,
236
+
237
+ *[Symbol.iterator]() {
238
+ for (const key of Array.from(signals.keys())) {
239
+ const signal = signals.get(key)
240
+ if (signal)
241
+ yield [key, signal] as [
242
+ string,
243
+ (
244
+ | State<T[keyof T] & {}>
245
+ | Store<UnknownRecord>
246
+ | List<unknown & {}>
247
+ ),
248
+ ]
249
+ }
250
+ },
251
+
252
+ keys() {
253
+ if (activeSink) {
254
+ if (!node.sinks && options?.watched)
255
+ node.stop = options.watched()
256
+ link(node, activeSink)
257
+ }
258
+ return signals.keys()
259
+ },
260
+
261
+ byKey<K extends keyof T & string>(key: K) {
262
+ return signals.get(key) as T[K] extends readonly (infer U extends
263
+ {})[]
264
+ ? List<U>
265
+ : T[K] extends UnknownRecord
266
+ ? Store<T[K]>
267
+ : T[K] extends unknown & {}
268
+ ? State<T[K] & {}>
269
+ : State<T[K] & {}> | undefined
270
+ },
271
+
272
+ get() {
273
+ if (activeSink) {
274
+ if (!node.sinks && options?.watched)
275
+ node.stop = options.watched()
276
+ link(node, activeSink)
277
+ }
278
+ if (node.sources) {
279
+ // Fast path: edges already established, rebuild value directly
280
+ // from child signals using untrack to avoid creating spurious
281
+ // edges to the current effect/memo consumer
282
+ if (node.flags) {
283
+ node.value = untrack(buildValue)
284
+ node.flags = FLAG_CLEAN
285
+ }
286
+ } else {
287
+ // First access: use refresh() to establish child → store edges
288
+ refresh(node as unknown as SinkNode)
289
+ if (node.error) throw node.error
290
+ }
291
+ return node.value
292
+ },
293
+
294
+ set(newValue: T) {
295
+ // Use cached value if clean, recompute if dirty
296
+ const currentValue =
297
+ node.flags & FLAG_DIRTY ? buildValue() : node.value
298
+
299
+ const changes = diffRecords(currentValue, newValue)
300
+ if (applyChanges(changes)) {
301
+ // Call propagate BEFORE marking dirty to ensure it doesn't early-return
302
+ propagate(node as unknown as SinkNode)
303
+ node.flags |= FLAG_DIRTY
304
+ if (batchDepth === 0) flush()
305
+ }
306
+ },
307
+
308
+ update(fn: (prev: T) => T) {
309
+ store.set(fn(store.get()))
310
+ },
311
+
312
+ add<K extends keyof T & string>(key: K, value: T[K]) {
313
+ if (signals.has(key))
314
+ throw new DuplicateKeyError(TYPE_STORE, key, value)
315
+ addSignal(key, value)
316
+ node.sources = null
317
+ node.sourcesTail = null
318
+ propagate(node as unknown as SinkNode)
319
+ node.flags |= FLAG_DIRTY
320
+ if (batchDepth === 0) flush()
321
+ return key
322
+ },
323
+
324
+ remove(key: string) {
325
+ const ok = signals.delete(key)
326
+ if (ok) {
327
+ node.sources = null
328
+ node.sourcesTail = null
329
+ propagate(node as unknown as SinkNode)
330
+ node.flags |= FLAG_DIRTY
331
+ if (batchDepth === 0) flush()
332
+ }
333
+ },
334
+ }
335
+
336
+ // --- Proxy ---
337
+ return new Proxy(store, {
338
+ get(target, prop) {
339
+ if (prop in target) {
340
+ const value = Reflect.get(target, prop)
341
+ return isFunction(value) ? value.bind(target) : value
342
+ }
343
+ if (typeof prop !== 'symbol')
344
+ return target.byKey(prop as keyof T & string)
345
+ },
346
+ has(target, prop) {
347
+ if (prop in target) return true
348
+ return target.byKey(String(prop) as keyof T & string) !== undefined
349
+ },
350
+ ownKeys(target) {
351
+ return Array.from(target.keys())
352
+ },
353
+ getOwnPropertyDescriptor(target, prop) {
354
+ if (prop in target)
355
+ return Reflect.getOwnPropertyDescriptor(target, prop)
356
+ if (typeof prop === 'symbol') return undefined
357
+ const signal = target.byKey(String(prop) as keyof T & string)
358
+ return signal
359
+ ? {
360
+ enumerable: true,
361
+ configurable: true,
362
+ writable: true,
363
+ value: signal,
364
+ }
365
+ : undefined
366
+ },
367
+ }) as Store<T>
368
+ }
369
+
370
+ /**
371
+ * Checks if a value is a Store signal.
372
+ *
373
+ * @since 0.15.0
374
+ * @param value - The value to check
375
+ * @returns True if the value is a Store
376
+ */
377
+ function isStore<T extends UnknownRecord>(value: unknown): value is Store<T> {
378
+ return isObjectOfType(value, TYPE_STORE)
379
+ }
380
+
381
+ /* === Exports === */
382
+
383
+ export { createStore, isStore, type Store, type StoreOptions, TYPE_STORE }