LCOV - code coverage report
Current view: top level - dashql-app/src/demos/shared - use_live_query.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 63 63
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import type * as arrow from 'apache-arrow';
       2              : import * as React from 'react';
       3              : 
       4              : import type { HyperDBSourceConnection, HyperDBSourceDatabase } from '../../platform/hyperdb/hyperdb_incremental_extract.js';
       5              : 
       6              : /// The latest result of a query that runs again and again
       7              : export interface LiveQueryState {
       8              :     /// The latest result, kept when a later run fails
       9              :     table: arrow.Table | null;
      10              :     /// The time of the latest run in milliseconds
      11              :     latencyMs: number | null;
      12              :     /// The error of the latest run, null once a run succeeds again
      13              :     error: Error | null;
      14              : }
      15              : 
      16            1 : export const NO_LIVE_QUERY_RESULT: LiveQueryState = { table: null, latencyMs: null, error: null };
      17              : 
      18              : /// The options of a live query loop
      19              : export interface LiveQueryLoopOptions {
      20              :     /// The time between the end of a run and the start of the next one
      21              :     intervalMs: number;
      22              :     /// Skip runs while the page is hidden
      23              :     isHidden?: () => boolean;
      24              : }
      25              : 
      26              : /// Run a query on its own connection, again and again.
      27              : /// The next run starts an interval after the previous one ended, so that runs never overlap, e.g. on a slow remote Hyper.
      28            1 : export class LiveQueryLoop {
      29              :     /// Stops the loop and cancels the running query
      30            1 :     private readonly abort = new AbortController();
      31              :     /// The timer of the next run
      32            1 :     private timer: ReturnType<typeof setTimeout> | null = null;
      33              :     /// The connection of the runs
      34            1 :     private connection: Promise<HyperDBSourceConnection> | null = null;
      35              :     /// The running query
      36            1 :     private running: Promise<void> | null = null;
      37              : 
      38            1 :     constructor(
      39            1 :         private readonly database: HyperDBSourceDatabase,
      40            1 :         private readonly sql: string,
      41            1 :         private readonly options: LiveQueryLoopOptions,
      42            1 :         private readonly onResult: (state: LiveQueryState) => void,
      43            1 :     ) {}
      44              : 
      45              :     /// Start the first run
      46            1 :     start(): void {
      47            1 :         this.running = this.run();
      48            1 :     }
      49              : 
      50              :     /// Stop the loop, cancel the running query and close the connection
      51            1 :     async stop(): Promise<void> {
      52            1 :         this.abort.abort();
      53            1 :         if (this.timer != null) clearTimeout(this.timer);
      54            1 :         this.timer = null;
      55            1 :         await this.running?.catch(() => { });
      56            1 :         const connection = await this.connection?.catch(() => null);
      57            1 :         await connection?.close().catch(() => { });
      58            1 :     }
      59              : 
      60            1 :     private schedule(): void {
      61            1 :         if (this.abort.signal.aborted) return;
      62            1 :         this.timer = setTimeout(() => {
      63            1 :             this.timer = null;
      64            1 :             this.running = this.run();
      65            1 :         }, this.options.intervalMs);
      66            1 :     }
      67              : 
      68            1 :     private async run(): Promise<void> {
      69            1 :         if (this.options.isHidden?.()) {
      70            1 :             this.schedule();
      71            1 :             return;
      72            1 :         }
      73            1 :         const start = performance.now();
      74            1 :         let table: arrow.Table | null = null;
      75            1 :         let error: Error | null = null;
      76            1 :         try {
      77            1 :             this.connection ??= this.database.connect();
      78            1 :             table = await (await this.connection).query(this.sql, this.abort.signal);
      79            1 :         } catch (e: any) {
      80            1 :             error = e instanceof Error ? e : new Error(String(e));
      81              :             // A connection that failed to open is opened again by the next run
      82            1 :             if (await this.connection?.then(() => false, () => true)) this.connection = null;
      83            1 :         }
      84            1 :         if (this.abort.signal.aborted) return;
      85            1 :         this.onResult({ table, latencyMs: performance.now() - start, error });
      86            1 :         this.schedule();
      87            1 :     }
      88            1 : }
      89              : 
      90              : /// Run a query again and again while the component is mounted, null for no database or no query
      91            1 : export function useLiveQuery(database: HyperDBSourceDatabase | null, sql: string | null, intervalMs: number): LiveQueryState {
      92            1 :     const [state, setState] = React.useState<LiveQueryState>(NO_LIVE_QUERY_RESULT);
      93            1 :     React.useEffect(() => {
      94            1 :         setState(NO_LIVE_QUERY_RESULT);
      95            1 :         if (database == null || sql == null) return;
      96            1 :         const loop = new LiveQueryLoop(database, sql, { intervalMs, isHidden: () => document.hidden }, result => {
      97              :             // A failed run keeps the previous result on screen
      98            1 :             setState(prev => result.table == null ? { ...prev, latencyMs: result.latencyMs, error: result.error } : result);
      99            1 :         });
     100            1 :         loop.start();
     101            1 :         return () => { void loop.stop(); };
     102            1 :     }, [database, sql, intervalMs]);
     103            1 :     return state;
     104            1 : }
        

Generated by: LCOV version 2.0-1