refactor(api): tidy the api and shared packages (#2733)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 17:37:48 +02:00
1 parent 580401d2dc
commit 5afbf67a70
333 files changed
+10180 -7910

No files matched your search

@@ -3,9 +3,14 @@
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
import type {WorkerJobOptions, WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes';
export interface WorkerTaskAttempt {
readonly isLastAttempt: boolean;
}
export interface WorkerTaskHelpers {
logger: LoggerInterface;
jobId: bigint;
attempt?: WorkerTaskAttempt;
addJob: <TPayload extends WorkerJobPayload = WorkerJobPayload>(
taskType: string,
payload: TPayload,
@@ -24,37 +24,33 @@ export class HttpWorkerQueue implements IQueueProvider {
this.tracing = options.tracing;
}
private createTimeoutController(): AbortController {
private async withResponse<T>(
input: string | URL,
init: RequestInit,
consume: (response: Response) => Promise<T>,
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
(
controller as {
timeoutId?: NodeJS.Timeout;
}
).timeoutId = timeoutId;
return controller;
}
private async fetchWithTimeout(input: string | URL, init?: RequestInit): Promise<Response> {
const controller = this.createTimeoutController();
try {
const response = await fetch(input, {
...init,
signal: controller.signal,
});
return response;
return await consume(response);
} finally {
const timeoutId = (
controller as {
timeoutId?: NodeJS.Timeout;
}
).timeoutId;
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
clearTimeout(timeoutId);
controller.abort();
}
}
private async requireSuccess(response: Response, action: string): Promise<void> {
if (response.ok) {
return;
}
const text = await response.text();
throw new Error(`Failed to ${action}: ${response.status} ${text}`);
}
private async withOptionalSpan<T>(
options: {
name: string;
@@ -100,20 +96,22 @@ export class HttpWorkerQueue implements IQueueProvider {
run_at: options?.runAt?.toISOString(),
max_attempts: options?.maxAttempts ?? 5,
};
const response = await this.fetchWithTimeout(`${this.baseUrl}/enqueue`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to enqueue job: ${response.status} ${text}`);
}
const jobIdResult = (await response.json()) as {
job_id: string;
};
this.setSpanAttributes({'queue.job_id': jobIdResult.job_id});
return jobIdResult.job_id;
return this.withResponse(
`${this.baseUrl}/enqueue`,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
},
async (response) => {
await this.requireSuccess(response, 'enqueue job');
const jobIdResult = (await response.json()) as {
job_id: string;
};
this.setSpanAttributes({'queue.job_id': jobIdResult.job_id});
return jobIdResult.job_id;
},
);
},
);
}
@@ -134,34 +132,32 @@ export class HttpWorkerQueue implements IQueueProvider {
url.searchParams.set('task_types', taskTypes.join(','));
url.searchParams.set('limit', limit.toString());
url.searchParams.set('wait_time_ms', '0');
const response = await this.fetchWithTimeout(url.toString(), {method: 'GET'});
if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to dequeue job: ${response.status} ${text}`);
}
this.addSpanEvent('dequeue.parse_response');
const jobs = (await response.json()) as Array<LeasedQueueJob>;
const jobCount = jobs?.length ?? 0;
this.setSpanAttributes({
'queue.jobs_returned': jobCount,
'queue.empty': jobCount === 0,
return this.withResponse(url, {method: 'GET'}, async (response) => {
await this.requireSuccess(response, 'dequeue job');
this.addSpanEvent('dequeue.parse_response');
const jobs = (await response.json()) as Array<LeasedQueueJob>;
const jobCount = jobs?.length ?? 0;
this.setSpanAttributes({
'queue.jobs_returned': jobCount,
'queue.empty': jobCount === 0,
});
this.addSpanEvent('dequeue.complete');
return jobs ?? [];
});
this.addSpanEvent('dequeue.complete');
return jobs ?? [];
},
);
}
async upsertCron(id: string, taskType: string, payload: WorkerJobPayload, cronExpression: string): Promise<void> {
const response = await this.fetchWithTimeout(`${this.baseUrl}/cron`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id, task_type: taskType, payload, cron_expression: cronExpression}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to upsert cron job: ${response.status} ${text}`);
}
await this.withResponse(
`${this.baseUrl}/cron`,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id, task_type: taskType, payload, cron_expression: cronExpression}),
},
(response) => this.requireSuccess(response, 'upsert cron job'),
);
}
async complete(receipt: string): Promise<void> {
@@ -172,17 +168,16 @@ export class HttpWorkerQueue implements IQueueProvider {
'queue.receipt': receipt,
},
},
async () => {
const response = await this.fetchWithTimeout(`${this.baseUrl}/ack`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({receipt}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to complete job: ${response.status} ${text}`);
}
},
async () =>
this.withResponse(
`${this.baseUrl}/ack`,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({receipt}),
},
(response) => this.requireSuccess(response, 'complete job'),
),
);
}
@@ -195,48 +190,45 @@ export class HttpWorkerQueue implements IQueueProvider {
'queue.error_message': error,
},
},
async () => {
const response = await this.fetchWithTimeout(`${this.baseUrl}/nack`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({receipt, error}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to fail job: ${response.status} ${text}`);
}
},
async () =>
this.withResponse(
`${this.baseUrl}/nack`,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({receipt, error}),
},
(response) => this.requireSuccess(response, 'fail job'),
),
);
}
async cancelJob(jobId: string): Promise<boolean> {
const response = await this.fetchWithTimeout(`${this.baseUrl}/job/${jobId}`, {
method: 'DELETE',
});
if (!response.ok) {
const text = await response.text();
if (response.status === 404) {
return false;
return this.withResponse(`${this.baseUrl}/job/${jobId}`, {method: 'DELETE'}, async (response) => {
if (!response.ok) {
const text = await response.text();
if (response.status === 404) {
return false;
}
throw new Error(`Failed to cancel job: ${response.status} ${text}`);
}
throw new Error(`Failed to cancel job: ${response.status} ${text}`);
}
const result = (await response.json()) as {
success: boolean;
};
return result.success ?? true;
const result = (await response.json()) as {
success: boolean;
};
return result.success ?? true;
});
}
async retryDeadLetterJob(jobId: string): Promise<boolean> {
const response = await this.fetchWithTimeout(`${this.baseUrl}/retry/${jobId}`, {
method: 'POST',
});
if (!response.ok) {
const text = await response.text();
if (response.status === 404) {
return false;
return this.withResponse(`${this.baseUrl}/retry/${jobId}`, {method: 'POST'}, async (response) => {
if (!response.ok) {
const text = await response.text();
if (response.status === 404) {
return false;
}
throw new Error(`Failed to retry job: ${response.status} ${text}`);
}
throw new Error(`Failed to retry job: ${response.status} ${text}`);
}
return true;
return true;
});
}
}