@zeix/cause-effect 0.17.0 → 0.17.2

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 (50) hide show
  1. package/.ai-context.md +26 -5
  2. package/.cursorrules +8 -3
  3. package/.github/copilot-instructions.md +13 -4
  4. package/CLAUDE.md +191 -262
  5. package/README.md +268 -420
  6. package/archive/collection.ts +23 -25
  7. package/archive/computed.ts +5 -4
  8. package/archive/list.ts +21 -28
  9. package/archive/memo.ts +4 -2
  10. package/archive/state.ts +2 -1
  11. package/archive/store.ts +21 -32
  12. package/archive/task.ts +6 -9
  13. package/index.dev.js +411 -220
  14. package/index.js +1 -1
  15. package/index.ts +25 -8
  16. package/package.json +1 -1
  17. package/src/classes/collection.ts +103 -77
  18. package/src/classes/composite.ts +28 -33
  19. package/src/classes/computed.ts +90 -31
  20. package/src/classes/list.ts +39 -33
  21. package/src/classes/ref.ts +96 -0
  22. package/src/classes/state.ts +41 -8
  23. package/src/classes/store.ts +47 -30
  24. package/src/diff.ts +2 -1
  25. package/src/effect.ts +19 -9
  26. package/src/errors.ts +31 -1
  27. package/src/match.ts +5 -12
  28. package/src/resolve.ts +3 -2
  29. package/src/signal.ts +0 -1
  30. package/src/system.ts +159 -43
  31. package/src/util.ts +0 -10
  32. package/test/collection.test.ts +383 -67
  33. package/test/computed.test.ts +268 -11
  34. package/test/effect.test.ts +2 -2
  35. package/test/list.test.ts +249 -21
  36. package/test/ref.test.ts +381 -0
  37. package/test/state.test.ts +13 -13
  38. package/test/store.test.ts +473 -28
  39. package/types/index.d.ts +6 -5
  40. package/types/src/classes/collection.d.ts +27 -12
  41. package/types/src/classes/composite.d.ts +4 -4
  42. package/types/src/classes/computed.d.ts +17 -0
  43. package/types/src/classes/list.d.ts +6 -6
  44. package/types/src/classes/ref.d.ts +48 -0
  45. package/types/src/classes/state.d.ts +9 -0
  46. package/types/src/classes/store.d.ts +4 -4
  47. package/types/src/effect.d.ts +1 -2
  48. package/types/src/errors.d.ts +9 -1
  49. package/types/src/system.d.ts +40 -24
  50. package/types/src/util.d.ts +1 -3
@@ -8,6 +8,7 @@ import {
8
8
  State,
9
9
  UNSET,
10
10
  } from '../index.ts'
11
+ import { HOOK_WATCH } from '../src/system'
11
12
 
