@zeix/cause-effect 0.16.1 → 0.17.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 (61) hide show
  1. package/.ai-context.md +71 -21
  2. package/.cursorrules +3 -2
  3. package/.github/copilot-instructions.md +59 -13
  4. package/CLAUDE.md +170 -24
  5. package/LICENSE +1 -1
  6. package/README.md +156 -52
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +19 -19
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +138 -0
  12. package/{src → archive}/state.ts +13 -11
  13. package/archive/store.ts +368 -0
  14. package/archive/task.ts +194 -0
  15. package/eslint.config.js +1 -0
  16. package/index.dev.js +899 -503
  17. package/index.js +1 -1
  18. package/index.ts +41 -22
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +272 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +304 -0
  24. package/src/classes/state.ts +98 -0
  25. package/src/classes/store.ts +210 -0
  26. package/src/diff.ts +26 -53
  27. package/src/effect.ts +9 -9
  28. package/src/errors.ts +50 -25
  29. package/src/signal.ts +58 -41
  30. package/src/system.ts +79 -42
  31. package/src/util.ts +16 -30
  32. package/test/batch.test.ts +15 -17
  33. package/test/benchmark.test.ts +4 -4
  34. package/test/collection.test.ts +796 -0
  35. package/test/computed.test.ts +138 -130
  36. package/test/diff.test.ts +2 -2
  37. package/test/effect.test.ts +36 -35
  38. package/test/list.test.ts +754 -0
  39. package/test/match.test.ts +25 -25
  40. package/test/resolve.test.ts +17 -19
  41. package/test/signal.test.ts +70 -119
  42. package/test/state.test.ts +44 -44
  43. package/test/store.test.ts +253 -929
  44. package/types/index.d.ts +10 -8
  45. package/types/src/classes/collection.d.ts +32 -0
  46. package/types/src/classes/composite.d.ts +15 -0
  47. package/types/src/classes/computed.d.ts +97 -0
  48. package/types/src/classes/list.d.ts +41 -0
  49. package/types/src/classes/state.d.ts +52 -0
  50. package/types/src/classes/store.d.ts +51 -0
  51. package/types/src/diff.d.ts +8 -12
  52. package/types/src/errors.d.ts +12 -11
  53. package/types/src/signal.d.ts +27 -14
  54. package/types/src/system.d.ts +41 -20
  55. package/types/src/util.d.ts +6 -3
  56. package/src/store.ts +0 -474
  57. package/types/src/collection.d.ts +0 -26
  58. package/types/src/computed.d.ts +0 -33
  59. package/types/src/scheduler.d.ts +0 -55
  60. package/types/src/state.d.ts +0 -24
  61. package/types/src/store.d.ts +0 -65
