char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c42f8c8
main
1import { 2Map , 3MapPrototypeDelete as remove , 4MapPrototypeGet as get , 5MapPrototypeHas as has , 6MapPrototypeSet as set , 7MathMin as min , 8ReflectApply as apply , 9TypeError , 10} from "ext:core/primordials" ; 11import { 12op_timer_cancel as cancel , 13op_timer_now as now , 14op_timer_wait as wait , 15op_timer_wait_unreferenced as waitUnreferenced , 16} from "ext:core/ops" ; 17import { promiseThen as then } from "ext:core/promises" ; 18import { enqueueMicrotask } from "ext:core/microtasks" ; 19import { enqueueTask } from "ext:core/tasks" ; 20import { array , missing , required , string } from "ext:web/idl.js" ; 21import { reportErrorValue } from "ext:web/events.js" ; 22 23interface Timer { 24id :number ; 25handler :Function | null ; 26args :unknown [] | null ; 27repeat :boolean ; 28delay :number ; 29privateTimer :boolean ; 30nesting :number ; 31deadline :number ; 32sequence :number ; 33} 34 35const receiver = globalThis ; 36const timers = new Map < number , Timer >(), queue = array < Timer >(); 37let nextId = 1 , pending :{ deadline :number ; referenced :boolean } | null = null , 38firing = false , nesting = 0 ; 39// Heap order is (deadline, sequence); the sequence restores FIFO among equals. 40let sequence = 0 ; 41// Cleared entries stay in the heap until they surface; stale counts them so 42// schedule() can rebuild when garbage dominates. 43let stale = 0 ; 44// Non-private timers in the map, including the one currently firing. 45let referencedCount = 0 ; 46 47function report ( error :unknown ) :void { 48reportErrorValue ( error ); 49} 50function earlier ( a :Timer , b :Timer ) :boolean { 51return a . deadline < b . deadline || 52( a . deadline === b . deadline && a . sequence < b . sequence ); 53} 54function siftUp ( index :number ) :void { 55while ( index > 0 ) { 56const parent = ( index - 1 ) >> 1 ; 57if ( ! earlier ( queue [ index ], queue [ parent ])) break ; 58const entry = queue [ index ]; 59queue [ index ] = queue [ parent ]; 60queue [ parent ] = entry ; 61index = parent ; 62} 63} 64function siftDown ( index :number ) :void { 65const count = queue . length ; 66for (;;) { 67const left = index * 2 + 1 ; 68const right = left + 1 ; 69let least = index ; 70if ( left < count && earlier ( queue [ left ], queue [ least ])) least = left ; 71if ( right < count && earlier ( queue [ right ], queue [ least ])) least = right ; 72if ( least === index ) break ; 73const entry = queue [ index ]; 74queue [ index ] = queue [ least ]; 75queue [ least ] = entry ; 76index = least ; 77} 78} 79function insert ( entry :Timer ) :void { 80entry . sequence = sequence ++ ; 81const index = queue . length ; 82queue [ index ] = entry ; 83siftUp ( index ); 84} 85function popHead () :Timer { 86const head = queue [ 0 ]; 87const last = queue [ queue . length - 1 ]; 88queue . length = queue . length - 1 ; 89if ( queue . length > 0 ) { 90queue [ 0 ] = last ; 91siftDown ( 0 ); 92} 93return head ; 94} 95// Drop cleared heads so the live head is on top. Interior garbage stays 96// until it surfaces; schedule() rebuilds if it dominates. 97function peekHead () :Timer | undefined { 98while ( queue . length > 0 ) { 99const head = queue [ 0 ]; 100if ( get ( timers , head . id ) === head ) return head ; 101popHead (); 102if ( stale > 0 ) stale -- ; 103} 104return undefined ; 105} 106function compactIfNeeded () :void { 107if ( stale <= 0 || queue . length < 128 || stale * 2 <= queue . length ) return ; 108let live = 0 ; 109for ( let index = 0 ; index < queue . length ; index ++ ) { 110const entry = queue [ index ]; 111if ( get ( timers , entry . id ) === entry ) queue [ live ++ ] = entry ; 112} 113queue . length = live ; 114stale = 0 ; 115for ( let index = ( live >> 1 ) - 1 ; index >= 0 ; index -- ) siftDown ( index ); 116} 117function schedule () :void { 118if ( firing ) return ; 119compactIfNeeded (); 120const head = peekHead (); 121if ( ! head ) { 122pending = null ; 123cancel ( null ); 124return ; 125} 126const referenced = referencedCount > 0 ; 127if ( 128pending && pending . deadline <= head . deadline && 129pending . referenced === referenced 130) return ; 131const record = { deadline :head . deadline , referenced}; 132pending = record ; 133const promise = ( referenced ?wait :waitUnreferenced )( record . deadline ); 134// Promise reactions must not consult guest-controlled constructor/species hooks. 135then ( promise , () => { 136if ( pending !== record ) return ; 137pending = null ; 138const entry = peekHead (); 139if ( ! entry || entry . deadline > now ( null )) { 140schedule (); 141return ; 142} 143popHead (); 144firing = true ; 145enqueueTask (() => { 146if ( get ( timers , entry . id ) !== entry ) return ; 147nesting = entry . privateTimer ?0 :entry . nesting ; 148try { 149// Map membership above guarantees this entry has not been cleared. 150apply ( entry . handler ! , receiver , entry . args ! ); 151} catch ( error ) { 152report ( error ); 153} finally { 154nesting = 0 ; 155} 156}, () => { 157if ( get ( timers , entry . id ) === entry ) { 158if ( entry . repeat ) { 159const level = entry . nesting ++ ; 160entry . deadline = now ( null ) + 161( level > 5 && entry . delay < 4 ?4 :entry . delay ); 162insert ( entry ); 163} else { 164remove ( timers , entry . id ); 165if ( ! entry . privateTimer ) referencedCount -- ; 166} 167} 168firing = false ; 169schedule (); 170}); 171}, ( error ) => { 172if ( pending !== record ) return ; 173pending = null ; 174report ( error ); 175}); 176} 177function start ( handler :unknown , rawDelay :unknown , args :unknown [], repeat :boolean , privateTimer = false ) :number { 178if ( typeof handler !== "function" ) handler = string ( handler ); 179// Keep unary +: Number() would accept BigInt. 180let delay = + ( rawDelay as number ) >> 0 ; 181if ( typeof handler !== "function" ) missing ( "string timer callbacks" ); 182if ( delay < 0 ) delay = 0 ; 183const id = nextId ; 184do { 185nextId = nextId === 2147483647 ?1 :nextId + 1 ; 186} while ( has ( timers , nextId )); 187const entry = { 188 id, 189 handler, 190 args, 191 repeat, 192 delay, 193 privateTimer, 194nesting :nesting + 1 , 195deadline :now ( null ) + 196( ! privateTimer && nesting > 5 && delay < 4 ?4 :delay ), 197sequence :0 , 198}; 199set ( timers , id , entry ); 200if ( ! privateTimer ) referencedCount ++ ; 201insert ( entry ); 202schedule (); 203return id ; 204} 205function scheduleTimeout ( handler :() => void , milliseconds :number ) :void { 206let remaining = milliseconds , previous = now ( null ); 207function fire () :void { 208const current = now ( null ); 209remaining -= current - previous ; 210previous = current ; 211if ( remaining <= 0 ) handler (); 212else start ( fire , min ( remaining , 2147483647 ), array (), false , true ); 213} 214start ( fire , min ( remaining , 2147483647 ), array (), false , true ); 215} 216function clear ( rawId :unknown ) :void { 217const id = + ( rawId as number ) >> 0 ; 218const entry = get ( timers , id ); 219if ( ! entry || entry . privateTimer ) return ; 220remove ( timers , id ); 221referencedCount -- ; 222entry . handler = null ; 223entry . args = null ; 224stale ++ ; 225schedule (); 226} 227const api = { 228setTimeout ( handler :unknown , timeout :unknown = 0 , ...args :unknown []) :number { 229required ( arguments . length , 1 ); 230return start ( handler , timeout , args , false ); 231}, 232setInterval ( handler :unknown , timeout :unknown = 0 , ...args :unknown []) :number { 233required ( arguments . length , 1 ); 234return start ( handler , timeout , args , true ); 235}, 236clearTimeout ( id :unknown = 0 ) :void { 237clear ( id ); 238}, 239clearInterval ( id :unknown = 0 ) :void { 240clear ( id ); 241}, 242queueMicrotask ( callback :unknown ) :void { 243required ( arguments . length , 1 ); 244if ( typeof callback !== "function" ) { 245throw new TypeError ( "Expected a microtask callback" ); 246} 247enqueueMicrotask ( callback ); 248}, 249}; 250const { setTimeout, setInterval, clearTimeout, clearInterval, queueMicrotask} = 251api ; 252export { 253clearInterval , 254clearTimeout , 255queueMicrotask , 256scheduleTimeout , 257setInterval , 258setTimeout , 259};