@shivaedev/effect-pg-boss 0.10.1 → 0.12.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 (87) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +51 -4
  3. package/dist/server/definition.d.ts +59 -25
  4. package/dist/server/definition.d.ts.map +1 -1
  5. package/dist/server/definition.js +25 -27
  6. package/dist/server/definition.js.map +1 -1
  7. package/dist/server/internal/batchHandler.d.ts.map +1 -1
  8. package/dist/server/internal/batchHandler.js +13 -7
  9. package/dist/server/internal/batchHandler.js.map +1 -1
  10. package/dist/server/internal/deferrableAttempt.d.ts +7 -0
  11. package/dist/server/internal/deferrableAttempt.d.ts.map +1 -0
  12. package/dist/server/internal/deferrableAttempt.js +28 -0
  13. package/dist/server/internal/deferrableAttempt.js.map +1 -0
  14. package/dist/server/internal/deferral.d.ts +15 -0
  15. package/dist/server/internal/deferral.d.ts.map +1 -0
  16. package/dist/server/internal/deferral.js +14 -0
  17. package/dist/server/internal/deferral.js.map +1 -0
  18. package/dist/server/internal/deferralStorePlan.d.ts +2 -0
  19. package/dist/server/internal/deferralStorePlan.d.ts.map +1 -0
  20. package/dist/server/internal/deferralStorePlan.js +34 -0
  21. package/dist/server/internal/deferralStorePlan.js.map +1 -0
  22. package/dist/server/internal/definitionOptions.d.ts +17 -0
  23. package/dist/server/internal/definitionOptions.d.ts.map +1 -0
  24. package/dist/server/internal/definitionOptions.js +12 -0
  25. package/dist/server/internal/definitionOptions.js.map +1 -0
  26. package/dist/server/internal/expireAttempt.d.ts +1 -1
  27. package/dist/server/internal/expireAttempt.d.ts.map +1 -1
  28. package/dist/server/internal/expireAttempt.js +4 -2
  29. package/dist/server/internal/expireAttempt.js.map +1 -1
  30. package/dist/server/internal/lifecycle.d.ts.map +1 -1
  31. package/dist/server/internal/lifecycle.js +4 -0
  32. package/dist/server/internal/lifecycle.js.map +1 -1
  33. package/dist/server/internal/register.d.ts +1 -1
  34. package/dist/server/internal/register.d.ts.map +1 -1
  35. package/dist/server/internal/register.js +10 -10
  36. package/dist/server/internal/register.js.map +1 -1
  37. package/dist/server/internal/registerWorker.d.ts +2 -2
  38. package/dist/server/internal/registerWorker.d.ts.map +1 -1
  39. package/dist/server/internal/registerWorker.js +7 -2
  40. package/dist/server/internal/registerWorker.js.map +1 -1
  41. package/dist/server/internal/scheduleDefinition.d.ts +3 -0
  42. package/dist/server/internal/scheduleDefinition.d.ts.map +1 -0
  43. package/dist/server/internal/scheduleDefinition.js +19 -0
  44. package/dist/server/internal/scheduleDefinition.js.map +1 -0
  45. package/dist/server/internal/service.js +1 -1
  46. package/dist/server/internal/service.js.map +1 -1
  47. package/dist/server/internal/settleDeferral.d.ts +7 -0
  48. package/dist/server/internal/settleDeferral.d.ts.map +1 -0
  49. package/dist/server/internal/settleDeferral.js +47 -0
  50. package/dist/server/internal/settleDeferral.js.map +1 -0
  51. package/dist/server/internal/testingClient.d.ts.map +1 -1
  52. package/dist/server/internal/testingClient.js +4 -1
  53. package/dist/server/internal/testingClient.js.map +1 -1
  54. package/dist/server/internal/testingDeferrals.d.ts +11 -0
  55. package/dist/server/internal/testingDeferrals.d.ts.map +1 -0
  56. package/dist/server/internal/testingDeferrals.js +28 -0
  57. package/dist/server/internal/testingDeferrals.js.map +1 -0
  58. package/dist/server/schema.d.ts.map +1 -1
  59. package/dist/server/schema.js +8 -1
  60. package/dist/server/schema.js.map +1 -1
  61. package/dist/server/service.d.ts +5 -3
  62. package/dist/server/service.d.ts.map +1 -1
  63. package/dist/server/service.js +1 -1
  64. package/dist/server/service.js.map +1 -1
  65. package/dist/server/testing.d.ts +8 -3
  66. package/dist/server/testing.d.ts.map +1 -1
  67. package/dist/server/testing.js +4 -0
  68. package/dist/server/testing.js.map +1 -1
  69. package/package.json +5 -5
  70. package/src/server/definition.ts +75 -58
  71. package/src/server/internal/batchHandler.ts +21 -6
  72. package/src/server/internal/deferrableAttempt.ts +41 -0
  73. package/src/server/internal/deferral.ts +19 -0
  74. package/src/server/internal/deferralStorePlan.ts +34 -0
  75. package/src/server/internal/definitionOptions.ts +14 -0
  76. package/src/server/internal/expireAttempt.ts +4 -2
  77. package/src/server/internal/lifecycle.ts +4 -0
  78. package/src/server/internal/register.ts +15 -15
  79. package/src/server/internal/registerWorker.ts +39 -26
  80. package/src/server/internal/scheduleDefinition.ts +21 -0
  81. package/src/server/internal/service.ts +1 -1
  82. package/src/server/internal/settleDeferral.ts +53 -0
  83. package/src/server/internal/testingClient.ts +4 -1
  84. package/src/server/internal/testingDeferrals.ts +43 -0
  85. package/src/server/schema.ts +8 -1
  86. package/src/server/service.ts +7 -7
  87. package/src/server/testing.ts +7 -3
