@zeix/cause-effect 0.17.3 → 0.18.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 (94) hide show
  1. package/.ai-context.md +169 -227
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +176 -116
  4. package/ARCHITECTURE.md +276 -0
  5. package/CHANGELOG.md +29 -0
  6. package/CLAUDE.md +201 -143
  7. package/GUIDE.md +298 -0
  8. package/README.md +246 -193
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/context7.json +4 -0
  12. package/examples/events-sensor.ts +187 -0
  13. package/examples/selector-sensor.ts +173 -0
  14. package/index.dev.js +1390 -1008
  15. package/index.js +1 -1
  16. package/index.ts +60 -74
  17. package/package.json +5 -2
  18. package/skills/changelog-keeper/SKILL.md +59 -0
  19. package/skills/changelog-keeper/agents/openai.yaml +4 -0
  20. package/src/errors.ts +118 -74
  21. package/src/graph.ts +612 -0
  22. package/src/nodes/collection.ts +512 -0
  23. package/src/nodes/effect.ts +149 -0
  24. package/src/nodes/list.ts +589 -0
  25. package/src/nodes/memo.ts +148 -0
  26. package/src/nodes/sensor.ts +149 -0
  27. package/src/nodes/state.ts +135 -0
  28. package/src/nodes/store.ts +378 -0
  29. package/src/nodes/task.ts +174 -0
  30. package/src/signal.ts +112 -66
  31. package/src/util.ts +26 -57
  32. package/test/batch.test.ts +96 -62
  33. package/test/benchmark.test.ts +473 -487
  34. package/test/collection.test.ts +456 -707
  35. package/test/effect.test.ts +293 -696
  36. package/test/list.test.ts +335 -592
  37. package/test/memo.test.ts +574 -0
  38. package/test/regression.test.ts +156 -0
  39. package/test/scope.test.ts +191 -0
  40. package/test/sensor.test.ts +454 -0
  41. package/test/signal.test.ts +220 -213
  42. package/test/state.test.ts +217 -265
  43. package/test/store.test.ts +346 -446
  44. package/test/task.test.ts +529 -0
  45. package/test/untrack.test.ts +167 -0
  46. package/types/index.d.ts +13 -15
  47. package/types/src/errors.d.ts +73 -17
  48. package/types/src/graph.d.ts +218 -0
  49. package/types/src/nodes/collection.d.ts +69 -0
  50. package/types/src/nodes/effect.d.ts +48 -0
  51. package/types/src/nodes/list.d.ts +66 -0
  52. package/types/src/nodes/memo.d.ts +63 -0
  53. package/types/src/nodes/sensor.d.ts +81 -0
  54. package/types/src/nodes/state.d.ts +78 -0
  55. package/types/src/nodes/store.d.ts +51 -0
  56. package/types/src/nodes/task.d.ts +79 -0
  57. package/types/src/signal.d.ts +43 -29
  58. package/types/src/util.d.ts +9 -16
  59. package/archive/benchmark.ts +0 -683
  60. package/archive/collection.ts +0 -253
  61. package/archive/composite.ts +0 -85
  62. package/archive/computed.ts +0 -195
  63. package/archive/list.ts +0 -483
  64. package/archive/memo.ts +0 -139
  65. package/archive/state.ts +0 -90
  66. package/archive/store.ts +0 -298
  67. package/archive/task.ts +0 -189
  68. package/src/classes/collection.ts +0 -245
  69. package/src/classes/computed.ts +0 -349
  70. package/src/classes/list.ts +0 -343
  71. package/src/classes/ref.ts +0 -70
  72. package/src/classes/state.ts +0 -102
  73. package/src/classes/store.ts +0 -262
  74. package/src/diff.ts +0 -138
  75. package/src/effect.ts +0 -93
  76. package/src/match.ts +0 -45
  77. package/src/resolve.ts +0 -49
  78. package/src/system.ts +0 -257
  79. package/test/computed.test.ts +0 -1108
  80. package/test/diff.test.ts +0 -955
  81. package/test/match.test.ts +0 -388
  82. package/test/ref.test.ts +0 -353
  83. package/test/resolve.test.ts +0 -154
  84. package/types/src/classes/collection.d.ts +0 -45
  85. package/types/src/classes/computed.d.ts +0 -94
  86. package/types/src/classes/list.d.ts +0 -43
  87. package/types/src/classes/ref.d.ts +0 -35
  88. package/types/src/classes/state.d.ts +0 -49
  89. package/types/src/classes/store.d.ts +0 -52
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -78
@@ -1,343 +0,0 @@
1
- import { type DiffResult, diff, isEqual, type UnknownArray } from '../diff'
2
- import {
3
- DuplicateKeyError,
4
- guardMutableSignal,
5
- validateSignalValue,
6
- } from '../errors'
7
- import {
8
- batch,
9
- notifyOf,
10
- registerWatchCallbacks,
11
- type SignalOptions,
12
- subscribeTo,
13
- UNSET,
14
- unsubscribeAllFrom,
15
- } from '../system'
16
- import { isFunction, isNumber, isObjectOfType, isString } from '../util'
17
- import { type CollectionCallback, DerivedCollection } from './collection'
18
- import { State } from './state'
19
-
20
- /* === Types === */
21
-
22
- type ArrayToRecord<T extends UnknownArray> = {
23
- [key: string]: T extends Array<infer U extends {}> ? U : never
24
- }
25
-
26
- type KeyConfig<T> = string | ((item: T) => string)
27
- type ListOptions<T extends {}> = SignalOptions<T> & {
28
- keyConfig?: KeyConfig<T>
29
- }
30
-
31
- /* === Constants === */
32
-
33
- const TYPE_LIST = 'List' as const
34
-
35
- /* === Class === */
36
-
37
- class List<T extends {}> {
38
- #signals = new Map<string, State<T>>()
39
- #keys: string[] = []
40
- #generateKey: (item: T) => string
41
- #validate: (key: string, value: unknown) => value is T
42
-
43
- constructor(initialValue: T[], options?: ListOptions<T>) {
44
- validateSignalValue(TYPE_LIST, initialValue, Array.isArray)
45
-
46
- let keyCounter = 0
47
- const keyConfig = options?.keyConfig
48
- this.#generateKey = isString(keyConfig)
49
- ? () => `${keyConfig}${keyCounter++}`
50
- : isFunction<string>(keyConfig)
51
- ? (item: T) => keyConfig(item)
52
- : () => String(keyCounter++)
53
-
54
- this.#validate = (key: string, value: unknown): value is T => {
55
- validateSignalValue(
56
- `${TYPE_LIST} item for key "${key}"`,
57
- value,
58
- options?.guard,
59
- )
60
- return true
61
- }
62
-
63
- this.#change({
64
- add: this.#toRecord(initialValue),
65
- change: {},
66
- remove: {},
67
- changed: true,
68
- })
69
- if (options?.watched)
70
- registerWatchCallbacks(this, options.watched, options.unwatched)
71
- }
72
-
73
- // Convert array to record with stable keys
74
- #toRecord(array: T[]): ArrayToRecord<T[]> {
75
- const record = {} as Record<string, T>
76
-
77
- for (let i = 0; i < array.length; i++) {
78
- const value = array[i]
79
- if (value === undefined) continue // Skip sparse array positions
80
-
81
- let key = this.#keys[i]
82
- if (!key) {
83
- key = this.#generateKey(value)
84
- this.#keys[i] = key
85
- }
86
- record[key] = value
87
- }
88
- return record
89
- }
90
-
91
- #add(key: string, value: T) {
92
- if (!this.#validate(key, value)) return false
93
-
94
- this.#signals.set(key, new State(value))
95
- return true
96
- }
97
-
98
- #change(changes: DiffResult) {
99
- // Additions
100
- if (Object.keys(changes.add).length) {
101
- for (const key in changes.add) this.#add(key, changes.add[key] as T)
102
- }
103
-
104
- // Changes
105
- if (Object.keys(changes.change).length) {
106
- batch(() => {
107
- for (const key in changes.change) {
108
- const value = changes.change[key]
109
- if (!this.#validate(key as keyof T & string, value))
110
- continue
111
-
112
- const signal = this.#signals.get(key)
113
- if (
114
- guardMutableSignal(
115
- `${TYPE_LIST} item "${key}"`,
116
- value,
117
- signal,
118
- )
119
- )
120
- signal.set(value)
121
- }
122
- })
123
- }
124
-
125
- // Removals
126
- if (Object.keys(changes.remove).length) {
127
- for (const key in changes.remove) {
128
- this.#signals.delete(key)
129
- const index = this.#keys.indexOf(key)
130
- if (index !== -1) this.#keys.splice(index, 1)
131
- }
132
- this.#keys = this.#keys.filter(() => true)
133
- }
134
-
135
- return changes.changed
136
- }
137
-
138
- get #value(): T[] {
139
- return this.#keys
140
- .map(key => this.#signals.get(key)?.get())
141
- .filter(v => v !== undefined) as T[]
142
- }
143
-
144
- // Public methods
145
- get [Symbol.toStringTag](): 'List' {
146
- return TYPE_LIST
147
- }
148
-
149
- get [Symbol.isConcatSpreadable](): true {
150
- return true
151
- }
152
-
153
- *[Symbol.iterator](): IterableIterator<State<T>> {
154
- for (const key of this.#keys) {
155
- const signal = this.#signals.get(key)
156
- if (signal) yield signal as State<T>
157
- }
158
- }
159
-
160
- get length(): number {
161
- subscribeTo(this)
162
- return this.#keys.length
163
- }
164
-
165
- get(): T[] {
166
- subscribeTo(this)
167
- return this.#value
168
- }
169
-
170
- set(newValue: T[]): void {
171
- if (UNSET === newValue) {
172
- this.#signals.clear()
173
- notifyOf(this)
174
- unsubscribeAllFrom(this)
175
- return
176
- }
177
-
178
- const changes = diff(
179
- this.#toRecord(this.#value),
180
- this.#toRecord(newValue),
181
- )
182
- if (this.#change(changes)) notifyOf(this)
183
- }
184
-
185
- update(fn: (oldValue: T[]) => T[]): void {
186
- this.set(fn(this.get()))
187
- }
188
-
189
- at(index: number): State<T> | undefined {
190
- return this.#signals.get(this.#keys[index])
191
- }
192
-
193
- keys(): IterableIterator<string> {
194
- subscribeTo(this)
195
- return this.#keys.values()
196
- }
197
-
198
- byKey(key: string): State<T> | undefined {
199
- return this.#signals.get(key)
200
- }
201
-
202
- keyAt(index: number): string | undefined {
203
- return this.#keys[index]
204
- }
205
-
206
- indexOfKey(key: string): number {
207
- return this.#keys.indexOf(key)
208
- }
209
-
210
- add(value: T): string {
211
- const key = this.#generateKey(value)
212
- if (this.#signals.has(key))
213
- throw new DuplicateKeyError('store', key, value)
214
-
215
- if (!this.#keys.includes(key)) this.#keys.push(key)
216
- const ok = this.#add(key, value)
217
- if (ok) notifyOf(this)
218
- return key
219
- }
220
-
221
- remove(keyOrIndex: string | number): void {
222
- const key = isNumber(keyOrIndex) ? this.#keys[keyOrIndex] : keyOrIndex
223
- const ok = this.#signals.delete(key)
224
- if (ok) {
225
- const index = isNumber(keyOrIndex)
226
- ? keyOrIndex
227
- : this.#keys.indexOf(key)
228
- if (index >= 0) this.#keys.splice(index, 1)
229
- this.#keys = this.#keys.filter(() => true)
230
- notifyOf(this)
231
- }
232
- }
233
-
234
- sort(compareFn?: (a: T, b: T) => number): void {
235
- const entries = this.#keys
236
- .map(key => [key, this.#signals.get(key)?.get()] as [string, T])
237
- .sort(
238
- isFunction(compareFn)
239
- ? (a, b) => compareFn(a[1], b[1])
240
- : (a, b) => String(a[1]).localeCompare(String(b[1])),
241
- )
242
- const newOrder = entries.map(([key]) => key)
243
-
244
- if (!isEqual(this.#keys, newOrder)) {
245
- this.#keys = newOrder
246
- notifyOf(this)
247
- }
248
- }
249
-
250
- splice(start: number, deleteCount?: number, ...items: T[]): T[] {
251
- const length = this.#keys.length
252
- const actualStart =
253
- start < 0 ? Math.max(0, length + start) : Math.min(start, length)
254
- const actualDeleteCount = Math.max(
255
- 0,
256
- Math.min(
257
- deleteCount ?? Math.max(0, length - Math.max(0, actualStart)),
258
- length - actualStart,
259
- ),
260
- )
261
-
262
- const add = {} as Record<string, T>
263
- const remove = {} as Record<string, T>
264
-
265
- // Collect items to delete and their keys
266
- for (let i = 0; i < actualDeleteCount; i++) {
267
- const index = actualStart + i
268
- const key = this.#keys[index]
269
- if (key) {
270
- const signal = this.#signals.get(key)
271
- if (signal) remove[key] = signal.get() as T
272
- }
273
- }
274
-
275
- // Build new order: items before splice point
276
- const newOrder = this.#keys.slice(0, actualStart)
277
-
278
- // Add new items
279
- for (const item of items) {
280
- const key = this.#generateKey(item)
281
- newOrder.push(key)
282
- add[key] = item as T
283
- }
284
-
285
- // Add items after splice point
286
- newOrder.push(...this.#keys.slice(actualStart + actualDeleteCount))
287
-
288
- const changed = !!(
289
- Object.keys(add).length || Object.keys(remove).length
290
- )
291
-
292
- if (changed) {
293
- this.#change({
294
- add,
295
- change: {} as Record<string, T>,
296
- remove,
297
- changed,
298
- })
299
- this.#keys = newOrder.filter(() => true) // Update order array
300
- notifyOf(this)
301
- }
302
-
303
- return Object.values(remove)
304
- }
305
-
306
- deriveCollection<R extends {}>(
307
- callback: (sourceValue: T) => R,
308
- options?: SignalOptions<R[]>,
309
- ): DerivedCollection<R, T>
310
- deriveCollection<R extends {}>(
311
- callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
312
- options?: SignalOptions<R[]>,
313
- ): DerivedCollection<R, T>
314
- deriveCollection<R extends {}>(
315
- callback: CollectionCallback<R, T>,
316
- options?: SignalOptions<R[]>,
317
- ): DerivedCollection<R, T> {
318
- return new DerivedCollection(this, callback, options)
319
- }
320
- }
321
-
322
- /* === Functions === */
323
-
324
- /**
325
- * Check if the provided value is a List instance
326
- *
327
- * @since 0.15.0
328
- * @param {unknown} value - Value to check
329
- * @returns {boolean} - True if the value is a List instance, false otherwise
330
- */
331
- const isList = <T extends {}>(value: unknown): value is List<T> =>
332
- isObjectOfType(value, TYPE_LIST)
333
-
334
- /* === Exports === */
335
-
336
- export {
337
- isList,
338
- List,
339
- TYPE_LIST,
340
- type ArrayToRecord,
341
- type KeyConfig,
342
- type ListOptions,
343
- }
@@ -1,70 +0,0 @@
1
- import { validateSignalValue } from '../errors'
2
- import {
3
- notifyOf,
4
- registerWatchCallbacks,
5
- type SignalOptions,
6
- subscribeTo,
7
- } from '../system'
8
- import { isObjectOfType } from '../util'
9
-
10
- /* === Constants === */
11
-
12
- const TYPE_REF = 'Ref'
13
-
14
- /* === Class === */
15
-
16
- /**
17
- * Create a new ref signal.
18
- *
19
- * @since 0.17.1
20
- * @param {T} value - Reference to external object
21
- * @param {Guard<T>} guard - Optional guard function to validate the value
22
- * @throws {NullishSignalValueError} - If the value is null or undefined
23
- * @throws {InvalidSignalValueError} - If the value is invalid
24
- */
25
- class Ref<T extends {}> {
26
- #value: T
27
-
28
- constructor(value: T, options?: SignalOptions<T>) {
29
- validateSignalValue(TYPE_REF, value, options?.guard)
30
-
31
- this.#value = value
32
- if (options?.watched)
33
- registerWatchCallbacks(this, options.watched, options.unwatched)
34
- }
35
-
36
- get [Symbol.toStringTag](): string {
37
- return TYPE_REF
38
- }
39
-
40
- /**
41
- * Get the value of the ref signal.
42
- *
43
- * @returns {T} - Object reference
44
- */
45
- get(): T {
46
- subscribeTo(this)
47
- return this.#value
48
- }
49
-
50
- /**
51
- * Notify watchers of relevant changes in the external reference.
52
- */
53
- notify(): void {
54
- notifyOf(this)
55
- }
56
- }
57
-
58
- /* === Functions === */
59
-
60
- /**
61
- * Check if the provided value is a Ref instance
62
- *
63
- * @since 0.17.1
64
- * @param {unknown} value - Value to check
65
- * @returns {boolean} - Whether the value is a Ref instance
66
- */
67
- const isRef = /*#__PURE__*/ <T extends {}>(value: unknown): value is Ref<T> =>
68
- isObjectOfType(value, TYPE_REF)
69
-
70
- export { TYPE_REF, Ref, isRef }
@@ -1,102 +0,0 @@
1
- import { isEqual } from '../diff'
2
- import { validateCallback, validateSignalValue } from '../errors'
3
- import {
4
- type SignalOptions,
5
- registerWatchCallbacks,
6
- notifyOf,
7
- subscribeTo,
8
- UNSET,
9
- unsubscribeAllFrom,
10
- } from '../system'
11
- import { isObjectOfType } from '../util'
12
-
13
- /* === Constants === */
14
-
15
- const TYPE_STATE = 'State' as const
16
-
17
- /* === Class === */
18
-
19
- /**
20
- * Create a new state signal.
21
- *
22
- * @since 0.17.0
23
- * @param {T} initialValue - Initial value of the state
24
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
25
- * @throws {InvalidSignalValueError} - If the initial value is invalid
26
- */
27
- class State<T extends {}> {
28
- #value: T
29
-
30
- constructor(initialValue: T, options?: SignalOptions<T>) {
31
- validateSignalValue(TYPE_STATE, initialValue, options?.guard)
32
-
33
- this.#value = initialValue
34
- if (options?.watched)
35
- registerWatchCallbacks(this, options.watched, options.unwatched)
36
- }
37
-
38
- get [Symbol.toStringTag](): string {
39
- return TYPE_STATE
40
- }
41
-
42
- /**
43
- * Get the current value of the state signal.
44
- *
45
- * @returns {T} - Current value of the state
46
- */
47
- get(): T {
48
- subscribeTo(this)
49
- return this.#value
50
- }
51
-
52
- /**
53
- * Set the value of the state signal.
54
- *
55
- * @param {T} newValue - New value of the state
56
- * @returns {void}
57
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
58
- * @throws {InvalidSignalValueError} - If the initial value is invalid
59
- */
60
- set(newValue: T): void {
61
- validateSignalValue(TYPE_STATE, newValue)
62
-
63
- if (isEqual(this.#value, newValue)) return
64
- this.#value = newValue
65
- notifyOf(this)
66
-
67
- // Setting to UNSET clears the watchers so the signal can be garbage collected
68
- if (UNSET === this.#value) unsubscribeAllFrom(this)
69
- }
70
-
71
- /**
72
- * Update the value of the state signal.
73
- *
74
- * @param {Function} updater - Function that takes the current value and returns the new value
75
- * @returns {void}
76
- * @throws {InvalidCallbackError} - If the updater function is not a function
77
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
78
- * @throws {InvalidSignalValueError} - If the initial value is invalid
79
- */
80
- update(updater: (oldValue: T) => T): void {
81
- validateCallback(`${TYPE_STATE} update`, updater)
82
-
83
- this.set(updater(this.#value))
84
- }
85
- }
86
-
87
- /* === Functions === */
88
-
89
- /**
90
- * Check if the provided value is a State instance
91
- *
92
- * @since 0.9.0
93
- * @param {unknown} value - Value to check
94
- * @returns {boolean} - True if the value is a State instance, false otherwise
95
- */
96
- const isState = /*#__PURE__*/ <T extends {}>(
97
- value: unknown,
98
- ): value is State<T> => isObjectOfType(value, TYPE_STATE)
99
-
100
- /* === Exports === */
101
-
102
- export { TYPE_STATE, isState, State }