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

            Line data    Source code
       1              : import type { HyperDBSourceConnection } from '../../platform/hyperdb/hyperdb_incremental_extract.js';
       2              : import { sqlLiteral } from '../shared/sql_template.js';
       3              : 
       4              : /// A trade of a symbol with the best bid and ask at its time, a row of the table `ticks`
       5              : export interface Tick {
       6              :     /// The symbol, e.g. AAPL or BTC-USD
       7              :     symbol: string;
       8              :     /// The time of the trade in milliseconds since the epoch
       9              :     ts: number;
      10              :     /// The price of the trade
      11              :     price: number;
      12              :     /// The traded quantity
      13              :     size: number;
      14              :     /// The best bid, null if unknown
      15              :     bid: number | null;
      16              :     /// The best ask, null if unknown
      17              :     ask: number | null;
      18              : }
      19              : 
      20              : /// The state of a feed, shown next to its name
      21              : export interface TickFeedStatus {
      22              :     /// Is the feed delivering ticks?
      23              :     state: 'connecting' | 'live' | 'paused' | 'failed';
      24              :     /// The reason of a failure or a reconnect
      25              :     detail?: string;
      26              : }
      27              : 
      28              : /// A source of ticks, e.g. a generator or a market data stream
      29              : export interface TickFeed {
      30              :     /// Start delivering ticks, the feed calls emit with the ticks since its previous call
      31              :     start(emit: (ticks: Tick[]) => void, onStatus: (status: TickFeedStatus) => void): void;
      32              :     /// Stop delivering ticks
      33              :     stop(): void;
      34              : }
      35              : 
      36              : /// The statement that inserts ticks into the table `ticks`
      37            1 : export function insertTicksSql(ticks: readonly Tick[]): string {
      38            1 :     const rows = ticks.map(tick => `(${[
      39            1 :         sqlLiteral(tick.symbol),
      40            1 :         `${sqlLiteral(new Date(tick.ts).toISOString())}::timestamptz`,
      41            1 :         sqlLiteral(tick.price),
      42            1 :         sqlLiteral(tick.size),
      43            1 :         sqlLiteral(tick.bid),
      44            1 :         sqlLiteral(tick.ask),
      45            1 :     ].join(', ')})`);
      46            1 :     return `insert into ticks (symbol, ts, price, size, bid, ask) values\n${rows.join(',\n')}`;
      47            1 : }
      48              : 
      49              : /// The statement that deletes the ticks older than the retention
      50            1 : export function deleteTicksBeforeSql(retentionMs: number): string {
      51            1 :     return `delete from ticks where ts < now() - interval '${Math.round(retentionMs / 1000)} seconds'`;
      52            1 : }
      53              : 
      54              : /// The options of a tick writer
      55              : export interface TickWriterOptions {
      56              :     /// The time between two inserts
      57              :     flushIntervalMs?: number;
      58              :     /// The maximum number of ticks of an insert, the rest waits for the next one
      59              :     maxBatchSize?: number;
      60              :     /// The age of the oldest ticks that the table keeps
      61              :     retentionMs?: number;
      62              :     /// The time between two deletions of old ticks
      63              :     retentionIntervalMs?: number;
      64              :     /// Receives the failures of statements, the writer keeps going
      65              :     onError?: (error: unknown) => void;
      66              : }
      67              : 
      68              : /// Write the ticks of a feed into the table `ticks` in batches.
      69              : /// All statements run one after another on the connection of the writer, a HyperDB WASM connection runs one at a time.
      70            1 : export class TickWriter {
      71              :     /// The ticks waiting for the next insert
      72            1 :     private pending: Tick[] = [];
      73              :     /// The timer of the next flush
      74            1 :     private timer: ReturnType<typeof setTimeout> | null = null;
      75              :     /// The running flush
      76            1 :     private flushing: Promise<void> | null = null;
      77              :     /// The time of the last deletion of old ticks, the first one is due an interval after the start
      78            1 :     private lastRetention = Date.now();
      79              :     /// Is the writer stopped?
      80            1 :     private stopped = false;
      81              :     /// The number of inserted ticks
      82            1 :     written = 0;
      83              :     /// The number of ticks that were dropped because the inserts did not keep up
      84            1 :     dropped = 0;
      85              : 
      86            1 :     private readonly flushIntervalMs: number;
      87            1 :     private readonly maxBatchSize: number;
      88            1 :     private readonly retentionMs: number;
      89            1 :     private readonly retentionIntervalMs: number;
      90              : 
      91            1 :     constructor(private readonly connection: HyperDBSourceConnection, private readonly options: TickWriterOptions = {}) {
      92            1 :         this.flushIntervalMs = options.flushIntervalMs ?? 200;
      93            1 :         this.maxBatchSize = options.maxBatchSize ?? 10000;
      94            1 :         this.retentionMs = options.retentionMs ?? 30 * 60 * 1000;
      95            1 :         this.retentionIntervalMs = options.retentionIntervalMs ?? 10 * 1000;
      96            1 :     }
      97              : 
      98              :     /// Queue ticks for the next insert. Beyond 10 batches the oldest ticks are dropped, the table stays live.
      99            1 :     push(ticks: readonly Tick[]): void {
     100            1 :         if (this.stopped) return;
     101              :         // A spread of a large backfill would exceed the argument limit of push
     102            1 :         for (const tick of ticks) this.pending.push(tick);
     103            1 :         const overflow = this.pending.length - 10 * this.maxBatchSize;
     104            1 :         if (overflow > 0) {
     105            1 :             this.pending.splice(0, overflow);
     106            1 :             this.dropped += overflow;
     107            1 :         }
     108            1 :         this.timer ??= setTimeout(() => {
     109            1 :             this.timer = null;
     110            1 :             this.flushing = this.flush();
     111            1 :         }, this.flushIntervalMs);
     112            1 :     }
     113              : 
     114              :     /// Insert the pending ticks and delete old ones when it is time to
     115            1 :     async flush(): Promise<void> {
     116            1 :         await this.flushing;
     117            1 :         if (this.stopped) return;
     118            1 :         try {
     119            1 :             while (this.pending.length > 0) {
     120            1 :                 const batch = this.pending.splice(0, this.maxBatchSize);
     121            1 :                 await this.connection.execute(insertTicksSql(batch));
     122            1 :                 this.written += batch.length;
     123            1 :             }
     124            1 :             const now = Date.now();
     125            1 :             if (now - this.lastRetention >= this.retentionIntervalMs) {
     126            1 :                 this.lastRetention = now;
     127            1 :                 await this.connection.execute(deleteTicksBeforeSql(this.retentionMs));
     128            1 :             }
     129            1 :         } catch (e) {
     130            1 :             this.options.onError?.(e);
     131            1 :         }
     132            1 :     }
     133              : 
     134              :     /// Stop writing, pending ticks are dropped
     135            1 :     async stop(): Promise<void> {
     136            1 :         this.stopped = true;
     137            1 :         if (this.timer != null) clearTimeout(this.timer);
     138            1 :         this.timer = null;
     139            1 :         this.pending = [];
     140            1 :         await this.flushing?.catch(() => { });
     141            1 :     }
     142            1 : }
        

Generated by: LCOV version 2.0-1