bansa 0.0.23 → 0.0.25

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,1319 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { $, createScope, type DerivedAtom, type PrimitiveAtom, type ThenableSignal } from '../src/index';
3
-
4
- const flushMicrotasks = () =>
5
- new Promise((resolve) => {
6
- const { port1, port2 } = new MessageChannel();
7
- port1.onmessage = resolve;
8
- port2.postMessage(null);
9
- });
10
- const wait = () =>
11
- new Promise((resolve) => {
12
- setTimeout(resolve, 4);
13
- });
14
-
15
- const inc = (x: number) => x + 1;
16
- const nop = () => {};
17
- const nops = (): (() => void)[] => [];
18
-
19
- describe('Atom Library - Basic Tests', () => {
20
- it('primitive atom', async () => {
21
- const atom = $(42);
22
- expect(atom.get()).toBe(42);
23
-
24
- atom.set(100);
25
- await flushMicrotasks();
26
- expect(atom.get()).toBe(100);
27
-
28
- atom.set((x) => x * 2);
29
- await flushMicrotasks();
30
- expect(atom.get()).toBe(200);
31
- });
32
-
33
- it('derived atom', async () => {
34
- const atom1 = $(5);
35
- const atom2 = $(5);
36
- const derivedAtom = $((get) => get(atom1) + get(atom2));
37
-
38
- atom1.set(10);
39
- expect(derivedAtom.get()).toBe(10);
40
- await flushMicrotasks();
41
- expect(derivedAtom.get()).toBe(15);
42
-
43
- atom2.set(10);
44
- expect(derivedAtom.get()).toBe(15);
45
- await flushMicrotasks();
46
- expect(derivedAtom.get()).toBe(20);
47
- });
48
-
49
- it('async atom', async () => {
50
- const atom1 = $(async () => 10);
51
- const atom2 = $(async () => 20);
52
- const atom3 = $(0);
53
- const derivedAtom = $((get) => {
54
- return get(atom1) + get(atom2) + get(atom3);
55
- });
56
- derivedAtom.subscribe(nop);
57
-
58
- expect(!!atom1.state.promise).toBe(true);
59
- expect(!!atom2.state.promise).toBe(true);
60
- expect(!!derivedAtom.state.promise).toBe(true);
61
-
62
- await flushMicrotasks();
63
- expect(derivedAtom.state.value).toBe(30);
64
-
65
- atom3.set(inc);
66
- // expect(!!derivedAtom.state.promise).toBe(true);
67
- await flushMicrotasks();
68
- expect(derivedAtom.state.value).toBe(31);
69
- });
70
-
71
- it('repeated addition', async () => {
72
- const atom = $(0);
73
- const atom2 = $(2);
74
- const derivedAtom = $((get) => get(atom) * get(atom2));
75
-
76
- for (let i = 0; i < 100; i++) {
77
- atom.set(inc);
78
- }
79
- await flushMicrotasks();
80
- expect(derivedAtom.get()).toBe(200);
81
-
82
- atom2.set(3);
83
- await flushMicrotasks();
84
- expect(derivedAtom.get()).toBe(300);
85
- });
86
-
87
- it('deep addition', async () => {
88
- const atom = $(0);
89
- const atom2 = $(1);
90
- let derivedAtom = $((get) => get(atom) + get(atom2));
91
- for (let i = 1; i < 100; i++) {
92
- const prevAtom = derivedAtom;
93
- derivedAtom = $((get) => get(prevAtom) + get(atom2));
94
- }
95
- expect(derivedAtom.get()).toBe(100);
96
-
97
- atom.set(1);
98
- await flushMicrotasks();
99
- expect(derivedAtom.get()).toBe(101);
100
-
101
- atom2.set(2);
102
- await flushMicrotasks();
103
- expect(derivedAtom.get()).toBe(201);
104
- });
105
-
106
- it('primitive atom subscribe', async () => {
107
- const atom = $(42);
108
-
109
- const mockFn = vi.fn();
110
- const unsub = atom.subscribe(mockFn);
111
- await flushMicrotasks();
112
- expect(mockFn).toHaveBeenCalledWith(42, expect.anything());
113
- mockFn.mockClear();
114
-
115
- atom.set(100);
116
- await flushMicrotasks();
117
- expect(mockFn).toHaveBeenCalledWith(100, expect.anything());
118
- mockFn.mockClear();
119
-
120
- unsub();
121
- atom.set(0);
122
- await flushMicrotasks();
123
- expect(mockFn).not.toHaveBeenCalled();
124
- });
125
-
126
- it('derived atom subscribe', async () => {
127
- const atom = $(42);
128
- const derivedAtom = $((get) => get(atom) * 2);
129
-
130
- const mockFn = vi.fn();
131
- const unsub = derivedAtom.subscribe(mockFn);
132
- await flushMicrotasks();
133
- expect(mockFn).toHaveBeenCalledWith(84, expect.anything());
134
- mockFn.mockClear();
135
-
136
- atom.set(100);
137
- await flushMicrotasks();
138
- expect(mockFn).toHaveBeenCalledWith(200, expect.anything());
139
- mockFn.mockClear();
140
-
141
- unsub();
142
- atom.set(0);
143
- await flushMicrotasks();
144
- expect(mockFn).not.toHaveBeenCalled();
145
- });
146
-
147
- it('atom watch', async () => {
148
- const atom = $(42);
149
- const derivedAtom = $((get) => Promise.resolve(get(atom) * 2));
150
-
151
- const mockFn = vi.fn();
152
- const unwatch = derivedAtom.watch(mockFn);
153
- await flushMicrotasks();
154
- expect(mockFn).toBeCalledTimes(2);
155
- mockFn.mockClear();
156
-
157
- unwatch();
158
- atom.set(0);
159
- await flushMicrotasks();
160
- expect(mockFn).toBeCalledTimes(0);
161
- });
162
-
163
- it('state property reflects current value', async () => {
164
- const atom = $(42);
165
- expect(atom.state).toEqual({
166
- promise: undefined,
167
- error: undefined,
168
- value: 42,
169
- });
170
- });
171
- });
172
-
173
- describe('Atom Library - Advanced Tests', () => {
174
- it('handles complex dependency chains', async () => {
175
- const baseAtom = $(1);
176
- const derived1 = $((get) => get(baseAtom) * 2);
177
- const derived2 = $((get) => get(derived1) + 1);
178
- let resolve = nop;
179
- const asyncDerived = $(async (get) => {
180
- const value = get(derived2);
181
- await new Promise<void>((r) => (resolve = r));
182
- return value * 2;
183
- });
184
- asyncDerived.subscribe(nop);
185
- await flushMicrotasks();
186
- expect(!!asyncDerived.state.promise).toBe(true);
187
-
188
- resolve();
189
- await flushMicrotasks();
190
- expect(asyncDerived.state.value).toBe(6);
191
-
192
- baseAtom.set(2);
193
- await flushMicrotasks();
194
- expect(!!asyncDerived.state.promise).toBe(true);
195
-
196
- resolve();
197
- await flushMicrotasks();
198
- expect(asyncDerived.state.value).toBe(10);
199
- });
200
-
201
- it('first promise first resolved 1', async () => {
202
- const countAtom = $(0);
203
- const resolve = nops();
204
- const asyncAtom = $(async (get) => {
205
- const count = get(countAtom);
206
- await new Promise<void>((r) => resolve.push(r));
207
- return count;
208
- });
209
- const mock = vi.fn();
210
- asyncAtom.subscribe(mock);
211
-
212
- countAtom.set((c) => c + 1);
213
- countAtom.set((c) => c + 1);
214
- await flushMicrotasks();
215
- expect(resolve.length).toBe(1);
216
-
217
- resolve.shift()?.();
218
- await flushMicrotasks();
219
- expect(asyncAtom.state.value).toBe(2);
220
- });
221
-
222
- it('first promise first resolved 2', async () => {
223
- const countAtom = $(0);
224
- const resolve = nops();
225
- const asyncAtom = $(async (get) => {
226
- const count = get(countAtom);
227
- await new Promise<void>((r) => resolve.push(r));
228
- return count;
229
- });
230
- const mock = vi.fn();
231
- asyncAtom.subscribe(mock);
232
-
233
- countAtom.set((c) => c + 1);
234
- await flushMicrotasks();
235
- countAtom.set((c) => c + 1);
236
- await flushMicrotasks();
237
- expect(resolve.length).toBe(2);
238
-
239
- resolve.shift()?.();
240
- await flushMicrotasks();
241
- expect(!!asyncAtom.state.promise).toBe(true);
242
-
243
- resolve.shift()?.();
244
- await flushMicrotasks();
245
- expect(asyncAtom.state.value).toBe(2);
246
- });
247
-
248
- it('last promise first resolved 1', async () => {
249
- const countAtom = $(0);
250
- const resolve = nops();
251
- const asyncAtom = $(async (get) => {
252
- const count = get(countAtom);
253
- await new Promise<void>((r) => resolve.push(r));
254
- return count;
255
- });
256
- const mock = vi.fn();
257
- asyncAtom.subscribe(mock);
258
-
259
- countAtom.set((c) => c + 1);
260
- countAtom.set((c) => c + 1);
261
- await flushMicrotasks();
262
- expect(resolve.length).toBe(1);
263
-
264
- resolve.pop()?.();
265
- await flushMicrotasks();
266
- expect(asyncAtom.state.value).toBe(2);
267
- });
268
-
269
- it('last promise first resolved 2', async () => {
270
- const countAtom = $(0);
271
- const resolve = nops();
272
- const asyncAtom = $(async (get) => {
273
- const count = get(countAtom);
274
- await new Promise<void>((r) => resolve.push(r));
275
- return count;
276
- });
277
- const mock = vi.fn();
278
- asyncAtom.subscribe(mock);
279
-
280
- countAtom.set((c) => c + 1);
281
- await flushMicrotasks();
282
- countAtom.set((c) => c + 1);
283
- await flushMicrotasks();
284
- expect(resolve.length).toBe(2);
285
-
286
- resolve.pop()?.();
287
- await flushMicrotasks();
288
- expect(asyncAtom.state.value).toBe(2);
289
-
290
- resolve.pop()?.();
291
- await flushMicrotasks();
292
- expect(asyncAtom.state.value).toBe(2);
293
- });
294
-
295
- it('deep addition', async () => {
296
- const atom = $(0);
297
- let derivedAtom = $((get) => get(atom) + 1);
298
- for (let i = 1; i < 100; i++) {
299
- const prevAtom = derivedAtom;
300
- derivedAtom = $((get) => get(prevAtom) + 1);
301
- }
302
- expect(derivedAtom.get()).toBe(100);
303
- });
304
-
305
- it('custom equality function prevents unnecessary updates', async () => {
306
- const mockFn = vi.fn();
307
- const atom = $(
308
- { value: 42 },
309
- {
310
- equals: (a, b) => a.value === b.value,
311
- },
312
- );
313
-
314
- atom.subscribe(mockFn);
315
- await flushMicrotasks();
316
- expect(mockFn).toHaveBeenCalled();
317
- mockFn.mockClear();
318
-
319
- // This should not trigger an update due to custom equality
320
- atom.set({ value: 42 });
321
- await flushMicrotasks();
322
- expect(mockFn).not.toHaveBeenCalled();
323
-
324
- // This should trigger an update
325
- atom.set({ value: 100 });
326
- await flushMicrotasks();
327
- expect(mockFn).toHaveBeenCalled();
328
- });
329
-
330
- it('async derived atoms handle promises correctly', async () => {
331
- const atom = $(1);
332
- const asyncAtom = $((get) => {
333
- const id = get(atom);
334
- return Promise.resolve(`user-${id}`);
335
- });
336
- const mockFn1 = vi.fn();
337
- asyncAtom.subscribe(mockFn1);
338
-
339
- // Initial fetch should be pending
340
- expect(!!asyncAtom.state.promise).toBe(true);
341
-
342
- // Wait for resolution
343
- await flushMicrotasks();
344
- await flushMicrotasks();
345
-
346
- expect(!!asyncAtom.state.promise).toBe(false);
347
- expect(asyncAtom.get()).toBe('user-1');
348
-
349
- // Update dependency
350
- atom.set(2);
351
- await flushMicrotasks();
352
- await flushMicrotasks();
353
-
354
- expect(asyncAtom.get()).toBe('user-2');
355
- });
356
-
357
- it('atom mount/unmount', async () => {
358
- const atom1 = $(10);
359
- const atom2 = $(20);
360
-
361
- const metrics1 = { mounted: 0, unmounted: 0 };
362
- const derivedAtom1 = $(async (get, { signal }) => {
363
- metrics1.mounted++;
364
- signal.then(() => {
365
- metrics1.unmounted++;
366
- });
367
- return get(atom1);
368
- });
369
-
370
- const metrics2 = { mounted: 0, unmounted: 0 };
371
- const derivedAtom2 = $(async (get, { signal }) => {
372
- metrics2.mounted++;
373
- signal.then(() => {
374
- metrics2.unmounted++;
375
- });
376
- return get(atom2);
377
- });
378
-
379
- let resolve = nop;
380
- const metrics3 = { mounted: 0, unmounted: 0 };
381
- const derivedAtom3 = $(async (get, { signal }) => {
382
- metrics3.mounted++;
383
- signal.then(() => {
384
- metrics3.unmounted++;
385
- });
386
- const v1 = get(derivedAtom1);
387
- await new Promise<void>((r) => (resolve = r));
388
- const v2 = get(derivedAtom2);
389
- return v1 + v2;
390
- });
391
-
392
- const unsub = derivedAtom3.subscribe(nop);
393
- await flushMicrotasks();
394
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
395
- expect(metrics2).toEqual({ mounted: 0, unmounted: 0 });
396
- expect(metrics3).toEqual({ mounted: 2, unmounted: 1 });
397
- expect(derivedAtom1.state.value).toEqual(10);
398
- expect(derivedAtom2.state.value).toEqual(undefined);
399
- expect(!!derivedAtom3.state.promise).toEqual(true);
400
-
401
- resolve();
402
- await flushMicrotasks();
403
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
404
- expect(metrics2).toEqual({ mounted: 1, unmounted: 0 });
405
- expect(metrics3).toEqual({ mounted: 3, unmounted: 2 });
406
- expect(derivedAtom1.state.value).toEqual(10);
407
- expect(derivedAtom2.state.value).toEqual(20);
408
- expect(!!derivedAtom3.state.promise).toEqual(true);
409
-
410
- resolve();
411
- await flushMicrotasks();
412
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
413
- expect(metrics2).toEqual({ mounted: 1, unmounted: 0 });
414
- expect(metrics3).toEqual({ mounted: 3, unmounted: 2 });
415
- expect(derivedAtom1.state.value).toEqual(10);
416
- expect(derivedAtom2.state.value).toEqual(20);
417
- expect(derivedAtom3.state.value).toEqual(30);
418
-
419
- atom2.set(30);
420
- await flushMicrotasks();
421
- resolve();
422
- await wait();
423
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
424
- expect(metrics2).toEqual({ mounted: 2, unmounted: 1 });
425
- expect(metrics3).toEqual({ mounted: 4, unmounted: 3 });
426
- expect(derivedAtom1.state.value).toEqual(10);
427
- expect(derivedAtom2.state.value).toEqual(30);
428
- expect(derivedAtom3.state.value).toEqual(40);
429
-
430
- unsub();
431
- resolve = nop;
432
- await new Promise((r) => setTimeout(r, 10));
433
- expect(metrics1).toEqual({ mounted: 1, unmounted: 1 });
434
- expect(metrics2).toEqual({ mounted: 2, unmounted: 2 });
435
- expect(metrics3).toEqual({ mounted: 4, unmounted: 4 });
436
- expect(!!derivedAtom3.state.promise).toEqual(true);
437
-
438
- atom1.set(20);
439
- await flushMicrotasks();
440
- expect(resolve).toBe(nop);
441
- await flushMicrotasks();
442
- expect(metrics1).toEqual({ mounted: 1, unmounted: 1 });
443
- expect(metrics2).toEqual({ mounted: 2, unmounted: 2 });
444
- expect(metrics3).toEqual({ mounted: 4, unmounted: 4 });
445
- expect(derivedAtom3.state.value).toEqual(undefined);
446
- });
447
-
448
- it('gc test', async () => {
449
- const atom1 = $(10);
450
-
451
- const metrics1 = { mounted: 0, unmounted: 0 };
452
- const derivedAtom1 = $((get, { signal }) => {
453
- metrics1.mounted++;
454
- signal.then(() => {
455
- metrics1.unmounted++;
456
- });
457
- return get(atom1);
458
- });
459
-
460
- const unsub = derivedAtom1.subscribe(nop);
461
- await flushMicrotasks();
462
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
463
- expect(derivedAtom1.state.value).toEqual(10);
464
-
465
- unsub();
466
- await flushMicrotasks();
467
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
468
- expect(derivedAtom1.state.value).toEqual(10);
469
-
470
- const unsub2 = derivedAtom1.subscribe(nop);
471
- await flushMicrotasks();
472
- expect(metrics1).toEqual({ mounted: 1, unmounted: 0 });
473
- expect(derivedAtom1.state.value).toEqual(10);
474
-
475
- atom1.set(20);
476
- await flushMicrotasks();
477
- expect(metrics1).toEqual({ mounted: 2, unmounted: 1 });
478
- expect(derivedAtom1.state.value).toEqual(20);
479
-
480
- unsub2();
481
- await new Promise((r) => setTimeout(r, 10));
482
- expect(metrics1).toEqual({ mounted: 2, unmounted: 2 });
483
- expect(derivedAtom1.state.value).toEqual(undefined);
484
-
485
- atom1.set(30);
486
- await flushMicrotasks();
487
- expect(metrics1).toEqual({ mounted: 2, unmounted: 2 });
488
- expect(derivedAtom1.state.value).toEqual(undefined);
489
- });
490
-
491
- it('scope with deep dependencies', async () => {
492
- const $x1 = $(1);
493
- const $x2 = $((get) => get($x1) + 1);
494
- const $x3 = $((get) => get($x2) + 1);
495
- const $x4 = $((get) => get($x3) + 1);
496
- const $x5 = $((get) => get($x4) + 1);
497
- const identity = (x: any) => x;
498
- const scope0 = createScope(identity);
499
- const scope1 = createScope(identity, [
500
- [$x1, 11],
501
- ]);
502
- const scope2 = createScope(identity, [
503
- [$x2, 22],
504
- ]);
505
-
506
- $x5.subscribe(nop);
507
- await flushMicrotasks();
508
-
509
- const $y0 = scope0($x5);
510
- const $y1 = scope1($x5);
511
- const $y2 = scope2($x5);
512
-
513
- expect($x5.get()).toBe(5);
514
- expect($y0.get()).toBe(5);
515
- expect($y1.get()).toBe(15);
516
- expect($y2.get()).toBe(25);
517
-
518
- $x1.set(101);
519
- await flushMicrotasks();
520
- expect($x5.get()).toBe(105);
521
- expect($y0.get()).toBe(105);
522
- expect($y1.get()).toBe(15);
523
- expect($y2.get()).toBe(25);
524
-
525
- scope1($x1).set(201);
526
- scope2($x1).set(201);
527
- await flushMicrotasks();
528
- expect($x5.get()).toBe(205);
529
- expect($y0.get()).toBe(205);
530
- expect($y1.get()).toBe(205);
531
- expect($y2.get()).toBe(25);
532
- });
533
-
534
- it('deep scope', async () => {
535
- const $x1 = $(1); // 1
536
- const $x2 = $((get) => get($x1) + 1); // 2
537
- const $x3 = $((get) => get($x2) + 1); // 3
538
- const $x4 = $((get) => get($x3) + 1); // 4
539
- const $x5 = $((get) => get($x1) + get($x2) + get($x3) + get($x4)); // 10
540
- const identity = (x: any) => x;
541
- const scope0 = createScope(identity);
542
- const scope1 = createScope(scope0, [
543
- [$x1, 11],
544
- ]);
545
- const scope2 = createScope(scope1, [
546
- [$x2, 22],
547
- ]);
548
-
549
- $x5.subscribe(nop);
550
- await flushMicrotasks();
551
-
552
- const $y0 = scope0($x5);
553
- const $y1 = scope1($x5);
554
- const $y2 = scope2($x5);
555
-
556
- expect($x5.get()).toBe(10);
557
- expect($y0.get()).toBe(10);
558
- expect($y1.get()).toBe(50);
559
- expect($y2.get()).toBe(80);
560
-
561
- $x1.set(101);
562
- await flushMicrotasks();
563
- expect($x5.get()).toBe(410);
564
- expect($y0.get()).toBe(410);
565
- expect($y1.get()).toBe(50);
566
- expect($y2.get()).toBe(80);
567
-
568
- scope1($x1).set(101);
569
- await flushMicrotasks();
570
- expect($x5.get()).toBe(410);
571
- expect($y0.get()).toBe(410);
572
- expect($y1.get()).toBe(410);
573
- expect($y2.get()).toBe(170);
574
-
575
- scope2($x1).set(201);
576
- await flushMicrotasks();
577
- expect($x5.get()).toBe(410);
578
- expect($y0.get()).toBe(410);
579
- expect($y1.get()).toBe(810);
580
- expect($y2.get()).toBe(270);
581
-
582
- (scope2($x2) as PrimitiveAtom<number>).set(202);
583
- await flushMicrotasks();
584
- expect($x5.get()).toBe(410);
585
- expect($y0.get()).toBe(410);
586
- expect($y1.get()).toBe(810);
587
- expect($y2.get()).toBe(810);
588
- });
589
-
590
- it('deep scope (self referenced)', async () => {
591
- const $x = $(1);
592
- const identity = (x: any) => x;
593
- const scope1 = createScope(identity, [
594
- [$x, $((get) => get($x) + 1)],
595
- ]);
596
- const scope2 = createScope(scope1, [
597
- [$x, $((get) => get($x) + 1)],
598
- ]);
599
-
600
- await flushMicrotasks();
601
-
602
- expect($x.get()).toBe(1);
603
- expect(scope1($x).get()).toBe(2);
604
- expect(scope2($x).get()).toBe(3);
605
-
606
- $x.set(11);
607
- await flushMicrotasks();
608
- expect($x.get()).toBe(11);
609
- expect(scope1($x).get()).toBe(12);
610
- expect(scope2($x).get()).toBe(13);
611
- });
612
-
613
- it('should not provide stale values to conditional dependents', async () => {
614
- const dataAtom = $([100]);
615
- const hasFilterAtom = $(false);
616
- const filteredAtom = $((get) => {
617
- const data = get(dataAtom);
618
- return get(hasFilterAtom) ? [] : data;
619
- });
620
- const stageAtom = $((get) =>
621
- !get(hasFilterAtom) ? 0 : get(filteredAtom).length === 0 ? 1 : 2,
622
- );
623
-
624
- filteredAtom.subscribe(nop);
625
- stageAtom.subscribe(nop);
626
-
627
- expect(stageAtom.get(), 'should start without filter').toBe(0);
628
-
629
- hasFilterAtom.set(true);
630
- await flushMicrotasks();
631
- expect(stageAtom.get(), 'should update').toBe(1);
632
- });
633
-
634
- it('async derived atoms handle errors correctly', async () => {
635
- const error = new Error('Test error');
636
- const errorAtom = $(() => Promise.reject(error));
637
- const mockFn1 = vi.fn();
638
- errorAtom.subscribe(mockFn1);
639
-
640
- await flushMicrotasks();
641
- await flushMicrotasks(); // Additional wait for promise resolution
642
-
643
- expect(errorAtom.state.error).toBe(error);
644
- expect(() => errorAtom.get()).toThrowError(error);
645
- });
646
-
647
- it('multiple subscribers receive updates', async () => {
648
- const atom = $(42);
649
- const mockFn1 = vi.fn();
650
- const mockFn2 = vi.fn();
651
-
652
- atom.subscribe(mockFn1);
653
- atom.subscribe(mockFn2);
654
- await flushMicrotasks();
655
- mockFn1.mockClear();
656
- mockFn2.mockClear();
657
-
658
- atom.set(100);
659
- await flushMicrotasks();
660
-
661
- expect(mockFn1).toHaveBeenCalledWith(100, expect.anything());
662
- expect(mockFn2).toHaveBeenCalledWith(100, expect.anything());
663
- });
664
-
665
- it('atoms with unused dependencies are garbage collected', async () => {
666
- const a = $(10);
667
- const b = $((get) => get(a) + 5);
668
-
669
- // Create a derived atom and subscribe to it
670
- const c = $((get) => get(b) * 2);
671
- const unsub = c.subscribe(nop);
672
- await flushMicrotasks();
673
-
674
- // Get initial state
675
- expect(c.get()).toBe(30);
676
-
677
- // Unsubscribe and ensure atom is "disabled"
678
- unsub();
679
- await flushMicrotasks();
680
-
681
- // Change dependency
682
- a.set(20);
683
- await flushMicrotasks();
684
-
685
- // Re-access should recompute with latest values
686
- expect(c.get()).toBe(50);
687
- });
688
-
689
- it('batched updates are processed efficiently', async () => {
690
- const atom = $(0);
691
- const derivedAtom = $((get) => get(atom) * 2);
692
- const mockFn = vi.fn();
693
-
694
- derivedAtom.subscribe(mockFn);
695
- await flushMicrotasks();
696
- mockFn.mockClear();
697
-
698
- // Multiple updates in same microtask should batch
699
- atom.set(1);
700
- atom.set(2);
701
- atom.set(3);
702
-
703
- await flushMicrotasks();
704
-
705
- // Should only be called once with final value
706
- expect(mockFn).toHaveBeenCalledTimes(1);
707
- expect(mockFn).toHaveBeenCalledWith(6, expect.anything());
708
- });
709
-
710
- it('can propagate updates with async $ chains', async () => {
711
- const countAtom = $(1);
712
- let resolve = nop;
713
- const asyncAtom = $(async (get) => {
714
- const count = get(countAtom);
715
- await new Promise<void>((r) => (resolve = r));
716
- return count;
717
- });
718
- const async2Atom = $((get) => get(asyncAtom) % 3);
719
- const mockFn = vi.fn((get: (atom: DerivedAtom<number>) => any) => get(async2Atom));
720
- const async3Atom = $(mockFn);
721
-
722
- async3Atom.subscribe(nop);
723
- await flushMicrotasks();
724
- resolve();
725
- await flushMicrotasks();
726
- expect(async3Atom.state.value).toBe(1);
727
- expect(mockFn).toHaveBeenCalledTimes(2);
728
-
729
- countAtom.set((c) => c + 1);
730
- await flushMicrotasks();
731
- resolve();
732
- await flushMicrotasks();
733
- expect(async3Atom.state.value).toBe(2);
734
- expect(mockFn).toHaveBeenCalledTimes(3);
735
-
736
- countAtom.set((c) => c + 3);
737
- await flushMicrotasks();
738
- resolve();
739
- await flushMicrotasks();
740
- expect(async3Atom.state.value).toBe(2);
741
- expect(mockFn).toHaveBeenCalledTimes(3);
742
- });
743
- });
744
-
745
- describe('Bansa Documentation Examples as Tests', () => {
746
- // fetch 모킹
747
- const mockFetch = vi.fn();
748
- beforeEach(() => {
749
- vi.stubGlobal('fetch', mockFetch);
750
- mockFetch.mockClear();
751
- });
752
-
753
- // --- "상태를 얼마나 쪼개는 게 좋나요?" 예제 테스트 ---
754
- describe('Atom Granularity Example', () => {
755
- // 테스트에 사용할 가상 DOM 요소
756
- const userElm = { innerHTML: '' };
757
- const postElm = { innerHTML: '' };
758
- const commentElm = { innerHTML: '' };
759
-
760
- beforeEach(() => {
761
- userElm.innerHTML = '';
762
- postElm.innerHTML = '';
763
- commentElm.innerHTML = '';
764
- mockFetch.mockImplementation(async (url: string) => {
765
- if (url.includes('/users/')) {
766
- return {
767
- ok: true,
768
- json: async () => ({
769
- name: 'John Doe',
770
- author: 'John Doe',
771
- }),
772
- };
773
- }
774
- if (url.includes('/posts/')) {
775
- return {
776
- ok: true,
777
- json: async () => ({
778
- html: '<p>Post content</p>',
779
- author: 'Jane Smith',
780
- }),
781
- };
782
- }
783
- return { ok: false, status: 404 };
784
- });
785
- });
786
-
787
- it('Bad Practice: Combined state causes unnecessary re-fetches', async () => {
788
- const $userId = $(123);
789
- const $postId = $(456);
790
-
791
- // 모든 로직이 하나의 파생 상태에 결합된 경우
792
- const $pageData = $(async (get, { signal }) => {
793
- const user = await fetch(`/users/${get($userId)}`, {
794
- signal,
795
- }).then((res) => res.json());
796
- const post = await fetch(`/posts/${get($postId)}`, {
797
- signal,
798
- }).then((res) => res.json());
799
-
800
- userElm.innerHTML = user.name;
801
- postElm.innerHTML = post.html;
802
- commentElm.innerHTML = `Hello ${user.name}! Comment to ${post.author}.`;
803
- return { user, post };
804
- });
805
-
806
- const unsub = $pageData.subscribe(() => {});
807
- await flushMicrotasks(); // 초기 실행 대기
808
-
809
- expect(mockFetch).toHaveBeenCalledTimes(2);
810
- expect(mockFetch).toHaveBeenCalledWith(
811
- '/users/123',
812
- expect.anything(),
813
- );
814
- expect(mockFetch).toHaveBeenCalledWith(
815
- '/posts/456',
816
- expect.anything(),
817
- );
818
- expect(userElm.innerHTML).toBe('John Doe');
819
- expect(postElm.innerHTML).toBe('<p>Post content</p>');
820
-
821
- mockFetch.mockClear();
822
-
823
- // postId만 변경되어도 user와 post를 모두 다시 fetch함
824
- $postId.set(789);
825
- await flushMicrotasks(); // 업데이트 대기
826
-
827
- expect(mockFetch).toHaveBeenCalledTimes(2);
828
- expect(mockFetch).toHaveBeenCalledWith(
829
- '/users/123',
830
- expect.anything(),
831
- ); // 불필요한 호출
832
- expect(mockFetch).toHaveBeenCalledWith(
833
- '/posts/789',
834
- expect.anything(),
835
- );
836
-
837
- unsub();
838
- });
839
-
840
- it('Good Practice: Split states prevent unnecessary re-fetches', async () => {
841
- const $userId = $(123);
842
- const $user = $((get, { signal }) =>
843
- fetch(`/users/${get($userId)}`, { signal }).then((res) =>
844
- res.json(),
845
- ),
846
- );
847
-
848
- const $postId = $(456);
849
- const $post = $((get, { signal }) =>
850
- fetch(`/posts/${get($postId)}`, { signal }).then((res) =>
851
- res.json(),
852
- ),
853
- );
854
-
855
- const $pageData = $((get) => ({
856
- userName: get($user).name,
857
- postAuthor: get($post).author,
858
- }));
859
-
860
- const userSub = $user.subscribe((user) => {
861
- userElm.innerHTML = user.name;
862
- });
863
- const postSub = $post.subscribe((post) => {
864
- postElm.innerHTML = post.html;
865
- });
866
- const pageDataSub = $pageData.subscribe(
867
- ({ userName, postAuthor }) => {
868
- commentElm.innerHTML = `Hello ${userName}! Comment to ${postAuthor}.`;
869
- },
870
- );
871
-
872
- await flushMicrotasks(); // 초기 실행 대기
873
-
874
- expect(mockFetch).toHaveBeenCalledTimes(2);
875
- expect(mockFetch).toHaveBeenCalledWith(
876
- '/users/123',
877
- expect.anything(),
878
- );
879
- expect(mockFetch).toHaveBeenCalledWith(
880
- '/posts/456',
881
- expect.anything(),
882
- );
883
- expect(userElm.innerHTML).toBe('John Doe');
884
- expect(postElm.innerHTML).toBe('<p>Post content</p>');
885
- expect(commentElm.innerHTML).toBe(
886
- 'Hello John Doe! Comment to Jane Smith.',
887
- );
888
-
889
- mockFetch.mockClear();
890
-
891
- // postId만 변경. user는 다시 fetch되지 않음
892
- $postId.set(789);
893
- await flushMicrotasks(); // 업데이트 대기
894
-
895
- expect(mockFetch).toHaveBeenCalledTimes(1); // post만 호출됨
896
- expect(mockFetch).toHaveBeenCalledWith(
897
- '/posts/789',
898
- expect.anything(),
899
- );
900
- expect(mockFetch).not.toHaveBeenCalledWith(
901
- '/users/123',
902
- expect.anything(),
903
- );
904
- expect(commentElm.innerHTML).toBe(
905
- 'Hello John Doe! Comment to Jane Smith.',
906
- ); // post.author가 업데이트됨
907
-
908
- userSub();
909
- postSub();
910
- pageDataSub();
911
- });
912
- });
913
-
914
- // --- "onMount/onCleanup은 어떻게 하나요?" 예제 테스트 ---
915
- describe('onMount / onCleanup Patterns', () => {
916
- it('Pattern 1: Atom returning an atom for shared resources', async () => {
917
- const onMount = vi.fn();
918
- const onCleanup = vi.fn();
919
- const mockWs = {
920
- close: vi.fn(),
921
- send: vi.fn(),
922
- addEventListener: vi.fn(),
923
- removeEventListener: vi.fn(),
924
- };
925
-
926
- // 공유 커넥션 상태
927
- const $wsConnection = $((_, { signal }) => {
928
- onMount();
929
- signal.then(onCleanup);
930
- signal.then(() => mockWs.close());
931
-
932
- return {
933
- send: (message: string) => mockWs.send(message),
934
- addEventListener: (
935
- listener: (data: any) => void,
936
- listenerSignal: ThenableSignal,
937
- ) => {
938
- mockWs.addEventListener('message', listener);
939
- listenerSignal.then(() =>
940
- mockWs.removeEventListener('message', listener),
941
- );
942
- },
943
- };
944
- });
945
-
946
- // 커넥션을 사용하는 상태 팩토리
947
- const lastMessage = (name: string) =>
948
- $((get, { signal }) => {
949
- const { send, addEventListener } = get($wsConnection);
950
- const $lastMessage = $(null);
951
- addEventListener(({ type, value }) => {
952
- if (type === name) $lastMessage.set(value);
953
- }, signal);
954
-
955
- send(`+${name}`);
956
- signal.then(() => send(`-${name}`));
957
- return $lastMessage;
958
- });
959
-
960
- const $alice = lastMessage('alice');
961
- const $bob = lastMessage('bob');
962
-
963
- // 첫 구독: onMount 호출
964
- const unsubAlice = $alice.subscribe(() => {});
965
- await flushMicrotasks();
966
- expect(onMount).toHaveBeenCalledTimes(1);
967
- expect(onCleanup).not.toHaveBeenCalled();
968
- expect(mockWs.send).toHaveBeenCalledWith('+alice');
969
-
970
- // 두 번째 구독: onMount는 다시 호출되지 않음
971
- const unsubBob = $bob.subscribe(() => {});
972
- await flushMicrotasks();
973
- expect(onMount).toHaveBeenCalledTimes(1);
974
- expect(mockWs.send).toHaveBeenCalledWith('+bob');
975
-
976
- // 첫 구독 해제: onCleanup은 아직 호출되지 않음
977
- unsubAlice();
978
- await new Promise((r) => setTimeout(r, 10)); // disableAtom 대기
979
- expect(onCleanup).not.toHaveBeenCalled();
980
- expect(mockWs.send).toHaveBeenCalledWith('-alice');
981
-
982
- // 마지막 구독 해제: onCleanup 호출
983
- unsubBob();
984
- await new Promise((r) => setTimeout(r, 10)); // disableAtom 대기
985
- expect(onCleanup).toHaveBeenCalledTimes(1);
986
- expect(mockWs.close).toHaveBeenCalledTimes(1);
987
- expect(mockWs.send).toHaveBeenCalledWith('-bob');
988
- });
989
-
990
- it('Pattern 2: Read/Write separation for lifecycles', async () => {
991
- const onMount = vi.fn();
992
- const onCleanup = vi.fn();
993
-
994
- const $writer = $(0);
995
- const $shared = $((_, { signal }) => {
996
- onMount();
997
- signal.then(onCleanup);
998
- });
999
- const $reader = $((get) => {
1000
- get($shared); // $shared의 생명주기에 의존
1001
- return get($writer);
1002
- });
1003
-
1004
- // 구독 전: 아무것도 호출되지 않음
1005
- expect(onMount).not.toHaveBeenCalled();
1006
-
1007
- // 구독 시: onMount 호출
1008
- const unsub = $reader.subscribe(() => {});
1009
- await flushMicrotasks();
1010
- expect(onMount).toHaveBeenCalledTimes(1);
1011
- expect($reader.get()).toBe(0);
1012
-
1013
- // 쓰기 상태 업데이트: onMount는 다시 호출되지 않음
1014
- $writer.set(10);
1015
- await flushMicrotasks();
1016
- expect(onMount).toHaveBeenCalledTimes(1);
1017
- expect($reader.get()).toBe(10);
1018
-
1019
- // 구독 해제 시: onCleanup 호출
1020
- unsub();
1021
- await new Promise((r) => setTimeout(r, 10)); // disableAtom 대기
1022
- expect(onCleanup).toHaveBeenCalledTimes(1);
1023
- });
1024
- });
1025
-
1026
- // --- "디바운스-스로틀링" 예제 테스트 ---
1027
- describe('Debounce-Throttling Example', () => {
1028
- beforeEach(() => {
1029
- vi.useFakeTimers();
1030
- });
1031
- afterEach(() => {
1032
- vi.useRealTimers();
1033
- });
1034
-
1035
- const delayedState = <Value>(
1036
- initial: Value,
1037
- minDelay: number,
1038
- maxDelay: number,
1039
- ) => {
1040
- const $value = $(initial);
1041
- const $delayedValue = $(initial);
1042
-
1043
- const $eventStartTime = $(0);
1044
- const $eventLastTime = $(0);
1045
- const $delayedTime = $((get) =>
1046
- Math.min(
1047
- get($eventStartTime) + maxDelay,
1048
- get($eventLastTime) + minDelay,
1049
- ),
1050
- );
1051
- const $delayedInfo = $((get) => ({
1052
- value: get($value),
1053
- time: get($delayedTime),
1054
- }));
1055
- $delayedInfo.subscribe(({ value, time }, { signal }) => {
1056
- const timeout = Math.max(0, time - Date.now());
1057
- const timer = setTimeout(
1058
- () => $delayedValue.set(value),
1059
- timeout,
1060
- );
1061
- signal.then(() => clearTimeout(timer));
1062
- });
1063
-
1064
- const update = (value: Value, eager = false) => {
1065
- const now = eager ? -Infinity : Date.now();
1066
- if ($delayedValue.state.value === $value.state.value) {
1067
- $eventStartTime.set(now);
1068
- }
1069
- $eventLastTime.set(now);
1070
- $value.set(value);
1071
- };
1072
-
1073
- return [$delayedValue, update] as const;
1074
- };
1075
-
1076
- it('should update after minDelay', async () => {
1077
- const [$inputValue, updateInput] = delayedState('', 200, 1000);
1078
- const subscriber = vi.fn();
1079
- $inputValue.subscribe(subscriber);
1080
- await flushMicrotasks();
1081
- subscriber.mockClear();
1082
-
1083
- updateInput('test');
1084
- await flushMicrotasks();
1085
- expect(subscriber).not.toHaveBeenCalled();
1086
-
1087
- vi.advanceTimersByTime(199);
1088
- await flushMicrotasks();
1089
- expect(subscriber).not.toHaveBeenCalled();
1090
-
1091
- vi.advanceTimersByTime(2); // 총 201ms 경과
1092
- await flushMicrotasks();
1093
- expect(subscriber).toHaveBeenCalledWith('test', expect.anything());
1094
- });
1095
-
1096
- it('should update after maxDelay even with continuous updates', async () => {
1097
- const [$inputValue, updateInput] = delayedState('', 200, 1000);
1098
- const subscriber = vi.fn();
1099
- $inputValue.subscribe(subscriber);
1100
- await flushMicrotasks();
1101
- subscriber.mockClear();
1102
-
1103
- updateInput('a'); // 0ms
1104
- await flushMicrotasks();
1105
-
1106
- vi.advanceTimersByTime(500); // 500ms
1107
- updateInput('ab');
1108
- await flushMicrotasks();
1109
-
1110
- vi.advanceTimersByTime(500); // 1000ms
1111
- await flushMicrotasks();
1112
- expect(subscriber).toHaveBeenCalledWith('ab', expect.anything());
1113
- subscriber.mockClear();
1114
-
1115
- updateInput('abc');
1116
- await flushMicrotasks();
1117
- vi.advanceTimersByTime(1001); // maxDelay 경과
1118
- await flushMicrotasks();
1119
- expect(subscriber).toHaveBeenCalledWith('abc', expect.anything());
1120
- });
1121
- });
1122
-
1123
- // --- 스코프 테스트 ---
1124
- describe('Scope', () => {
1125
- it('scope with initial values', async () => {
1126
- const $x = $(0);
1127
- const $y = $(1);
1128
- const $x2 = $(100);
1129
- const scope = createScope(null, [
1130
- [$x, $x2],
1131
- [$y, 101],
1132
- ]);
1133
-
1134
- const $y2 = scope($y);
1135
- expect(scope($x).get()).toBe($x2.get());
1136
- expect($y2).not.toBe($y);
1137
- expect($y2).toBe(scope($y));
1138
-
1139
- expect($x.get()).toBe(0);
1140
- expect($y.get()).toBe(1);
1141
- expect($x2.get()).toBe(100);
1142
- expect($y2.get()).toBe(101);
1143
- });
1144
-
1145
- it('scope with updates (1)', async () => {
1146
- const $x = $(0);
1147
- const $y = $((get) => get($x) + 1);
1148
- const scope = createScope();
1149
- const $x2 = scope($x);
1150
- const $y2 = scope($y);
1151
-
1152
- expect($x.get()).toBe(0);
1153
- expect($y.get()).toBe(1);
1154
- expect($x2.get()).toBe(0);
1155
- expect($y2.get()).toBe(1);
1156
-
1157
- $x.set(10);
1158
- await flushMicrotasks();
1159
-
1160
- expect($x.get()).toBe(10);
1161
- expect($y.get()).toBe(11);
1162
- expect($x2.get()).toBe(0);
1163
- expect($y2.get()).toBe(1);
1164
-
1165
- $x2.set(100);
1166
- await flushMicrotasks();
1167
-
1168
- expect($x.get()).toBe(10);
1169
- expect($y.get()).toBe(11);
1170
- expect($x2.get()).toBe(100);
1171
- expect($y2.get()).toBe(101);
1172
- });
1173
-
1174
- it('scope with updates (2)', async () => {
1175
- const $x = $(0);
1176
- const $y = $((get) => get($x) + 1);
1177
- const scope = createScope(null, [
1178
- [$x, 100],
1179
- ]);
1180
- const $x2 = scope($x);
1181
- const $y2 = scope($y);
1182
-
1183
- expect($x.get()).toBe(0);
1184
- expect($y.get()).toBe(1);
1185
- expect($x2.get()).toBe(100);
1186
- expect($y2.get()).toBe(101);
1187
-
1188
- $x.set(10);
1189
- await flushMicrotasks();
1190
-
1191
- expect($x.get()).toBe(10);
1192
- expect($y.get()).toBe(11);
1193
- expect($x2.get()).toBe(100);
1194
- expect($y2.get()).toBe(101);
1195
-
1196
- $x2.set(1000);
1197
- await flushMicrotasks();
1198
-
1199
- expect($x.get()).toBe(10);
1200
- expect($y.get()).toBe(11);
1201
- expect($x2.get()).toBe(1000);
1202
- expect($y2.get()).toBe(1001);
1203
- });
1204
-
1205
- it('scope with updates (3)', async () => {
1206
- const $x = $(0);
1207
- const $y = $((get) => get($x) + 1);
1208
- const $x2 = $(100);
1209
- const scope = createScope(null, [
1210
- [$x, $x2],
1211
- ]);
1212
- const $y2 = scope($y);
1213
-
1214
- expect($x.get()).toBe(0);
1215
- expect($y.get()).toBe(1);
1216
- expect($x2.get()).toBe(100);
1217
- expect($y2.get()).toBe(101);
1218
-
1219
- $x.set(10);
1220
- await flushMicrotasks();
1221
-
1222
- expect($x.get()).toBe(10);
1223
- expect($y.get()).toBe(11);
1224
- expect($x2.get()).toBe(100);
1225
- expect($y2.get()).toBe(101);
1226
-
1227
- $x2.set(1000);
1228
- await flushMicrotasks();
1229
-
1230
- expect($x.get()).toBe(10);
1231
- expect($y.get()).toBe(11);
1232
- expect($x2.get()).toBe(1000);
1233
- expect($y2.get()).toBe(101);
1234
- });
1235
- });
1236
-
1237
- // --- "스크롤 방향 감지" 예제 테스트 ---
1238
- /*
1239
- describe('Scroll Direction Detection Example', () => {
1240
- let scrollHandler: () => void;
1241
-
1242
- beforeEach(() => {
1243
- vi.spyOn(window, 'scrollY', 'get').mockReturnValue(0);
1244
- vi.spyOn(window, 'addEventListener').mockImplementation((event, handler) => {
1245
- if (event === 'scroll' || event === 'resize') {
1246
- scrollHandler = handler as () => void;
1247
- }
1248
- });
1249
- vi.spyOn(window, 'removeEventListener');
1250
- });
1251
-
1252
- afterEach(() => {
1253
- vi.mocked(window.addEventListener).mockRestore();
1254
- vi.mocked(window.removeEventListener).mockRestore();
1255
- vi.mocked(window.scrollY, 'get').mockRestore();
1256
- });
1257
-
1258
- it('should detect scroll direction and update navHidden state', async () => {
1259
- const $windowScroll = $((_, { signal }) => {
1260
- let lastTime = Date.now();
1261
- const $scrollY = $(window.scrollY);
1262
- const $scrollOnTop = $((get) => get($scrollY) === 0);
1263
-
1264
- const $scrollMovingAvgY = $(0);
1265
- const $scrollDirectionY = $((get) => Math.sign(get($scrollMovingAvgY)));
1266
-
1267
- const onScrollChange = () => {
1268
- const now = Date.now();
1269
- const alpha = 0.995 ** (now - lastTime);
1270
- lastTime = now;
1271
- const scrollY = window.scrollY;
1272
- const deltaY = scrollY - $scrollY.state.value;
1273
- const movingAvgY = alpha * $scrollMovingAvgY.state.value + (1 - alpha) * deltaY;
1274
-
1275
- $scrollY.set(scrollY);
1276
- $scrollMovingAvgY.set(movingAvgY);
1277
- };
1278
- window.addEventListener('scroll', onScrollChange, { passive: true, signal });
1279
- window.addEventListener('resize', onScrollChange, { passive: true, signal });
1280
-
1281
- return { $scrollOnTop, $scrollDirectionY };
1282
- });
1283
-
1284
- const $navHidden = $((get) => {
1285
- const { $scrollOnTop, $scrollDirectionY } = get($windowScroll);
1286
- const scrollOnTop = get($scrollOnTop);
1287
- const directionY = get($scrollDirectionY);
1288
- return !scrollOnTop && directionY > 0;
1289
- });
1290
-
1291
- const unsub = $navHidden.subscribe(() => {});
1292
- await flushMicrotasks();
1293
-
1294
- // 초기 상태: 스크롤 최상단, nav 보임
1295
- expect($navHidden.get()).toBe(false);
1296
-
1297
- // 아래로 스크롤: nav 숨김
1298
- vi.spyOn(window, 'scrollY', 'get').mockReturnValue(200);
1299
- scrollHandler();
1300
- await flushMicrotasks();
1301
- expect($navHidden.get()).toBe(true);
1302
-
1303
- // 위로 스크롤: nav 보임
1304
- vi.spyOn(window, 'scrollY', 'get').mockReturnValue(100);
1305
- scrollHandler();
1306
- await flushMicrotasks();
1307
- expect($navHidden.get()).toBe(false);
1308
-
1309
- // 최상단으로 스크롤: nav 보임
1310
- vi.spyOn(window, 'scrollY', 'get').mockReturnValue(0);
1311
- scrollHandler();
1312
- await flushMicrotasks();
1313
- expect($navHidden.get()).toBe(false);
1314
-
1315
- unsub();
1316
- });
1317
- });
1318
- */
1319
- });