char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
7bca99b
main
1export const PAGE_SIZE = 10 ; 2 3/** 4* prev/next pagination over a deduplicated async item stream. fetched pages 5* are cached; a failed fetch rolls its items back and reopens the stream so 6* "retry" starts clean. exhaustion is detected lazily: "next" at the very 7* end no-ops once (instantly, the stream is already drained) then disables. 8*/ 9export function paginate < T >( opts :{ 10/** items are appended here as they arrive */ 11list :HTMLElement ; 12/** receives the prev/next buttons */ 13control :HTMLElement ; 14/** fresh iterator; re-invoked to restart after an error */ 15open :() => AsyncIterator < T >; 16/** stable id for cross-page dedup */ 17key :( item :T ) => string ; 18render :( item :T ) => HTMLElement ; 19/** pre-rendered page 0 (e.g. the static log) */ 20seed ?:{ items :HTMLElement []; keys :string [] }; 21onShow ?:( count :number ) => void ; 22onError ?:( err :unknown ) => void ; 23}) :void { 24const { list, control, open, key, render} = opts ; 25list . classList . add ( "paginated" ); 26const shown = new Set ( opts . seed ?. keys ); 27const pages :HTMLElement [][] = opts . seed ?[ opts . seed . items ] :[]; 28let current = pages . length - 1 ; 29let exhausted = false ; 30let failed = false ; 31let source = open (); 32 33const nextUnseen = async () :Promise < T | null > => { 34while ( true ) { 35const next = await source . next (); 36if ( next . done ) return null ; 37if ( ! shown . has ( key ( next . value ))) return next . value ; 38} 39}; 40 41const showPage = () => { 42if ( current >= 0 ) list . replaceChildren ( ...pages [ current ]); 43previous . disabled = current <= 0 ; 44const atEnd = exhausted && current === pages . length - 1 ; 45next . disabled = atEnd ; 46list . classList . toggle ( "exhausted" , atEnd ); 47next . textContent = "next →" ; 48control . removeAttribute ( "title" ); 49if ( current >= 0 ) opts . onShow ?.( pages [ current ]. length ); 50}; 51 52async function goNext () { 53failed = false ; 54if ( current + 1 < pages . length ) { 55current ++ ; 56showPage (); 57return ; 58} 59previous . disabled = true ; 60next . disabled = true ; 61next . textContent = `(0 / ${ PAGE_SIZE } )` ; 62list . replaceChildren (); 63const page :HTMLElement [] = []; 64const pageKeys :string [] = []; 65try { 66while ( page . length < PAGE_SIZE ) { 67const item = await nextUnseen (); 68if ( ! item ) { 69exhausted = true ; 70break ; 71} 72shown . add ( key ( item )); 73pageKeys . push ( key ( item )); 74const element = render ( item ); 75page . push ( element ); 76list . append ( element ); 77next . textContent = `( ${ page . length } / ${ PAGE_SIZE } )` ; 78} 79if ( page . length > 0 ) { 80pages . push ( page ); 81current ++ ; 82} 83showPage (); 84} catch ( err ) { 85for ( const k of pageKeys ) shown . delete ( k ); 86source = open (); 87failed = true ; 88previous . disabled = current <= 0 ; 89next . disabled = false ; 90next . textContent = "retry →" ; 91if ( opts . onError ) opts . onError ( err ); 92else control . title = String ( err ); 93} 94} 95 96const previous = ( 97< button type = "button" class = "log-page-button prev" _onclick = {() => { 98// from the failed/retry state, prev re-shows the current page: the 99// failed page was never pushed, so current already points at it 100if ( ! failed ) current -- ; 101failed = false ; 102showPage (); 103}}> ← prev </ button > 104) as HTMLButtonElement ; 105const next = ( 106< button type = "button" class = "log-page-button next" _onclick = { goNext }> next → < / button> 107) as HTMLButtonElement ; 108control . replaceChildren ( previous , next ); 109if ( opts . seed ) showPage (); 110else void goNext (); 111}