@zeix/cause-effect 0.17.2 → 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 (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  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 -81
@@ -1,392 +0,0 @@
1
- import { isEqual } from '../diff'
2
- import {
3
- CircularDependencyError,
4
- createError,
5
- InvalidHookError,
6
- validateCallback,
7
- validateSignalValue,
8
- } from '../errors'
9
- import {
10
- type Cleanup,
11
- createWatcher,
12
- flushPendingReactions,
13
- HOOK_CLEANUP,
14
- HOOK_WATCH,
15
- type HookCallback,
16
- notifyWatchers,
17
- subscribeActiveWatcher,
18
- trackSignalReads,
19
- UNSET,
20
- type Watcher,
21
- type WatchHook,
22
- } from '../system'
23
- import {
24
- isAbortError,
25
- isAsyncFunction,
26
- isObjectOfType,
27
- isSyncFunction,
28
- } from '../util'
29
-
30
- /* === Types === */
31
-
32
- type Computed<T extends {}> = {
33
- readonly [Symbol.toStringTag]: 'Computed'
34
- get(): T
35
- }
36
-
37
- type MemoCallback<T extends {} & { then?: undefined }> = (oldValue: T) => T
38
-
39
- type TaskCallback<T extends {} & { then?: undefined }> = (
40
- oldValue: T,
41
- abort: AbortSignal,
42
- ) => Promise<T>
43
-
44
- /* === Constants === */
45
-
46
- const TYPE_COMPUTED = 'Computed' as const
47
-
48
- /* === Classes === */
49
-
50
- /**
51
- * Create a new memoized signal for a synchronous function.
52
- *
53
- * @since 0.17.0
54
- */
55
- class Memo<T extends {}> {
56
- #watchers: Set<Watcher> = new Set()
57
- #callback: MemoCallback<T>
58
- #value: T
59
- #error: Error | undefined
60
- #dirty = true
61
- #computing = false
62
- #watcher: Watcher | undefined
63
- #watchHookCallbacks: Set<HookCallback> | undefined
64
-
65
- /**
66
- * Create a new memoized signal.
67
- *
68
- * @param {MemoCallback<T>} callback - Callback function to compute the memoized value
69
- * @param {T} [initialValue = UNSET] - Initial value of the signal
70
- * @throws {InvalidCallbackError} If the callback is not an sync function
71
- * @throws {InvalidSignalValueError} If the initial value is not valid
72
- */
73
- constructor(callback: MemoCallback<T>, initialValue: T = UNSET) {
74
- validateCallback(this.constructor.name, callback, isMemoCallback)
75
- validateSignalValue(this.constructor.name, initialValue)
76
-
77
- this.#callback = callback
78
- this.#value = initialValue
79
- }
80
-
81
- #getWatcher(): Watcher {
82
- if (!this.#watcher) {
83
- // Own watcher: called by notifyWatchers() in upstream signals (push)
84
- this.#watcher = createWatcher(() => {
85
- this.#dirty = true
86
- if (!notifyWatchers(this.#watchers)) this.#watcher?.stop()
87
- })
88
- this.#watcher.on(HOOK_CLEANUP, () => {
89
- this.#watcher = undefined
90
- })
91
- }
92
- return this.#watcher
93
- }
94
-
95
- get [Symbol.toStringTag](): 'Computed' {
96
- return TYPE_COMPUTED
97
- }
98
-
99
- /**
100
- * Return the memoized value after computing it if necessary.
101
- *
102
- * @returns {T}
103
- * @throws {CircularDependencyError} If a circular dependency is detected
104
- * @throws {Error} If an error occurs during computation
105
- */
106
- get(): T {
107
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
108
- flushPendingReactions()
109
-
110
- if (this.#dirty) {
111
- const watcher = this.#getWatcher()
112
- trackSignalReads(watcher, () => {
113
- if (this.#computing) throw new CircularDependencyError('memo')
114
-
115
- let result: T
116
- this.#computing = true
117
- try {
118
- result = this.#callback(this.#value)
119
- } catch (e) {
120
- // Err track
121
- this.#value = UNSET
122
- this.#error = createError(e)
123
- this.#computing = false
124
- return
125
- }
126
-
127
- if (null == result || UNSET === result) {
128
- // Nil track
129
- this.#value = UNSET
130
- this.#error = undefined
131
- } else {
132
- // Ok track
133
- this.#value = result
134
- this.#error = undefined
135
- this.#dirty = false
136
- }
137
- this.#computing = false
138
- })
139
- }
140
-
141
- if (this.#error) throw this.#error
142
- return this.#value
143
- }
144
-
145
- /**
146
- * Register a callback to be called when HOOK_WATCH is triggered.
147
- *
148
- * @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
149
- * @param {HookCallback} callback - The callback to register
150
- * @returns {Cleanup} - A function to unregister the callback
151
- */
152
- on(type: WatchHook, callback: HookCallback): Cleanup {
153
- if (type === HOOK_WATCH) {
154
- this.#watchHookCallbacks ||= new Set()
155
- this.#watchHookCallbacks.add(callback)
156
- return () => {
157
- this.#watchHookCallbacks?.delete(callback)
158
- }
159
- }
160
- throw new InvalidHookError(this.constructor.name, type)
161
- }
162
- }
163
-
164
- /**
165
- * Create a new task signals that memoizes the result of an asynchronous function.
166
- *
167
- * @since 0.17.0
168
- */
169
- class Task<T extends {}> {
170
- #watchers: Set<Watcher> = new Set()
171
- #callback: TaskCallback<T>
172
- #value: T
173
- #error: Error | undefined
174
- #dirty = true
175
- #computing = false
176
- #changed = false
177
- #watcher: Watcher | undefined
178
- #controller: AbortController | undefined
179
- #watchHookCallbacks: Set<HookCallback> | undefined
180
-
181
- /**
182
- * Create a new task signal for an asynchronous function.
183
- *
184
- * @param {TaskCallback<T>} callback - The asynchronous function to compute the memoized value
185
- * @param {T} [initialValue = UNSET] - Initial value of the signal
186
- * @throws {InvalidCallbackError} If the callback is not an async function
187
- * @throws {InvalidSignalValueError} If the initial value is not valid
188
- */
189
- constructor(callback: TaskCallback<T>, initialValue: T = UNSET) {
190
- validateCallback(this.constructor.name, callback, isTaskCallback)
191
- validateSignalValue(this.constructor.name, initialValue)
192
-
193
- this.#callback = callback
194
- this.#value = initialValue
195
- }
196
-
197
- #getWatcher(): Watcher {
198
- if (!this.#watcher) {
199
- // Own watcher: called by notifyWatchers() in upstream signals (push)
200
- this.#watcher = createWatcher(() => {
201
- this.#dirty = true
202
- this.#controller?.abort()
203
- if (!notifyWatchers(this.#watchers)) this.#watcher?.stop()
204
- })
205
- this.#watcher.on(HOOK_CLEANUP, () => {
206
- this.#controller?.abort()
207
- this.#controller = undefined
208
- this.#watcher = undefined
209
- })
210
- }
211
- return this.#watcher
212
- }
213
-
214
- get [Symbol.toStringTag](): 'Computed' {
215
- return TYPE_COMPUTED
216
- }
217
-
218
- /**
219
- * Return the memoized value after executing the async function if necessary.
220
- *
221
- * @returns {T}
222
- * @throws {CircularDependencyError} If a circular dependency is detected
223
- * @throws {Error} If an error occurs during computation
224
- */
225
- get(): T {
226
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
227
- flushPendingReactions()
228
-
229
- // Functions to update internal state
230
- const ok = (v: T): undefined => {
231
- if (!isEqual(v, this.#value)) {
232
- this.#value = v
233
- this.#changed = true
234
- }
235
- this.#error = undefined
236
- this.#dirty = false
237
- }
238
- const nil = (): undefined => {
239
- this.#changed = UNSET !== this.#value
240
- this.#value = UNSET
241
- this.#error = undefined
242
- }
243
- const err = (e: unknown): undefined => {
244
- const newError = createError(e)
245
- this.#changed =
246
- !this.#error ||
247
- newError.name !== this.#error.name ||
248
- newError.message !== this.#error.message
249
- this.#value = UNSET
250
- this.#error = newError
251
- }
252
- const settle =
253
- <T>(fn: (arg: T) => void) =>
254
- (arg: T) => {
255
- this.#computing = false
256
- this.#controller = undefined
257
- fn(arg)
258
- if (this.#changed && !notifyWatchers(this.#watchers))
259
- this.#watcher?.stop()
260
- }
261
-
262
- const compute = () =>
263
- trackSignalReads(this.#getWatcher(), () => {
264
- if (this.#computing) throw new CircularDependencyError('task')
265
- this.#changed = false
266
-
267
- // Return current value until promise resolves
268
- if (this.#controller) return this.#value
269
-
270
- this.#controller = new AbortController()
271
- this.#controller.signal.addEventListener(
272
- 'abort',
273
- () => {
274
- this.#computing = false
275
- this.#controller = undefined
276
-
277
- // Retry computation with updated state
278
- compute()
279
- },
280
- {
281
- once: true,
282
- },
283
- )
284
- let result: Promise<T>
285
- this.#computing = true
286
- try {
287
- result = this.#callback(
288
- this.#value,
289
- this.#controller.signal,
290
- )
291
- } catch (e) {
292
- if (isAbortError(e)) nil()
293
- else err(e)
294
- this.#computing = false
295
- return
296
- }
297
-
298
- if (result instanceof Promise)
299
- result.then(settle(ok), settle(err))
300
- else if (null == result || UNSET === result) nil()
301
- else ok(result)
302
- this.#computing = false
303
- })
304
-
305
- if (this.#dirty) compute()
306
-
307
- if (this.#error) throw this.#error
308
- return this.#value
309
- }
310
-
311
- /**
312
- * Register a callback to be called when HOOK_WATCH is triggered.
313
- *
314
- * @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
315
- * @param {HookCallback} callback - The callback to register
316
- * @returns {Cleanup} - A function to unregister the callback
317
- */
318
- on(type: WatchHook, callback: HookCallback): Cleanup {
319
- if (type === HOOK_WATCH) {
320
- this.#watchHookCallbacks ||= new Set()
321
- this.#watchHookCallbacks.add(callback)
322
- return () => {
323
- this.#watchHookCallbacks?.delete(callback)
324
- }
325
- }
326
- throw new InvalidHookError(this.constructor.name, type)
327
- }
328
- }
329
-
330
- /* === Functions === */
331
-
332
- /**
333
- * Create a derived signal from existing signals
334
- *
335
- * @since 0.9.0
336
- * @param {MemoCallback<T> | TaskCallback<T>} callback - Computation callback function
337
- */
338
- const createComputed = <T extends {}>(
339
- callback: TaskCallback<T> | MemoCallback<T>,
340
- initialValue: T = UNSET,
341
- ) =>
342
- isAsyncFunction(callback)
343
- ? new Task(callback as TaskCallback<T>, initialValue)
344
- : new Memo(callback as MemoCallback<T>, initialValue)
345
-
346
- /**
347
- * Check if a value is a computed signal
348
- *
349
- * @since 0.9.0
350
- * @param {unknown} value - Value to check
351
- * @returns {boolean} - True if value is a computed signal, false otherwise
352
- */
353
- const isComputed = /*#__PURE__*/ <T extends {}>(
354
- value: unknown,
355
- ): value is Memo<T> => isObjectOfType(value, TYPE_COMPUTED)
356
-
357
- /**
358
- * Check if the provided value is a callback that may be used as input for createSignal() to derive a computed state
359
- *
360
- * @since 0.12.0
361
- * @param {unknown} value - Value to check
362
- * @returns {boolean} - True if value is a sync callback, false otherwise
363
- */
364
- const isMemoCallback = /*#__PURE__*/ <T extends {} & { then?: undefined }>(
365
- value: unknown,
366
- ): value is MemoCallback<T> => isSyncFunction(value) && value.length < 2
367
-
368
- /**
369
- * Check if the provided value is a callback that may be used as input for createSignal() to derive a computed state
370
- *
371
- * @since 0.17.0
372
- * @param {unknown} value - Value to check
373
- * @returns {boolean} - True if value is an async callback, false otherwise
374
- */
375
- const isTaskCallback = /*#__PURE__*/ <T extends {}>(
376
- value: unknown,
377
- ): value is TaskCallback<T> => isAsyncFunction(value) && value.length < 3
378
-
379
- /* === Exports === */
380
-
381
- export {
382
- TYPE_COMPUTED,
383
- createComputed,
384
- isComputed,
385
- isMemoCallback,
386
- isTaskCallback,
387
- Memo,
388
- Task,
389
- type Computed,
390
- type MemoCallback,
391
- type TaskCallback,
392
- }
@@ -1,310 +0,0 @@
1
- import { diff, isEqual, type UnknownArray } from '../diff'
2
- import {
3
- DuplicateKeyError,
4
- InvalidHookError,
5
- validateSignalValue,
6
- } from '../errors'
7
- import {
8
- type Cleanup,
9
- HOOK_ADD,
10
- HOOK_CHANGE,
11
- HOOK_REMOVE,
12
- HOOK_SORT,
13
- HOOK_WATCH,
14
- type Hook,
15
- type HookCallback,
16
- type HookCallbacks,
17
- isHandledHook,
18
- notifyWatchers,
19
- subscribeActiveWatcher,
20
- triggerHook,
21
- UNSET,
22
- type Watcher,
23
- } from '../system'
24
- import { isFunction, isNumber, isObjectOfType, isString } from '../util'
25
- import { type CollectionCallback, DerivedCollection } from './collection'
26
- import { Composite } from './composite'
27
- import { State } from './state'
28
-
29
- /* === Types === */
30
-
31
- type ArrayToRecord<T extends UnknownArray> = {
32
- [key: string]: T extends Array<infer U extends {}> ? U : never
33
- }
34
-
35
- type KeyConfig<T> = string | ((item: T) => string)
36
-
37
- /* === Constants === */
38
-
39
- const TYPE_LIST = 'List' as const
40
-
41
- /* === Class === */
42
-
43
- class List<T extends {}> {
44
- #composite: Composite<Record<string, T>, State<T>>
45
- #watchers = new Set<Watcher>()
46
- #hookCallbacks: HookCallbacks = {}
47
- #order: string[] = []
48
- #generateKey: (item: T) => string
49
-
50
- constructor(initialValue: T[], keyConfig?: KeyConfig<T>) {
51
- validateSignalValue(TYPE_LIST, initialValue, Array.isArray)
52
-
53
- let keyCounter = 0
54
- this.#generateKey = isString(keyConfig)
55
- ? () => `${keyConfig}${keyCounter++}`
56
- : isFunction<string>(keyConfig)
57
- ? (item: T) => keyConfig(item)
58
- : () => String(keyCounter++)
59
-
60
- this.#composite = new Composite<ArrayToRecord<T[]>, State<T>>(
61
- this.#toRecord(initialValue),
62
- (key: string, value: unknown): value is T => {
63
- validateSignalValue(`${TYPE_LIST} for key "${key}"`, value)
64
- return true
65
- },
66
- value => new State(value),
67
- )
68
- }
69
-
70
- // Convert array to record with stable keys
71
- #toRecord(array: T[]): ArrayToRecord<T[]> {
72
- const record = {} as Record<string, T>
73
-
74
- for (let i = 0; i < array.length; i++) {
75
- const value = array[i]
76
- if (value === undefined) continue // Skip sparse array positions
77
-
78
- let key = this.#order[i]
79
- if (!key) {
80
- key = this.#generateKey(value)
81
- this.#order[i] = key
82
- }
83
- record[key] = value
84
- }
85
- return record
86
- }
87
-
88
- get #value(): T[] {
89
- return this.#order
90
- .map(key => this.#composite.signals.get(key)?.get())
91
- .filter(v => v !== undefined) as T[]
92
- }
93
-
94
- // Public methods
95
- get [Symbol.toStringTag](): 'List' {
96
- return TYPE_LIST
97
- }
98
-
99
- get [Symbol.isConcatSpreadable](): true {
100
- return true
101
- }
102
-
103
- *[Symbol.iterator](): IterableIterator<State<T>> {
104
- for (const key of this.#order) {
105
- const signal = this.#composite.signals.get(key)
106
- if (signal) yield signal as State<T>
107
- }
108
- }
109
-
110
- get length(): number {
111
- subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
112
- return this.#order.length
113
- }
114
-
115
- get(): T[] {
116
- subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
117
- return this.#value
118
- }
119
-
120
- set(newValue: T[]): void {
121
- if (UNSET === newValue) {
122
- this.#composite.clear()
123
- notifyWatchers(this.#watchers)
124
- this.#watchers.clear()
125
- return
126
- }
127
-
128
- const oldValue = this.#value
129
- const changes = diff(this.#toRecord(oldValue), this.#toRecord(newValue))
130
- const removedKeys = Object.keys(changes.remove)
131
-
132
- const changed = this.#composite.change(changes)
133
- if (changed) {
134
- for (const key of removedKeys) {
135
- const index = this.#order.indexOf(key)
136
- if (index !== -1) this.#order.splice(index, 1)
137
- }
138
- this.#order = this.#order.filter(() => true)
139
- notifyWatchers(this.#watchers)
140
- }
141
- }
142
-
143
- update(fn: (oldValue: T[]) => T[]): void {
144
- this.set(fn(this.get()))
145
- }
146
-
147
- at(index: number): State<T> | undefined {
148
- return this.#composite.signals.get(this.#order[index])
149
- }
150
-
151
- keys(): IterableIterator<string> {
152
- return this.#order.values()
153
- }
154
-
155
- byKey(key: string): State<T> | undefined {
156
- return this.#composite.signals.get(key)
157
- }
158
-
159
- keyAt(index: number): string | undefined {
160
- return this.#order[index]
161
- }
162
-
163
- indexOfKey(key: string): number {
164
- return this.#order.indexOf(key)
165
- }
166
-
167
- add(value: T): string {
168
- const key = this.#generateKey(value)
169
- if (this.#composite.signals.has(key))
170
- throw new DuplicateKeyError('store', key, value)
171
-
172
- if (!this.#order.includes(key)) this.#order.push(key)
173
- const ok = this.#composite.add(key, value)
174
- if (ok) notifyWatchers(this.#watchers)
175
- return key
176
- }
177
-
178
- remove(keyOrIndex: string | number): void {
179
- const key = isNumber(keyOrIndex) ? this.#order[keyOrIndex] : keyOrIndex
180
- const ok = this.#composite.remove(key)
181
- if (ok) {
182
- const index = isNumber(keyOrIndex)
183
- ? keyOrIndex
184
- : this.#order.indexOf(key)
185
- if (index >= 0) this.#order.splice(index, 1)
186
- this.#order = this.#order.filter(() => true)
187
- notifyWatchers(this.#watchers)
188
- }
189
- }
190
-
191
- sort(compareFn?: (a: T, b: T) => number): void {
192
- const entries = this.#order
193
- .map(
194
- key =>
195
- [key, this.#composite.signals.get(key)?.get()] as [
196
- string,
197
- T,
198
- ],
199
- )
200
- .sort(
201
- isFunction(compareFn)
202
- ? (a, b) => compareFn(a[1], b[1])
203
- : (a, b) => String(a[1]).localeCompare(String(b[1])),
204
- )
205
- const newOrder = entries.map(([key]) => key)
206
-
207
- if (!isEqual(this.#order, newOrder)) {
208
- this.#order = newOrder
209
- notifyWatchers(this.#watchers)
210
- triggerHook(this.#hookCallbacks.sort, this.#order)
211
- }
212
- }
213
-
214
- splice(start: number, deleteCount?: number, ...items: T[]): T[] {
215
- const length = this.#order.length
216
- const actualStart =
217
- start < 0 ? Math.max(0, length + start) : Math.min(start, length)
218
- const actualDeleteCount = Math.max(
219
- 0,
220
- Math.min(
221
- deleteCount ?? Math.max(0, length - Math.max(0, actualStart)),
222
- length - actualStart,
223
- ),
224
- )
225
-
226
- const add = {} as Record<string, T>
227
- const remove = {} as Record<string, T>
228
-
229
- // Collect items to delete and their keys
230
- for (let i = 0; i < actualDeleteCount; i++) {
231
- const index = actualStart + i
232
- const key = this.#order[index]
233
- if (key) {
234
- const signal = this.#composite.signals.get(key)
235
- if (signal) remove[key] = signal.get() as T
236
- }
237
- }
238
-
239
- // Build new order: items before splice point
240
- const newOrder = this.#order.slice(0, actualStart)
241
-
242
- // Add new items
243
- for (const item of items) {
244
- const key = this.#generateKey(item)
245
- newOrder.push(key)
246
- add[key] = item as T
247
- }
248
-
249
- // Add items after splice point
250
- newOrder.push(...this.#order.slice(actualStart + actualDeleteCount))
251
-
252
- const changed = !!(
253
- Object.keys(add).length || Object.keys(remove).length
254
- )
255
-
256
- if (changed) {
257
- this.#composite.change({
258
- add,
259
- change: {} as Record<string, T>,
260
- remove,
261
- changed,
262
- })
263
- this.#order = newOrder.filter(() => true) // Update order array
264
- notifyWatchers(this.#watchers)
265
- }
266
-
267
- return Object.values(remove)
268
- }
269
-
270
- on(type: Hook, callback: HookCallback): Cleanup {
271
- if (isHandledHook(type, [HOOK_SORT, HOOK_WATCH])) {
272
- this.#hookCallbacks[type] ||= new Set<HookCallback>()
273
- this.#hookCallbacks[type].add(callback)
274
- return () => {
275
- this.#hookCallbacks[type]?.delete(callback)
276
- }
277
- } else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
278
- return this.#composite.on(type, callback)
279
- }
280
- throw new InvalidHookError(TYPE_LIST, type)
281
- }
282
-
283
- deriveCollection<R extends {}>(
284
- callback: (sourceValue: T) => R,
285
- ): DerivedCollection<R, T>
286
- deriveCollection<R extends {}>(
287
- callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
288
- ): DerivedCollection<R, T>
289
- deriveCollection<R extends {}>(
290
- callback: CollectionCallback<R, T>,
291
- ): DerivedCollection<R, T> {
292
- return new DerivedCollection(this, callback)
293
- }
294
- }
295
-
296
- /* === Functions === */
297
-
298
- /**
299
- * Check if the provided value is a List instance
300
- *
301
- * @since 0.15.0
302
- * @param {unknown} value - Value to check
303
- * @returns {boolean} - True if the value is a List instance, false otherwise
304
- */
305
- const isList = <T extends {}>(value: unknown): value is List<T> =>
306
- isObjectOfType(value, TYPE_LIST)
307
-
308
- /* === Exports === */
309
-
310
- export { isList, List, TYPE_LIST, type ArrayToRecord, type KeyConfig }