/ src / stores / store-utils.test.ts
store-utils.test.ts
  1  import assert from 'node:assert/strict'
  2  import { describe, it, mock } from 'node:test'
  3  import { createLoader, createInflightDeduplicator } from './store-utils'
  4  
  5  // ── createLoader ────────────────────────────────────────────────────
  6  
  7  describe('createLoader', () => {
  8    it('calls setIfChanged with fetched value on success', async () => {
  9      const calls: Array<{ key: string; value: unknown }> = []
 10      const set = (partial: Partial<{ items: string[] }>) => {
 11        for (const [k, v] of Object.entries(partial)) calls.push({ key: k, value: v })
 12      }
 13      const fetcher = async () => ['a', 'b']
 14  
 15      const loader = createLoader<{ items: string[] }>(set, 'items', fetcher)
 16      await loader()
 17  
 18      assert.equal(calls.length, 1)
 19      assert.equal(calls[0].key, 'items')
 20      assert.deepEqual(calls[0].value, ['a', 'b'])
 21    })
 22  
 23    it('logs warning and writes fallback on error', async () => {
 24      const calls: Array<{ key: string; value: unknown }> = []
 25      const set = (partial: Partial<{ items: string[] }>) => {
 26        for (const [k, v] of Object.entries(partial)) calls.push({ key: k, value: v })
 27      }
 28      const fetcher = async (): Promise<string[]> => {
 29        throw new Error('network fail')
 30      }
 31  
 32      const warn = mock.method(console, 'warn', () => {})
 33      try {
 34        const loader = createLoader<{ items: string[] }>(set, 'items', fetcher, [])
 35        await loader()
 36  
 37        assert.equal(warn.mock.callCount(), 1)
 38        assert.equal(calls.length, 1)
 39        assert.deepEqual(calls[0].value, [])
 40      } finally {
 41        warn.mock.restore()
 42      }
 43    })
 44  
 45    it('logs warning and leaves store unchanged when no fallback', async () => {
 46      const calls: Array<{ key: string; value: unknown }> = []
 47      const set = (partial: Partial<{ items: string[] }>) => {
 48        for (const [k, v] of Object.entries(partial)) calls.push({ key: k, value: v })
 49      }
 50      const fetcher = async (): Promise<string[]> => {
 51        throw new Error('boom')
 52      }
 53  
 54      const warn = mock.method(console, 'warn', () => {})
 55      try {
 56        const loader = createLoader<{ items: string[] }>(set, 'items', fetcher)
 57        await loader()
 58  
 59        assert.equal(warn.mock.callCount(), 1)
 60        assert.equal(calls.length, 0)
 61      } finally {
 62        warn.mock.restore()
 63      }
 64    })
 65  })
 66  
 67  // ── createInflightDeduplicator ──────────────────────────────────────
 68  
 69  describe('createInflightDeduplicator', () => {
 70    it('returns same promise for concurrent calls with same ID', async () => {
 71      const { dedup } = createInflightDeduplicator('test_dedup_same_id')
 72      let callCount = 0
 73      const fn = async () => {
 74        callCount++
 75        await new Promise((r) => setTimeout(r, 10))
 76      }
 77  
 78      await Promise.all([dedup('x', fn), dedup('x', fn), dedup('x', fn)])
 79  
 80      assert.equal(callCount, 1)
 81    })
 82  
 83    it('runs fn separately for different IDs', async () => {
 84      const { dedup } = createInflightDeduplicator('test_dedup_diff_ids')
 85      let callCount = 0
 86      const fn = async () => {
 87        callCount++
 88        await new Promise((r) => setTimeout(r, 10))
 89      }
 90  
 91      await Promise.all([dedup('a', fn), dedup('b', fn)])
 92  
 93      assert.equal(callCount, 2)
 94    })
 95  
 96    it('cleans up after resolve', async () => {
 97      const key = 'test_dedup_cleanup_resolve'
 98      const { dedup } = createInflightDeduplicator(key)
 99      const g = globalThis as Record<string, unknown>
100      const inflight = g[key] as Map<string, Promise<void>>
101  
102      await dedup('z', async () => {})
103  
104      assert.equal(inflight.has('z'), false)
105    })
106  
107    it('cleans up after reject', async () => {
108      const key = 'test_dedup_cleanup_reject'
109      const { dedup } = createInflightDeduplicator(key)
110      const g = globalThis as Record<string, unknown>
111      const inflight = g[key] as Map<string, Promise<void>>
112  
113      await assert.rejects(() => dedup('z', async () => { throw new Error('fail') }))
114  
115      assert.equal(inflight.has('z'), false)
116    })
117  
118    it('allows new call after previous completes', async () => {
119      const { dedup } = createInflightDeduplicator('test_dedup_after_complete')
120      let callCount = 0
121      const fn = async () => { callCount++ }
122  
123      await dedup('x', fn)
124      await dedup('x', fn)
125  
126      assert.equal(callCount, 2)
127    })
128  })