@shivaedev/jobs 0.25.0 → 0.27.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 (165) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +58 -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/adapters/retireJobs.d.ts +5 -0
  16. package/dist/server/adapters/retireJobs.d.ts.map +1 -0
  17. package/dist/server/adapters/retireJobs.js +28 -0
  18. package/dist/server/adapters/retireJobs.js.map +1 -0
  19. package/dist/server/{internal → adapters}/runSchemaPlan.d.ts.map +1 -1
  20. package/dist/server/{internal → adapters}/runSchemaPlan.js.map +1 -1
  21. package/dist/server/{internal → adapters}/schemaIndexBuilds.d.ts.map +1 -1
  22. package/dist/server/{internal → adapters}/schemaIndexBuilds.js.map +1 -1
  23. package/dist/server/{internal → adapters}/testingExpirations.d.ts.map +1 -1
  24. package/dist/server/{internal → adapters}/testingExpirations.js.map +1 -1
  25. package/dist/server/{internal → adapters}/transactionDb.d.ts.map +1 -1
  26. package/dist/server/{internal → adapters}/transactionDb.js.map +1 -1
  27. package/dist/server/adapters/workerDiagnostics.d.ts +3 -0
  28. package/dist/server/adapters/workerDiagnostics.d.ts.map +1 -0
  29. package/dist/server/adapters/workerDiagnostics.js +24 -0
  30. package/dist/server/adapters/workerDiagnostics.js.map +1 -0
  31. package/dist/server/client.d.ts +2 -1
  32. package/dist/server/client.d.ts.map +1 -1
  33. package/dist/server/client.js +1 -2
  34. package/dist/server/client.js.map +1 -1
  35. package/dist/server/defaultClientFactory.d.ts +3 -0
  36. package/dist/server/defaultClientFactory.d.ts.map +1 -0
  37. package/dist/server/{internal/nativeClient.js → defaultClientFactory.js} +2 -2
  38. package/dist/server/defaultClientFactory.js.map +1 -0
  39. package/dist/server/defineSchedule.d.ts +3 -0
  40. package/dist/server/defineSchedule.d.ts.map +1 -0
  41. package/dist/server/{internal/scheduleDefinition.js → defineSchedule.js} +2 -2
  42. package/dist/server/defineSchedule.js.map +1 -0
  43. package/dist/server/definition.d.ts +0 -1
  44. package/dist/server/definition.d.ts.map +1 -1
  45. package/dist/server/definition.js +0 -4
  46. package/dist/server/definition.js.map +1 -1
  47. package/dist/server/error.d.ts +11 -1
  48. package/dist/server/error.d.ts.map +1 -1
  49. package/dist/server/error.js +6 -1
  50. package/dist/server/error.js.map +1 -1
  51. package/dist/server/health.d.ts +1 -0
  52. package/dist/server/health.d.ts.map +1 -1
  53. package/dist/server/internal/adapters/testingFetches.d.ts +7 -0
  54. package/dist/server/internal/adapters/testingFetches.d.ts.map +1 -0
  55. package/dist/server/internal/adapters/testingFetches.js +30 -0
  56. package/dist/server/internal/adapters/testingFetches.js.map +1 -0
  57. package/dist/server/internal/batchHandler.d.ts +1 -2
  58. package/dist/server/internal/batchHandler.d.ts.map +1 -1
  59. package/dist/server/internal/batchHandler.js +9 -1
  60. package/dist/server/internal/batchHandler.js.map +1 -1
  61. package/dist/server/internal/deadLetterResolutionPlan.d.ts +2 -0
  62. package/dist/server/internal/deadLetterResolutionPlan.d.ts.map +1 -0
  63. package/dist/server/internal/deadLetterResolutionPlan.js +28 -0
  64. package/dist/server/internal/deadLetterResolutionPlan.js.map +1 -0
  65. package/dist/server/internal/enqueueFor.d.ts.map +1 -1
  66. package/dist/server/internal/enqueueFor.js +2 -2
  67. package/dist/server/internal/enqueueFor.js.map +1 -1
  68. package/dist/server/internal/jobControl.d.ts.map +1 -1
  69. package/dist/server/internal/jobControl.js +2 -1
  70. package/dist/server/internal/jobControl.js.map +1 -1
  71. package/dist/server/internal/jobStorePlans.d.ts +2 -0
  72. package/dist/server/internal/jobStorePlans.d.ts.map +1 -0
  73. package/dist/server/internal/jobStorePlans.js +6 -0
  74. package/dist/server/internal/jobStorePlans.js.map +1 -0
  75. package/dist/server/internal/lifecycle.d.ts +1 -1
  76. package/dist/server/internal/lifecycle.d.ts.map +1 -1
  77. package/dist/server/internal/lifecycle.js +5 -3
  78. package/dist/server/internal/lifecycle.js.map +1 -1
  79. package/dist/server/internal/listJobs.d.ts.map +1 -1
  80. package/dist/server/internal/listJobs.js +3 -2
  81. package/dist/server/internal/listJobs.js.map +1 -1
  82. package/dist/server/internal/makeService.js +1 -1
  83. package/dist/server/internal/makeService.js.map +1 -1
  84. package/dist/server/internal/register.d.ts +1 -2
  85. package/dist/server/internal/register.d.ts.map +1 -1
  86. package/dist/server/internal/register.js +8 -8
  87. package/dist/server/internal/register.js.map +1 -1
  88. package/dist/server/internal/registerReleasedJobs.d.ts +1 -2
  89. package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -1
  90. package/dist/server/internal/registerReleasedJobs.js +5 -4
  91. package/dist/server/internal/registerReleasedJobs.js.map +1 -1
  92. package/dist/server/internal/runQueueWorker.d.ts.map +1 -1
  93. package/dist/server/internal/runQueueWorker.js +3 -2
  94. package/dist/server/internal/runQueueWorker.js.map +1 -1
  95. package/dist/server/internal/storedRows.js +1 -1
  96. package/dist/server/internal/storedRows.js.map +1 -1
  97. package/dist/server/internal/testingClient.d.ts.map +1 -1
  98. package/dist/server/internal/testingClient.js +1 -1
  99. package/dist/server/internal/testingClient.js.map +1 -1
  100. package/dist/server/internal/testingWaits.d.ts +16 -0
  101. package/dist/server/internal/testingWaits.d.ts.map +1 -0
  102. package/dist/server/internal/testingWaits.js +56 -0
  103. package/dist/server/internal/testingWaits.js.map +1 -0
  104. package/dist/server/schema.d.ts.map +1 -1
  105. package/dist/server/schema.js +11 -53
  106. package/dist/server/schema.js.map +1 -1
  107. package/dist/server/service.d.ts +5 -4
  108. package/dist/server/service.d.ts.map +1 -1
  109. package/dist/server/service.js +6 -10
  110. package/dist/server/service.js.map +1 -1
  111. package/dist/server/testing.d.ts +5 -0
  112. package/dist/server/testing.d.ts.map +1 -1
  113. package/dist/server/testing.js +7 -2
  114. package/dist/server/testing.js.map +1 -1
  115. package/package.json +4 -4
  116. package/src/server/adapters/clientErrorReporter.ts +8 -0
  117. package/src/server/adapters/installSchema.ts +68 -0
  118. package/src/server/{internal → adapters}/registerWorker.ts +6 -7
  119. package/src/server/adapters/retireJobs.ts +30 -0
  120. package/src/server/adapters/workerDiagnostics.ts +28 -0
  121. package/src/server/client.ts +2 -3
  122. package/src/server/{internal/nativeClient.ts → defaultClientFactory.ts} +1 -1
  123. package/src/server/{internal/scheduleDefinition.ts → defineSchedule.ts} +1 -1
  124. package/src/server/definition.ts +0 -5
  125. package/src/server/error.ts +11 -1
  126. package/src/server/health.ts +1 -0
  127. package/src/server/internal/adapters/testingFetches.ts +36 -0
  128. package/src/server/internal/batchHandler.ts +11 -8
  129. package/src/server/internal/deadLetterResolutionPlan.ts +28 -0
  130. package/src/server/internal/enqueueFor.ts +2 -2
  131. package/src/server/internal/jobControl.ts +2 -1
  132. package/src/server/internal/jobStorePlans.ts +6 -0
  133. package/src/server/internal/lifecycle.ts +6 -3
  134. package/src/server/internal/listJobs.ts +11 -3
  135. package/src/server/internal/makeService.ts +1 -1
  136. package/src/server/internal/register.ts +6 -21
  137. package/src/server/internal/registerReleasedJobs.ts +6 -5
  138. package/src/server/internal/runQueueWorker.ts +3 -1
  139. package/src/server/internal/storedRows.ts +1 -1
  140. package/src/server/internal/testingClient.ts +1 -1
  141. package/src/server/internal/testingWaits.ts +98 -0
  142. package/src/server/schema.ts +14 -65
  143. package/src/server/service.ts +11 -14
  144. package/src/server/testing.ts +11 -2
  145. package/dist/server/internal/nativeClient.d.ts +0 -3
  146. package/dist/server/internal/nativeClient.d.ts.map +0 -1
  147. package/dist/server/internal/nativeClient.js.map +0 -1
  148. package/dist/server/internal/registerWorker.d.ts +0 -6
  149. package/dist/server/internal/registerWorker.d.ts.map +0 -1
  150. package/dist/server/internal/registerWorker.js.map +0 -1
  151. package/dist/server/internal/scheduleDefinition.d.ts +0 -3
  152. package/dist/server/internal/scheduleDefinition.d.ts.map +0 -1
  153. package/dist/server/internal/scheduleDefinition.js.map +0 -1
  154. /package/dist/server/{internal → adapters}/runSchemaPlan.d.ts +0 -0
  155. /package/dist/server/{internal → adapters}/runSchemaPlan.js +0 -0
  156. /package/dist/server/{internal → adapters}/schemaIndexBuilds.d.ts +0 -0
  157. /package/dist/server/{internal → adapters}/schemaIndexBuilds.js +0 -0
  158. /package/dist/server/{internal → adapters}/testingExpirations.d.ts +0 -0
  159. /package/dist/server/{internal → adapters}/testingExpirations.js +0 -0
  160. /package/dist/server/{internal → adapters}/transactionDb.d.ts +0 -0
  161. /package/dist/server/{internal → adapters}/transactionDb.js +0 -0
  162. /package/src/server/{internal → adapters}/runSchemaPlan.ts +0 -0
  163. /package/src/server/{internal → adapters}/schemaIndexBuilds.ts +0 -0
  164. /package/src/server/{internal → adapters}/testingExpirations.ts +0 -0
  165. /package/src/server/{internal → adapters}/transactionDb.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import { Duration, Effect, Redacted } from "effect";
