Step 7.1: add pg-boss job queue
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0044e355f1
commit
9847a7ae18
|
|
@ -11,3 +11,6 @@ BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
# HTTP port the API server listens on
|
# HTTP port the API server listens on
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
|
# Run background job workers (arrival resolution, cleanup) in the API process
|
||||||
|
WORKERS_ENABLED=true
|
||||||
|
|
|
||||||
|
|
@ -340,3 +340,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
|
|
||||||
- **No new mechanism was needed:** since Step 6.1 every authenticated endpoint runs in `withPlayer`, a transaction that first takes `SELECT ... FOR UPDATE` on the player's company row. Requests for one player run one after another; different players never block each other.
|
- **No new mechanism was needed:** since Step 6.1 every authenticated endpoint runs in `withPlayer`, a transaction that first takes `SELECT ... FOR UPDATE` on the player's company row. Requests for one player run one after another; different players never block each other.
|
||||||
- **Race tests go beyond the plan's double purchase:** eight simultaneous purchases with money for three, the same contract dispatched twice at once, and the same bay leased twice at once. With the row lock removed, three of the four fail, which confirms the tests exercise the lock. The two-purchase race can pass by luck without the lock, which is why the eight-purchase version exists.
|
- **Race tests go beyond the plan's double purchase:** eight simultaneous purchases with money for three, the same contract dispatched twice at once, and the same bay leased twice at once. With the row lock removed, three of the four fail, which confirms the tests exercise the lock. The two-purchase race can pass by luck without the lock, which is why the eight-purchase version exists.
|
||||||
|
|
||||||
|
## Step 7.1
|
||||||
|
|
||||||
|
- **Jobs sit behind a small `JobQueue` interface** (`start`, `stop`, `send`). Production uses pg-boss (v12) in the same Postgres database, in its own `pgboss` schema; most tests pass a recording fake, so they don't start pg-boss, and call job handlers directly (decision 0.5.18). Only the queue test uses real pg-boss.
|
||||||
|
- **`WORKERS_ENABLED` (default `true`)** controls whether this process runs workers and cron schedules. Queues are always created and pg-boss always starts, so an API-only process can still enqueue arrival jobs. Tests set it to `false`.
|
||||||
|
- **The queue starts in Fastify's `onReady` hook and stops (gracefully) in `onClose`,** before the database pool closes.
|
||||||
|
- **The server listens on `0.0.0.0` (IPv4).** Clients that resolve `localhost` to IPv6 `::1` first (PowerShell, Node 17+ `fetch`) cannot reach it by that name; use `127.0.0.1`. To revisit when the client dev proxy is set up (Phase 8).
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"drizzle-orm": "^0.45.3",
|
"drizzle-orm": "^0.45.3",
|
||||||
"fastify": "^5.12.5",
|
"fastify": "^5.12.5",
|
||||||
"pg": "^8.23.1",
|
"pg": "^8.23.1",
|
||||||
|
"pg-boss": "^12.36.0",
|
||||||
"zod": "^4.6.5"
|
"zod": "^4.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type { ServerConfig } from "./config";
|
||||||
import { createDatabase, type Database } from "./db/client";
|
import { createDatabase, type Database } from "./db/client";
|
||||||
import { registerErrorHandler } from "./http/errors";
|
import { registerErrorHandler } from "./http/errors";
|
||||||
import { requirePlayerId } from "./http/session";
|
import { requirePlayerId } from "./http/session";
|
||||||
|
import { createPgBossQueue, type JobQueue } from "./jobs/queue";
|
||||||
import type { AppContext } from "./routes/context";
|
import type { AppContext } from "./routes/context";
|
||||||
import { registerBoardRoutes } from "./routes/boards";
|
import { registerBoardRoutes } from "./routes/boards";
|
||||||
import { registerDispatchRoutes } from "./routes/dispatches";
|
import { registerDispatchRoutes } from "./routes/dispatches";
|
||||||
|
|
@ -15,6 +16,8 @@ export interface AppDeps {
|
||||||
clock?: Clock;
|
clock?: Clock;
|
||||||
/** An existing database (tests). Otherwise the app opens one and closes it on shutdown. */
|
/** An existing database (tests). Otherwise the app opens one and closes it on shutdown. */
|
||||||
database?: Database;
|
database?: Database;
|
||||||
|
/** Background job queue. Defaults to pg-boss on the app's database. */
|
||||||
|
jobs?: JobQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the Fastify app without listening, so tests can start it on any port. */
|
/** Builds the Fastify app without listening, so tests can start it on any port. */
|
||||||
|
|
@ -22,10 +25,14 @@ export function buildApp(config: ServerConfig, deps: AppDeps = {}): FastifyInsta
|
||||||
const database = deps.database ?? createDatabase(config.databaseUrl);
|
const database = deps.database ?? createDatabase(config.databaseUrl);
|
||||||
const { db } = database;
|
const { db } = database;
|
||||||
const auth = createAuth(db, { secret: config.authSecret, baseUrl: config.authUrl });
|
const auth = createAuth(db, { secret: config.authSecret, baseUrl: config.authUrl });
|
||||||
|
const jobs =
|
||||||
|
deps.jobs ??
|
||||||
|
createPgBossQueue(config.databaseUrl, { workers: config.workersEnabled, jobs: [] });
|
||||||
const ctx: AppContext = {
|
const ctx: AppContext = {
|
||||||
db,
|
db,
|
||||||
auth,
|
auth,
|
||||||
clock: deps.clock ?? systemClock,
|
clock: deps.clock ?? systemClock,
|
||||||
|
jobs,
|
||||||
options: {
|
options: {
|
||||||
requirePlayer: (request) => requirePlayerId(request, auth, db),
|
requirePlayer: (request) => requirePlayerId(request, auth, db),
|
||||||
validateResponses: config.env !== "production",
|
validateResponses: config.env !== "production",
|
||||||
|
|
@ -36,7 +43,12 @@ export function buildApp(config: ServerConfig, deps: AppDeps = {}): FastifyInsta
|
||||||
// Structured JSON logs (pino); silenced in tests unless LOG_LEVEL says otherwise.
|
// Structured JSON logs (pino); silenced in tests unless LOG_LEVEL says otherwise.
|
||||||
logger: { level: config.logLevel },
|
logger: { level: config.logLevel },
|
||||||
});
|
});
|
||||||
if (!deps.database) app.addHook("onClose", () => database.close());
|
// The queue starts with the app and stops before the database closes.
|
||||||
|
app.addHook("onReady", () => jobs.start());
|
||||||
|
app.addHook("onClose", async () => {
|
||||||
|
await jobs.stop();
|
||||||
|
if (!deps.database) await database.close();
|
||||||
|
});
|
||||||
registerErrorHandler(app);
|
registerErrorHandler(app);
|
||||||
|
|
||||||
// Under /api so it is reachable through the same-domain proxy (decision 0.5.20).
|
// Under /api so it is reachable through the same-domain proxy (decision 0.5.20).
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ const envSchema = z.object({
|
||||||
TEST_DATABASE_URL: z.url().optional(),
|
TEST_DATABASE_URL: z.url().optional(),
|
||||||
BETTER_AUTH_SECRET: z.string().min(32),
|
BETTER_AUTH_SECRET: z.string().min(32),
|
||||||
BETTER_AUTH_URL: z.url(),
|
BETTER_AUTH_URL: z.url(),
|
||||||
|
/** Run background job workers in this process (Step 7.1). */
|
||||||
|
WORKERS_ENABLED: z.enum(["true", "false"]).default("true"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
|
|
@ -19,6 +21,7 @@ export interface ServerConfig {
|
||||||
testDatabaseUrl: string | undefined;
|
testDatabaseUrl: string | undefined;
|
||||||
authSecret: string;
|
authSecret: string;
|
||||||
authUrl: string;
|
authUrl: string;
|
||||||
|
workersEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validates the environment, throwing one error that lists every problem. */
|
/** Validates the environment, throwing one error that lists every problem. */
|
||||||
|
|
@ -37,5 +40,6 @@ export function loadConfig(env: Record<string, string | undefined>): ServerConfi
|
||||||
testDatabaseUrl: e.TEST_DATABASE_URL,
|
testDatabaseUrl: e.TEST_DATABASE_URL,
|
||||||
authSecret: e.BETTER_AUTH_SECRET,
|
authSecret: e.BETTER_AUTH_SECRET,
|
||||||
authUrl: e.BETTER_AUTH_URL,
|
authUrl: e.BETTER_AUTH_URL,
|
||||||
|
workersEnabled: e.WORKERS_ENABLED === "true",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { PgBoss } from "pg-boss";
|
||||||
|
|
||||||
|
export type JobHandler = (data: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
export interface JobDefinition {
|
||||||
|
name: string;
|
||||||
|
handler: JobHandler;
|
||||||
|
/** Cron expression for a recurring job (UTC). */
|
||||||
|
cron?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Background work. The real one is pg-boss; tests can pass a recording fake. */
|
||||||
|
export interface JobQueue {
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
/** Enqueues a job, optionally not before `startAfter` (Unix ms). */
|
||||||
|
send(name: string, data: Record<string, unknown>, startAfter?: number): Promise<string | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pg-boss in the same Postgres database (its own `pgboss` schema). Queues are
|
||||||
|
* always created so the app can enqueue; workers and cron schedules run only
|
||||||
|
* when `workers` is true (Step 7.1).
|
||||||
|
*/
|
||||||
|
export function createPgBossQueue(
|
||||||
|
databaseUrl: string,
|
||||||
|
options: { workers: boolean; jobs: JobDefinition[]; onError?: (error: Error) => void },
|
||||||
|
): JobQueue & { boss: PgBoss } {
|
||||||
|
const boss = new PgBoss({ connectionString: databaseUrl, schedule: options.workers });
|
||||||
|
if (options.onError) boss.on("error", options.onError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
boss,
|
||||||
|
async start() {
|
||||||
|
await boss.start();
|
||||||
|
for (const job of options.jobs) {
|
||||||
|
await boss.createQueue(job.name);
|
||||||
|
if (!options.workers) continue;
|
||||||
|
await boss.work<Record<string, unknown>>(job.name, async (batch) => {
|
||||||
|
for (const item of batch) await job.handler(item.data);
|
||||||
|
});
|
||||||
|
if (job.cron) await boss.schedule(job.name, job.cron);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async stop() {
|
||||||
|
await boss.stop({ graceful: true });
|
||||||
|
},
|
||||||
|
send(name, data, startAfter) {
|
||||||
|
return boss.send(
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
startAfter === undefined ? {} : { startAfter: new Date(startAfter) },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A queue that only records what was sent; for tests that call handlers directly. */
|
||||||
|
export function createRecordingQueue(): JobQueue & {
|
||||||
|
sent: { name: string; data: Record<string, unknown>; startAfter?: number }[];
|
||||||
|
} {
|
||||||
|
const sent: { name: string; data: Record<string, unknown>; startAfter?: number }[] = [];
|
||||||
|
return {
|
||||||
|
sent,
|
||||||
|
async start() {},
|
||||||
|
async stop() {},
|
||||||
|
async send(name, data, startAfter) {
|
||||||
|
sent.push({ name, data, ...(startAfter === undefined ? {} : { startAfter }) });
|
||||||
|
return `recorded-${sent.length}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,13 @@ import type { Auth } from "../auth";
|
||||||
import type { Clock } from "../clock";
|
import type { Clock } from "../clock";
|
||||||
import type { Db } from "../db/client";
|
import type { Db } from "../db/client";
|
||||||
import type { RouteOptions } from "../http/route";
|
import type { RouteOptions } from "../http/route";
|
||||||
|
import type { JobQueue } from "../jobs/queue";
|
||||||
|
|
||||||
/** What every route module needs. */
|
/** What every route module needs. */
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
db: Db;
|
db: Db;
|
||||||
auth: Auth;
|
auth: Auth;
|
||||||
clock: Clock;
|
clock: Clock;
|
||||||
|
jobs: JobQueue;
|
||||||
options: RouteOptions;
|
options: RouteOptions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { afterEach, beforeEach } from "vitest";
|
import { afterEach, beforeEach } from "vitest";
|
||||||
import { buildApp } from "../src/app";
|
import { buildApp } from "../src/app";
|
||||||
|
import { createRecordingQueue } from "../src/jobs/queue";
|
||||||
import type { Database, Db } from "../src/db/client";
|
import type { Database, Db } from "../src/db/client";
|
||||||
import { contractBoards, dispatches, pilots, players, ships } from "../src/db/schema";
|
import { contractBoards, dispatches, pilots, players, ships } from "../src/db/schema";
|
||||||
import { TEST_NOW } from "./db";
|
import { TEST_NOW } from "./db";
|
||||||
|
|
@ -35,7 +36,14 @@ export function useTestApp(database: () => Database): () => TestApp {
|
||||||
let current: TestApp | undefined;
|
let current: TestApp | undefined;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const clock = new TestClock();
|
const clock = new TestClock();
|
||||||
current = { app: buildApp(testConfig(), { database: database(), clock: clock.read }), clock };
|
current = {
|
||||||
|
app: buildApp(testConfig(), {
|
||||||
|
database: database(),
|
||||||
|
clock: clock.read,
|
||||||
|
jobs: createRecordingQueue(),
|
||||||
|
}),
|
||||||
|
clock,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
afterEach(() => current?.app.close());
|
afterEach(() => current?.app.close());
|
||||||
return () => current!;
|
return () => current!;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "@deadhead/schema";
|
} from "@deadhead/schema";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { buildApp } from "../src/app";
|
import { buildApp } from "../src/app";
|
||||||
|
import { createRecordingQueue } from "../src/jobs/queue";
|
||||||
import { useTestDatabase } from "./db";
|
import { useTestDatabase } from "./db";
|
||||||
import { testConfig } from "./helpers";
|
import { testConfig } from "./helpers";
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ function cookieFrom(setCookie: string | string[] | undefined): string {
|
||||||
describe("guest accounts", () => {
|
describe("guest accounts", () => {
|
||||||
let app: ReturnType<typeof buildApp>;
|
let app: ReturnType<typeof buildApp>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
app = buildApp(testConfig(), { database: database() });
|
app = buildApp(testConfig(), { database: database(), jobs: createRecordingQueue() });
|
||||||
return () => app.close();
|
return () => app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import { buildApp } from "../src/app";
|
import { buildApp } from "../src/app";
|
||||||
|
import { createRecordingQueue } from "../src/jobs/queue";
|
||||||
import { loadConfig } from "../src/config";
|
import { loadConfig } from "../src/config";
|
||||||
import { testConfig } from "./helpers";
|
import { testConfig } from "./helpers";
|
||||||
|
|
||||||
describe("server skeleton", () => {
|
describe("server skeleton", () => {
|
||||||
const app = buildApp(testConfig());
|
const app = buildApp(testConfig(), { jobs: createRecordingQueue() });
|
||||||
let baseUrl = "";
|
let baseUrl = "";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export function testConfig(overrides: Record<string, string> = {}): ServerConfig
|
||||||
TEST_DATABASE_URL: testDatabaseUrl,
|
TEST_DATABASE_URL: testDatabaseUrl,
|
||||||
BETTER_AUTH_SECRET: "test-secret-that-is-at-least-32-characters-long",
|
BETTER_AUTH_SECRET: "test-secret-that-is-at-least-32-characters-long",
|
||||||
BETTER_AUTH_URL: "http://localhost:3000",
|
BETTER_AUTH_URL: "http://localhost:3000",
|
||||||
|
WORKERS_ENABLED: "false",
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createPgBossQueue } from "../src/jobs/queue";
|
||||||
|
import { testConfig } from "./helpers";
|
||||||
|
|
||||||
|
/** Polls `check` until it returns true or the timeout passes. */
|
||||||
|
async function waitFor(check: () => Promise<boolean> | boolean, timeoutMs = 20_000) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (await check()) return;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
throw new Error("Timed out waiting");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("pg-boss job queue", () => {
|
||||||
|
it("runs an enqueued no-op job to completion", async () => {
|
||||||
|
const handled: unknown[] = [];
|
||||||
|
const queue = createPgBossQueue(testConfig().databaseUrl, {
|
||||||
|
workers: true,
|
||||||
|
jobs: [{ name: "test-noop", handler: async (data) => void handled.push(data) }],
|
||||||
|
});
|
||||||
|
await queue.start();
|
||||||
|
try {
|
||||||
|
const id = await queue.send("test-noop", { hello: "world" });
|
||||||
|
expect(id).not.toBeNull();
|
||||||
|
|
||||||
|
await waitFor(() => handled.length > 0);
|
||||||
|
expect(handled).toEqual([{ hello: "world" }]);
|
||||||
|
await waitFor(
|
||||||
|
async () => (await queue.boss.getJobById("test-noop", id!))?.state === "completed",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await queue.stop();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
|
it("only enqueues when workers are disabled", async () => {
|
||||||
|
const handled: unknown[] = [];
|
||||||
|
const queue = createPgBossQueue(testConfig().databaseUrl, {
|
||||||
|
workers: false,
|
||||||
|
jobs: [{ name: "test-idle", handler: async (data) => void handled.push(data) }],
|
||||||
|
});
|
||||||
|
await queue.start();
|
||||||
|
try {
|
||||||
|
const id = await queue.send("test-idle", {});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
expect(handled).toEqual([]);
|
||||||
|
expect((await queue.boss.getJobById("test-idle", id!))?.state).toBe("created");
|
||||||
|
} finally {
|
||||||
|
await queue.stop();
|
||||||
|
}
|
||||||
|
}, 30_000);
|
||||||
|
});
|
||||||
|
|
@ -3,10 +3,11 @@ import { serverTimeSchema } from "@deadhead/schema";
|
||||||
import { realToGame } from "@deadhead/sim";
|
import { realToGame } from "@deadhead/sim";
|
||||||
import { afterAll, describe, expect, it } from "vitest";
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
import { buildApp } from "../src/app";
|
import { buildApp } from "../src/app";
|
||||||
|
import { createRecordingQueue } from "../src/jobs/queue";
|
||||||
import { testConfig } from "./helpers";
|
import { testConfig } from "./helpers";
|
||||||
|
|
||||||
describe("server time endpoint", () => {
|
describe("server time endpoint", () => {
|
||||||
const app = buildApp(testConfig());
|
const app = buildApp(testConfig(), { jobs: createRecordingQueue() });
|
||||||
afterAll(() => app.close());
|
afterAll(() => app.close());
|
||||||
|
|
||||||
it("returns the current real time and the matching game time", async () => {
|
it("returns the current real time and the matching game time", async () => {
|
||||||
|
|
@ -19,7 +20,7 @@ describe("server time endpoint", () => {
|
||||||
|
|
||||||
it("reads time from the injected clock", async () => {
|
it("reads time from the injected clock", async () => {
|
||||||
const fixed = Date.parse("2100-06-01T12:00:00Z");
|
const fixed = Date.parse("2100-06-01T12:00:00Z");
|
||||||
const fixedApp = buildApp(testConfig(), { clock: () => fixed });
|
const fixedApp = buildApp(testConfig(), { clock: () => fixed, jobs: createRecordingQueue() });
|
||||||
const body = (await fixedApp.inject({ method: "GET", url: "/api/time" })).json();
|
const body = (await fixedApp.inject({ method: "GET", url: "/api/time" })).json();
|
||||||
expect(body).toEqual({ realTime: fixed, gameTime: realToGame(fixed, content.config) });
|
expect(body).toEqual({ realTime: fixed, gameTime: realToGame(fixed, content.config) });
|
||||||
await fixedApp.close();
|
await fixedApp.close();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { errorResponseSchema, routes } from "@deadhead/schema";
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { buildApp } from "../src/app";
|
import { buildApp } from "../src/app";
|
||||||
|
import { createRecordingQueue } from "../src/jobs/queue";
|
||||||
import { registerErrorHandler } from "../src/http/errors";
|
import { registerErrorHandler } from "../src/http/errors";
|
||||||
import { registerRoute } from "../src/http/route";
|
import { registerRoute } from "../src/http/route";
|
||||||
import { useTestDatabase } from "./db";
|
import { useTestDatabase } from "./db";
|
||||||
|
|
@ -12,7 +13,7 @@ const database = useTestDatabase();
|
||||||
describe("request validation from the route catalog", () => {
|
describe("request validation from the route catalog", () => {
|
||||||
let app: ReturnType<typeof buildApp>;
|
let app: ReturnType<typeof buildApp>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
app = buildApp(testConfig(), { database: database() });
|
app = buildApp(testConfig(), { database: database(), jobs: createRecordingQueue() });
|
||||||
});
|
});
|
||||||
afterEach(() => app.close());
|
afterEach(() => app.close());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,9 @@
|
||||||
- [x] **Step 6.12 — Spend skill points.** `POST /api/pilots/:pilotId/skills` (`spendSkillPoint`): +1 skill level, +1 point spent; `409 no-skill-points`, `409 skill-maxed`, `404 unknown-skill`.
|
- [x] **Step 6.12 — Spend skill points.** `POST /api/pilots/:pilotId/skills` (`spendSkillPoint`): +1 skill level, +1 point spent; `409 no-skill-points`, `409 skill-maxed`, `404 unknown-skill`.
|
||||||
- [x] **Step 6.13 — Acknowledge summaries.** `POST /api/summaries/acknowledge` (`src/game/summaries.ts`) sets `acknowledged_at` on the player's resolved, unacknowledged dispatches among the given ids and returns how many; resolved effects are untouched.
|
- [x] **Step 6.13 — Acknowledge summaries.** `POST /api/summaries/acknowledge` (`src/game/summaries.ts`) sets `acknowledged_at` on the player's resolved, unacknowledged dispatches among the given ids and returns how many; resolved effects are untouched.
|
||||||
- [x] **Step 6.14 — Concurrency and money safety.** Every authenticated endpoint runs in `withPlayer` (transaction + company row lock). Race tests: two and eight simultaneous purchases, a contract dispatched twice at once, a bay leased twice at once; three of them fail when the lock is removed.
|
- [x] **Step 6.14 — Concurrency and money safety.** Every authenticated endpoint runs in `withPlayer` (transaction + company row lock). Race tests: two and eight simultaneous purchases, a contract dispatched twice at once, a bay leased twice at once; three of them fail when the lock is removed.
|
||||||
|
|
||||||
|
## Phase 7 — Background Workers
|
||||||
|
|
||||||
|
- [x] **Step 7.1 — pg-boss setup.** `src/jobs/queue.ts`: `JobQueue` interface, pg-boss implementation (queues always created; workers and cron schedules only when `WORKERS_ENABLED=true`), and a recording fake for tests. The app starts the queue on ready and stops it on close. Test runs a no-op job to completion with real pg-boss.
|
||||||
|
- [ ] Step 7.2 — Arrival resolution job
|
||||||
|
- [ ] Step 7.3 — Expired contract cleanup
|
||||||
|
|
|
||||||
145
pnpm-lock.yaml
145
pnpm-lock.yaml
|
|
@ -37,7 +37,7 @@ importers:
|
||||||
version: 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
version: 8.71.0(eslint@10.12.0)(typescript@6.0.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
||||||
|
|
||||||
apps/server:
|
apps/server:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -52,16 +52,19 @@ importers:
|
||||||
version: link:../../packages/sim
|
version: link:../../packages/sim
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: ^1.7.7
|
specifier: ^1.7.7
|
||||||
version: 1.7.7(drizzle-kit@0.31.11)(drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))(pg@8.23.1)(vitest@5.0.3)
|
version: 1.7.7(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))(pg@8.23.1)(vitest@5.0.3)
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.3
|
specifier: ^0.45.3
|
||||||
version: 0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1)
|
version: 0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1)
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.12.5
|
specifier: ^5.12.5
|
||||||
version: 5.12.5
|
version: 5.12.5
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.23.1
|
specifier: ^8.23.1
|
||||||
version: 8.23.1
|
version: 8.23.1
|
||||||
|
pg-boss:
|
||||||
|
specifier: ^12.36.0
|
||||||
|
version: 12.36.0(@opentelemetry/api@1.9.1)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.6.5
|
specifier: ^4.6.5
|
||||||
version: 4.6.5
|
version: 4.6.5
|
||||||
|
|
@ -769,6 +772,10 @@ packages:
|
||||||
resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
|
resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.1':
|
||||||
|
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.43.0':
|
'@opentelemetry/semantic-conventions@1.43.0':
|
||||||
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
|
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
@ -1125,6 +1132,10 @@ packages:
|
||||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
cron-parser@5.10.1:
|
||||||
|
resolution: {integrity: sha512-pKRrRagItwk9rGIStcUyMxFX34x56zoX3KDf9HJ4ttwkXIK1qxK63EvXvEDmlxI9AdPJ+Y7TEKRftRlW5eSS7A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -1547,6 +1558,10 @@ packages:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
luxon@3.7.2:
|
||||||
|
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
magic-string@1.4.2:
|
magic-string@1.4.2:
|
||||||
resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==}
|
resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==}
|
||||||
|
|
||||||
|
|
@ -1572,6 +1587,10 @@ packages:
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
non-error@0.1.0:
|
||||||
|
resolution: {integrity: sha512-TMB1uHiGsHRGv1uYclfhivcnf0/PdFp2pNqRxXjncaAsjYMoisaQJI+SSZCqRq+VliwRTC8tsMQfmrWjDMhkPQ==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
obug@2.2.1:
|
obug@2.2.1:
|
||||||
resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
|
resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
@ -1600,6 +1619,13 @@ packages:
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
pg-boss@12.36.0:
|
||||||
|
resolution: {integrity: sha512-67Jj4xNuv8mkShTzfmsFtJeVa2ChXyrknjlw/WKZCIMuzRTwyHOUGjl2mf5Snrhmb0xglvFbkfAt5+hLtXukag==}
|
||||||
|
engines: {node: '>=22.12.0'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
|
||||||
pg-cloudflare@1.4.1:
|
pg-cloudflare@1.4.1:
|
||||||
resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==}
|
resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==}
|
||||||
|
|
||||||
|
|
@ -1726,6 +1752,9 @@ packages:
|
||||||
rou3@0.9.2:
|
rou3@0.9.2:
|
||||||
resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==}
|
resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==}
|
||||||
|
|
||||||
|
rrule-temporal@2.2.8:
|
||||||
|
resolution: {integrity: sha512-+wWUe0J02oMh1UWKIt1RTu2H0Cz+gP11O/iweK0MN2UP6JLGeepTUkbSBa90Hxq0W+ywSxM9IlJNouIUdMOwhg==}
|
||||||
|
|
||||||
safe-regex2@5.1.1:
|
safe-regex2@5.1.1:
|
||||||
resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==}
|
resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -1742,6 +1771,10 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
serialize-error@13.0.2:
|
||||||
|
resolution: {integrity: sha512-G+YKwp9I6IvWMe5RlwqxdmTPIcq/G8HK/sU51gdV1XAJznYkRSTB0ph+zB9AfSxG0fx6xrqYA6i9HeIcHFTXow==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
set-cookie-parser@2.7.2:
|
set-cookie-parser@2.7.2:
|
||||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||||
|
|
||||||
|
|
@ -1777,6 +1810,13 @@ packages:
|
||||||
std-env@4.3.0:
|
std-env@4.3.0:
|
||||||
resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
|
resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0:
|
||||||
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
temporal-spec@1.0.1:
|
||||||
|
resolution: {integrity: sha512-wxVoanmDeavXie1vu2JaQ3WIc3JZnWAOYFBsJyATaVsXsycKYUflGsyBmrRSnoCpZJpwPyr38VpgSUlQ8CbFxg==}
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
thread-stream@4.2.0:
|
||||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
@ -1816,6 +1856,10 @@ packages:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
type-fest@5.10.0:
|
||||||
|
resolution: {integrity: sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
typescript-eslint@8.71.0:
|
typescript-eslint@8.71.0:
|
||||||
resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
|
resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -1960,7 +2004,7 @@ snapshots:
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)':
|
'@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.2
|
'@better-fetch/fetch': 1.3.2
|
||||||
|
|
@ -1971,39 +2015,41 @@ snapshots:
|
||||||
kysely: 0.29.6
|
kysely: 0.29.6
|
||||||
nanostores: 1.5.4
|
nanostores: 1.5.4
|
||||||
zod: 4.6.5
|
zod: 4.6.5
|
||||||
|
optionalDependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
|
||||||
'@better-auth/drizzle-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))':
|
'@better-auth/drizzle-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
drizzle-orm: 0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1)
|
drizzle-orm: 0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1)
|
||||||
|
|
||||||
'@better-auth/kysely-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(kysely@0.29.6)':
|
'@better-auth/kysely-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(kysely@0.29.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
kysely: 0.29.6
|
kysely: 0.29.6
|
||||||
|
|
||||||
'@better-auth/memory-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)':
|
'@better-auth/memory-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
|
|
||||||
'@better-auth/mongo-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)':
|
'@better-auth/mongo-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
|
|
||||||
'@better-auth/prisma-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)':
|
'@better-auth/prisma-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
|
|
||||||
'@better-auth/telemetry@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)':
|
'@better-auth/telemetry@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.2
|
'@better-fetch/fetch': 1.3.2
|
||||||
|
|
||||||
|
|
@ -2357,6 +2403,8 @@ snapshots:
|
||||||
|
|
||||||
'@noble/hashes@2.4.0': {}
|
'@noble/hashes@2.4.0': {}
|
||||||
|
|
||||||
|
'@opentelemetry/api@1.9.1': {}
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.152.0': {}
|
'@oxc-project/types@0.152.0': {}
|
||||||
|
|
@ -2536,7 +2584,7 @@ snapshots:
|
||||||
obug: 2.2.1
|
obug: 2.2.1
|
||||||
std-env: 4.3.0
|
std-env: 4.3.0
|
||||||
tinyrainbow: 3.2.0
|
tinyrainbow: 3.2.0
|
||||||
vitest: 5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
||||||
|
|
||||||
'@vitest/istanbul-lib-coverage@1.0.2': {}
|
'@vitest/istanbul-lib-coverage@1.0.2': {}
|
||||||
|
|
||||||
|
|
@ -2600,15 +2648,15 @@ snapshots:
|
||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
better-auth@1.7.7(drizzle-kit@0.31.11)(drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))(pg@8.23.1)(vitest@5.0.3):
|
better-auth@1.7.7(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))(pg@8.23.1)(vitest@5.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
'@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4)
|
||||||
'@better-auth/drizzle-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))
|
'@better-auth/drizzle-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1))
|
||||||
'@better-auth/kysely-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(kysely@0.29.6)
|
'@better-auth/kysely-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(kysely@0.29.6)
|
||||||
'@better-auth/memory-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)
|
'@better-auth/memory-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)
|
||||||
'@better-auth/mongo-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)
|
'@better-auth/mongo-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)
|
||||||
'@better-auth/prisma-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)
|
'@better-auth/prisma-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)
|
||||||
'@better-auth/telemetry': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)
|
'@better-auth/telemetry': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.2
|
'@better-fetch/fetch': 1.3.2
|
||||||
'@noble/ciphers': 2.4.0
|
'@noble/ciphers': 2.4.0
|
||||||
|
|
@ -2621,9 +2669,9 @@ snapshots:
|
||||||
zod: 4.6.5
|
zod: 4.6.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
drizzle-kit: 0.31.11
|
drizzle-kit: 0.31.11
|
||||||
drizzle-orm: 0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1)
|
drizzle-orm: 0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1)
|
||||||
pg: 8.23.1
|
pg: 8.23.1
|
||||||
vitest: 5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@opentelemetry/api'
|
- '@opentelemetry/api'
|
||||||
|
|
||||||
|
|
@ -2654,6 +2702,10 @@ snapshots:
|
||||||
|
|
||||||
cookie@1.1.1: {}
|
cookie@1.1.1: {}
|
||||||
|
|
||||||
|
cron-parser@5.10.1:
|
||||||
|
dependencies:
|
||||||
|
luxon: 3.7.2
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|
@ -2679,8 +2731,9 @@ snapshots:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
tsx: 4.23.15
|
tsx: 4.23.15
|
||||||
|
|
||||||
drizzle-orm@0.45.3(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1):
|
drizzle-orm@0.45.3(@opentelemetry/api@1.9.1)(@types/pg@8.23.1)(kysely@0.29.6)(pg@8.23.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
'@types/pg': 8.23.1
|
'@types/pg': 8.23.1
|
||||||
kysely: 0.29.6
|
kysely: 0.29.6
|
||||||
pg: 8.23.1
|
pg: 8.23.1
|
||||||
|
|
@ -3041,6 +3094,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
||||||
|
luxon@3.7.2: {}
|
||||||
|
|
||||||
magic-string@1.4.2:
|
magic-string@1.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@jridgewell/sourcemap-codec': 1.6.0
|
||||||
|
|
@ -3063,6 +3118,8 @@ snapshots:
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
non-error@0.1.0: {}
|
||||||
|
|
||||||
obug@2.2.1: {}
|
obug@2.2.1: {}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
@ -3088,6 +3145,16 @@ snapshots:
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
pg-boss@12.36.0(@opentelemetry/api@1.9.1):
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
cron-parser: 5.10.1
|
||||||
|
pg: 8.23.1
|
||||||
|
rrule-temporal: 2.2.8
|
||||||
|
serialize-error: 13.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- pg-native
|
||||||
|
|
||||||
pg-cloudflare@1.4.1:
|
pg-cloudflare@1.4.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -3214,6 +3281,10 @@ snapshots:
|
||||||
|
|
||||||
rou3@0.9.2: {}
|
rou3@0.9.2: {}
|
||||||
|
|
||||||
|
rrule-temporal@2.2.8:
|
||||||
|
dependencies:
|
||||||
|
temporal-spec: 1.0.1
|
||||||
|
|
||||||
safe-regex2@5.1.1:
|
safe-regex2@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ret: 0.5.0
|
ret: 0.5.0
|
||||||
|
|
@ -3224,6 +3295,11 @@ snapshots:
|
||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5: {}
|
||||||
|
|
||||||
|
serialize-error@13.0.2:
|
||||||
|
dependencies:
|
||||||
|
non-error: 0.1.0
|
||||||
|
type-fest: 5.10.0
|
||||||
|
|
||||||
set-cookie-parser@2.7.2: {}
|
set-cookie-parser@2.7.2: {}
|
||||||
|
|
||||||
set-cookie-parser@3.1.2: {}
|
set-cookie-parser@3.1.2: {}
|
||||||
|
|
@ -3251,6 +3327,10 @@ snapshots:
|
||||||
|
|
||||||
std-env@4.3.0: {}
|
std-env@4.3.0: {}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
|
temporal-spec@1.0.1: {}
|
||||||
|
|
||||||
thread-stream@4.2.0:
|
thread-stream@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
real-require: 1.0.0
|
real-require: 1.0.0
|
||||||
|
|
@ -3282,6 +3362,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
|
type-fest@5.10.0:
|
||||||
|
dependencies:
|
||||||
|
tagged-tag: 1.0.0
|
||||||
|
|
||||||
typescript-eslint@8.71.0(eslint@10.12.0)(typescript@6.0.3):
|
typescript-eslint@8.71.0(eslint@10.12.0)(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3)
|
'@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0)(typescript@6.0.3))(eslint@10.12.0)(typescript@6.0.3)
|
||||||
|
|
@ -3314,7 +3398,7 @@ snapshots:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
tsx: 4.23.15
|
tsx: 4.23.15
|
||||||
|
|
||||||
vitest@5.0.3(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15)):
|
vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/mocker': 5.0.3(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
'@vitest/mocker': 5.0.3(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15))
|
||||||
|
|
@ -3331,6 +3415,7 @@ snapshots:
|
||||||
vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15)
|
vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(tsx@4.23.15)
|
||||||
why-is-node-running: 3.2.1
|
why-is-node-running: 3.2.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
'@types/node': 24.19.1
|
'@types/node': 24.19.1
|
||||||
'@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
|
'@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue