@zeix/cause-effect 0.17.2 → 0.17.3
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 +11 -5
- package/.github/copilot-instructions.md +1 -1
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +18 -79
- package/README.md +23 -37
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +5 -62
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +17 -20
- package/archive/list.ts +6 -67
- package/archive/memo.ts +13 -14
- package/archive/store.ts +7 -66
- package/archive/task.ts +18 -20
- package/index.dev.js +438 -614
- package/index.js +1 -1
- package/index.ts +8 -19
- package/package.json +6 -6
- package/src/classes/collection.ts +59 -112
- package/src/classes/computed.ts +146 -189
- package/src/classes/list.ts +138 -105
- package/src/classes/ref.ts +16 -42
- package/src/classes/state.ts +16 -45
- package/src/classes/store.ts +107 -72
- package/src/effect.ts +9 -12
- package/src/errors.ts +12 -8
- package/src/signal.ts +3 -1
- package/src/system.ts +136 -154
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +46 -306
- package/test/computed.test.ts +205 -223
- package/test/list.test.ts +35 -303
- package/test/ref.test.ts +38 -66
- package/test/state.test.ts +6 -12
- package/test/store.test.ts +37 -489
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +2 -2
- package/types/src/classes/collection.d.ts +4 -6
- package/types/src/classes/computed.d.ts +17 -37
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +7 -20
- package/types/src/classes/state.d.ts +5 -17
- package/types/src/classes/store.d.ts +12 -11
- package/types/src/errors.d.ts +2 -4
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +41 -44
- package/src/classes/composite.ts +0 -171
- package/types/src/classes/composite.d.ts +0 -15
package/test/store.test.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
State,
|
|
9
9
|
UNSET,
|
|
10
10
|
} from '../index.ts'
|
|
11
|
-
import { HOOK_WATCH } from '../src/system'
|
|
12
11
|
|
|
13
12
|
describe('store', () => {
|
|
14
13
|
describe('creation and basic operations', () => {
|
|
@@ -226,114 +225,6 @@ describe('store', () => {
|
|
|
226
225
|
})
|
|
227
226
|
})
|
|
228
227
|
|
|
229
|
-
describe('Hooks', () => {
|
|
230
|
-
test('triggers HOOK_ADD when properties are added', () => {
|
|
231
|
-
let addedKeys: readonly string[] | undefined
|
|
232
|
-
const user = createStore<{ name: string; email?: string }>({
|
|
233
|
-
name: 'John',
|
|
234
|
-
})
|
|
235
|
-
user.on('add', add => {
|
|
236
|
-
addedKeys = add
|
|
237
|
-
})
|
|
238
|
-
user.add('email', 'john@example.com')
|
|
239
|
-
expect(addedKeys).toContain('email')
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test('triggers HOOK_CHANGE when properties are modified', () => {
|
|
243
|
-
const user = createStore({ name: 'John' })
|
|
244
|
-
let changedKeys: readonly string[] | undefined
|
|
245
|
-
user.on('change', change => {
|
|
246
|
-
changedKeys = change
|
|
247
|
-
})
|
|
248
|
-
user.name.set('Jane')
|
|
249
|
-
expect(changedKeys).toContain('name')
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
test('triggers HOOK_CHANGE for nested property changes', () => {
|
|
253
|
-
const user = createStore({
|
|
254
|
-
preferences: {
|
|
255
|
-
theme: 'light',
|
|
256
|
-
},
|
|
257
|
-
})
|
|
258
|
-
let changedKeys: readonly string[] | undefined
|
|
259
|
-
user.on('change', change => {
|
|
260
|
-
changedKeys = change
|
|
261
|
-
})
|
|
262
|
-
user.preferences.theme.set('dark')
|
|
263
|
-
expect(changedKeys).toContain('preferences')
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
test('triggers HOOK_REMOVE when properties are removed', () => {
|
|
267
|
-
const user = createStore({
|
|
268
|
-
name: 'John',
|
|
269
|
-
email: 'john@example.com',
|
|
270
|
-
})
|
|
271
|
-
let removedKeys: readonly string[] | undefined
|
|
272
|
-
user.on('remove', remove => {
|
|
273
|
-
removedKeys = remove
|
|
274
|
-
})
|
|
275
|
-
user.remove('email')
|
|
276
|
-
expect(removedKeys).toContain('email')
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
test('set() correctly handles mixed changes, additions, and removals', () => {
|
|
280
|
-
const user = createStore<{
|
|
281
|
-
name: string
|
|
282
|
-
email?: string
|
|
283
|
-
preferences: { theme?: string }
|
|
284
|
-
age?: number
|
|
285
|
-
}>({
|
|
286
|
-
name: 'John',
|
|
287
|
-
email: 'john@example.com',
|
|
288
|
-
preferences: {
|
|
289
|
-
theme: 'light',
|
|
290
|
-
},
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
let changedKeys: readonly string[] | undefined
|
|
294
|
-
let addedKeys: readonly string[] | undefined
|
|
295
|
-
let removedKeys: readonly string[] | undefined
|
|
296
|
-
|
|
297
|
-
user.on('change', change => {
|
|
298
|
-
changedKeys = change
|
|
299
|
-
})
|
|
300
|
-
user.on('add', add => {
|
|
301
|
-
addedKeys = add
|
|
302
|
-
})
|
|
303
|
-
user.on('remove', remove => {
|
|
304
|
-
removedKeys = remove
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
user.set({
|
|
308
|
-
name: 'Jane',
|
|
309
|
-
preferences: {
|
|
310
|
-
theme: 'dark',
|
|
311
|
-
},
|
|
312
|
-
age: 30,
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
expect(changedKeys).toContain('name')
|
|
316
|
-
expect(changedKeys).toContain('preferences')
|
|
317
|
-
expect(addedKeys).toContain('age')
|
|
318
|
-
expect(removedKeys).toContain('email')
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
test('hooks can be removed', () => {
|
|
322
|
-
const user = createStore({ name: 'John' })
|
|
323
|
-
let notificationCount = 0
|
|
324
|
-
const listener = () => {
|
|
325
|
-
notificationCount++
|
|
326
|
-
}
|
|
327
|
-
const off = user.on('change', listener)
|
|
328
|
-
user.name.set('Jane')
|
|
329
|
-
expect(notificationCount).toBe(1)
|
|
330
|
-
|
|
331
|
-
off()
|
|
332
|
-
user.name.set('Bob')
|
|
333
|
-
expect(notificationCount).toBe(1)
|
|
334
|
-
})
|
|
335
|
-
})
|
|
336
|
-
|
|
337
228
|
describe('reactivity', () => {
|
|
338
229
|
test('store-level get() is reactive', () => {
|
|
339
230
|
const user = createStore({
|
|
@@ -662,173 +553,39 @@ describe('store', () => {
|
|
|
662
553
|
})
|
|
663
554
|
})
|
|
664
555
|
|
|
665
|
-
describe('
|
|
666
|
-
test('
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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',
|
|
556
|
+
describe('Watch Callbacks', () => {
|
|
557
|
+
test('Root store watched callback triggered only by direct store access', async () => {
|
|
558
|
+
let rootStoreCounter = 0
|
|
559
|
+
let intervalId: Timer | undefined
|
|
560
|
+
const store = createStore(
|
|
561
|
+
{
|
|
562
|
+
user: {
|
|
563
|
+
name: 'John',
|
|
564
|
+
profile: {
|
|
565
|
+
email: 'john@example.com',
|
|
735
566
|
},
|
|
736
567
|
},
|
|
737
568
|
},
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
HOOK_WATCH,
|
|
746
|
-
() => {
|
|
747
|
-
intervalId = setInterval(() => {
|
|
748
|
-
counter++
|
|
749
|
-
}, 10)
|
|
750
|
-
|
|
751
|
-
return () => {
|
|
569
|
+
{
|
|
570
|
+
watched: () => {
|
|
571
|
+
intervalId = setInterval(() => {
|
|
572
|
+
rootStoreCounter++
|
|
573
|
+
}, 10)
|
|
574
|
+
},
|
|
575
|
+
unwatched: () => {
|
|
752
576
|
if (intervalId) {
|
|
753
577
|
clearInterval(intervalId)
|
|
754
578
|
intervalId = undefined
|
|
755
579
|
}
|
|
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
580
|
},
|
|
807
581
|
},
|
|
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
|
-
})
|
|
582
|
+
)
|
|
826
583
|
|
|
827
584
|
expect(rootStoreCounter).toBe(0)
|
|
828
585
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
829
586
|
expect(rootStoreCounter).toBe(0)
|
|
830
587
|
|
|
831
|
-
// Access nested property directly - should NOT trigger root
|
|
588
|
+
// Access nested property directly - should NOT trigger root watched callback
|
|
832
589
|
const nestedEffectCleanup = createEffect(() => {
|
|
833
590
|
store.user.name.get()
|
|
834
591
|
})
|
|
@@ -837,7 +594,7 @@ describe('store', () => {
|
|
|
837
594
|
expect(rootStoreCounter).toBe(0) // Still 0 - nested access doesn't trigger root
|
|
838
595
|
expect(intervalId).toBeUndefined()
|
|
839
596
|
|
|
840
|
-
// Access root store directly - should trigger
|
|
597
|
+
// Access root store directly - should trigger watched callback
|
|
841
598
|
const rootEffectCleanup = createEffect(() => {
|
|
842
599
|
store.get()
|
|
843
600
|
})
|
|
@@ -851,169 +608,37 @@ describe('store', () => {
|
|
|
851
608
|
nestedEffectCleanup()
|
|
852
609
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
853
610
|
expect(intervalId).toBeUndefined()
|
|
854
|
-
|
|
855
|
-
cleanupHookCallback()
|
|
856
611
|
})
|
|
857
612
|
|
|
858
|
-
test('
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
port: 5432,
|
|
864
|
-
},
|
|
613
|
+
test('Store property addition/removal affects store watched callback', async () => {
|
|
614
|
+
let usersStoreCounter = 0
|
|
615
|
+
const store = createStore(
|
|
616
|
+
{
|
|
617
|
+
users: {} as Record<string, { name: string }>,
|
|
865
618
|
},
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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,
|
|
619
|
+
{
|
|
620
|
+
watched: () => {
|
|
621
|
+
usersStoreCounter++
|
|
622
|
+
},
|
|
623
|
+
unwatched: () => {
|
|
624
|
+
usersStoreCounter--
|
|
625
|
+
},
|
|
954
626
|
},
|
|
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
|
-
})
|
|
627
|
+
)
|
|
1003
628
|
|
|
1004
629
|
expect(usersStoreCounter).toBe(0)
|
|
1005
630
|
|
|
1006
|
-
// Watch the
|
|
631
|
+
// Watch the entire store
|
|
1007
632
|
const usersEffect = createEffect(() => {
|
|
1008
|
-
store.
|
|
633
|
+
store.get()
|
|
1009
634
|
})
|
|
1010
635
|
expect(usersStoreCounter).toBe(1)
|
|
1011
636
|
|
|
1012
|
-
// Add a user - this modifies the users store content but doesn't affect
|
|
637
|
+
// Add a user - this modifies the users store content but doesn't affect watched callback
|
|
1013
638
|
store.users.add('user1', { name: 'Alice' })
|
|
1014
639
|
expect(usersStoreCounter).toBe(1) // Still 1
|
|
1015
640
|
|
|
1016
|
-
// Watch a specific user property - this doesn't trigger users store
|
|
641
|
+
// Watch a specific user property - this doesn't trigger users store watched callback
|
|
1017
642
|
const userEffect = createEffect(() => {
|
|
1018
643
|
store.users.user1?.name.get()
|
|
1019
644
|
})
|
|
@@ -1026,83 +651,6 @@ describe('store', () => {
|
|
|
1026
651
|
// Cleanup users effect
|
|
1027
652
|
usersEffect()
|
|
1028
653
|
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
654
|
})
|
|
1107
655
|
})
|
|
1108
656
|
})
|
|
@@ -124,7 +124,7 @@ function makeDependentRows(
|
|
|
124
124
|
): Computed<number>[][] {
|
|
125
125
|
let prevRow = sources
|
|
126
126
|
const rand = new Random('seed')
|
|
127
|
-
const rows = []
|
|
127
|
+
const rows: Computed<number>[][] = []
|
|
128
128
|
for (let l = 0; l < numRows; l++) {
|
|
129
129
|
const row = makeRow(
|
|
130
130
|
prevRow,
|
|
@@ -135,7 +135,7 @@ function makeDependentRows(
|
|
|
135
135
|
l,
|
|
136
136
|
rand,
|
|
137
137
|
)
|
|
138
|
-
rows.push(row
|
|
138
|
+
rows.push(row)
|
|
139
139
|
prevRow = row
|
|
140
140
|
}
|
|
141
141
|
return rows
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationDir": "./types",
|
|
7
|
+
"emitDeclarationOnly": true,
|
|
8
|
+
},
|
|
9
|
+
"include": ["./index.ts", "./src/**/*.ts"],
|
|
10
|
+
"exclude": ["node_modules", "types", "test", "archive"],
|
|
11
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"allowImportingTsExtensions": true,
|
|
14
14
|
"verbatimModuleSyntax": true,
|
|
15
15
|
|
|
16
|
+
// Editor-only mode - no emit
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
|
|
16
19
|
// Best practices
|
|
17
20
|
"strict": true,
|
|
18
21
|
"skipLibCheck": true,
|
|
@@ -22,12 +25,7 @@
|
|
|
22
25
|
"noUnusedLocals": false,
|
|
23
26
|
"noUnusedParameters": false,
|
|
24
27
|
"noPropertyAccessFromIndexSignature": false,
|
|
25
|
-
|
|
26
|
-
// Declarations
|
|
27
|
-
"declaration": true,
|
|
28
|
-
"declarationDir": "./types",
|
|
29
|
-
"emitDeclarationOnly": true
|
|
30
28
|
},
|
|
31
|
-
"include": ["
|
|
32
|
-
"exclude": ["node_modules", "
|
|
29
|
+
"include": ["./**/*.ts"],
|
|
30
|
+
"exclude": ["node_modules", "types"],
|
|
33
31
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.17.
|
|
3
|
+
* @version 0.17.3
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
6
|
export { type Collection, type CollectionCallback, type CollectionSource, DerivedCollection, isCollection, TYPE_COLLECTION, } from './src/classes/collection';
|
|
@@ -15,5 +15,5 @@ export { CircularDependencyError, createError, DuplicateKeyError, type Guard, gu
|
|
|
15
15
|
export { type MatchHandlers, match } from './src/match';
|
|
16
16
|
export { type ResolveResult, resolve } from './src/resolve';
|
|
17
17
|
export { createSignal, isMutableSignal, isSignal, type Signal, type SignalValues, type UnknownSignalRecord, } from './src/signal';
|
|
18
|
-
export {
|
|
18
|
+
export { batch, type Cleanup, createWatcher, flush, notifyOf, type SignalOptions, subscribeTo, track, UNSET, untrack, type Watcher, } from './src/system';
|
|
19
19
|
export { isAbortError, isAsyncFunction, isFunction, isNumber, isObjectOfType, isRecord, isRecordOrArray, isString, isSymbol, valueString, } from './src/util';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Signal } from '../signal';
|
|
2
|
-
import { type
|
|
2
|
+
import { type SignalOptions } from '../system';
|
|
3
3
|
import { type Computed } from './computed';
|
|
4
4
|
import { type List } from './list';
|
|
5
5
|
type CollectionSource<T extends {}> = List<T> | Collection<T>;
|
|
@@ -14,14 +14,13 @@ type Collection<T extends {}> = {
|
|
|
14
14
|
byKey: (key: string) => Signal<T> | undefined;
|
|
15
15
|
keyAt: (index: number) => string | undefined;
|
|
16
16
|
indexOfKey: (key: string) => number | undefined;
|
|
17
|
-
on: <K extends Hook>(type: K, callback: HookCallback) => Cleanup;
|
|
18
17
|
deriveCollection: <R extends {}>(callback: CollectionCallback<R, T>) => DerivedCollection<R, T>;
|
|
19
18
|
readonly length: number;
|
|
20
19
|
};
|
|
21
20
|
declare const TYPE_COLLECTION: "Collection";
|
|
22
21
|
declare class DerivedCollection<T extends {}, U extends {}> implements Collection<T> {
|
|
23
22
|
#private;
|
|
24
|
-
constructor(source: CollectionSource<U> | (() => CollectionSource<U>), callback: CollectionCallback<T, U>);
|
|
23
|
+
constructor(source: CollectionSource<U> | (() => CollectionSource<U>), callback: CollectionCallback<T, U>, options?: SignalOptions<T[]>);
|
|
25
24
|
get [Symbol.toStringTag](): 'Collection';
|
|
26
25
|
get [Symbol.isConcatSpreadable](): true;
|
|
27
26
|
[Symbol.iterator](): IterableIterator<Computed<T>>;
|
|
@@ -31,9 +30,8 @@ declare class DerivedCollection<T extends {}, U extends {}> implements Collectio
|
|
|
31
30
|
byKey(key: string): Computed<T> | undefined;
|
|
32
31
|
keyAt(index: number): string | undefined;
|
|
33
32
|
indexOfKey(key: string): number;
|
|
34
|
-
|
|
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>;
|
|
33
|
+
deriveCollection<R extends {}>(callback: (sourceValue: T) => R, options?: SignalOptions<R[]>): DerivedCollection<R, T>;
|
|
34
|
+
deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>, options?: SignalOptions<R[]>): DerivedCollection<R, T>;
|
|
37
35
|
get length(): number;
|
|
38
36
|
}
|
|
39
37
|
/**
|