@shivaedev/jobs 0.22.1 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +47 -53
- package/dist/JobFailure.d.ts +7 -0
- package/dist/JobFailure.d.ts.map +1 -0
- package/dist/JobFailure.js +8 -0
- package/dist/JobFailure.js.map +1 -0
- package/dist/server/PgBoss.d.ts +11 -0
- package/dist/server/PgBoss.d.ts.map +1 -0
- package/dist/server/PgBoss.js +4 -0
- package/dist/server/PgBoss.js.map +1 -0
- package/dist/server/client.d.ts +1 -1
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/health.d.ts +3 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js.map +1 -1
- package/dist/server/internal/batchHandler.d.ts +2 -1
- package/dist/server/internal/batchHandler.d.ts.map +1 -1
- package/dist/server/internal/batchHandler.js +49 -31
- package/dist/server/internal/batchHandler.js.map +1 -1
- package/dist/server/internal/clientErrors.d.ts.map +1 -1
- package/dist/server/internal/clientErrors.js +2 -1
- package/dist/server/internal/clientErrors.js.map +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts.map +1 -1
- package/dist/server/internal/deferrableAttempt.js +5 -1
- package/dist/server/internal/deferrableAttempt.js.map +1 -1
- package/dist/server/internal/ensureQueue.d.ts +2 -1
- package/dist/server/internal/ensureQueue.d.ts.map +1 -1
- package/dist/server/internal/ensureQueue.js +16 -21
- package/dist/server/internal/ensureQueue.js.map +1 -1
- package/dist/server/internal/failureOutput.d.ts +4 -0
- package/dist/server/internal/failureOutput.d.ts.map +1 -0
- package/dist/server/internal/failureOutput.js +9 -0
- package/dist/server/internal/failureOutput.js.map +1 -0
- package/dist/server/internal/lifecycle.d.ts +1 -1
- package/dist/server/internal/lifecycle.d.ts.map +1 -1
- package/dist/server/internal/lifecycle.js +10 -16
- package/dist/server/internal/lifecycle.js.map +1 -1
- package/dist/server/internal/listJobs.d.ts.map +1 -1
- package/dist/server/internal/listJobs.js +44 -39
- package/dist/server/internal/listJobs.js.map +1 -1
- package/dist/server/internal/makeService.js +1 -1
- package/dist/server/internal/makeService.js.map +1 -1
- package/dist/server/internal/register.d.ts +4 -3
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +34 -48
- package/dist/server/internal/register.js.map +1 -1
- package/dist/server/internal/registerReleasedJobs.d.ts +2 -1
- package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -1
- package/dist/server/internal/registerReleasedJobs.js +3 -3
- package/dist/server/internal/registerReleasedJobs.js.map +1 -1
- package/dist/server/internal/registerWorker.d.ts +3 -2
- package/dist/server/internal/registerWorker.d.ts.map +1 -1
- package/dist/server/internal/registerWorker.js +26 -33
- package/dist/server/internal/registerWorker.js.map +1 -1
- package/dist/server/internal/retryWake.d.ts +4 -0
- package/dist/server/internal/retryWake.d.ts.map +1 -0
- package/dist/server/internal/retryWake.js +32 -0
- package/dist/server/internal/retryWake.js.map +1 -0
- package/dist/server/internal/runQueueWorker.d.ts +5 -0
- package/dist/server/internal/runQueueWorker.d.ts.map +1 -0
- package/dist/server/internal/runQueueWorker.js +12 -0
- package/dist/server/internal/runQueueWorker.js.map +1 -0
- package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -1
- package/dist/server/internal/schemaIndexBuilds.js +5 -2
- package/dist/server/internal/schemaIndexBuilds.js.map +1 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +16 -5
- package/dist/server/schema.js.map +1 -1
- package/dist/server/service.d.ts +13 -8
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +53 -22
- package/dist/server/service.js.map +1 -1
- package/package.json +14 -8
- package/src/JobFailure.ts +9 -0
- package/src/server/PgBoss.ts +8 -0
- package/src/server/client.ts +1 -1
- package/src/server/health.ts +3 -2
- package/src/server/internal/batchHandler.ts +61 -42
- package/src/server/internal/clientErrors.ts +2 -1
- package/src/server/internal/deferrableAttempt.ts +5 -1
- package/src/server/internal/ensureQueue.ts +18 -20
- package/src/server/internal/failureOutput.ts +9 -0
- package/src/server/internal/lifecycle.ts +13 -16
- package/src/server/internal/listJobs.ts +57 -47
- package/src/server/internal/makeService.ts +1 -1
- package/src/server/internal/register.ts +68 -66
- package/src/server/internal/registerReleasedJobs.ts +8 -3
- package/src/server/internal/registerWorker.ts +35 -46
- package/src/server/internal/retryWake.ts +33 -0
- package/src/server/internal/runQueueWorker.ts +19 -0
- package/src/server/internal/schemaIndexBuilds.ts +6 -2
- package/src/server/schema.ts +22 -6
- package/src/server/service.ts +84 -49
|
@@ -3,6 +3,7 @@ import { WorkerRelease } from "@shivaedev/sql/WorkerRelease.ts";
|
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
4
|
import type { JobRegistration } from "#definition.ts";
|
|
5
5
|
import { toPgBossError } from "#error.ts";
|
|
6
|
+
import type { PgBossRunner } from "#internal/batchHandler.ts";
|
|
6
7
|
import { registerJobs, registrationName } from "./register.ts";
|
|
7
8
|
|
|
8
9
|
export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
|
|
@@ -10,16 +11,20 @@ export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
|
|
|
10
11
|
jobs: readonly JobRegistration[],
|
|
11
12
|
context: Context.Context<R>,
|
|
12
13
|
replaceWorkers: boolean,
|
|
13
|
-
|
|
14
|
+
execute: PgBossRunner,
|
|
15
|
+
schema: string | undefined,
|
|
16
|
+
graceful: boolean,
|
|
14
17
|
) {
|
|
15
18
|
const signal = yield* Effect.serviceOption(WorkerRelease);
|
|
16
19
|
const workers = jobs.filter((job) => job._tag !== "QueueDefinition");
|
|
17
20
|
const scope = yield* Effect.scope;
|
|
18
|
-
const register = registerJobs(client, jobs, context, replaceWorkers, schema).pipe(
|
|
21
|
+
const register = registerJobs(client, jobs, context, replaceWorkers, execute, schema, true, graceful).pipe(
|
|
22
|
+
Effect.provideService(Scope.Scope, scope),
|
|
23
|
+
);
|
|
19
24
|
if (Option.isNone(signal) || workers.length === 0) {
|
|
20
25
|
return yield* register;
|
|
21
26
|
}
|
|
22
|
-
yield* registerJobs(client, jobs, context, replaceWorkers, schema, false);
|
|
27
|
+
yield* registerJobs(client, jobs, context, replaceWorkers, execute, schema, false, graceful);
|
|
23
28
|
let working = replaceWorkers;
|
|
24
29
|
let paused = false;
|
|
25
30
|
yield* signal.value.register((active) =>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Context, Effect, Fiber, Scope } from "effect";
|
|
2
2
|
import type { Job, WorkOptions } from "pg-boss";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { toPgBossError } from "#error.ts";
|
|
5
|
+
import { batchHandler, type PgBossRunner } from "#internal/batchHandler.ts";
|
|
5
6
|
import { deferrableAttempt } from "#internal/deferrableAttempt.ts";
|
|
7
|
+
import { retryWake } from "#internal/retryWake.ts";
|
|
6
8
|
import { traceJob } from "#internal/tracing.ts";
|
|
7
9
|
|
|
8
|
-
export
|
|
10
|
+
export const registerWorker = Effect.fnUntraced(function* (
|
|
9
11
|
client: PgBossClient,
|
|
10
12
|
name: string,
|
|
11
13
|
options: WorkOptions,
|
|
@@ -14,47 +16,34 @@ export function registerWorker(
|
|
|
14
16
|
schema: string,
|
|
15
17
|
maxDeferrals: number,
|
|
16
18
|
context: Context.Context<unknown>,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const workerIds = client
|
|
47
|
-
.getWipData?.()
|
|
48
|
-
.filter((worker) => worker.workId === workId)
|
|
49
|
-
.map((worker) => worker.id) ?? [workId];
|
|
50
|
-
for (const workerId of workerIds) {
|
|
51
|
-
client.notifyWorker?.(workerId);
|
|
52
|
-
}
|
|
53
|
-
}).pipe(Effect.forkIn(scope)),
|
|
54
|
-
);
|
|
55
|
-
},
|
|
56
|
-
Effect.runPromiseWith(context),
|
|
57
|
-
),
|
|
19
|
+
execute: PgBossRunner,
|
|
20
|
+
graceful: boolean,
|
|
21
|
+
) {
|
|
22
|
+
const fibers = yield* Scope.fork(scope);
|
|
23
|
+
function runJob(job: Job<unknown>) {
|
|
24
|
+
return Effect.provide(traceJob(deferrableAttempt(run(job), client, schema, job, maxDeferrals), job, name), context);
|
|
25
|
+
}
|
|
26
|
+
function afterFailure(attempt: Job<unknown>) {
|
|
27
|
+
return retryWake(client, name, attempt.id, registration).pipe(Effect.forkIn(fibers), Effect.asVoid);
|
|
28
|
+
}
|
|
29
|
+
const handle = yield* Effect.try({
|
|
30
|
+
catch: (error) => toPgBossError("register", error, name),
|
|
31
|
+
try: () => batchHandler(client, name, runJob, afterFailure),
|
|
32
|
+
});
|
|
33
|
+
const registration: Promise<string> = yield* Effect.try({
|
|
34
|
+
catch: (error) => toPgBossError("register", error, name),
|
|
35
|
+
try: () =>
|
|
36
|
+
client.work(name, { ...options, includeMetadata: true }, (jobs) =>
|
|
37
|
+
execute(handle(jobs).pipe(Effect.forkIn(fibers), Effect.flatMap(Fiber.join))),
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
const id = yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error, name), try: () => registration });
|
|
41
|
+
yield* Scope.addFinalizer(
|
|
42
|
+
scope,
|
|
43
|
+
Effect.tryPromise({
|
|
44
|
+
catch: (error) => toPgBossError("stop", error, name),
|
|
45
|
+
try: () => client.offWork(name, { id, wait: graceful }),
|
|
46
|
+
}).pipe(Effect.catch(Effect.logError)),
|
|
58
47
|
);
|
|
59
|
-
return
|
|
60
|
-
}
|
|
48
|
+
return id;
|
|
49
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import { redactDiagnosticCause } from "@shivaedev/errors/redactCause.ts";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { toPgBossError } from "#error.ts";
|
|
5
|
+
|
|
6
|
+
export function retryWake(client: PgBossClient, name: string, id: string, registration: Promise<string>): Effect.Effect<void> {
|
|
7
|
+
return Effect.gen(function* () {
|
|
8
|
+
const findJobs = client.findJobs?.bind(client);
|
|
9
|
+
if (findJobs === undefined || client.notifyWorker === undefined) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const jobs = yield* Effect.tryPromise({ catch: (error) => toPgBossError("settle", error, name), try: () => findJobs(name, { id }) });
|
|
13
|
+
const job = jobs[0];
|
|
14
|
+
if (job?.state !== "retry") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const workId = yield* Effect.tryPromise({ catch: (error) => toPgBossError("register", error, name), try: () => registration });
|
|
18
|
+
// Host timers can fire early; PostgreSQL retains precision that Date drops.
|
|
19
|
+
const deadline = job.startAfter.getTime() + 1;
|
|
20
|
+
let remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
21
|
+
while (remaining > 0) {
|
|
22
|
+
yield* Effect.sleep(remaining);
|
|
23
|
+
remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
24
|
+
}
|
|
25
|
+
const workerIds = client
|
|
26
|
+
.getWipData?.()
|
|
27
|
+
.filter((worker) => worker.workId === workId)
|
|
28
|
+
.map((worker) => worker.id) ?? [workId];
|
|
29
|
+
for (const workerId of workerIds) {
|
|
30
|
+
client.notifyWorker(workerId);
|
|
31
|
+
}
|
|
32
|
+
}).pipe(Effect.catchCause((cause) => Effect.logError(redactDiagnosticCause(cause))));
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import { jobContext, type QueueWorker } from "#definition.ts";
|
|
4
|
+
import { PgBossPayloadError } from "#error.ts";
|
|
5
|
+
import { payloadOf } from "#internal/tracing.ts";
|
|
6
|
+
|
|
7
|
+
export function runQueueWorker(worker: QueueWorker, job: Job<unknown>) {
|
|
8
|
+
return Schema.decodeUnknownEffect(worker.queue.schema)(payloadOf(job.data)).pipe(
|
|
9
|
+
Effect.mapError(
|
|
10
|
+
(error) =>
|
|
11
|
+
new PgBossPayloadError({
|
|
12
|
+
direction: "decode",
|
|
13
|
+
original: Redacted.make(error),
|
|
14
|
+
queue: worker.queue.name,
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
Effect.flatMap((payload) => worker.handler(payload, jobContext(job))),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
1
2
|
import type { PgBoss } from "pg-boss";
|
|
2
3
|
|
|
4
|
+
const IndexBuilds = Schema.Array(Schema.Struct({ command: Schema.String, id: Schema.String }));
|
|
5
|
+
const IndexValidity = Schema.Array(Schema.Struct({ invalid: Schema.Boolean }));
|
|
6
|
+
|
|
3
7
|
const BUILD_PREFIX = "effect-pg-boss-schema:";
|
|
4
8
|
|
|
5
9
|
function indexName(command: string): string {
|
|
@@ -30,11 +34,11 @@ export async function resumeSchemaIndexes(client: PgBoss, schema: string): Promi
|
|
|
30
34
|
`SELECT id, command FROM ${schema}.bam WHERE starts_with(name, $1) AND status <> 'completed' ORDER BY created_on, name`,
|
|
31
35
|
[BUILD_PREFIX],
|
|
32
36
|
);
|
|
33
|
-
for (const row of rows) {
|
|
37
|
+
for (const row of Schema.decodeUnknownSync(IndexBuilds)(rows)) {
|
|
34
38
|
const name = indexName(row.command);
|
|
35
39
|
if (/^\s*CREATE /mu.test(row.command)) {
|
|
36
40
|
const probe = await db.executeSql("SELECT NOT indisvalid AS invalid FROM pg_index WHERE indexrelid = to_regclass($1)", [`${schema}.${name}`]);
|
|
37
|
-
if (probe.rows[0]?.invalid === true) {
|
|
41
|
+
if (Schema.decodeUnknownSync(IndexValidity)(probe.rows)[0]?.invalid === true) {
|
|
38
42
|
await db.executeSql(`DROP INDEX CONCURRENTLY IF EXISTS ${schema}."${name}"`);
|
|
39
43
|
}
|
|
40
44
|
}
|
package/src/server/schema.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
3
|
import type { Connection } from "effect/sql/SqlConnection";
|
|
4
|
-
import { getConstructionPlans, getMigrationPlans,
|
|
4
|
+
import { getConstructionPlans, getMigrationPlans, PgBoss } from "pg-boss";
|
|
5
5
|
import pgBossPackage from "pg-boss/package.json" with { type: "json" };
|
|
6
6
|
import type { SchemaComponent } from "@shivaedev/sql/SchemaComponent.ts";
|
|
7
7
|
import { type PgBossError, toPgBossError } from "#error.ts";
|
|
@@ -9,6 +9,15 @@ import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
|
|
|
9
9
|
import { runSchemaPlan } from "#internal/runSchemaPlan.ts";
|
|
10
10
|
import { resumeSchemaIndexes } from "#internal/schemaIndexBuilds.ts";
|
|
11
11
|
|
|
12
|
+
const Partitions = Schema.Array(
|
|
13
|
+
Schema.Struct({
|
|
14
|
+
policy: Schema.String,
|
|
15
|
+
tableName: Schema.String,
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
const InstalledSchema = Schema.Array(Schema.Struct({ name: Schema.NullOr(Schema.String) }));
|
|
19
|
+
const InstalledVersion = Schema.Array(Schema.Struct({ version: Schema.Number }));
|
|
20
|
+
|
|
12
21
|
export interface PgBossSchemaOptions {
|
|
13
22
|
readonly schema?: string;
|
|
14
23
|
}
|
|
@@ -39,10 +48,13 @@ async function installSchema(
|
|
|
39
48
|
await resumeSchemaIndexes(client, schema);
|
|
40
49
|
}
|
|
41
50
|
if (version < pgBossPackage.pgboss.schema) {
|
|
42
|
-
const partitionTables
|
|
51
|
+
const partitionTables = Schema.decodeUnknownSync(Partitions)(
|
|
43
52
|
await run(connection.executeUnprepared(`SELECT table_name AS "tableName", policy FROM ${schema}.queue WHERE partition = true`, [], undefined)),
|
|
44
53
|
);
|
|
45
|
-
await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables }), run, {
|
|
54
|
+
await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables: Array.from(partitionTables) }), run, {
|
|
55
|
+
schema,
|
|
56
|
+
version: pgBossPackage.pgboss.schema,
|
|
57
|
+
});
|
|
46
58
|
await resumeSchemaIndexes(client, schema);
|
|
47
59
|
}
|
|
48
60
|
}
|
|
@@ -72,9 +84,13 @@ export function pgBossSchema(options: PgBossSchemaOptions = {}): SchemaComponent
|
|
|
72
84
|
pending: Effect.gen(function* () {
|
|
73
85
|
const sql = yield* SqlClient.SqlClient;
|
|
74
86
|
const table = sql`${sql(schema)}.${sql("version")}`;
|
|
75
|
-
const [installed] = yield* sql
|
|
87
|
+
const [installed] = yield* sql`select to_regclass(${table.compile()[0]})::text as name`.withoutTransform.pipe(
|
|
88
|
+
Effect.flatMap(Schema.decodeUnknownEffect(InstalledSchema)),
|
|
89
|
+
);
|
|
76
90
|
const [row] =
|
|
77
|
-
installed === undefined || installed.name === null
|
|
91
|
+
installed === undefined || installed.name === null
|
|
92
|
+
? []
|
|
93
|
+
: yield* sql`select version from ${table}`.withoutTransform.pipe(Effect.flatMap(Schema.decodeUnknownEffect(InstalledVersion)));
|
|
78
94
|
const required = pgBossPackage.pgboss.schema;
|
|
79
95
|
return row === undefined || row.version < required
|
|
80
96
|
? `schema ${schema}, installed version ${row?.version ?? "missing"}, required version ${required}`
|
package/src/server/service.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Context, Effect, Layer, type Option, type Schema } from "effect";
|
|
1
|
+
import { Context, Effect, Exit, Layer, type Option, Redacted, type Schema, Scope } from "effect";
|
|
2
2
|
import type { ConstructorOptions, JobWithMetadata, SendOptions, StopOptions } from "pg-boss";
|
|
3
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
4
|
+
import { genericMethod } from "@shivaedev/service/genericMethod.ts";
|
|
3
5
|
import type { PgBossClientFactory } from "#client.ts";
|
|
4
6
|
import type { JobPayloadSchema, JobRegistration, QueueDefinition, RegistrationRequirements } from "#definition.ts";
|
|
5
7
|
import type { PgBossClientOptionsError, PgBossError, PgBossPayloadError, PgBossQueueOptionsError } from "#error.ts";
|
|
@@ -9,13 +11,14 @@ import { acquireClient, releaseClient } from "#internal/lifecycle.ts";
|
|
|
9
11
|
import { makeService } from "#internal/makeService.ts";
|
|
10
12
|
import { registrationName } from "#internal/register.ts";
|
|
11
13
|
import { registerReleasedJobs } from "#internal/registerReleasedJobs.ts";
|
|
14
|
+
import { PgBoss } from "#PgBoss.ts";
|
|
12
15
|
|
|
13
16
|
export interface PgBossService {
|
|
14
17
|
readonly counts: (queue: Pick<QueueDefinition, "name">) => Effect.Effect<QueueCounts, PgBossError>;
|
|
15
18
|
readonly deadLetteredJobs: <const TName extends string, const TPayload extends JobPayloadSchema | typeof Schema.Null>(
|
|
16
19
|
queue: { readonly name: TName; readonly schema: TPayload },
|
|
17
20
|
options: JobPageOptions,
|
|
18
|
-
) => Effect.Effect<JobPage<DeadLetteredJob<TPayload["Type"]>>, PgBossError | PgBossPayloadError, TPayload["DecodingServices"]>;
|
|
21
|
+
) => Effect.Effect<JobPage<DeadLetteredJob<TPayload["Type"]>>, PgBossError | PgBossPayloadError | Schema.SchemaError, TPayload["DecodingServices"]>;
|
|
19
22
|
readonly enqueue: <const TName extends string, const TPayload extends JobPayloadSchema, const TQueue extends QueueDefinition<TName, TPayload>>(
|
|
20
23
|
queue: QueueDefinition<TName, TPayload> & TQueue,
|
|
21
24
|
payload: NoInfer<Schema.Schema.Type<TPayload>>,
|
|
@@ -24,77 +27,109 @@ export interface PgBossService {
|
|
|
24
27
|
readonly failedJobs: <const TName extends string, const TPayload extends JobPayloadSchema | typeof Schema.Null>(
|
|
25
28
|
queue: { readonly name: TName; readonly schema: TPayload },
|
|
26
29
|
options: JobPageOptions,
|
|
27
|
-
) => Effect.Effect<JobPage<ListedJob<TPayload["Type"]>>, PgBossError | PgBossPayloadError, TPayload["DecodingServices"]>;
|
|
30
|
+
) => Effect.Effect<JobPage<ListedJob<TPayload["Type"]>>, PgBossError | PgBossPayloadError | Schema.SchemaError, TPayload["DecodingServices"]>;
|
|
28
31
|
readonly getJob: <const TName extends string, const TPayload extends JobPayloadSchema | typeof Schema.Null>(
|
|
29
32
|
queue: { readonly name: TName; readonly schema: TPayload },
|
|
30
33
|
id: string,
|
|
31
34
|
) => Effect.Effect<Option.Option<JobWithMetadata<TPayload["Type"]>>, PgBossError | PgBossPayloadError, TPayload["DecodingServices"]>;
|
|
32
|
-
readonly health: Effect.Effect<JobsHealth, PgBossError>;
|
|
35
|
+
readonly health: () => Effect.Effect<JobsHealth, PgBossError>;
|
|
33
36
|
readonly retry: (queue: Pick<QueueDefinition, "name">, id: string) => Effect.Effect<void, PgBossError>;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
export type
|
|
39
|
+
export type PgBossOptions = Omit<ConstructorOptions, "connectionString"> & {
|
|
40
|
+
readonly connectionString?: Redacted.Redacted<string>;
|
|
37
41
|
readonly clientFactory?: PgBossClientFactory;
|
|
38
|
-
// One started client is reference-counted per key, so development module reloads reuse it instead of starting another.
|
|
39
42
|
readonly clientCacheKey?: string | symbol | undefined;
|
|
40
|
-
readonly jobs: TRegistrations;
|
|
41
|
-
readonly onError?: (error: Error) => Effect.Effect<unknown, never, TErrorRequirements>;
|
|
42
43
|
readonly stop?: StopOptions;
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
export function pgBossLayer(options: PgBossOptions): Layer.Layer<PgBoss, PgBossError | PgBossClientOptionsError> {
|
|
47
|
+
const { connectionString, clientFactory, clientCacheKey, stop, ...settings } = options;
|
|
48
|
+
return Layer.effect(
|
|
49
|
+
PgBoss,
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const dependencies = yield* Effect.context<never>();
|
|
52
|
+
const execute = Effect.runPromiseWith(dependencies);
|
|
53
|
+
const acquired = yield* Effect.acquireRelease(
|
|
54
|
+
acquireClient(
|
|
55
|
+
{
|
|
56
|
+
clientCacheKey,
|
|
57
|
+
clientFactory,
|
|
58
|
+
constructor: {
|
|
59
|
+
...settings,
|
|
60
|
+
...(connectionString === undefined ? {} : { connectionString: Redacted.value(connectionString) }),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
(error) => {
|
|
64
|
+
void execute(logClientError(error));
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
(client) => releaseClient(client, stop),
|
|
68
|
+
);
|
|
69
|
+
return { ...acquired, schema: settings.schema, stop };
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PgBossLayerOptions<TRegistrations extends readonly JobRegistration[], TErrorRequirements> {
|
|
75
|
+
readonly jobs: TRegistrations;
|
|
76
|
+
readonly onError?: (error: Error) => Effect.Effect<unknown, never, TErrorRequirements>;
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
export interface PgBossDefinition<TIdentifier extends string = string> extends Context.Service<TIdentifier, PgBossService> {
|
|
46
80
|
readonly layer: <const TRegistrations extends readonly JobRegistration[], TErrorRequirements = never>(
|
|
47
81
|
options: PgBossLayerOptions<TRegistrations, TErrorRequirements>,
|
|
48
82
|
) => Layer.Layer<
|
|
49
83
|
TIdentifier,
|
|
50
|
-
PgBossError | PgBossQueueOptionsError
|
|
51
|
-
Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | TErrorRequirements
|
|
84
|
+
PgBossError | PgBossQueueOptionsError,
|
|
85
|
+
PgBoss | Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | TErrorRequirements
|
|
52
86
|
>;
|
|
53
87
|
}
|
|
54
88
|
|
|
55
89
|
export function makePgBoss<const TIdentifier extends string>(identifier: TIdentifier): PgBossDefinition<TIdentifier> {
|
|
56
|
-
|
|
57
|
-
|
|
90
|
+
function definition<R>(options: PgBossLayerOptions<readonly JobRegistration[], R>, dependencies: Context.Context<R>) {
|
|
91
|
+
return defineService({
|
|
92
|
+
id: identifier,
|
|
93
|
+
initialize: Effect.gen(function* () {
|
|
94
|
+
const acquired = yield* PgBoss;
|
|
95
|
+
const execute = Effect.runPromiseWith(dependencies);
|
|
96
|
+
const workerScope = yield* Scope.make();
|
|
97
|
+
const service = makeService(acquired.client, options.jobs.map(registrationName), acquired.schema);
|
|
98
|
+
const context = Context.add(dependencies, Service, service);
|
|
99
|
+
const reportError = options.onError ?? logClientError;
|
|
100
|
+
const unsubscribe = subscribeClientErrors(acquired.client, (error) => {
|
|
101
|
+
void execute(Effect.exit(Effect.provide(reportError(error), context)));
|
|
102
|
+
});
|
|
103
|
+
yield* Effect.addFinalizer(() => Scope.close(workerScope, Exit.void).pipe(Effect.ensuring(Effect.sync(unsubscribe))));
|
|
104
|
+
yield* registerReleasedJobs(
|
|
105
|
+
acquired.client,
|
|
106
|
+
options.jobs,
|
|
107
|
+
context,
|
|
108
|
+
acquired.reused,
|
|
109
|
+
(effect) => execute(effect),
|
|
110
|
+
acquired.schema,
|
|
111
|
+
acquired.stop?.graceful !== false,
|
|
112
|
+
).pipe(Effect.provideService(Scope.Scope, workerScope), Effect.provide(dependencies));
|
|
113
|
+
return service;
|
|
114
|
+
}),
|
|
115
|
+
methods: (service) => ({
|
|
116
|
+
counts: service.counts,
|
|
117
|
+
deadLetteredJobs: genericMethod(service.deadLetteredJobs),
|
|
118
|
+
enqueue: genericMethod(service.enqueue),
|
|
119
|
+
failedJobs: genericMethod(service.failedJobs),
|
|
120
|
+
getJob: genericMethod(service.getJob),
|
|
121
|
+
health: service.health,
|
|
122
|
+
retry: service.retry,
|
|
123
|
+
}),
|
|
124
|
+
requires: [PgBoss],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const Service: Context.Service<TIdentifier, PgBossService> = definition({ jobs: [] }, Context.empty());
|
|
58
128
|
function layer<const TRegistrations extends readonly JobRegistration[], TErrorRequirements = never>(
|
|
59
129
|
options: PgBossLayerOptions<TRegistrations, TErrorRequirements>,
|
|
60
|
-
)
|
|
61
|
-
TIdentifier,
|
|
62
|
-
PgBossError | PgBossQueueOptionsError | PgBossClientOptionsError,
|
|
63
|
-
Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | TErrorRequirements
|
|
64
|
-
> {
|
|
130
|
+
) {
|
|
65
131
|
type Requirements = Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | TErrorRequirements;
|
|
66
|
-
|
|
67
|
-
const { clientFactory, clientCacheKey, jobs, onError, stop, ...constructorOptions } = options;
|
|
68
|
-
|
|
69
|
-
const acquire = Effect.acquireRelease(
|
|
70
|
-
Effect.gen(function* () {
|
|
71
|
-
const dependencies = yield* Effect.context<Requirements>();
|
|
72
|
-
const acquired = yield* acquireClient({
|
|
73
|
-
clientCacheKey,
|
|
74
|
-
clientFactory,
|
|
75
|
-
constructor: constructorOptions,
|
|
76
|
-
});
|
|
77
|
-
const service = makeService(acquired.client, names, constructorOptions.schema);
|
|
78
|
-
const context = Context.add(dependencies, Service, service);
|
|
79
|
-
const reportError: (error: Error) => Effect.Effect<unknown, never, Requirements | TIdentifier> = onError ?? logClientError;
|
|
80
|
-
function errorListener(error: Error): void {
|
|
81
|
-
void Effect.runPromise(Effect.exit(Effect.provide(reportError(error), context)));
|
|
82
|
-
}
|
|
83
|
-
const unsubscribe = subscribeClientErrors(acquired.client, errorListener);
|
|
84
|
-
const registration = registerReleasedJobs(acquired.client, jobs, context, acquired.reused, constructorOptions.schema);
|
|
85
|
-
return yield* registration.pipe(
|
|
86
|
-
Effect.as({ acquired, service, unsubscribe }),
|
|
87
|
-
Effect.onError(() => releaseClient(acquired, stop).pipe(Effect.ensuring(Effect.sync(unsubscribe)))),
|
|
88
|
-
);
|
|
89
|
-
}),
|
|
90
|
-
(resource) => releaseClient(resource.acquired, stop).pipe(Effect.ensuring(Effect.sync(resource.unsubscribe))),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
return Layer.effect(
|
|
94
|
-
Service,
|
|
95
|
-
Effect.map(acquire, ({ service }) => service),
|
|
96
|
-
);
|
|
132
|
+
return Layer.unwrap(Effect.map(Effect.context<Requirements>(), (dependencies) => definition(options, dependencies).layer));
|
|
97
133
|
}
|
|
98
|
-
|
|
99
134
|
return Object.assign(Service, { layer });
|
|
100
135
|
}
|