2
+ import { defaultClientFactory } from "#defaultClientFactory.ts";
2
3
  import { PgBossTestingError } from "#error.ts";
3
- import { nativeClient } from "#internal/nativeClient.ts";
4
+ import { testingFetches } from "#internal/adapters/testingFetches.ts";
4
5
  import { testingClient } from "#internal/testingClient.ts";
5
6
  import { testingDeferrals } from "#internal/testingDeferrals.ts";
6
7
  import { testingIdle } from "#internal/testingIdle.ts";
@@ -8,9 +9,11 @@ import { testingOperation, testingReadout } from "#internal/testingReadout.ts";
8
9
  import { testingSchedules } from "#internal/testingSchedules.ts";
9
10
  import { testingSignal } from "#internal/testingSignal.ts";
10
11
  import { testingSnapshot } from "#internal/testingSnapshot.ts";
12
+ import { testingWaits } from "#internal/testingWaits.ts";
11
13
  export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
12
14
  export function makePgBossTesting(options = {}) {
13
15
  let current;
16
+ let fetches;
14
17
  let schema = "pgboss";
15
18
  const signal = testingSignal();
16
19
  const work = { batches: new Set(), queues: new Set() };
@@ -33,12 +36,14 @@ export function makePgBossTesting(options = {}) {
33
36
  throw new Error("A testing controller owns one jobs client; create a controller per runtime");
34
37
  }
35
38
  schema = clientOptions.schema ?? "pgboss";
36
- current = nativeClient({ ...clientOptions, "__test__enableSpies": true });
39
+ current = defaultClientFactory({ ...clientOptions, "__test__enableSpies": true });
40
+ fetches = testingFetches(current.getDb(), signal.changed);
37
41
  return testingClient(current, clientOptions.schema ?? "pgboss", controls);
38
42
  };
