QueryGuard.ts
utils/QueryGuard.ts
No strong subsystem tag
122
Lines
3597
Bytes
1
Exports
1
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 122 lines, 1 detected imports, and 1 detected exports.
Important relationships
Detected exports
QueryGuard
Keywords
idlestatusgenerationquerydispatchingrunningreturnsnotifyqueryguardusesyncexternalstore
Detected imports
./signal.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* Synchronous state machine for the query lifecycle, compatible with
* React's `useSyncExternalStore`.
*
* Three states:
* idle → no query, safe to dequeue and process
* dispatching → an item was dequeued, async chain hasn't reached onQuery yet
* running → onQuery called tryStart(), query is executing
*
* Transitions:
* idle → dispatching (reserve)
* dispatching → running (tryStart)
* idle → running (tryStart, for direct user submissions)
* running → idle (end / forceEnd)
* dispatching → idle (cancelReservation, when processQueueIfReady fails)
*
* `isActive` returns true for both dispatching and running, preventing
* re-entry from the queue processor during the async gap.
*
* Usage with React:
* const queryGuard = useRef(new QueryGuard()).current
* const isQueryActive = useSyncExternalStore(
* queryGuard.subscribe,
* queryGuard.getSnapshot,
* )
*/
import { createSignal } from './signal.js'
export class QueryGuard {
private _status: 'idle' | 'dispatching' | 'running' = 'idle'
private _generation = 0
private _changed = createSignal()
/**
* Reserve the guard for queue processing. Transitions idle → dispatching.
* Returns false if not idle (another query or dispatch in progress).
*/
reserve(): boolean {
if (this._status !== 'idle') return false
this._status = 'dispatching'
this._notify()
return true
}
/**
* Cancel a reservation when processQueueIfReady had nothing to process.
* Transitions dispatching → idle.
*/
cancelReservation(): void {
if (this._status !== 'dispatching') return
this._status = 'idle'
this._notify()
}
/**
* Start a query. Returns the generation number on success,
* or null if a query is already running (concurrent guard).
* Accepts transitions from both idle (direct user submit)
* and dispatching (queue processor path).
*/
tryStart(): number | null {
if (this._status === 'running') return null
this._status = 'running'
++this._generation
this._notify()
return this._generation
}
/**
* End a query. Returns true if this generation is still current
* (meaning the caller should perform cleanup). Returns false if a
* newer query has started (stale finally block from a cancelled query).
*/
end(generation: number): boolean {
if (this._generation !== generation) return false
if (this._status !== 'running') return false
this._status = 'idle'
this._notify()
return true
}
/**
* Force-end the current query regardless of generation.
* Used by onCancel where any running query should be terminated.
* Increments generation so stale finally blocks from the cancelled
* query's promise rejection will see a mismatch and skip cleanup.
*/
forceEnd(): void {
if (this._status === 'idle') return
this._status = 'idle'
++this._generation
this._notify()
}
/**
* Is the guard active (dispatching or running)?
* Always synchronous — not subject to React state batching delays.
*/
get isActive(): boolean {
return this._status !== 'idle'
}
get generation(): number {
return this._generation
}
// --
// useSyncExternalStore interface
/** Subscribe to state changes. Stable reference — safe as useEffect dep. */
subscribe = this._changed.subscribe
/** Snapshot for useSyncExternalStore. Returns `isActive`. */
getSnapshot = (): boolean => {
return this._status !== 'idle'
}
private _notify(): void {
this._changed.emit()
}
}