@zeix/cause-effect 0.15.2 → 0.16.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.
@@ -1,36 +1,35 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- computed,
4
- effect,
3
+ createComputed,
4
+ createEffect,
5
+ createState,
6
+ createStore,
5
7
  isStore,
6
8
  type State,
7
- type StoreAddEvent,
8
- type StoreChangeEvent,
9
- type StoreRemoveEvent,
10
- type StoreSortEvent,
11
- state,
12
- store,
13
9
  UNSET,
14
10
  } from '..'
15
11
 
16
12
  describe('store', () => {
17
13
  describe('creation and basic operations', () => {
18
14
  test('creates a store with initial values', () => {
19
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
15
+ const user = createStore({
16
+ name: 'Hannah',
17
+ email: 'hannah@example.com',
18
+ })
20
19
 
21
20
  expect(user.name.get()).toBe('Hannah')
22
21
  expect(user.email.get()).toBe('hannah@example.com')
23
22
  })
24
23
 
25
24
  test('has Symbol.toStringTag of Store', () => {
26
- const s = store({ a: 1 })
25
+ const s = createStore({ a: 1 })
27
26
  expect(s[Symbol.toStringTag]).toBe('Store')
28
27
  })
29
28
 
30
29
  test('isStore identifies store instances correctly', () => {
31
- const s = store({ a: 1 })
32
- const st = state(1)
33
- const c = computed(() => 1)
30
+ const s = createStore({ a: 1 })
31
+ const st = createState(1)
32
+ const c = createComputed(() => 1)
34
33
 
35
34
  expect(isStore(s)).toBe(true)
36
35
  expect(isStore(st)).toBe(false)
@@ -40,14 +39,19 @@ describe('store', () => {
40
39
  })
41
40
 
42
41
  test('get() returns the complete store value', () => {
43
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
42
+ const user = createStore({
43
+ name: 'Hannah',
44
+ email: 'hannah@example.com',
45
+ })
44
46
 
45
47
  expect(user.get()).toEqual({
46
48
  name: 'Hannah',
47
49
  email: 'hannah@example.com',
48
50
  })
49
51
 
50
- const participants = store<{ name: string; tags: string[] }[]>([
52
+ const participants = createStore<
53
+ { name: string; tags: string[] }[]
54
+ >([
51
55
  { name: 'Alice', tags: ['friends', 'mates'] },
52
56
  { name: 'Bob', tags: ['friends'] },
53
57
  ])
@@ -60,7 +64,7 @@ describe('store', () => {
60
64
 
61
65
  describe('proxy data access and modification', () => {
62
66
  test('properties can be accessed and modified via signals', () => {
63
- const user = store({ name: 'Hannah', age: 25 })
67
+ const user = createStore({ name: 'Hannah', age: 25 })
64
68
 
65
69
  // Get signals from store proxy
66
70
  expect(user.name.get()).toBe('Hannah')
@@ -75,14 +79,14 @@ describe('store', () => {
75
79
  })
76
80
 
77
81
  test('returns undefined for non-existent properties', () => {
78
- const user = store({ name: 'Hannah' })
82
+ const user = createStore({ name: 'Hannah' })
79
83
 
80
84
  // @ts-expect-error accessing non-existent property
81
85
  expect(user.nonExistent).toBeUndefined()
82
86
  })
83
87
 
84
88
  test('supports numeric key access', () => {
85
- const items = store({ '0': 'first', '1': 'second' })
89
+ const items = createStore({ '0': 'first', '1': 'second' })
86
90
 
87
91
  expect(items[0].get()).toBe('first')
88
92
  expect(items['0'].get()).toBe('first')
@@ -91,7 +95,7 @@ describe('store', () => {
91
95
  })
92
96
 
93
97
  test('can add new properties via add method', () => {
94
- const user = store<{ name: string; email?: string }>({
98
+ const user = createStore<{ name: string; email?: string }>({
95
99
  name: 'Hannah',
96
100
  })
97
101
 
@@ -105,7 +109,7 @@ describe('store', () => {
105
109
  })
106
110
 
107
111
  test('can remove existing properties via remove method', () => {
108
- const user = store<{ name: string; email?: string }>({
112
+ const user = createStore<{ name: string; email?: string }>({
109
113
  name: 'Hannah',
110
114
  email: 'hannah@example.com',
111
115
  })
@@ -121,7 +125,7 @@ describe('store', () => {
121
125
  })
122
126
 
123
127
  test('add method prevents null values', () => {
124
- const user = store<{ name: string; tags?: string[] }>({
128
+ const user = createStore<{ name: string; tags?: string[] }>({
125
129
  name: 'Alice',
126
130
  })
127
131
 
@@ -136,7 +140,7 @@ describe('store', () => {
136
140
 
137
141
  describe('nested stores', () => {
138
142
  test('creates nested stores for object properties', () => {
139
- const user = store({
143
+ const user = createStore({
140
144
  name: 'Hannah',
141
145
  preferences: {
142
146
  theme: 'dark',
@@ -150,7 +154,7 @@ describe('store', () => {
150
154
  })
151
155
 
152
156
  test('nested properties are reactive', () => {
153
- const user = store({
157
+ const user = createStore({
154
158
  preferences: {
155
159
  theme: 'dark',
156
160
  },
@@ -162,7 +166,7 @@ describe('store', () => {
162
166
  })
163
167
 
164
168
  test('deeply nested stores work correctly', () => {
165
- const config = store({
169
+ const config = createStore({
166
170
  ui: {
167
171
  theme: {
168
172
  colors: {
@@ -180,7 +184,10 @@ describe('store', () => {
180
184
 
181
185
  describe('set() and update() methods', () => {
182
186
  test('set() replaces entire store value', () => {
183
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
187
+ const user = createStore({
188
+ name: 'Hannah',
189
+ email: 'hannah@example.com',
190
+ })
184
191
 
185
192
  user.set({ name: 'Alice', email: 'alice@example.com' })
186
193
 
@@ -191,7 +198,7 @@ describe('store', () => {
191
198
  })
192
199
 
193
200
  test('update() modifies store using function', () => {
194
- const user = store({ name: 'Hannah', age: 25 })
201
+ const user = createStore({ name: 'Hannah', age: 25 })
195
202
 
196
203
  user.update(prev => ({ ...prev, age: prev.age + 1 }))
197
204
 
@@ -204,7 +211,7 @@ describe('store', () => {
204
211
 
205
212
  describe('iterator protocol', () => {
206
213
  test('supports for...of iteration', () => {
207
- const user = store({ name: 'Hannah', age: 25 })
214
+ const user = createStore({ name: 'Hannah', age: 25 })
208
215
  const entries: Array<[string, unknown & {}]> = []
209
216
 
210
217
  for (const [key, signal] of user) {
@@ -218,7 +225,7 @@ describe('store', () => {
218
225
 
219
226
  describe('change tracking', () => {
220
227
  test('tracks size changes', () => {
221
- const user = store<{ name: string; email?: string }>({
228
+ const user = createStore<{ name: string; email?: string }>({
222
229
  name: 'Hannah',
223
230
  })
224
231
 
@@ -231,149 +238,324 @@ describe('store', () => {
231
238
  expect(user.size.get()).toBe(1)
232
239
  })
233
240
 
234
- test('dispatches store-add event on initial creation', async () => {
235
- let addEvent: StoreAddEvent<{ name: string }> | null = null
236
- const user = store({ name: 'Hannah' })
241
+ test('emits an add notification on initial creation', async () => {
242
+ let addNotification: Record<string, string> | null = null
243
+ const user = createStore({ name: 'Hannah' })
237
244
 
238
- user.addEventListener('store-add', event => {
239
- addEvent = event
245
+ user.on('add', change => {
246
+ addNotification = change
240
247
  })
241
248
 
242
249
  // Wait for the async initial event
243
250
  await new Promise(resolve => setTimeout(resolve, 10))
244
251
 
245
- expect(addEvent).toBeTruthy()
252
+ expect(addNotification).toBeTruthy()
246
253
  // biome-ignore lint/style/noNonNullAssertion: test
247
- expect(addEvent!.detail).toEqual({ name: 'Hannah' })
254
+ expect(addNotification!).toEqual({ name: 'Hannah' })
248
255
  })
249
256
 
250
- test('dispatches store-add event for new properties', () => {
251
- const user = store<{ name: string; email?: string }>({
257
+ test('emits an add notification for new properties', () => {
258
+ const user = createStore<{ name: string; email?: string }>({
252
259
  name: 'Hannah',
253
260
  })
254
261
 
255
- let addEvent: StoreAddEvent<{
256
- name: string
257
- email?: string
258
- }> | null = null
259
- user.addEventListener('store-add', event => {
260
- addEvent = event
262
+ let addNotification: Record<string, string> | null = null
263
+ user.on('add', change => {
264
+ addNotification = change
261
265
  })
262
266
 
263
267
  user.update(v => ({ ...v, email: 'hannah@example.com' }))
264
268
 
265
- expect(addEvent).toBeTruthy()
269
+ expect(addNotification).toBeTruthy()
266
270
  // biome-ignore lint/style/noNonNullAssertion: test
267
- expect(addEvent!.detail).toEqual({
271
+ expect(addNotification!).toEqual({
268
272
  email: 'hannah@example.com',
269
273
  })
270
274
  })
271
275
 
272
- test('dispatches store-change event for property changes', () => {
273
- const user = store({ name: 'Hannah' })
276
+ test('emits a change notification for property changes', () => {
277
+ const user = createStore({ name: 'Hannah' })
274
278
 
275
- let changeEvent: StoreChangeEvent<{ name: string }> | null = null
276
- user.addEventListener('store-change', event => {
277
- changeEvent = event
279
+ let changeNotification: Record<string, string> | null = null
280
+ user.on('change', change => {
281
+ changeNotification = change
278
282
  })
279
283
 
280
284
  user.set({ name: 'Alice' })
281
285
 
282
- expect(changeEvent).toBeTruthy()
286
+ expect(changeNotification).toBeTruthy()
283
287
  // biome-ignore lint/style/noNonNullAssertion: test
284
- expect(changeEvent!.detail).toEqual({
288
+ expect(changeNotification!).toEqual({
285
289
  name: 'Alice',
286
290
  })
287
291
  })
288
292
 
289
- test('dispatches store-change event for signal changes', () => {
290
- const user = store({ name: 'Hannah' })
293
+ test('emits a change notification for signal changes', () => {
294
+ const user = createStore({ name: 'Hannah' })
291
295
 
292
- let changeEvent: StoreChangeEvent<{ name: string }> | null = null
293
- user.addEventListener('store-change', event => {
294
- changeEvent = event
296
+ let changeNotification: Record<string, string> | null = null
297
+ user.on('change', change => {
298
+ changeNotification = change
295
299
  })
296
300
 
297
301
  user.name.set('Bob')
298
302
 
299
- expect(changeEvent).toBeTruthy()
303
+ expect(changeNotification).toBeTruthy()
300
304
  // biome-ignore lint/style/noNonNullAssertion: test
301
- expect(changeEvent!.detail).toEqual({
305
+ expect(changeNotification!).toEqual({
302
306
  name: 'Bob',
303
307
  })
304
308
  })
305
309
 
306
- test('dispatches store-remove event for removed properties', () => {
307
- const user = store<{ name: string; email?: string }>({
310
+ test('emits a change notification when nested properties change', () => {
311
+ const user = createStore({
312
+ name: 'Hannah',
313
+ preferences: {
314
+ theme: 'dark',
315
+ notifications: true,
316
+ },
317
+ })
318
+
319
+ let changeNotification: Record<string, unknown> = {}
320
+ user.on('change', change => {
321
+ changeNotification = change
322
+ })
323
+
324
+ // Change a nested property
325
+ user.preferences.theme.set('light')
326
+
327
+ expect(changeNotification).toBeTruthy()
328
+ // Should notify about the direct child "preferences" that contains the changed nested property
329
+ expect(changeNotification).toEqual({
330
+ preferences: {
331
+ theme: 'light',
332
+ notifications: true,
333
+ },
334
+ })
335
+ })
336
+
337
+ test('emits a change notification for deeply nested property changes', () => {
338
+ const config = createStore({
339
+ ui: {
340
+ theme: {
341
+ colors: {
342
+ primary: 'blue',
343
+ secondary: 'green',
344
+ },
345
+ mode: 'dark',
346
+ },
347
+ },
348
+ })
349
+
350
+ let changeNotification: Record<string, unknown> = {}
351
+ config.on('change', change => {
352
+ changeNotification = change
353
+ })
354
+
355
+ // Change a deeply nested property (3 levels deep)
356
+ config.ui.theme.colors.primary.set('red')
357
+
358
+ expect(changeNotification).toBeTruthy()
359
+ // Should notify about the direct child "ui" that contains the changed nested structure
360
+ expect(changeNotification).toEqual({
361
+ ui: {
362
+ theme: {
363
+ colors: {
364
+ primary: 'red',
365
+ secondary: 'green',
366
+ },
367
+ mode: 'dark',
368
+ },
369
+ },
370
+ })
371
+ })
372
+
373
+ test('emits a remove notification when nested properties are removed', () => {
374
+ const user = createStore<{
375
+ name: string
376
+ preferences?: {
377
+ theme: string
378
+ notifications: boolean
379
+ }
380
+ }>({
381
+ name: 'Hannah',
382
+ preferences: {
383
+ theme: 'dark',
384
+ notifications: true,
385
+ },
386
+ })
387
+
388
+ let removeNotification: Record<string, unknown> = {}
389
+ user.on('remove', remove => {
390
+ removeNotification = remove
391
+ })
392
+
393
+ // Remove the entire preferences object by setting the store without it
394
+ user.set({ name: 'Hannah' })
395
+
396
+ expect(removeNotification).toBeTruthy()
397
+ // Should notify about the removal of the preferences property
398
+ expect(removeNotification).toEqual({
399
+ preferences: expect.anything(), // The actual structure doesn't matter, just that it was removed
400
+ })
401
+ })
402
+
403
+ test('set() correctly handles mixed changes, additions, and removals', () => {
404
+ const user = createStore<{
405
+ name: string
406
+ email?: string
407
+ preferences?: {
408
+ theme: string
409
+ notifications?: boolean
410
+ }
411
+ age?: number
412
+ }>({
308
413
  name: 'Hannah',
309
414
  email: 'hannah@example.com',
415
+ preferences: {
416
+ theme: 'dark',
417
+ },
310
418
  })
311
419
 
312
- let removeEvent: StoreRemoveEvent<{
420
+ let changeNotification: Record<string, unknown> = {}
421
+ let addNotification: Record<string, unknown> = {}
422
+ let removeNotification: Record<string, unknown> = {}
423
+
424
+ user.on('change', change => {
425
+ changeNotification = change
426
+ })
427
+ user.on('add', add => {
428
+ addNotification = add
429
+ })
430
+ user.on('remove', remove => {
431
+ removeNotification = remove
432
+ })
433
+
434
+ // Perform a set() that changes name, removes email, adds age, and keeps preferences
435
+ user.set({
436
+ name: 'Alice', // changed
437
+ preferences: {
438
+ theme: 'dark', // unchanged nested
439
+ },
440
+ age: 30, // added
441
+ // email removed
442
+ })
443
+
444
+ // Should emit change notification for changed properties
445
+ expect(changeNotification).toEqual({
446
+ name: 'Alice',
447
+ })
448
+
449
+ // Should emit add notification for new properties
450
+ expect(addNotification).toEqual({
451
+ age: 30,
452
+ })
453
+
454
+ // Should emit remove notification for removed properties
455
+ expect(removeNotification).toEqual({
456
+ email: expect.anything(),
457
+ })
458
+ })
459
+
460
+ test('set() with only removals emits only remove notifications', () => {
461
+ const user = createStore<{
313
462
  name: string
314
463
  email?: string
315
- }> | null = null
316
- user.addEventListener('store-remove', event => {
317
- removeEvent = event
464
+ age?: number
465
+ }>({
466
+ name: 'Hannah',
467
+ email: 'hannah@example.com',
468
+ age: 25,
469
+ })
470
+
471
+ let changeNotification: Record<string, unknown> | null = null
472
+ let removeNotification: Record<string, unknown> = {}
473
+
474
+ user.on('change', change => {
475
+ changeNotification = change
476
+ })
477
+ user.on('remove', remove => {
478
+ removeNotification = remove
479
+ })
480
+
481
+ // Set to a subset that only removes properties (no changes)
482
+ user.set({ name: 'Hannah' }) // same name, removes email and age
483
+
484
+ // Should NOT emit change notification since name didn't change
485
+ expect(changeNotification).toBe(null)
486
+
487
+ // Should emit remove notification for removed properties
488
+ expect(removeNotification).toEqual({
489
+ email: expect.anything(),
490
+ age: expect.anything(),
491
+ })
492
+ })
493
+
494
+ test('emits a remove notification for removed properties', () => {
495
+ const user = createStore<{ name: string; email?: string }>({
496
+ name: 'Hannah',
497
+ email: 'hannah@example.com',
498
+ })
499
+
500
+ let removeNotification: Record<string, string> | null = null
501
+ user.on('remove', change => {
502
+ removeNotification = change
318
503
  })
319
504
 
320
505
  user.remove('email')
321
506
 
322
- expect(removeEvent).toBeTruthy()
507
+ expect(removeNotification).toBeTruthy()
323
508
  // biome-ignore lint/style/noNonNullAssertion: test
324
- expect(removeEvent!.detail.email).toBe(UNSET)
509
+ expect(removeNotification!.email).toBe(UNSET)
325
510
  })
326
511
 
327
- test('dispatches store-add event when using add method', () => {
328
- const user = store<{ name: string; email?: string }>({
512
+ test('emits an add notification when using add method', () => {
513
+ const user = createStore<{ name: string; email?: string }>({
329
514
  name: 'Hannah',
330
515
  })
331
516
 
332
- let addEvent: StoreAddEvent<{
333
- name: string
334
- email?: string
335
- }> | null = null
336
- user.addEventListener('store-add', event => {
337
- addEvent = event
517
+ let addNotification: Record<string, string> | null = null
518
+ user.on('add', change => {
519
+ addNotification = change
338
520
  })
339
521
 
340
522
  user.add('email', 'hannah@example.com')
341
523
 
342
- expect(addEvent).toBeTruthy()
524
+ expect(addNotification).toBeTruthy()
343
525
  // biome-ignore lint/style/noNonNullAssertion: test
344
- expect(addEvent!.detail).toEqual({
526
+ expect(addNotification!).toEqual({
345
527
  email: 'hannah@example.com',
346
528
  })
347
529
  })
348
530
 
349
- test('can remove event listeners', () => {
350
- const user = store({ name: 'Hannah' })
531
+ test('can remove notification listeners', () => {
532
+ const user = createStore({ name: 'Hannah' })
351
533
 
352
- let eventCount = 0
534
+ let notificationCount = 0
353
535
  const listener = () => {
354
- eventCount++
536
+ notificationCount++
355
537
  }
356
538
 
357
- user.addEventListener('store-change', listener)
539
+ const off = user.on('change', listener)
358
540
  user.name.set('Alice')
359
- expect(eventCount).toBe(1)
541
+ expect(notificationCount).toBe(1)
360
542
 
361
- user.removeEventListener('store-change', listener)
543
+ off()
362
544
  user.name.set('Bob')
363
- expect(eventCount).toBe(1) // Should not increment
545
+ expect(notificationCount).toBe(1) // Should not increment
364
546
  })
365
547
 
366
- test('supports multiple event listeners for the same event', () => {
367
- const user = store({ name: 'Hannah' })
548
+ test('supports multiple notification listeners for the same type', () => {
549
+ const user = createStore({ name: 'Hannah' })
368
550
 
369
551
  let listener1Called = false
370
552
  let listener2Called = false
371
553
 
372
- user.addEventListener('store-change', () => {
554
+ user.on('change', () => {
373
555
  listener1Called = true
374
556
  })
375
557
 
376
- user.addEventListener('store-change', () => {
558
+ user.on('change', () => {
377
559
  listener2Called = true
378
560
  })
379
561
 
@@ -386,10 +568,13 @@ describe('store', () => {
386
568
 
387
569
  describe('reactivity', () => {
388
570
  test('store-level get() is reactive', () => {
389
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
571
+ const user = createStore({
572
+ name: 'Hannah',
573
+ email: 'hannah@example.com',
574
+ })
390
575
  let lastValue = { name: '', email: '' }
391
576
 
392
- effect(() => {
577
+ createEffect(() => {
393
578
  lastValue = user.get()
394
579
  })
395
580
 
@@ -402,14 +587,17 @@ describe('store', () => {
402
587
  })
403
588
 
404
589
  test('individual signal reactivity works', () => {
405
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
590
+ const user = createStore({
591
+ name: 'Hannah',
592
+ email: 'hannah@example.com',
593
+ })
406
594
  let lastName = ''
407
595
  let nameEffectRuns = 0
408
596
 
409
597
  // Get signal for name property directly
410
598
  const nameSignal = user.name
411
599
 
412
- effect(() => {
600
+ createEffect(() => {
413
601
  lastName = nameSignal.get()
414
602
  nameEffectRuns++
415
603
  })
@@ -421,14 +609,14 @@ describe('store', () => {
421
609
  })
422
610
 
423
611
  test('nested store changes propagate to parent', () => {
424
- const user = store({
612
+ const user = createStore({
425
613
  preferences: {
426
614
  theme: 'dark',
427
615
  },
428
616
  })
429
617
  let effectRuns = 0
430
618
 
431
- effect(() => {
619
+ createEffect(() => {
432
620
  user.get() // Watch entire store
433
621
  effectRuns++
434
622
  })
@@ -438,13 +626,13 @@ describe('store', () => {
438
626
  })
439
627
 
440
628
  test('updates are reactive', () => {
441
- const user = store<{ name: string; email?: string }>({
629
+ const user = createStore<{ name: string; email?: string }>({
442
630
  name: 'Hannah',
443
631
  })
444
632
  let lastValue = {}
445
633
  let effectRuns = 0
446
634
 
447
- effect(() => {
635
+ createEffect(() => {
448
636
  lastValue = user.get()
449
637
  effectRuns++
450
638
  })
@@ -458,14 +646,14 @@ describe('store', () => {
458
646
  })
459
647
 
460
648
  test('remove method is reactive', () => {
461
- const user = store<{ name: string; email?: string }>({
649
+ const user = createStore<{ name: string; email?: string }>({
462
650
  name: 'Hannah',
463
651
  email: 'hannah@example.com',
464
652
  })
465
653
  let lastValue = {}
466
654
  let effectRuns = 0
467
655
 
468
- effect(() => {
656
+ createEffect(() => {
469
657
  lastValue = user.get()
470
658
  effectRuns++
471
659
  })
@@ -480,7 +668,7 @@ describe('store', () => {
480
668
  })
481
669
 
482
670
  test('add method does not overwrite existing properties', () => {
483
- const user = store<{ name: string; email?: string }>({
671
+ const user = createStore<{ name: string; email?: string }>({
484
672
  name: 'Hannah',
485
673
  email: 'original@example.com',
486
674
  })
@@ -498,7 +686,7 @@ describe('store', () => {
498
686
  })
499
687
 
500
688
  test('remove method has no effect on non-existent properties', () => {
501
- const user = store<{ name: string; email?: string }>({
689
+ const user = createStore<{ name: string; email?: string }>({
502
690
  name: 'Hannah',
503
691
  })
504
692
 
@@ -511,9 +699,9 @@ describe('store', () => {
511
699
 
512
700
  describe('computed integration', () => {
513
701
  test('works with computed signals', () => {
514
- const user = store({ firstName: 'Hannah', lastName: 'Smith' })
702
+ const user = createStore({ firstName: 'Hannah', lastName: 'Smith' })
515
703
 
516
- const fullName = computed(() => {
704
+ const fullName = createComputed(() => {
517
705
  return `${user.firstName.get()} ${user.lastName.get()}`
518
706
  })
519
707
 
@@ -524,13 +712,13 @@ describe('store', () => {
524
712
  })
525
713
 
526
714
  test('computed reacts to nested store changes', () => {
527
- const config = store({
715
+ const config = createStore({
528
716
  ui: {
529
717
  theme: 'dark',
530
718
  },
531
719
  })
532
720
 
533
- const themeDisplay = computed(() => {
721
+ const themeDisplay = createComputed(() => {
534
722
  return `Theme: ${config.ui.theme.get()}`
535
723
  })
536
724
 
@@ -544,11 +732,11 @@ describe('store', () => {
544
732
  describe('array-derived stores with computed sum', () => {
545
733
  test('computes sum correctly and updates when items are added, removed, or changed', () => {
546
734
  // Create a store with an array of numbers
547
- const numbers = store([1, 2, 3, 4, 5])
735
+ const numbers = createStore([1, 2, 3, 4, 5])
548
736
 
549
737
  // Create a computed that calculates the sum by accessing the array via .get()
550
738
  // This ensures reactivity to both value changes and structural changes
551
- const sum = computed(() => {
739
+ const sum = createComputed(() => {
552
740
  const array = numbers.get()
553
741
  if (!Array.isArray(array)) return 0
554
742
  return array.reduce((acc, num) => acc + num, 0)
@@ -592,9 +780,9 @@ describe('store', () => {
592
780
 
593
781
  test('handles empty array and single element operations', () => {
594
782
  // Start with empty array
595
- const numbers = store<number[]>([])
783
+ const numbers = createStore<number[]>([])
596
784
 
597
- const sum = computed(() => {
785
+ const sum = createComputed(() => {
598
786
  const array = numbers.get()
599
787
  if (!Array.isArray(array)) return 0
600
788
  return array.reduce((acc, num) => acc + num, 0)
@@ -620,10 +808,10 @@ describe('store', () => {
620
808
  })
621
809
 
622
810
  test('computed sum using store iteration with size tracking', () => {
623
- const numbers = store([10, 20, 30])
811
+ const numbers = createStore([10, 20, 30])
624
812
 
625
813
  // Use iteration but also track size to ensure reactivity to additions/removals
626
- const sum = computed(() => {
814
+ const sum = createComputed(() => {
627
815
  // Access size to subscribe to structural changes
628
816
  numbers.size.get()
629
817
  let total = 0
@@ -650,10 +838,10 @@ describe('store', () => {
650
838
 
651
839
  test('demonstrates array compaction behavior with remove operations', () => {
652
840
  // Create a store with an array
653
- const numbers = store([10, 20, 30, 40, 50])
841
+ const numbers = createStore([10, 20, 30, 40, 50])
654
842
 
655
843
  // Create a computed using iteration approach with size tracking
656
- const sumWithIteration = computed(() => {
844
+ const sumWithIteration = createComputed(() => {
657
845
  // Access size to subscribe to structural changes
658
846
  numbers.size.get()
659
847
  let total = 0
@@ -664,7 +852,7 @@ describe('store', () => {
664
852
  })
665
853
 
666
854
  // Create a computed using .get() approach for comparison
667
- const sumWithGet = computed(() => {
855
+ const sumWithGet = createComputed(() => {
668
856
  const array = numbers.get()
669
857
  if (!Array.isArray(array)) return 0
670
858
  return array.reduce((acc, num) => acc + num, 0)
@@ -700,7 +888,7 @@ describe('store', () => {
700
888
 
701
889
  test('verifies root cause: diff works on array representation but reconcile uses sparse keys', () => {
702
890
  // Create a sparse array scenario
703
- const numbers = store([10, 20, 30])
891
+ const numbers = createStore([10, 20, 30])
704
892
 
705
893
  // Remove middle element to create sparse structure
706
894
  numbers.remove(1) // Now has keys ["0", "2"] with values [10, 30]
@@ -726,7 +914,7 @@ describe('store', () => {
726
914
 
727
915
  describe('arrays and edge cases', () => {
728
916
  test('handles arrays as store values', () => {
729
- const data = store({ items: [1, 2, 3] })
917
+ const data = createStore({ items: [1, 2, 3] })
730
918
 
731
919
  // Arrays become stores with string indices
732
920
  expect(isStore(data.items)).toBe(true)
@@ -736,7 +924,7 @@ describe('store', () => {
736
924
  })
737
925
 
738
926
  test('array-derived nested stores have correct type inference', () => {
739
- const todoApp = store({
927
+ const todoApp = createStore({
740
928
  todos: ['Buy milk', 'Walk the dog', 'Write code'],
741
929
  users: [
742
930
  { name: 'Alice', active: true },
@@ -795,7 +983,7 @@ describe('store', () => {
795
983
  })
796
984
 
797
985
  test('handles UNSET values', () => {
798
- const data = store({ value: UNSET as string })
986
+ const data = createStore({ value: UNSET as string })
799
987
 
800
988
  expect(data.value.get()).toBe(UNSET)
801
989
  data.value.set('some string')
@@ -803,7 +991,7 @@ describe('store', () => {
803
991
  })
804
992
 
805
993
  test('handles primitive values', () => {
806
- const data = store({
994
+ const data = createStore({
807
995
  str: 'hello',
808
996
  num: 42,
809
997
  bool: true,
@@ -817,13 +1005,19 @@ describe('store', () => {
817
1005
 
818
1006
  describe('proxy behavior', () => {
819
1007
  test('Object.keys returns property keys', () => {
820
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
1008
+ const user = createStore({
1009
+ name: 'Hannah',
1010
+ email: 'hannah@example.com',
1011
+ })
821
1012
 
822
1013
  expect(Object.keys(user)).toEqual(['name', 'email'])
823
1014
  })
824
1015
 
825
1016
  test('property enumeration works', () => {
826
- const user = store({ name: 'Hannah', email: 'hannah@example.com' })
1017
+ const user = createStore({
1018
+ name: 'Hannah',
1019
+ email: 'hannah@example.com',
1020
+ })
827
1021
  const keys: string[] = []
828
1022
 
829
1023
  for (const key in user) {
@@ -834,14 +1028,14 @@ describe('store', () => {
834
1028
  })
835
1029
 
836
1030
  test('in operator works', () => {
837
- const user = store({ name: 'Hannah' })
1031
+ const user = createStore({ name: 'Hannah' })
838
1032
 
839
1033
  expect('name' in user).toBe(true)
840
1034
  expect('email' in user).toBe(false)
841
1035
  })
842
1036
 
843
1037
  test('Object.getOwnPropertyDescriptor works', () => {
844
- const user = store({ name: 'Hannah' })
1038
+ const user = createStore({ name: 'Hannah' })
845
1039
 
846
1040
  const descriptor = Object.getOwnPropertyDescriptor(user, 'name')
847
1041
  expect(descriptor).toEqual({
@@ -855,7 +1049,7 @@ describe('store', () => {
855
1049
 
856
1050
  describe('type conversion via toSignal', () => {
857
1051
  test('arrays are converted to stores', () => {
858
- const fruits = store({ items: ['apple', 'banana', 'cherry'] })
1052
+ const fruits = createStore({ items: ['apple', 'banana', 'cherry'] })
859
1053
 
860
1054
  expect(isStore(fruits.items)).toBe(true)
861
1055
  expect(fruits.items['0'].get()).toBe('apple')
@@ -864,7 +1058,7 @@ describe('store', () => {
864
1058
  })
865
1059
 
866
1060
  test('nested objects become nested stores', () => {
867
- const config = store({
1061
+ const config = createStore({
868
1062
  database: {
869
1063
  host: 'localhost',
870
1064
  port: 5432,
@@ -879,7 +1073,7 @@ describe('store', () => {
879
1073
 
880
1074
  describe('spread operator behavior', () => {
881
1075
  test('spreading store spreads individual signals', () => {
882
- const user = store({ name: 'Hannah', age: 25, active: true })
1076
+ const user = createStore({ name: 'Hannah', age: 25, active: true })
883
1077
 
884
1078
  // Spread the store - should get individual signals
885
1079
  const spread = { ...user }
@@ -908,7 +1102,7 @@ describe('store', () => {
908
1102
  })
909
1103
 
910
1104
  test('spreading nested store works correctly', () => {
911
- const config = store({
1105
+ const config = createStore({
912
1106
  app: { name: 'MyApp', version: '1.0' },
913
1107
  settings: { theme: 'dark', debug: false },
914
1108
  })
@@ -952,7 +1146,7 @@ describe('store', () => {
952
1146
 
953
1147
  // Parse JSON and create store - works seamlessly
954
1148
  const apiData = JSON.parse(jsonResponse)
955
- const userStore = store<{
1149
+ const userStore = createStore<{
956
1150
  user: {
957
1151
  id: number
958
1152
  name: string
@@ -1075,7 +1269,7 @@ describe('store', () => {
1075
1269
  const data = JSON.parse(complexJson)
1076
1270
 
1077
1271
  // Test that null values in initial JSON are filtered out (treated as UNSET)
1078
- const dashboardStore = store<{
1272
+ const dashboardStore = createStore<{
1079
1273
  dashboard: {
1080
1274
  widgets: {
1081
1275
  id: number
@@ -1182,7 +1376,7 @@ describe('store', () => {
1182
1376
  },
1183
1377
  }
1184
1378
 
1185
- const formStore = store<{
1379
+ const formStore = createStore<{
1186
1380
  profile: {
1187
1381
  id?: number
1188
1382
  createdAt?: string
@@ -1257,7 +1451,7 @@ describe('store', () => {
1257
1451
 
1258
1452
  describe('Symbol.isConcatSpreadable and polymorphic behavior', () => {
1259
1453
  test('array-like stores have Symbol.isConcatSpreadable true and length property', () => {
1260
- const numbers = store([1, 2, 3])
1454
+ const numbers = createStore([1, 2, 3])
1261
1455
 
1262
1456
  // Should be concat spreadable
1263
1457
  expect(numbers[Symbol.isConcatSpreadable]).toBe(true)
@@ -1272,7 +1466,7 @@ describe('store', () => {
1272
1466
  })
1273
1467
 
1274
1468
  test('object-like stores have Symbol.isConcatSpreadable false and no length property', () => {
1275
- const user = store({ name: 'John', age: 25 })
1469
+ const user = createStore({ name: 'John', age: 25 })
1276
1470
 
1277
1471
  // Should not be concat spreadable
1278
1472
  expect(user[Symbol.isConcatSpreadable]).toBe(false)
@@ -1284,7 +1478,7 @@ describe('store', () => {
1284
1478
  })
1285
1479
 
1286
1480
  test('array-like stores iterate over signals only', () => {
1287
- const numbers = store([10, 20, 30])
1481
+ const numbers = createStore([10, 20, 30])
1288
1482
  const signals = [...numbers]
1289
1483
 
1290
1484
  // Should yield signals, not [key, signal] pairs
@@ -1300,7 +1494,7 @@ describe('store', () => {
1300
1494
  })
1301
1495
 
1302
1496
  test('object-like stores iterate over [key, signal] pairs', () => {
1303
- const user = store({ name: 'Alice', age: 30 })
1497
+ const user = createStore({ name: 'Alice', age: 30 })
1304
1498
  const entries = [...user]
1305
1499
 
1306
1500
  // Should yield [key, signal] pairs
@@ -1320,7 +1514,7 @@ describe('store', () => {
1320
1514
  })
1321
1515
 
1322
1516
  test('array-like stores support single-parameter add() method', () => {
1323
- const fruits = store(['apple', 'banana'])
1517
+ const fruits = createStore(['apple', 'banana'])
1324
1518
 
1325
1519
  // Should add to end without specifying key
1326
1520
  fruits.add('cherry')
@@ -1331,7 +1525,10 @@ describe('store', () => {
1331
1525
  })
1332
1526
 
1333
1527
  test('object-like stores require key parameter for add() method', () => {
1334
- const config = store<{ debug: boolean; timeout?: number }>({
1528
+ const config = createStore<{
1529
+ debug: boolean
1530
+ timeout?: number
1531
+ }>({
1335
1532
  debug: true,
1336
1533
  })
1337
1534
 
@@ -1342,9 +1539,9 @@ describe('store', () => {
1342
1539
  })
1343
1540
 
1344
1541
  test('concat works correctly with array-like stores', () => {
1345
- const numbers = store([2, 3])
1346
- const prefix = [state(1)]
1347
- const suffix = [state(4), state(5)]
1542
+ const numbers = createStore([2, 3])
1543
+ const prefix = [createState(1)]
1544
+ const suffix = [createState(4), createState(5)]
1348
1545
 
1349
1546
  // Should spread signals when concat-ed
1350
1547
  const combined = prefix.concat(
@@ -1361,10 +1558,10 @@ describe('store', () => {
1361
1558
  })
1362
1559
 
1363
1560
  test('spread operator works correctly with array-like stores', () => {
1364
- const numbers = store([10, 20])
1561
+ const numbers = createStore([10, 20])
1365
1562
 
1366
1563
  // Should spread signals
1367
- const spread = [state(5), ...numbers, state(30)]
1564
+ const spread = [createState(5), ...numbers, createState(30)]
1368
1565
 
1369
1566
  expect(spread).toHaveLength(4)
1370
1567
  expect(spread[0].get()).toBe(5)
@@ -1374,7 +1571,7 @@ describe('store', () => {
1374
1571
  })
1375
1572
 
1376
1573
  test('array-like stores maintain numeric key ordering', () => {
1377
- const items = store(['first', 'second', 'third'])
1574
+ const items = createStore(['first', 'second', 'third'])
1378
1575
 
1379
1576
  // Get the keys
1380
1577
  const keys = Object.keys(items)
@@ -1389,17 +1586,19 @@ describe('store', () => {
1389
1586
 
1390
1587
  test('polymorphic behavior is determined at creation time', () => {
1391
1588
  // Created as array - stays array-like
1392
- const arrayStore = store([1, 2])
1589
+ const arrayStore = createStore([1, 2])
1393
1590
  expect(arrayStore[Symbol.isConcatSpreadable]).toBe(true)
1394
1591
  expect(arrayStore.length).toBe(2)
1395
1592
 
1396
1593
  // Created as object - stays object-like
1397
- const objectStore = store<{ a: number; b: number; c?: number }>(
1398
- {
1399
- a: 1,
1400
- b: 2,
1401
- },
1402
- )
1594
+ const objectStore = createStore<{
1595
+ a: number
1596
+ b: number
1597
+ c?: number
1598
+ }>({
1599
+ a: 1,
1600
+ b: 2,
1601
+ })
1403
1602
  expect(objectStore[Symbol.isConcatSpreadable]).toBe(false)
1404
1603
  // @ts-expect-error deliberate access to non-existent length property
1405
1604
  expect(objectStore.length).toBeUndefined()
@@ -1413,8 +1612,8 @@ describe('store', () => {
1413
1612
  })
1414
1613
 
1415
1614
  test('runtime type detection using typeof length', () => {
1416
- const arrayStore = store([1, 2, 3])
1417
- const objectStore = store({ x: 1, y: 2 })
1615
+ const arrayStore = createStore([1, 2, 3])
1616
+ const objectStore = createStore({ x: 1, y: 2 })
1418
1617
 
1419
1618
  // Can distinguish at runtime
1420
1619
  expect(typeof arrayStore.length === 'number').toBe(true)
@@ -1423,8 +1622,8 @@ describe('store', () => {
1423
1622
  })
1424
1623
 
1425
1624
  test('empty stores behave correctly', () => {
1426
- const emptyArray = store([])
1427
- const emptyObject = store({})
1625
+ const emptyArray = createStore([])
1626
+ const emptyObject = createStore({})
1428
1627
 
1429
1628
  // Empty array store
1430
1629
  expect(emptyArray[Symbol.isConcatSpreadable]).toBe(true)
@@ -1440,10 +1639,10 @@ describe('store', () => {
1440
1639
  })
1441
1640
 
1442
1641
  test('debug length property issue', () => {
1443
- const numbers = store([1, 2, 3])
1642
+ const numbers = createStore([1, 2, 3])
1444
1643
 
1445
1644
  // Test length in computed context
1446
- const lengthComputed = computed(() => numbers.length)
1645
+ const lengthComputed = createComputed(() => numbers.length)
1447
1646
  numbers.add(4)
1448
1647
 
1449
1648
  // Test if length property is actually reactive
@@ -1454,7 +1653,7 @@ describe('store', () => {
1454
1653
 
1455
1654
  describe('sort() method', () => {
1456
1655
  test('sorts array-like store with numeric compareFn', () => {
1457
- const numbers = store([3, 1, 4, 1, 5])
1656
+ const numbers = createStore([3, 1, 4, 1, 5])
1458
1657
 
1459
1658
  // Capture old signal references
1460
1659
  const oldSignals = [
@@ -1479,7 +1678,7 @@ describe('store', () => {
1479
1678
  })
1480
1679
 
1481
1680
  test('sorts array-like store with string compareFn', () => {
1482
- const names = store(['Charlie', 'Alice', 'Bob'])
1681
+ const names = createStore(['Charlie', 'Alice', 'Bob'])
1483
1682
 
1484
1683
  names.sort((a, b) => a.localeCompare(b))
1485
1684
 
@@ -1487,7 +1686,7 @@ describe('store', () => {
1487
1686
  })
1488
1687
 
1489
1688
  test('sorts record-like store by value', () => {
1490
- const users = store({
1689
+ const users = createStore({
1491
1690
  user1: { name: 'Charlie', age: 25 },
1492
1691
  user2: { name: 'Alice', age: 30 },
1493
1692
  user3: { name: 'Bob', age: 20 },
@@ -1513,30 +1712,28 @@ describe('store', () => {
1513
1712
  expect(users.user3).toBe(oldSignals.user3)
1514
1713
  })
1515
1714
 
1516
- test('emits store-sort event with new order', () => {
1517
- const numbers = store([30, 10, 20])
1518
- let sortEvent: StoreSortEvent | null = null
1715
+ test('emits a sort notification with new order', () => {
1716
+ const numbers = createStore([30, 10, 20])
1717
+ let sortNotification: string[] | null = null
1519
1718
 
1520
- numbers.addEventListener('store-sort', event => {
1521
- sortEvent = event
1719
+ numbers.on('sort', change => {
1720
+ sortNotification = change
1522
1721
  })
1523
1722
 
1524
1723
  numbers.sort((a, b) => a - b)
1525
1724
 
1526
- expect(sortEvent).not.toBeNull()
1527
- // biome-ignore lint/style/noNonNullAssertion: test
1528
- expect(sortEvent!.type).toBe('store-sort')
1725
+ expect(sortNotification).not.toBeNull()
1529
1726
  // Keys in new sorted order: [10, 20, 30] came from indices [1, 2, 0]
1530
1727
  // biome-ignore lint/style/noNonNullAssertion: test
1531
- expect(sortEvent!.detail).toEqual(['1', '2', '0'])
1728
+ expect(sortNotification!).toEqual(['1', '2', '0'])
1532
1729
  })
1533
1730
 
1534
1731
  test('sort is reactive - watchers are notified', () => {
1535
- const numbers = store([3, 1, 2])
1732
+ const numbers = createStore([3, 1, 2])
1536
1733
  let effectCount = 0
1537
1734
  let lastValue: number[] = []
1538
1735
 
1539
- effect(() => {
1736
+ createEffect(() => {
1540
1737
  lastValue = numbers.get()
1541
1738
  effectCount++
1542
1739
  })
@@ -1553,7 +1750,7 @@ describe('store', () => {
1553
1750
  })
1554
1751
 
1555
1752
  test('nested signals remain reactive after sorting', () => {
1556
- const items = store([
1753
+ const items = createStore([
1557
1754
  { name: 'Charlie', score: 85 },
1558
1755
  { name: 'Alice', score: 95 },
1559
1756
  { name: 'Bob', score: 75 },
@@ -1578,7 +1775,7 @@ describe('store', () => {
1578
1775
  })
1579
1776
 
1580
1777
  test('sort with complex nested structures', () => {
1581
- const posts = store([
1778
+ const posts = createStore([
1582
1779
  {
1583
1780
  id: 'post1',
1584
1781
  title: 'Hello World',
@@ -1612,7 +1809,7 @@ describe('store', () => {
1612
1809
  })
1613
1810
 
1614
1811
  test('sort preserves array length and size', () => {
1615
- const arr = store([5, 2, 8, 1])
1812
+ const arr = createStore([5, 2, 8, 1])
1616
1813
 
1617
1814
  expect(arr.length).toBe(4)
1618
1815
  expect(arr.size.get()).toBe(4)
@@ -1625,7 +1822,7 @@ describe('store', () => {
1625
1822
  })
1626
1823
 
1627
1824
  test('sort with no compareFn uses default string sorting like Array.prototype.sort()', () => {
1628
- const items = store(['banana', 'cherry', 'apple', '10', '2'])
1825
+ const items = createStore(['banana', 'cherry', 'apple', '10', '2'])
1629
1826
 
1630
1827
  items.sort()
1631
1828
 
@@ -1636,7 +1833,7 @@ describe('store', () => {
1636
1833
  })
1637
1834
 
1638
1835
  test('default sort handles numbers as strings like Array.prototype.sort()', () => {
1639
- const numbers = store([80, 9, 100])
1836
+ const numbers = createStore([80, 9, 100])
1640
1837
 
1641
1838
  numbers.sort()
1642
1839
 
@@ -1646,7 +1843,7 @@ describe('store', () => {
1646
1843
  })
1647
1844
 
1648
1845
  test('default sort handles mixed values with proper string conversion', () => {
1649
- const mixed = store(['b', 0, 'a', '', 'c'])
1846
+ const mixed = createStore(['b', 0, 'a', '', 'c'])
1650
1847
 
1651
1848
  mixed.sort()
1652
1849
 
@@ -1655,7 +1852,7 @@ describe('store', () => {
1655
1852
  })
1656
1853
 
1657
1854
  test('multiple sorts work correctly', () => {
1658
- const numbers = store([3, 1, 4, 1, 5])
1855
+ const numbers = createStore([3, 1, 4, 1, 5])
1659
1856
 
1660
1857
  // Sort ascending
1661
1858
  numbers.sort((a, b) => a - b)
@@ -1666,24 +1863,120 @@ describe('store', () => {
1666
1863
  expect(numbers.get()).toEqual([5, 4, 3, 1, 1])
1667
1864
  })
1668
1865
 
1669
- test('sort event contains correct movement mapping for records', () => {
1670
- const users = store({
1866
+ test('sort notification contains correct movement mapping for records', () => {
1867
+ const users = createStore({
1671
1868
  alice: { age: 30 },
1672
1869
  bob: { age: 20 },
1673
1870
  charlie: { age: 25 },
1674
1871
  })
1675
1872
 
1676
- let sortEvent: StoreSortEvent | null = null
1677
- users.addEventListener('store-sort', event => {
1678
- sortEvent = event
1873
+ let sortNotification: string[] | null = null
1874
+ users.on('sort', change => {
1875
+ sortNotification = change
1679
1876
  })
1680
1877
 
1681
1878
  // Sort by age
1682
1879
  users.sort((a, b) => b.age - a.age)
1683
1880
 
1684
- expect(sortEvent).not.toBeNull()
1881
+ expect(sortNotification).not.toBeNull()
1685
1882
  // biome-ignore lint/style/noNonNullAssertion: test
1686
- expect(sortEvent!.detail).toEqual(['alice', 'charlie', 'bob'])
1883
+ expect(sortNotification!).toEqual(['alice', 'charlie', 'bob'])
1884
+ })
1885
+ })
1886
+
1887
+ describe('cross-component communication pattern', () => {
1888
+ test('event bus with UNSET initialization - type-safe pattern', () => {
1889
+ // Component B (the owner) declares the shape of events it will emit
1890
+ type EventBusSchema = {
1891
+ userLogin: { userId: number; timestamp: number }
1892
+ userLogout: { userId: number }
1893
+ userUpdate: { userId: number; profile: { name: string } }
1894
+ }
1895
+
1896
+ // Initialize the event bus with proper typing
1897
+ const eventBus = createStore<EventBusSchema>({
1898
+ userLogin: UNSET,
1899
+ userLogout: UNSET,
1900
+ userUpdate: UNSET,
1901
+ })
1902
+
1903
+ // Simple type-safe on functions
1904
+ const on = (
1905
+ event: keyof EventBusSchema,
1906
+ callback: (data: EventBusSchema[keyof EventBusSchema]) => void,
1907
+ ) =>
1908
+ createEffect(() => {
1909
+ const data = eventBus[event].get()
1910
+ if (data !== UNSET) callback(data)
1911
+ })
1912
+
1913
+ // Test the pattern with properly typed variables
1914
+ let receivedLogin: unknown = null
1915
+ let receivedLogout: unknown = null
1916
+ let receivedUpdate: unknown = null
1917
+
1918
+ // Component A listens for events
1919
+ on('userLogin', data => {
1920
+ receivedLogin = data
1921
+ })
1922
+
1923
+ on('userLogout', data => {
1924
+ receivedLogout = data
1925
+ })
1926
+
1927
+ on('userUpdate', data => {
1928
+ receivedUpdate = data
1929
+ })
1930
+
1931
+ // Initially nothing should be received (all UNSET)
1932
+ expect(receivedLogin).toBe(null)
1933
+ expect(receivedLogout).toBe(null)
1934
+ expect(receivedUpdate).toBe(null)
1935
+
1936
+ // Component B emits user events with full type safety
1937
+ const loginData: EventBusSchema['userLogin'] = {
1938
+ userId: 123,
1939
+ timestamp: Date.now(),
1940
+ }
1941
+ eventBus.userLogin.set(loginData)
1942
+
1943
+ expect(receivedLogin).toEqual(loginData)
1944
+ expect(receivedLogout).toBe(null) // Should not have triggered
1945
+ expect(receivedUpdate).toBe(null) // Should not have triggered
1946
+
1947
+ // Test second event
1948
+ const logoutData: EventBusSchema['userLogout'] = { userId: 123 }
1949
+ eventBus.userLogout.set(logoutData)
1950
+
1951
+ expect(receivedLogout).toEqual(logoutData)
1952
+ expect(receivedLogin).toEqual(loginData) // Should remain unchanged
1953
+
1954
+ // Test third event
1955
+ const updateData: EventBusSchema['userUpdate'] = {
1956
+ userId: 456,
1957
+ profile: { name: 'Alice' },
1958
+ }
1959
+ eventBus.userUpdate.set(updateData)
1960
+
1961
+ expect(receivedUpdate).toEqual(updateData)
1962
+ expect(receivedLogin).toEqual(loginData) // Should remain unchanged
1963
+ expect(receivedLogout).toEqual(logoutData) // Should remain unchanged
1964
+
1965
+ // Test updating existing event
1966
+ const newLoginData: EventBusSchema['userLogin'] = {
1967
+ userId: 789,
1968
+ timestamp: Date.now(),
1969
+ }
1970
+ eventBus.userLogin.set(newLoginData)
1971
+
1972
+ expect(receivedLogin).toEqual(newLoginData) // Should update
1973
+ expect(receivedLogout).toEqual(logoutData) // Should remain unchanged
1974
+ expect(receivedUpdate).toEqual(updateData) // Should remain unchanged
1975
+
1976
+ // Compile-time type checking prevents errors:
1977
+ // emitUserLogin({ userId: 'invalid' }) // ❌ TypeScript error
1978
+ // emitUserLogin({ userId: 123, extraProp: 'invalid' }) // ❌ TypeScript error
1979
+ // emitUserLogout({ userId: 123, extraProp: 'invalid' }) // ❌ TypeScript error
1687
1980
  })
1688
1981
  })
1689
1982
  })