πŸ“„ File detail

utils/QueryGuard.ts

🧩 .tsπŸ“ 122 linesπŸ’Ύ 3,597 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes QueryGuard β€” mainly types, interfaces, or factory objects. It composes internal code from signal (relative imports). What the file header says: 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 e.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

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

πŸ“€ Exports (heuristic)

  • QueryGuard

πŸ–₯️ Source preview

/**
 * 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()
  }
}