@shivaedev/jobs 0.25.0 → 0.26.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 (157) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +44 -19
  3. package/dist/server/adapters/clientErrorReporter.d.ts +3 -0
  4. package/dist/server/adapters/clientErrorReporter.d.ts.map +1 -0
  5. package/dist/server/adapters/clientErrorReporter.js +8 -0
  6. package/dist/server/adapters/clientErrorReporter.js.map +1 -0
  7. package/dist/server/adapters/installSchema.d.ts +5 -0
  8. package/dist/server/adapters/installSchema.d.ts.map +1 -0
  9. package/dist/server/adapters/installSchema.js +57 -0
  10. package/dist/server/adapters/installSchema.js.map +1 -0
  11. package/dist/server/adapters/registerWorker.d.ts +5 -0
  12. package/dist/server/adapters/registerWorker.d.ts.map +1 -0
  13. package/dist/server/{internal → adapters}/registerWorker.js +3 -2
  14. package/dist/server/adapters/registerWorker.js.map +1 -0
  15. package/dist/server/{internal → adapters}/runSchemaPlan.d.ts.map +1 -1
  16. package/dist/server/{internal → adapters}/runSchemaPlan.js.map +1 -1
  17. package/dist/server/{internal → adapters}/schemaIndexBuilds.d.ts.map +1 -1
  18. package/dist/server/{internal → adapters}/schemaIndexBuilds.js.map +1 -1
  19. package/dist/server/{internal → adapters}/testingExpirations.d.ts.map +1 -1
  20. package/dist/server/{internal → adapters}/testingExpirations.js.map +1 -1
  21. package/dist/server/{internal → adapters}/transactionDb.d.ts.map +1 -1
  22. package/dist/server/{internal → adapters}/transactionDb.js.map +1 -1
  23. package/dist/server/adapters/workerDiagnostics.d.ts +3 -0
  24. package/dist/server/adapters/workerDiagnostics.d.ts.map +1 -0
  25. package/dist/server/adapters/workerDiagnostics.js +24 -0
  26. package/dist/server/adapters/workerDiagnostics.js.map +1 -0
  27. package/dist/server/client.d.ts +0 -1
  28. package/dist/server/client.d.ts.map +1 -1
  29. package/dist/server/client.js +1 -2
  30. package/dist/server/client.js.map +1 -1
  31. package/dist/server/defaultClientFactory.d.ts +3 -0
  32. package/dist/server/defaultClientFactory.d.ts.map +1 -0
  33. package/dist/server/{internal/nativeClient.js → defaultClientFactory.js} +2 -2
  34. package/dist/server/defaultClientFactory.js.map +1 -0
  35. package/dist/server/defineSchedule.d.ts +3 -0
  36. package/dist/server/defineSchedule.d.ts.map +1 -0
  37. package/dist/server/{internal/scheduleDefinition.js → defineSchedule.js} +2 -2
  38. package/dist/server/defineSchedule.js.map +1 -0
  39. package/dist/server/definition.d.ts +0 -1
  40. package/dist/server/definition.d.ts.map +1 -1
  41. package/dist/server/definition.js +0 -4
  42. package/dist/server/definition.js.map +1 -1
  43. package/dist/server/error.d.ts +11 -1
  44. package/dist/server/error.d.ts.map +1 -1
  45. package/dist/server/error.js +6 -1
  46. package/dist/server/error.js.map +1 -1
  47. package/dist/server/health.d.ts +1 -0
  48. package/dist/server/health.d.ts.map +1 -1
  49. package/dist/server/internal/adapters/testingFetches.d.ts +7 -0
  50. package/dist/server/internal/adapters/testingFetches.d.ts.map +1 -0
  51. package/dist/server/internal/adapters/testingFetches.js +30 -0
  52. package/dist/server/internal/adapters/testingFetches.js.map +1 -0
  53. package/dist/server/internal/batchHandler.d.ts +1 -2
  54. package/dist/server/internal/batchHandler.d.ts.map +1 -1
  55. package/dist/server/internal/batchHandler.js +9 -1
  56. package/dist/server/internal/batchHandler.js.map +1 -1
  57. package/dist/server/internal/deadLetterResolutionPlan.d.ts +2 -0
  58. package/dist/server/internal/deadLetterResolutionPlan.d.ts.map +1 -0
  59. package/dist/server/internal/deadLetterResolutionPlan.js +28 -0
  60. package/dist/server/internal/deadLetterResolutionPlan.js.map +1 -0
  61. package/dist/server/internal/enqueueFor.d.ts.map +1 -1
  62. package/dist/server/internal/enqueueFor.js +2 -2
  63. package/dist/server/internal/enqueueFor.js.map +1 -1
  64. package/dist/server/internal/jobControl.d.ts.map +1 -1
  65. package/dist/server/internal/jobControl.js +2 -1
  66. package/dist/server/internal/jobControl.js.map +1 -1
  67. package/dist/server/internal/jobStorePlans.d.ts +2 -0
  68. package/dist/server/internal/jobStorePlans.d.ts.map +1 -0
  69. package/dist/server/internal/jobStorePlans.js +6 -0
  70. package/dist/server/internal/jobStorePlans.js.map +1 -0
  71. package/dist/server/internal/lifecycle.d.ts +1 -1
  72. package/dist/server/internal/lifecycle.d.ts.map +1 -1
  73. package/dist/server/internal/lifecycle.js +5 -3
  74. package/dist/server/internal/lifecycle.js.map +1 -1
  75. package/dist/server/internal/listJobs.d.ts.map +1 -1
  76. package/dist/server/internal/listJobs.js +3 -2
  77. package/dist/server/internal/listJobs.js.map +1 -1
  78. package/dist/server/internal/register.d.ts +1 -2
  79. package/dist/server/internal/register.d.ts.map +1 -1
  80. package/dist/server/internal/register.js +8 -8
  81. package/dist/server/internal/register.js.map +1 -1
  82. package/dist/server/internal/registerReleasedJobs.d.ts +1 -2
  83. package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -1
  84. package/dist/server/internal/registerReleasedJobs.js +3 -3
  85. package/dist/server/internal/registerReleasedJobs.js.map +1 -1
  86. package/dist/server/internal/runQueueWorker.d.ts.map +1 -1
  87. package/dist/server/internal/runQueueWorker.js +3 -2
  88. package/dist/server/internal/runQueueWorker.js.map +1 -1
  89. package/dist/server/internal/storedRows.js +1 -1
  90. package/dist/server/internal/storedRows.js.map +1 -1
  91. package/dist/server/internal/testingClient.d.ts.map +1 -1
  92. package/dist/server/internal/testingClient.js +1 -1
  93. package/dist/server/internal/testingClient.js.map +1 -1
  94. package/dist/server/internal/testingWaits.d.ts +16 -0
  95. package/dist/server/internal/testingWaits.d.ts.map +1 -0
  96. package/dist/server/internal/testingWaits.js +56 -0
  97. package/dist/server/internal/testingWaits.js.map +1 -0
  98. package/dist/server/schema.d.ts.map +1 -1
  99. package/dist/server/schema.js +11 -53
  100. package/dist/server/schema.js.map +1 -1
  101. package/dist/server/service.d.ts +3 -3
  102. package/dist/server/service.d.ts.map +1 -1
  103. package/dist/server/service.js +6 -10
  104. package/dist/server/service.js.map +1 -1
  105. package/dist/server/testing.d.ts +5 -0
  106. package/dist/server/testing.d.ts.map +1 -1
  107. package/dist/server/testing.js +7 -2
  108. package/dist/server/testing.js.map +1 -1
  109. package/package.json +4 -4
  110. package/src/server/adapters/clientErrorReporter.ts +8 -0
  111. package/src/server/adapters/installSchema.ts +68 -0
  112. package/src/server/{internal → adapters}/registerWorker.ts +6 -7
  113. package/src/server/adapters/workerDiagnostics.ts +28 -0
  114. package/src/server/client.ts +0 -3
  115. package/src/server/{internal/nativeClient.ts → defaultClientFactory.ts} +1 -1
  116. package/src/server/{internal/scheduleDefinition.ts → defineSchedule.ts} +1 -1
  117. package/src/server/definition.ts +0 -5
  118. package/src/server/error.ts +11 -1
  119. package/src/server/health.ts +1 -0
  120. package/src/server/internal/adapters/testingFetches.ts +36 -0
  121. package/src/server/internal/batchHandler.ts +11 -8
  122. package/src/server/internal/deadLetterResolutionPlan.ts +28 -0
  123. package/src/server/internal/enqueueFor.ts +2 -2
  124. package/src/server/internal/jobControl.ts +2 -1
  125. package/src/server/internal/jobStorePlans.ts +6 -0
  126. package/src/server/internal/lifecycle.ts +6 -3
  127. package/src/server/internal/listJobs.ts +11 -3
  128. package/src/server/internal/register.ts +6 -21
  129. package/src/server/internal/registerReleasedJobs.ts +2 -6
  130. package/src/server/internal/runQueueWorker.ts +3 -1
  131. package/src/server/internal/storedRows.ts +1 -1
  132. package/src/server/internal/testingClient.ts +1 -1
  133. package/src/server/internal/testingWaits.ts +98 -0
  134. package/src/server/schema.ts +14 -65
  135. package/src/server/service.ts +12 -21
  136. package/src/server/testing.ts +11 -2
  137. package/dist/server/internal/nativeClient.d.ts +0 -3
  138. package/dist/server/internal/nativeClient.d.ts.map +0 -1
  139. package/dist/server/internal/nativeClient.js.map +0 -1
  140. package/dist/server/internal/registerWorker.d.ts +0 -6
  141. package/dist/server/internal/registerWorker.d.ts.map +0 -1
  142. package/dist/server/internal/registerWorker.js.map +0 -1
  143. package/dist/server/internal/scheduleDefinition.d.ts +0 -3
  144. package/dist/server/internal/scheduleDefinition.d.ts.map +0 -1
  145. package/dist/server/internal/scheduleDefinition.js.map +0 -1
  146. /package/dist/server/{internal → adapters}/runSchemaPlan.d.ts +0 -0
  147. /package/dist/server/{internal → adapters}/runSchemaPlan.js +0 -0
  148. /package/dist/server/{internal → adapters}/schemaIndexBuilds.d.ts +0 -0
  149. /package/dist/server/{internal → adapters}/schemaIndexBuilds.js +0 -0
  150. /package/dist/server/{internal → adapters}/testingExpirations.d.ts +0 -0
  151. /package/dist/server/{internal → adapters}/testingExpirations.js +0 -0
  152. /package/dist/server/{internal → adapters}/transactionDb.d.ts +0 -0
  153. /package/dist/server/{internal → adapters}/transactionDb.js +0 -0
  154. /package/src/server/{internal → adapters}/runSchemaPlan.ts +0 -0
  155. /package/src/server/{internal → adapters}/schemaIndexBuilds.ts +0 -0
  156. /package/src/server/{internal → adapters}/testingExpirations.ts +0 -0
  157. /package/src/server/{internal → adapters}/transactionDb.ts +0 -0