39
43
  return {
40
44
  ...testingDeferrals(client, () => schema, signal.changed),
41
45
  ...testingSchedules(client, signal.changed),
46
+ ...testingWaits(client, () => fetches, signal),
42
47
  checkBatchWindow: (queue, millisecondsPerJob) => testingOperation("checkBatchWindow", async () => {
43
48
  const stored = await client().getQueue(queue.name);
44
49
  if (stored === null) {
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/jobs",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Effect-native jobs and lifecycle management for pg-boss",
5
5
  "keywords": [
6
6
  "effect"
@@ -48,16 +48,16 @@
48
48
  "README.md"
49
49
  ],
50
50
  "dependencies": {
51
- "@shivaedev/errors": "0.1.3",
51
+ "@shivaedev/errors": "0.3.0",
52
52
  "@shivaedev/service": "0.6.0",
53
53
  "@shivaedev/sql": "0.27.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@shivaedev/http": "0.3.1",
57
- "@shivaedev/quality": "0.48.2",
57
+ "@shivaedev/quality": "0.50.0",
58
58
  "@shivaedev/test": "0.12.0",
59
59
  "@shivaedev/test-sql": "0.15.0",
60
- "@shivaedev/test-story": "0.28.0",
60
+ "@shivaedev/test-story": "0.30.0",
61
61
  "@types/node": "24.19.2",
62
62
  "effect": "4.0.6",
63
63
  "pg-boss": "12.38.0",
@@ -0,0 +1,8 @@
1
+ import { Effect } from "effect";
2
+
3
+ export const clientErrorReporter = Effect.fnUntraced(function* <R>(report: (error: Error) => Effect.Effect<unknown, never, R>) {
4
+ const run = Effect.runPromiseWith(yield* Effect.context<R>());
5
+ return (error: Error): void => {
6
+ void run(Effect.exit(report(error)));
7
+ };
8
+ });
@@ -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,30 @@
1
+ import { Effect } from "effect";
2
+ import type { PgBossClient } from "#client.ts";
3
+ import type { JobRegistration } from "#definition.ts";
4
+ import { toPgBossError } from "#error.ts";
5
+ import { deadLetterQueueName } from "#health.ts";
6
+ import { registrationNames } from "#internal/register.ts";
7
+
8
+ export const retireJobs = Effect.fnUntraced(function* (client: PgBossClient, jobs: readonly JobRegistration[], retired: readonly string[]) {
9
+ yield* Effect.try({
10
+ catch: (error) => toPgBossError("register", error),
11
+ try: () => {
12
+ const active = registrationNames(jobs).flatMap((name) => [name, deadLetterQueueName(name)]);
13
+ if (retired.some((name) => active.includes(name))) {
14
+ throw new TypeError("Retired queues must not be registered or used as dead-letter queues");
15
+ }
16
+ },
17
+ });
18
+ for (const name of retired) {
19
+ yield* Effect.tryPromise({
20
+ catch: (error) => toPgBossError("register", error, name),
21
+ try: async () => {
22
+ if (client.deleteQueue === undefined) {
23
+ throw new TypeError("This pg-boss client does not support deleteQueue");
24
+ }
25
+ await client.offWork(name);
26
+ await client.deleteQueue(name);
27
+ },
28
+ });
29
+ }
30
+ });
@@ -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,11 +11,11 @@ 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"];
18
17
  readonly createQueue: (name: string, options?: Omit<Queue, "name">) => Promise<void>;
18
+ readonly deleteQueue?: PgBoss["deleteQueue"];
19
19
  readonly fail?: PgBoss["fail"];
20
20
  readonly findJobs?: (name: string, options?: FindJobsOptions) => Promise<readonly JobWithMetadata<unknown>[]>;
21
21
  readonly getDb?: PgBoss["getDb"];
@@ -30,11 +30,10 @@ export interface PgBossClient {
30
30
  readonly send: (name: string, data?: object | null, options?: SendOptions) => Promise<string | null>;
31
31
  readonly start: () => Promise<unknown>;
32
32
  readonly stop: (options?: StopOptions) => Promise<void>;
33
+ readonly unschedule?: PgBoss["unschedule"];
33
34
  readonly update?: PgBoss["update"];
34
35
  readonly updateQueue?: PgBoss["updateQueue"];
35
36
  readonly work: (name: string, options: WorkOptions, handler: (jobs: readonly Job<unknown>[]) => Promise<unknown>) => Promise<string>;
36
37
  }
37
38
 
38
39
  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
  });
@@ -13,7 +13,7 @@ export function makeService(client: PgBossClient, names: readonly string[], sche
13
13
  enqueue: enqueueFor(client),
14
14
  failedJobs: failedJobsFor(client, schema),
15
15
  getJob: getJobFor(client),
16
- health: () => healthFor(client, names),
16
+ health: (queues) => healthFor(client, queues === undefined ? names : queues.map((queue) => queue.name)),
17
17
  retry: retryFor(client),
18
18
  };
19
19
  }