@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
package/test/ref.test.ts DELETED
@@ -1,381 +0,0 @@
1
- import { expect, mock, test } from 'bun:test'
2
- import { isRef, Ref } from '../src/classes/ref'
3
- import { createEffect } from '../src/effect'
4
- import { HOOK_WATCH } from '../src/system'
5
-
6
- test('Ref - basic functionality', () => {
7
- const obj = { name: 'test', value: 42 }
8
- const ref = new Ref(obj)
9
-
10
- expect(ref.get()).toBe(obj)
11
- expect(ref[Symbol.toStringTag]).toBe('Ref')
12
- })
13
-
14
- test('Ref - isRef type guard', () => {
15
- const ref = new Ref({ test: true })
16
- const notRef = { test: true }
17
-
18
- expect(isRef(ref)).toBe(true)
19
- expect(isRef(notRef)).toBe(false)
20
- expect(isRef(null)).toBe(false)
21
- expect(isRef(undefined)).toBe(false)
22
- })
23
-
24
- test('Ref - validation with guard function', () => {
25
- const isConfig = (
26
- value: unknown,
27
- ): value is { host: string; port: number } =>
28
- typeof value === 'object' &&
29
- value !== null &&
30
- 'host' in value &&
31
- 'port' in value &&
32
- typeof value.host === 'string' &&
33
- typeof value.port === 'number'
34
-
35
- const validConfig = { host: 'localhost', port: 3000 }
36
- const invalidConfig = { host: 'localhost' } // missing port
37
-
38
- expect(() => new Ref(validConfig, isConfig)).not.toThrow()
39
- expect(() => new Ref(invalidConfig, isConfig)).toThrow()
40
- })
41
-
42
- test('Ref - reactive subscriptions', () => {
43
- const server = { status: 'offline', connections: 0 }
44
- const ref = new Ref(server)
45
-
46
- let effectRunCount = 0
47
- let lastStatus: string = ''
48
-
49
- createEffect(() => {
50
- const current = ref.get()
51
- lastStatus = current.status
52
- effectRunCount++
53
- })
54
-
55
- expect(effectRunCount).toBe(1)
56
- expect(lastStatus).toBe('offline')
57
-
58
- // Simulate external change without going through reactive system
59
- server.status = 'online'
60
- server.connections = 5
61
-
62
- // Effect shouldn't re-run yet (reference hasn't changed)
63
- expect(effectRunCount).toBe(1)
64
-
65
- // Notify that the external object has changed
66
- ref.notify()
67
-
68
- expect(effectRunCount).toBe(2)
69
- expect(lastStatus).toBe('online')
70
- })
71
-
72
- test('Ref - notify triggers watchers even with same reference', () => {
73
- const fileObj = { path: '/test.txt', size: 100, modified: Date.now() }
74
- const ref = new Ref(fileObj)
75
-
76
- const mockCallback = mock(() => {})
77
-
78
- createEffect(() => {
79
- ref.get()
80
- mockCallback()
81
- })
82
-
83
- expect(mockCallback).toHaveBeenCalledTimes(1)
84
-
85
- // Simulate file modification (same object reference, different content)
86
- fileObj.size = 200
87
- fileObj.modified = Date.now()
88
-
89
- // Notify about external change
90
- ref.notify()
91
-
92
- expect(mockCallback).toHaveBeenCalledTimes(2)
93
-
94
- // Multiple notifies should trigger multiple times
95
- ref.notify()
96
- expect(mockCallback).toHaveBeenCalledTimes(3)
97
- })
98
-
99
- test('Ref - multiple effects with same ref', () => {
100
- const database = { connected: false, queries: 0 }
101
- const ref = new Ref(database)
102
-
103
- const effect1Mock = mock(() => {})
104
- const effect2Mock = mock((_connected: boolean) => {})
105
-
106
- createEffect(() => {
107
- ref.get()
108
- effect1Mock()
109
- })
110
-
111
- createEffect(() => {
112
- const db = ref.get()
113
- effect2Mock(db.connected)
114
- })
115
-
116
- expect(effect1Mock).toHaveBeenCalledTimes(1)
117
- expect(effect2Mock).toHaveBeenCalledTimes(1)
118
- expect(effect2Mock).toHaveBeenCalledWith(false)
119
-
120
- // Simulate database connection change
121
- database.connected = true
122
- database.queries = 10
123
- ref.notify()
124
-
125
- expect(effect1Mock).toHaveBeenCalledTimes(2)
126
- expect(effect2Mock).toHaveBeenCalledTimes(2)
127
- expect(effect2Mock).toHaveBeenLastCalledWith(true)
128
- })
129
-
130
- test('Ref - with Bun.file() scenario', () => {
131
- // Mock a file-like object that could change externally
132
- const fileRef = {
133
- name: 'config.json',
134
- size: 1024,
135
- lastModified: Date.now(),
136
- // Simulate file methods
137
- exists: () => true,
138
- text: () => Promise.resolve('{"version": "1.0"}'),
139
- }
140
-
141
- const ref = new Ref(fileRef)
142
-
143
- let sizeChanges = 0
144
- createEffect(() => {
145
- const file = ref.get()
146
- if (file.size > 1000) sizeChanges++
147
- })
148
-
149
- expect(sizeChanges).toBe(1) // Initial run
150
-
151
- // Simulate file growing (external change)
152
- fileRef.size = 2048
153
- fileRef.lastModified = Date.now()
154
- ref.notify()
155
-
156
- expect(sizeChanges).toBe(2) // Effect re-ran and condition still met
157
-
158
- // Simulate file shrinking
159
- fileRef.size = 500
160
- ref.notify()
161
-
162
- expect(sizeChanges).toBe(2) // Effect re-ran but condition no longer met
163
- })
164
-
165
- test('Ref - validation errors', () => {
166
- // @ts-expect-error deliberatly provoked error
167
- expect(() => new Ref(null)).toThrow()
168
- // @ts-expect-error deliberatly provoked error
169
- expect(() => new Ref(undefined)).toThrow()
170
- })
171
-
172
- test('Ref - server config object scenario', () => {
173
- const config = {
174
- host: 'localhost',
175
- port: 3000,
176
- ssl: false,
177
- maxConnections: 100,
178
- }
179
-
180
- const configRef = new Ref(config)
181
- const connectionAttempts: string[] = []
182
-
183
- createEffect(() => {
184
- const cfg = configRef.get()
185
- const protocol = cfg.ssl ? 'https' : 'http'
186
- connectionAttempts.push(`${protocol}://${cfg.host}:${cfg.port}`)
187
- })
188
-
189
- expect(connectionAttempts).toEqual(['http://localhost:3000'])
190
-
191
- // Simulate config reload from file/environment
192
- config.ssl = true
193
- config.port = 8443
194
- configRef.notify()
195
-
196
- expect(connectionAttempts).toEqual([
197
- 'http://localhost:3000',
198
- 'https://localhost:8443',
199
- ])
200
- })
201
-
202
- test('Ref - handles complex nested objects', () => {
203
- const apiResponse = {
204
- status: 200,
205
- data: {
206
- users: [{ id: 1, name: 'Alice' }],
207
- pagination: { page: 1, total: 1 },
208
- },
209
- headers: { 'content-type': 'application/json' },
210
- }
211
-
212
- const ref = new Ref(apiResponse)
213
- let userCount = 0
214
-
215
- createEffect(() => {
216
- const response = ref.get()
217
- userCount = response.data.users.length
218
- })
219
-
220
- expect(userCount).toBe(1)
221
-
222
- // Simulate API response update
223
- apiResponse.data.users.push({ id: 2, name: 'Bob' })
224
- apiResponse.data.pagination.total = 2
225
- ref.notify()
226
-
227
- expect(userCount).toBe(2)
228
- })
229
-
230
- test('Ref - HOOK_WATCH lazy resource management', async () => {
231
- // 1. Create Ref with current Date
232
- const currentDate = new Date()
233
- const ref = new Ref(currentDate)
234
-
235
- let counter = 0
236
- let intervalId: Timer | undefined
237
-
238
- // 2. Add HOOK_WATCH callback that starts setInterval and returns cleanup
239
- const cleanupHookCallback = ref.on(HOOK_WATCH, () => {
240
- intervalId = setInterval(() => {
241
- counter++
242
- }, 10) // Use short interval for faster test
243
-
244
- // Return cleanup function to clear interval
245
- return () => {
246
- if (intervalId) {
247
- clearInterval(intervalId)
248
- intervalId = undefined
249
- }
250
- }
251
- })
252
-
253
- // 3. Counter should not be running yet
254
- expect(counter).toBe(0)
255
-
256
- // Wait a bit to ensure counter doesn't increment
257
- await new Promise(resolve => setTimeout(resolve, 50))
258
- expect(counter).toBe(0)
259
- expect(intervalId).toBeUndefined()
260
-
261
- // 4. Effect subscribes by .get()ting the signal value
262
- const effectCleanup = createEffect(() => {
263
- ref.get()
264
- })
265
-
266
- // 5. Counter should now be running
267
- await new Promise(resolve => setTimeout(resolve, 50))
268
- expect(counter).toBeGreaterThan(0)
269
- expect(intervalId).toBeDefined()
270
-
271
- // 6. Call effect cleanup, which should stop internal watcher and unsubscribe
272
- effectCleanup()
273
- const counterAfterStop = counter
274
-
275
- // 7. Ref signal should call #unwatch() and counter should stop incrementing
276
- await new Promise(resolve => setTimeout(resolve, 50))
277
- expect(counter).toBe(counterAfterStop) // Counter should not have incremented
278
- expect(intervalId).toBeUndefined() // Interval should be cleared
279
-
280
- // Clean up hook callback registration
281
- cleanupHookCallback()
282
- })
283
-
284
- test('Ref - HOOK_WATCH exception handling', async () => {
285
- const ref = new Ref({ test: 'value' })
286
-
287
- // Mock console.error to capture error logs
288
- const originalError = console.error
289
- const errorSpy = mock(() => {})
290
- console.error = errorSpy
291
-
292
- let successfulCallbackCalled = false
293
- let throwingCallbackCalled = false
294
-
295
- // Add callback that throws an exception
296
- const cleanup1 = ref.on(HOOK_WATCH, () => {
297
- throwingCallbackCalled = true
298
- throw new Error('Test error in HOOK_WATCH callback')
299
- })
300
-
301
- // Add callback that works normally
302
- const cleanup2 = ref.on(HOOK_WATCH, () => {
303
- successfulCallbackCalled = true
304
- return () => {
305
- // cleanup function
306
- }
307
- })
308
-
309
- // Subscribe to trigger HOOK_WATCH callbacks
310
- const effectCleanup = createEffect(() => {
311
- ref.get()
312
- })
313
-
314
- // Both callbacks should have been called despite the exception
315
- expect(throwingCallbackCalled).toBe(true)
316
- expect(successfulCallbackCalled).toBe(true)
317
-
318
- // Error should have been logged
319
- expect(errorSpy).toHaveBeenCalledWith(
320
- 'Error in effect callback:',
321
- expect.any(Error),
322
- )
323
-
324
- // Cleanup
325
- effectCleanup()
326
- cleanup1()
327
- cleanup2()
328
- console.error = originalError
329
- })
330
-
331
- test('Ref - cleanup function exception handling', async () => {
332
- const ref = new Ref({ test: 'value' })
333
-
334
- // Mock console.error to capture error logs
335
- const originalError = console.error
336
- const errorSpy = mock(() => {})
337
- console.error = errorSpy
338
-
339
- let cleanup1Called = false
340
- let cleanup2Called = false
341
-
342
- // Add callbacks with cleanup functions, one throws
343
- const hookCleanup1 = ref.on(HOOK_WATCH, () => {
344
- return () => {
345
- cleanup1Called = true
346
- throw new Error('Test error in cleanup function')
347
- }
348
- })
349
-
350
- const hookCleanup2 = ref.on(HOOK_WATCH, () => {
351
- return () => {
352
- cleanup2Called = true
353
- }
354
- })
355
-
356
- // Subscribe and then unsubscribe to trigger cleanup
357
- const effectCleanup = createEffect(() => {
358
- ref.get()
359
- })
360
-
361
- // Unsubscribe to trigger cleanup functions
362
- effectCleanup()
363
-
364
- // Wait a bit for cleanup to complete
365
- await new Promise(resolve => setTimeout(resolve, 10))
366
-
367
- // Both cleanup functions should have been called despite the exception
368
- expect(cleanup1Called).toBe(true)
369
- expect(cleanup2Called).toBe(true)
370
-
371
- // Error should have been logged
372
- expect(errorSpy).toHaveBeenCalledWith(
373
- 'Error in effect cleanup:',
374
- expect.any(Error),
375
- )
376
-
377
- // Cleanup
378
- hookCleanup1()
379
- hookCleanup2()
380
- console.error = originalError
381
- })
@@ -1,154 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { Memo, resolve, State, Task, UNSET } from '../index.ts'
3
-
4
- /* === Tests === */
5
-
6
- describe('Resolve Function', () => {
7
- test('should return discriminated union for successful resolution', () => {
8
- const a = new State(10)
9
- const b = new State('hello')
10
-
11
- const result = resolve({ a, b })
12
-
13
- expect(result.ok).toBe(true)
14
- if (result.ok) {
15
- expect(result.values.a).toBe(10)
16
- expect(result.values.b).toBe('hello')
17
- expect(result.errors).toBeUndefined()
18
- expect(result.pending).toBeUndefined()
19
- }
20
- })
21
-
22
- test('should return discriminated union for pending signals', () => {
23
- const a = new State(10)
24
- const b = new State(UNSET)
25
-
26
- const result = resolve({ a, b })
27
-
28
- expect(result.ok).toBe(false)
29
- expect(result.pending).toBe(true)
30
- expect(result.values).toBeUndefined()
31
- expect(result.errors).toBeUndefined()
32
- })
33
-
34
- test('should return discriminated union for error signals', () => {
35
- const a = new State(10)
36
- const b = new Memo(() => {
37
- throw new Error('Test error')
38
- })
39
-
40
- const result = resolve({ a, b })
41
-
42
- expect(result.ok).toBe(false)
43
- expect(result.pending).toBeUndefined()
44
- expect(result.values).toBeUndefined()
45
- expect(result.errors).toBeDefined()
46
- if (result.errors) {
47
- expect(result.errors[0].message).toBe('Test error')
48
- }
49
- })
50
-
51
- test('should handle mixed error and valid signals', () => {
52
- const valid = new State('valid')
53
- const error1 = new Memo(() => {
54
- throw new Error('Error 1')
55
- })
56
- const error2 = new Memo(() => {
57
- throw new Error('Error 2')
58
- })
59
-
60
- const result = resolve({ valid, error1, error2 })
61
-
62
- expect(result.ok).toBe(false)
63
- expect(result.errors).toBeDefined()
64
- if (result.errors) {
65
- expect(result.errors).toHaveLength(2)
66
- expect(result.errors[0].message).toBe('Error 1')
67
- expect(result.errors[1].message).toBe('Error 2')
68
- }
69
- })
70
-
71
- test('should prioritize pending over errors', () => {
72
- const pending = new State(UNSET)
73
- const error = new Memo(() => {
74
- throw new Error('Test error')
75
- })
76
-
77
- const result = resolve({ pending, error })
78
-
79
- expect(result.ok).toBe(false)
80
- expect(result.pending).toBe(true)
81
- expect(result.errors).toBeUndefined()
82
- })
83
-
84
- test('should handle empty signals object', () => {
85
- const result = resolve({})
86
-
87
- expect(result.ok).toBe(true)
88
- if (result.ok) {
89
- expect(result.values).toEqual({})
90
- }
91
- })
92
-
93
- test('should handle complex nested object signals', () => {
94
- const user = new State({ name: 'Alice', age: 25 })
95
- const settings = new State({ theme: 'dark', lang: 'en' })
96
-
97
- const result = resolve({ user, settings })
98
-
99
- expect(result.ok).toBe(true)
100
- if (result.ok) {
101
- expect(result.values.user.name).toBe('Alice')
102
- expect(result.values.user.age).toBe(25)
103
- expect(result.values.settings.theme).toBe('dark')
104
- expect(result.values.settings.lang).toBe('en')
105
- }
106
- })
107
-
108
- test('should handle async computed signals that resolve', async () => {
109
- const wait = (ms: number) =>
110
- new Promise(resolve => setTimeout(resolve, ms))
111
-
112
- const asyncSignal = new Task(async () => {
113
- await wait(10)
114
- return 'async result'
115
- })
116
-
117
- // Initially should be pending
118
- let result = resolve({ asyncSignal })
119
- expect(result.ok).toBe(false)
120
- expect(result.pending).toBe(true)
121
-
122
- // Wait for resolution
123
- await wait(20)
124
-
125
- result = resolve({ asyncSignal })
126
- expect(result.ok).toBe(true)
127
- if (result.ok) expect(result.values.asyncSignal).toBe('async result')
128
- })
129
-
130
- test('should handle async computed signals that error', async () => {
131
- const wait = (ms: number) =>
132
- new Promise(resolve => setTimeout(resolve, ms))
133
-
134
- const asyncError = new Task(async () => {
135
- await wait(10)
136
- throw new Error('Async error')
137
- })
138
-
139
- // Initially should be pending
140
- let result = resolve({ asyncError })
141
- expect(result.ok).toBe(false)
142
- expect(result.pending).toBe(true)
143
-
144
- // Wait for error
145
- await wait(20)
146
-
147
- result = resolve({ asyncError })
148
- expect(result.ok).toBe(false)
149
- expect(result.errors).toBeDefined()
150
- if (result.errors) {
151
- expect(result.errors[0].message).toBe('Async error')
152
- }
153
- })
154
- })
@@ -1,47 +0,0 @@
1
- import type { Signal } from '../signal';
2
- import { type Cleanup, type Hook, type HookCallback } from '../system';
3
- import { type Computed } from './computed';
4
- import { type List } from './list';
5
- type CollectionSource<T extends {}> = List<T> | Collection<T>;
6
- type CollectionCallback<T extends {}, U extends {}> = ((sourceValue: U) => T) | ((sourceValue: U, abort: AbortSignal) => Promise<T>);
7
- type Collection<T extends {}> = {
8
- readonly [Symbol.toStringTag]: 'Collection';
9
- readonly [Symbol.isConcatSpreadable]: true;
10
- [Symbol.iterator](): IterableIterator<Signal<T>>;
11
- keys(): IterableIterator<string>;
12
- get: () => T[];
13
- at: (index: number) => Signal<T> | undefined;
14
- byKey: (key: string) => Signal<T> | undefined;
15
- keyAt: (index: number) => string | undefined;
16
- indexOfKey: (key: string) => number | undefined;
17
- on: <K extends Hook>(type: K, callback: HookCallback) => Cleanup;
18
- deriveCollection: <R extends {}>(callback: CollectionCallback<R, T>) => DerivedCollection<R, T>;
19
- readonly length: number;
20
- };
21
- declare const TYPE_COLLECTION: "Collection";
22
- declare class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
23
- #private;
24
- constructor(source: CollectionSource<U> | (() => CollectionSource<U>), callback: CollectionCallback<T, U>);
25
- get [Symbol.toStringTag](): 'Collection';
26
- get [Symbol.isConcatSpreadable](): true;
27
- [Symbol.iterator](): IterableIterator<Computed<T>>;
28
- keys(): IterableIterator<string>;
29
- get(): T[];
30
- at(index: number): Computed<T> | undefined;
31
- byKey(key: string): Computed<T> | undefined;
32
- keyAt(index: number): string | undefined;
33
- indexOfKey(key: string): number;
34
- on(type: Hook, callback: HookCallback): Cleanup;
35
- deriveCollection<R extends {}>(callback: (sourceValue: T) => R): DerivedCollection<R, T>;
36
- deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): DerivedCollection<R, T>;
37
- get length(): number;
38
- }
39
- /**
40
- * Check if a value is a collection signal
41
- *
42
- * @since 0.17.2
43
- * @param {unknown} value - Value to check
44
- * @returns {boolean} - True if value is a collection signal, false otherwise
45
- */
46
- declare const isCollection: <T extends {}>(value: unknown) => value is Collection<T>;
47
- export { type Collection, type CollectionSource, type CollectionCallback, DerivedCollection, isCollection, TYPE_COLLECTION, };
@@ -1,15 +0,0 @@
1
- import type { DiffResult, UnknownRecord } from '../diff';
2
- import type { Signal } from '../signal';
3
- import { type Cleanup, type HookCallback } from '../system';
4
- type CompositeHook = 'add' | 'change' | 'remove';
5
- declare class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
6
- #private;
7
- signals: Map<string, S>;
8
- constructor(values: T, validate: <K extends keyof T & string>(key: K, value: unknown) => value is T[K] & {}, create: <V extends T[keyof T] & {}>(value: V) => S);
9
- add<K extends keyof T & string>(key: K, value: T[K]): boolean;
10
- remove<K extends keyof T & string>(key: K): boolean;
11
- change(changes: DiffResult, initialRun?: boolean): boolean;
12
- clear(): boolean;
13
- on(type: CompositeHook, callback: HookCallback): Cleanup;
14
- }
15
- export { Composite, type CompositeHook as CompositeListeners };