@@ -0,0 +1,68 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import type { Connection } from "effect/sql/SqlConnection";
4
+ import { getConstructionPlans, getMigrationPlans, PgBoss } from "pg-boss";
5
+ import pgBossPackage from "pg-boss/package.json" with { type: "json" };
6
+ import { jobStorePlans } from "#internal/jobStorePlans.ts";
7
+ import type { PgBossSchemaOptions } from "#schema.ts";
8
+ import { runSchemaPlan } from "./runSchemaPlan.ts";
9
+ import { resumeSchemaIndexes } from "./schemaIndexBuilds.ts";
10
+
11
+ const Partitions = Schema.Array(
12
+ Schema.Struct({
13
+ policy: Schema.String,
14
+ tableName: Schema.String,
15
+ }),
16
+ );
17
+ async function installNativeSchema(
18
+ connection: Connection,
19
+ options: PgBossSchemaOptions,
20
+ run: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>,
21
+ ): Promise<void> {
22
+ const schema = options.schema ?? "pgboss";
23
+ const construction = getConstructionPlans(schema);
24
+ const client = new PgBoss({
25
+ db: { executeSql: async (text, values = []) => ({ rows: Array.from(await run(connection.executeUnprepared(text, values, undefined))) }) },
26
+ schema,
27
+ });
28
+ if (!(await client.isInstalled())) {
29
+ await runSchemaPlan(connection, construction, run);
30
+ return;
31
+ }
32
+ const version = await client.schemaVersion();
33
+ if (version === null) {
34
+ throw new Error("The pg-boss version table is empty");
35
+ }
36
+ if (version > pgBossPackage.pgboss.schema) {
37
+ return;
38
+ }
39
+ if (version >= 27) {
40
+ await resumeSchemaIndexes(client, schema);
41
+ }
42
+ if (version < pgBossPackage.pgboss.schema) {
43
+ const partitionTables = Schema.decodeUnknownSync(Partitions)(
44
+ await run(connection.executeUnprepared(`SELECT table_name AS "tableName", policy FROM ${schema}.queue WHERE partition = true`, [], undefined)),
45
+ );
46
+ await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables: Array.from(partitionTables) }), run, {
47
+ schema,
48
+ version: pgBossPackage.pgboss.schema,
49
+ });
50
+ await resumeSchemaIndexes(client, schema);
51
+ }
52
+ }
53
+
54
+ export const installSchema = Effect.fnUntraced(function* (options: PgBossSchemaOptions) {
55
+ const sql = yield* SqlClient.SqlClient;
56
+ const connection = yield* sql.reserve;
57
+ const run = Effect.runPromiseWith(yield* Effect.context<never>());
58
+ yield* Effect.tryPromise({
59
+ catch: (error) => error,
60
+ try: async (signal) => {
61
+ const execute: typeof run = (effect) => run(effect, { signal });
62
+ await installNativeSchema(connection, options, execute);
63
+ },
64
+ });
65
+ for (const plan of jobStorePlans(options.schema ?? "pgboss")) {
66
+ yield* connection.executeRaw(plan, []);
67
+ }
68
+ });
@@ -1,27 +1,26 @@
1
- import { type Context, Effect, Fiber, Scope } from "effect";
1
+ import { Effect, Fiber, Scope } from "effect";
2
2
  import type { Job, WorkOptions } from "pg-boss";