@@ -0,0 +1,312 @@
1
+ import type { UnknownArray } from '../src/diff'
2
+ import { match } from '../src/match'
3
+ import { resolve } from '../src/resolve'
4
+ import type { Signal } from '../src/signal'
5
+ import {
6
+ type Cleanup,
7
+ createWatcher,
8
+ emitNotification,
9
+ type Listener,
10
+ type Listeners,
11
+ type Notifications,
12
+ notifyWatchers,
13
+ subscribeActiveWatcher,
14
+ trackSignalReads,
15
+ type Watcher,
16
+ } from '../src/system'
17
+ import { isAsyncFunction, isObjectOfType, isSymbol, UNSET } from '../src/util'
18
+ import { type Computed, createComputed } from './computed'
19
+ import type { List } from './list'
20
+
21
+ /* === Types === */
22
+
23
+ type CollectionKeySignal<T extends {}> = T extends UnknownArray
24
+ ? Collection<T>
25
+ : Computed<T>
26
+
27
+ type CollectionCallback<T extends {} & { then?: undefined }, O extends {}> =
28
+ | ((originValue: O, abort: AbortSignal) => Promise<T>)
29
+ | ((originValue: O) => T)
30
+
31
+ type Collection<T extends {}> = {
32
+ readonly [Symbol.toStringTag]: typeof TYPE_COLLECTION
33
+ readonly [Symbol.isConcatSpreadable]: boolean
34
+ [Symbol.iterator](): IterableIterator<CollectionKeySignal<T>>
35
+ readonly [n: number]: CollectionKeySignal<T>
36
+ readonly length: number
37
+
38
+ byKey(key: string): CollectionKeySignal<T> | undefined
39
+ get(): T[]
40
+ keyAt(index: number): string | undefined
41
+ indexOfKey(key: string): number
42
+ on<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
43
+ sort(compareFn?: (a: T, b: T) => number): void
44
+ }
45
+
46
+ /* === Constants === */
47
+
48
+ const TYPE_COLLECTION = 'Collection' as const
49
+
50
+ /* === Exported Functions === */
51
+
52
+ /**
53
+ * Collections - Read-Only Derived Array-Like Stores
54
+ *
55
+ * Collections are the read-only, derived counterpart to array-like Stores.
56
+ * They provide reactive, memoized, and lazily-evaluated array transformations
57
+ * while maintaining the familiar array-like store interface.
58
+ *
59
+ * @since 0.16.2
60
+ * @param {List<O> | Collection<O>} origin - Origin of collection to derive values from
61
+ * @param {ComputedCallback<ArrayItem<T>>} callback - Callback function to transform array items
62
+ * @returns {Collection<T>} - New collection with reactive properties that preserves the original type T
63
+ */
64
+ const createCollection = <T extends {}, O extends {}>(
65
+ origin: List<O> | Collection<O>,
66
+ callback: CollectionCallback<T, O>,
67
+ ): Collection<T> => {
68
+ const watchers = new Set<Watcher>()
69
+ const listeners: Listeners = {
70
+ add: new Set<Listener<'add'>>(),
71
+ change: new Set<Listener<'change'>>(),
72
+ remove: new Set<Listener<'remove'>>(),
73
+ sort: new Set<Listener<'sort'>>(),
74
+ }
75
+ const signals = new Map<string, Signal<T>>()
76
+ const signalWatchers = new Map<string, Watcher>()
77
+
78
+ let order: string[] = []
79
+
80
+ // Add nested signal and effect
81
+ const addProperty = (key: string): boolean => {
82
+ const computedCallback = isAsyncFunction(callback)
83
+ ? async (_: T, abort: AbortSignal) => {
84
+ const originSignal = origin.byKey(key)
85
+ if (!originSignal) return UNSET
86
+
87
+ let result = UNSET
88
+ match(resolve({ originSignal }), {
89
+ ok: async ({ originSignal: originValue }) => {
90
+ result = await callback(originValue, abort)
91
+ },
92
+ err: (errors: readonly Error[]) => {
93
+ console.log(errors)
94
+ },
95
+ })
96
+ return result
97
+ }
98
+ : () => {
99
+ const originSignal = origin.byKey(key)
100
+ if (!originSignal) return UNSET
101
+
102
+ let result = UNSET
103
+ match(resolve({ originSignal }), {
104
+ ok: ({ originSignal: originValue }) => {
105
+ result = (callback as (originValue: O) => T)(
106
+ originValue as unknown as O,
107
+ )
108
+ },
109
+ err: (errors: readonly Error[]) => {
110
+ console.log(errors)
111
+ },
112
+ })
113
+ return result
114
+ }
115
+
116
+ const signal = createComputed(computedCallback)
117
+
118
+ // Set internal states
119
+ signals.set(key, signal)
120
+ if (!order.includes(key)) order.push(key)
121
+ const watcher = createWatcher(() =>
122
+ trackSignalReads(watcher, () => {
123
+ signal.get() // Subscribe to the signal
124
+ emitNotification(listeners.change, [key])
125
+ }),
126
+ )
127
+ watcher()
128
+ signalWatchers.set(key, watcher)
129
+ return true
130
+ }
131
+
132
+ // Remove nested signal and effect
133
+ const removeProperty = (key: string) => {
134
+ // Remove signal for key
135
+ const ok = signals.delete(key)
136
+ if (!ok) return
137
+
138
+ // Clean up internal states
139
+ const index = order.indexOf(key)
140
+ if (index >= 0) order.splice(index, 1)
141
+ const watcher = signalWatchers.get(key)
142
+ if (watcher) {
143
+ watcher.stop()
144
+ signalWatchers.delete(key)
145
+ }
146
+ }
147
+
148
+ // Initialize properties
149
+ for (let i = 0; i < origin.length; i++) {
150
+ const key = origin.keyAt(i)
151
+ if (!key) continue
152
+ addProperty(key)
153
+ }
154
+ origin.on('add', additions => {
155
+ for (const key of additions) {
156
+ if (!signals.has(key)) addProperty(key)
157
+ }
158
+ notifyWatchers(watchers)
159
+ emitNotification(listeners.add, additions)
160
+ })
161
+ origin.on('remove', removals => {
162
+ for (const key of Object.keys(removals)) {
163
+ if (!signals.has(key)) continue
164
+ removeProperty(key)
165
+ }
166
+ order = order.filter(() => true) // Compact array
167
+ notifyWatchers(watchers)
168
+ emitNotification(listeners.remove, removals)
169
+ })
170
+ origin.on('sort', newOrder => {
171
+ order = [...newOrder]
172
+ notifyWatchers(watchers)
173
+ emitNotification(listeners.sort, newOrder)
174
+ })
175
+
176
+ // Get signal by key or index
177
+ const getSignal = (prop: string): Signal<T> | undefined => {
178
+ let key = prop
179
+ const index = Number(prop)
180
+ if (Number.isInteger(index) && index >= 0) key = order[index] ?? prop
181
+ return signals.get(key)
182
+ }
183
+
184
+ // Get current array
185
+ const current = (): T =>
186
+ order
187
+ .map(key => signals.get(key)?.get())
188
+ .filter(v => v !== UNSET) as unknown as T
189
+
190
+ // Methods and Properties
191
+ const collection: Record<PropertyKey, unknown> = {}
192
+ Object.defineProperties(collection, {
193
+ [Symbol.toStringTag]: {
194
+ value: TYPE_COLLECTION,
195
+ },
196
+ [Symbol.isConcatSpreadable]: {
197
+ value: true,
198
+ },
199
+ [Symbol.iterator]: {
200
+ value: function* () {
201
+ for (const key of order) {
202
+ const signal = signals.get(key)
203
+ if (signal) yield signal
204
+ }
205
+ },
206
+ },
207
+ byKey: {
208
+ value(key: string) {
209
+ return getSignal(key)
210
+ },
211
+ },
212
+ keyAt: {
213
+ value(index: number): string | undefined {
214
+ return order[index]
215
+ },
216
+ },
217
+ indexOfKey: {
218
+ value(key: string): number {
219
+ return order.indexOf(key)
220
+ },
221
+ },
222
+ get: {
223
+ value: (): T => {
224
+ subscribeActiveWatcher(watchers)
225
+ return current()
226
+ },
227
+ },
228
+ sort: {
229
+ value: (compareFn?: (a: T, b: T) => number): void => {
230
+ const entries = order
231
+ .map((key, index) => {
232
+ const signal = signals.get(key)
233
+ return [
234
+ index,
235
+ key,
236
+ signal ? signal.get() : undefined,
237
+ ] as [number, string, T]
238
+ })
239
+ .sort(
240
+ compareFn
241
+ ? (a, b) => compareFn(a[2], b[2])
242
+ : (a, b) =>
243
+ String(a[2]).localeCompare(String(b[2])),
244
+ )
245
+
246
+ // Set new order
247
+ order = entries.map(([_, key]) => key)
248
+
249
+ notifyWatchers(watchers)
250
+ emitNotification(listeners.sort, order)
251
+ },
252
+ },
253
+ on: {
254
+ value: <K extends keyof Listeners>(
255
+ type: K,
256
+ listener: Listener<K>,
257
+ ): Cleanup => {
258
+ listeners[type].add(listener)
259
+ return () => listeners[type].delete(listener)
260
+ },
261
+ },
262
+ length: {
263
+ get(): number {
264
+ subscribeActiveWatcher(watchers)
265
+ return signals.size
266
+ },
267
+ },
268
+ })
269
+
270
+ // Return proxy directly with integrated signal methods
271
+ return new Proxy(collection as Collection<T>, {
272
+ get(target, prop) {
273
+ if (prop in target) return Reflect.get(target, prop)
274
+ if (!isSymbol(prop)) return getSignal(prop)
275
+ },
276
+ has(target, prop) {
277
+ if (prop in target) return true
278
+ return signals.has(String(prop))
279
+ },
280
+ ownKeys(target) {
281
+ const staticKeys = Reflect.ownKeys(target)
282
+ return [...new Set([...order, ...staticKeys])]
283
+ },
284
+ getOwnPropertyDescriptor(target, prop) {
285
+ if (prop in target)
286
+ return Reflect.getOwnPropertyDescriptor(target, prop)
287
+ if (isSymbol(prop)) return undefined
288
+
289
+ const signal = getSignal(prop)
290
+ return signal
291
+ ? {
292
+ enumerable: true,
293
+ configurable: true,
294
+ writable: true,
295
+ value: signal,
296
+ }
297
+ : undefined
298
+ },
299
+ })
300
+ }
301
+
302
+ const isCollection = /*#__PURE__*/ <T extends UnknownArray>(
303
+ value: unknown,
304
+ ): value is Collection<T> => isObjectOfType(value, TYPE_COLLECTION)
305
+
306
+ export {
307
+ type Collection,
308
+ type CollectionCallback,
309
+ createCollection,
310
+ isCollection,
311
+ TYPE_COLLECTION,
312
+ }
@@ -1,17 +1,17 @@
1
- import { isEqual } from './diff'
1
+ import { isEqual } from '../src/diff'
2
2
  import {
3
3
  CircularDependencyError,
4
4
  InvalidCallbackError,
5
5
  NullishSignalValueError,
6
- } from './errors'
6
+ } from '../src/errors'
7
7
  import {
8
8
  createWatcher,
9
- flush,
10
- notify,
11
- observe,
12
- subscribe,
9
+ flushPendingReactions,
10
+ notifyWatchers,
11
+ subscribeActiveWatcher,
12
+ trackSignalReads,
13
13
  type Watcher,
14
- } from './system'
14
+ } from '../src/system'
15
15
  import {
16
16
  isAbortError,
17
17
  isAsyncFunction,
@@ -19,8 +19,7 @@ import {
19
19
  isObjectOfType,
20
20
  toError,
21
21
  UNSET,
22
- valueString,
23
- } from './util'
22
+ } from '../src/util'
24
23
 
25
24
  /* === Types === */
26
25
 
@@ -28,13 +27,14 @@ type Computed<T extends {}> = {
28
27
  readonly [Symbol.toStringTag]: 'Computed'
29
28
  get(): T
30
29
  }
30
+
31
31
  type ComputedCallback<T extends {} & { then?: undefined }> =
32
32
  | ((oldValue: T, abort: AbortSignal) => Promise<T>)
33
33
  | ((oldValue: T) => T)
34
34
 
35
35
  /* === Constants === */
36
36
 
37
- const TYPE_COMPUTED = 'Computed'
37
+ const TYPE_COMPUTED = 'Computed' as const
38
38
 
39
39
  /* === Functions === */
40
40
 
@@ -50,7 +50,7 @@ const createComputed = <T extends {}>(
50
50
  initialValue: T = UNSET,
51
51
  ): Computed<T> => {
52
52
  if (!isComputedCallback(callback))
53
- throw new InvalidCallbackError('computed', valueString(callback))
53
+ throw new InvalidCallbackError('computed', callback)
54
54
  if (initialValue == null) throw new NullishSignalValueError('computed')
55
55
 
56
56
  const watchers: Set<Watcher> = new Set()
@@ -92,23 +92,23 @@ const createComputed = <T extends {}>(
92
92
  computing = false
93
93
  controller = undefined
94
94
  fn(arg)
95
- if (changed) notify(watchers)
95
+ if (changed) notifyWatchers(watchers)
96
96
  }
97
97
 
98
98
  // Own watcher: called when notified from sources (push)
99
99
  const watcher = createWatcher(() => {
100
100
  dirty = true
101
101
  controller?.abort()
102
- if (watchers.size) notify(watchers)
103
- else watcher.cleanup()
102
+ if (watchers.size) notifyWatchers(watchers)
103
+ else watcher.stop()
104
104
  })
105
- watcher.unwatch(() => {
105
+ watcher.onCleanup(() => {
106
106
  controller?.abort()
107
107
  })
108
108
 
109
109
  // Called when requested by dependencies (pull)
110
110
  const compute = () =>
111
- observe(() => {
111
+ trackSignalReads(watcher, () => {
112
112
  if (computing) throw new CircularDependencyError('computed')
113
113
  changed = false
114
114
  if (isAsyncFunction(callback)) {
@@ -143,7 +143,7 @@ const createComputed = <T extends {}>(
143
143
  else if (null == result || UNSET === result) nil()
144
144
  else ok(result)
145
145
  computing = false
146
- }, watcher)
146
+ })
147
147
 
148
148
  const computed: Record<PropertyKey, unknown> = {}
149
149
  Object.defineProperties(computed, {
@@ -152,8 +152,8 @@ const createComputed = <T extends {}>(
152
152
  },
153
153
  get: {
154
154
  value: (): T => {
155
- subscribe(watchers)
156
- flush()
155
+ subscribeActiveWatcher(watchers)
156
+ flushPendingReactions()
157
157
  if (dirty) compute()
158
158
  if (error) throw error
159
159
  return value