All files / plainweb/src/task database.ts

97.05% Statements 66/68
100% Branches 8/8
87.5% Functions 7/8
97.05% Lines 66/68

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 941x 1x 1x 1x 1x           1x               1x   1x 1x 1x 1x 1x 1x 1x 1x 1x     1x   1x                 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x     1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x  
import type BetterSqlite3Database from "better-sqlite3";
import { type ExtractTablesWithRelations, and, lte, or } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { asc } from "drizzle-orm";
import { isNull } from "drizzle-orm";
import {
  type BaseSQLiteDatabase,
  int,
  sqliteTable,
  text,
} from "drizzle-orm/sqlite-core";
import {
  type DefineTaskOpts,
  type PersistedTask,
  type Task,
  type TaskStorage,
  composePersistedTask,
  defineTaskWithAdapter,
} from "./task";
 
const tasks = sqliteTable("tasks", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  data: text("data", { mode: "json" }),
  created: int("created").notNull(),
  failedLast: int("failed_last"),
  failedNr: int("failed_nr"),
  failedError: text("failed_error"),
});
 
// internal check
const _ = tasks.$inferSelect satisfies PersistedTask<unknown>;
 
const schema = { tasks };
 
export type Database = BaseSQLiteDatabase<
  "sync",
  BetterSqlite3Database.RunResult,
  typeof schema,
  ExtractTablesWithRelations<typeof schema>
>;
 
function composeDatabaseAdapter(database: Database): TaskStorage<unknown> {
  return {
    async enqueue({ data, name }) {
      const persistedTask = composePersistedTask({ data, name });
      await database.insert(tasks).values(persistedTask);
    },
    async fetch({ name: taskName, batchSize, maxRetries, retryIntervall }) {
      const tasks = await database.query.tasks.findMany({
        limit: batchSize,
        orderBy: ({ created }) => asc(created),
        where: ({ failedNr, failedLast, name }) =>
          and(
            or(lte(failedNr, maxRetries), isNull(failedNr)),
            or(
              lte(failedLast, Date.now() - retryIntervall),
              isNull(failedLast),
            ),
            eq(name, taskName),
          ),
      });
      return tasks;
    },
    async success({ task }) {
      await database.delete(tasks).where(eq(tasks.id, task.id));
    },
    async failure({ task, err }) {
      await database
        .update(tasks)
        .set({
          failedLast: Date.now(),
          failedNr: (task.failedNr ?? 0) + 1,
          failedError: JSON.stringify({
            instance: (err as Error).constructor.name,
            message: (err as Error).message,
            stack: (err as Error).stack,
          }),
        })
        .where(eq(tasks.id, task.id));
    },
  };
}
 
export function defineDatabaseTask<T>(
  database: Database,
  opts: DefineTaskOpts<T>,
): Task<T> {
  return defineTaskWithAdapter(
    composeDatabaseAdapter(database),
    opts as DefineTaskOpts<unknown>,
  );
}