80 lines · 3.3 KB
import { backfillAccounts, backfillDone, backfillOnce } from "../backfill";
import { all, batch, stmt } from "../db";
import { accountDb } from "../scoped-db";
import type { ScopedValue } from "../scoped-db/types";
import { scratchAccountIds } from "./directory";
import { periodOf } from "./record";

const INDEX = "usage-leaderboard";

export type UsageRow = { account_id: string; meter: string; quantity: number };
type Sql = { exec<T extends Record<string, ScopedValue>>(sql: string, ...params: ScopedValue[]): { toArray(): T[] } };

/** An account's counters and gauge peaks for a period, as its own AccountState stores them. */
export const PERIOD_USAGE_SQL = `SELECT account_id, meter, quantity FROM usage_periods WHERE period = ?1 AND quantity > 0
  UNION ALL SELECT account_id, meter, peak AS quantity FROM usage_gauges WHERE period = ?1 AND peak > 0`;

async function write(accountId: string, period: string, rows: UsageRow[], now: number): Promise<void> {
  const ids = [...new Set([accountId, ...rows.map((row) => row.account_id)])];
  await batch([
    stmt(
      `DELETE FROM usage_leaderboard WHERE period = ? AND account_id IN (${ids.map(() => "?").join(", ")})`,
      period,
      ...ids,
    ),
    ...rows.map((row) =>
      stmt(
        `INSERT INTO usage_leaderboard (period, meter, account_id, quantity, updated_at) VALUES (?, ?, ?, ?, ?)
         ON CONFLICT (period, meter, account_id) DO UPDATE SET quantity = excluded.quantity, updated_at = excluded.updated_at`,
        period,
        row.meter,
        row.account_id,
        row.quantity,
        now,
      ),
    ),
  ]);
}

/**
 * AccountState alarm, a few minutes after usage moves: replaces the account's rows for the current period, and