source dump of claude code
at main 121 lines 3.6 kB view raw
1/** 2 * Synchronous state machine for the query lifecycle, compatible with 3 * React's `useSyncExternalStore`. 4 * 5 * Three states: 6 * idle → no query, safe to dequeue and process 7 * dispatching → an item was dequeued, async chain hasn't reached onQuery yet 8 * running → onQuery called tryStart(), query is executing 9 * 10 * Transitions: 11 * idle → dispatching (reserve) 12 * dispatching → running (tryStart) 13 * idle → running (tryStart, for direct user submissions) 14 * running → idle (end / forceEnd) 15 * dispatching → idle (cancelReservation, when processQueueIfReady fails) 16 * 17 * `isActive` returns true for both dispatching and running, preventing 18 * re-entry from the queue processor during the async gap. 19 * 20 * Usage with React: 21 * const queryGuard = useRef(new QueryGuard()).current 22 * const isQueryActive = useSyncExternalStore( 23 * queryGuard.subscribe, 24 * queryGuard.getSnapshot, 25 * ) 26 */ 27import { createSignal } from './signal.js' 28 29export class QueryGuard { 30 private _status: 'idle' | 'dispatching' | 'running' = 'idle' 31 private _generation = 0 32 private _changed = createSignal() 33 34 /** 35 * Reserve the guard for queue processing. Transitions idle → dispatching. 36 * Returns false if not idle (another query or dispatch in progress). 37 */ 38 reserve(): boolean { 39 if (this._status !== 'idle') return false 40 this._status = 'dispatching' 41 this._notify() 42 return true 43 } 44 45 /** 46 * Cancel a reservation when processQueueIfReady had nothing to process. 47 * Transitions dispatching → idle. 48 */ 49 cancelReservation(): void { 50 if (this._status !== 'dispatching') return 51 this._status = 'idle' 52 this._notify() 53 } 54 55 /** 56 * Start a query. Returns the generation number on success, 57 * or null if a query is already running (concurrent guard). 58 * Accepts transitions from both idle (direct user submit) 59 * and dispatching (queue processor path). 60 */ 61 tryStart(): number | null { 62 if (this._status === 'running') return null 63 this._status = 'running' 64 ++this._generation 65 this._notify() 66 return this._generation 67 } 68 69 /** 70 * End a query. Returns true if this generation is still current 71 * (meaning the caller should perform cleanup). Returns false if a 72 * newer query has started (stale finally block from a cancelled query). 73 */ 74 end(generation: number): boolean { 75 if (this._generation !== generation) return false 76 if (this._status !== 'running') return false 77 this._status = 'idle' 78 this._notify() 79 return true 80 } 81 82 /** 83 * Force-end the current query regardless of generation. 84 * Used by onCancel where any running query should be terminated. 85 * Increments generation so stale finally blocks from the cancelled 86 * query's promise rejection will see a mismatch and skip cleanup. 87 */ 88 forceEnd(): void { 89 if (this._status === 'idle') return 90 this._status = 'idle' 91 ++this._generation 92 this._notify() 93 } 94 95 /** 96 * Is the guard active (dispatching or running)? 97 * Always synchronous — not subject to React state batching delays. 98 */ 99 get isActive(): boolean { 100 return this._status !== 'idle' 101 } 102 103 get generation(): number { 104 return this._generation 105 } 106 107 // -- 108 // useSyncExternalStore interface 109 110 /** Subscribe to state changes. Stable reference — safe as useEffect dep. */ 111 subscribe = this._changed.subscribe 112 113 /** Snapshot for useSyncExternalStore. Returns `isActive`. */ 114 getSnapshot = (): boolean => { 115 return this._status !== 'idle' 116 } 117 118 private _notify(): void { 119 this._changed.emit() 120 } 121}