@shivaedev/jobs 0.22.1 → 0.23.0

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +47 -53
  3. package/dist/JobFailure.d.ts +7 -0
  4. package/dist/JobFailure.d.ts.map +1 -0
  5. package/dist/JobFailure.js +8 -0
  6. package/dist/JobFailure.js.map +1 -0
  7. package/dist/server/PgBoss.d.ts +11 -0
  8. package/dist/server/PgBoss.d.ts.map +1 -0
  9. package/dist/server/PgBoss.js +4 -0
  10. package/dist/server/PgBoss.js.map +1 -0
  11. package/dist/server/client.d.ts +1 -1
  12. package/dist/server/client.d.ts.map +1 -1
  13. package/dist/server/health.d.ts +3 -2
  14. package/dist/server/health.d.ts.map +1 -1
  15. package/dist/server/health.js.map +1 -1
  16. package/dist/server/internal/batchHandler.d.ts +2 -1
  17. package/dist/server/internal/batchHandler.d.ts.map +1 -1
  18. package/dist/server/internal/batchHandler.js +49 -31
  19. package/dist/server/internal/batchHandler.js.map +1 -1
  20. package/dist/server/internal/clientErrors.d.ts.map +1 -1
  21. package/dist/server/internal/clientErrors.js +2 -1
  22. package/dist/server/internal/clientErrors.js.map +1 -1
  23. package/dist/server/internal/deferrableAttempt.d.ts +1 -1
  24. package/dist/server/internal/deferrableAttempt.d.ts.map +1 -1
  25. package/dist/server/internal/deferrableAttempt.js +5 -1
  26. package/dist/server/internal/deferrableAttempt.js.map +1 -1
  27. package/dist/server/internal/ensureQueue.d.ts +2 -1
  28. package/dist/server/internal/ensureQueue.d.ts.map +1 -1
  29. package/dist/server/internal/ensureQueue.js +16 -21
  30. package/dist/server/internal/ensureQueue.js.map +1 -1
  31. package/dist/server/internal/failureOutput.d.ts +4 -0
  32. package/dist/server/internal/failureOutput.d.ts.map +1 -0
  33. package/dist/server/internal/failureOutput.js +9 -0
  34. package/dist/server/internal/failureOutput.js.map +1 -0
  35. package/dist/server/internal/lifecycle.d.ts +1 -1
  36. package/dist/server/internal/lifecycle.d.ts.map +1 -1
  37. package/dist/server/internal/lifecycle.js +10 -16
  38. package/dist/server/internal/lifecycle.js.map +1 -1
  39. package/dist/server/internal/listJobs.d.ts.map +1 -1
  40. package/dist/server/internal/listJobs.js +44 -39
  41. package/dist/server/internal/listJobs.js.map +1 -1
  42. package/dist/server/internal/makeService.js +1 -1
  43. package/dist/server/internal/makeService.js.map +1 -1
  44. package/dist/server/internal/register.d.ts +4 -3
  45. package/dist/server/internal/register.d.ts.map +1 -1
  46. package/dist/server/internal/register.js +34 -48
  47. package/dist/server/internal/register.js.map +1 -1
  48. package/dist/server/internal/registerReleasedJobs.d.ts +2 -1
  49. package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -1
  50. package/dist/server/internal/registerReleasedJobs.js +3 -3
  51. package/dist/server/internal/registerReleasedJobs.js.map +1 -1
  52. package/dist/server/internal/registerWorker.d.ts +3 -2
  53. package/dist/server/internal/registerWorker.d.ts.map +1 -1
  54. package/dist/server/internal/registerWorker.js +26 -33
  55. package/dist/server/internal/registerWorker.js.map +1 -1
  56. package/dist/server/internal/retryWake.d.ts +4 -0
  57. package/dist/server/internal/retryWake.d.ts.map +1 -0
  58. package/dist/server/internal/retryWake.js +32 -0
  59. package/dist/server/internal/retryWake.js.map +1 -0
  60. package/dist/server/internal/runQueueWorker.d.ts +5 -0
  61. package/dist/server/internal/runQueueWorker.d.ts.map +1 -0
  62. package/dist/server/internal/runQueueWorker.js +12 -0
  63. package/dist/server/internal/runQueueWorker.js.map +1 -0
  64. package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -1
  65. package/dist/server/internal/schemaIndexBuilds.js +5 -2
  66. package/dist/server/internal/schemaIndexBuilds.js.map +1 -1
  67. package/dist/server/schema.d.ts.map +1 -1
  68. package/dist/server/schema.js +16 -5
  69. package/dist/server/schema.js.map +1 -1
  70. package/dist/server/service.d.ts +13 -8
  71. package/dist/server/service.d.ts.map +1 -1
  72. package/dist/server/service.js +53 -22
  73. package/dist/server/service.js.map +1 -1
  74. package/package.json +14 -8
  75. package/src/JobFailure.ts +9 -0
  76. package/src/server/PgBoss.ts +8 -0
  77. package/src/server/client.ts +1 -1
  78. package/src/server/health.ts +3 -2
  79. package/src/server/internal/batchHandler.ts +61 -42
  80. package/src/server/internal/clientErrors.ts +2 -1
  81. package/src/server/internal/deferrableAttempt.ts +5 -1
  82. package/src/server/internal/ensureQueue.ts +18 -20
  83. package/src/server/internal/failureOutput.ts +9 -0
  84. package/src/server/internal/lifecycle.ts +13 -16
  85. package/src/server/internal/listJobs.ts +57 -47
  86. package/src/server/internal/makeService.ts +1 -1
  87. package/src/server/internal/register.ts +68 -66
  88. package/src/server/internal/registerReleasedJobs.ts +8 -3
  89. package/src/server/internal/registerWorker.ts +35 -46
  90. package/src/server/internal/retryWake.ts +33 -0
  91. package/src/server/internal/runQueueWorker.ts +19 -0
  92. package/src/server/internal/schemaIndexBuilds.ts +6 -2
  93. package/src/server/schema.ts +22 -6
  94. 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