12
13
  describe('store', () => {
13
14
  describe('creation and basic operations', () => {
@@ -225,54 +226,54 @@ describe('store', () => {
225
226
  })
226
227
  })
227
228
 
228
- describe('change tracking and notifications', () => {
229
- test('emits add notifications', () => {
230
- let addNotification: readonly string[] = []
229
+ describe('Hooks', () => {
230
+ test('triggers HOOK_ADD when properties are added', () => {
231
+ let addedKeys: readonly string[] | undefined
231
232
  const user = createStore<{ name: string; email?: string }>({
232
233
  name: 'John',
233
234
  })
234
235
  user.on('add', add => {
235
- addNotification = add
236
+ addedKeys = add
236
237
  })
237
238
  user.add('email', 'john@example.com')
238
- expect(addNotification).toContain('email')
239
+ expect(addedKeys).toContain('email')
239
240
  })
240
241
 
241
- test('emits change notifications when properties are modified', () => {
242
+ test('triggers HOOK_CHANGE when properties are modified', () => {
242
243
  const user = createStore({ name: 'John' })
243
- let changeNotification: readonly string[] = []
244
+ let changedKeys: readonly string[] | undefined
244
245
  user.on('change', change => {
245
- changeNotification = change
246
+ changedKeys = change
246
247
  })
247
248
  user.name.set('Jane')
248
- expect(changeNotification).toContain('name')
249
+ expect(changedKeys).toContain('name')
249
250
  })
250
251
 
251
- test('emits change notifications for nested property changes', () => {
252
+ test('triggers HOOK_CHANGE for nested property changes', () => {
252
253
  const user = createStore({
253
254
  preferences: {
254
255
  theme: 'light',
255
256
  },
256
257
  })
257
- let changeNotification: readonly string[] = []
258
+ let changedKeys: readonly string[] | undefined
258
259
  user.on('change', change => {
259
- changeNotification = change
260
+ changedKeys = change
260
261
  })
261
262
  user.preferences.theme.set('dark')
262
- expect(changeNotification).toContain('preferences')
263
+ expect(changedKeys).toContain('preferences')
263
264
  })
264
265
 
265
- test('emits remove notifications when properties are removed', () => {
266
+ test('triggers HOOK_REMOVE when properties are removed', () => {
266
267
  const user = createStore({
267
268
  name: 'John',
268
269
  email: 'john@example.com',
269
270
  })
270
- let removeNotification: readonly string[] = []
271
+ let removedKeys: readonly string[] | undefined
271
272
  user.on('remove', remove => {
272
- removeNotification = remove
273
+ removedKeys = remove
273
274
  })
274
275
  user.remove('email')
275
- expect(removeNotification).toContain('email')
276
+ expect(removedKeys).toContain('email')
276
277
  })
277
278
 
278
279
  test('set() correctly handles mixed changes, additions, and removals', () => {
@@ -289,18 +290,18 @@ describe('store', () => {
289
290
  },
290
291
  })
291
292
 
292
- let changeNotification: readonly string[] = []
293
- let addNotification: readonly string[] = []
294
- let removeNotification: readonly string[] = []
293
+ let changedKeys: readonly string[] | undefined
294
+ let addedKeys: readonly string[] | undefined
295
+ let removedKeys: readonly string[] | undefined
295
296
 
296
297
  user.on('change', change => {
297
- changeNotification = change
298
+ changedKeys = change
298
299
  })
299
300
  user.on('add', add => {
300
- addNotification = add
301
+ addedKeys = add
301
302
  })
302
303
  user.on('remove', remove => {
303
- removeNotification = remove
304
+ removedKeys = remove
304
305
  })
305
306
 
306
307
  user.set({
@@ -311,13 +312,13 @@ describe('store', () => {
311
312
  age: 30,
312
313
  })
313
314
 
314
- expect(changeNotification).toContain('name')
315
- expect(changeNotification).toContain('preferences')
316
- expect(addNotification).toContain('age')
317
- expect(removeNotification).toContain('email')
315
+ expect(changedKeys).toContain('name')
316
+ expect(changedKeys).toContain('preferences')
317
+ expect(addedKeys).toContain('age')
318
+ expect(removedKeys).toContain('email')
318
319
  })
319
320
 
320
- test('notification listeners can be removed', () => {
321
+ test('hooks can be removed', () => {
321
322
  const user = createStore({ name: 'John' })
322
323
  let notificationCount = 0
323
324
  const listener = () => {
@@ -660,4 +661,448 @@ describe('store', () => {
660
661
  expect(config.database.port.get()).toBe(5432)
661
662
  })
662
663
  })
664
+
665
+ describe('HOOK_WATCH - Store Hierarchy Resource Management', () => {
666
+ test('Store HOOK_WATCH triggers for all nested stores when accessing parent', async () => {
667
+ const store = createStore({
668
+ app: {
669
+ database: {
670
+ host: 'localhost',
671
+ port: 5432,
672
+ },
673
+ cache: {
674
+ ttl: 3600,
675
+ },
676
+ },
677
+ })
678
+
679
+ let appCounter = 0
680
+ let databaseCounter = 0
681
+ let cacheCounter = 0
682
+
683
+ const appCleanup = store.app.on(HOOK_WATCH, () => {
684
+ appCounter++
685
+ return () => {
686
+ appCounter--
687
+ }
688
+ })
689
+
690
+ const databaseCleanup = store.app.database.on(HOOK_WATCH, () => {
691
+ databaseCounter++
692
+ return () => {
693
+ databaseCounter--
694
+ }
695
+ })
696
+
697
+ const cacheCleanup = store.app.cache.on(HOOK_WATCH, () => {
698
+ cacheCounter++
699
+ return () => {
700
+ cacheCounter--
701
+ }
702
+ })
703
+
704
+ // Initially no watchers
705
+ expect(appCounter).toBe(0)
706
+ expect(databaseCounter).toBe(0)
707
+ expect(cacheCounter).toBe(0)
708
+
709
+ // Access app store - should trigger ALL nested HOOK_WATCH callbacks
710
+ const appEffect = createEffect(() => {
711
+ store.app.get()
712
+ })
713
+
714
+ expect(appCounter).toBe(1)
715
+ expect(databaseCounter).toBe(1)
716
+ expect(cacheCounter).toBe(1)
717
+
718
+ // Cleanup should reset all counters
719
+ appEffect()
720
+ expect(appCounter).toBe(0)
721
+ expect(databaseCounter).toBe(0)
722
+ expect(cacheCounter).toBe(0)
723
+
724
+ appCleanup()
725
+ databaseCleanup()
726
+ cacheCleanup()
727
+ })
728
+
729
+ test('Nested store cleanup only happens when all levels are unwatched', async () => {
730
+ const store = createStore({
731
+ user: {
732
+ profile: {
733
+ settings: {
734
+ theme: 'dark',
735
+ },
736
+ },
737
+ },
738
+ })
739
+
740
+ let counter = 0
741
+ let intervalId: Timer | undefined
742
+
743
+ // Add HOOK_WATCH to deepest nested store
744
+ const settingsCleanup = store.user.profile.settings.on(
745
+ HOOK_WATCH,
746
+ () => {
747
+ intervalId = setInterval(() => {
748
+ counter++
749
+ }, 10)
750
+
751
+ return () => {
752
+ if (intervalId) {
753
+ clearInterval(intervalId)
754
+ intervalId = undefined
755
+ }
756
+ }
757
+ },
758
+ )
759
+
760
+ expect(counter).toBe(0)
761
+
762
+ // Access parent store - should trigger settings HOOK_WATCH
763
+ const parentEffect = createEffect(() => {
764
+ store.user.get()
765
+ })
766
+
767
+ await new Promise(resolve => setTimeout(resolve, 50))
768
+ expect(counter).toBeGreaterThan(0)
769
+ expect(intervalId).toBeDefined()
770
+
771
+ // Access intermediate store - settings should still be active
772
+ const profileEffect = createEffect(() => {
773
+ store.user.profile.get()
774
+ })
775
+
776
+ const counterAfterProfile = counter
777
+ await new Promise(resolve => setTimeout(resolve, 50))
778
+ expect(counter).toBeGreaterThan(counterAfterProfile)
779
+ expect(intervalId).toBeDefined()
780
+
781
+ // Remove parent watcher, but profile watcher still active
782
+ parentEffect()
783
+
784
+ const counterAfterParentRemoval = counter
785
+ await new Promise(resolve => setTimeout(resolve, 50))
786
+ expect(counter).toBeGreaterThan(counterAfterParentRemoval)
787
+ expect(intervalId).toBeDefined() // Still running
788
+
789
+ // Remove profile watcher - now should cleanup
790
+ profileEffect()
791
+
792
+ const counterAfterAllRemoval = counter
793
+ await new Promise(resolve => setTimeout(resolve, 50))
794
+ expect(counter).toBe(counterAfterAllRemoval) // Stopped
795
+ expect(intervalId).toBeUndefined()
796
+
797
+ settingsCleanup()
798
+ })
799
+
800
+ test('Root store HOOK_WATCH triggered only by direct store access', async () => {
801
+ const store = createStore({
802
+ user: {
803
+ name: 'John',
804
+ profile: {
805
+ email: 'john@example.com',
806
+ },
807
+ },
808
+ })
809
+
810
+ let rootStoreCounter = 0
811
+ let intervalId: Timer | undefined
812
+
813
+ // Add HOOK_WATCH callback to root store
814
+ const cleanupHookCallback = store.on(HOOK_WATCH, () => {
815
+ intervalId = setInterval(() => {
816
+ rootStoreCounter++
817
+ }, 10)
818
+
819
+ return () => {
820
+ if (intervalId) {
821
+ clearInterval(intervalId)
822
+ intervalId = undefined
823
+ }
824
+ }
825
+ })
826
+
827
+ expect(rootStoreCounter).toBe(0)
828
+ await new Promise(resolve => setTimeout(resolve, 50))
829
+ expect(rootStoreCounter).toBe(0)
830
+
831
+ // Access nested property directly - should NOT trigger root HOOK_WATCH
832
+ const nestedEffectCleanup = createEffect(() => {
833
+ store.user.name.get()
834
+ })
835
+
836
+ await new Promise(resolve => setTimeout(resolve, 50))
837
+ expect(rootStoreCounter).toBe(0) // Still 0 - nested access doesn't trigger root
838
+ expect(intervalId).toBeUndefined()
839
+
840
+ // Access root store directly - should trigger HOOK_WATCH
841
+ const rootEffectCleanup = createEffect(() => {
842
+ store.get()
843
+ })
844
+
845
+ await new Promise(resolve => setTimeout(resolve, 50))
846
+ expect(rootStoreCounter).toBeGreaterThan(0) // Now triggered
847
+ expect(intervalId).toBeDefined()
848
+
849
+ // Cleanup
850
+ rootEffectCleanup()
851
+ nestedEffectCleanup()
852
+ await new Promise(resolve => setTimeout(resolve, 50))
853
+ expect(intervalId).toBeUndefined()
854
+
855
+ cleanupHookCallback()
856
+ })
857
+
858
+ test('Each store level manages its own HOOK_WATCH independently', async () => {
859
+ const store = createStore({
860
+ config: {
861
+ database: {
862
+ host: 'localhost',
863
+ port: 5432,
864
+ },
865
+ },
866
+ })
867
+
868
+ let rootCounter = 0
869
+ let configCounter = 0
870
+ let databaseCounter = 0
871
+
872
+ // Add HOOK_WATCH to each level
873
+ const rootCleanup = store.on(HOOK_WATCH, () => {
874
+ rootCounter++
875
+ return () => {
876
+ rootCounter--
877
+ }
878
+ })
879
+
880
+ const configCleanup = store.config.on(HOOK_WATCH, () => {
881
+ configCounter++
882
+ return () => {
883
+ configCounter--
884
+ }
885
+ })
886
+
887
+ const databaseCleanup = store.config.database.on(HOOK_WATCH, () => {
888
+ databaseCounter++
889
+ return () => {
890
+ databaseCounter--
891
+ }
892
+ })
893
+
894
+ // All should start at 0
895
+ expect(rootCounter).toBe(0)
896
+ expect(configCounter).toBe(0)
897
+ expect(databaseCounter).toBe(0)
898
+
899
+ // Access deepest level - should NOT trigger any store HOOK_WATCH
900
+ // because we're only accessing the State signal, not calling .get() on stores
901
+ const deepEffectCleanup = createEffect(() => {
902
+ store.config.database.host.get()
903
+ })
904
+
905
+ expect(rootCounter).toBe(0)
906
+ expect(configCounter).toBe(0)
907
+ expect(databaseCounter).toBe(0)
908
+
909
+ // Access config level - should trigger config AND database HOOK_WATCH
910
+ const configEffectCleanup = createEffect(() => {
911
+ store.config.get()
912
+ })
913
+
914
+ expect(rootCounter).toBe(0)
915
+ expect(configCounter).toBe(1)
916
+ expect(databaseCounter).toBe(1) // Triggered by parent access
917
+
918
+ // Access root level - should trigger root HOOK_WATCH (config/database already active)
919
+ const rootEffectCleanup = createEffect(() => {
920
+ store.get()
921
+ })
922
+
923
+ expect(rootCounter).toBe(1)
924
+ expect(configCounter).toBe(1)
925
+ expect(databaseCounter).toBe(1)
926
+
927
+ // Cleanup in reverse order - database should stay active until config is cleaned up
928
+ rootEffectCleanup()
929
+ expect(rootCounter).toBe(0)
930
+ expect(configCounter).toBe(1)
931
+ expect(databaseCounter).toBe(1) // Still active due to config watcher
932
+
933
+ configEffectCleanup()
934
+ expect(rootCounter).toBe(0)
935
+ expect(configCounter).toBe(0)
936
+ expect(databaseCounter).toBe(0) // Now cleaned up
937
+
938
+ deepEffectCleanup()
939
+ expect(rootCounter).toBe(0)
940
+ expect(configCounter).toBe(0)
941
+ expect(databaseCounter).toBe(0)
942
+
943
+ // Cleanup hooks
944
+ rootCleanup()
945
+ configCleanup()
946
+ databaseCleanup()
947
+ })
948
+
949
+ test('Store HOOK_WATCH with multiple watchers at same level', async () => {
950
+ const store = createStore({
951
+ data: {
952
+ items: [] as string[],
953
+ count: 0,
954
+ },
955
+ })
956
+
957
+ let dataStoreCounter = 0
958
+
959
+ const dataCleanup = store.data.on(HOOK_WATCH, () => {
960
+ dataStoreCounter++
961
+ return () => {
962
+ dataStoreCounter--
963
+ }
964
+ })
965
+
966
+ expect(dataStoreCounter).toBe(0)
967
+
968
+ // Create multiple effects watching the data store
969
+ const effect1 = createEffect(() => {
970
+ store.data.get()
971
+ })
972
+ const effect2 = createEffect(() => {
973
+ store.data.get()
974
+ })
975
+
976
+ // Should only trigger once (shared resources)
977
+ expect(dataStoreCounter).toBe(1)
978
+
979
+ // Stop one effect
980
+ effect1()
981
+ expect(dataStoreCounter).toBe(1) // Still active
982
+
983
+ // Stop second effect
984
+ effect2()
985
+ expect(dataStoreCounter).toBe(0) // Now cleaned up
986
+
987
+ dataCleanup()
988
+ })
989
+
990
+ test('Store property addition/removal affects individual store HOOK_WATCH', async () => {
991
+ const store = createStore({
992
+ users: {} as Record<string, { name: string }>,
993
+ })
994
+
995
+ let usersStoreCounter = 0
996
+
997
+ const usersCleanup = store.users.on(HOOK_WATCH, () => {
998
+ usersStoreCounter++
999
+ return () => {
1000
+ usersStoreCounter--
1001
+ }
1002
+ })
1003
+
1004
+ expect(usersStoreCounter).toBe(0)
1005
+
1006
+ // Watch the users store
1007
+ const usersEffect = createEffect(() => {
1008
+ store.users.get()
1009
+ })
1010
+ expect(usersStoreCounter).toBe(1)
1011
+
1012
+ // Add a user - this modifies the users store content but doesn't affect HOOK_WATCH
1013
+ store.users.add('user1', { name: 'Alice' })
1014
+ expect(usersStoreCounter).toBe(1) // Still 1
1015
+
1016
+ // Watch a specific user property - this doesn't trigger users store HOOK_WATCH
1017
+ const userEffect = createEffect(() => {
1018
+ store.users.user1?.name.get()
1019
+ })
1020
+ expect(usersStoreCounter).toBe(1) // Still 1
1021
+
1022
+ // Cleanup user effect
1023
+ userEffect()
1024
+ expect(usersStoreCounter).toBe(1) // Still active due to usersEffect
1025
+
1026
+ // Cleanup users effect
1027
+ usersEffect()
1028
+ expect(usersStoreCounter).toBe(0) // Now cleaned up
1029
+
1030
+ usersCleanup()
1031
+ })
1032
+
1033
+ test('Exception handling in store HOOK_WATCH callbacks', async () => {
1034
+ const store = createStore({
1035
+ config: { theme: 'dark' },
1036
+ })
1037
+
1038
+ let successfulCallbackCalled = false
1039
+ let throwingCallbackCalled = false
1040
+
1041
+ // Add throwing callback
1042
+ const cleanup1 = store.on(HOOK_WATCH, () => {
1043
+ throwingCallbackCalled = true
1044
+ throw new Error('Test error in store HOOK_WATCH')
1045
+ })
1046
+
1047
+ // Add successful callback
1048
+ const cleanup2 = store.on(HOOK_WATCH, () => {
1049
+ successfulCallbackCalled = true
1050
+ return () => {
1051
+ // cleanup
1052
+ }
1053
+ })
1054
+
1055
+ // Trigger callbacks through direct store access - should throw
1056
+ expect(() => store.get()).toThrow('Test error in store HOOK_WATCH')
1057
+
1058
+ // Both callbacks should have been called
1059
+ expect(throwingCallbackCalled).toBe(true)
1060
+ expect(successfulCallbackCalled).toBe(true)
1061
+
1062
+ cleanup1()
1063
+ cleanup2()
1064
+ })
1065
+
1066
+ test('Nested store HOOK_WATCH with computed signals', async () => {
1067
+ const store = createStore({
1068
+ user: {
1069
+ firstName: 'John',
1070
+ lastName: 'Doe',
1071
+ },
1072
+ })
1073
+
1074
+ let userStoreCounter = 0
1075
+
1076
+ const userCleanup = store.user.on(HOOK_WATCH, () => {
1077
+ userStoreCounter++
1078
+ return () => {
1079
+ userStoreCounter--
1080
+ }
1081
+ })
1082
+
1083
+ expect(userStoreCounter).toBe(0)
1084
+
1085
+ // Access user store directly - should trigger user store HOOK_WATCH
1086
+ const userEffect = createEffect(() => {
1087
+ store.user.get()
1088
+ })
1089
+ expect(userStoreCounter).toBe(1)
1090
+
1091
+ // Access individual properties - should NOT trigger user store HOOK_WATCH again
1092
+ const nameEffect = createEffect(() => {
1093
+ store.user.firstName.get()
1094
+ })
1095
+ expect(userStoreCounter).toBe(1) // Still 1
1096
+
1097
+ // Cleanup individual property effect first
1098
+ nameEffect()
1099
+ expect(userStoreCounter).toBe(1) // Still active due to user store effect
1100
+
1101
+ // Cleanup user store effect - now should be cleaned up
1102
+ userEffect()
1103
+ expect(userStoreCounter).toBe(0) // Now cleaned up
1104
+
1105
+ userCleanup()
1106
+ })
1107
+ })
663
1108
  })
package/types/index.d.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.17.0
3
+ * @version 0.17.2
4
4
  * @author Esther Brunner
5
5
  */
6
- export { Collection, type CollectionCallback, isCollection, TYPE_COLLECTION, } from './src/classes/collection';
6
+ export { type Collection, type CollectionCallback, type CollectionSource, DerivedCollection, isCollection, TYPE_COLLECTION, } from './src/classes/collection';
7
7
  export { type Computed, createComputed, isComputed, isMemoCallback, isTaskCallback, Memo, type MemoCallback, Task, type TaskCallback, TYPE_COMPUTED, } from './src/classes/computed';
8
8
  export { type ArrayToRecord, isList, type KeyConfig, List, TYPE_LIST, } from './src/classes/list';
9
+ export { isRef, Ref, TYPE_REF } from './src/classes/ref';
9
10
  export { isState, State, TYPE_STATE } from './src/classes/state';
10
11
  export { BaseStore, createStore, isStore, type Store, TYPE_STORE, } from './src/classes/store';
11
12
  export { type DiffResult, diff, isEqual, type UnknownArray, type UnknownRecord, } from './src/diff';
12
13
  export { createEffect, type EffectCallback, type MaybeCleanup, } from './src/effect';
13
- export { CircularDependencyError, DuplicateKeyError, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, } from './src/errors';
14
+ export { CircularDependencyError, createError, DuplicateKeyError, type Guard, guardMutableSignal, InvalidCallbackError, InvalidCollectionSourceError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, validateCallback, validateSignalValue, } from './src/errors';
14
15
  export { type MatchHandlers, match } from './src/match';
15
16
  export { type ResolveResult, resolve } from './src/resolve';
16
17
  export { createSignal, isMutableSignal, isSignal, type Signal, type SignalValues, type UnknownSignalRecord, } from './src/signal';
17
- export { batchSignalWrites, type Cleanup, createWatcher, emitNotification, flushPendingReactions, type Listener, type Listeners, type Notifications, notifyWatchers, subscribeActiveWatcher, trackSignalReads, type Watcher, } from './src/system';
18
- export { isAbortError, isAsyncFunction, isFunction, isNumber, isObjectOfType, isRecord, isRecordOrArray, isString, isSymbol, toError, UNSET, valueString, } from './src/util';
18
+ export { batchSignalWrites, type Cleanup, createWatcher, flushPendingReactions, HOOK_ADD, HOOK_CHANGE, HOOK_CLEANUP, HOOK_REMOVE, HOOK_SORT, HOOK_WATCH, type Hook, type CleanupHook, type WatchHook, type HookCallback, type HookCallbacks, isHandledHook, notifyWatchers, subscribeActiveWatcher, trackSignalReads, triggerHook, UNSET, type Watcher, } from './src/system';
19
+ export { isAbortError, isAsyncFunction, isFunction, isNumber, isObjectOfType, isRecord, isRecordOrArray, isString, isSymbol, valueString, } from './src/util';
@@ -1,32 +1,47 @@
1
- import { type Cleanup, type Listener, type Listeners } from '../system';
1
+ import type { Signal } from '../signal';
2
+ import { type Cleanup, type Hook, type HookCallback } from '../system';
2
3
  import { type Computed } from './computed';
3
4
  import { type List } from './list';
4
- type CollectionSource<T extends {}> = List<T> | Collection<T, any>;
5
+ type CollectionSource<T extends {}> = List<T> | Collection<T>;
5
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
+ };
6
21
  declare const TYPE_COLLECTION: "Collection";
7
- declare class Collection<T extends {}, U extends {}> {
22
+ declare class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
8
23
  #private;
9
24
  constructor(source: CollectionSource<U> | (() => CollectionSource<U>), callback: CollectionCallback<T, U>);
10
25
  get [Symbol.toStringTag](): 'Collection';
11
- get [Symbol.isConcatSpreadable](): boolean;
26
+ get [Symbol.isConcatSpreadable](): true;
12
27
  [Symbol.iterator](): IterableIterator<Computed<T>>;
13
- get length(): number;
28
+ keys(): IterableIterator<string>;
14
29
  get(): T[];
15
30
  at(index: number): Computed<T> | undefined;
16
- keys(): IterableIterator<string>;
17
31
  byKey(key: string): Computed<T> | undefined;
18
32
  keyAt(index: number): string | undefined;
19
33
  indexOfKey(key: string): number;
20
- on<K extends keyof Listeners>(type: K, listener: Listener<K>): Cleanup;
21
- deriveCollection<R extends {}>(callback: (sourceValue: T) => R): Collection<R, T>;
22
- deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R, T>;
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;
23
38
  }
24
39
  /**
25
40
  * Check if a value is a collection signal
26
41
  *
27
- * @since 0.17.0
42
+ * @since 0.17.2
28
43
  * @param {unknown} value - Value to check
29
44
  * @returns {boolean} - True if value is a collection signal, false otherwise
30
45
  */
31
- declare const isCollection: <T extends {}, U extends {}>(value: unknown) => value is Collection<T, U>;
32
- export { Collection, type CollectionSource, type CollectionCallback, isCollection, TYPE_COLLECTION, };
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,7 +1,7 @@
1
1
  import type { DiffResult, UnknownRecord } from '../diff';
2
2
  import type { Signal } from '../signal';
3
- import { type Cleanup, type Listener, type Listeners } from '../system';
4
- type CompositeListeners = Pick<Listeners, 'add' | 'change' | 'remove'>;
3
+ import { type Cleanup, type HookCallback } from '../system';
4
+ type CompositeHook = 'add' | 'change' | 'remove';
5
5
  declare class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
6
6
  #private;
7
7
  signals: Map<string, S>;
@@ -10,6 +10,6 @@ declare class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {
10
10
  remove<K extends keyof T & string>(key: K): boolean;
11
11
  change(changes: DiffResult, initialRun?: boolean): boolean;
12
12
  clear(): boolean;
13
- on<K extends keyof CompositeListeners>(type: K, listener: Listener<K>): Cleanup;
13
+ on(type: CompositeHook, callback: HookCallback): Cleanup;
14
14
  }
15
- export { Composite, type CompositeListeners };
15
+ export { Composite, type CompositeHook as CompositeListeners };