Skip to content

IStore interface

All store packages implement a common IStore interface from @flaky-tests/core. This is what makes backends swappable — the rest of the system never depends on a concrete store implementation, only on this contract.

import type {
IStore,
InsertRunInput,
UpdateRunInput,
InsertFailureInput,
GetNewPatternsOptions,
GetRecentRunsOptions,
FlakyPattern,
RecentRun,
} from '@flaky-tests/core'
interface IStore {
migrate(): Promise<void>
insertRun(input: InsertRunInput): Promise<void>
updateRun(runId: string, input: UpdateRunInput): Promise<void>
insertFailure(input: InsertFailureInput): Promise<void>
insertFailures(inputs: readonly InsertFailureInput[]): Promise<void>
getNewPatterns(options?: GetNewPatternsOptions): Promise<FlakyPattern[]>
getRecentRuns(options: GetRecentRunsOptions): Promise<RecentRun[]>
close(): Promise<void>
}

Every method is async so adapters backed by an HTTP client (Turso, Supabase, Postgres) share the same signature as the synchronous SQLite adapter.

Creates tables and runs any pending schema migrations. Migrations are versioned and idempotent — the sqlite and turso adapters track applied versions in a _flaky_tests_migrations table and skip anything already applied, so calling migrate() on every startup is safe and cheap. New schema versions are picked up automatically when the adapter package is upgraded.

Implementations must wrap any driver-level error thrown by a public method in a StoreError with { package, method, message, cause } set. The cause field preserves the original stack for inspection. Validation errors raised by arktype (surfaced as ValidationError) propagate unwrapped so bad input stays distinguishable from a downstream driver failure.

import { StoreError } from '@flaky-tests/core'
try {
await store.insertRun(input)
} catch (error) {
if (error instanceof StoreError) {
console.error(error.package) // e.g. '@flaky-tests/store-supabase'
console.error(error.method) // e.g. 'insertRun'
console.error(error.cause) // original driver error
}
throw error
}

StoreError.message is formatted as [<package>] <method>: <message> so logs are self-describing without extra plumbing.

Network-backed adapters (turso, supabase, postgres) automatically retry transient read failures using exponential backoff with jitter via withRetry() from @flaky-tests/core. Only these failures are retried:

  • Network transport errors: ECONNRESET, ECONNREFUSED, ETIMEDOUT, ENETUNREACH, EAI_AGAIN
  • Timeout messages (timed out, socket hang up, fetch failed)
  • HTTP 5xx responses

HTTP 4xx responses, validation errors, and anything else propagate immediately so logic bugs are never masked by a retry loop. The default schedule is 3 attempts with a 100 ms base delay doubling each attempt; aborting the query’s AbortSignal cancels any pending backoff sleep.

Both read methods — getNewPatterns(options?) and getRecentRuns(options) — accept an optional signal?: AbortSignal. When the signal aborts, the pending promise rejects with a native AbortError (the signal’s reason), even for drivers that cannot cancel the underlying request. In that case the in-flight query still resolves in the background but its result is discarded.

const controller = new AbortController()
setTimeout(() => controller.abort(), 5_000)
try {
const patterns = await store.getNewPatterns({
windowDays: 7,
threshold: 2,
signal: controller.signal,
})
} catch (error) {
if ((error as Error).name === 'AbortError') {
// query was cancelled — handle as expected
} else {
throw error
}
}

signal is a TypeScript-only extension on the options type; arktype drops it during validation and adapters read it straight off the caller’s options object.

Every adapter registers a descriptor at module import time so the dispatcher in @flaky-tests/core can find it by name. There is no hardcoded list of adapters anywhere in the dispatcher.

import { definePlugin, type Config, type IStore } from '@flaky-tests/core'
export const myStorePlugin = definePlugin({
name: 'store-mything',
create(config: Config): IStore {
return new MyStore(/* unpack config */)
},
})

See the custom stores guide for a complete walkthrough.

interface InsertRunInput {
runId: string // stable id for the run (UUID)
startedAt: string // ISO 8601 timestamp
project?: string | null // optional project slug for multi-project stores
gitSha?: string | null
gitDirty?: boolean | null
runtimeVersion?: string | null
testArgs?: string | null
}
interface UpdateRunInput {
endedAt?: string // ISO 8601 timestamp
durationMs?: number
status?: 'pass' | 'fail'
totalTests?: number
passedTests?: number
failedTests?: number
errorsBetweenTests?: number
}
interface InsertFailureInput {
runId: string
testFile: string // relative path to test file
testName: string // full test name including describe blocks
failureKind: FailureKind
errorMessage?: string | null
errorStack?: string | null
durationMs?: number | null
failedAt: string // ISO 8601 timestamp
}
type FailureKind = 'assertion' | 'timeout' | 'uncaught' | 'unknown'

Determined by categorizeError(error) in @flaky-tests/core.

interface GetNewPatternsOptions {
/**
* Length of each detection window in days.
* The current window is [now - windowDays, now].
* The prior window is [now - 2*windowDays, now - windowDays].
* Default: 7
*/
windowDays?: number
/**
* Minimum failures in the current window to flag as a new pattern.
* Default: 2
*/
threshold?: number
/**
* Filter to a single project. Pass `null` / leave undefined to match
* rows whose `project` column is NULL.
*/
project?: string | null
/** Abort the query when this signal aborts; rejects with `AbortError`. */
signal?: AbortSignal
}
interface GetRecentRunsOptions {
limit: number
/** Filter to a single project (same semantics as `GetNewPatternsOptions.project`). */
project?: string | null
/** Abort the query when this signal aborts; rejects with `AbortError`. */
signal?: AbortSignal
}
interface FlakyPattern {
testFile: string
testName: string
recentFails: number // failures in the current window
priorFails: number // failures in the prior window (always 0 for new patterns)
failureKinds: FailureKind[] // distinct kinds seen in the current window
lastErrorMessage: string | null
lastErrorStack: string | null
lastFailed: string // ISO 8601 timestamp of most recent failure
}
interface RecentRun {
runId: string
project: string | null
startedAt: string
endedAt: string | null
durationMs: number | null
status: 'pass' | 'fail' | null
totalTests: number | null
passedTests: number | null
failedTests: number | null
errorsBetweenTests: number | null
gitSha: string | null
gitDirty: boolean | null
}

@flaky-tests/core/test-helpers exports a shared runContractTests(label, makeStore) that exercises scenarios covering the full IStore surface — round-trip, validation, window-edge, threshold, multi-pattern sorting, distinct kinds, last-error capture, infra-blowup filter, AbortSignal cancellation, StoreError wrapping, and close() idempotency. Every built-in adapter passes it; custom adapters should invoke it too.

import { runContractTests } from '@flaky-tests/core/test-helpers'
runContractTests('mything', () => new MyStore({ /* test config */ }))