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 })