- schema?: string,
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(Effect.provideService(Scope.Scope, scope));
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 { Clock, type Context, Effect, type Scope } from "effect";
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 { batchHandler } from "#internal/batchHandler.ts";
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 function registerWorker(
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
- ): Promise<string> {
18
- const registration = client.work(
19
- name,
20
- { ...options, includeMetadata: true },
21
- batchHandler(
22
- client,
23
- name,
24
- (job) => {
25
- const attempt = traceJob(deferrableAttempt(run(job), client, schema, job, maxDeferrals), job, name);
26
- return Effect.provide(attempt, context);
27
- },
28
- async (attempt) => {
29
- if (client.findJobs === undefined || client.notifyWorker === undefined) {
30
- return;
31
- }
32
- const job = (await client.findJobs(name, { id: attempt.id }))[0];
33
- if (job?.state !== "retry") {
34
- return;
35
- }
36
- const workId = await registration;
37
- await Effect.runPromise(
38
- Effect.gen(function* () {
39
- // Host timers can fire early; PostgreSQL also retains precision that Date drops.
40
- const deadline = job.startAfter.getTime() + 1;
41
- let remaining = deadline - (yield* Clock.currentTimeMillis);
42
- while (remaining > 0) {
43
- yield* Effect.sleep(remaining);
44
- remaining = deadline - (yield* Clock.currentTimeMillis);
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 registration;
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
  }
@@ -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, type MigrationPartition, PgBoss } from "pg-boss";
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: MigrationPartition[] = Array.from(
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, { schema, version: pgBossPackage.pgboss.schema });
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<{ name: string | null }>`select to_regclass(${table.compile()[0]})::text as name`.withoutTransform;
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 ? [] : yield* sql<{ version: number }>`select version from ${table}`.withoutTransform;
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}`
@@ -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 PgBossLayerOptions<TRegistrations extends readonly JobRegistration[], TErrorRequirements> = ConstructorOptions & {
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 | PgBossClientOptionsError,
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
- const Service = Context.Service<TIdentifier, PgBossService>(identifier);
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
- ): Layer.Layer<
61
- TIdentifier,
62
- PgBossError | PgBossQueueOptionsError | PgBossClientOptionsError,
63
- Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | TErrorRequirements
64
- > {
130
+ ) {
65
131
  type Requirements = Exclude<RegistrationRequirements<TRegistrations>, TIdentifier> | TErrorRequirements;
66
- const names = options.jobs.map(registrationName);
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
  }