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 : }
|