Files
zpan/server/usecases/background-job.ts
T
Jasper Van ad0f21bb39 fix: unify list pagination and realtime updates (#524)
* fix!: unify pagination and realtime change delivery

Replace offset paging on affected unbounded collections with signed keyset tokens and infinite loading. Persist scoped resource changes so one global SSE connection can resume and invalidate query caches safely.

BREAKING CHANGE: migrated list APIs now accept pageToken and return nextPageToken instead of page and total fields.

Refs #450

* fix: keep page tokens at the HTTP boundary

Move signed page-token handling out of the pure domain layer so dependency-cruiser architecture checks pass without changing behavior.

* fix: route background job stats through usecase

Keep the HTTP boundary from reaching directly into repository ports and cover the new usecase wrapper.

* fix: align clients and checks with cursor pagination

* refactor: unify pagination boundaries and infinite loading
2026-07-27 02:02:53 -04:00

66 lines
2.5 KiB
TypeScript

// The background-jobs resource usecase (/api/background-jobs). Owns every
// port call behind the routes: enqueue + dispatch on create, the list/get/cancel
// reads, and the retry-then-redispatch flow that re-parses a job's stored request
// so a fresh attempt rides the same archive pipeline.
//
// BackgroundJobError is thrown by the repo (not_found / not_cancelable /
// not_retryable) and propagates through these functions untouched; the http
// handler catches it and maps the code to a status. Org/user resolution stays in
// the handler as input parsing.
import type { CreateBackgroundJobRequest } from '@shared/schemas'
import { createBackgroundJobRequestSchema } from '@shared/schemas'
import type { BackgroundJob } from '@shared/types'
import { enqueueArchiveJob } from './archive-processing'
// create dispatches on top of enqueueArchiveJob, which reaches across the archive
// ports, so it forwards the whole Deps; the reads need only the narrow ports.
import type { Deps } from './deps'
import type { ListBackgroundJobsOptions } from './ports'
export function listBackgroundJobs(deps: Pick<Deps, 'backgroundJobs'>, orgId: string, opts: ListBackgroundJobsOptions) {
return deps.backgroundJobs.list(orgId, opts)
}
export async function getActiveBackgroundJobCount(deps: Pick<Deps, 'backgroundJobs'>, orgId: string): Promise<number> {
return (await deps.backgroundJobs.activeSummary(orgId)).count
}
export function getBackgroundJob(
deps: Pick<Deps, 'backgroundJobs'>,
orgId: string,
id: string,
): Promise<BackgroundJob> {
return deps.backgroundJobs.get(orgId, id)
}
export function cancelBackgroundJob(
deps: Pick<Deps, 'backgroundJobs'>,
orgId: string,
id: string,
): Promise<BackgroundJob> {
return deps.backgroundJobs.cancel(orgId, id)
}
export async function createBackgroundJob(
deps: Deps,
params: { orgId: string; userId: string; request: CreateBackgroundJobRequest },
): Promise<BackgroundJob> {
const { orgId, userId, request } = params
const job = await enqueueArchiveJob(deps, { orgId, userId, request })
await deps.archiveJobs.dispatch({ orgId, userId, request, jobId: job.id })
return job
}
export async function retryBackgroundJob(
deps: Pick<Deps, 'backgroundJobs' | 'archiveJobs'>,
orgId: string,
id: string,
): Promise<BackgroundJob> {
const job = await deps.backgroundJobs.retry(orgId, id)
const request = createBackgroundJobRequestSchema.safeParse(job.metadata)
if (request.success) {
await deps.archiveJobs.dispatch({ orgId, userId: job.userId, request: request.data, jobId: job.id })
}
return job
}