@zeix/cause-effect 0.16.0 → 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.
- package/.ai-context.md +71 -21
- package/.cursorrules +3 -2
- package/.github/copilot-instructions.md +59 -13
- package/CLAUDE.md +170 -24
- package/LICENSE +1 -1
- package/README.md +156 -52
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +33 -34
- package/archive/list.ts +551 -0
- package/archive/memo.ts +138 -0
- package/archive/state.ts +89 -0
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +902 -501
- package/index.js +1 -1
- package/index.ts +42 -22
- package/package.json +1 -1
- package/src/classes/collection.ts +272 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +304 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +28 -52
- package/src/effect.ts +9 -9
- package/src/errors.ts +50 -25
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +796 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +72 -121
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +344 -1663
- package/types/index.d.ts +11 -9
- package/types/src/classes/collection.d.ts +32 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +12 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -3
- package/src/state.ts +0 -98
- package/src/store.ts +0 -525
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -66
|
@@ -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 '
|
|
1
|
+
import { isEqual } from '../src/diff'
|
|
2
2
|
import {
|
|
3
3
|
CircularDependencyError,
|
|
4
4
|
InvalidCallbackError,
|
|
5
5
|
NullishSignalValueError,
|
|
6
|
-
} from '
|
|
6
|
+
} from '../src/errors'
|
|
7
7
|
import {
|
|
8
8
|
createWatcher,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
flushPendingReactions,
|
|
10
|
+
notifyWatchers,
|
|
11
|
+
subscribeActiveWatcher,
|
|
12
|
+
trackSignalReads,
|
|
13
13
|
type Watcher,
|
|
14
|
-
} from '
|
|
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
|
-
|
|
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',
|
|
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)
|
|
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)
|
|
103
|
-
else watcher.
|
|
102
|
+
if (watchers.size) notifyWatchers(watchers)
|
|
103
|
+
else watcher.stop()
|
|
104
104
|
})
|
|
105
|
-
watcher.
|
|
105
|
+
watcher.onCleanup(() => {
|
|
106
106
|
controller?.abort()
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
// Called when requested by dependencies (pull)
|
|
110
110
|
const compute = () =>
|
|
111
|
-
|
|
111
|
+
trackSignalReads(watcher, () => {
|
|
112
112
|
if (computing) throw new CircularDependencyError('computed')
|
|
113
113
|
changed = false
|
|
114
114
|
if (isAsyncFunction(callback)) {
|
|
@@ -143,25 +143,24 @@ const createComputed = <T extends {}>(
|
|
|
143
143
|
else if (null == result || UNSET === result) nil()
|
|
144
144
|
else ok(result)
|
|
145
145
|
computing = false
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* Get the current value of the computed
|
|
153
|
-
*
|
|
154
|
-
* @since 0.9.0
|
|
155
|
-
* @returns {T} - Current value of the computed
|
|
156
|
-
*/
|
|
157
|
-
get: (): T => {
|
|
158
|
-
subscribe(watchers)
|
|
159
|
-
flush()
|
|
160
|
-
if (dirty) compute()
|
|
161
|
-
if (error) throw error
|
|
162
|
-
return value
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const computed: Record<PropertyKey, unknown> = {}
|
|
149
|
+
Object.defineProperties(computed, {
|
|
150
|
+
[Symbol.toStringTag]: {
|
|
151
|
+
value: TYPE_COMPUTED,
|
|
163
152
|
},
|
|
164
|
-
|
|
153
|
+
get: {
|
|
154
|
+
value: (): T => {
|
|
155
|
+
subscribeActiveWatcher(watchers)
|
|
156
|
+
flushPendingReactions()
|
|
157
|
+
if (dirty) compute()
|
|
158
|
+
if (error) throw error
|
|
159
|
+
return value
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
return computed as Computed<T>
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
/**
|