@@ -2,11 +2,13 @@ import { Effect, Redacted } from "effect";
2
2
  import { PgBoss } from "pg-boss";
3
3
  import { PgBossTestingError } from "#error.ts";
4
4
  import { testingClient } from "#internal/testingClient.ts";
5
+ import { testingDeferrals } from "#internal/testingDeferrals.ts";
5
6
  import { testingOperation, testingReadout } from "#internal/testingReadout.ts";
6
7
  import { testingSignal } from "#internal/testingSignal.ts";
7
8
  export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
8
9
  export function makePgBossTesting(options = {}) {
9
10
  let current;
11
+ let schema = "pgboss";
10
12
  const signal = testingSignal();
11
13
  const controls = {
12
14
  changed: signal.changed,
@@ -24,10 +26,12 @@ export function makePgBossTesting(options = {}) {
24
26
  if (current !== undefined) {
25
27
  throw new Error("A testing controller owns one jobs client; create a controller per runtime");
26
28
  }
29
+ schema = clientOptions.schema ?? "pgboss";
27
30
  current = new PgBoss({ ...clientOptions, "__test__enableSpies": true });
28
31
  return testingClient(current, clientOptions.schema ?? "pgboss", controls);
29
32
  };
30
33
  return {
34
+ ...testingDeferrals(client, () => schema, signal.changed),
31
35
  checkBatchWindow: (queue, millisecondsPerJob) => testingOperation("checkBatchWindow", async () => {
32
36
  const stored = await client().getQueue(queue.name);
33
37
  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/effect-pg-boss",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Effect-native jobs and lifecycle management for pg-boss",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-pg-boss#readme",
6
6
  "bugs": {
@@ -41,10 +41,10 @@
41
41
  "README.md"
42
42
  ],
43
43
  "devDependencies": {
44
- "@shivaedev/effect-test": "0.6.0",
45
- "@shivaedev/effect-test-sql": "0.6.0",
46
- "@shivaedev/quality": "0.27.1",
47
- "@shivaedev/test-story": "0.10.0",
44
+ "@shivaedev/effect-test": "0.7.1",
45
+ "@shivaedev/effect-test-sql": "0.6.1",
46
+ "@shivaedev/quality": "0.28.0",
47
+ "@shivaedev/test-story": "0.12.0",
48
48
  "@types/node": "24.19.2",
49
49
  "effect": "4.0.4",
50
50
  "pg-boss": "12.37.1",
@@ -1,6 +1,19 @@
1
- import { Clock, Duration, Effect, type Schema } from "effect";
2
- import type { Job, Queue, ScheduleOptions, WorkOptions } from "pg-boss";
1
+ import { Clock, Duration, Effect, Schema } from "effect";
2
+ import type { Job, policies, Queue, ScheduleOptions, WorkOptions } from "pg-boss";
3
3
  import { deadlineOf } from "#internal/deadlineOf.ts";
4
+ import { maxDeferrals, nativeQueueOptions } from "#internal/definitionOptions.ts";
5
+ import { scheduleDefinition } from "#internal/scheduleDefinition.ts";
6
+
7
+ export class RetryAt extends Schema.TaggedError<RetryAt>()("RetryAt", {
8
+ at: Schema.DateTimeUtc,
9
+ reason: Schema.String,
10
+ }) {}
11
+
12
+ export type QueuePolicy = (typeof policies)[keyof typeof policies];
13
+ export type QueueOptions = Omit<Queue, "name" | "deadLetter" | "policy"> & {
14
+ readonly maxDeferrals?: number;
15
+ readonly policy?: QueuePolicy;
16
+ };
4
17
 
5
18
  export interface JobPayloadSchema extends Schema.Top {
6
19
  readonly Encoded: object;
@@ -16,108 +29,112 @@ export interface JobContext {
16
29
  readonly signal: AbortSignal;
17
30
  }
18
31
 
19
- export interface QueueDefinition<Name extends string = string, Payload extends JobPayloadSchema = JobPayloadSchema> {
32
+ export interface QueueDefinition<TName extends string = string, TPayload extends JobPayloadSchema = JobPayloadSchema> {
20
33
  readonly _tag: "QueueDefinition";
21
34
  readonly handle: <E, R>(
22
- handler: (payload: Schema.Schema.Type<Payload>, job: JobContext) => Effect.Effect<unknown, E, R>,
23
- ) => QueueWorker<Payload, R>;
24
- readonly name: Name;
35
+ handler: (payload: Schema.Schema.Type<TPayload>, job: JobContext) => Effect.Effect<unknown, E, R>,
36
+ ) => QueueWorker<TPayload, R>;
37
+ readonly key?: ((payload: Schema.Schema.Type<TPayload>) => string) | undefined;
38
+ readonly maxDeferrals: number;
39
+ readonly name: TName;
25
40
  readonly queueOptions: Omit<Queue, "name" | "deadLetter">;
26
- readonly schema: Payload;
41
+ readonly schema: TPayload;
27
42
  readonly workerOptions: WorkOptions;
28
43
  }
29
44
 
30
- export interface QueueWorker<Payload extends JobPayloadSchema = JobPayloadSchema, Requirements = unknown> {
45
+ export interface QueueWorker<TPayload extends JobPayloadSchema = JobPayloadSchema, TRequirements = unknown> {
31
46
  readonly _tag: "QueueWorker";
32
- readonly handler: (payload: Schema.Schema.Type<Payload>, job: JobContext) => Effect.Effect<unknown, unknown, Requirements>;
33
- readonly queue: QueueDefinition<string, Payload>;
47
+ readonly handler: (payload: Schema.Schema.Type<TPayload>, job: JobContext) => Effect.Effect<unknown, unknown, TRequirements>;
48
+ readonly queue: QueueDefinition<string, TPayload>;
34
49
  }
35
50
 
36
51
  export interface JobRegistration {
37
52
  readonly _tag: "QueueDefinition" | "QueueWorker" | "ScheduledWorker";
38
53
  }
39
54
 
40
- export interface ScheduleDefinition<Name extends string = string> {
55
+ export interface ScheduleDefinition<TName extends string = string> {
41
56
  readonly _tag: "ScheduleDefinition";
42
57
  readonly cron: string;
43
- readonly name: Name;
58
+ readonly maxDeferrals: number;
59
+ readonly name: TName;
44
60
  readonly queueOptions: Omit<Queue, "name" | "deadLetter">;
45
61
  readonly run: <E, R>(effect: Effect.Effect<unknown, E, R>) => ScheduledWorker<R>;
46
62
  readonly scheduleOptions: ScheduleOptions;
47
63
  readonly workerOptions: WorkOptions;
48
64
  }
49
65
 
50
- export interface ScheduledWorker<Requirements = unknown> {
66
+ export interface ScheduledWorker<TRequirements = unknown> {
51
67
  readonly _tag: "ScheduledWorker";
52
- readonly effect: Effect.Effect<unknown, unknown, Requirements>;
68
+ readonly effect: Effect.Effect<unknown, unknown, TRequirements>;
53
69
  readonly schedule: ScheduleDefinition;
54
70
  }
55
71
 
56
- export type RegistrationRequirements<Registrations extends readonly JobRegistration[]> = Registrations[number] extends infer Registration
57
- ? Registration extends QueueWorker<infer Payload, infer Requirements>
58
- ? Requirements | Payload["DecodingServices"]
59
- : Registration extends ScheduledWorker<infer Requirements>
60
- ? Requirements
72
+ export type RegistrationRequirements<TRegistrations extends readonly JobRegistration[]> = TRegistrations[number] extends infer TRegistration
73
+ ? TRegistration extends QueueWorker<infer TPayload, infer TRequirements>
74
+ ? TRequirements | TPayload["DecodingServices"]
75
+ : TRegistration extends ScheduledWorker<infer TRequirements>
76
+ ? TRequirements
61
77
  : never
62
78
  : never;
63
79
 
64
- export interface DefineQueueOptions<Name extends string, Payload extends JobPayloadSchema> {
65
- readonly name: Name;
66
- readonly queue?: Omit<Queue, "name" | "deadLetter">;
67
- readonly schema: Payload;
80
+ export type DefineQueueOptions<TName extends string, TPayload extends JobPayloadSchema> = {
81
+ readonly name: TName;
82
+ readonly schema: TPayload;
68
83
  readonly worker?: WorkOptions;
69
- }
84
+ } & (
85
+ | { readonly key: (payload: Schema.Schema.Type<TPayload>) => string; readonly queue: QueueOptions & { readonly policy: "singleton" } }
86
+ | { readonly key?: never; readonly queue?: QueueOptions & { readonly policy?: Exclude<QueuePolicy, "singleton"> } }
87
+ );
70
88
 
71
- export const defineQueue = <const Name extends string, const Payload extends JobPayloadSchema>(
72
- options: DefineQueueOptions<Name, Payload>,
73
- ): QueueDefinition<Name, Payload> => {
74
- const definition: QueueDefinition<Name, Payload> = {
89
+ export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
90
+ options: DefineQueueOptions<TName, TPayload> & { readonly key: (payload: Schema.Schema.Type<TPayload>) => string },
91
+ ): QueueDefinition<TName, TPayload> & { readonly key: (payload: Schema.Schema.Type<TPayload>) => string };
92
+ export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
93
+ options: DefineQueueOptions<TName, TPayload> & { readonly key?: never },
94
+ ): QueueDefinition<TName, TPayload> & { readonly key?: never };
95
+ export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
96
+ options: DefineQueueOptions<TName, TPayload>,
97
+ ): QueueDefinition<TName, TPayload>;
98
+ export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
99
+ options: DefineQueueOptions<TName, TPayload>,
100
+ ): QueueDefinition<TName, TPayload> {
101
+ const definition: QueueDefinition<TName, TPayload> = {
75
102
  _tag: "QueueDefinition",
103
+ ...(options.key === undefined ? {} : { key: options.key }),
76
104
  handle: (handler) =>
77
105
  Object.freeze({
78
106
  _tag: "QueueWorker" as const,
79
107
  handler,
80
108
  queue: definition,
81
109
  }),
110
+ maxDeferrals: maxDeferrals(options.queue),
82
111
  name: options.name,
83
- queueOptions: Object.freeze({ ...options.queue }),
112
+ queueOptions: nativeQueueOptions(options.queue),
84
113
  schema: options.schema,
85
114
  workerOptions: Object.freeze({ batchSize: 10, burstWhenBatchFull: true, ...options.worker }),
86
115
  };
87
116
  return Object.freeze(definition);
88
- };
117
+ }
89
118
 
90
- export interface DefineScheduleOptions<Name extends string> {
119
+ export interface DefineScheduleOptions<TName extends string> {
91
120
  readonly cron: string;
92
- readonly name: Name;
93
- readonly queue?: Omit<Queue, "name" | "deadLetter">;
121
+ readonly name: TName;
122
+ readonly queue?: QueueOptions;
94
123
  readonly schedule?: ScheduleOptions;
95
124
  readonly worker?: WorkOptions;
96
125
  }
97
126
 
98
- export const defineSchedule = <const Name extends string>(options: DefineScheduleOptions<Name>): ScheduleDefinition<Name> => {
99
- const definition: ScheduleDefinition<Name> = {
100
- _tag: "ScheduleDefinition",
101
- cron: options.cron,
102
- name: options.name,
103
- queueOptions: Object.freeze({ ...options.queue }),
104
- run: (effect) =>
105
- Object.freeze({
106
- _tag: "ScheduledWorker" as const,
107
- effect,
108
- schedule: definition,
109
- }),
110
- scheduleOptions: Object.freeze({ tz: "UTC", ...options.schedule }),
111
- workerOptions: Object.freeze({ batchSize: 10, burstWhenBatchFull: true, ...options.worker }),
112
- };
113
- return Object.freeze(definition);
114
- };
127
+ export function defineSchedule<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
128
+ return scheduleDefinition(options);
129
+ }
115
130
 
116
- export const jobContext = (job: Job<unknown>): JobContext => ({
117
- expireInSeconds: job.expireInSeconds,
118
- heartbeatSeconds: job.heartbeatSeconds,
119
- id: job.id,
120
- name: job.name,
121
- remainingBudget: Effect.map(Clock.currentTimeMillis, (now) => Duration.millis(Math.max(0, deadlineOf(job) - now))),
122
- signal: job.signal,
123
- });
131
+ export function jobContext(job: Job<unknown>): JobContext {
132
+ return {
133
+ expireInSeconds: job.expireInSeconds,
134
+ heartbeatSeconds: job.heartbeatSeconds,
135
+ id: job.id,
136
+ name: job.name,
137
+ remainingBudget: Effect.map(Clock.currentTimeMillis, (now) => Duration.millis(Math.max(0, deadlineOf(job) - now))),
138
+ signal: job.signal,
139
+ };
140
+ }
@@ -2,12 +2,31 @@ import { Cause, Clock, Effect, Exit } from "effect";
2
2
  import type { Job } from "pg-boss";
3
3
  import type { PgBossClient } from "#client.ts";
4
4
  import { deadlineOf } from "#internal/deadlineOf.ts";
5
+ import { deferredAttempt } from "#internal/deferral.ts";
5
6
 
6
7
  function failureOutput(cause: Cause.Cause<unknown>): object {
7
8
  const error = Cause.squash(cause);
8
9
  return typeof error === "object" && error !== null ? error : { message: String(error) };
9
10
  }
10
11
 
12
+ async function settleAttempt(
13
+ complete: NonNullable<PgBossClient["complete"]>,
14
+ fail: NonNullable<PgBossClient["fail"]>,
15
+ name: string,
16
+ job: Job<unknown>,
17
+ exit: Exit.Exit<unknown, unknown>,
18
+ afterFailure?: (job: Job<unknown>) => Promise<void>,
19
+ ) {
20
+ if (Exit.isSuccess(exit)) {
21
+ if (exit.value !== deferredAttempt) {
22
+ await complete(name, job);
23
+ }
24
+ } else {
25
+ await fail(name, job, failureOutput(exit.cause));
26
+ await afterFailure?.(job);
27
+ }
28
+ }
29
+
11
30
  export function batchHandler(
12
31
  client: PgBossClient,
13
32
  name: string,
@@ -19,6 +38,7 @@ export function batchHandler(
19
38
  if (complete === undefined || fail === undefined) {
20
39
  throw new TypeError("This pg-boss client does not support complete and fail");
21
40
  }
41
+
22
42
  return async (jobs: readonly Job<unknown>[]): Promise<void> => {
23
43
  let expired = false;
24
44
  for (const job of jobs) {
@@ -38,12 +58,7 @@ export function batchHandler(
38
58
  // Returning would let pg-boss complete the unprocessed jobs; its expiry owns them instead.
39
59
  return new Promise<never>(() => undefined);
40
60
  }
41
- if (Exit.isSuccess(exit)) {
42
- await complete(name, job);
43
- } else {
44
- await fail(name, job, failureOutput(exit.cause));
45
- await afterFailure?.(job);
46
- }
61
+ await settleAttempt(complete, fail, name, job, exit, afterFailure);
47
62
  }
48
63
  if (expired) {
49
64
  return new Promise<never>(() => undefined);
@@ -0,0 +1,41 @@
1
+ import { Clock, DateTime, Effect, Schema } from "effect";
2
+ import type { Job } from "pg-boss";
3
+ import type { PgBossClient } from "#client.ts";
4
+ import { RetryAt } from "#definition.ts";
5
+ import { deferralObservers, deferredAttempt } from "#internal/deferral.ts";
6
+ import { settleDeferral } from "#internal/settleDeferral.ts";
7
+
8
+ const recordDeferral = Effect.fnUntraced(function* (at: string, reason: string) {
9
+ const span = yield* Effect.currentSpan.pipe(Effect.orDie);
10
+ span.event("job.deferred", yield* Clock.currentTimeNanos, { at, reason });
11
+ });
12
+
13
+ export function deferrableAttempt<A, E, R>(
14
+ effect: Effect.Effect<A, E, R>,
15
+ client: PgBossClient,
16
+ schema: string,
17
+ job: Job<unknown>,
18
+ maxDeferrals: number,
19
+ ) {
20
+ return effect.pipe(
21
+ Effect.catchIf(Schema.is(RetryAt), (error) => {
22
+ if (maxDeferrals === 0) {
23
+ return Effect.fail(error);
24
+ }
25
+ const at = DateTime.formatIso(error.at);
26
+ return Effect.promise(() => settleDeferral(client, schema, job, at, error.reason, maxDeferrals)).pipe(
27
+ Effect.flatMap((settled) => {
28
+ if (settled.claimed && !settled.deferred) {
29
+ return Effect.fail(error);
30
+ }
31
+ // Native batch settlement must not match a sibling's new claim at the unchanged retry count.
32
+ job.retryCount = Number.MAX_SAFE_INTEGER;
33
+ if (settled.deferred) {
34
+ deferralObservers.get(client)?.();
35
+ }
36
+ return (settled.deferred ? recordDeferral(at, error.reason) : Effect.void).pipe(Effect.as(deferredAttempt));
37
+ }),
38
+ );
39
+ }),
40
+ );
41
+ }
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+ import type { Job } from "pg-boss";
3
+ import type { PgBossClient } from "#client.ts";
4
+
5
+ export const deferredAttempt = Symbol("deferredAttempt");
6
+ export const deferralObservers = new WeakMap<PgBossClient, () => void>();
7
+ export const STATELY_DEFERRAL_POLICY = "effect-stately-deferred";
8
+
9
+ export const Deferral = Schema.Struct({
10
+ at: Schema.String,
11
+ reason: Schema.String,
12
+ });
13
+
14
+ const Output = Schema.Struct({ jobDeferral: Deferral });
15
+
16
+ export function deferralOf(job: Job<unknown>) {
17
+ const output: unknown = Reflect.get(job, "output");
18
+ return Schema.is(Output)(output) ? output.jobDeferral : undefined;
19
+ }
@@ -0,0 +1,34 @@
1
+ import { STATELY_DEFERRAL_POLICY } from "./deferral.ts";
2
+
3
+ export function deferralStorePlan(schema: string): string {
4
+ const quoted = `"${schema.replaceAll('"', '""')}"`;
5
+ const jobTable = `${quoted}.job`.replaceAll("'", "''");
6
+ const key = `${schema}.effect_job_deferral`.replaceAll("'", "''");
7
+ const body = `BEGIN
8
+ PERFORM pg_advisory_xact_lock(hashtextextended('${key}', 0));
9
+ CREATE TABLE IF NOT EXISTS ${quoted}.effect_job_deferral (
10
+ name text NOT NULL,
11
+ id uuid NOT NULL,
12
+ created_on timestamptz NOT NULL,
13
+ count bigint NOT NULL,
14
+ keep_until timestamptz NOT NULL,
15
+ PRIMARY KEY (name, id)
16
+ );
17
+ CREATE INDEX IF NOT EXISTS effect_job_deferral_keep_until ON ${quoted}.effect_job_deferral (keep_until);
18
+ CREATE OR REPLACE FUNCTION ${quoted}.effect_stately_deferral() RETURNS trigger LANGUAGE plpgsql AS $claim$
19
+ BEGIN
20
+ IF NEW.policy = '${STATELY_DEFERRAL_POLICY}' AND NEW.state = 'active' THEN
21
+ NEW.policy := 'stately';
22
+ END IF;
23
+ RETURN NEW;
24
+ END
25
+ $claim$;
26
+ IF NOT EXISTS (
27
+ SELECT 1 FROM pg_trigger WHERE tgrelid = '${jobTable}'::regclass AND tgname = 'effect_stately_deferral'
28
+ ) THEN
29
+ CREATE TRIGGER effect_stately_deferral BEFORE UPDATE OF state, policy ON ${quoted}.job
30
+ FOR EACH ROW EXECUTE FUNCTION ${quoted}.effect_stately_deferral();
31
+ END IF;
32
+ END`;
33
+ return `DO '${body.replaceAll("'", "''")}'`;
34
+ }
@@ -0,0 +1,14 @@
1
+ import type { QueueOptions } from "#definition.ts";
2
+
3
+ export function maxDeferrals(options: QueueOptions | undefined): number {
4
+ const limit = options?.maxDeferrals ?? 10;
5
+ if (!Number.isSafeInteger(limit) || limit < 0) {
6
+ throw new RangeError("maxDeferrals must be a nonnegative safe integer");
7
+ }
8
+ return limit;
9
+ }
10
+
11
+ export function nativeQueueOptions(options: QueueOptions | undefined) {
12
+ const { maxDeferrals: _limit, ...native } = options ?? {};
13
+ return Object.freeze(native);
14
+ }
@@ -1,12 +1,14 @@
1
1
  import type { Job, PgBoss } from "pg-boss";
2
2
 
3
- export async function expireAttempt(client: PgBoss, schema: string, job: Job<unknown>): Promise<void> {
3
+ export async function expireAttempt(client: PgBoss, schema: string, job: Job<unknown>, cause: "expiry" | "heartbeat" = "expiry"): Promise<void> {
4
4
  if ((await client.schemaVersion()) !== 45) {
5
5
  throw new Error("Testing expiry requires pg-boss schema version 45");
6
6
  }
7
7
  const quoted = `"${schema.replaceAll('"', '""')}"`;
8
+ const timestamp = cause === "expiry" ? "started_on" : "heartbeat_on";
9
+ const seconds = cause === "expiry" ? "expire_seconds" : "heartbeat_seconds";
8
10
  await client.getDb().executeSql(
9
- `UPDATE ${quoted}.job SET started_on = ${quoted}.job_now() - (expire_seconds + 1) * interval '1 second'
11
+ `UPDATE ${quoted}.job SET ${timestamp} = ${quoted}.job_now() - (${seconds} + 1) * interval '1 second'
10
12
  WHERE name = $1 AND id = $2 AND retry_count = $3 AND state = 'active'`,
11
13
  [job.name, job.id, job.retryCount],
12
14
  );
@@ -2,6 +2,7 @@ import { Effect } from "effect";
2
2
  import type { ConstructorOptions, StopOptions } from "pg-boss";
3
3
  import { defaultClientFactory, type PgBossClient, type PgBossClientFactory } from "#client.ts";
4
4
  import { toPgBossError } from "#error.ts";
5
+ import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
5
6
  import { acquireCachedClient, releaseCachedClient } from "./clientCache.ts";
6
7
  import { logClientError, observeClientErrors } from "./clientErrors.ts";
7
8
 
@@ -22,6 +23,9 @@ async function startClient(options: AcquireClientOptions, report: (error: Error)
22
23
  observeClientErrors(client, report);
23
24
  try {
24
25
  await client.start();
26
+ if (options.constructor.migrate !== false && client.getDb !== undefined) {
27
+ await client.getDb().executeSql(deferralStorePlan(options.constructor.schema ?? "pgboss"));
28
+ }
25
29
  return client;
26
30
  } catch (error) {
27
31
  try {
@@ -12,7 +12,7 @@ import {
12
12
  import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
13
13
  import { ensureQueue } from "#internal/ensureQueue.ts";
14
14
  import { registerWorker } from "#internal/registerWorker.ts";
15
- import { payloadOf, traceJob } from "#internal/tracing.ts";
15
+ import { payloadOf } from "#internal/tracing.ts";
16
16
 
17
17
  function isQueueWorker(registration: JobRegistration): registration is QueueWorker {
18
18
  return registration._tag === "QueueWorker";
@@ -65,7 +65,7 @@ function workerContext(context: unknown): unknown {
65
65
  return context;
66
66
  }
67
67
 
68
- function runQueueWorker(worker: QueueWorker, job: Job<unknown>, context: Context.Context<unknown>) {
68
+ function runQueueWorker(worker: QueueWorker, job: Job<unknown>) {
69
69
  return Schema.decodeUnknownEffect(worker.queue.schema)(payloadOf(job.data)).pipe(
70
70
  Effect.mapError(
71
71
  (error) =>
@@ -76,8 +76,6 @@ function runQueueWorker(worker: QueueWorker, job: Job<unknown>, context: Context
76
76
  }),
77
77
  ),
78
78
  Effect.flatMap((payload) => worker.handler(payload, jobContext(job))),
79
- (effect) => traceJob(effect, job, worker.queue.name),
80
- Effect.provide(context),
81
79
  );
82
80
  }
83
81
 
@@ -86,24 +84,25 @@ async function registerQueue(
86
84
  registration: QueueWorker | QueueDefinition,
87
85
  context: Context.Context<unknown>,
88
86
  scope: Scope.Scope,
87
+ schema: string,
89
88
  ): Promise<void> {
90
89
  const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
91
90
  const name = queue.name;
92
91
  if (registration._tag === "QueueDefinition") {
93
92
  return;
94
93
  }
95
- await registerWorker(client, name, queue.workerOptions, (job) => runQueueWorker(registration, job, context), scope);
94
+ await registerWorker(client, name, queue.workerOptions, (job) => runQueueWorker(registration, job), scope, schema, queue.maxDeferrals, context);
96
95
  }
97
96
 
98
- async function registerSchedule(client: PgBossClient, worker: ScheduledWorker, context: Context.Context<unknown>, scope: Scope.Scope): Promise<void> {
97
+ async function registerSchedule(
98
+ client: PgBossClient,
99
+ worker: ScheduledWorker,
100
+ context: Context.Context<unknown>,
101
+ scope: Scope.Scope,
102
+ schema: string,
103
+ ): Promise<void> {
99
104
  const { schedule } = worker;
100
- await registerWorker(
101
- client,
102
- schedule.name,
103
- schedule.workerOptions,
104
- (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context),
105
- scope,
106
- );
105
+ await registerWorker(client, schedule.name, schedule.workerOptions, () => worker.effect, scope, schema, schedule.maxDeferrals, context);
107
106
  await client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions);
108
107
  }
109
108
 
@@ -120,6 +119,7 @@ export const registerJobs = <R>(
120
119
  registrations: readonly JobRegistration[],
121
120
  context: Context.Context<R>,
122
121
  replaceWorkers: boolean,
122
+ schema = "pgboss",
123
123
  ): Effect.Effect<void, PgBossError | PgBossQueueOptionsError, Scope.Scope> =>
124
124
  Effect.flatMap(Effect.scope, (scope) =>
125
125
  Effect.tryPromise({
@@ -130,9 +130,9 @@ export const registerJobs = <R>(
130
130
  const provided = workerContext(context);
131
131
  for (const registration of registrations) {
132
132
  if (isQueueWorker(registration) || isQueueDefinition(registration)) {
133
- await registerQueue(client, registration, provided, scope);
133
+ await registerQueue(client, registration, provided, scope, schema);
134
134
  } else if (isScheduledWorker(registration)) {
135
- await registerSchedule(client, registration, provided, scope);
135
+ await registerSchedule(client, registration, provided, scope, schema);
136
136
  }
137
137
  }
138
138
  },
@@ -1,42 +1,55 @@
1
- import { Clock, Effect, type Scope } from "effect";
1
+ import { Clock, type Context, Effect, type Scope } from "effect";
2
2
  import type { Job, WorkOptions } from "pg-boss";
3
3
  import type { PgBossClient } from "#client.ts";
4
4
  import { batchHandler } from "#internal/batchHandler.ts";
5
+ import { deferrableAttempt } from "#internal/deferrableAttempt.ts";
6
+ import { traceJob } from "#internal/tracing.ts";
5
7
 
6
8
  export function registerWorker(
7
9
  client: PgBossClient,
8
10
  name: string,
9
11
  options: WorkOptions,
10
- run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>,
12
+ run: (job: Job<unknown>) => Effect.Effect<unknown, unknown, unknown>,
11
13
  scope: Scope.Scope,
14
+ schema: string,
15
+ maxDeferrals: number,
16
+ context: Context.Context<unknown>,
12
17
  ): Promise<string> {
13
18
  const registration = client.work(
14
19
  name,
15
20
  { ...options, includeMetadata: true },
16
- batchHandler(client, name, run, async (attempt) => {
17
- if (client.findJobs === undefined || client.notifyWorker === undefined) {
18
- return;
19
- }
20
- const job = (await client.findJobs(name, { id: attempt.id }))[0];
21
- if (job?.state !== "retry") {
22
- return;
23
- }
24
- const workId = await registration;
25
- await Effect.runPromise(
26
- Effect.gen(function* () {
27
- const now = yield* Clock.currentTimeMillis;
28
- // PostgreSQL timestamps retain sub-millisecond precision that Date drops.
29
- yield* Effect.sleep(Math.max(0, job.startAfter.getTime() + 1 - now));
30
- const workerIds = client
31
- .getWipData?.()
32
- .filter((worker) => worker.workId === workId)
33
- .map((worker) => worker.id) ?? [workId];
34
- for (const workerId of workerIds) {
35
- client.notifyWorker?.(workerId);
36
- }
37
- }).pipe(Effect.forkIn(scope)),
38
- );
39
- }),
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
+ const now = yield* Clock.currentTimeMillis;
40
+ // PostgreSQL timestamps retain sub-millisecond precision that Date drops.
41
+ yield* Effect.sleep(Math.max(0, job.startAfter.getTime() + 1 - now));
42
+ const workerIds = client
43
+ .getWipData?.()
44
+ .filter((worker) => worker.workId === workId)
45
+ .map((worker) => worker.id) ?? [workId];
46
+ for (const workerId of workerIds) {
47
+ client.notifyWorker?.(workerId);
48
+ }
49
+ }).pipe(Effect.forkIn(scope)),
50
+ );
51
+ },
52
+ ),
40
53
  );
41
54
  return registration;
42
55
  }
@@ -0,0 +1,21 @@
1
+ import type { DefineScheduleOptions, ScheduleDefinition } from "#definition.ts";
2
+ import { maxDeferrals, nativeQueueOptions } from "#internal/definitionOptions.ts";
3
+
4
+ export function scheduleDefinition<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
5
+ const definition: ScheduleDefinition<TName> = {
6
+ _tag: "ScheduleDefinition",
7
+ cron: options.cron,
8
+ maxDeferrals: maxDeferrals(options.queue),
9
+ name: options.name,
10
+ queueOptions: nativeQueueOptions(options.queue),
11
+ run: (effect) =>
12
+ Object.freeze({
13
+ _tag: "ScheduledWorker" as const,
14
+ effect,
15
+ schedule: definition,
16
+ }),
17
+ scheduleOptions: Object.freeze({ tz: "UTC", ...options.schedule }),
18
+ workerOptions: Object.freeze({ batchSize: 10, burstWhenBatchFull: true, ...options.worker }),
19
+ };
20
+ return Object.freeze(definition);
21
+ }