@shivaedev/jobs 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +166 -172
- package/dist/JobFailure.d.ts +7 -0
- package/dist/JobFailure.d.ts.map +1 -0
- package/dist/JobFailure.js +8 -0
- package/dist/JobFailure.js.map +1 -0
- package/dist/server/PgBoss.d.ts +11 -0
- package/dist/server/PgBoss.d.ts.map +1 -0
- package/dist/server/PgBoss.js +4 -0
- package/dist/server/PgBoss.js.map +1 -0
- package/dist/server/client.d.ts +1 -1
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/health.d.ts +3 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js.map +1 -1
- package/dist/server/internal/batchHandler.d.ts +2 -1
- package/dist/server/internal/batchHandler.d.ts.map +1 -1
- package/dist/server/internal/batchHandler.js +49 -31
- package/dist/server/internal/batchHandler.js.map +1 -1
- package/dist/server/internal/clientErrors.d.ts.map +1 -1
- package/dist/server/internal/clientErrors.js +2 -1
- package/dist/server/internal/clientErrors.js.map +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts.map +1 -1
- package/dist/server/internal/deferrableAttempt.js +5 -1
- package/dist/server/internal/deferrableAttempt.js.map +1 -1
- package/dist/server/internal/ensureQueue.d.ts +2 -1
- package/dist/server/internal/ensureQueue.d.ts.map +1 -1
- package/dist/server/internal/ensureQueue.js +16 -21
- package/dist/server/internal/ensureQueue.js.map +1 -1
- package/dist/server/internal/failureOutput.d.ts +4 -0
- package/dist/server/internal/failureOutput.d.ts.map +1 -0
- package/dist/server/internal/failureOutput.js +9 -0
- package/dist/server/internal/failureOutput.js.map +1 -0
- package/dist/server/internal/lifecycle.d.ts +1 -1
- package/dist/server/internal/lifecycle.d.ts.map +1 -1
- package/dist/server/internal/lifecycle.js +10 -16
- package/dist/server/internal/lifecycle.js.map +1 -1
- package/dist/server/internal/listJobs.d.ts.map +1 -1
- package/dist/server/internal/listJobs.js +44 -39
- package/dist/server/internal/listJobs.js.map +1 -1
- package/dist/server/internal/makeService.js +1 -1
- package/dist/server/internal/makeService.js.map +1 -1
- package/dist/server/internal/register.d.ts +4 -3
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +34 -48
- package/dist/server/internal/register.js.map +1 -1
- package/dist/server/internal/registerReleasedJobs.d.ts +2 -1
- package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -1
- package/dist/server/internal/registerReleasedJobs.js +3 -3
- package/dist/server/internal/registerReleasedJobs.js.map +1 -1
- package/dist/server/internal/registerWorker.d.ts +3 -2
- package/dist/server/internal/registerWorker.d.ts.map +1 -1
- package/dist/server/internal/registerWorker.js +26 -33
- package/dist/server/internal/registerWorker.js.map +1 -1
- package/dist/server/internal/retryWake.d.ts +4 -0
- package/dist/server/internal/retryWake.d.ts.map +1 -0
- package/dist/server/internal/retryWake.js +32 -0
- package/dist/server/internal/retryWake.js.map +1 -0
- package/dist/server/internal/runQueueWorker.d.ts +5 -0
- package/dist/server/internal/runQueueWorker.d.ts.map +1 -0
- package/dist/server/internal/runQueueWorker.js +12 -0
- package/dist/server/internal/runQueueWorker.js.map +1 -0
- package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -1
- package/dist/server/internal/schemaIndexBuilds.js +5 -2
- package/dist/server/internal/schemaIndexBuilds.js.map +1 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +16 -5
- package/dist/server/schema.js.map +1 -1
- package/dist/server/service.d.ts +13 -8
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +53 -22
- package/dist/server/service.js.map +1 -1
- package/package.json +16 -10
- package/src/JobFailure.ts +9 -0
- package/src/server/PgBoss.ts +8 -0
- package/src/server/client.ts +1 -1
- package/src/server/health.ts +3 -2
- package/src/server/internal/batchHandler.ts +61 -42
- package/src/server/internal/clientErrors.ts +2 -1
- package/src/server/internal/deferrableAttempt.ts +5 -1
- package/src/server/internal/ensureQueue.ts +18 -20
- package/src/server/internal/failureOutput.ts +9 -0
- package/src/server/internal/lifecycle.ts +13 -16
- package/src/server/internal/listJobs.ts +57 -47
- package/src/server/internal/makeService.ts +1 -1
- package/src/server/internal/register.ts +68 -66
- package/src/server/internal/registerReleasedJobs.ts +8 -3
- package/src/server/internal/registerWorker.ts +35 -46
- package/src/server/internal/retryWake.ts +33 -0
- package/src/server/internal/runQueueWorker.ts +19 -0
- package/src/server/internal/schemaIndexBuilds.ts +6 -2
- package/src/server/schema.ts +22 -6
- package/src/server/service.ts +84 -49
package/README.md
CHANGED
|
@@ -12,13 +12,11 @@ import { Effect, Schema } from "effect";
|
|
|
12
12
|
import { Mail } from "./services.ts";
|
|
13
13
|
|
|
14
14
|
const SendEmail = defineQueue({
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
name: "send-email",
|
|
16
|
+
schema: Schema.Struct({ userId: Schema.NumberFromString }),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const emailWorker = SendEmail.handle(({ userId }) =>
|
|
20
|
-
Effect.flatMap(Mail, (mail) => mail.send(userId)),
|
|
21
|
-
);
|
|
19
|
+
const emailWorker = SendEmail.handle(({ userId }) => Effect.flatMap(Mail, (mail) => mail.send(userId)));
|
|
22
20
|
```
|
|
23
21
|
|
|
24
22
|
`Mail` is the application's Effect service, defined in the guide below. The worker receives a number even though the durable payload stores a string. The same Schema encodes the producer's payload and decodes the worker's input; malformed stored data fails before the handler runs. Provide the worker's services when the jobs Layer starts, and the handler uses that captured context when pg-boss calls it later.
|
|
@@ -31,7 +29,7 @@ A **queue definition** names a pg-boss queue and the Schema of its payload. Appl
|
|
|
31
29
|
|
|
32
30
|
A **schedule definition** describes a queue with a cron trigger. `schedule.run(effect)` pairs it with an Effect that needs no payload. Queue workers and scheduled workers are **registrations**: the values you put in the jobs Layer's `jobs` list.
|
|
33
31
|
|
|
34
|
-
The application creates one jobs service with `makePgBoss(identifier)`. Its Layer
|
|
32
|
+
The application creates one jobs service with `makePgBoss(identifier)`. Its Layer reads the shared `PgBoss` service, registers the job list and captures the services those workers need. One `pgBossLayer` owns the client and pool for the application; feature registrations drain before that Layer closes its client. Callers use the jobs service to enqueue a decoded payload, look up or retry a job, read queue health and live counts, or list failed and dead-lettered jobs.
|
|
35
33
|
|
|
36
34
|
Use a distinct literal identifier for each jobs service. The service and its Layer keep that identifier in their types, so providing a Layer for a different identifier leaves the required jobs service unprovided at compile time.
|
|
37
35
|
|
|
@@ -46,19 +44,19 @@ import { pgBossSchema } from "@shivaedev/jobs/server/schema.ts";
|
|
|
46
44
|
import { appDatabase } from "@shivaedev/server/appHost.ts";
|
|
47
45
|
|
|
48
46
|
const database = appDatabase({
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
migrations: {
|
|
48
|
+
loader,
|
|
49
|
+
components: [pgBossSchema({ schema: "app_jobs" })],
|
|
50
|
+
mode: "migrate",
|
|
51
|
+
},
|
|
54
52
|
});
|
|
55
53
|
```
|
|
56
54
|
|
|
57
55
|
The component uses `installPgBossSchema` as its only install and upgrade path. It is pending when the schema is missing or its stored version is below the installed pg-boss package's required schema version; the readiness error names the schema and both versions (`missing` for an absent version). Start every producer and worker with the same schema and `migrate: false`. Use host mode `"wait"` for services that must wait for a deployment's migration process; no additional jobs-layer wait is needed.
|
|
58
56
|
|
|
59
|
-
With server's `appDatabase({ migrations: { mode: "wait", release, ... } })`, each jobs Layer automatically reads sql's optional `WorkerRelease` Context service. When the database belongs to another release, it calls pg-boss `offWork` for every registered worker queue, lets active jobs finish, logs `Workers paused for another database release` once, and idles. Producers and enqueueing continue. When its release owns the marker again and old workers have drained, it registers workers through the normal path and logs `Workers resumed for database release`. Equality permits rollbacks; no version ordering or payload compatibility policy is added.
|
|
57
|
+
With server's `appDatabase({ migrations: { mode: "wait", release, ... } })`, each jobs Layer automatically reads sql's optional `WorkerRelease` Context service. When the database belongs to another release, it calls pg-boss `offWork` for every registered worker queue, lets active jobs finish, logs `Workers paused for another database release` once, and idles. Producers and enqueueing continue. When its release owns the marker again and old workers have drained, it registers workers through the normal path and logs `Workers resumed for database release`. Equality permits rollbacks; no version ordering or payload compatibility policy is added. Without that service, the Layer registers its workers at startup and never pauses them.
|
|
60
58
|
|
|
61
|
-
A worker keeps its sql
|
|
59
|
+
A paused worker keeps its process registered with sql's worker release while active jobs drain, so the owning release waits for it; [sql's worker release](https://github.com/ShivaeDev/platform/blob/main/packages/sql/README.md) owns the heartbeat and how long a crashed worker can block. Closing the jobs Layer's scope waits for the native pg-boss `stop` before releasing the services the Layer captured. Choose the three bounds together: each queue's `expireInSeconds`, a `stop: { graceful: true, timeout }` in milliseconds that covers it, and a host termination grace beyond that timeout. For example, a queue with `expireInSeconds: 60` pairs with `stop: { graceful: true, timeout: 65_000 }` under a 75-second termination grace.
|
|
62
60
|
|
|
63
61
|
`installPgBossSchema` installs or upgrades pg-boss through the provided Effect SQL client without starting workers or schedules. Run it outside an application transaction, in a release step or the `fixtures` Effect of `makePostgresTestLayer`, so stories inherit the schema from the migrated template:
|
|
64
62
|
|
|
@@ -70,7 +68,7 @@ const jobsFixture = installPgBossSchema({ schema: "app_jobs" });
|
|
|
70
68
|
|
|
71
69
|
The Effect requires `SqlClient` and fails with `PgBossError` whose `operation` is `"install"`. The schema defaults to `"pgboss"`. Repeating installation preserves existing jobs. Upgrades apply pg-boss's migration SQL and concurrent index builds, including partition indexes, before returning. Pending index commands are recorded in pg-boss's BAM table in the same transaction as the schema version. A retry resumes unfinished commands and repairs invalid indexes left by failed builds, even when the schema version has already committed.
|
|
72
70
|
|
|
73
|
-
After installation, pass the same schema and `migrate: false` to `
|
|
71
|
+
After installation, pass the same schema and `migrate: false` to `pgBossLayer` to start producers and workers without schema installation or migration. Startup fails if the schema is missing. Ordinary Layer startup retains pg-boss's default migration behavior when `migrate` is omitted.
|
|
74
72
|
|
|
75
73
|
### Once per application: the jobs service
|
|
76
74
|
|
|
@@ -78,19 +76,22 @@ Define the application's jobs service and worker dependencies in `services.ts`:
|
|
|
78
76
|
|
|
79
77
|
```ts
|
|
80
78
|
import { makePgBoss } from "@shivaedev/jobs/server/service.ts";
|
|
81
|
-
import {
|
|
79
|
+
import { Effect } from "effect";
|
|
80
|
+
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
82
81
|
|
|
83
82
|
export const Jobs = makePgBoss("@app/Jobs");
|
|
84
83
|
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
export const Mail = defineService({
|
|
85
|
+
id: "@app/Mail",
|
|
86
|
+
requires: [],
|
|
87
|
+
initialize: Effect.void,
|
|
88
|
+
methods: () => ({
|
|
89
|
+
send: (userId: number) => Effect.log({ event: "send_email", userId }),
|
|
90
|
+
removeExpired: () => Effect.log({ event: "remove_expired_mail" }),
|
|
91
|
+
}),
|
|
93
92
|
});
|
|
93
|
+
|
|
94
|
+
export const MailLive = Mail.layer;
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
`Jobs` is the service that callers yield. `Mail` is a separate domain service that workers use. The example implementation logs calls so the wiring is complete; an application supplies its own implementation of those methods. The jobs package does not need a provider-specific interface.
|
|
@@ -105,27 +106,25 @@ import { Effect, Schema } from "effect";
|
|
|
105
106
|
import { Mail } from "./services.ts";
|
|
106
107
|
|
|
107
108
|
export const SendEmail = defineQueue({
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
name: "send-email",
|
|
110
|
+
schema: Schema.Struct({ userId: Schema.NumberFromString }),
|
|
111
|
+
queue: { retryLimit: 5 },
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
export const emailWorker = SendEmail.handle(({ userId }, job) =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
const mail = yield* Mail;
|
|
117
|
+
yield* mail.send(userId);
|
|
118
|
+
yield* Effect.log({ jobId: job.id });
|
|
119
|
+
}),
|
|
119
120
|
);
|
|
120
121
|
|
|
121
122
|
export const Cleanup = defineSchedule({
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
name: "cleanup-mail",
|
|
124
|
+
cron: "17 3 * * *",
|
|
124
125
|
});
|
|
125
126
|
|
|
126
|
-
export const cleanupWorker = Cleanup.run(
|
|
127
|
-
Effect.flatMap(Mail, (mail) => mail.removeExpired),
|
|
128
|
-
);
|
|
127
|
+
export const cleanupWorker = Cleanup.run(Effect.flatMap(Mail, (mail) => mail.removeExpired()));
|
|
129
128
|
```
|
|
130
129
|
|
|
131
130
|
The queue takes `{ userId: number }` in application code and stores `{ userId: string }`. Its handler receives the decoded number and a job context, including the job's `id`. Keep the queue definition available to producers; the runtime registers the worker created by `handle`.
|
|
@@ -142,16 +141,16 @@ Queue and schedule workers default to `batchSize: 10` and `burstWhenBatchFull: t
|
|
|
142
141
|
|
|
143
142
|
```ts
|
|
144
143
|
const Compare = defineQueue({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
name: "compare",
|
|
145
|
+
schema: Schema.Struct({ id: Schema.Number }),
|
|
146
|
+
queue: { notify: true },
|
|
147
|
+
worker: { batchSize: 2, localConcurrency: 5 },
|
|
149
148
|
});
|
|
150
149
|
```
|
|
151
150
|
|
|
152
151
|
This worker runs up to five handlers at once. `worker` accepts pg-boss 12's `WorkOptions`, including `batchSize`, `localConcurrency`, `pollingIntervalSeconds`, `notifyPollingIntervalSeconds` and the burst options. Explicit options override the adapter defaults. pg-boss ignores `burstWhenBatchFull` at `batchSize: 1`; keep a batch size of at least two for immediate backlog draining. Each job is settled separately through pg-boss: success completes that job, and a typed failure, payload decoding failure or defect fails only that job with its own error. A failed job does not spend the retries of other jobs in its batch or rerun their successful handlers. Interruption stops the batch and leaves the interrupted and remaining jobs to pg-boss expiry; handlers must still tolerate repeated side effects across retries and interrupted attempts.
|
|
153
152
|
|
|
154
|
-
Apps do not need to shorten the polling backstop to drain a backlog. With `notify: true` and an active LISTEN/NOTIFY listener, pg-boss wakes workers on notifications and defaults to a 30-second backstop (`notifyPollingIntervalSeconds`). Without active notifications its base poll defaults to two seconds (`pollingIntervalSeconds`). Bursting takes precedence over both intervals. After an adapter worker fails a job, it reads pg-boss's stored retry time and schedules a wake for every native worker in its registration at that time, so an idle sibling can retry while another worker is busy. The wake belongs to the worker Layer's scope and is canceled when that scope closes. Override the backstop when the application needs a different bound for other work that becomes runnable without a notification, such as delayed jobs or retries initiated outside the adapter worker.
|
|
153
|
+
Apps do not need to shorten the polling backstop to drain a backlog. With `notify: true` and an active LISTEN/NOTIFY listener, pg-boss wakes workers on notifications and defaults to a 30-second backstop (`notifyPollingIntervalSeconds`). Without active notifications its base poll defaults to two seconds (`pollingIntervalSeconds`). Bursting takes precedence over both intervals. After an adapter worker fails a job, it reads pg-boss's stored retry time and schedules a wake for every native worker in its registration at that time, so an idle sibling can retry while another worker is busy. The wake, including its retry-time read, belongs to the worker Layer's scope and is canceled when that scope closes. Wake failures are logged with redacted diagnostics and do not fail the batch. Override the backstop when the application needs a different bound for other work that becomes runnable without a notification, such as delayed jobs or retries initiated outside the adapter worker.
|
|
155
154
|
|
|
156
155
|
### Defer until a provider is ready
|
|
157
156
|
|
|
@@ -161,10 +160,12 @@ Fail a queue handler or scheduled Effect with `RetryAt` when a provider gives a
|
|
|
161
160
|
import { DateTime, Effect } from "effect";
|
|
162
161
|
import { RetryAt } from "@shivaedev/jobs/server/definition.ts";
|
|
163
162
|
|
|
164
|
-
const waitForQuota = Effect.fail(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
const waitForQuota = Effect.fail(
|
|
164
|
+
new RetryAt({
|
|
165
|
+
at: DateTime.makeUnsafe("2030-01-01T00:01:00Z"),
|
|
166
|
+
reason: "Provider quota resets",
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
The same job returns to waiting with `startAfter` set to `at`; its ID, singleton key, payload and retry count stay unchanged. A past or current `at` is eligible on the next fetch. Deferral does not dead-letter the job or call `onError`. Its job span ends OK with a `job.deferred` event whose `at` is an ISO UTC string and whose `reason` is the supplied string. Deferred jobs rely on polling, including queues using `notify: true`. Stately jobs sharing a singleton key can defer together and still run one at a time. While waiting, their native job metadata reports the adapter's `effect-stately-deferred` policy; a database trigger restores `stately` when pg-boss claims them, preserving its active-job exclusion.
|
|
@@ -177,19 +178,17 @@ Put related job kinds in one tagged-union queue and choose the entity key in the
|
|
|
177
178
|
|
|
178
179
|
```ts
|
|
179
180
|
const PullRequestJobs = defineQueue({
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
name: "pull-request",
|
|
182
|
+
schema: Schema.Union([
|
|
183
|
+
Schema.TaggedStruct("Refresh", { pullRequestId: Schema.NumberFromString }),
|
|
184
|
+
Schema.TaggedStruct("Reconcile", { pullRequestId: Schema.NumberFromString }),
|
|
185
|
+
]),
|
|
186
|
+
queue: { policy: "singleton" },
|
|
187
|
+
key: ({ pullRequestId }) => `pr:${pullRequestId.toFixed(0)}`,
|
|
188
|
+
worker: { batchSize: 1, localConcurrency: 3 },
|
|
188
189
|
});
|
|
189
190
|
|
|
190
|
-
const refresh = Effect.flatMap(Jobs, (jobs) =>
|
|
191
|
-
jobs.enqueue(PullRequestJobs, { _tag: "Refresh", pullRequestId: 42 }),
|
|
192
|
-
);
|
|
191
|
+
const refresh = Effect.flatMap(Jobs, (jobs) => jobs.enqueue(PullRequestJobs, { _tag: "Refresh", pullRequestId: 42 }));
|
|
193
192
|
```
|
|
194
193
|
|
|
195
194
|
`queue.policy` accepts pg-boss's policy literals and rejects typos. The `singleton` policy requires `key: (payload) => string`. The callback sees the decoded payload, so `pullRequestId` is a number. `enqueue` supplies pg-boss's `singletonKey` automatically; callers cannot supply a conflicting key in send options.
|
|
@@ -204,15 +203,15 @@ pg-boss marks every job in a fetched batch active together. Each job's expiry wi
|
|
|
204
203
|
|
|
205
204
|
```ts
|
|
206
205
|
const Ingest = defineQueue({
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
name: "ingest",
|
|
207
|
+
schema: Schema.Struct({ sourceId: Schema.String }),
|
|
208
|
+
queue: { expireInSeconds: 300 },
|
|
209
|
+
worker: { batchSize: 1 },
|
|
211
210
|
});
|
|
212
211
|
|
|
213
212
|
const IndexTitle = defineQueue({
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
name: "index-title",
|
|
214
|
+
schema: Schema.Struct({ titleId: Schema.String }),
|
|
216
215
|
});
|
|
217
216
|
```
|
|
218
217
|
|
|
@@ -228,10 +227,10 @@ Queue handlers can yield `job.remainingBudget`, an `Effect.Effect<Duration.Durat
|
|
|
228
227
|
import { Duration, Effect } from "effect";
|
|
229
228
|
|
|
230
229
|
const ingestWorker = Ingest.handle(({ sourceId }, job) =>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
Effect.gen(function* () {
|
|
231
|
+
const budget = yield* job.remainingBudget;
|
|
232
|
+
yield* Effect.log({ sourceId, remainingMilliseconds: Duration.toMillis(budget) });
|
|
233
|
+
}),
|
|
235
234
|
);
|
|
236
235
|
```
|
|
237
236
|
|
|
@@ -242,36 +241,36 @@ Keep handler work interruptible and use cancellation-aware Effect integrations f
|
|
|
242
241
|
Build the jobs Layer in `runtime.ts`:
|
|
243
242
|
|
|
244
243
|
```ts
|
|
245
|
-
import { Layer } from "effect";
|
|
244
|
+
import { Layer, type Redacted } from "effect";
|
|
245
|
+
import { pgBossLayer } from "@shivaedev/jobs/server/service.ts";
|
|
246
246
|
import { cleanupWorker, emailWorker } from "./jobs.ts";
|
|
247
247
|
import { Jobs, MailLive } from "./services.ts";
|
|
248
248
|
|
|
249
|
-
export const jobsLayer = (databaseUrl: string) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}).pipe(Layer.provide(MailLive));
|
|
249
|
+
export const jobsLayer = (databaseUrl: Redacted.Redacted<string>) =>
|
|
250
|
+
Jobs.layer({ jobs: [emailWorker, cleanupWorker] }).pipe(
|
|
251
|
+
Layer.provide(MailLive),
|
|
252
|
+
Layer.provide(pgBossLayer({ connectionString: databaseUrl, schema: "app_jobs" })),
|
|
253
|
+
);
|
|
255
254
|
```
|
|
256
255
|
|
|
257
|
-
The
|
|
256
|
+
The redacted connection string is unwrapped only at the pg-boss adapter. Put connection, pool size, client factory, schema, migration and stop settings in `pgBossLayer`. Provide the same Layer to the merged registration, worker and producer services so native Layer memoization starts one client. `jobs` is the complete list of registrations for this Layer. The Layer captures `MailLive` for both the queue handler and the scheduled Effect. A startup or registration failure fails Layer acquisition with `PgBossError` or `PgBossQueueOptionsError` and closes its registrations. The owning app Layer releases the client. Registration creates a queue again, up to six times with a short backoff, when PostgreSQL rolls the creation back as a deadlock victim (SQLSTATE `40P01`), which a partitioned queue can meet when other queues in the schema have traffic. A shared client stays running while another scope still owns a reference.
|
|
258
257
|
|
|
259
258
|
Keep the Layer in the application's worker runtime scope. A short scope is useful for an acquisition or health check; a worker process needs its scope to remain open while it handles jobs.
|
|
260
259
|
|
|
261
|
-
Set `clientCacheKey` to share one reference-counted client across Layer builds with equal pg-boss constructor settings. Reusing a key with different settings fails acquisition with `PgBossClientOptionsError` before constructing another client. Its `key`, `options` and message identify the key and differing option names without exposing connection values. Nested options are compared by value.
|
|
260
|
+
Set `clientCacheKey` on `pgBossLayer` to share one reference-counted client across Layer builds with equal pg-boss constructor settings. Reusing a key with different settings fails acquisition with `PgBossClientOptionsError` before constructing another client. Its `key`, `options` and message identify the key and differing option names without exposing connection values. Nested options are compared by value.
|
|
262
261
|
|
|
263
262
|
```ts
|
|
264
|
-
import { Effect } from "effect";
|
|
263
|
+
import { Effect, type Redacted } from "effect";
|
|
265
264
|
import { jobsLayer } from "./runtime.ts";
|
|
266
265
|
import { Jobs } from "./services.ts";
|
|
267
266
|
|
|
268
|
-
const inspectJobs = (databaseUrl: string) =>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
267
|
+
const inspectJobs = (databaseUrl: Redacted.Redacted<string>) =>
|
|
268
|
+
Effect.scoped(
|
|
269
|
+
Effect.gen(function* () {
|
|
270
|
+
const jobs = yield* Jobs;
|
|
271
|
+
return yield* jobs.health();
|
|
272
|
+
}).pipe(Effect.provide(jobsLayer(databaseUrl))),
|
|
273
|
+
);
|
|
275
274
|
```
|
|
276
275
|
|
|
277
276
|
This Effect starts the jobs Layer, reads health and closes the scope. The client stops when that scope closes.
|
|
@@ -284,10 +283,10 @@ import { SendEmail } from "./jobs.ts";
|
|
|
284
283
|
import { Jobs } from "./services.ts";
|
|
285
284
|
|
|
286
285
|
export const enqueueEmail = (userId: number) =>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
286
|
+
Effect.gen(function* () {
|
|
287
|
+
const jobs = yield* Jobs;
|
|
288
|
+
return yield* jobs.enqueue(SendEmail, { userId });
|
|
289
|
+
});
|
|
291
290
|
```
|
|
292
291
|
|
|
293
292
|
Provide `Jobs` through the application's existing runtime. `enqueue` takes the decoded payload, so passing a string `userId` is a type error. It encodes the number to a string before calling pg-boss. The result type is `Option.Option<string>`: a job identifier becomes `Some`, and a `null` send result becomes `None`.
|
|
@@ -309,15 +308,15 @@ import { Effect } from "effect";
|
|
|
309
308
|
import { Jobs } from "./services.ts";
|
|
310
309
|
|
|
311
310
|
export const readJobCounts = Effect.gen(function* () {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
311
|
+
const jobs = yield* Jobs;
|
|
312
|
+
const health = yield* jobs.health();
|
|
313
|
+
return {
|
|
314
|
+
active: health.activeTotal,
|
|
315
|
+
deadLettered: health.deadLetteredTotal,
|
|
316
|
+
failed: health.failedTotal,
|
|
317
|
+
queued: health.queuedTotal,
|
|
318
|
+
ready: health.readyTotal,
|
|
319
|
+
};
|
|
321
320
|
});
|
|
322
321
|
```
|
|
323
322
|
|
|
@@ -330,15 +329,16 @@ import { Effect, Option } from "effect";
|
|
|
330
329
|
import { SendEmail } from "./jobs.ts";
|
|
331
330
|
import { Jobs } from "./services.ts";
|
|
332
331
|
|
|
333
|
-
export const inspectEmail = (id: string) =>
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
332
|
+
export const inspectEmail = (id: string) =>
|
|
333
|
+
Effect.gen(function* () {
|
|
334
|
+
const jobs = yield* Jobs;
|
|
335
|
+
const job = yield* jobs.getJob(SendEmail, id);
|
|
336
|
+
if (Option.isSome(job) && job.value.state === "failed") {
|
|
337
|
+
yield* jobs.retry(SendEmail, id);
|
|
338
|
+
}
|
|
339
|
+
const counts = yield* jobs.counts(SendEmail);
|
|
340
|
+
return counts.queuedCount === 0 && counts.activeCount === 0;
|
|
341
|
+
});
|
|
342
342
|
```
|
|
343
343
|
|
|
344
344
|
`counts(queue)` reads the job table directly on each call. `queuedCount` includes created and retry-waiting jobs, including future jobs; `activeCount` counts running jobs, `failedCount` counts retained failures, and `totalCount` includes retained terminal jobs. A queue has drained when queued and active counts are both zero. These counts are a snapshot, so a producer can enqueue more work after the query returns.
|
|
@@ -352,23 +352,24 @@ import { Effect, Option } from "effect";
|
|
|
352
352
|
import { SendEmail } from "./jobs.ts";
|
|
353
353
|
import { Jobs } from "./services.ts";
|
|
354
354
|
|
|
355
|
-
export const failedEmails = (cursor?: string) =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
355
|
+
export const failedEmails = (cursor?: string) =>
|
|
356
|
+
Effect.gen(function* () {
|
|
357
|
+
const jobs = yield* Jobs;
|
|
358
|
+
const failed = yield* jobs.failedJobs(SendEmail, { cursor, limit: 20 });
|
|
359
|
+
const deadLettered = yield* jobs.deadLetteredJobs(SendEmail, { limit: 20 });
|
|
360
|
+
return {
|
|
361
|
+
deadLettered: deadLettered.jobs.map((job) => ({ id: job.id, original: Option.getOrNull(job.sourceId), error: job.sourceOutput })),
|
|
362
|
+
failed: failed.jobs.map((job) => ({ id: job.id, attempt: job.attempt, error: job.output, userId: job.data.userId })),
|
|
363
|
+
next: Option.getOrUndefined(failed.next),
|
|
364
|
+
};
|
|
365
|
+
});
|
|
365
366
|
```
|
|
366
367
|
|
|
367
368
|
`health` and `counts` say how many jobs failed; these two listings say which ones and why. `failedJobs(queue, options)` lists the queue's jobs in the `failed` state, the ones that used up their retries. `deadLetteredJobs(queue, options)` lists every job in the queue's dead-letter queue. Dead-letter entries live in the `-dlq` queue: each is a new job with its own ID that pg-boss copied there when the original failed for good. Both listings read pg-boss's `job` table on each call, through the jobs Layer's own pg-boss connection and in its configured schema, so applications never query pg-boss tables themselves. A custom client needs the native `getDb` capability for them.
|
|
368
369
|
|
|
369
|
-
Each page holds at most `limit` jobs, newest first by creation time; a dead-letter copy is created when its original fails for good. `limit` must be a positive integer. When more jobs remain, `next` is `Some` opaque cursor; pass it back as `cursor` for the following page, and stop when `next` is `None`. Paging continues from the last job it returned, so jobs that fail while you page appear in a fresh listing instead of shifting later pages, and no job appears twice. A malformed cursor or limit fails with `
|
|
370
|
+
Each page holds at most `limit` jobs, newest first by creation time; a dead-letter copy is created when its original fails for good. `limit` must be a positive integer. When more jobs remain, `next` is `Some` opaque cursor; pass it back as `cursor` for the following page, and stop when `next` is `None`. Paging continues from the last job it returned, so jobs that fail while you page appear in a fresh listing instead of shifting later pages, and no job appears twice. A malformed cursor or limit fails with `SchemaError` before pg-boss is called.
|
|
370
371
|
|
|
371
|
-
An entry carries the job's `id`, `queue`, native `state`, `retryCount`, `retryLimit` and `attempt`, the one-based number of its last started attempt (zero for a job that never started). `createdOn` is a UTC `DateTime`; `startedOn` and `completedOn` are `Option`s of UTC `DateTime`s. `output` is
|
|
372
|
+
An entry carries the job's `id`, `queue`, native `state`, `retryCount`, `retryLimit` and `attempt`, the one-based number of its last started attempt (zero for a job that never started). `createdOn` is a UTC `DateTime`; `startedOn` and `completedOn` are `Option`s of UTC `DateTime`s. `output` is `JobFailure | null`, decoded through `JobFailure` from `@shivaedev/jobs/JobFailure.ts`. Handler failures store only `{ _tag: string, message: string }`, with the message redacted through `@shivaedev/errors`; stack traces, cause chains and other error fields are omitted. `null` means pg-boss stored no output. Native pg-boss failures without a tag decode with `_tag: "Error"`; decoding also redacts messages from older stored failures. Stored output that does not match the Schema fails the listing with `PgBossPayloadError`. `data` is decoded by the queue's Schema, including traced payloads; invalid stored data fails the listing with `PgBossPayloadError`, as `getJob` does, and the Effect requires the Schema's decoding services. A dead-letter entry adds `sourceId`, the original job's ID as an `Option`, and `sourceOutput`, the original's decoded `JobFailure | null`. The copy has not run, so its own `output` stays `null` and its `attempt` is zero.
|
|
372
373
|
|
|
373
374
|
Pass a failed entry's `id` to `retry(queue, id)` to run that job again under its original ID; once it completes it leaves the failed listing. Dead-letter IDs belong to the `-dlq` queue, so `retry(queue, id)` does not find them and leaves them unchanged.
|
|
374
375
|
|
|
@@ -381,20 +382,19 @@ Startup creates missing queues and reconciles declared settings on existing queu
|
|
|
381
382
|
### Share a client between Layer builds
|
|
382
383
|
|
|
383
384
|
```ts
|
|
384
|
-
import { Layer } from "effect";
|
|
385
|
+
import { Layer, type Redacted } from "effect";
|
|
386
|
+
import { pgBossLayer } from "@shivaedev/jobs/server/service.ts";
|
|
385
387
|
import { cleanupWorker, emailWorker } from "./jobs.ts";
|
|
386
388
|
import { Jobs, MailLive } from "./services.ts";
|
|
387
389
|
|
|
388
|
-
const sharedJobsLayer = (databaseUrl: string) =>
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
jobs: [emailWorker, cleanupWorker],
|
|
394
|
-
}).pipe(Layer.provide(MailLive));
|
|
390
|
+
const sharedJobsLayer = (databaseUrl: Redacted.Redacted<string>) =>
|
|
391
|
+
Jobs.layer({ jobs: [emailWorker, cleanupWorker] }).pipe(
|
|
392
|
+
Layer.provide(MailLive),
|
|
393
|
+
Layer.provide(pgBossLayer({ connectionString: databaseUrl, schema: "app_jobs", clientCacheKey: "app-jobs" })),
|
|
394
|
+
);
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
Overlapping Layer builds with the same `clientCacheKey` share one started client. The later build replaces the registered worker, and the final scope release stops the shared client. A later build after that release starts a new client.
|
|
397
|
+
Overlapping Layer builds with the same `clientCacheKey` share one started client. The later build replaces the registered worker, and the final scope release stops the shared client. A later build after that release starts a new client. Within one Layer graph, merged feature services share the provided app client without a cache key. Separate app Layer builds create separate clients without a key. The application chooses whether to pass a key; give each distinct constructor configuration its own key.
|
|
398
398
|
|
|
399
399
|
### Separate producers and workers
|
|
400
400
|
|
|
@@ -404,11 +404,10 @@ Pass a queue definition directly in the jobs Layer to create its queue and dead-
|
|
|
404
404
|
import { SendEmail } from "./jobs.ts";
|
|
405
405
|
import { Jobs } from "./services.ts";
|
|
406
406
|
|
|
407
|
-
export const producerLayer = (
|
|
408
|
-
Jobs.layer({ connectionString: databaseUrl, jobs: [SendEmail] });
|
|
407
|
+
export const producerLayer = Jobs.layer({ jobs: [SendEmail] });
|
|
409
408
|
```
|
|
410
409
|
|
|
411
|
-
Provide this Layer
|
|
410
|
+
Provide the app `pgBossLayer` to this Layer in the request runtime and call `jobs.enqueue(SendEmail, { userId })` as above. A separate worker process registers `SendEmail.handle(...)` against the same database. Producer queues use the same retry options and appear in `health`; only worker registrations capture handler services.
|
|
412
411
|
|
|
413
412
|
### Enqueue follow-ups from a handler
|
|
414
413
|
|
|
@@ -416,30 +415,27 @@ A handler can yield the same `Jobs` service that owns it. The Layer supplies tha
|
|
|
416
415
|
|
|
417
416
|
```ts
|
|
418
417
|
const Receipt = defineQueue({
|
|
419
|
-
|
|
420
|
-
|
|
418
|
+
name: "receipt",
|
|
419
|
+
schema: Schema.Struct({ userId: Schema.Number }),
|
|
421
420
|
});
|
|
422
421
|
|
|
423
422
|
const sendAndQueueReceipt = SendEmail.handle(({ userId }) =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
423
|
+
Effect.gen(function* () {
|
|
424
|
+
const mail = yield* Mail;
|
|
425
|
+
yield* mail.send(userId);
|
|
426
|
+
const jobs = yield* Jobs;
|
|
427
|
+
yield* jobs.enqueue(Receipt, { userId });
|
|
428
|
+
}),
|
|
430
429
|
);
|
|
431
430
|
|
|
432
|
-
const live = Jobs.layer({
|
|
433
|
-
connectionString: databaseUrl,
|
|
434
|
-
jobs: [sendAndQueueReceipt, Receipt],
|
|
435
|
-
}).pipe(Layer.provide(MailLive));
|
|
431
|
+
const live = Jobs.layer({ jobs: [sendAndQueueReceipt, Receipt] }).pipe(Layer.provide(MailLive));
|
|
436
432
|
```
|
|
437
433
|
|
|
438
434
|
Include every target queue in `jobs`, as either a worker or a producer-only definition. Startup creates all queues before starting consumers. The example registers `Receipt` for another process to consume. A follow-up target can also have a worker in the same Layer. Scheduled effects can yield `Jobs` too. Keep one jobs Layer in the process's native Layer graph; that Layer owns one client for enqueueing and handling.
|
|
439
435
|
|
|
440
436
|
### Test through the queue contract
|
|
441
437
|
|
|
442
|
-
`testing.ts` gives story kits a controller for one real pg-boss client. Create a fresh controller per runtime, pass its `clientFactory` to
|
|
438
|
+
`testing.ts` gives story kits a controller for one real pg-boss client. Create a fresh controller per runtime, pass its `clientFactory` to `pgBossLayer`, and keep that Layer scoped as usual. `jobs(queue, options?)` returns native job metadata with `data` decoded through the queue's Schema, including traced payloads. Invalid durable data fails with `PgBossPayloadError`; decoding services remain in the Effect's requirements.
|
|
443
439
|
|
|
444
440
|
This producer story uses the `SendEmail` definition and `Jobs` service from above. `databaseUrl` points to the story's PostgreSQL database:
|
|
445
441
|
|
|
@@ -447,32 +443,29 @@ This producer story uses the `SendEmail` definition and `Jobs` service from abov
|
|
|
447
443
|
import { Context, Layer, Option } from "effect";
|
|
448
444
|
import { expect } from "vitest";
|
|
449
445
|
import { storyKit } from "@shivaedev/test-story/server/storyKit.ts";
|
|
446
|
+
import { pgBossLayer } from "@shivaedev/jobs/server/service.ts";
|
|
450
447
|
import { makePgBossTesting, testRetryPolicy } from "@shivaedev/jobs/server/testing.ts";
|
|
451
448
|
|
|
452
449
|
const emailStories = storyKit({
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
testing,
|
|
468
|
-
}),
|
|
450
|
+
clock: "live",
|
|
451
|
+
*create() {
|
|
452
|
+
const testing = makePgBossTesting({ retryPolicy: testRetryPolicy });
|
|
453
|
+
const context = yield* Layer.build(
|
|
454
|
+
Jobs.layer({ jobs: [SendEmail] }).pipe(Layer.provide(pgBossLayer({ connectionString: databaseUrl, clientFactory: testing.clientFactory }))),
|
|
455
|
+
);
|
|
456
|
+
return { jobs: Context.get(context, Jobs), testing };
|
|
457
|
+
},
|
|
458
|
+
name: "queued email",
|
|
459
|
+
stages: ["ready"],
|
|
460
|
+
verbs: ({ jobs, testing }) => ({
|
|
461
|
+
enqueue: (userId: number) => jobs.enqueue(SendEmail, { userId }),
|
|
462
|
+
testing,
|
|
463
|
+
}),
|
|
469
464
|
});
|
|
470
465
|
|
|
471
466
|
emailStories.it("decodes the queued email", [], function* ({ enqueue, testing }) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
{ id, data: { userId: 42 }, state: "created" },
|
|
475
|
-
]);
|
|
467
|
+
const id = Option.getOrThrow(yield* enqueue(42));
|
|
468
|
+
expect(yield* testing.jobs(SendEmail)).toMatchObject([{ id, data: { userId: 42 }, state: "created" }]);
|
|
476
469
|
});
|
|
477
470
|
```
|
|
478
471
|
|
|
@@ -500,15 +493,16 @@ A kit can also capture `revision` before reading jobs and yield `waitForChange(r
|
|
|
500
493
|
|
|
501
494
|
Import the module that defines the name; the package has no root entry.
|
|
502
495
|
|
|
503
|
-
| Module
|
|
504
|
-
|
|
|
496
|
+
| Module | Public names |
|
|
497
|
+
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
505
498
|
| `@shivaedev/jobs/server/definition.ts` | `defineQueue`, `defineSchedule`, `jobContext`, `RetryAt`; `DefineQueueOptions`, `DefineScheduleOptions`, `JobContext`, `JobPayloadSchema`, `JobRegistration`, `QueueDefinition`, `QueueOptions`, `QueuePolicy`, `QueueWorker`, `RegistrationRequirements`, `ScheduleDefinition`, `ScheduledWorker` |
|
|
506
|
-
| `@shivaedev/jobs/server/service.ts`
|
|
507
|
-
| `@shivaedev/jobs/server/
|
|
508
|
-
| `@shivaedev/jobs/server/
|
|
509
|
-
| `@shivaedev/jobs/server/
|
|
510
|
-
| `@shivaedev/jobs/server/
|
|
511
|
-
| `@shivaedev/jobs/server/
|
|
499
|
+
| `@shivaedev/jobs/server/service.ts` | `makePgBoss`, `pgBossLayer`; `PgBossDefinition`, `PgBossLayerOptions`, `PgBossOptions`, `PgBossService` |
|
|
500
|
+
| `@shivaedev/jobs/server/PgBoss.ts` | `PgBoss`: the shared client Context service. |
|
|
501
|
+
| `@shivaedev/jobs/server/schema.ts` | `installPgBossSchema`, `pgBossSchema`; `PgBossSchemaOptions` |
|
|
502
|
+
| `@shivaedev/jobs/server/health.ts` | `deadLetterQueueName`; `DeadLetteredJob`, `JobPage`, `JobPageOptions`, `JobsHealth`, `ListedJob`, `QueueCounts`, `QueueHealth` |
|
|
503
|
+
| `@shivaedev/jobs/server/error.ts` | `PgBossError`, `PgBossPayloadError`, `PgBossQueueOptionsError`, `PgBossClientOptionsError`, `PgBossTestingError`, `toPgBossError`; `PgBossOperation` |
|
|
504
|
+
| `@shivaedev/jobs/server/client.ts` | `defaultClientFactory`; `PgBossClient`, `PgBossClientFactory` |
|
|
505
|
+
| `@shivaedev/jobs/server/testing.ts` | `makePgBossTesting`, `testRetryPolicy`; `PgBossTesting`, `PgBossTestingOptions`, `SnapshotQueue`, `UnfinishedJob` |
|
|
512
506
|
|
|
513
507
|
The definition options keep the pg-boss names: `queue` uses `QueueOptions` (`Queue` except `name` and `deadLetter`, with literal `QueuePolicy` values and `maxDeferrals`), `worker` uses `WorkOptions`, and a schedule's `schedule` uses `ScheduleOptions`. A queue also requires `name` and `schema`; a schedule requires `name` and `cron`.
|
|
514
508
|
|
|
@@ -523,11 +517,11 @@ Worker registration always requests `includeMetadata: true`, including when `wor
|
|
|
523
517
|
### Install, setup and limits
|
|
524
518
|
|
|
525
519
|
```sh
|
|
526
|
-
pnpm add @shivaedev/jobs effect
|
|
520
|
+
pnpm add @shivaedev/jobs effect pg-boss
|
|
527
521
|
```
|
|
528
522
|
|
|
529
523
|
The package requires Node.js 24 or later and PostgreSQL. `effect` and `pg-boss` are peers; use versions that satisfy the package's published peer ranges.
|
|
530
524
|
|
|
531
|
-
`connectionString` configures pg-boss's own `pg` Pool, separately from the application's Effect SQL pool
|
|
525
|
+
`connectionString` configures pg-boss's own `pg` Pool, separately from the application's Effect SQL pool, and pg-boss's driver decides how its `sslmode` behaves. Set the TLS mode in that URL; the [framework database connection security guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/quickstart.md#database-connection-security) compares the drivers and the application's connection paths.
|
|
532
526
|
|
|
533
527
|
Provide the database connection and job registrations at the application runtime boundary. Application services own the domain work and provider calls; pg-boss owns queue execution. When `SqlClient` is provided and a SQL transaction is active, `enqueue` uses pg-boss’s per-call `db` option on that transaction’s connection. The application write and queued jobs commit or roll back together. The jobs client and SQL client must use the same PostgreSQL database. Outside a transaction, enqueueing keeps using the jobs client as usual. To keep enqueueing independent of an application transaction, move it after the transaction returns and handle its failure separately.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export declare const JobFailure: Schema.Struct<{
|
|
3
|
+
readonly _tag: Schema.withDecodingDefaultKey<Schema.String, never>;
|
|
4
|
+
readonly message: Schema.decodeTo<Schema.String, Schema.String, never, never>;
|
|
5
|
+
}>;
|
|
6
|
+
export type JobFailure = typeof JobFailure.Type;
|
|
7
|
+
//# sourceMappingURL=JobFailure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JobFailure.d.ts","sourceRoot":"","sources":["../src/JobFailure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAItD,eAAO,MAAM,UAAU;;;EAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect, Schema, SchemaGetter } from "effect";
|
|
2
|
+
import { isSensitiveKey, redactText } from "@shivaedev/errors/sensitive.ts";
|
|
3
|
+
const redactMessage = SchemaGetter.transform((message) => redactText(message, isSensitiveKey));
|
|
4
|
+
export const JobFailure = Schema.Struct({
|
|
5
|
+
_tag: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("Error"))),
|
|
6
|
+
message: Schema.String.pipe(Schema.decodeTo(Schema.String, { decode: redactMessage, encode: redactMessage })),
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=JobFailure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JobFailure.js","sourceRoot":"","sources":["../src/JobFailure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAE5E,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;AACvG,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAChF,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;CAC7G,CAAC,CAAC"}
|