@shivaedev/effect-pg-boss 0.9.0 → 0.10.1

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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +28 -25
  3. package/dist/server/client.d.ts +2 -0
  4. package/dist/server/client.d.ts.map +1 -1
  5. package/dist/server/client.js.map +1 -1
  6. package/dist/server/error.d.ts +1 -1
  7. package/dist/server/error.d.ts.map +1 -1
  8. package/dist/server/internal/batchHandler.d.ts +1 -1
  9. package/dist/server/internal/batchHandler.d.ts.map +1 -1
  10. package/dist/server/internal/batchHandler.js +2 -1
  11. package/dist/server/internal/batchHandler.js.map +1 -1
  12. package/dist/server/internal/register.d.ts +2 -2
  13. package/dist/server/internal/register.d.ts.map +1 -1
  14. package/dist/server/internal/register.js +9 -9
  15. package/dist/server/internal/register.js.map +1 -1
  16. package/dist/server/internal/registerWorker.d.ts +5 -0
  17. package/dist/server/internal/registerWorker.d.ts.map +1 -0
  18. package/dist/server/internal/registerWorker.js +28 -0
  19. package/dist/server/internal/registerWorker.js.map +1 -0
  20. package/dist/server/internal/runSchemaPlan.d.ts +7 -0
  21. package/dist/server/internal/runSchemaPlan.d.ts.map +1 -0
  22. package/dist/server/internal/runSchemaPlan.js +25 -0
  23. package/dist/server/internal/runSchemaPlan.js.map +1 -0
  24. package/dist/server/internal/schemaIndexBuilds.d.ts +4 -0
  25. package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -0
  26. package/dist/server/internal/schemaIndexBuilds.js +34 -0
  27. package/dist/server/internal/schemaIndexBuilds.js.map +1 -0
  28. package/dist/server/schema.d.ts +8 -0
  29. package/dist/server/schema.d.ts.map +1 -0
  30. package/dist/server/schema.js +43 -0
  31. package/dist/server/schema.js.map +1 -0
  32. package/package.json +7 -9
  33. package/src/server/client.ts +2 -0
  34. package/src/server/error.ts +1 -1
  35. package/src/server/internal/batchHandler.ts +7 -1
  36. package/src/server/internal/register.ts +32 -27
  37. package/src/server/internal/registerWorker.ts +42 -0
  38. package/src/server/internal/runSchemaPlan.ts +32 -0
  39. package/src/server/internal/schemaIndexBuilds.ts +44 -0
  40. package/src/server/schema.ts +57 -0
  41. package/docs/north-star.md +0 -48
  42. package/docs/roadmap.md +0 -35
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.1 - 2026-10-18
4
+
5
+ ### Fixed
6
+
7
+ - Wake workers when a failed job's retry delay ends instead of waiting for the polling backstop, including idle siblings when another worker in a concurrent registration is busy. Pending wakes are canceled when the worker Layer's scope closes. Custom clients can supply the optional `PgBossClient.notifyWorker` method to enable retry wakes and `PgBossClient.getWipData` to wake every native worker in the registration.
8
+
9
+ ## 0.10.0 - 2026-10-18
10
+
11
+ ### Added
12
+
13
+ - `installPgBossSchema(options?: PgBossSchemaOptions)` in `schema.ts` installs or upgrades the pg-boss schema through an Effect SQL client without starting workers or schedules. Use it in a test template fixture or release step, then start jobs Layers with `migrate: false`. Failures use `PgBossError` with the new `"install"` operation. Retrying a failed concurrent upgrade rebuilds missing and invalid managed indexes even when the schema version has already committed.
14
+
3
15
  ## 0.9.0 - 2026-10-18
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -37,6 +37,20 @@ Use a distinct literal identifier for each jobs service. The service and its Lay
37
37
 
38
38
  The work separates naturally: declare the application services once, define a queue and its worker for each feature, provide the complete registration list at the runtime boundary, and enqueue from ordinary application Effects.
39
39
 
40
+ ### Once per database: install the schema
41
+
42
+ `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:
43
+
44
+ ```ts
45
+ import { installPgBossSchema } from "@shivaedev/effect-pg-boss/schema.ts";
46
+
47
+ const jobsFixture = installPgBossSchema({ schema: "app_jobs" });
48
+ ```
49
+
50
+ 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.
51
+
52
+ After installation, pass the same schema and `migrate: false` to `Jobs.layer` 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.
53
+
40
54
  ### Once per application: the jobs service
41
55
 
42
56
  Define the application's jobs service and worker dependencies in `services.ts`:
@@ -114,7 +128,7 @@ const Compare = defineQueue({
114
128
 
115
129
  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.
116
130
 
117
- 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. Override the backstop only when the application needs a different bound for work that becomes runnable without a notification, such as delayed jobs or retries. The PostgreSQL backlog tests insert 30 jobs at once and verify completion within 2.5 seconds with both defaults and configured batches and concurrency.
131
+ 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.
118
132
 
119
133
  ### Size batches against expiry
120
134
 
@@ -206,7 +220,7 @@ export const enqueueEmail = (userId: number) =>
206
220
 
207
221
  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`.
208
222
 
209
- `PgBossPayloadError` names a payload boundary failure; malformed durable input is rejected before the worker's domain code runs. The PostgreSQL test also checks that invalid data sent directly to pg-boss reaches the dead-letter queue without reaching the handler.
223
+ `PgBossPayloadError` names a payload boundary failure; malformed durable input is rejected before the worker's domain code runs.
210
224
 
211
225
  ### Trace jobs
212
226
 
@@ -214,7 +228,7 @@ Each queue or scheduled job attempt runs in one root consumer span named for its
214
228
 
215
229
  `enqueue` captures the current Effect span, including an external parent span, when present. It stores that context in a typed envelope in pg-boss's existing job `data`; the adapter unwraps the envelope before Schema decoding in the handler and `getJob`, so the application's payload Schema stays unchanged. Jobs sent without a span keep their ordinary encoded payload, and workers also accept those unwrapped jobs. Direct pg-boss data readers see the envelope for traced sends.
216
230
 
217
- The consumer span links to the sender rather than sharing its parent trace, following OpenTelemetry messaging conventions for asynchronous processing. Each retry gets its own job trace. Provide the application's Effect tracer when building the worker Layer; the worker uses that captured tracer. The PostgreSQL tracing tests use Effect's in-memory tracer and a producer that shuts down before its worker starts; the scheduled-worker test checks the same span boundary with a fake client.
231
+ The consumer span links to the sender rather than sharing its parent trace, following OpenTelemetry messaging conventions for asynchronous processing. Each retry gets its own job trace. Provide the application's Effect tracer when building the worker Layer; the worker uses that captured tracer.
218
232
 
219
233
  ### Read health
220
234
 
@@ -261,7 +275,7 @@ export const inspectEmail = (id: string) => Effect.gen(function* () {
261
275
 
262
276
  ### Queue settings on startup
263
277
 
264
- `queue` accepts pg-boss `Queue` options except `name` and `deadLetter`, which the definition and adapter own. For prompt delivery, set `queue: { notify: true }` and enable `useListenNotify: true` in the worker Layer's constructor options. The PostgreSQL notification test waits for an idle worker with a ten-second polling interval and verifies delivery within 1.5 seconds.
278
+ `queue` accepts pg-boss `Queue` options except `name` and `deadLetter`, which the definition and adapter own. For prompt delivery, set `queue: { notify: true }` and enable `useListenNotify: true` in the worker Layer's constructor options.
265
279
 
266
280
  Startup creates missing queues and reconciles declared settings on existing queues through `updateQueue`. Unspecified settings keep their stored values; the adapter always supplies its default retry settings and dead-letter destination. Changing an explicitly declared `policy` or `partition` on an existing queue fails startup with `PgBossQueueOptionsError`; its `queue`, `options` and message identify the conflict. Queue setting changes apply to subsequently enqueued jobs. Schedule queues follow the same registration path.
267
281
 
@@ -281,7 +295,7 @@ const sharedJobsLayer = (databaseUrl: string) =>
281
295
  }).pipe(Layer.provide(MailLive));
282
296
  ```
283
297
 
284
- 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. Without a key, each build creates its own client. The application chooses whether to pass a key.
298
+ 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. Without a key, each build creates its own client. The application chooses whether to pass a key; give each distinct constructor configuration its own key.
285
299
 
286
300
  ### Separate producers and workers
287
301
 
@@ -295,7 +309,7 @@ export const producerLayer = (databaseUrl: string) =>
295
309
  Jobs.layer({ connectionString: databaseUrl, jobs: [SendEmail] });
296
310
  ```
297
311
 
298
- Provide this Layer to 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. The PostgreSQL producer test creates both queues through this Layer, enqueues a transformed payload, and observes a failing worker exhaust two retries before the job reaches its dead-letter queue.
312
+ Provide this Layer to 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.
299
313
 
300
314
  ### Enqueue follow-ups from a handler
301
315
 
@@ -322,7 +336,7 @@ const live = Jobs.layer({
322
336
  }).pipe(Layer.provide(MailLive));
323
337
  ```
324
338
 
325
- 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. The PostgreSQL follow-up test completes a chain across two workers, enqueues to a producer-only queue and asserts one client acquisition and shutdown.
339
+ 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.
326
340
 
327
341
  ### Test through the queue contract
328
342
 
@@ -365,11 +379,11 @@ emailStories.it("decodes the queued email", [], function* ({ enqueue, testing })
365
379
 
366
380
  Set `retryPolicy: testRetryPolicy` through the controller to override queue and send settings with `retryDelay: 0` and `retryBackoff: false`. A failing worker can then exhaust its native retries without production backoff. The policy applies to subsequently sent jobs; it does not rewrite jobs already stored.
367
381
 
368
- `failNextSend(queue)` arms one send failure for that queue. The next valid `enqueue` fails with the usual typed `PgBossError` (`operation: "enqueue"`); the following send proceeds normally. `expireAfter(queue, n)` takes a positive attempt count: configure it before enqueueing to limit subsequent jobs to `n` attempts. Each successful send keeps its own expiry rule; later changes and duplicate-ID sends do not alter pending jobs. Earlier attempts run the handler normally; on attempt `n`, the controller expires the claimed job instead of invoking its handler. PostgreSQL supervision performs the terminal failure and dead lettering. A job that succeeds earlier completes normally. This expiry hook supports pg-boss schema version 45 and keeps its SQL inside the testing module.
382
+ `failNextSend(queue)` arms one send failure for that queue. The next valid `enqueue` fails with the usual typed `PgBossError` (`operation: "enqueue"`); the following send proceeds normally. `expireAfter(queue, n)` takes a positive attempt count: configure it before enqueueing to limit subsequent jobs to `n` attempts. Each successful send keeps its own expiry rule; later changes and duplicate-ID sends do not alter pending jobs. Earlier attempts run the handler normally; on attempt `n`, the controller expires the claimed job instead of invoking its handler. PostgreSQL supervision performs the terminal failure and dead lettering. A job that succeeds earlier completes normally. This expiry hook supports pg-boss schema version 45.
369
383
 
370
384
  `checkBatchWindow(queue, millisecondsPerJob)` checks that the declared sequential batch fits strictly inside the stored queue expiry window. `schemaVersion` reads the installed version, and `checkSchemaVersion(expected)` fails when it differs. Test control, read and check failures use `PgBossTestingError` with `operation`, optional `queue` and a redacted `original`.
371
385
 
372
- A kit can capture `revision` before reading jobs and yield `waitForChange(revision)` when its condition is unmet. The wait resumes after a local send, settlement, worker fetch or return to idle, including terminal native worker timeouts whose handler promises remain pending. It also resumes immediately if a change occurred between the read and the wait. This signal observes the controller's client; external clients, future scheduled work and external database writes need their own wake source. Stories should retain a bounded test timeout. The package's PostgreSQL story kit uses this signal to settle failed-job stories without timer polling or application SQL.
386
+ A kit can capture `revision` before reading jobs and yield `waitForChange(revision)` when its condition is unmet. The wait resumes after a local send, settlement, worker fetch or return to idle, including terminal native worker timeouts whose handler promises remain pending. It also resumes immediately if a change occurred between the read and the wait. This signal observes the controller's client; external clients, future scheduled work and external database writes need their own wake source. Stories should retain a bounded test timeout.
373
387
 
374
388
  ### API
375
389
 
@@ -379,6 +393,7 @@ Import the module that defines the name; the package has no root entry.
379
393
  | --- | --- |
380
394
  | `@shivaedev/effect-pg-boss/definition.ts` | `defineQueue`, `defineSchedule`, `jobContext`; `DefineQueueOptions`, `DefineScheduleOptions`, `JobContext`, `JobPayloadSchema`, `JobRegistration`, `QueueDefinition`, `QueueWorker`, `RegistrationRequirements`, `ScheduleDefinition`, `ScheduledWorker` |
381
395
  | `@shivaedev/effect-pg-boss/service.ts` | `makePgBoss`; `PgBossDefinition`, `PgBossLayerOptions`, `PgBossService` |
396
+ | `@shivaedev/effect-pg-boss/schema.ts` | `installPgBossSchema`; `PgBossSchemaOptions` |
382
397
  | `@shivaedev/effect-pg-boss/health.ts` | `deadLetterQueueName`; `JobsHealth`, `QueueCounts`, `QueueHealth` |
383
398
  | `@shivaedev/effect-pg-boss/error.ts` | `PgBossError`, `PgBossPayloadError`, `PgBossQueueOptionsError`, `PgBossTestingError`, `toPgBossError`; `PgBossOperation` |
384
399
  | `@shivaedev/effect-pg-boss/client.ts` | `defaultClientFactory`; `PgBossClient`, `PgBossClientFactory` |
@@ -386,19 +401,11 @@ Import the module that defines the name; the package has no root entry.
386
401
 
387
402
  The definition options keep the pg-boss names: `queue` uses `Queue` except `name` and `deadLetter`, `worker` uses `WorkOptions`, and a schedule's `schedule` uses `ScheduleOptions`. A queue also requires `name` and `schema`; a schedule requires `name` and `cron`.
388
403
 
389
- The application's service has these operations:
390
-
391
- - `enqueue(queue, payload, options?)`: an Effect returning `Option.Option<string>`, with `PgBossError | PgBossPayloadError` in its error type. Its options use pg-boss `SendOptions`.
392
- - `health`: an Effect returning cached `JobsHealth`, with `PgBossError` in its error type.
393
- - `counts(queue)`: an Effect returning live `QueueCounts`, with `PgBossError` in its error type.
394
- - `getJob(queue, id)`: an Effect returning `Option.Option<JobWithMetadata<Payload>>` with decoded payload data, `PgBossError | PgBossPayloadError` in its error type and the Schema's decoding services in its requirements.
395
- - `retry(queue, id)`: an Effect returning `void`, with `PgBossError` in its error type.
404
+ `Jobs.layer(options)` accepts pg-boss `ConstructorOptions` plus the adapter's own options. Custom clients must supply native `complete` and `fail` capabilities to register queue or schedule workers; missing settlement capabilities fail Layer acquisition with a registration `PgBossError`. Producer-only clients do not need them. Custom clients can also supply the optional native capabilities `findJobs`, `retry`, `update`, `getDb` and `updateQueue` for lookup, retry, live counts and reconciliation; a requested operation fails with `PgBossError` if its required capability is absent.
396
405
 
397
- `Jobs.layer(options)` accepts pg-boss `ConstructorOptions` plus `jobs`, optional `clientCacheKey`, optional `clientFactory`, optional `onError` and optional `stop`. The factory type accepts constructor options and returns `PgBossClient`. Custom clients must supply native `complete` and `fail` capabilities to register queue or schedule workers; missing settlement capabilities fail Layer acquisition with a registration `PgBossError`. Producer-only clients do not need them. Custom clients can also supply the optional native capabilities `findJobs`, `retry`, `update`, `getDb` and `updateQueue` for lookup, retry, live counts and reconciliation; a requested operation fails with `PgBossError` if its required capability is absent. `onError` has the type `(error: Error) => Effect.Effect<unknown, never, R>`; `stop` uses pg-boss `StopOptions`.
406
+ `onError` handles pg-boss client `error` events using the services captured by the Layer. Only the owning jobs service is supplied automatically; a handler using another jobs definition still requires that definition's Layer.
398
407
 
399
- `onError` handles pg-boss client `error` events using the services captured by the Layer. The Layer's requirement type includes the workers' services and their payload Schemas' `DecodingServices`, excluding its own jobs identifier, plus the callback's services. Only the owning jobs service is supplied automatically; a handler using another jobs definition still requires that definition's Layer. `enqueue` includes the payload Schema's `EncodingServices` in its requirement type.
400
-
401
- `JobContext` has `id`, `name`, `signal`, `expireInSeconds`, `heartbeatSeconds` and `remainingBudget`. Worker registration always requests `includeMetadata: true`, including when `worker` supplies `includeMetadata: false`; custom clients must return pg-boss's `startedOn` metadata. When calling `jobContext` directly, reading `remainingBudget` requires that same metadata. `QueueHealth` has `name`, `activeCount`, `deadLetteredCount`, `failedCount`, `queuedCount` and `readyCount`. `JobsHealth` has `jobs` and the corresponding `activeTotal`, `deadLetteredTotal`, `failedTotal`, `queuedTotal` and `readyTotal` fields.
408
+ Worker registration always requests `includeMetadata: true`, including when `worker` supplies `includeMetadata: false`; custom clients must return pg-boss's `startedOn` metadata. When calling `jobContext` directly, reading `remainingBudget` requires that same metadata.
402
409
 
403
410
  `PgBossError` has `operation`, optional `queue` and a `Redacted` `original`. Its message names the operation, the queue and the original error's class and `code`, such as a PostgreSQL SQLSTATE, but never the original message, which can quote payloads or connection strings. A registration failure names the queue it was preparing. `PgBossPayloadError` has `direction`, `queue` and a `Redacted` `original`. Those fields distinguish the pg-boss operation from the payload boundary.
404
411
 
@@ -410,8 +417,4 @@ pnpm add @shivaedev/effect-pg-boss effect@4.0.4 pg-boss@12.37.0
410
417
 
411
418
  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.
412
419
 
413
- 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. Treat enqueueing and an application database transaction as separate operations. Enqueue after the application write commits, and handle queue failures in the application so they cannot roll back the write. The starter logs enqueue failures and returns the saved note; a failed enqueue leaves its word count pending. The package supplies no application transaction adapter.
414
-
415
- For package development, the real PostgreSQL test uses `PLATFORM_EFFECT_PG_BOSS_TEST_DATABASE_URL` and skips when the variable is absent. A skipped test does not establish durable queue behavior. Fake-client tests cover registration and lifecycle calls; they do not establish real cron execution, retry timing or deployment behavior. The package is built for ShivaeDev applications and may change without a deprecation period.
416
-
417
- The [north star](./docs/north-star.md) explains the design trade-offs; the [roadmap](./docs/roadmap.md) owns package work and open choices.
420
+ 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. Treat enqueueing and an application database transaction as separate operations. Enqueue after the application write commits, and handle queue failures in the application so they cannot roll back the write. The package supplies no application transaction adapter.
@@ -7,6 +7,8 @@ export interface PgBossClient {
7
7
  readonly findJobs?: (name: string, options?: FindJobsOptions) => Promise<readonly JobWithMetadata<unknown>[]>;
8
8
  readonly getDb?: PgBoss["getDb"];
9
9
  readonly getQueue: (name: string) => Promise<QueueResult | null>;
10
+ readonly getWipData?: PgBoss["getWipData"];
11
+ readonly notifyWorker?: PgBoss["notifyWorker"];
10
12
  readonly off: (event: "error", listener: (error: Error) => void) => unknown;
11
13
  readonly offWork: (name: string) => Promise<void>;
12
14
  readonly on: (event: "error", listener: (error: Error) => void) => unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/server/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,kBAAkB,EAClB,eAAe,EACf,GAAG,EACH,eAAe,EACf,KAAK,EACL,WAAW,EACX,eAAe,EACf,WAAW,EACX,WAAW,EACX,WAAW,EACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,SAAS,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9G,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACjE,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;IAC5E,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;IAC3E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClH,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACrI;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,kBAAkB,KAAK,YAAY,CAAC;AAEhF,eAAO,MAAM,oBAAoB,EAAE,mBAAsD,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/server/client.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/server/client.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAwBjC,MAAM,CAAC,MAAM,oBAAoB,GAAwB,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/server/client.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AA0BjC,MAAM,CAAC,MAAM,oBAAoB,GAAwB,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { Redacted } from "effect";
2
- export type PgBossOperation = "counts" | "enqueue" | "getJob" | "health" | "register" | "retry" | "start" | "stop";
2
+ export type PgBossOperation = "counts" | "enqueue" | "getJob" | "health" | "install" | "register" | "retry" | "start" | "stop";
3
3
  declare const PgBossError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
4
4
  readonly _tag: "PgBossError";
5
5
  } & Readonly<A>;
@@ -1 +1 @@
1
- {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;;;;AAEnH,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;IAED,IAAa,OAAO,IAAI,MAAM,CAG7B;CACD;;;;AAUD,qBAAa,uBAAwB,SAAQ,6BAA4C;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,QAAQ,CAAC,EAAE,CAAC;CACtD,CAAC;IACD,IAAa,OAAO,IAAI,MAAM,CAE7B;CACD;;;;AAED,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;CAAG;;;;AAEL,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;CAAG;AAEL,eAAO,MAAM,aAAa,cAAe,eAAe,SAAS,OAAO,UAAU,MAAM,KAAG,WAKxF,CAAC"}
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;;;;AAE/H,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;IAED,IAAa,OAAO,IAAI,MAAM,CAG7B;CACD;;;;AAUD,qBAAa,uBAAwB,SAAQ,6BAA4C;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,QAAQ,CAAC,EAAE,CAAC;CACtD,CAAC;IACD,IAAa,OAAO,IAAI,MAAM,CAE7B;CACD;;;;AAED,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;CAAG;;;;AAEL,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;CAAG;AAEL,eAAO,MAAM,aAAa,cAAe,eAAe,SAAS,OAAO,UAAU,MAAM,KAAG,WAKxF,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { Effect } from "effect";
2
2
  import type { Job } from "pg-boss";
3
3
  import type { PgBossClient } from "#client.ts";
4
- export declare function batchHandler(client: PgBossClient, name: string, run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>): (jobs: readonly Job<unknown>[]) => Promise<void>;
4
+ export declare function batchHandler(client: PgBossClient, name: string, run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>, afterFailure?: (job: Job<unknown>) => Promise<void>): (jobs: readonly Job<unknown>[]) => Promise<void>;
5
5
  //# sourceMappingURL=batchHandler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"batchHandler.d.ts","sourceRoot":"","sources":["../../../src/server/internal/batchHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAQ,MAAM,QAAQ,CAAC;AACpD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAQ/C,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,UAMvG,SAAS,GAAG,CAAC,OAAO,CAAC,EAAE,KAAG,OAAO,CAAC,IAAI,CAAC,CA6B3D"}
1
+ {"version":3,"file":"batchHandler.d.ts","sourceRoot":"","sources":["../../../src/server/internal/batchHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAQ,MAAM,QAAQ,CAAC;AACpD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAQ/C,wBAAgB,YAAY,CAC3B,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,EAC3D,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,UAO/B,SAAS,GAAG,CAAC,OAAO,CAAC,EAAE,KAAG,OAAO,CAAC,IAAI,CAAC,CA8B3D"}
@@ -4,7 +4,7 @@ function failureOutput(cause) {
4
4
  const error = Cause.squash(cause);
5
5
  return typeof error === "object" && error !== null ? error : { message: String(error) };
6
6
  }
7
- export function batchHandler(client, name, run) {
7
+ export function batchHandler(client, name, run, afterFailure) {
8
8
  const complete = client.complete?.bind(client);
9
9
  const fail = client.fail?.bind(client);
10
10
  if (complete === undefined || fail === undefined) {
@@ -31,6 +31,7 @@ export function batchHandler(client, name, run) {
31
31
  }
32
32
  else {
33
33
  await fail(name, job, failureOutput(exit.cause));
34
+ await afterFailure?.(job);
34
35
  }
35
36
  }
36
37
  if (expired) {
@@ -1 +1 @@
1
- {"version":3,"file":"batchHandler.js","sourceRoot":"","sources":["../../../src/server/internal/batchHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGpD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,SAAS,aAAa,CAAC,KAA2B;IACjD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACzF,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAoB,EAAE,IAAY,EAAE,GAA2D;IAC3H,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,wDAAwD,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,KAAK,EAAE,IAA6B,EAAiB,EAAE;QAC7D,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC;gBACpE,OAAO,GAAG,IAAI,CAAC;gBACf,SAAS;YACV,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CACvC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC/C,MAAM,SAAS,GAAG,QAAQ,GAAG,GAAG,CAAC;gBACjC,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YACzI,CAAC,CAAC,EACF,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CACtB,CAAC;YACF,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBACrF,2FAA2F;gBAC3F,OAAO,IAAI,OAAO,CAAQ,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC5C,CAAC;YACD,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1B,MAAM,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACP,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YAClD,CAAC;QACF,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACb,OAAO,IAAI,OAAO,CAAQ,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,CAAC;IACF,CAAC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"batchHandler.js","sourceRoot":"","sources":["../../../src/server/internal/batchHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGpD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,SAAS,aAAa,CAAC,KAA2B;IACjD,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACzF,CAAC;AAED,MAAM,UAAU,YAAY,CAC3B,MAAoB,EACpB,IAAY,EACZ,GAA2D,EAC3D,YAAmD;IAEnD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACvC,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QAClD,MAAM,IAAI,SAAS,CAAC,wDAAwD,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,KAAK,EAAE,IAA6B,EAAiB,EAAE;QAC7D,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC;gBACpE,OAAO,GAAG,IAAI,CAAC;gBACf,SAAS;YACV,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CACvC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC/C,MAAM,SAAS,GAAG,QAAQ,GAAG,GAAG,CAAC;gBACjC,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;YACzI,CAAC,CAAC,EACF,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CACtB,CAAC;YACF,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBACrF,2FAA2F;gBAC3F,OAAO,IAAI,OAAO,CAAQ,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC5C,CAAC;YACD,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1B,MAAM,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACP,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBACjD,MAAM,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;QACF,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACb,OAAO,IAAI,OAAO,CAAQ,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,CAAC;IACF,CAAC,CAAC;AACH,CAAC"}
@@ -1,8 +1,8 @@
1
- import { type Context, Effect } from "effect";
1
+ import { type Context, Effect, type Scope } from "effect";
2
2
  import type { PgBossClient } from "#client.ts";
3
3
  import { type JobRegistration } from "#definition.ts";
4
4
  import { PgBossError, PgBossQueueOptionsError } from "#error.ts";
5
5
  export declare function registrationName(registration: JobRegistration): string;
6
6
  export declare const registrationNames: (registrations: readonly JobRegistration[]) => readonly string[];
7
- export declare const registerJobs: <R>(client: PgBossClient, registrations: readonly JobRegistration[], context: Context.Context<R>, replaceWorkers: boolean) => Effect.Effect<void, PgBossError | PgBossQueueOptionsError>;
7
+ export declare const registerJobs: <R>(client: PgBossClient, registrations: readonly JobRegistration[], context: Context.Context<R>, replaceWorkers: boolean) => Effect.Effect<void, PgBossError | PgBossQueueOptionsError, Scope.Scope>;
8
8
  //# sourceMappingURL=register.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACN,KAAK,eAAe,EAMpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAsB,uBAAuB,EAAiB,MAAM,WAAW,CAAC;AA8BpG,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,eAAe,GAAG,MAAM,CAEtE;AA+DD,eAAO,MAAM,iBAAiB,kBAAmB,SAAS,eAAe,EAAE,KAAG,SAAS,MAAM,EAM5F,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,CAAC,UACrB,YAAY,iBACL,SAAS,eAAe,EAAE,WAChC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,kBACX,OAAO,KACrB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,uBAAuB,CAezD,CAAC"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAoB,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACN,KAAK,eAAe,EAMpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAsB,uBAAuB,EAAiB,MAAM,WAAW,CAAC;AA8BpG,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,eAAe,GAAG,MAAM,CAEtE;AAkED,eAAO,MAAM,iBAAiB,kBAAmB,SAAS,eAAe,EAAE,KAAG,SAAS,MAAM,EAM5F,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,CAAC,UACrB,YAAY,iBACL,SAAS,eAAe,EAAE,WAChC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,kBACX,OAAO,KACrB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,uBAAuB,EAAE,KAAK,CAAC,KAAK,CAiBvE,CAAC"}
@@ -1,8 +1,8 @@
1
1
  import { Effect, Redacted, Schema } from "effect";
2
2
  import { jobContext, } from "#definition.ts";
3
3
  import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
4
- import { batchHandler } from "#internal/batchHandler.ts";
5
4
  import { ensureQueue } from "#internal/ensureQueue.ts";
5
+ import { registerWorker } from "#internal/registerWorker.ts";
6
6
  import { payloadOf, traceJob } from "#internal/tracing.ts";
7
7
  function isQueueWorker(registration) {
8
8
  return registration._tag === "QueueWorker";
@@ -54,17 +54,17 @@ function runQueueWorker(worker, job, context) {
54
54
  queue: worker.queue.name,
55
55
  })), Effect.flatMap((payload) => worker.handler(payload, jobContext(job))), (effect) => traceJob(effect, job, worker.queue.name), Effect.provide(context));
56
56
  }
57
- async function registerQueue(client, registration, context) {
57
+ async function registerQueue(client, registration, context, scope) {
58
58
  const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
59
59
  const name = queue.name;
60
60
  if (registration._tag === "QueueDefinition") {
61
61
  return;
62
62
  }
63
- await client.work(name, { ...queue.workerOptions, includeMetadata: true }, batchHandler(client, name, (job) => runQueueWorker(registration, job, context)));
63
+ await registerWorker(client, name, queue.workerOptions, (job) => runQueueWorker(registration, job, context), scope);
64
64
  }
65
- async function registerSchedule(client, worker, context) {
65
+ async function registerSchedule(client, worker, context, scope) {
66
66
  const { schedule } = worker;
67
- await client.work(schedule.name, { ...schedule.workerOptions, includeMetadata: true }, batchHandler(client, schedule.name, (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context)));
67
+ await registerWorker(client, schedule.name, schedule.workerOptions, (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context), scope);
68
68
  await client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions);
69
69
  }
70
70
  export const registrationNames = (registrations) => {
@@ -74,7 +74,7 @@ export const registrationNames = (registrations) => {
74
74
  }
75
75
  return names;
76
76
  };
77
- export const registerJobs = (client, registrations, context, replaceWorkers) => Effect.tryPromise({
77
+ export const registerJobs = (client, registrations, context, replaceWorkers) => Effect.flatMap(Effect.scope, (scope) => Effect.tryPromise({
78
78
  catch: (error) => (error instanceof PgBossQueueOptionsError || error instanceof PgBossError ? error : toPgBossError("register", error)),
79
79
  try: async () => {
80
80
  registrationNames(registrations);
@@ -82,12 +82,12 @@ export const registerJobs = (client, registrations, context, replaceWorkers) =>
82
82
  const provided = workerContext(context);
83
83
  for (const registration of registrations) {
84
84
  if (isQueueWorker(registration) || isQueueDefinition(registration)) {
85
- await registerQueue(client, registration, provided);
85
+ await registerQueue(client, registration, provided, scope);
86
86
  }
87
87
  else if (isScheduledWorker(registration)) {
88
- await registerSchedule(client, registration, provided);
88
+ await registerSchedule(client, registration, provided, scope);
89
89
  }
90
90
  }
91
91
  },
92
- });
92
+ }));
93
93
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhE,OAAO,EAEN,UAAU,GAKV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE3D,SAAS,aAAa,CAAC,YAA6B;IACnD,OAAO,YAAY,CAAC,IAAI,KAAK,aAAa,CAAC;AAC5C,CAAC;AAED,SAAS,iBAAiB,CAAC,YAA6B;IACvD,OAAO,YAAY,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAChD,CAAC;AAED,SAAS,iBAAiB,CAAC,YAA6B;IACvD,OAAO,YAAY,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAChD,CAAC;AAED,SAAS,sBAAsB,CAAC,YAA6B;IAC5D,IAAI,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC;QACrC,OAAO,YAAY,CAAC;IACrB,CAAC;IACD,IAAI,aAAa,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,OAAO,YAAY,CAAC,KAAK,CAAC;IAC3B,CAAC;IACD,IAAI,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC;QACrC,OAAO,YAAY,CAAC,QAAQ,CAAC;IAC9B,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,iCAAiC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,YAA6B;IAC7D,OAAO,sBAAsB,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,MAAoB,EAAE,aAAyC,EAAE,cAAuB;IACpH,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;QAC1C,IAAI,cAAc,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC;YACxD,MAAM,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;IACD,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;QAC1C,MAAM,UAAU,GAAG,sBAAsB,CAAC,YAAY,CAAC,CAAC;QACxD,IAAI,CAAC;YACJ,MAAM,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,YAAY,CAAC,CAAC;QACrE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,KAAK,YAAY,uBAAuB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;QAC5G,CAAC;IACF,CAAC;AACF,CAAC;AAID,SAAS,aAAa,CAAC,OAAgB;IACtC,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,SAAS,cAAc,CAAC,MAAmB,EAAE,GAAiB,EAAE,OAAiC;IAChG,OAAO,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAC/E,MAAM,CAAC,QAAQ,CACd,CAAC,KAAK,EAAE,EAAE,CACT,IAAI,kBAAkB,CAAC;QACtB,SAAS,EAAE,QAAQ;QACnB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI;KACxB,CAAC,CACH,EACD,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EACrE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EACpD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CACvB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,MAAoB,EAAE,YAA2C,EAAE,OAAiC;IAChI,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC;IACtF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,IAAI,YAAY,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QAC7C,OAAO;IACR,CAAC;IACD,MAAM,MAAM,CAAC,IAAI,CAChB,IAAI,EACJ,EAAE,GAAG,KAAK,CAAC,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,EACjD,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAC/E,CAAC;AACH,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,MAAoB,EAAE,MAAuB,EAAE,OAAiC;IAC/G,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAC5B,MAAM,MAAM,CAAC,IAAI,CAChB,QAAQ,CAAC,IAAI,EACb,EAAE,GAAG,QAAQ,CAAC,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,EACpD,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAClH,CAAC;IACF,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,aAAyC,EAAqB,EAAE;IACjG,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClD,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,kCAAkC,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,CAC3B,MAAoB,EACpB,aAAyC,EACzC,OAA2B,EAC3B,cAAuB,EACsC,EAAE,CAC/D,MAAM,CAAC,UAAU,CAAC;IACjB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,YAAY,uBAAuB,IAAI,KAAK,YAAY,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACvI,GAAG,EAAE,KAAK,IAAI,EAAE;QACf,iBAAiB,CAAC,aAAa,CAAC,CAAC;QACjC,MAAM,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACxC,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;YAC1C,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC;gBACpE,MAAM,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;YACrD,CAAC;iBAAM,IAAI,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC5C,MAAM,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;YACxD,CAAC;QACF,CAAC;IACF,CAAC;CACD,CAAC,CAAC"}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { Effect, type Scope } from "effect";
2
+ import type { Job, WorkOptions } from "pg-boss";
3
+ import type { PgBossClient } from "#client.ts";
4
+ export declare function registerWorker(client: PgBossClient, name: string, options: WorkOptions, run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>, scope: Scope.Scope): Promise<string>;
5
+ //# sourceMappingURL=registerWorker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registerWorker.d.ts","sourceRoot":"","sources":["../../../src/server/internal/registerWorker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,wBAAgB,cAAc,CAC7B,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,KAAK,CAAC,KAAK,GAChB,OAAO,CAAC,MAAM,CAAC,CA8BjB"}
@@ -0,0 +1,28 @@
1
+ import { Clock, Effect } from "effect";
2
+ import { batchHandler } from "#internal/batchHandler.ts";
3
+ export function registerWorker(client, name, options, run, scope) {
4
+ const registration = client.work(name, { ...options, includeMetadata: true }, batchHandler(client, name, run, async (attempt) => {
5
+ if (client.findJobs === undefined || client.notifyWorker === undefined) {
6
+ return;
7
+ }
8
+ const job = (await client.findJobs(name, { id: attempt.id }))[0];
9
+ if (job?.state !== "retry") {
10
+ return;
11
+ }
12
+ const workId = await registration;
13
+ await Effect.runPromise(Effect.gen(function* () {
14
+ const now = yield* Clock.currentTimeMillis;
15
+ // PostgreSQL timestamps retain sub-millisecond precision that Date drops.
16
+ yield* Effect.sleep(Math.max(0, job.startAfter.getTime() + 1 - now));
17
+ const workerIds = client
18
+ .getWipData?.()
19
+ .filter((worker) => worker.workId === workId)
20
+ .map((worker) => worker.id) ?? [workId];
21
+ for (const workerId of workerIds) {
22
+ client.notifyWorker?.(workerId);
23
+ }
24
+ }).pipe(Effect.forkIn(scope)));
25
+ }));
26
+ return registration;
27
+ }
28
+ //# sourceMappingURL=registerWorker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registerWorker.js","sourceRoot":"","sources":["../../../src/server/internal/registerWorker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,MAAM,UAAU,cAAc,CAC7B,MAAoB,EACpB,IAAY,EACZ,OAAoB,EACpB,GAA2D,EAC3D,KAAkB;IAElB,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAC/B,IAAI,EACJ,EAAE,GAAG,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,EACrC,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACjD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACxE,OAAO;QACR,CAAC;QACD,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjE,IAAI,GAAG,EAAE,KAAK,KAAK,OAAO,EAAE,CAAC;YAC5B,OAAO;QACR,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;QAClC,MAAM,MAAM,CAAC,UAAU,CACtB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACnB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;YAC3C,0EAA0E;YAC1E,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YACrE,MAAM,SAAS,GAAG,MAAM;iBACtB,UAAU,EAAE,EAAE;iBACd,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC;iBAC5C,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACzC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBAClC,MAAM,CAAC,YAAY,EAAE,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;QACF,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAC7B,CAAC;IACH,CAAC,CAAC,CACF,CAAC;IACF,OAAO,YAAY,CAAC;AACrB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Effect } from "effect";
2
+ import type { Connection } from "effect/sql/SqlConnection";
3
+ export declare function runSchemaPlan(connection: Connection, plan: string, run: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>, upgrade?: {
4
+ readonly schema: string;
5
+ readonly version: number;
6
+ }): Promise<void>;
7
+ //# sourceMappingURL=runSchemaPlan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runSchemaPlan.d.ts","sourceRoot":"","sources":["../../../src/server/internal/runSchemaPlan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAG3D,wBAAsB,aAAa,CAClC,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAAC,IAAI,CAAC,CAsBf"}
@@ -0,0 +1,25 @@
1
+ import { pendingIndexInserts } from "./schemaIndexBuilds.js";
2
+ export async function runSchemaPlan(connection, plan, run, upgrade) {
3
+ // Native exported plans append concurrent index DDL after the migration transaction.
4
+ const parts = /^\s*BEGIN;(?<body>[\s\S]*)\n {4}COMMIT;(?<concurrent>[\s\S]*)$/u.exec(plan);
5
+ if (parts?.groups === undefined) {
6
+ throw new Error("Unrecognized pg-boss schema plan transaction");
7
+ }
8
+ const { body = "", concurrent = "" } = parts.groups;
9
+ function quote(text) {
10
+ return `'${text.replaceAll("'", "''")}'`;
11
+ }
12
+ const commands = concurrent
13
+ .trim()
14
+ .split(/;\s*(?:\n|$)/u)
15
+ .filter((statement) => statement.trim() !== "");
16
+ const pending = upgrade === undefined ? "" : pendingIndexInserts(upgrade.schema, upgrade.version, commands, quote);
17
+ // Effect SQL uses the extended protocol, which accepts one statement per command.
18
+ await run(connection.executeRaw(`DO ${quote(`BEGIN EXECUTE ${quote(`${body}\n${pending}`)}; END`)}`, []));
19
+ if (upgrade === undefined) {
20
+ for (const statement of commands) {
21
+ await run(connection.executeRaw(statement, []));
22
+ }
23
+ }
24
+ }
25
+ //# sourceMappingURL=runSchemaPlan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runSchemaPlan.js","sourceRoot":"","sources":["../../../src/server/internal/runSchemaPlan.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,UAAsB,EACtB,IAAY,EACZ,GAAsD,EACtD,OAA+D;IAE/D,qFAAqF;IACrF,MAAM,KAAK,GAAG,iEAAiE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3F,IAAI,KAAK,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,UAAU,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC;IACpD,SAAS,KAAK,CAAC,IAAY;QAC1B,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;IAC1C,CAAC;IACD,MAAM,QAAQ,GAAG,UAAU;SACzB,IAAI,EAAE;SACN,KAAK,CAAC,eAAe,CAAC;SACtB,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IACnH,kFAAkF;IAClF,MAAM,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,iBAAiB,KAAK,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAC1G,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,KAAK,MAAM,SAAS,IAAI,QAAQ,EAAE,CAAC;YAClC,MAAM,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QACjD,CAAC;IACF,CAAC;AACF,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { PgBoss } from "pg-boss";
2
+ export declare function pendingIndexInserts(schema: string, version: number, commands: readonly string[], quote: (text: string) => string): string;
3
+ export declare function resumeSchemaIndexes(client: PgBoss, schema: string): Promise<void>;
4
+ //# sourceMappingURL=schemaIndexBuilds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemaIndexBuilds.d.ts","sourceRoot":"","sources":["../../../src/server/internal/schemaIndexBuilds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAgBtC,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CAQzI;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBvF"}
@@ -0,0 +1,34 @@
1
+ const BUILD_PREFIX = "effect-pg-boss-schema:";
2
+ function indexName(command) {
3
+ const match = /^\s*(?:CREATE (?:UNIQUE )?INDEX CONCURRENTLY IF NOT EXISTS |DROP INDEX CONCURRENTLY IF EXISTS (?:(?:"[^"]+"|\w+)\.))(?<name>\w+)/mu.exec(command);
4
+ const name = match?.groups?.name;
5
+ if (name === undefined) {
6
+ throw new Error("Unrecognized pg-boss concurrent index command");
7
+ }
8
+ return name;
9
+ }
10
+ export function pendingIndexInserts(schema, version, commands, quote) {
11
+ return commands
12
+ .map((command, position) => {
13
+ const table = indexName(command).replace(/_i\d+$/u, "");
14
+ const name = `${BUILD_PREFIX}${String(position).padStart(6, "0")}`;
15
+ return `INSERT INTO ${schema}.bam (name, version, table_name, command) VALUES (${quote(name)}, ${version}, ${quote(table)}, ${quote(command)});`;
16
+ })
17
+ .join("\n");
18
+ }
19
+ export async function resumeSchemaIndexes(client, schema) {
20
+ const db = client.getDb();
21
+ const { rows } = await db.executeSql(`SELECT id, command FROM ${schema}.bam WHERE starts_with(name, $1) AND status <> 'completed' ORDER BY created_on, name`, [BUILD_PREFIX]);
22
+ for (const row of rows) {
23
+ const name = indexName(row.command);
24
+ if (/^\s*CREATE /mu.test(row.command)) {
25
+ const probe = await db.executeSql("SELECT NOT indisvalid AS invalid FROM pg_index WHERE indexrelid = to_regclass($1)", [`${schema}.${name}`]);
26
+ if (probe.rows[0]?.invalid === true) {
27
+ await db.executeSql(`DROP INDEX CONCURRENTLY IF EXISTS ${schema}."${name}"`);
28
+ }
29
+ }
30
+ await db.executeSql(row.command);
31
+ await db.executeSql(`UPDATE ${schema}.bam SET status = 'completed', completed_on = clock_timestamp() WHERE id = $1`, [row.id]);
32
+ }
33
+ }
34
+ //# sourceMappingURL=schemaIndexBuilds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemaIndexBuilds.js","sourceRoot":"","sources":["../../../src/server/internal/schemaIndexBuilds.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C,SAAS,SAAS,CAAC,OAAe;IACjC,MAAM,KAAK,GACV,oIAAoI,CAAC,IAAI,CACxI,OAAO,CACP,CAAC;IACH,MAAM,IAAI,GAAG,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC;IACjC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,OAAe,EAAE,QAA2B,EAAE,KAA+B;IAChI,OAAO,QAAQ;SACb,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC1B,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,GAAG,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QACnE,OAAO,eAAe,MAAM,qDAAqD,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,KAAK,KAAK,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;IAClJ,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAc,EAAE,MAAc;IACvE,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;IAC1B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,UAAU,CACnC,2BAA2B,MAAM,sFAAsF,EACvH,CAAC,YAAY,CAAC,CACd,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,mFAAmF,EAAE,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YAC9I,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrC,MAAM,EAAE,CAAC,UAAU,CAAC,qCAAqC,MAAM,KAAK,IAAI,GAAG,CAAC,CAAC;YAC9E,CAAC;QACF,CAAC;QACD,MAAM,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,EAAE,CAAC,UAAU,CAAC,UAAU,MAAM,+EAA+E,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAChI,CAAC;AACF,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { type PgBossError } from "#error.ts";
4
+ export interface PgBossSchemaOptions {
5
+ readonly schema?: string;
6
+ }
7
+ export declare function installPgBossSchema(options?: PgBossSchemaOptions): Effect.Effect<void, PgBossError, SqlClient.SqlClient>;
8
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/server/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAIvC,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,WAAW,CAAC;AAI5D,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAoCD,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,CAAC,SAAS,CAAC,CAS5H"}
@@ -0,0 +1,43 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { getConstructionPlans, getMigrationPlans, PgBoss } from "pg-boss";
4
+ import pgBossPackage from "pg-boss/package.json" with { type: "json" };
5
+ import { toPgBossError } from "#error.ts";
6
+ import { runSchemaPlan } from "#internal/runSchemaPlan.ts";
7
+ import { resumeSchemaIndexes } from "#internal/schemaIndexBuilds.ts";
8
+ async function installSchema(connection, options, run) {
9
+ const schema = options.schema ?? "pgboss";
10
+ const construction = getConstructionPlans(schema);
11
+ const client = new PgBoss({
12
+ db: { executeSql: async (text, values = []) => ({ rows: Array.from(await run(connection.executeUnprepared(text, values, undefined))) }) },
13
+ schema,
14
+ });
15
+ if (!(await client.isInstalled())) {
16
+ await runSchemaPlan(connection, construction, run);
17
+ return;
18
+ }
19
+ const version = await client.schemaVersion();
20
+ if (version === null) {
21
+ throw new Error("The pg-boss version table is empty");
22
+ }
23
+ if (version > pgBossPackage.pgboss.schema) {
24
+ return;
25
+ }
26
+ if (version >= 27) {
27
+ await resumeSchemaIndexes(client, schema);
28
+ }
29
+ if (version < pgBossPackage.pgboss.schema) {
30
+ const partitionTables = Array.from(await run(connection.executeUnprepared(`SELECT table_name AS "tableName", policy FROM ${schema}.queue WHERE partition = true`, [], undefined)));
31
+ await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables }), run, { schema, version: pgBossPackage.pgboss.schema });
32
+ await resumeSchemaIndexes(client, schema);
33
+ }
34
+ }
35
+ export function installPgBossSchema(options = {}) {
36
+ return Effect.scoped(Effect.gen(function* () {
37
+ const sql = yield* SqlClient.SqlClient;
38
+ const connection = yield* sql.reserve;
39
+ const run = Effect.runPromiseWith(yield* Effect.context());
40
+ yield* Effect.tryPromise({ catch: (error) => error, try: () => installSchema(connection, options, run) });
41
+ }).pipe(Effect.mapError((error) => toPgBossError("install", error)))).pipe(Effect.uninterruptible);
42
+ }
43
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/server/schema.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.9.0",
3
+ "version": "0.10.1",
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": {
@@ -24,9 +24,8 @@
24
24
  },
25
25
  "exports": {
26
26
  "./*.ts": {
27
- "types": "./dist/server/*.d.ts",
28
27
  "source": "./src/server/*.ts",
29
- "import": "./dist/server/*.js",
28
+ "types": "./dist/server/*.d.ts",
30
29
  "default": "./dist/server/*.js"
31
30
  },
32
31
  "./internal/*": null,
@@ -38,19 +37,18 @@
38
37
  "!src/**/*.test.ts",
39
38
  "!src/**/*.spec.ts",
40
39
  "!src/**/test-support",
41
- "docs",
42
40
  "CHANGELOG.md",
43
41
  "README.md"
44
42
  ],
45
43
  "devDependencies": {
46
- "@shivaedev/effect-test-sql": "0.5.4",
47
- "@shivaedev/quality": "0.23.0",
48
- "@shivaedev/test-story": "0.6.0",
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",
49
48
  "@types/node": "24.19.2",
50
- "@typescript/native": "npm:typescript@7.0.2",
51
49
  "effect": "4.0.4",
52
50
  "pg-boss": "12.37.1",
53
- "typescript": "npm:@typescript/typescript6@6.0.2",
51
+ "typescript": "7.0.2",
54
52
  "vitest": "5.0.3"
55
53
  },
56
54
  "peerDependencies": {
@@ -19,6 +19,8 @@ export interface PgBossClient {
19
19
  readonly findJobs?: (name: string, options?: FindJobsOptions) => Promise<readonly JobWithMetadata<unknown>[]>;
20
20
  readonly getDb?: PgBoss["getDb"];
21
21
  readonly getQueue: (name: string) => Promise<QueueResult | null>;
22
+ readonly getWipData?: PgBoss["getWipData"];
23
+ readonly notifyWorker?: PgBoss["notifyWorker"];
22
24
  readonly off: (event: "error", listener: (error: Error) => void) => unknown;
23
25
  readonly offWork: (name: string) => Promise<void>;
24
26
  readonly on: (event: "error", listener: (error: Error) => void) => unknown;
@@ -1,6 +1,6 @@
1
1
  import { Data, Redacted } from "effect";
2
2
 
3
- export type PgBossOperation = "counts" | "enqueue" | "getJob" | "health" | "register" | "retry" | "start" | "stop";
3
+ export type PgBossOperation = "counts" | "enqueue" | "getJob" | "health" | "install" | "register" | "retry" | "start" | "stop";
4
4
 
5
5
  export class PgBossError extends Data.TaggedError("PgBossError")<{
6
6
  readonly operation: PgBossOperation;
@@ -8,7 +8,12 @@ function failureOutput(cause: Cause.Cause<unknown>): object {
8
8
  return typeof error === "object" && error !== null ? error : { message: String(error) };
9
9
  }
10
10
 
11
- export function batchHandler(client: PgBossClient, name: string, run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>) {
11
+ export function batchHandler(
12
+ client: PgBossClient,
13
+ name: string,
14
+ run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>,
15
+ afterFailure?: (job: Job<unknown>) => Promise<void>,
16
+ ) {
12
17
  const complete = client.complete?.bind(client);
13
18
  const fail = client.fail?.bind(client);
14
19
  if (complete === undefined || fail === undefined) {
@@ -37,6 +42,7 @@ export function batchHandler(client: PgBossClient, name: string, run: (job: Job<
37
42
  await complete(name, job);
38
43
  } else {
39
44
  await fail(name, job, failureOutput(exit.cause));
45
+ await afterFailure?.(job);
40
46
  }
41
47
  }
42
48
  if (expired) {
@@ -1,4 +1,4 @@
1
- import { type Context, Effect, Redacted, Schema } from "effect";
1
+ import { type Context, Effect, Redacted, Schema, type Scope } from "effect";
2
2
  import type { Job } from "pg-boss";
3
3
  import type { PgBossClient } from "#client.ts";
4
4
  import {
@@ -10,8 +10,8 @@ import {
10
10
  type ScheduledWorker,
11
11
  } from "#definition.ts";
12
12
  import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
13
- import { batchHandler } from "#internal/batchHandler.ts";
14
13
  import { ensureQueue } from "#internal/ensureQueue.ts";
14
+ import { registerWorker } from "#internal/registerWorker.ts";
15
15
  import { payloadOf, traceJob } from "#internal/tracing.ts";
16
16
 
17
17
  function isQueueWorker(registration: JobRegistration): registration is QueueWorker {
@@ -81,25 +81,28 @@ function runQueueWorker(worker: QueueWorker, job: Job<unknown>, context: Context
81
81
  );
82
82
  }
83
83
 
84
- async function registerQueue(client: PgBossClient, registration: QueueWorker | QueueDefinition, context: Context.Context<unknown>): Promise<void> {
84
+ async function registerQueue(
85
+ client: PgBossClient,
86
+ registration: QueueWorker | QueueDefinition,
87
+ context: Context.Context<unknown>,
88
+ scope: Scope.Scope,
89
+ ): Promise<void> {
85
90
  const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
86
91
  const name = queue.name;
87
92
  if (registration._tag === "QueueDefinition") {
88
93
  return;
89
94
  }
90
- await client.work(
91
- name,
92
- { ...queue.workerOptions, includeMetadata: true },
93
- batchHandler(client, name, (job) => runQueueWorker(registration, job, context)),
94
- );
95
+ await registerWorker(client, name, queue.workerOptions, (job) => runQueueWorker(registration, job, context), scope);
95
96
  }
96
97
 
97
- async function registerSchedule(client: PgBossClient, worker: ScheduledWorker, context: Context.Context<unknown>): Promise<void> {
98
+ async function registerSchedule(client: PgBossClient, worker: ScheduledWorker, context: Context.Context<unknown>, scope: Scope.Scope): Promise<void> {
98
99
  const { schedule } = worker;
99
- await client.work(
100
+ await registerWorker(
101
+ client,
100
102
  schedule.name,
101
- { ...schedule.workerOptions, includeMetadata: true },
102
- batchHandler(client, schedule.name, (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context)),
103
+ schedule.workerOptions,
104
+ (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context),
105
+ scope,
103
106
  );
104
107
  await client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions);
105
108
  }
@@ -117,19 +120,21 @@ export const registerJobs = <R>(
117
120
  registrations: readonly JobRegistration[],
118
121
  context: Context.Context<R>,
119
122
  replaceWorkers: boolean,
120
- ): Effect.Effect<void, PgBossError | PgBossQueueOptionsError> =>
121
- Effect.tryPromise({
122
- catch: (error) => (error instanceof PgBossQueueOptionsError || error instanceof PgBossError ? error : toPgBossError("register", error)),
123
- try: async () => {
124
- registrationNames(registrations);
125
- await prepareQueues(client, registrations, replaceWorkers);
126
- const provided = workerContext(context);
127
- for (const registration of registrations) {
128
- if (isQueueWorker(registration) || isQueueDefinition(registration)) {
129
- await registerQueue(client, registration, provided);
130
- } else if (isScheduledWorker(registration)) {
131
- await registerSchedule(client, registration, provided);
123
+ ): Effect.Effect<void, PgBossError | PgBossQueueOptionsError, Scope.Scope> =>
124
+ Effect.flatMap(Effect.scope, (scope) =>
125
+ Effect.tryPromise({
126
+ catch: (error) => (error instanceof PgBossQueueOptionsError || error instanceof PgBossError ? error : toPgBossError("register", error)),
127
+ try: async () => {
128
+ registrationNames(registrations);
129
+ await prepareQueues(client, registrations, replaceWorkers);
130
+ const provided = workerContext(context);
131
+ for (const registration of registrations) {
132
+ if (isQueueWorker(registration) || isQueueDefinition(registration)) {
133
+ await registerQueue(client, registration, provided, scope);
134
+ } else if (isScheduledWorker(registration)) {
135
+ await registerSchedule(client, registration, provided, scope);
136
+ }
132
137
  }
133
- }
134
- },
135
- });
138
+ },
139
+ }),
140
+ );
@@ -0,0 +1,42 @@
1
+ import { Clock, Effect, type Scope } from "effect";
2
+ import type { Job, WorkOptions } from "pg-boss";
3
+ import type { PgBossClient } from "#client.ts";
4
+ import { batchHandler } from "#internal/batchHandler.ts";
5
+
6
+ export function registerWorker(
7
+ client: PgBossClient,
8
+ name: string,
9
+ options: WorkOptions,
10
+ run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>,
11
+ scope: Scope.Scope,
12
+ ): Promise<string> {
13
+ const registration = client.work(
14
+ name,
15
+ { ...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
+ }),
40
+ );
41
+ return registration;
42
+ }
@@ -0,0 +1,32 @@
1
+ import type { Effect } from "effect";
2
+ import type { Connection } from "effect/sql/SqlConnection";
3
+ import { pendingIndexInserts } from "./schemaIndexBuilds.ts";
4
+
5
+ export async function runSchemaPlan(
6
+ connection: Connection,
7
+ plan: string,
8
+ run: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>,
9
+ upgrade?: { readonly schema: string; readonly version: number },
10
+ ): Promise<void> {
11
+ // Native exported plans append concurrent index DDL after the migration transaction.
12
+ const parts = /^\s*BEGIN;(?<body>[\s\S]*)\n {4}COMMIT;(?<concurrent>[\s\S]*)$/u.exec(plan);
13
+ if (parts?.groups === undefined) {
14
+ throw new Error("Unrecognized pg-boss schema plan transaction");
15
+ }
16
+ const { body = "", concurrent = "" } = parts.groups;
17
+ function quote(text: string) {
18
+ return `'${text.replaceAll("'", "''")}'`;
19
+ }
20
+ const commands = concurrent
21
+ .trim()
22
+ .split(/;\s*(?:\n|$)/u)
23
+ .filter((statement) => statement.trim() !== "");
24
+ const pending = upgrade === undefined ? "" : pendingIndexInserts(upgrade.schema, upgrade.version, commands, quote);
25
+ // Effect SQL uses the extended protocol, which accepts one statement per command.
26
+ await run(connection.executeRaw(`DO ${quote(`BEGIN EXECUTE ${quote(`${body}\n${pending}`)}; END`)}`, []));
27
+ if (upgrade === undefined) {
28
+ for (const statement of commands) {
29
+ await run(connection.executeRaw(statement, []));
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,44 @@
1
+ import type { PgBoss } from "pg-boss";
2
+
3
+ const BUILD_PREFIX = "effect-pg-boss-schema:";
4
+
5
+ function indexName(command: string): string {
6
+ const match =
7
+ /^\s*(?:CREATE (?:UNIQUE )?INDEX CONCURRENTLY IF NOT EXISTS |DROP INDEX CONCURRENTLY IF EXISTS (?:(?:"[^"]+"|\w+)\.))(?<name>\w+)/mu.exec(
8
+ command,
9
+ );
10
+ const name = match?.groups?.name;
11
+ if (name === undefined) {
12
+ throw new Error("Unrecognized pg-boss concurrent index command");
13
+ }
14
+ return name;
15
+ }
16
+
17
+ export function pendingIndexInserts(schema: string, version: number, commands: readonly string[], quote: (text: string) => string): string {
18
+ return commands
19
+ .map((command, position) => {
20
+ const table = indexName(command).replace(/_i\d+$/u, "");
21
+ const name = `${BUILD_PREFIX}${String(position).padStart(6, "0")}`;
22
+ return `INSERT INTO ${schema}.bam (name, version, table_name, command) VALUES (${quote(name)}, ${version}, ${quote(table)}, ${quote(command)});`;
23
+ })
24
+ .join("\n");
25
+ }
26
+
27
+ export async function resumeSchemaIndexes(client: PgBoss, schema: string): Promise<void> {
28
+ const db = client.getDb();
29
+ const { rows } = await db.executeSql(
30
+ `SELECT id, command FROM ${schema}.bam WHERE starts_with(name, $1) AND status <> 'completed' ORDER BY created_on, name`,
31
+ [BUILD_PREFIX],
32
+ );
33
+ for (const row of rows) {
34
+ const name = indexName(row.command);
35
+ if (/^\s*CREATE /mu.test(row.command)) {
36
+ const probe = await db.executeSql("SELECT NOT indisvalid AS invalid FROM pg_index WHERE indexrelid = to_regclass($1)", [`${schema}.${name}`]);
37
+ if (probe.rows[0]?.invalid === true) {
38
+ await db.executeSql(`DROP INDEX CONCURRENTLY IF EXISTS ${schema}."${name}"`);
39
+ }
40
+ }
41
+ await db.executeSql(row.command);
42
+ await db.executeSql(`UPDATE ${schema}.bam SET status = 'completed', completed_on = clock_timestamp() WHERE id = $1`, [row.id]);
43
+ }
44
+ }
@@ -0,0 +1,57 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import type { Connection } from "effect/sql/SqlConnection";
4
+ import { getConstructionPlans, getMigrationPlans, type MigrationPartition, PgBoss } from "pg-boss";
5
+ import pgBossPackage from "pg-boss/package.json" with { type: "json" };
6
+ import { type PgBossError, toPgBossError } from "#error.ts";
7
+ import { runSchemaPlan } from "#internal/runSchemaPlan.ts";
8
+ import { resumeSchemaIndexes } from "#internal/schemaIndexBuilds.ts";
9
+
10
+ export interface PgBossSchemaOptions {
11
+ readonly schema?: string;
12
+ }
13
+
14
+ async function installSchema(
15
+ connection: Connection,
16
+ options: PgBossSchemaOptions,
17
+ run: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>,
18
+ ): Promise<void> {
19
+ const schema = options.schema ?? "pgboss";
20
+ const construction = getConstructionPlans(schema);
21
+ const client = new PgBoss({
22
+ db: { executeSql: async (text, values = []) => ({ rows: Array.from(await run(connection.executeUnprepared(text, values, undefined))) }) },
23
+ schema,
24
+ });
25
+ if (!(await client.isInstalled())) {
26
+ await runSchemaPlan(connection, construction, run);
27
+ return;
28
+ }
29
+ const version = await client.schemaVersion();
30
+ if (version === null) {
31
+ throw new Error("The pg-boss version table is empty");
32
+ }
33
+ if (version > pgBossPackage.pgboss.schema) {
34
+ return;
35
+ }
36
+ if (version >= 27) {
37
+ await resumeSchemaIndexes(client, schema);
38
+ }
39
+ if (version < pgBossPackage.pgboss.schema) {
40
+ const partitionTables: MigrationPartition[] = Array.from(
41
+ await run(connection.executeUnprepared(`SELECT table_name AS "tableName", policy FROM ${schema}.queue WHERE partition = true`, [], undefined)),
42
+ );
43
+ await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables }), run, { schema, version: pgBossPackage.pgboss.schema });
44
+ await resumeSchemaIndexes(client, schema);
45
+ }
46
+ }
47
+
48
+ export function installPgBossSchema(options: PgBossSchemaOptions = {}): Effect.Effect<void, PgBossError, SqlClient.SqlClient> {
49
+ return Effect.scoped(
50
+ Effect.gen(function* () {
51
+ const sql = yield* SqlClient.SqlClient;
52
+ const connection = yield* sql.reserve;
53
+ const run = Effect.runPromiseWith(yield* Effect.context<never>());
54
+ yield* Effect.tryPromise({ catch: (error) => error, try: () => installSchema(connection, options, run) });
55
+ }).pipe(Effect.mapError((error) => toPgBossError("install", error))),
56
+ ).pipe(Effect.uninterruptible);
57
+ }
@@ -1,48 +0,0 @@
1
- # North star
2
-
3
- ## The problem
4
-
5
- A durable job crosses two boundaries at once. Data leaves the producer's process and is read later by a worker; execution leaves the request's Effect context and is called later by pg-boss. When applications bridge those boundaries by hand, they repeat payload validation, dependency capture, queue registration and shutdown logic. A producer can drift from its worker's payload shape, a worker can rebuild services differently from the rest of the application, and a client can outlive the scope that should own it.
6
-
7
- The package exists to make that boundary one reusable path. pg-boss remains the PostgreSQL queue engine. Effect remains the application's language for services, errors and resources. The adapter should join those pieces without making the application learn another execution model.
8
-
9
- ## The ideal
10
-
11
- A feature declares one object Schema for a queue, then attaches an ordinary Effect handler. The producer uses its decoded type; the durable boundary uses its encoded type; the handler receives decoded data. Domain code never has to guess what an old or malformed stored payload means.
12
-
13
- A scheduled feature declares a name and cron expression, then attaches a payload-free Effect. Queue workers and scheduled workers become one list of registrations at the application's runtime boundary. The application's jobs Layer supplies the workers' services and owns the client that pg-boss calls through.
14
-
15
- An application that needs only queueing should not import a repository framework, transport or UI package. A worker that needs application services should declare them through ordinary Effect requirements. The jobs adapter should preserve those requirements rather than hiding them in a global runtime or making each callback rebuild its dependencies.
16
-
17
- ## What good looks like
18
-
19
- - The payload contract is authored once, including transformations between application and durable representations.
20
- - Invalid stored data is rejected before the worker's domain handler executes.
21
- - Worker services are supplied where the jobs Layer is built, and their requirements remain visible to TypeScript.
22
- - The client starts under an Effect scope. Startup failure, registration failure and scope release have a clear resource owner.
23
- - Client reuse is requested with a key. Applications choose the environments and registrations that should share a client.
24
- - pg-boss options remain pg-boss options. The package removes repeated boundary plumbing, not the engine's vocabulary.
25
- - Operational observations are data, such as queue counts; an application decides what those observations mean for its health endpoint and alerts.
26
- - Documentation distinguishes a fake client's observed calls from durable behavior demonstrated against PostgreSQL.
27
-
28
- ## Trade-offs
29
-
30
- **The durable contract wins over permissive input.** A queue Schema must describe object data on both sides. A malformed payload should not become a partially trusted value in domain code merely to keep a worker running.
31
-
32
- **Explicit dependencies win over callback convenience.** The jobs Layer requires the services its handlers use. Adding a hidden runtime or service locator would make a worker shorter while moving its real dependencies out of the type system.
33
-
34
- **Resource ownership wins over reuse.** Sharing a client can avoid duplicate starts across Layer builds, but a cache key is an application choice. It must not become a reason to ignore cleanup or silently infer environment policy.
35
-
36
- **A thin boundary wins over orchestration features.** pg-boss already owns queue execution and scheduling. A new helper earns its place by removing repeated application code at the Effect boundary, with tests for the semantics it preserves.
37
-
38
- **Proven behavior wins over a broader pitch.** Passing `retryBackoff: true` proves registration supplied that option. It does not prove retry timing. A fake client cannot establish a PostgreSQL delivery guarantee, and a PostgreSQL library test cannot establish application adoption or deployment behavior.
39
-
40
- ## What it deliberately leaves out
41
-
42
- - A replacement for pg-boss storage, cron interpretation, queue policy or retry execution.
43
- - Domain workers, authorization rules, external-provider interfaces and application logging or health policy.
44
- - Another general service declaration system; native Context and Layer stay available, and general declaration ergonomics belong to `effect-service`.
45
- - Application repositories or migration ownership; those belong to `effect-sql`.
46
- - Transactional enqueueing: jobs use a separate connection from application SQL transactions. Applications own any required outbox policy; after-commit changes have their own path through `effect-changes`.
47
- - A promise of exactly-once domain side effects or provider idempotency. Those need application-specific decisions and evidence.
48
- - Browser, device or offline synchronization behavior. A PostgreSQL queue boundary cannot establish those product contracts.
package/docs/roadmap.md DELETED
@@ -1,35 +0,0 @@
1
- # Roadmap
2
-
3
- This file owns the package's implementation inventory and open API choices. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md#next-application-and-deployment-acceptance) owns jobs integration with application services and external providers, including deployment and device validation.
4
-
5
- ## Built
6
-
7
- - [x] `makePgBoss` retains its literal service identity in `PgBossDefinition` and its Layer type, so a different jobs Layer does not satisfy the required service. [Compiler evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-pg-boss/src/server/service.typecheck.test.ts).
8
- - [x] Object payload Schema definitions, decoded enqueue types and queue workers. Type tests reject primitive schemas, missing fields and encoded values passed as decoded payloads.
9
- - [x] Encoding at enqueue and decoding before the handler. The PostgreSQL test round-trips a transformed payload and checks that malformed stored data reaches the dead-letter queue without calling the domain handler.
10
- - [x] Queue and schedule registration, with dead-letter queues, default retry settings and UTC schedule registration. The [PostgreSQL schedule test](../src/server/schedulePostgres.spec.ts) reads the stored cron, explicit UTC option, default retries and dead-letter queue through pg-boss. Fake-client tests also verify a retry-limit override.
11
- - [x] Producer-only queue definitions create queues and dead-letter queues through `Jobs.layer({ jobs: [Queue] })`, without starting a consumer. The [PostgreSQL producer test](../src/server/producerPostgres.spec.ts) enqueues a transformed payload and observes worker retry exhaustion and dead-letter delivery. The [starter producer](../../../examples/starter/src/server/noteProducer.ts) uses this API.
12
- - [x] Native pg-boss burst defaults (`batchSize: 10`, `burstWhenBatchFull: true`) and configurable batches and local concurrency. [PostgreSQL backlog evidence](../src/server/throughputPostgres.spec.ts) drains 30 jobs within 2.5 seconds.
13
- - [x] Handlers and scheduled effects can yield their owning jobs service without an external self requirement. [PostgreSQL follow-up evidence](../src/server/followUpPostgres.spec.ts) completes a worker chain and enqueues to a producer-only queue using one scoped client. All declared queues exist before consumers start.
14
- - [x] Worker and scheduled Effect service capture. Runtime and packed-consumer type tests preserve handler requirements.
15
- - [x] Scoped startup and shutdown, including cleanup when startup or registration fails.
16
- - [x] Explicit keyed client reuse and unkeyed per-build acquisition. Lifecycle tests cover two builds sharing one client and replacing the registered worker.
17
- - [x] Full pg-boss queue creation options, excluding the adapter's name and dead-letter setup. [PostgreSQL evidence](../src/server/notifyPostgres.spec.ts) verifies notification delivery to a waiting worker and immediate live counts after completion; [type tests](../src/server/service.typecheck.test.ts) enforce option ownership.
18
- - [x] Schema-decoded lookup by job ID and retry under the same ID for failed or retry-waiting jobs. [PostgreSQL evidence](../src/server/jobControlPostgres.spec.ts) verifies both states complete after a manual retry, missing lookups return `None` and duplicate-ID sends return `None`.
19
- - [x] Startup reconciles declared mutable queue settings and rejects immutable policy or partition changes with a typed error. [PostgreSQL restart evidence](../src/server/queueOptionsPostgres.spec.ts) checks expiry and retry-limit changes on an existing queue.
20
- - [x] Queue health totals for active, failed, queued, ready and dead-lettered counts. The [PostgreSQL health test](../src/server/healthPostgres.spec.ts) checks two populated queues and their totals after the pg-boss monitor refreshes its cached counts, including delayed jobs in queued counts.
21
- - [x] Background error events reach the captured reporting service and the listener is removed on release, verified with a [fake client](../src/server/edgeCases.spec.ts). Job AbortSignal cancellation interrupts the handler and leaves the rest of the batch to pg-boss expiry, verified with a [fake client](../src/server/settlement.spec.ts).
22
- - [x] Public defining-module imports and an installed-package type fixture.
23
-
24
- ## Next
25
-
26
- No further package API expansion is selected. Let the framework's jobs integration work identify the next package change, and preserve the existing payload and lifecycle contracts while it does. That integration's status stays in the framework roadmap.
27
-
28
- ## Decisions
29
-
30
- Enqueueing is separate from application SQL transactions. The package supplies no transactional enqueue adapter. Applications use transactions for database invariants and handle queue errors without failing those transactions. The starter enqueues after its note write transaction and logs queue failures; its [PostgreSQL queue-failure story](../../../examples/starter/src/server/queueFailure.spec.ts) proves that rejected enqueueing leaves create and edit writes saved. Its [rollback story](../../../examples/starter/src/server/committedJobs.spec.ts) also proves that an independently enqueued job survives an outer application rollback.
31
-
32
- ## Open questions
33
-
34
- - Should a shared client key be checked against constructor settings and registration changes? The lifecycle fixture uses the same Layer twice; it does not settle incompatible configurations under one key.
35
- - What evidence is needed before the guide makes stronger claims about actual cron execution? PostgreSQL tests verify schedule registration; cron execution timing remains untested.