3
3
  import type { PgBossClient } from "#client.ts";
4
4
  import { toPgBossError } from "#error.ts";
5
- import { batchHandler, type PgBossRunner } from "#internal/batchHandler.ts";
5
+ import { batchHandler } from "#internal/batchHandler.ts";
6
6
  import { deferrableAttempt } from "#internal/deferrableAttempt.ts";
7
7
  import { retryWake } from "#internal/retryWake.ts";
8
8
  import { traceJob } from "#internal/tracing.ts";
9
9
 
10
- export const registerWorker = Effect.fnUntraced(function* (
10
+ export const registerWorker = Effect.fnUntraced(function* <R>(
11
11
  client: PgBossClient,
12
12
  name: string,
13
13
  options: WorkOptions,
14
- run: (job: Job<unknown>, deadline: number) => Effect.Effect<unknown, unknown, unknown>,
14
+ run: (job: Job<unknown>, deadline: number) => Effect.Effect<unknown, unknown, R>,
15
15
  scope: Scope.Scope,
16
16
  schema: string,
17
17
  maxDeferrals: number,
18
- context: Context.Context<unknown>,
19
- execute: PgBossRunner,
20
18
  graceful: boolean,
21
19
  ) {
20
+ const execute = Effect.runPromiseWith(yield* Effect.context<R>());
22
21
  const fibers = yield* Scope.fork(scope);
23
22
  function runJob(job: Job<unknown>, deadline: number) {
24
- return Effect.provide(traceJob(deferrableAttempt(run(job, deadline), client, schema, job, maxDeferrals), job, name), context);
23
+ return traceJob(deferrableAttempt(run(job, deadline), client, schema, job, maxDeferrals), job, name);
25
24
  }
26
25
  function afterFailure(attempt: Job<unknown>) {
27
26
  return retryWake(client, name, attempt.id, registration).pipe(Effect.forkIn(fibers), Effect.asVoid);
@@ -0,0 +1,28 @@
1
+ import { Context, Effect, Logger, References } from "effect";
2
+ import { redact } from "@shivaedev/errors/redact.ts";
3
+ import { redactDiagnosticCause } from "@shivaedev/errors/redactCause.ts";
4
+ import { isSensitiveKey, REDACTED } from "@shivaedev/errors/sensitive.ts";
5
+
6
+ function maskedLogger(logger: Logger.Logger<unknown, unknown>) {
7
+ return Logger.make((options) => {
8
+ const context = options.fiber.context;
9
+ const annotations = Object.fromEntries(
10
+ Object.entries(options.fiber.getRef(References.CurrentLogAnnotations)).map(([key, value]) => [
11
+ key,
12
+ isSensitiveKey(key) ? REDACTED : redact(value),
13
+ ]),
14
+ );
15
+ options.fiber.setContext(Context.add(context, References.CurrentLogAnnotations, annotations));
16
+ try {
17
+ return logger.log({ ...options, cause: redactDiagnosticCause(options.cause), message: redact(options.message) });
18
+ } finally {
19
+ options.fiber.setContext(context);
20
+ }
21
+ });
22
+ }
23
+
24
+ export function workerDiagnostics<A, E, R>(effect: Effect.Effect<A, E, R>) {
25
+ return Effect.flatMap(Logger.CurrentLoggers, (loggers) =>
26
+ Effect.provideService(effect, Logger.CurrentLoggers, new Set([...loggers].map(maskedLogger))),
27
+ );
28
+ }
@@ -11,7 +11,6 @@ import type {
11
11
  StopOptions,
12
12
  WorkOptions,
13
13
  } from "pg-boss";
14
- import { nativeClient } from "#internal/nativeClient.ts";
15
14
 
16
15
  export interface PgBossClient {
17
16
  readonly complete?: PgBoss["complete"];
@@ -36,5 +35,3 @@ export interface PgBossClient {
36
35
  }
37
36
 
38
37
  export type PgBossClientFactory = (options: ConstructorOptions) => PgBossClient;
39
-
40
- export const defaultClientFactory: PgBossClientFactory = nativeClient;
@@ -1,7 +1,7 @@
1
1
  import { Socket } from "node:net";
2
2
  import { type ConstructorOptions, PgBoss } from "pg-boss";
3
3
 
4
- export function nativeClient(options: ConstructorOptions) {
4
+ export function defaultClientFactory(options: ConstructorOptions) {
5
5
  const disconnected = new Set<Promise<void>>();
6
6
  const config = {
7
7
  ...options,
@@ -1,7 +1,7 @@
1
1
  import type { DefineScheduleOptions, ScheduleDefinition } from "#definition.ts";
2
2
  import { maxDeferrals, nativeQueueOptions } from "#internal/definitionOptions.ts";
3
3
 
4
- export function scheduleDefinition<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
4
+ export function defineSchedule<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
5
5
  const definition: ScheduleDefinition<TName> = {
6
6
  _tag: "ScheduleDefinition",
7
7
  cron: options.cron,
@@ -1,7 +1,6 @@
1
1
  import { Clock, Duration, Effect, Schema } from "effect";
2
2
  import type { Job, policies, Queue, ScheduleOptions, WorkOptions } from "pg-boss";
3
3
  import { maxDeferrals, nativeQueueOptions } from "#internal/definitionOptions.ts";
4
- import { scheduleDefinition } from "#internal/scheduleDefinition.ts";
5
4
 
6
5
  export class RetryAt extends Schema.TaggedError<RetryAt>()("RetryAt", {
7
6
  at: Schema.DateTimeUtc,
@@ -123,10 +122,6 @@ export interface DefineScheduleOptions<TName extends string> {
123
122
  readonly worker?: WorkOptions;
124
123
  }
125
124
 
126
- export function defineSchedule<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
127
- return scheduleDefinition(options);
128
- }
129
-
130
125
  export function jobContext(job: Job<unknown>, deadline: number): JobContext {
131
126
  return {
132
127
  expireInSeconds: job.expireInSeconds,
@@ -1,4 +1,4 @@
1
- import { Cause, Data, Redacted } from "effect";
1
+ import { Cause, Data, Duration, Redacted } from "effect";
2
2
  import { failureOutput } from "#internal/failureOutput.ts";
3
3
 
4
4
  export type PgBossOperation =
@@ -76,6 +76,16 @@ export class PgBossTestingError extends Data.TaggedError("PgBossTestingError")<{
76
76
  readonly queue?: string | undefined;
77
77
  }> {}
78
78
 
79
+ export class PgBossWaitError extends Data.TaggedError("PgBossWaitError")<{
80
+ readonly lastSeen: string;
81
+ readonly waitingFor: string;
82
+ readonly within: Duration.Duration;
83
+ }> {
84
+ override get message(): string {
85
+ return `Gave up after ${Duration.format(this.within)} waiting until ${this.waitingFor}; last seen: ${this.lastSeen}`;
86
+ }
87
+ }
88
+
79
89
  export function toPgBossError(operation: PgBossOperation, error: unknown, queue?: string): PgBossError {
80
90
  return new PgBossError({
81
91
  operation,
@@ -48,6 +48,7 @@ export interface ListedJob<TData> {
48
48
  export interface DeadLetteredJob<TData> extends ListedJob<TData> {
49
49
  readonly sourceId: Option.Option<string>;
50
50
  readonly sourceOutput: JobFailure | null;
51
+ readonly sourceQueue: string;
51
52
  }
52
53
 
53
54
  export interface JobPageOptions {
@@ -0,0 +1,36 @@
1
+ import type { PgBoss } from "pg-boss";
2
+
3
+ // pg-boss offers no hook for a worker fetch that finds nothing, so the controller recognizes its claim statement on the client's database.
4
+ const claim = /UPDATE\s+\S+\s+j\s+SET\s+state\s*=\s*'active'/u;
5
+ const claimedQueue = /\bj\.name\s*=\s*'(?<queue>[^']+)'/u;
6
+
7
+ export interface TestingFetches {
8
+ readonly begun: (queue: string) => number;
9
+ readonly passedSince: (queue: string, begun: number) => number;
10
+ }
11
+
12
+ export function testingFetches(db: ReturnType<PgBoss["getDb"]>, changed: () => void): TestingFetches {
13
+ const begun = new Map<string, number>();
14
+ const passed = new Map<string, number[]>();
15
+ const executeSql = db.executeSql.bind(db);
16
+ db.executeSql = async (text: string, values?: unknown[]) => {
17
+ const queue = claim.test(text) ? claimedQueue.exec(text)?.groups?.queue : undefined;
18
+ if (queue === undefined) {
19
+ return executeSql(text, values);
20
+ }
21
+ const ordinal = (begun.get(queue) ?? 0) + 1;
22
+ begun.set(queue, ordinal);
23
+ try {
24
+ return await executeSql(text, values);
25
+ } finally {
26
+ const ordinals = passed.get(queue) ?? [];
27
+ ordinals.push(ordinal);
28
+ passed.set(queue, ordinals);
29
+ changed();
30
+ }
31
+ };
32
+ return {
33
+ begun: (queue) => begun.get(queue) ?? 0,
34
+ passedSince: (queue, mark) => (passed.get(queue) ?? []).filter((ordinal) => ordinal > mark).length,
35
+ };
36
+ }
@@ -24,7 +24,14 @@ function settleAttempt(
24
24
  });
25
25
  }
26
26
 
27
- export type PgBossRunner = <A, E>(effect: Effect.Effect<A, E>) => Promise<A>;
27
+ const interruptAt = Effect.fnUntraced(function* (deadline: number) {
28
+ let remaining = deadline - (yield* Clock.currentTimeMillis);
29
+ while (remaining > 0) {
30
+ yield* Effect.sleep(remaining);
31
+ remaining = deadline - (yield* Clock.currentTimeMillis);
32
+ }
33
+ return yield* Effect.interrupt;
34
+ });
28
35
 
29
36
  function aborted(signal: AbortSignal) {
30
37
  return Effect.callback<never>((resume) => {
@@ -40,11 +47,11 @@ function aborted(signal: AbortSignal) {
40
47
  });
41
48
  }
42
49
 
43
- export function batchHandler(
50
+ export function batchHandler<R>(
44
51
  client: PgBossClient,
45
52
  name: string,
46
53
  schema: string,
47
- run: (job: Job<unknown>, deadline: number) => Effect.Effect<unknown, unknown>,
54
+ run: (job: Job<unknown>, deadline: number) => Effect.Effect<unknown, unknown, R>,
48
55
  afterFailure: (job: Job<unknown>) => Effect.Effect<void, unknown> = () => Effect.void,
49
56
  ) {
50
57
  const complete = client.complete?.bind(client);
@@ -61,11 +68,7 @@ export function batchHandler(
61
68
  if (remaining <= 0) {
62
69
  return "expired";
63
70
  }
64
- const exit = yield* run(job, deadline).pipe(
65
- Effect.timeoutOrElse({ duration: remaining, orElse: () => Effect.interrupt }),
66
- Effect.raceFirst(aborted(job.signal)),
67
- Effect.exit,
68
- );
71
+ const exit = yield* run(job, deadline).pipe(Effect.raceFirst(interruptAt(deadline)), Effect.raceFirst(aborted(job.signal)), Effect.exit);
69
72
  if (job.signal.aborted || (Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause))) {
70
73
  return "interrupted";
71
74
  }
@@ -0,0 +1,28 @@
1
+ import { quotedSchema } from "#internal/storedRows.ts";
2
+
3
+ export function deadLetterResolutionPlan(schema: string): string {
4
+ const quoted = quotedSchema(schema);
5
+ const jobTable = `${quoted}.job`.replaceAll("'", "''");
6
+ const body = `BEGIN
7
+ PERFORM pg_advisory_xact_lock(hashtextextended('${jobTable}.effect_dead_letter_resolution', 0));
8
+ CREATE OR REPLACE FUNCTION ${quoted}.effect_dead_letter_resolution() RETURNS trigger LANGUAGE plpgsql AS $resolve$
9
+ BEGIN
10
+ DELETE FROM ${quoted}.job
11
+ WHERE name = NEW.name || '-dlq' AND source_name = NEW.name AND source_id = NEW.id
12
+ AND source_created_on = NEW.created_on;
13
+ RETURN NEW;
14
+ END
15
+ $resolve$;
16
+ IF NOT EXISTS (
17
+ SELECT 1 FROM pg_trigger WHERE tgrelid = '${jobTable}'::regclass AND tgname = 'effect_dead_letter_resolution'
18
+ ) THEN
19
+ CREATE TRIGGER effect_dead_letter_resolution AFTER UPDATE OF state ON ${quoted}.job
20
+ FOR EACH ROW WHEN (NEW.state = 'completed' AND OLD.state IS DISTINCT FROM NEW.state)
21
+ EXECUTE FUNCTION ${quoted}.effect_dead_letter_resolution();
22
+ DELETE FROM ${quoted}.job AS copy USING ${quoted}.job AS source
23
+ WHERE source.state = 'completed' AND copy.name = source.name || '-dlq'
24
+ AND copy.source_name = source.name AND copy.source_id = source.id AND copy.source_created_on = source.created_on;
25
+ END IF;
26
+ END`;
27
+ return `DO '${body.replaceAll("'", "''")}'`;
28
+ }
@@ -1,14 +1,14 @@
1
1
  import { Effect, Option, Redacted, Schema } from "effect";
2
+ import { transactionDb } from "#adapters/transactionDb.ts";
2
3
  import type { PgBossClient } from "#client.ts";
3
4
  import { PgBossPayloadError, toPgBossError } from "#error.ts";
4
5
  import { capturePayload } from "#internal/tracing.ts";
5
- import { transactionDb } from "#internal/transactionDb.ts";
6
6
  import type { PgBossService } from "#service.ts";
7
7
 
8
8
  export function enqueueFor(client: PgBossClient): PgBossService["enqueue"] {
9
9
  return (queue, payload, options) =>
10
10
  Effect.gen(function* () {
11
- const encoded = yield* Schema.encodeUnknownEffect(queue.schema)(payload).pipe(
11
+ const encoded = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(queue.schema))(payload).pipe(
12
12
  Effect.mapError((error) => new PgBossPayloadError({ direction: "encode", original: Redacted.make(error), queue: queue.name })),
13
13
  );
14
14
  if (typeof encoded !== "object" || encoded === null) {
@@ -1,4 +1,5 @@
1
1
  import { Effect, Option } from "effect";
2
+ import { redact } from "@shivaedev/errors/redact.ts";
2
3
  import type { PgBossClient } from "#client.ts";
3
4
  import { toPgBossError } from "#error.ts";
4
5
  import { decodeStoredData } from "#internal/storedRows.ts";
@@ -18,7 +19,7 @@ export function getJobFor(client: PgBossClient): PgBossService["getJob"] {
18
19
  Effect.flatMap((job) =>
19
20
  job === undefined
20
21
  ? Effect.succeed(Option.none())
21
- : decodeStoredData(queue, job.data).pipe(Effect.map((data) => Option.some({ ...job, data }))),
22
+ : decodeStoredData(queue, job.data).pipe(Effect.map((data) => Option.some({ ...job, data: redact(data) }))),
22
23
  ),
23
24
  );
24
25
  }
@@ -0,0 +1,6 @@
1
+ import { deadLetterResolutionPlan } from "#internal/deadLetterResolutionPlan.ts";
2
+ import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
3
+
4
+ export function jobStorePlans(schema: string): readonly string[] {
5
+ return [deferralStorePlan(schema), deadLetterResolutionPlan(schema)];
6
+ }
@@ -1,8 +1,9 @@
1
1
  import { Effect } from "effect";
2
2
  import type { ConstructorOptions, StopOptions } from "pg-boss";
3
- import { defaultClientFactory, type PgBossClient, type PgBossClientFactory } from "#client.ts";
3
+ import type { PgBossClient, PgBossClientFactory } from "#client.ts";
4
+ import { defaultClientFactory } from "#defaultClientFactory.ts";
4
5
  import { PgBossClientOptionsError, type PgBossError, toPgBossError } from "#error.ts";
5
- import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
6
+ import { jobStorePlans } from "#internal/jobStorePlans.ts";
6
7
  import { acquireCachedClient, releaseCachedClient } from "./clientCache.ts";
7
8
  import { observeClientErrors } from "./clientErrors.ts";
8
9
 
@@ -24,7 +25,9 @@ async function startClient(options: AcquireClientOptions, report: (error: Error)
24
25
  try {
25
26
  await client.start();
26
27
  if (options.constructor.migrate !== false && client.getDb !== undefined) {
27
- await client.getDb().executeSql(deferralStorePlan(options.constructor.schema ?? "pgboss"));
28
+ for (const plan of jobStorePlans(options.constructor.schema ?? "pgboss")) {
29
+ await client.getDb().executeSql(plan);
30
+ }
28
31
  }
29
32
  return client;
30
33
  } catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { Effect, Option, Redacted, Schema } from "effect";
2
+ import { redact } from "@shivaedev/errors/redact.ts";
2
3
  import type { PgBossClient } from "#client.ts";
3
4
  import type { JobPayloadSchema } from "#definition.ts";
4
5
  import { type PgBossError, PgBossPayloadError, toPgBossError } from "#error.ts";
@@ -50,7 +51,10 @@ export function failedJobsFor(client: PgBossClient, schema: string): PgBossServi
50
51
  }
51
52
 
52
53
  export function deadLetteredJobsFor(client: PgBossClient, schema: string): PgBossService["deadLetteredJobs"] {
53
- return (queue, options) => listJobs(client, schema, "deadLetteredJobs", queue, options);
54
+ return (queue, options) =>
55
+ listJobs(client, schema, "deadLetteredJobs", queue, options).pipe(
56
+ Effect.map(({ jobs, next }) => ({ jobs: jobs.map((job) => ({ ...job, sourceQueue: queue.name })), next })),
57
+ );
54
58
  }
55
59
 
56
60
  const listJobs = Effect.fnUntraced(function* <const TName extends string, const TPayload extends JobPayloadSchema | typeof Schema.Null>(
@@ -59,7 +63,11 @@ const listJobs = Effect.fnUntraced(function* <const TName extends string, const
59
63
  listing: Listing,
60
64
  queue: { readonly name: TName; readonly schema: TPayload },
61
65
  options: JobPageOptions,
62
- ): Effect.fn.Return<JobPage<DeadLetteredJob<TPayload["Type"]>>, PgBossError | PgBossPayloadError | Schema.SchemaError, TPayload["DecodingServices"]> {
66
+ ): Effect.fn.Return<
67
+ JobPage<Omit<DeadLetteredJob<unknown>, "sourceQueue">>,
68
+ PgBossError | PgBossPayloadError | Schema.SchemaError,
69
+ TPayload["DecodingServices"]
70
+ > {
63
71
  const { cursor: after, limit } = yield* Schema.decodeUnknownEffect(PageOptions)(options);
64
72
  const rows = yield* Effect.tryPromise({
65
73
  catch: (error) => toPgBossError(listing, error, queue.name),
@@ -102,7 +110,7 @@ const listJobs = Effect.fnUntraced(function* <const TName extends string, const
102
110
  const next = decodedRows.length > limit && last !== undefined ? Option.some(Schema.encodeSync(Cursor)([last.cursorAt, last.id])) : Option.none();
103
111
  return yield* Effect.forEach(page, ({ cursorAt: _, data, ...row }) =>
104
112
  decodeStoredData(queue, data).pipe(
105
- Effect.map((decoded) => ({ ...row, attempt: Option.isSome(row.startedOn) ? row.retryCount + 1 : 0, data: decoded })),
113
+ Effect.map((decoded) => ({ ...row, attempt: Option.isSome(row.startedOn) ? row.retryCount + 1 : 0, data: redact(decoded) })),
106
114
  ),
107
115
  ).pipe(Effect.map((jobs) => ({ jobs, next })));
108
116
  });
@@ -1,10 +1,9 @@
1
1
  import { type Context, Effect, type Scope } from "effect";
2
+ import { registerWorker } from "#adapters/registerWorker.ts";
2
3
  import type { PgBossClient } from "#client.ts";
3
4
  import type { JobRegistration, QueueDefinition, QueueWorker, ScheduleDefinition, ScheduledWorker } from "#definition.ts";
4
5
  import { PgBossQueueOptionsError, toPgBossError } from "#error.ts";
5
- import type { PgBossRunner } from "#internal/batchHandler.ts";
6
6
  import { ensureQueue } from "#internal/ensureQueue.ts";
7
- import { registerWorker } from "#internal/registerWorker.ts";
8
7
  import { runQueueWorker } from "#internal/runQueueWorker.ts";
9
8
 
10
9
  function isQueueWorker(registration: JobRegistration): registration is QueueWorker {
@@ -62,7 +61,6 @@ const registerQueue = Effect.fnUntraced(function* (
62
61
  context: Context.Context<unknown>,
63
62
  scope: Scope.Scope,
64
63
  schema: string,
65
- execute: PgBossRunner,
66
64
  graceful: boolean,
67
65
  ) {
68
66
  const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
@@ -78,10 +76,8 @@ const registerQueue = Effect.fnUntraced(function* (
78
76
  scope,
79
77
  schema,
80
78
  queue.maxDeferrals,
81
- context,
82
- execute,
83
79
  graceful,
84
- );
80
+ ).pipe(Effect.provide(context));
85
81
  });
86
82
 
87
83
  const registerSchedule = Effect.fnUntraced(function* (
@@ -90,21 +86,11 @@ const registerSchedule = Effect.fnUntraced(function* (
90
86
  context: Context.Context<unknown>,
91
87
  scope: Scope.Scope,
92
88
  schema: string,
93
- execute: PgBossRunner,
94
89
  graceful: boolean,
95
90
  ) {
96
91
  const { schedule } = worker;
97
- yield* registerWorker(
98
- client,
99
- schedule.name,
100
- schedule.workerOptions,
101
- () => worker.effect,
102
- scope,
103
- schema,
104
- schedule.maxDeferrals,
105
- context,
106
- execute,
107
- graceful,
92
+ yield* registerWorker(client, schedule.name, schedule.workerOptions, () => worker.effect, scope, schema, schedule.maxDeferrals, graceful).pipe(
93
+ Effect.provide(context),
108
94
  );
109
95
  yield* Effect.tryPromise({
110
96
  catch: (error) => toPgBossError("register", error, schedule.name),
@@ -125,7 +111,6 @@ export const registerJobs = Effect.fnUntraced(function* <R>(
125
111
  registrations: readonly JobRegistration[],
126
112
  context: Context.Context<R>,
127
113
  replaceWorkers: boolean,
128
- execute: PgBossRunner,
129
114
  schema = "pgboss",
130
115
  startWorkers = true,
131
116
  graceful = true,
@@ -139,9 +124,9 @@ export const registerJobs = Effect.fnUntraced(function* <R>(
139
124
  const provided = workerContext(context);
140
125
  for (const registration of registrations) {
141
126
  if (isQueueWorker(registration) || isQueueDefinition(registration)) {
142
- yield* registerQueue(client, registration, provided, scope, schema, execute, graceful);
127
+ yield* registerQueue(client, registration, provided, scope, schema, graceful);
143
128
  } else if (isScheduledWorker(registration)) {
144
- yield* registerSchedule(client, registration, provided, scope, schema, execute, graceful);
129
+ yield* registerSchedule(client, registration, provided, scope, schema, graceful);
145
130
  }
146
131
  }
147
132
  });
@@ -3,7 +3,6 @@ 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";
7
6
  import { registerJobs, registrationName } from "./register.ts";
8
7
 
9
8
  export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
@@ -11,20 +10,17 @@ export const registerReleasedJobs = Effect.fnUntraced(function* <R>(
11
10
  jobs: readonly JobRegistration[],
12
11
  context: Context.Context<R>,
13
12
  replaceWorkers: boolean,
14
- execute: PgBossRunner,
15
13
  schema: string | undefined,
16
14
  graceful: boolean,
17
15
  ) {
18
16
  const signal = yield* Effect.serviceOption(WorkerRelease);
19
17
  const workers = jobs.filter((job) => job._tag !== "QueueDefinition");
20
18
  const scope = yield* Effect.scope;
21
- const register = registerJobs(client, jobs, context, replaceWorkers, execute, schema, true, graceful).pipe(
22
- Effect.provideService(Scope.Scope, scope),
23
- );
19
+ const register = registerJobs(client, jobs, context, replaceWorkers, schema, true, graceful).pipe(Effect.provideService(Scope.Scope, scope));
24
20
  if (Option.isNone(signal) || workers.length === 0) {
25
21
  return yield* register;
26
22
  }
27
- yield* registerJobs(client, jobs, context, replaceWorkers, execute, schema, false, graceful);
23
+ yield* registerJobs(client, jobs, context, replaceWorkers, schema, false, graceful);
28
24
  let working = replaceWorkers;
29
25
  let paused = false;
30
26
  yield* signal.value.register((active) =>
@@ -1,11 +1,12 @@
1
1
  import { Effect, Redacted, Schema } from "effect";
2
2
  import type { Job } from "pg-boss";
3
+ import { workerDiagnostics } from "#adapters/workerDiagnostics.ts";
3
4
  import { jobContext, type QueueWorker } from "#definition.ts";
4
5
  import { PgBossPayloadError } from "#error.ts";
5
6
  import { payloadOf } from "#internal/tracing.ts";
6
7
 
7
8
  export function runQueueWorker(worker: QueueWorker, job: Job<unknown>, deadline: number) {
8
- return Schema.decodeUnknownEffect(worker.queue.schema)(payloadOf(job.data)).pipe(
9
+ return Schema.decodeUnknownEffect(Schema.toCodecJson(worker.queue.schema))(payloadOf(job.data)).pipe(
9
10
  Effect.mapError(
10
11
  (error) =>
11
12
  new PgBossPayloadError({
@@ -15,5 +16,6 @@ export function runQueueWorker(worker: QueueWorker, job: Job<unknown>, deadline:
15
16
  }),
16
17
  ),
17
18
  Effect.flatMap((payload) => worker.handler(payload, jobContext(job, deadline))),
19
+ workerDiagnostics,
18
20
  );
19
21
  }
@@ -11,7 +11,7 @@ export function decodeStoredData<const TName extends string, const TPayload exte
11
11
  queue: { readonly name: TName; readonly schema: TPayload },
12
12
  data: unknown,
13
13
  ): Effect.Effect<TPayload["Type"], PgBossPayloadError, TPayload["DecodingServices"]> {
14
- return Schema.decodeUnknownEffect(queue.schema)(payloadOf(data)).pipe(
14
+ return Schema.decodeUnknownEffect(Schema.toCodecJson(queue.schema))(payloadOf(data)).pipe(
15
15
  Effect.mapError((error) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(error), queue: queue.name })),
16
16
  );
17
17
  }
@@ -1,9 +1,9 @@
1
1
  import type { Job, PgBoss, SendOptions, UpdateOptions, UpdateRequest, UpdateResponse } from "pg-boss";
2
+ import { testingExpirations } from "#adapters/testingExpirations.ts";
2
3
  import type { PgBossClient } from "#client.ts";
3
4
  import { deferralObservers } from "#internal/deferral.ts";
4
5
  import { expireAttempt } from "#internal/expireAttempt.ts";
5
6
  import { observeTestingSettlements } from "#internal/observeTestingSettlements.ts";
6
- import { testingExpirations } from "#internal/testingExpirations.ts";
7
7
  import type { TestingBatch, TestingWork } from "#internal/testingIdle.ts";
8
8
 
9
9
  export interface TestingControls {