refactor(worker): modularize worker entrypoint and add vitest infra
move worker startup logic from server.ts to runtime.ts for better modularity add worker-loop-policy.ts for loop control abstraction delete legacy unit tests under tests/unit/ related to compute worker and control-plane add vitest config and test directories for compute-core and compute-worker add vitest workflow for CI update package.json scripts for new test commands and add vitest as dev dependency BREAKING CHANGE: worker entrypoint is now compute/worker/src/runtime.ts instead of server.ts; legacy Playwright-based unit tests for compute worker are removed in favor of Vitest
This commit is contained in:
parent
b0a4bccf4c
commit
e1483ab80a
22 changed files with 2701 additions and 1700 deletions
23
.github/workflows/vitest.yml
vendored
Normal file
23
.github/workflows/vitest.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name: Vitest Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
vitest-testing:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: lts/*
|
||||
package-manager-cache: false
|
||||
- uses: pnpm/action-setup@v6
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Vitest suites
|
||||
run: pnpm test:vitest
|
||||
95
compute/core/tests/control-plane/orchestrator.test.ts
Normal file
95
compute/core/tests/control-plane/orchestrator.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import type { WorkerOperationRequest } from '../../src/api-contracts';
|
||||
import {
|
||||
InMemoryOperationEventStream,
|
||||
InMemoryOperationQueue,
|
||||
InMemoryOperationStateStore,
|
||||
OperationOrchestrator,
|
||||
} from '../../src/control-plane';
|
||||
|
||||
function buildRequest(opKey: string): WorkerOperationRequest {
|
||||
return {
|
||||
kind: 'pdf_layout',
|
||||
opKey,
|
||||
payload: {
|
||||
documentId: `doc-${opKey}`,
|
||||
namespace: null,
|
||||
documentObjectKey: `s3://bucket/${opKey}.pdf`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('operation orchestrator', () => {
|
||||
test('reuses fresh operation and replaces stale operation', async () => {
|
||||
let now = 1_000;
|
||||
let nextId = 1;
|
||||
const queue = new InMemoryOperationQueue();
|
||||
const stateStore = new InMemoryOperationStateStore();
|
||||
const eventStream = new InMemoryOperationEventStream();
|
||||
|
||||
const orchestrator = new OperationOrchestrator({
|
||||
queue,
|
||||
stateStore,
|
||||
eventStream,
|
||||
config: { opStaleMs: 2_000, maxCasRetries: 5 },
|
||||
clock: { now: () => now },
|
||||
idFactory: {
|
||||
opId: () => `op-${nextId}`,
|
||||
jobId: () => `job-${nextId++}`,
|
||||
},
|
||||
});
|
||||
|
||||
const request = buildRequest('shared-op');
|
||||
|
||||
const first = await orchestrator.enqueueOrReuse(request);
|
||||
expect(first.opId).toBe('op-1');
|
||||
|
||||
now = 2_000;
|
||||
const reused = await orchestrator.enqueueOrReuse(request);
|
||||
expect(reused.opId).toBe('op-1');
|
||||
|
||||
await orchestrator.markRunning({ opId: first.opId, updatedAt: 2_100 });
|
||||
|
||||
now = 6_000;
|
||||
const replaced = await orchestrator.enqueueOrReuse(request);
|
||||
expect(replaced.opId).toBe('op-2');
|
||||
expect(await stateStore.getOpIndex('shared-op')).toEqual({ opId: 'op-2' });
|
||||
expect(queue.size('pdf_layout')).toBe(2);
|
||||
});
|
||||
|
||||
test('survives transient CAS conflict and eventually creates operation', async () => {
|
||||
const queue = new InMemoryOperationQueue();
|
||||
const eventStream = new InMemoryOperationEventStream();
|
||||
const store = new InMemoryOperationStateStore();
|
||||
|
||||
let firstAttempt = true;
|
||||
const conflictStore = {
|
||||
getOpState: store.getOpState.bind(store),
|
||||
putOpState: store.putOpState.bind(store),
|
||||
getOpIndex: store.getOpIndex.bind(store),
|
||||
compareAndSetOpIndex: async (input: { opKey: string; newOpId: string; expectedOpId: string | null }) => {
|
||||
if (firstAttempt && input.expectedOpId === null) {
|
||||
firstAttempt = false;
|
||||
return false;
|
||||
}
|
||||
return store.compareAndSetOpIndex(input);
|
||||
},
|
||||
};
|
||||
|
||||
let id = 1;
|
||||
const orchestrator = new OperationOrchestrator({
|
||||
queue,
|
||||
stateStore: conflictStore,
|
||||
eventStream,
|
||||
config: { opStaleMs: 2_000, maxCasRetries: 4 },
|
||||
idFactory: {
|
||||
opId: () => `op-${id}`,
|
||||
jobId: () => `job-${id++}`,
|
||||
},
|
||||
});
|
||||
|
||||
const created = await orchestrator.enqueueOrReuse(buildRequest('cas-key'));
|
||||
expect(created.opId).toMatch(/^op-/);
|
||||
expect(await store.getOpIndex('cas-key')).toEqual({ opId: created.opId });
|
||||
});
|
||||
});
|
||||
26
compute/core/tests/control-plane/sse.test.ts
Normal file
26
compute/core/tests/control-plane/sse.test.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import { encodeSseFrame, parseSseEventId, parseSsePayload } from '../../src/control-plane/sse';
|
||||
|
||||
describe('sse codec', () => {
|
||||
test('encodes event id and payload and decodes both reliably', () => {
|
||||
const frame = encodeSseFrame({
|
||||
id: 42,
|
||||
event: 'snapshot',
|
||||
data: { ok: true, opId: 'op-1' },
|
||||
});
|
||||
|
||||
expect(frame).toContain('event: snapshot');
|
||||
expect(parseSseEventId(frame)).toBe(42);
|
||||
expect(parseSsePayload(frame)).toBe('{"ok":true,"opId":"op-1"}');
|
||||
});
|
||||
|
||||
test('supports multiline data payload', () => {
|
||||
const frame = encodeSseFrame({
|
||||
id: 5,
|
||||
data: 'line1\nline2',
|
||||
});
|
||||
|
||||
expect(parseSseEventId(frame)).toBe(5);
|
||||
expect(parseSsePayload(frame)).toBe('line1\nline2');
|
||||
});
|
||||
});
|
||||
76
compute/core/tests/control-plane/state-machine.test.ts
Normal file
76
compute/core/tests/control-plane/state-machine.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import type { WorkerOperationState } from '../../src/api-contracts';
|
||||
import {
|
||||
explainReplacementReason,
|
||||
isInflightStatus,
|
||||
isTerminalStatus,
|
||||
shouldReuseExistingOperation,
|
||||
} from '../../src/control-plane/state-machine';
|
||||
|
||||
function runningState(overrides: Partial<WorkerOperationState> = {}): WorkerOperationState {
|
||||
return {
|
||||
opId: 'op-1',
|
||||
opKey: 'key-1',
|
||||
kind: 'pdf_layout',
|
||||
jobId: 'job-1',
|
||||
status: 'running',
|
||||
queuedAt: 1_000,
|
||||
updatedAt: 2_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('state-machine decisions', () => {
|
||||
test('identifies terminal and inflight states', () => {
|
||||
expect(isTerminalStatus('succeeded')).toBe(true);
|
||||
expect(isTerminalStatus('failed')).toBe(true);
|
||||
expect(isTerminalStatus('queued')).toBe(false);
|
||||
|
||||
expect(isInflightStatus('queued')).toBe(true);
|
||||
expect(isInflightStatus('running')).toBe(true);
|
||||
expect(isInflightStatus('failed')).toBe(false);
|
||||
});
|
||||
|
||||
test('reuses fresh inflight operation and rejects stale inflight operation', () => {
|
||||
const current = runningState();
|
||||
|
||||
expect(shouldReuseExistingOperation({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 2_900,
|
||||
opStaleMs: 1_000,
|
||||
})).toBe(true);
|
||||
|
||||
expect(shouldReuseExistingOperation({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 3_100,
|
||||
opStaleMs: 1_000,
|
||||
})).toBe(false);
|
||||
|
||||
expect(explainReplacementReason({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 3_100,
|
||||
opStaleMs: 1_000,
|
||||
})).toBe('stale_running');
|
||||
});
|
||||
|
||||
test('never reuses kind-mismatched operation', () => {
|
||||
const current = runningState({ kind: 'whisper_align' });
|
||||
|
||||
expect(shouldReuseExistingOperation({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 2_100,
|
||||
opStaleMs: 10_000,
|
||||
})).toBe(false);
|
||||
|
||||
expect(explainReplacementReason({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 2_100,
|
||||
opStaleMs: 10_000,
|
||||
})).toBe('kind_mismatch');
|
||||
});
|
||||
});
|
||||
1428
compute/worker/src/runtime.ts
Normal file
1428
compute/worker/src/runtime.ts
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
22
compute/worker/src/worker-loop-policy.ts
Normal file
22
compute/worker/src/worker-loop-policy.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { WorkerOperationKind } from '@openreader/compute-core/api-contracts';
|
||||
|
||||
export type RetryAction = 'nak_retry' | 'term_fail';
|
||||
|
||||
export function buildQueueWaitTiming(queuedAt: number, now: number): { queueWaitMs: number } | undefined {
|
||||
if (!Number.isFinite(queuedAt) || !Number.isFinite(now)) return undefined;
|
||||
return { queueWaitMs: Math.max(0, Math.floor(now - queuedAt)) };
|
||||
}
|
||||
|
||||
export function decideRetryAction(input: {
|
||||
kind: WorkerOperationKind;
|
||||
deliveryCount: number;
|
||||
pdfAttempts: number;
|
||||
whisperMaxDeliver?: number;
|
||||
}): RetryAction {
|
||||
const whisperMaxDeliver = input.whisperMaxDeliver ?? 1;
|
||||
if (input.kind === 'whisper_align') {
|
||||
return input.deliveryCount < whisperMaxDeliver ? 'nak_retry' : 'term_fail';
|
||||
}
|
||||
|
||||
return input.deliveryCount < input.pdfAttempts ? 'nak_retry' : 'term_fail';
|
||||
}
|
||||
122
compute/worker/tests/api/routes.test.ts
Normal file
122
compute/worker/tests/api/routes.test.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||
import { createComputeWorkerApp } from '../../src/runtime';
|
||||
import { FakeControlPlane } from '../fixtures/fake-control-plane';
|
||||
|
||||
const AUTH = { authorization: 'Bearer test-token' };
|
||||
|
||||
describe('compute worker API routes', () => {
|
||||
let fake: FakeControlPlane;
|
||||
let runtime: Awaited<ReturnType<typeof createComputeWorkerApp>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
fake = new FakeControlPlane();
|
||||
runtime = await createComputeWorkerApp({
|
||||
workerToken: 'test-token',
|
||||
disableWorkers: true,
|
||||
routeDeps: fake.deps,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await runtime.close();
|
||||
});
|
||||
|
||||
test('allows unauthenticated health checks but protects operation routes', async () => {
|
||||
const live = await runtime.app.inject({ method: 'GET', url: '/health/live' });
|
||||
expect(live.statusCode).toBe(200);
|
||||
|
||||
const protectedRoute = await runtime.app.inject({ method: 'GET', url: '/ops/op-1' });
|
||||
expect(protectedRoute.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('validates operation creation body and returns 400 for invalid payload', async () => {
|
||||
const response = await runtime.app.inject({
|
||||
method: 'POST',
|
||||
url: '/ops',
|
||||
headers: AUTH,
|
||||
payload: {
|
||||
kind: 'pdf_layout',
|
||||
opKey: '',
|
||||
payload: {
|
||||
documentId: 'd1',
|
||||
namespace: null,
|
||||
documentObjectKey: 's3://bucket/doc.pdf',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json()).toMatchObject({ error: 'Invalid request body' });
|
||||
});
|
||||
|
||||
test('creates operation and fetches state by op id', async () => {
|
||||
const create = await runtime.app.inject({
|
||||
method: 'POST',
|
||||
url: '/ops',
|
||||
headers: AUTH,
|
||||
payload: {
|
||||
kind: 'pdf_layout',
|
||||
opKey: 'doc-1:layout',
|
||||
payload: {
|
||||
documentId: 'doc-1',
|
||||
namespace: null,
|
||||
documentObjectKey: 'openreader/doc-1.pdf',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(create.statusCode).toBe(202);
|
||||
const created = create.json();
|
||||
expect(created).toMatchObject({ kind: 'pdf_layout', status: 'queued' });
|
||||
|
||||
const fetch = await runtime.app.inject({
|
||||
method: 'GET',
|
||||
url: `/ops/${created.opId}`,
|
||||
headers: AUTH,
|
||||
});
|
||||
|
||||
expect(fetch.statusCode).toBe(200);
|
||||
expect(fetch.json()).toMatchObject({ opId: created.opId, status: 'queued' });
|
||||
});
|
||||
|
||||
test('returns not found for unknown operation and event stream lookups', async () => {
|
||||
const opResponse = await runtime.app.inject({
|
||||
method: 'GET',
|
||||
url: '/ops/missing',
|
||||
headers: AUTH,
|
||||
});
|
||||
expect(opResponse.statusCode).toBe(404);
|
||||
|
||||
const eventsResponse = await runtime.app.inject({
|
||||
method: 'GET',
|
||||
url: '/ops/missing/events',
|
||||
headers: AUTH,
|
||||
});
|
||||
expect(eventsResponse.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('streams initial SSE snapshot for terminal operation and honors cursor id', async () => {
|
||||
fake.seedState({
|
||||
opId: 'op-terminal',
|
||||
opKey: 'k-terminal',
|
||||
kind: 'pdf_layout',
|
||||
jobId: 'job-op-terminal',
|
||||
status: 'succeeded',
|
||||
queuedAt: 1000,
|
||||
updatedAt: 2000,
|
||||
result: { parsedObjectKey: 'openreader/parsed.json' },
|
||||
});
|
||||
|
||||
const stream = await runtime.app.inject({
|
||||
method: 'GET',
|
||||
url: '/ops/op-terminal/events?sinceEventId=7',
|
||||
headers: AUTH,
|
||||
});
|
||||
|
||||
expect(stream.statusCode).toBe(200);
|
||||
expect(stream.headers['content-type']).toContain('text/event-stream');
|
||||
expect(stream.body).toContain('event: snapshot');
|
||||
expect(stream.body).toContain('id: 7');
|
||||
expect(stream.body).toContain('"status":"succeeded"');
|
||||
});
|
||||
});
|
||||
119
compute/worker/tests/fixtures/fake-control-plane.ts
vendored
Normal file
119
compute/worker/tests/fixtures/fake-control-plane.ts
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import type {
|
||||
PdfLayoutJobResult,
|
||||
WhisperAlignJobResult,
|
||||
WorkerOperationEvent,
|
||||
WorkerOperationRequest,
|
||||
WorkerOperationState,
|
||||
} from '@openreader/compute-core/api-contracts';
|
||||
import type { ComputeWorkerRouteDeps } from '../../src/runtime';
|
||||
|
||||
type ComputeResult = WhisperAlignJobResult | PdfLayoutJobResult;
|
||||
type ComputeState = WorkerOperationState<ComputeResult>;
|
||||
type ComputeEvent = WorkerOperationEvent<ComputeResult>;
|
||||
|
||||
export class FakeControlPlane {
|
||||
private readonly stateByOpId = new Map<string, ComputeState>();
|
||||
private readonly opIdByOpKey = new Map<string, string>();
|
||||
private readonly eventsByOpId = new Map<string, ComputeEvent[]>();
|
||||
private nextOpId = 1;
|
||||
|
||||
readonly deps: ComputeWorkerRouteDeps = {
|
||||
orchestrator: {
|
||||
enqueueOrReuse: async (request) => this.enqueueOrReuse(request),
|
||||
markRunning: async (input) => this.updateState(input.opId, {
|
||||
status: 'running',
|
||||
startedAt: input.startedAt,
|
||||
updatedAt: input.updatedAt,
|
||||
timing: input.timing,
|
||||
}),
|
||||
markProgress: async (input) => this.updateState(input.opId, {
|
||||
status: 'running',
|
||||
progress: input.progress,
|
||||
updatedAt: input.updatedAt,
|
||||
timing: input.timing,
|
||||
}),
|
||||
markSucceeded: async (input) => this.updateState(input.opId, {
|
||||
status: 'succeeded',
|
||||
result: input.result as ComputeResult,
|
||||
updatedAt: input.updatedAt,
|
||||
timing: input.timing,
|
||||
}),
|
||||
markFailed: async (input) => this.updateState(input.opId, {
|
||||
status: 'failed',
|
||||
error: typeof input.error === 'string' ? { message: input.error } : input.error,
|
||||
updatedAt: input.updatedAt,
|
||||
timing: input.timing,
|
||||
}),
|
||||
},
|
||||
operationStateStore: {
|
||||
getOpState: async (opId) => this.stateByOpId.get(opId) ?? null,
|
||||
},
|
||||
operationEventStream: {
|
||||
subscribe: async ({ opId, sinceEventId, onEvent }) => {
|
||||
const since = Math.max(0, Math.floor(sinceEventId ?? 0));
|
||||
const replay = (this.eventsByOpId.get(opId) ?? []).filter((event) => event.eventId > since);
|
||||
for (const event of replay) {
|
||||
await onEvent(event);
|
||||
}
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
seedState(state: ComputeState): void {
|
||||
this.stateByOpId.set(state.opId, state);
|
||||
this.opIdByOpKey.set(state.opKey, state.opId);
|
||||
}
|
||||
|
||||
seedEvent(opId: string, event: ComputeEvent): void {
|
||||
const list = this.eventsByOpId.get(opId) ?? [];
|
||||
list.push(event);
|
||||
this.eventsByOpId.set(opId, list);
|
||||
}
|
||||
|
||||
private async enqueueOrReuse(request: WorkerOperationRequest): Promise<ComputeState> {
|
||||
const existingId = this.opIdByOpKey.get(request.opKey);
|
||||
if (existingId) {
|
||||
const existing = this.stateByOpId.get(existingId);
|
||||
if (existing) return existing;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const opId = `op-${this.nextOpId++}`;
|
||||
const state: ComputeState = {
|
||||
opId,
|
||||
opKey: request.opKey,
|
||||
kind: request.kind,
|
||||
jobId: `job-${opId}`,
|
||||
status: 'queued',
|
||||
queuedAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.stateByOpId.set(opId, state);
|
||||
this.opIdByOpKey.set(request.opKey, opId);
|
||||
this.seedEvent(opId, { eventId: 1, snapshot: state });
|
||||
return state;
|
||||
}
|
||||
|
||||
private async updateState(
|
||||
opId: string,
|
||||
patch: Partial<ComputeState>,
|
||||
): Promise<ComputeState> {
|
||||
const current = this.stateByOpId.get(opId);
|
||||
if (!current) {
|
||||
throw new Error(`Unknown opId: ${opId}`);
|
||||
}
|
||||
const next: ComputeState = {
|
||||
...current,
|
||||
...patch,
|
||||
updatedAt: patch.updatedAt ?? Date.now(),
|
||||
};
|
||||
this.stateByOpId.set(opId, next);
|
||||
const currentEvents = this.eventsByOpId.get(opId) ?? [];
|
||||
const nextEventId = (currentEvents.at(-1)?.eventId ?? 0) + 1;
|
||||
currentEvents.push({ eventId: nextEventId, snapshot: next });
|
||||
this.eventsByOpId.set(opId, currentEvents);
|
||||
return next;
|
||||
}
|
||||
}
|
||||
8
compute/worker/tests/setup-env.ts
Normal file
8
compute/worker/tests/setup-env.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
process.env.COMPUTE_WORKER_TOKEN = process.env.COMPUTE_WORKER_TOKEN || 'test-token';
|
||||
process.env.NATS_URL = process.env.NATS_URL || 'nats://127.0.0.1:4222';
|
||||
process.env.COMPUTE_PREWARM_MODELS = 'false';
|
||||
process.env.S3_BUCKET = process.env.S3_BUCKET || 'test-bucket';
|
||||
process.env.S3_REGION = process.env.S3_REGION || 'us-east-1';
|
||||
process.env.S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID || 'test';
|
||||
process.env.S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY || 'test';
|
||||
process.env.S3_PREFIX = process.env.S3_PREFIX || 'openreader';
|
||||
9
compute/worker/tests/tsconfig.json
Normal file
9
compute/worker/tests/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["node", "vitest/globals"]
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": []
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import { OperationOrchestrator } from '../../compute/core/src/control-plane';
|
||||
import type { WorkerOperationRequest } from '../../compute/core/src/api-contracts';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { OperationOrchestrator } from '@openreader/compute-core/control-plane';
|
||||
import type { WorkerOperationRequest } from '@openreader/compute-core/api-contracts';
|
||||
import {
|
||||
JetStreamOperationEventStream,
|
||||
JetStreamOperationQueue,
|
||||
JetStreamOperationStateStore,
|
||||
hashOpKey,
|
||||
opEventsSubject,
|
||||
opIndexKvKey,
|
||||
opStateKvKey,
|
||||
type KvEntryLike,
|
||||
type KvStoreLike,
|
||||
} from '../../compute/worker/src/control-plane/jetstream';
|
||||
} from '../../src/control-plane/jetstream';
|
||||
|
||||
class FakeKvStore implements KvStoreLike {
|
||||
private readonly data = new Map<string, KvEntryLike>();
|
||||
|
|
@ -18,46 +19,31 @@ class FakeKvStore implements KvStoreLike {
|
|||
|
||||
async get(key: string): Promise<KvEntryLike | null> {
|
||||
const value = this.data.get(key);
|
||||
if (!value) return null;
|
||||
return {
|
||||
operation: value.operation,
|
||||
value: value.value.slice(),
|
||||
revision: value.revision,
|
||||
};
|
||||
return value
|
||||
? {
|
||||
operation: value.operation,
|
||||
value: value.value.slice(),
|
||||
revision: value.revision,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
async put(key: string, data: Uint8Array): Promise<void> {
|
||||
this.revision += 1;
|
||||
this.data.set(key, {
|
||||
operation: 'PUT',
|
||||
value: data.slice(),
|
||||
revision: this.revision,
|
||||
});
|
||||
this.data.set(key, { operation: 'PUT', value: data.slice(), revision: this.revision });
|
||||
}
|
||||
|
||||
async create(key: string, data: Uint8Array): Promise<void> {
|
||||
if (this.data.has(key)) {
|
||||
throw new Error('key exists');
|
||||
}
|
||||
if (this.data.has(key)) throw new Error('key exists');
|
||||
this.revision += 1;
|
||||
this.data.set(key, {
|
||||
operation: 'PUT',
|
||||
value: data.slice(),
|
||||
revision: this.revision,
|
||||
});
|
||||
this.data.set(key, { operation: 'PUT', value: data.slice(), revision: this.revision });
|
||||
}
|
||||
|
||||
async update(key: string, data: Uint8Array, version: number): Promise<void> {
|
||||
const current = this.data.get(key);
|
||||
if (!current || current.revision !== version) {
|
||||
throw new Error('wrong last sequence');
|
||||
}
|
||||
if (!current || current.revision !== version) throw new Error('wrong last sequence');
|
||||
this.revision += 1;
|
||||
this.data.set(key, {
|
||||
operation: 'PUT',
|
||||
value: data.slice(),
|
||||
revision: this.revision,
|
||||
});
|
||||
this.data.set(key, { operation: 'PUT', value: data.slice(), revision: this.revision });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +58,7 @@ class FakeJetStream {
|
|||
|
||||
async publish(subject: string, data: Uint8Array): Promise<{ seq: number; stream: string; duplicate: boolean }> {
|
||||
this.seq += 1;
|
||||
const payload = JSON.parse(new TextDecoder().decode(data)) as unknown;
|
||||
const payload = JSON.parse(new TextDecoder().decode(data));
|
||||
this.published.push({ subject, payload, seq: this.seq });
|
||||
return { seq: this.seq, stream: 'fake', duplicate: false };
|
||||
}
|
||||
|
|
@ -90,97 +76,81 @@ function buildPdfRequest(opKey: string): WorkerOperationRequest {
|
|||
};
|
||||
}
|
||||
|
||||
test.describe('worker jetstream control-plane adapters', () => {
|
||||
test('state store compareAndSet handles create/update semantics', async () => {
|
||||
describe('jetstream adapters', () => {
|
||||
test('state store compareAndSet enforces create/update semantics', async () => {
|
||||
const kv = new FakeKvStore();
|
||||
const store = new JetStreamOperationStateStore({ getKv: async () => kv });
|
||||
|
||||
const created = await store.compareAndSetOpIndex({
|
||||
opKey: 'k1',
|
||||
newOpId: 'op-1',
|
||||
expectedOpId: null,
|
||||
});
|
||||
const failedCreate = await store.compareAndSetOpIndex({
|
||||
opKey: 'k1',
|
||||
newOpId: 'op-2',
|
||||
expectedOpId: null,
|
||||
});
|
||||
const wrongExpected = await store.compareAndSetOpIndex({
|
||||
opKey: 'k1',
|
||||
newOpId: 'op-2',
|
||||
expectedOpId: 'op-x',
|
||||
});
|
||||
const updated = await store.compareAndSetOpIndex({
|
||||
opKey: 'k1',
|
||||
newOpId: 'op-2',
|
||||
expectedOpId: 'op-1',
|
||||
});
|
||||
const created = await store.compareAndSetOpIndex({ opKey: 'k1', newOpId: 'op-1', expectedOpId: null });
|
||||
const duplicateCreate = await store.compareAndSetOpIndex({ opKey: 'k1', newOpId: 'op-2', expectedOpId: null });
|
||||
const wrongExpected = await store.compareAndSetOpIndex({ opKey: 'k1', newOpId: 'op-2', expectedOpId: 'op-x' });
|
||||
const updated = await store.compareAndSetOpIndex({ opKey: 'k1', newOpId: 'op-2', expectedOpId: 'op-1' });
|
||||
|
||||
expect(created).toBeTruthy();
|
||||
expect(failedCreate).toBeFalsy();
|
||||
expect(wrongExpected).toBeFalsy();
|
||||
expect(updated).toBeTruthy();
|
||||
expect(created).toBe(true);
|
||||
expect(duplicateCreate).toBe(false);
|
||||
expect(wrongExpected).toBe(false);
|
||||
expect(updated).toBe(true);
|
||||
expect(await store.getOpIndex('k1')).toEqual({ opId: 'op-2' });
|
||||
});
|
||||
|
||||
test('queue and event adapters publish expected JetStream subjects', async () => {
|
||||
test('queue routes layout jobs to the expected subject and publishes events', async () => {
|
||||
const js = new FakeJetStream();
|
||||
const queue = new JetStreamOperationQueue({
|
||||
getJs: async () => js as any,
|
||||
getJs: async () => js as never,
|
||||
whisperSubject: 'jobs.whisper',
|
||||
layoutSubject: 'jobs.layout',
|
||||
});
|
||||
const events = new JetStreamOperationEventStream({
|
||||
getJs: async () => js as any,
|
||||
getJs: async () => js as never,
|
||||
getJsm: async () => ({
|
||||
consumers: {
|
||||
add: async () => ({ name: 'noop' }),
|
||||
delete: async () => true,
|
||||
},
|
||||
}) as any,
|
||||
}) as never,
|
||||
eventsStreamName: 'compute_events',
|
||||
});
|
||||
|
||||
await queue.enqueue({
|
||||
jobId: 'j1',
|
||||
opId: 'o1',
|
||||
opKey: 'k1',
|
||||
jobId: 'job-1',
|
||||
opId: 'op-1',
|
||||
opKey: 'k-1',
|
||||
kind: 'pdf_layout',
|
||||
queuedAt: 1000,
|
||||
payload: { documentId: 'd1', namespace: null, documentObjectKey: 'obj' },
|
||||
});
|
||||
|
||||
const appended = await events.append('o1', {
|
||||
opId: 'o1',
|
||||
opKey: 'k1',
|
||||
const appended = await events.append('op-1', {
|
||||
opId: 'op-1',
|
||||
opKey: 'k-1',
|
||||
kind: 'pdf_layout',
|
||||
jobId: 'j1',
|
||||
jobId: 'job-1',
|
||||
status: 'queued',
|
||||
queuedAt: 1000,
|
||||
updatedAt: 1000,
|
||||
});
|
||||
|
||||
expect(js.published.map((entry) => entry.subject)).toEqual(['jobs.layout', opEventsSubject('o1')]);
|
||||
expect(js.published.map((entry) => entry.subject)).toEqual(['jobs.layout', opEventsSubject('op-1')]);
|
||||
expect(appended.eventId).toBe(2);
|
||||
});
|
||||
|
||||
test('orchestrator integration writes index/state and reuses active op', async () => {
|
||||
test('orchestrator writes expected index/state keys', async () => {
|
||||
const kv = new FakeKvStore();
|
||||
const js = new FakeJetStream();
|
||||
|
||||
const store = new JetStreamOperationStateStore({ getKv: async () => kv });
|
||||
const events = new JetStreamOperationEventStream({
|
||||
getJs: async () => js as any,
|
||||
const stateStore = new JetStreamOperationStateStore({ getKv: async () => kv });
|
||||
const eventStream = new JetStreamOperationEventStream({
|
||||
getJs: async () => js as never,
|
||||
getJsm: async () => ({
|
||||
consumers: {
|
||||
add: async () => ({ name: 'noop' }),
|
||||
delete: async () => true,
|
||||
},
|
||||
}) as any,
|
||||
}) as never,
|
||||
eventsStreamName: 'compute_events',
|
||||
});
|
||||
const queue = new JetStreamOperationQueue({
|
||||
getJs: async () => js as any,
|
||||
getJs: async () => js as never,
|
||||
whisperSubject: 'jobs.whisper',
|
||||
layoutSubject: 'jobs.layout',
|
||||
});
|
||||
|
|
@ -189,8 +159,8 @@ test.describe('worker jetstream control-plane adapters', () => {
|
|||
let nextId = 1;
|
||||
const orchestrator = new OperationOrchestrator({
|
||||
queue,
|
||||
stateStore: store,
|
||||
eventStream: events,
|
||||
stateStore,
|
||||
eventStream,
|
||||
config: { opStaleMs: 10_000, maxCasRetries: 3 },
|
||||
clock: { now: () => now },
|
||||
idFactory: {
|
||||
|
|
@ -199,18 +169,18 @@ test.describe('worker jetstream control-plane adapters', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const req = buildPdfRequest('k-integration');
|
||||
const first = await orchestrator.enqueueOrReuse(req);
|
||||
const first = await orchestrator.enqueueOrReuse(buildPdfRequest('k-integration'));
|
||||
now = 2_000;
|
||||
const reused = await orchestrator.enqueueOrReuse(req);
|
||||
const reused = await orchestrator.enqueueOrReuse(buildPdfRequest('k-integration'));
|
||||
|
||||
expect(first.opId).toBe('op-1');
|
||||
expect(reused.opId).toBe('op-1');
|
||||
|
||||
const indexEntry = await kv.get(opIndexKvKey('k-integration'));
|
||||
const stateEntry = await kv.get(opStateKvKey('op-1'));
|
||||
|
||||
expect(indexEntry?.operation).toBe('PUT');
|
||||
expect(stateEntry?.operation).toBe('PUT');
|
||||
expect(js.published.map((entry) => entry.subject)).toEqual(['ops.events.op-1', 'jobs.layout']);
|
||||
expect(hashOpKey('k-integration')).toHaveLength(64);
|
||||
});
|
||||
});
|
||||
37
compute/worker/tests/unit/key-and-progress.test.ts
Normal file
37
compute/worker/tests/unit/key-and-progress.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import { hashOpKey, opIndexKvKey, opStateKvKey } from '../../src/control-plane/jetstream';
|
||||
import {
|
||||
buildInferProgressForPageParsed,
|
||||
buildInferProgressForPageStart,
|
||||
} from '../../src/pdf-progress';
|
||||
|
||||
describe('compute worker helpers', () => {
|
||||
test('hash and kv key helpers are stable and deterministic', () => {
|
||||
const hashA = hashOpKey('doc-123|layout|v1');
|
||||
const hashB = hashOpKey('doc-123|layout|v1');
|
||||
const hashC = hashOpKey('doc-123|layout|v2');
|
||||
|
||||
expect(hashA).toBe(hashB);
|
||||
expect(hashA).toHaveLength(64);
|
||||
expect(hashC).not.toBe(hashA);
|
||||
|
||||
expect(opIndexKvKey('doc-123|layout|v1')).toBe(`op_index.${hashA}`);
|
||||
expect(opStateKvKey('op-1')).toBe('op_state.op-1');
|
||||
});
|
||||
|
||||
test('progress helpers infer page counters correctly', () => {
|
||||
expect(buildInferProgressForPageStart({ pageNumber: 1, totalPages: 12 })).toEqual({
|
||||
totalPages: 12,
|
||||
pagesParsed: 0,
|
||||
currentPage: 1,
|
||||
phase: 'infer',
|
||||
});
|
||||
|
||||
expect(buildInferProgressForPageParsed({ pageNumber: 5, totalPages: 12 })).toEqual({
|
||||
totalPages: 12,
|
||||
pagesParsed: 5,
|
||||
currentPage: 5,
|
||||
phase: 'infer',
|
||||
});
|
||||
});
|
||||
});
|
||||
18
compute/worker/tests/unit/worker-loop-policy.test.ts
Normal file
18
compute/worker/tests/unit/worker-loop-policy.test.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
import { buildQueueWaitTiming, decideRetryAction } from '../../src/worker-loop-policy';
|
||||
|
||||
describe('worker loop policy', () => {
|
||||
test('returns queue wait timing with non-negative clamped duration', () => {
|
||||
expect(buildQueueWaitTiming(1000, 1300)).toEqual({ queueWaitMs: 300 });
|
||||
expect(buildQueueWaitTiming(1500, 1300)).toEqual({ queueWaitMs: 0 });
|
||||
});
|
||||
|
||||
test('retry policy: layout jobs can retry until max attempts', () => {
|
||||
expect(decideRetryAction({ kind: 'pdf_layout', deliveryCount: 1, pdfAttempts: 3 })).toBe('nak_retry');
|
||||
expect(decideRetryAction({ kind: 'pdf_layout', deliveryCount: 3, pdfAttempts: 3 })).toBe('term_fail');
|
||||
});
|
||||
|
||||
test('retry policy: whisper jobs terminate immediately by default', () => {
|
||||
expect(decideRetryAction({ kind: 'whisper_align', deliveryCount: 1, pdfAttempts: 10 })).toBe('term_fail');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": []
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
"start:raw": "next start -p 3003",
|
||||
"lint": "next lint",
|
||||
"test": "playwright test",
|
||||
"test:e2e": "playwright test",
|
||||
"test:unit": "vitest run",
|
||||
"test:compute": "vitest run --project compute-core --project compute-worker",
|
||||
"test:ci-env": "CI=true playwright test",
|
||||
"migrate": "node drizzle/scripts/migrate.mjs",
|
||||
"migrate-fs": "node scripts/migrate-fs-v2.mjs",
|
||||
|
|
@ -90,6 +93,7 @@
|
|||
"eslint-config-next": "^15.5.18",
|
||||
"postcss": "^8.5.14",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
636
pnpm-lock.yaml
636
pnpm-lock.yaml
|
|
@ -42,7 +42,7 @@ importers:
|
|||
version: 7.0.1
|
||||
better-auth:
|
||||
specifier: ^1.6.11
|
||||
version: 1.6.11(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(kysely@0.28.17)(pg@8.20.0))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
version: 1.6.11(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(kysely@0.28.17)(pg@8.20.0))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3)))
|
||||
better-sqlite3:
|
||||
specifier: ^12.10.0
|
||||
version: 12.10.0
|
||||
|
|
@ -194,6 +194,9 @@ importers:
|
|||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.1.7
|
||||
version: 4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3))
|
||||
|
||||
compute/core:
|
||||
dependencies:
|
||||
|
|
@ -1569,6 +1572,12 @@ packages:
|
|||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.4':
|
||||
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
|
||||
peerDependencies:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@nats-io/jetstream@3.4.0':
|
||||
resolution: {integrity: sha512-GzHQodNJ942+R5LRb8PuZ5ugVWVWMRiufxUYLLVWkXKfwDXYN+Owo0d7L/b9O7BPyrbYD7jQWAC6+ZVuXa9Gyw==}
|
||||
|
||||
|
|
@ -1679,6 +1688,9 @@ packages:
|
|||
resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@oxc-project/types@0.132.0':
|
||||
resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==}
|
||||
|
||||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
|
|
@ -1717,6 +1729,104 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.2':
|
||||
resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.2':
|
||||
resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.2':
|
||||
resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.2':
|
||||
resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.2':
|
||||
resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.2':
|
||||
resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.2':
|
||||
resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.2':
|
||||
resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.2':
|
||||
resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.2':
|
||||
resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.2':
|
||||
resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.2':
|
||||
resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.2':
|
||||
resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.2':
|
||||
resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.2':
|
||||
resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/pluginutils@1.0.1':
|
||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
|
|
@ -1960,9 +2070,15 @@ packages:
|
|||
'@types/better-sqlite3@7.6.13':
|
||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
|
||||
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||
|
||||
|
|
@ -2207,6 +2323,35 @@ packages:
|
|||
vue-router:
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@4.1.7':
|
||||
resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==}
|
||||
|
||||
'@vitest/mocker@4.1.7':
|
||||
resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.1.7':
|
||||
resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==}
|
||||
|
||||
'@vitest/runner@4.1.7':
|
||||
resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==}
|
||||
|
||||
'@vitest/snapshot@4.1.7':
|
||||
resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==}
|
||||
|
||||
'@vitest/spy@4.1.7':
|
||||
resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==}
|
||||
|
||||
'@vitest/utils@4.1.7':
|
||||
resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==}
|
||||
|
||||
'@xmldom/xmldom@0.9.10':
|
||||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
|
@ -2327,6 +2472,10 @@ packages:
|
|||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-types-flow@0.0.8:
|
||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||
|
||||
|
|
@ -2569,6 +2718,10 @@ packages:
|
|||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -2635,6 +2788,9 @@ packages:
|
|||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||
engines: {'0': node >= 6.0}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie@1.1.1:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2909,6 +3065,9 @@ packages:
|
|||
resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@2.1.0:
|
||||
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3087,6 +3246,9 @@ packages:
|
|||
estree-util-is-identifier-name@3.0.0:
|
||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -3109,6 +3271,10 @@ packages:
|
|||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
expect-type@1.3.0:
|
||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
ext@1.7.0:
|
||||
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
|
||||
|
||||
|
|
@ -3624,6 +3790,80 @@ packages:
|
|||
light-my-request@6.6.0:
|
||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.32.0:
|
||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -3658,6 +3898,9 @@ packages:
|
|||
resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
make-cancellable-promise@1.3.2:
|
||||
resolution: {integrity: sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==}
|
||||
|
||||
|
|
@ -3953,6 +4196,9 @@ packages:
|
|||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
obug@2.1.1:
|
||||
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -4037,6 +4283,9 @@ packages:
|
|||
resolution: {integrity: sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pdfjs-dist@4.8.69:
|
||||
resolution: {integrity: sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4177,6 +4426,10 @@ packages:
|
|||
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postcss@8.5.15:
|
||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -4411,6 +4664,11 @@ packages:
|
|||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rolldown@1.0.2:
|
||||
resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rou3@0.7.12:
|
||||
resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
|
||||
|
||||
|
|
@ -4511,6 +4769,9 @@ packages:
|
|||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -4545,6 +4806,12 @@ packages:
|
|||
stable-hash@0.0.5:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@4.1.0:
|
||||
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -4692,10 +4959,21 @@ packages:
|
|||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@1.2.3:
|
||||
resolution: {integrity: sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyrainbow@3.1.0:
|
||||
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
|
@ -4825,6 +5103,90 @@ packages:
|
|||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite@8.0.14:
|
||||
resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^20.19.0 || >=22.12.0
|
||||
'@vitejs/devtools': ^0.1.18
|
||||
esbuild: ^0.27.0 || ^0.28.0
|
||||
jiti: '>=1.21.0'
|
||||
less: ^4.0.0
|
||||
sass: ^1.70.0
|
||||
sass-embedded: ^1.70.0
|
||||
stylus: '>=0.54.8'
|
||||
sugarss: ^5.0.0
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitejs/devtools':
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@4.1.7:
|
||||
resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.1.7
|
||||
'@vitest/browser-preview': 4.1.7
|
||||
'@vitest/browser-webdriverio': 4.1.7
|
||||
'@vitest/coverage-istanbul': 4.1.7
|
||||
'@vitest/coverage-v8': 4.1.7
|
||||
'@vitest/ui': 4.1.7
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser-playwright':
|
||||
optional: true
|
||||
'@vitest/browser-preview':
|
||||
optional: true
|
||||
'@vitest/browser-webdriverio':
|
||||
optional: true
|
||||
'@vitest/coverage-istanbul':
|
||||
optional: true
|
||||
'@vitest/coverage-v8':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
|
|
@ -4849,6 +5211,11 @@ packages:
|
|||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -6248,6 +6615,13 @@ snapshots:
|
|||
'@tybys/wasm-util': 0.10.2
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
'@emnapi/runtime': 1.10.0
|
||||
'@tybys/wasm-util': 0.10.2
|
||||
optional: true
|
||||
|
||||
'@nats-io/jetstream@3.4.0':
|
||||
dependencies:
|
||||
'@nats-io/nats-core': 3.4.0
|
||||
|
|
@ -6326,6 +6700,8 @@ snapshots:
|
|||
|
||||
'@opentelemetry/semantic-conventions@1.41.1': {}
|
||||
|
||||
'@oxc-project/types@0.132.0': {}
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
|
|
@ -6360,6 +6736,57 @@ snapshots:
|
|||
dependencies:
|
||||
react: 19.2.6
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.2':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
'@emnapi/runtime': 1.10.0
|
||||
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.2':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.1': {}
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@rushstack/eslint-patch@1.16.1': {}
|
||||
|
|
@ -6660,10 +7087,17 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 20.19.41
|
||||
|
||||
'@types/chai@5.2.3':
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
assertion-error: 2.0.1
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
|
|
@ -6873,6 +7307,47 @@ snapshots:
|
|||
next: 15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
react: 19.2.6
|
||||
|
||||
'@vitest/expect@4.1.7':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.1.7
|
||||
'@vitest/utils': 4.1.7
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.7(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.7
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3)
|
||||
|
||||
'@vitest/pretty-format@4.1.7':
|
||||
dependencies:
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/runner@4.1.7':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.1.7
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.1.7':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.7
|
||||
'@vitest/utils': 4.1.7
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.1.7': {}
|
||||
|
||||
'@vitest/utils@4.1.7':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.7
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@xmldom/xmldom@0.9.10': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
|
|
@ -7031,6 +7506,8 @@ snapshots:
|
|||
get-intrinsic: 1.3.0
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-types-flow@0.0.8: {}
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
|
@ -7096,7 +7573,7 @@ snapshots:
|
|||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-auth@1.6.11(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(kysely@0.28.17)(pg@8.20.0))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
better-auth@1.6.11(better-sqlite3@12.10.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(kysely@0.28.17)(pg@8.20.0))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(pg@8.20.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3))):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)
|
||||
'@better-auth/drizzle-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(kysely@0.28.17)(pg@8.20.0))
|
||||
|
|
@ -7123,6 +7600,7 @@ snapshots:
|
|||
pg: 8.20.0
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
vitest: 4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3))
|
||||
transitivePeerDependencies:
|
||||
- '@cloudflare/workers-types'
|
||||
- '@opentelemetry/api'
|
||||
|
|
@ -7219,6 +7697,8 @@ snapshots:
|
|||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
|
@ -7287,6 +7767,8 @@ snapshots:
|
|||
readable-stream: 3.6.2
|
||||
typedarray: 0.0.6
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
core-js@3.49.0: {}
|
||||
|
|
@ -7531,6 +8013,8 @@ snapshots:
|
|||
iterator.prototype: 1.1.5
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
es-module-lexer@2.1.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
@ -7891,6 +8375,10 @@ snapshots:
|
|||
|
||||
estree-util-is-identifier-name@3.0.0: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
event-emitter@0.3.5:
|
||||
|
|
@ -7910,6 +8398,8 @@ snapshots:
|
|||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
ext@1.7.0:
|
||||
dependencies:
|
||||
type: 2.7.3
|
||||
|
|
@ -8485,6 +8975,55 @@ snapshots:
|
|||
process-warning: 4.0.1
|
||||
set-cookie-parser: 2.7.2
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.32.0:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64: 1.32.0
|
||||
lightningcss-darwin-arm64: 1.32.0
|
||||
lightningcss-darwin-x64: 1.32.0
|
||||
lightningcss-freebsd-x64: 1.32.0
|
||||
lightningcss-linux-arm-gnueabihf: 1.32.0
|
||||
lightningcss-linux-arm64-gnu: 1.32.0
|
||||
lightningcss-linux-arm64-musl: 1.32.0
|
||||
lightningcss-linux-x64-gnu: 1.32.0
|
||||
lightningcss-linux-x64-musl: 1.32.0
|
||||
lightningcss-win32-arm64-msvc: 1.32.0
|
||||
lightningcss-win32-x64-msvc: 1.32.0
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
|
@ -8511,6 +9050,10 @@ snapshots:
|
|||
|
||||
lru-cache@11.3.6: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
make-cancellable-promise@1.3.2: {}
|
||||
|
||||
make-event-props@1.6.2: {}
|
||||
|
|
@ -9008,6 +9551,8 @@ snapshots:
|
|||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
obug@2.1.1: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
once@1.4.0:
|
||||
|
|
@ -9087,6 +9632,8 @@ snapshots:
|
|||
path2d@0.2.2:
|
||||
optional: true
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pdfjs-dist@4.8.69:
|
||||
optionalDependencies:
|
||||
canvas: 3.2.3
|
||||
|
|
@ -9232,6 +9779,12 @@ snapshots:
|
|||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.15:
|
||||
dependencies:
|
||||
nanoid: 3.3.12
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
|
||||
postgres-bytea@1.0.1: {}
|
||||
|
|
@ -9537,6 +10090,27 @@ snapshots:
|
|||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rolldown@1.0.2:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.132.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm64': 1.0.2
|
||||
'@rolldown/binding-darwin-arm64': 1.0.2
|
||||
'@rolldown/binding-darwin-x64': 1.0.2
|
||||
'@rolldown/binding-freebsd-x64': 1.0.2
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.2
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.0.2
|
||||
'@rolldown/binding-linux-arm64-musl': 1.0.2
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.0.2
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.0.2
|
||||
'@rolldown/binding-linux-x64-gnu': 1.0.2
|
||||
'@rolldown/binding-linux-x64-musl': 1.0.2
|
||||
'@rolldown/binding-openharmony-arm64': 1.0.2
|
||||
'@rolldown/binding-wasm32-wasi': 1.0.2
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.2
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.2
|
||||
|
||||
rou3@0.7.12: {}
|
||||
|
||||
run-parallel@1.2.0:
|
||||
|
|
@ -9678,6 +10252,8 @@ snapshots:
|
|||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
|
@ -9707,6 +10283,10 @@ snapshots:
|
|||
|
||||
stable-hash@0.0.5: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@4.1.0: {}
|
||||
|
||||
stop-iteration-iterator@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
@ -9928,11 +10508,17 @@ snapshots:
|
|||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.2.3: {}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
tinyrainbow@3.1.0: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
|
@ -10110,6 +10696,47 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.15
|
||||
rolldown: 1.0.2
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.41
|
||||
esbuild: 0.28.0
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
tsx: 4.22.3
|
||||
|
||||
vitest@4.1.7(@types/node@20.19.41)(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.7
|
||||
'@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3))
|
||||
'@vitest/pretty-format': 4.1.7
|
||||
'@vitest/runner': 4.1.7
|
||||
'@vitest/snapshot': 4.1.7
|
||||
'@vitest/spy': 4.1.7
|
||||
'@vitest/utils': 4.1.7
|
||||
es-module-lexer: 2.1.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 4.1.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.2.3
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.14(@types/node@20.19.41)(esbuild@0.28.0)(jiti@1.21.7)(tsx@4.22.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.41
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
|
@ -10159,6 +10786,11 @@ snapshots:
|
|||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import { isAbortLikeError } from '../../src/lib/server/compute/abort-like-error';
|
||||
|
||||
test.describe('isAbortLikeError', () => {
|
||||
test('matches abort-shaped errors', () => {
|
||||
expect(isAbortLikeError(new DOMException('This operation was aborted', 'AbortError'))).toBe(true);
|
||||
expect(isAbortLikeError(Object.assign(new Error('random'), { name: 'AbortError' }))).toBe(true);
|
||||
expect(isAbortLikeError(new Error('This operation was aborted'))).toBe(true);
|
||||
expect(isAbortLikeError({ name: 'AbortError' })).toBe(true);
|
||||
});
|
||||
|
||||
test('does not match non-abort errors', () => {
|
||||
expect(isAbortLikeError(new Error('boom'))).toBe(false);
|
||||
expect(isAbortLikeError({ name: 'TypeError' })).toBe(false);
|
||||
expect(isAbortLikeError(null)).toBe(false);
|
||||
expect(isAbortLikeError(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import type { WorkerOperationRequest, WorkerOperationState } from '../../compute/core/src/api-contracts';
|
||||
import {
|
||||
encodeSseFrame,
|
||||
explainReplacementReason,
|
||||
InMemoryOperationEventStream,
|
||||
InMemoryOperationQueue,
|
||||
InMemoryOperationStateStore,
|
||||
OperationOrchestrator,
|
||||
parseSseEventId,
|
||||
parseSsePayload,
|
||||
shouldReuseExistingOperation,
|
||||
} from '../../compute/core/src/control-plane';
|
||||
|
||||
function buildPdfLayoutRequest(opKey: string): WorkerOperationRequest {
|
||||
return {
|
||||
kind: 'pdf_layout',
|
||||
opKey,
|
||||
payload: {
|
||||
documentId: `doc-${opKey}`,
|
||||
documentObjectKey: `s3://bucket/${opKey}.pdf`,
|
||||
namespace: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test.describe('compute control-plane', () => {
|
||||
test('in-memory queue and state store support enqueue/claim/CAS', async () => {
|
||||
const queue = new InMemoryOperationQueue();
|
||||
const store = new InMemoryOperationStateStore();
|
||||
|
||||
await queue.enqueue({
|
||||
jobId: 'job-1',
|
||||
opId: 'op-1',
|
||||
opKey: 'k-1',
|
||||
kind: 'pdf_layout',
|
||||
queuedAt: 1000,
|
||||
payload: { documentId: 'd1', documentObjectKey: 'obj1', namespace: null },
|
||||
});
|
||||
await queue.enqueue({
|
||||
jobId: 'job-2',
|
||||
opId: 'op-2',
|
||||
opKey: 'k-2',
|
||||
kind: 'whisper_align',
|
||||
queuedAt: 1100,
|
||||
payload: { text: 'hello', audioObjectKey: 'obj2' },
|
||||
});
|
||||
|
||||
expect(queue.size()).toBe(2);
|
||||
expect(queue.size('pdf_layout')).toBe(1);
|
||||
expect(queue.size('whisper_align')).toBe(1);
|
||||
|
||||
const claimedLayout = await queue.claimNext('pdf_layout');
|
||||
expect(claimedLayout?.opId).toBe('op-1');
|
||||
expect(queue.size('pdf_layout')).toBe(0);
|
||||
|
||||
const firstCas = await store.compareAndSetOpIndex({
|
||||
opKey: 'k-1',
|
||||
newOpId: 'op-1',
|
||||
expectedOpId: null,
|
||||
});
|
||||
const secondCas = await store.compareAndSetOpIndex({
|
||||
opKey: 'k-1',
|
||||
newOpId: 'op-2',
|
||||
expectedOpId: null,
|
||||
});
|
||||
|
||||
expect(firstCas).toBeTruthy();
|
||||
expect(secondCas).toBeFalsy();
|
||||
expect(await store.getOpIndex('k-1')).toEqual({ opId: 'op-1' });
|
||||
});
|
||||
|
||||
test('in-memory event stream replays from sinceEventId and streams live events', async () => {
|
||||
const stream = new InMemoryOperationEventStream();
|
||||
|
||||
const queued: WorkerOperationState = {
|
||||
opId: 'op-1',
|
||||
opKey: 'k-1',
|
||||
kind: 'pdf_layout',
|
||||
jobId: 'job-1',
|
||||
status: 'queued',
|
||||
queuedAt: 1000,
|
||||
updatedAt: 1000,
|
||||
};
|
||||
const running: WorkerOperationState = {
|
||||
...queued,
|
||||
status: 'running',
|
||||
startedAt: 1200,
|
||||
updatedAt: 1200,
|
||||
};
|
||||
const succeeded: WorkerOperationState = {
|
||||
...running,
|
||||
status: 'succeeded',
|
||||
updatedAt: 1400,
|
||||
result: { ok: true },
|
||||
};
|
||||
|
||||
await stream.append('op-1', queued);
|
||||
await stream.append('op-1', running);
|
||||
|
||||
const receivedEventIds: number[] = [];
|
||||
const unsubscribe = await stream.subscribe({
|
||||
opId: 'op-1',
|
||||
sinceEventId: 1,
|
||||
onEvent: (event) => {
|
||||
receivedEventIds.push(event.eventId);
|
||||
},
|
||||
});
|
||||
|
||||
await stream.append('op-1', succeeded);
|
||||
unsubscribe();
|
||||
|
||||
expect(receivedEventIds).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
test('orchestrator reuses fresh inflight operations and replaces stale ones', async () => {
|
||||
let now = 1_000;
|
||||
let nextId = 1;
|
||||
const queue = new InMemoryOperationQueue();
|
||||
const stateStore = new InMemoryOperationStateStore();
|
||||
const eventStream = new InMemoryOperationEventStream();
|
||||
const orchestrator = new OperationOrchestrator({
|
||||
queue,
|
||||
stateStore,
|
||||
eventStream,
|
||||
config: { opStaleMs: 2_000, maxCasRetries: 5 },
|
||||
clock: { now: () => now },
|
||||
idFactory: {
|
||||
opId: () => `op-${nextId}`,
|
||||
jobId: () => `job-${nextId++}`,
|
||||
},
|
||||
});
|
||||
|
||||
const request = buildPdfLayoutRequest('same-op-key');
|
||||
|
||||
const first = await orchestrator.enqueueOrReuse(request);
|
||||
expect(first.opId).toBe('op-1');
|
||||
expect(queue.size('pdf_layout')).toBe(1);
|
||||
|
||||
now = 2_000;
|
||||
const reused = await orchestrator.enqueueOrReuse(request);
|
||||
expect(reused.opId).toBe('op-1');
|
||||
expect(queue.size('pdf_layout')).toBe(1);
|
||||
|
||||
await orchestrator.markRunning({ opId: first.opId, updatedAt: 2_100 });
|
||||
|
||||
now = 6_000;
|
||||
const replaced = await orchestrator.enqueueOrReuse(request);
|
||||
expect(replaced.opId).toBe('op-2');
|
||||
expect(queue.size('pdf_layout')).toBe(2);
|
||||
expect(await stateStore.getOpIndex('same-op-key')).toEqual({ opId: 'op-2' });
|
||||
|
||||
const op1Events = await eventStream.listSince('op-1', 0);
|
||||
const op2Events = await eventStream.listSince('op-2', 0);
|
||||
expect(op1Events.map((event) => event.eventId)).toEqual([1, 2]);
|
||||
expect(op2Events.map((event) => event.eventId)).toEqual([1]);
|
||||
});
|
||||
|
||||
test('state-machine helpers return consistent reuse/replacement decisions', () => {
|
||||
const current: WorkerOperationState = {
|
||||
opId: 'op-1',
|
||||
opKey: 'same-op-key',
|
||||
kind: 'pdf_layout',
|
||||
jobId: 'job-1',
|
||||
status: 'running',
|
||||
queuedAt: 1000,
|
||||
updatedAt: 2000,
|
||||
};
|
||||
|
||||
expect(shouldReuseExistingOperation({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 2500,
|
||||
opStaleMs: 1_000,
|
||||
})).toBeTruthy();
|
||||
|
||||
expect(shouldReuseExistingOperation({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 4005,
|
||||
opStaleMs: 1_000,
|
||||
})).toBeFalsy();
|
||||
|
||||
expect(explainReplacementReason({
|
||||
current,
|
||||
requestKind: 'pdf_layout',
|
||||
now: 4005,
|
||||
opStaleMs: 1_000,
|
||||
})).toBe('stale_running');
|
||||
});
|
||||
|
||||
test('sse helpers encode and decode frame payload and id', () => {
|
||||
const frame = encodeSseFrame({
|
||||
id: 42,
|
||||
event: 'snapshot',
|
||||
data: { ok: true },
|
||||
});
|
||||
expect(parseSseEventId(frame)).toBe(42);
|
||||
expect(parseSsePayload(frame)).toBe('{"ok":true}');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import { buildPdfOpKey } from '../../src/lib/server/compute/worker';
|
||||
|
||||
test.describe('compute worker pdf opKey', () => {
|
||||
test('keeps stable key when no force token is provided', () => {
|
||||
const base = {
|
||||
documentId: 'doc-123',
|
||||
namespace: 'ns-1',
|
||||
documentObjectKey: 'docs/ns-1/doc-123',
|
||||
};
|
||||
expect(buildPdfOpKey(base)).toBe('pdf_layout|v1|doc-123|ns-1|docs/ns-1/doc-123|');
|
||||
expect(buildPdfOpKey(base)).toBe(buildPdfOpKey(base));
|
||||
});
|
||||
|
||||
test('cache-busts key when force token is provided', () => {
|
||||
const base = {
|
||||
documentId: 'doc-123',
|
||||
namespace: 'ns-1',
|
||||
documentObjectKey: 'docs/ns-1/doc-123',
|
||||
};
|
||||
const opKeyA = buildPdfOpKey({ ...base, forceToken: 'force-a' });
|
||||
const opKeyB = buildPdfOpKey({ ...base, forceToken: 'force-b' });
|
||||
const normal = buildPdfOpKey(base);
|
||||
|
||||
expect(opKeyA).not.toBe(opKeyB);
|
||||
expect(opKeyA).not.toBe(normal);
|
||||
expect(opKeyB).not.toBe(normal);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
buildInferProgressForPageParsed,
|
||||
buildInferProgressForPageStart,
|
||||
} from '../../compute/worker/src/pdf-progress';
|
||||
|
||||
test.describe('compute worker pdf progress helpers', () => {
|
||||
test('page-start progress keeps current page but does not count it as parsed yet', () => {
|
||||
expect(buildInferProgressForPageStart({ pageNumber: 1, totalPages: 12 })).toEqual({
|
||||
totalPages: 12,
|
||||
pagesParsed: 0,
|
||||
currentPage: 1,
|
||||
phase: 'infer',
|
||||
});
|
||||
|
||||
expect(buildInferProgressForPageStart({ pageNumber: 5, totalPages: 12 })).toEqual({
|
||||
totalPages: 12,
|
||||
pagesParsed: 4,
|
||||
currentPage: 5,
|
||||
phase: 'infer',
|
||||
});
|
||||
});
|
||||
|
||||
test('page-parsed progress counts the current page as parsed', () => {
|
||||
expect(buildInferProgressForPageParsed({ pageNumber: 5, totalPages: 12 })).toEqual({
|
||||
totalPages: 12,
|
||||
pagesParsed: 5,
|
||||
currentPage: 5,
|
||||
phase: 'infer',
|
||||
});
|
||||
});
|
||||
});
|
||||
24
vitest.config.ts
Normal file
24
vitest.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
reporters: process.env.CI ? ['default', 'github'] : ['default'],
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'compute-core',
|
||||
environment: 'node',
|
||||
include: ['compute/core/tests/control-plane/**/*.test.ts'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'compute-worker',
|
||||
environment: 'node',
|
||||
include: ['compute/worker/tests/{unit,api}/**/*.test.ts'],
|
||||
setupFiles: ['compute/worker/tests/setup-env.ts'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue