@shivaedev/effect-pg-boss 0.8.1 → 0.9.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 +7 -0
- package/README.md +49 -1
- package/dist/server/error.d.ts +9 -0
- package/dist/server/error.d.ts.map +1 -1
- package/dist/server/error.js +2 -0
- package/dist/server/error.js.map +1 -1
- package/dist/server/internal/expireAttempt.d.ts +3 -0
- package/dist/server/internal/expireAttempt.d.ts.map +1 -0
- package/dist/server/internal/expireAttempt.js +14 -0
- package/dist/server/internal/expireAttempt.js.map +1 -0
- package/dist/server/internal/observeTestingSettlements.d.ts +6 -0
- package/dist/server/internal/observeTestingSettlements.d.ts.map +1 -0
- package/dist/server/internal/observeTestingSettlements.js +35 -0
- package/dist/server/internal/observeTestingSettlements.js.map +1 -0
- package/dist/server/internal/testingClient.d.ts +10 -0
- package/dist/server/internal/testingClient.d.ts.map +1 -0
- package/dist/server/internal/testingClient.js +101 -0
- package/dist/server/internal/testingClient.js.map +1 -0
- package/dist/server/internal/testingExpirations.d.ts +6 -0
- package/dist/server/internal/testingExpirations.d.ts.map +1 -0
- package/dist/server/internal/testingExpirations.js +27 -0
- package/dist/server/internal/testingExpirations.js.map +1 -0
- package/dist/server/internal/testingReadout.d.ts +44 -0
- package/dist/server/internal/testingReadout.d.ts.map +1 -0
- package/dist/server/internal/testingReadout.js +13 -0
- package/dist/server/internal/testingReadout.js.map +1 -0
- package/dist/server/internal/testingSignal.d.ts +7 -0
- package/dist/server/internal/testingSignal.d.ts.map +1 -0
- package/dist/server/internal/testingSignal.js +27 -0
- package/dist/server/internal/testingSignal.js.map +1 -0
- package/dist/server/testing.d.ts +61 -0
- package/dist/server/testing.d.ts.map +1 -0
- package/dist/server/testing.js +69 -0
- package/dist/server/testing.js.map +1 -0
- package/package.json +3 -2
- package/src/server/error.ts +6 -0
- package/src/server/internal/expireAttempt.ts +18 -0
- package/src/server/internal/observeTestingSettlements.ts +36 -0
- package/src/server/internal/testingClient.ts +115 -0
- package/src/server/internal/testingExpirations.ts +31 -0
- package/src/server/internal/testingReadout.ts +26 -0
- package/src/server/internal/testingSignal.ts +28 -0
- package/src/server/testing.ts +85 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Export `testing.ts` with `makePgBossTesting`, `testRetryPolicy`, `PgBossTesting` and `PgBossTestingOptions`, plus `PgBossTestingError` from `error.ts`. Story kits can read Schema-decoded jobs, fail the next send, expire a selected attempt, set immediate retries, check batch budgets and schema versions, and wait for local queue changes or worker idleness without application SQL or timer polling.
|
|
8
|
+
- Expiry rules stay with each successfully sent job, and native terminal worker timeouts wake queue-change waiters even when a handler promise stays pending.
|
|
9
|
+
|
|
3
10
|
## 0.8.1 - 2026-10-18
|
|
4
11
|
|
|
5
12
|
### Changed
|
package/README.md
CHANGED
|
@@ -324,6 +324,53 @@ const live = Jobs.layer({
|
|
|
324
324
|
|
|
325
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.
|
|
326
326
|
|
|
327
|
+
### Test through the queue contract
|
|
328
|
+
|
|
329
|
+
`testing.ts` gives story kits a controller for one real pg-boss client. Create a fresh controller per runtime, pass its `clientFactory` to the jobs Layer, 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.
|
|
330
|
+
|
|
331
|
+
This producer story uses the `SendEmail` definition and `Jobs` service from above. `databaseUrl` points to the story's PostgreSQL database:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import { Context, Layer, Option } from "effect";
|
|
335
|
+
import { expect } from "vitest";
|
|
336
|
+
import { storyKit } from "@shivaedev/test-story/storyKit.ts";
|
|
337
|
+
import { makePgBossTesting, testRetryPolicy } from "@shivaedev/effect-pg-boss/testing.ts";
|
|
338
|
+
|
|
339
|
+
const emailStories = storyKit({
|
|
340
|
+
clock: "live",
|
|
341
|
+
*create() {
|
|
342
|
+
const testing = makePgBossTesting({ retryPolicy: testRetryPolicy });
|
|
343
|
+
const context = yield* Layer.build(Jobs.layer({
|
|
344
|
+
connectionString: databaseUrl,
|
|
345
|
+
clientFactory: testing.clientFactory,
|
|
346
|
+
jobs: [SendEmail],
|
|
347
|
+
}));
|
|
348
|
+
return { jobs: Context.get(context, Jobs), testing };
|
|
349
|
+
},
|
|
350
|
+
name: "queued email",
|
|
351
|
+
stages: ["ready"],
|
|
352
|
+
verbs: ({ jobs, testing }) => ({
|
|
353
|
+
enqueue: (userId: number) => jobs.enqueue(SendEmail, { userId }),
|
|
354
|
+
testing,
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
emailStories.it("decodes the queued email", [], function* ({ enqueue, testing }) {
|
|
359
|
+
const id = Option.getOrThrow(yield* enqueue(42));
|
|
360
|
+
expect(yield* testing.jobs(SendEmail)).toMatchObject([
|
|
361
|
+
{ id, data: { userId: 42 }, state: "created" },
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
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
|
+
|
|
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.
|
|
369
|
+
|
|
370
|
+
`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
|
+
|
|
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.
|
|
373
|
+
|
|
327
374
|
### API
|
|
328
375
|
|
|
329
376
|
Import the module that defines the name; the package has no root entry.
|
|
@@ -333,8 +380,9 @@ Import the module that defines the name; the package has no root entry.
|
|
|
333
380
|
| `@shivaedev/effect-pg-boss/definition.ts` | `defineQueue`, `defineSchedule`, `jobContext`; `DefineQueueOptions`, `DefineScheduleOptions`, `JobContext`, `JobPayloadSchema`, `JobRegistration`, `QueueDefinition`, `QueueWorker`, `RegistrationRequirements`, `ScheduleDefinition`, `ScheduledWorker` |
|
|
334
381
|
| `@shivaedev/effect-pg-boss/service.ts` | `makePgBoss`; `PgBossDefinition`, `PgBossLayerOptions`, `PgBossService` |
|
|
335
382
|
| `@shivaedev/effect-pg-boss/health.ts` | `deadLetterQueueName`; `JobsHealth`, `QueueCounts`, `QueueHealth` |
|
|
336
|
-
| `@shivaedev/effect-pg-boss/error.ts` | `PgBossError`, `PgBossPayloadError`, `PgBossQueueOptionsError`, `toPgBossError`; `PgBossOperation` |
|
|
383
|
+
| `@shivaedev/effect-pg-boss/error.ts` | `PgBossError`, `PgBossPayloadError`, `PgBossQueueOptionsError`, `PgBossTestingError`, `toPgBossError`; `PgBossOperation` |
|
|
337
384
|
| `@shivaedev/effect-pg-boss/client.ts` | `defaultClientFactory`; `PgBossClient`, `PgBossClientFactory` |
|
|
385
|
+
| `@shivaedev/effect-pg-boss/testing.ts` | `makePgBossTesting`, `testRetryPolicy`; `PgBossTesting`, `PgBossTestingOptions` |
|
|
338
386
|
|
|
339
387
|
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`.
|
|
340
388
|
|
package/dist/server/error.d.ts
CHANGED
|
@@ -28,6 +28,15 @@ export declare class PgBossPayloadError extends PgBossPayloadError_base<{
|
|
|
28
28
|
readonly original: Redacted.Redacted<unknown>;
|
|
29
29
|
}> {
|
|
30
30
|
}
|
|
31
|
+
declare const PgBossTestingError_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 & {
|
|
32
|
+
readonly _tag: "PgBossTestingError";
|
|
33
|
+
} & Readonly<A>;
|
|
34
|
+
export declare class PgBossTestingError extends PgBossTestingError_base<{
|
|
35
|
+
readonly operation: string;
|
|
36
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
37
|
+
readonly queue?: string | undefined;
|
|
38
|
+
}> {
|
|
39
|
+
}
|
|
31
40
|
export declare const toPgBossError: (operation: PgBossOperation, error: unknown, queue?: string) => PgBossError;
|
|
32
41
|
export {};
|
|
33
42
|
//# sourceMappingURL=error.d.ts.map
|
|
@@ -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,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,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"}
|
package/dist/server/error.js
CHANGED
|
@@ -20,6 +20,8 @@ export class PgBossQueueOptionsError extends Data.TaggedError("PgBossQueueOption
|
|
|
20
20
|
}
|
|
21
21
|
export class PgBossPayloadError extends Data.TaggedError("PgBossPayloadError") {
|
|
22
22
|
}
|
|
23
|
+
export class PgBossTestingError extends Data.TaggedError("PgBossTestingError") {
|
|
24
|
+
}
|
|
23
25
|
export const toPgBossError = (operation, error, queue) => new PgBossError({
|
|
24
26
|
operation,
|
|
25
27
|
original: Redacted.make(error),
|
package/dist/server/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAIxC,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAI7D;IACD,yGAAyG;IACzG,IAAa,OAAO;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,KAAK,EAAE,CAAC;QAC1E,OAAO,WAAW,IAAI,CAAC,SAAS,GAAG,MAAM,YAAY,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;IACpG,CAAC;CACD;AAED,SAAS,YAAY,CAAC,QAAiB;IACtC,IAAI,CAAC,CAAC,QAAQ,YAAY,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,QAAQ,CAAC;IACxB,CAAC;IACD,MAAM,IAAI,GAAY,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC;AACtG,CAAC;AAED,MAAM,OAAO,uBAAwB,SAAQ,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAGrF;IACD,IAAa,OAAO;QACnB,OAAO,SAAS,IAAI,CAAC,KAAK,0CAA0C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC/F,CAAC;CACD;AAED,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAI3E;CAAG;AAEL,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAA0B,EAAE,KAAc,EAAE,KAAc,EAAe,EAAE,CACxG,IAAI,WAAW,CAAC;IACf,SAAS;IACT,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;IAC9B,KAAK;CACL,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAIxC,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAI7D;IACD,yGAAyG;IACzG,IAAa,OAAO;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,KAAK,EAAE,CAAC;QAC1E,OAAO,WAAW,IAAI,CAAC,SAAS,GAAG,MAAM,YAAY,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;IACpG,CAAC;CACD;AAED,SAAS,YAAY,CAAC,QAAiB;IACtC,IAAI,CAAC,CAAC,QAAQ,YAAY,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,QAAQ,CAAC;IACxB,CAAC;IACD,MAAM,IAAI,GAAY,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC;AACtG,CAAC;AAED,MAAM,OAAO,uBAAwB,SAAQ,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAGrF;IACD,IAAa,OAAO;QACnB,OAAO,SAAS,IAAI,CAAC,KAAK,0CAA0C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC/F,CAAC;CACD;AAED,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAI3E;CAAG;AAEL,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAI3E;CAAG;AAEL,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAA0B,EAAE,KAAc,EAAE,KAAc,EAAe,EAAE,CACxG,IAAI,WAAW,CAAC;IACf,SAAS;IACT,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;IAC9B,KAAK;CACL,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expireAttempt.d.ts","sourceRoot":"","sources":["../../../src/server/internal/expireAttempt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE3C,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAepG"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function expireAttempt(client, schema, job) {
|
|
2
|
+
if ((await client.schemaVersion()) !== 45) {
|
|
3
|
+
throw new Error("Testing expiry requires pg-boss schema version 45");
|
|
4
|
+
}
|
|
5
|
+
const quoted = `"${schema.replaceAll('"', '""')}"`;
|
|
6
|
+
await client.getDb().executeSql(`UPDATE ${quoted}.job SET started_on = ${quoted}.job_now() - (expire_seconds + 1) * interval '1 second'
|
|
7
|
+
WHERE name = $1 AND id = $2 AND retry_count = $3 AND state = 'active'`, [job.name, job.id, job.retryCount]);
|
|
8
|
+
// Supervision owns expiry and dead lettering; make its claim due with the job's deadline.
|
|
9
|
+
await client
|
|
10
|
+
.getDb()
|
|
11
|
+
.executeSql(`UPDATE ${quoted}.queue SET monitor_claim_on = ${quoted}.job_now() - interval '1 week' WHERE name = $1`, [job.name]);
|
|
12
|
+
await client.supervise(job.name);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=expireAttempt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expireAttempt.js","sourceRoot":"","sources":["../../../src/server/internal/expireAttempt.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,MAAc,EAAE,GAAiB;IACpF,IAAI,CAAC,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;IACnD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,UAAU,CAC9B,UAAU,MAAM,yBAAyB,MAAM;wEACuB,EACtE,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,UAAU,CAAC,CAClC,CAAC;IACF,0FAA0F;IAC1F,MAAM,MAAM;SACV,KAAK,EAAE;SACP,UAAU,CAAC,UAAU,MAAM,iCAAiC,MAAM,gDAAgD,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAClI,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observeTestingSettlements.d.ts","sourceRoot":"","sources":["../../../src/server/internal/observeTestingSettlements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI;IAG3E,KAAK;IAML,KAAK,OAAO,MAAM;EAwBnB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function observeTestingSettlements(client, changed) {
|
|
2
|
+
const observers = new Map();
|
|
3
|
+
return {
|
|
4
|
+
close() {
|
|
5
|
+
for (const close of observers.values()) {
|
|
6
|
+
close();
|
|
7
|
+
}
|
|
8
|
+
observers.clear();
|
|
9
|
+
},
|
|
10
|
+
watch(name) {
|
|
11
|
+
if (observers.has(name)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const spy = client.getSpy(name);
|
|
15
|
+
let open = true;
|
|
16
|
+
async function observe() {
|
|
17
|
+
while (true) {
|
|
18
|
+
await Promise.race([spy.waitForJob(() => true, "completed"), spy.waitForJob(() => true, "failed")]);
|
|
19
|
+
if (!open) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// This controller owns the spy; clear history and the losing wait before rearming both outcomes.
|
|
23
|
+
spy.clear();
|
|
24
|
+
changed();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
observers.set(name, () => {
|
|
28
|
+
open = false;
|
|
29
|
+
spy.clear();
|
|
30
|
+
});
|
|
31
|
+
void observe().catch((error) => client.emit("error", error instanceof Error ? error : new Error(String(error))));
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=observeTestingSettlements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observeTestingSettlements.js","sourceRoot":"","sources":["../../../src/server/internal/observeTestingSettlements.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,yBAAyB,CAAC,MAAc,EAAE,OAAmB;IAC5E,MAAM,SAAS,GAAG,IAAI,GAAG,EAAsB,CAAC;IAChD,OAAO;QACN,KAAK;YACJ,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;gBACxC,KAAK,EAAE,CAAC;YACT,CAAC;YACD,SAAS,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;QACD,KAAK,CAAC,IAAY;YACjB,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzB,OAAO;YACR,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,IAAI,GAAG,IAAI,CAAC;YAChB,KAAK,UAAU,OAAO;gBACrB,OAAO,IAAI,EAAE,CAAC;oBACb,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;oBACpG,IAAI,CAAC,IAAI,EAAE,CAAC;wBACX,OAAO;oBACR,CAAC;oBACD,iGAAiG;oBACjG,GAAG,CAAC,KAAK,EAAE,CAAC;oBACZ,OAAO,EAAE,CAAC;gBACX,CAAC;YACF,CAAC;YACD,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE;gBACxB,IAAI,GAAG,KAAK,CAAC;gBACb,GAAG,CAAC,KAAK,EAAE,CAAC;YACb,CAAC,CAAC,CAAC;YACH,KAAK,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3H,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PgBoss, SendOptions } from "pg-boss";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
export interface TestingControls {
|
|
4
|
+
readonly changed: () => void;
|
|
5
|
+
readonly expirations: Map<string, number>;
|
|
6
|
+
readonly failures: Set<string>;
|
|
7
|
+
readonly retryPolicy: Pick<SendOptions, "retryBackoff" | "retryDelay">;
|
|
8
|
+
}
|
|
9
|
+
export declare function testingClient(client: PgBoss, schema: string, controls: TestingControls): PgBossClient;
|
|
10
|
+
//# sourceMappingURL=testingClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingClient.d.ts","sourceRoot":"","sources":["../../../src/server/internal/testingClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAO,MAAM,EAAE,WAAW,EAAgD,MAAM,SAAS,CAAC;AACtG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;CACvE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,YAAY,CAqGrG"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { expireAttempt } from "#internal/expireAttempt.ts";
|
|
2
|
+
import { observeTestingSettlements } from "#internal/observeTestingSettlements.ts";
|
|
3
|
+
import { testingExpirations } from "#internal/testingExpirations.ts";
|
|
4
|
+
export function testingClient(client, schema, controls) {
|
|
5
|
+
const settlements = observeTestingSettlements(client, controls.changed);
|
|
6
|
+
const expirations = testingExpirations(client);
|
|
7
|
+
async function update(request, data, options) {
|
|
8
|
+
const result = typeof request === "string" ? await client.update(request, data, options) : await client.update(request);
|
|
9
|
+
controls.changed();
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
const overrides = {
|
|
13
|
+
complete: async (...args) => {
|
|
14
|
+
const result = await client.complete(...args);
|
|
15
|
+
controls.changed();
|
|
16
|
+
return result;
|
|
17
|
+
},
|
|
18
|
+
createQueue: (name, options) => {
|
|
19
|
+
const { retryDelayMax, ...rest } = options ?? {};
|
|
20
|
+
return client.createQueue(name, {
|
|
21
|
+
...rest,
|
|
22
|
+
...(controls.retryPolicy.retryBackoff === false || retryDelayMax === undefined ? {} : { retryDelayMax }),
|
|
23
|
+
...controls.retryPolicy,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
fail: async (...args) => {
|
|
27
|
+
const result = await client.fail(...args);
|
|
28
|
+
controls.changed();
|
|
29
|
+
return result;
|
|
30
|
+
},
|
|
31
|
+
retry: async (...args) => {
|
|
32
|
+
const result = await client.retry(...args);
|
|
33
|
+
controls.changed();
|
|
34
|
+
return result;
|
|
35
|
+
},
|
|
36
|
+
send: async (name, data, options) => {
|
|
37
|
+
if (controls.failures.delete(name)) {
|
|
38
|
+
throw new Error("Injected pg-boss send failure");
|
|
39
|
+
}
|
|
40
|
+
const attempts = controls.expirations.get(name);
|
|
41
|
+
const { retryDelayMax, ...rest } = options ?? {};
|
|
42
|
+
const id = await expirations.send(name, data, {
|
|
43
|
+
...rest,
|
|
44
|
+
...(controls.retryPolicy.retryBackoff === false || retryDelayMax === undefined ? {} : { retryDelayMax }),
|
|
45
|
+
...controls.retryPolicy,
|
|
46
|
+
...(attempts === undefined ? {} : { retryLimit: attempts - 1 }),
|
|
47
|
+
}, attempts);
|
|
48
|
+
controls.changed();
|
|
49
|
+
return id;
|
|
50
|
+
},
|
|
51
|
+
stop: async (options) => {
|
|
52
|
+
try {
|
|
53
|
+
await client.stop(options);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
settlements.close();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
update,
|
|
60
|
+
updateQueue: (name, options) => client.updateQueue(name, {
|
|
61
|
+
...options,
|
|
62
|
+
...controls.retryPolicy,
|
|
63
|
+
...(controls.retryPolicy.retryBackoff === false ? { retryDelayMax: null } : {}),
|
|
64
|
+
}),
|
|
65
|
+
work: (name, options, handler) => {
|
|
66
|
+
settlements.watch(name);
|
|
67
|
+
return client.work(name, options, async (jobs) => {
|
|
68
|
+
controls.changed();
|
|
69
|
+
try {
|
|
70
|
+
const ready = [];
|
|
71
|
+
for (const job of jobs) {
|
|
72
|
+
const attempts = await expirations.attemptsFor(job);
|
|
73
|
+
if (attempts !== undefined && job.retryCount + 1 >= attempts) {
|
|
74
|
+
await expireAttempt(client, schema, job);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
ready.push(job);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (ready.length > 0) {
|
|
81
|
+
await handler(ready);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
controls.changed();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return new Proxy(client, {
|
|
91
|
+
get(target, property) {
|
|
92
|
+
if (Object.hasOwn(overrides, property)) {
|
|
93
|
+
const override = Reflect.get(overrides, property);
|
|
94
|
+
return override;
|
|
95
|
+
}
|
|
96
|
+
const value = Reflect.get(target, property);
|
|
97
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=testingClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingClient.js","sourceRoot":"","sources":["../../../src/server/internal/testingClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Job, PgBoss, SendOptions } from "pg-boss";
|
|
2
|
+
export declare function testingExpirations(client: PgBoss): {
|
|
3
|
+
attemptsFor: (job: Job<unknown>) => Promise<number | undefined> | undefined;
|
|
4
|
+
send(name: string, data: object | null | undefined, options: SendOptions, attempts: number | undefined): Promise<string | null>;
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=testingExpirations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingExpirations.d.ts","sourceRoot":"","sources":["../../../src/server/internal/testingExpirations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAExD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAG/C,WAAW,QAAQ,GAAG,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,MAAM,QAAQ,MAAM,GAAG,IAAI,GAAG,SAAS,WAAW,WAAW,YAAY,MAAM,GAAG,SAAS;EAwBvG"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function testingExpirations(client) {
|
|
2
|
+
const snapshots = new Map();
|
|
3
|
+
return {
|
|
4
|
+
attemptsFor: (job) => snapshots.get(`${job.name}:${job.id}`),
|
|
5
|
+
send(name, data, options, attempts) {
|
|
6
|
+
if (attempts === undefined && options.id === undefined) {
|
|
7
|
+
return client.send(name, data, options);
|
|
8
|
+
}
|
|
9
|
+
const id = options.id ?? crypto.randomUUID();
|
|
10
|
+
const key = `${name}:${id}`;
|
|
11
|
+
const previous = snapshots.get(key);
|
|
12
|
+
const sending = client.send(name, data, { ...options, id });
|
|
13
|
+
// A worker may claim the row before send returns. Wait for its result before applying the snapshot.
|
|
14
|
+
const snapshot = sending
|
|
15
|
+
.then((sent) => (sent === id ? attempts : previous), () => previous)
|
|
16
|
+
.then((value) => {
|
|
17
|
+
if (value === undefined && snapshots.get(key) === snapshot) {
|
|
18
|
+
snapshots.delete(key);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
});
|
|
22
|
+
snapshots.set(key, snapshot);
|
|
23
|
+
return sending;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=testingExpirations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingExpirations.js","sourceRoot":"","sources":["../../../src/server/internal/testingExpirations.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAChD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAuC,CAAC;IACjE,OAAO;QACN,WAAW,EAAE,CAAC,GAAiB,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;QAC1E,IAAI,CAAC,IAAY,EAAE,IAA+B,EAAE,OAAoB,EAAE,QAA4B;YACrG,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;gBACxD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YACzC,CAAC;YACD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YAC7C,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;YAC5D,oGAAoG;YACpG,MAAM,QAAQ,GAAG,OAAO;iBACtB,IAAI,CACJ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,EAC7C,GAAG,EAAE,CAAC,QAAQ,CACd;iBACA,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;gBACf,IAAI,KAAK,KAAK,SAAS,IAAI,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,QAAQ,EAAE,CAAC;oBAC5D,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACvB,CAAC;gBACD,OAAO,KAAK,CAAC;YACd,CAAC,CAAC,CAAC;YACJ,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YAC7B,OAAO,OAAO,CAAC;QAChB,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { FindJobsOptions, PgBoss } from "pg-boss";
|
|
3
|
+
import type { JobPayloadSchema, QueueDefinition } from "#definition.ts";
|
|
4
|
+
import { PgBossPayloadError, PgBossTestingError } from "#error.ts";
|
|
5
|
+
export declare function testingOperation<A>(operation: string, run: () => Promise<A>, queue?: string): Effect.Effect<A, PgBossTestingError, never>;
|
|
6
|
+
export declare function testingReadout(client: () => PgBoss): <TName extends string, TPayload extends JobPayloadSchema>(queue: QueueDefinition<TName, TPayload>, options?: FindJobsOptions) => Effect.Effect<{
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
retryCount: number;
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
groupId?: string | null;
|
|
12
|
+
groupTier?: string | null;
|
|
13
|
+
priority: number;
|
|
14
|
+
state: 'created' | 'retry' | 'active' | 'completed' | 'cancelled' | 'failed';
|
|
15
|
+
retryLimit: number;
|
|
16
|
+
retryDelay: number;
|
|
17
|
+
retryBackoff: boolean;
|
|
18
|
+
retryDelayMax?: number;
|
|
19
|
+
startAfter: Date;
|
|
20
|
+
startedOn: Date;
|
|
21
|
+
singletonKey: string | null;
|
|
22
|
+
singletonOn: Date | null;
|
|
23
|
+
expireInSeconds: number;
|
|
24
|
+
deleteAfterSeconds: number;
|
|
25
|
+
createdOn: Date;
|
|
26
|
+
completedOn: Date | null;
|
|
27
|
+
keepUntil: Date;
|
|
28
|
+
policy: import("pg-boss").QueuePolicy;
|
|
29
|
+
heartbeatOn: Date | null;
|
|
30
|
+
heartbeatSeconds: number | null;
|
|
31
|
+
blocked: boolean;
|
|
32
|
+
blocking: boolean;
|
|
33
|
+
pendingDependencies: number;
|
|
34
|
+
deadLetter: string;
|
|
35
|
+
output: object;
|
|
36
|
+
sourceName: string | null;
|
|
37
|
+
sourceId: string | null;
|
|
38
|
+
sourceCreatedOn: Date | null;
|
|
39
|
+
sourceRetryCount: number | null;
|
|
40
|
+
sourceOutput: object | null;
|
|
41
|
+
sourceRootId: string | null;
|
|
42
|
+
data: TPayload["Type"];
|
|
43
|
+
}[], PgBossPayloadError | PgBossTestingError, TPayload["DecodingServices"]>;
|
|
44
|
+
//# sourceMappingURL=testingReadout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingReadout.d.ts","sourceRoot":"","sources":["../../../src/server/internal/testingReadout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGnE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,+CAK3F;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,MAAM,IAC1C,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,gBAAgB,SAAS,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4EAWnI"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import { PgBossPayloadError, PgBossTestingError } from "#error.ts";
|
|
3
|
+
import { payloadOf } from "#internal/tracing.ts";
|
|
4
|
+
export function testingOperation(operation, run, queue) {
|
|
5
|
+
return Effect.tryPromise({
|
|
6
|
+
catch: (original) => new PgBossTestingError({ operation, original: Redacted.make(original), queue }),
|
|
7
|
+
try: run,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function testingReadout(client) {
|
|
11
|
+
return (queue, options) => testingOperation("jobs", () => client().findJobs(queue.name, options), queue.name).pipe(Effect.flatMap((jobs) => Effect.forEach(jobs, (job) => Schema.decodeUnknownEffect(queue.schema)(payloadOf(job.data)).pipe(Effect.mapError((original) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(original), queue: queue.name })), Effect.map((data) => ({ ...job, data }))))));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=testingReadout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingReadout.js","sourceRoot":"","sources":["../../../src/server/internal/testingReadout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGlD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,MAAM,UAAU,gBAAgB,CAAI,SAAiB,EAAE,GAAqB,EAAE,KAAc;IAC3F,OAAO,MAAM,CAAC,UAAU,CAAC;QACxB,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;QACpG,GAAG,EAAE,GAAG;KACR,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAoB;IAClD,OAAO,CAA0D,KAAuC,EAAE,OAAyB,EAAE,EAAE,CACtI,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAU,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAC/F,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACvB,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAC5B,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CACjE,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,EACpI,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CACxC,CACD,CACD,CACD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingSignal.d.ts","sourceRoot":"","sources":["../../../src/server/internal/testingSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,wBAAgB,aAAa;IAI3B,OAAO;IAOP,QAAQ;IACR,aAAa,UAAU,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;EAatD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
export function testingSignal() {
|
|
3
|
+
let revision = 0;
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
return {
|
|
6
|
+
changed() {
|
|
7
|
+
revision += 1;
|
|
8
|
+
for (const listener of listeners) {
|
|
9
|
+
listener(revision);
|
|
10
|
+
}
|
|
11
|
+
listeners.clear();
|
|
12
|
+
},
|
|
13
|
+
revision: Effect.sync(() => revision),
|
|
14
|
+
waitForChange: (after) => Effect.callback((resume) => {
|
|
15
|
+
if (revision !== after) {
|
|
16
|
+
resume(Effect.succeed(revision));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
function listener(current) {
|
|
20
|
+
resume(Effect.succeed(current));
|
|
21
|
+
}
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return Effect.sync(() => listeners.delete(listener));
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=testingSignal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testingSignal.js","sourceRoot":"","sources":["../../../src/server/internal/testingSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,UAAU,aAAa;IAC5B,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,MAAM,SAAS,GAAG,IAAI,GAAG,EAA8B,CAAC;IACxD,OAAO;QACN,OAAO;YACN,QAAQ,IAAI,CAAC,CAAC;YACd,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBAClC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;YACD,SAAS,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;QACD,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC;QACrC,aAAa,EAAE,CAAC,KAAa,EAAyB,EAAE,CACvD,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,EAAE;YAC1B,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;gBACxB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACjC,OAAO;YACR,CAAC;YACD,SAAS,QAAQ,CAAC,OAAe;gBAChC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YACjC,CAAC;YACD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QACtD,CAAC,CAAC;KACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgBossClientFactory } from "#client.ts";
|
|
3
|
+
import type { QueueDefinition } from "#definition.ts";
|
|
4
|
+
import { PgBossTestingError } from "#error.ts";
|
|
5
|
+
export declare const testRetryPolicy: Readonly<{
|
|
6
|
+
retryBackoff: false;
|
|
7
|
+
retryDelay: 0;
|
|
8
|
+
}>;
|
|
9
|
+
export interface PgBossTestingOptions {
|
|
10
|
+
readonly retryPolicy?: typeof testRetryPolicy;
|
|
11
|
+
}
|
|
12
|
+
export declare function makePgBossTesting(options?: PgBossTestingOptions): {
|
|
13
|
+
checkBatchWindow: (queue: QueueDefinition, millisecondsPerJob: number) => Effect.Effect<void, PgBossTestingError, never>;
|
|
14
|
+
checkSchemaVersion: (expected: number) => Effect.Effect<void, PgBossTestingError, never>;
|
|
15
|
+
clientFactory: PgBossClientFactory;
|
|
16
|
+
expireAfter: (queue: QueueDefinition, attempts: number) => Effect.Effect<void, PgBossTestingError, never>;
|
|
17
|
+
failNextSend: (queue: QueueDefinition) => Effect.Effect<void, never, never>;
|
|
18
|
+
jobs: <TName extends string, TPayload extends import("#definition.ts").JobPayloadSchema>(queue: QueueDefinition<TName, TPayload>, options?: import("pg-boss").FindJobsOptions) => Effect.Effect<{
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
retryCount: number;
|
|
22
|
+
signal: AbortSignal;
|
|
23
|
+
groupId?: string | null;
|
|
24
|
+
groupTier?: string | null;
|
|
25
|
+
priority: number;
|
|
26
|
+
state: 'created' | 'retry' | 'active' | 'completed' | 'cancelled' | 'failed';
|
|
27
|
+
retryLimit: number;
|
|
28
|
+
retryDelay: number;
|
|
29
|
+
retryBackoff: boolean;
|
|
30
|
+
retryDelayMax?: number;
|
|
31
|
+
startAfter: Date;
|
|
32
|
+
startedOn: Date;
|
|
33
|
+
singletonKey: string | null;
|
|
34
|
+
singletonOn: Date | null;
|
|
35
|
+
expireInSeconds: number;
|
|
36
|
+
deleteAfterSeconds: number;
|
|
37
|
+
createdOn: Date;
|
|
38
|
+
completedOn: Date | null;
|
|
39
|
+
keepUntil: Date;
|
|
40
|
+
policy: import("pg-boss").QueuePolicy;
|
|
41
|
+
heartbeatOn: Date | null;
|
|
42
|
+
heartbeatSeconds: number | null;
|
|
43
|
+
blocked: boolean;
|
|
44
|
+
blocking: boolean;
|
|
45
|
+
pendingDependencies: number;
|
|
46
|
+
deadLetter: string;
|
|
47
|
+
output: object;
|
|
48
|
+
sourceName: string | null;
|
|
49
|
+
sourceId: string | null;
|
|
50
|
+
sourceCreatedOn: Date | null;
|
|
51
|
+
sourceRetryCount: number | null;
|
|
52
|
+
sourceOutput: object | null;
|
|
53
|
+
sourceRootId: string | null;
|
|
54
|
+
data: TPayload["Type"];
|
|
55
|
+
}[], import("#error.ts").PgBossPayloadError | PgBossTestingError, TPayload["DecodingServices"]>;
|
|
56
|
+
revision: Effect.Effect<number, never, never>;
|
|
57
|
+
schemaVersion: Effect.Effect<number | null, PgBossTestingError, never>;
|
|
58
|
+
waitForChange: (after: number) => Effect.Effect<number>;
|
|
59
|
+
};
|
|
60
|
+
export type PgBossTesting = ReturnType<typeof makePgBossTesting>;
|
|
61
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAY,MAAM,QAAQ,CAAC;AAE1C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAK/C,eAAO,MAAM,eAAe;;;EAAwD,CAAC;AAErF,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,eAAe,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,oBAAyB;8BAuBxC,eAAe,sBAAsB,MAAM;mCAqBtC,MAAM;;yBAOhB,eAAe,YAAY,MAAM;0BAUhC,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMtC;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Effect, Redacted } from "effect";
|
|
2
|
+
import { PgBoss } from "pg-boss";
|
|
3
|
+
import { PgBossTestingError } from "#error.ts";
|
|
4
|
+
import { testingClient } from "#internal/testingClient.ts";
|
|
5
|
+
import { testingOperation, testingReadout } from "#internal/testingReadout.ts";
|
|
6
|
+
import { testingSignal } from "#internal/testingSignal.ts";
|
|
7
|
+
export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
|
|
8
|
+
export function makePgBossTesting(options = {}) {
|
|
9
|
+
let current;
|
|
10
|
+
const signal = testingSignal();
|
|
11
|
+
const controls = {
|
|
12
|
+
changed: signal.changed,
|
|
13
|
+
expirations: new Map(),
|
|
14
|
+
failures: new Set(),
|
|
15
|
+
retryPolicy: options.retryPolicy ?? {},
|
|
16
|
+
};
|
|
17
|
+
function client() {
|
|
18
|
+
if (current === undefined) {
|
|
19
|
+
throw new Error("Provide the testing clientFactory to a jobs Layer first");
|
|
20
|
+
}
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
const clientFactory = (clientOptions) => {
|
|
24
|
+
if (current !== undefined) {
|
|
25
|
+
throw new Error("A testing controller owns one jobs client; create a controller per runtime");
|
|
26
|
+
}
|
|
27
|
+
current = new PgBoss({ ...clientOptions, "__test__enableSpies": true });
|
|
28
|
+
return testingClient(current, clientOptions.schema ?? "pgboss", controls);
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
checkBatchWindow: (queue, millisecondsPerJob) => testingOperation("checkBatchWindow", async () => {
|
|
32
|
+
const stored = await client().getQueue(queue.name);
|
|
33
|
+
if (stored === null) {
|
|
34
|
+
throw new Error("Queue is not registered");
|
|
35
|
+
}
|
|
36
|
+
if (stored.expireInSeconds === undefined) {
|
|
37
|
+
throw new Error("Queue has no expiry window");
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isFinite(millisecondsPerJob) || millisecondsPerJob < 0) {
|
|
40
|
+
throw new RangeError("Job duration must be nonnegative and finite");
|
|
41
|
+
}
|
|
42
|
+
const required = (queue.workerOptions.batchSize ?? 10) * millisecondsPerJob;
|
|
43
|
+
if (required >= stored.expireInSeconds * 1000) {
|
|
44
|
+
throw new Error("Sequential batch work must fit within the queue expiry window");
|
|
45
|
+
}
|
|
46
|
+
}, queue.name),
|
|
47
|
+
checkSchemaVersion: (expected) => testingOperation("checkSchemaVersion", async () => {
|
|
48
|
+
if ((await client().schemaVersion()) !== expected) {
|
|
49
|
+
throw new Error(`Expected pg-boss schema version ${expected}`);
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
clientFactory,
|
|
53
|
+
expireAfter: (queue, attempts) => Effect.try({
|
|
54
|
+
catch: (original) => new PgBossTestingError({ operation: "expireAfter", original: Redacted.make(original), queue: queue.name }),
|
|
55
|
+
try: () => {
|
|
56
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1) {
|
|
57
|
+
throw new RangeError("Attempts must be a positive integer");
|
|
58
|
+
}
|
|
59
|
+
controls.expirations.set(queue.name, attempts);
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
failNextSend: (queue) => Effect.sync(() => controls.failures.add(queue.name)).pipe(Effect.asVoid),
|
|
63
|
+
jobs: testingReadout(client),
|
|
64
|
+
revision: signal.revision,
|
|
65
|
+
schemaVersion: testingOperation("schemaVersion", () => client().schemaVersion()),
|
|
66
|
+
waitForChange: signal.waitForChange,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-pg-boss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Effect-native jobs and lifecycle management for pg-boss",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-pg-boss#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -43,8 +43,9 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@shivaedev/effect-test-sql": "0.5.
|
|
46
|
+
"@shivaedev/effect-test-sql": "0.5.4",
|
|
47
47
|
"@shivaedev/quality": "0.23.0",
|
|
48
|
+
"@shivaedev/test-story": "0.6.0",
|
|
48
49
|
"@types/node": "24.19.2",
|
|
49
50
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
50
51
|
"effect": "4.0.4",
|
package/src/server/error.ts
CHANGED
|
@@ -37,6 +37,12 @@ export class PgBossPayloadError extends Data.TaggedError("PgBossPayloadError")<{
|
|
|
37
37
|
readonly original: Redacted.Redacted<unknown>;
|
|
38
38
|
}> {}
|
|
39
39
|
|
|
40
|
+
export class PgBossTestingError extends Data.TaggedError("PgBossTestingError")<{
|
|
41
|
+
readonly operation: string;
|
|
42
|
+
readonly original: Redacted.Redacted<unknown>;
|
|
43
|
+
readonly queue?: string | undefined;
|
|
44
|
+
}> {}
|
|
45
|
+
|
|
40
46
|
export const toPgBossError = (operation: PgBossOperation, error: unknown, queue?: string): PgBossError =>
|
|
41
47
|
new PgBossError({
|
|
42
48
|
operation,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Job, PgBoss } from "pg-boss";
|
|
2
|
+
|
|
3
|
+
export async function expireAttempt(client: PgBoss, schema: string, job: Job<unknown>): Promise<void> {
|
|
4
|
+
if ((await client.schemaVersion()) !== 45) {
|
|
5
|
+
throw new Error("Testing expiry requires pg-boss schema version 45");
|
|
6
|
+
}
|
|
7
|
+
const quoted = `"${schema.replaceAll('"', '""')}"`;
|
|
8
|
+
await client.getDb().executeSql(
|
|
9
|
+
`UPDATE ${quoted}.job SET started_on = ${quoted}.job_now() - (expire_seconds + 1) * interval '1 second'
|
|
10
|
+
WHERE name = $1 AND id = $2 AND retry_count = $3 AND state = 'active'`,
|
|
11
|
+
[job.name, job.id, job.retryCount],
|
|
12
|
+
);
|
|
13
|
+
// Supervision owns expiry and dead lettering; make its claim due with the job's deadline.
|
|
14
|
+
await client
|
|
15
|
+
.getDb()
|
|
16
|
+
.executeSql(`UPDATE ${quoted}.queue SET monitor_claim_on = ${quoted}.job_now() - interval '1 week' WHERE name = $1`, [job.name]);
|
|
17
|
+
await client.supervise(job.name);
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PgBoss } from "pg-boss";
|
|
2
|
+
|
|
3
|
+
export function observeTestingSettlements(client: PgBoss, changed: () => void) {
|
|
4
|
+
const observers = new Map<string, () => void>();
|
|
5
|
+
return {
|
|
6
|
+
close() {
|
|
7
|
+
for (const close of observers.values()) {
|
|
8
|
+
close();
|
|
9
|
+
}
|
|
10
|
+
observers.clear();
|
|
11
|
+
},
|
|
12
|
+
watch(name: string) {
|
|
13
|
+
if (observers.has(name)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const spy = client.getSpy(name);
|
|
17
|
+
let open = true;
|
|
18
|
+
async function observe() {
|
|
19
|
+
while (true) {
|
|
20
|
+
await Promise.race([spy.waitForJob(() => true, "completed"), spy.waitForJob(() => true, "failed")]);
|
|
21
|
+
if (!open) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// This controller owns the spy; clear history and the losing wait before rearming both outcomes.
|
|
25
|
+
spy.clear();
|
|
26
|
+
changed();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
observers.set(name, () => {
|
|
30
|
+
open = false;
|
|
31
|
+
spy.clear();
|
|
32
|
+
});
|
|
33
|
+
void observe().catch((error: unknown) => client.emit("error", error instanceof Error ? error : new Error(String(error))));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Job, PgBoss, SendOptions, UpdateOptions, UpdateRequest, UpdateResponse } from "pg-boss";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
import { expireAttempt } from "#internal/expireAttempt.ts";
|
|
4
|
+
import { observeTestingSettlements } from "#internal/observeTestingSettlements.ts";
|
|
5
|
+
import { testingExpirations } from "#internal/testingExpirations.ts";
|
|
6
|
+
|
|
7
|
+
export interface TestingControls {
|
|
8
|
+
readonly changed: () => void;
|
|
9
|
+
readonly expirations: Map<string, number>;
|
|
10
|
+
readonly failures: Set<string>;
|
|
11
|
+
readonly retryPolicy: Pick<SendOptions, "retryBackoff" | "retryDelay">;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function testingClient(client: PgBoss, schema: string, controls: TestingControls): PgBossClient {
|
|
15
|
+
const settlements = observeTestingSettlements(client, controls.changed);
|
|
16
|
+
const expirations = testingExpirations(client);
|
|
17
|
+
function update(request: UpdateRequest): Promise<UpdateResponse>;
|
|
18
|
+
function update(name: string, data: object | null | undefined, options?: UpdateOptions): Promise<UpdateResponse>;
|
|
19
|
+
async function update(request: string | UpdateRequest, data?: object | null, options?: UpdateOptions): Promise<UpdateResponse> {
|
|
20
|
+
const result = typeof request === "string" ? await client.update(request, data, options) : await client.update(request);
|
|
21
|
+
controls.changed();
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
const overrides: Partial<PgBossClient> = {
|
|
25
|
+
complete: async (...args) => {
|
|
26
|
+
const result = await client.complete(...args);
|
|
27
|
+
controls.changed();
|
|
28
|
+
return result;
|
|
29
|
+
},
|
|
30
|
+
createQueue: (name, options) => {
|
|
31
|
+
const { retryDelayMax, ...rest } = options ?? {};
|
|
32
|
+
return client.createQueue(name, {
|
|
33
|
+
...rest,
|
|
34
|
+
...(controls.retryPolicy.retryBackoff === false || retryDelayMax === undefined ? {} : { retryDelayMax }),
|
|
35
|
+
...controls.retryPolicy,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
fail: async (...args) => {
|
|
39
|
+
const result = await client.fail(...args);
|
|
40
|
+
controls.changed();
|
|
41
|
+
return result;
|
|
42
|
+
},
|
|
43
|
+
retry: async (...args) => {
|
|
44
|
+
const result = await client.retry(...args);
|
|
45
|
+
controls.changed();
|
|
46
|
+
return result;
|
|
47
|
+
},
|
|
48
|
+
send: async (name, data, options) => {
|
|
49
|
+
if (controls.failures.delete(name)) {
|
|
50
|
+
throw new Error("Injected pg-boss send failure");
|
|
51
|
+
}
|
|
52
|
+
const attempts = controls.expirations.get(name);
|
|
53
|
+
const { retryDelayMax, ...rest } = options ?? {};
|
|
54
|
+
const id = await expirations.send(
|
|
55
|
+
name,
|
|
56
|
+
data,
|
|
57
|
+
{
|
|
58
|
+
...rest,
|
|
59
|
+
...(controls.retryPolicy.retryBackoff === false || retryDelayMax === undefined ? {} : { retryDelayMax }),
|
|
60
|
+
...controls.retryPolicy,
|
|
61
|
+
...(attempts === undefined ? {} : { retryLimit: attempts - 1 }),
|
|
62
|
+
},
|
|
63
|
+
attempts,
|
|
64
|
+
);
|
|
65
|
+
controls.changed();
|
|
66
|
+
return id;
|
|
67
|
+
},
|
|
68
|
+
stop: async (options) => {
|
|
69
|
+
try {
|
|
70
|
+
await client.stop(options);
|
|
71
|
+
} finally {
|
|
72
|
+
settlements.close();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
update,
|
|
76
|
+
updateQueue: (name, options) =>
|
|
77
|
+
client.updateQueue(name, {
|
|
78
|
+
...options,
|
|
79
|
+
...controls.retryPolicy,
|
|
80
|
+
...(controls.retryPolicy.retryBackoff === false ? { retryDelayMax: null } : {}),
|
|
81
|
+
}),
|
|
82
|
+
work: (name, options, handler) => {
|
|
83
|
+
settlements.watch(name);
|
|
84
|
+
return client.work(name, options, async (jobs: Job<unknown>[]) => {
|
|
85
|
+
controls.changed();
|
|
86
|
+
try {
|
|
87
|
+
const ready: Job<unknown>[] = [];
|
|
88
|
+
for (const job of jobs) {
|
|
89
|
+
const attempts = await expirations.attemptsFor(job);
|
|
90
|
+
if (attempts !== undefined && job.retryCount + 1 >= attempts) {
|
|
91
|
+
await expireAttempt(client, schema, job);
|
|
92
|
+
} else {
|
|
93
|
+
ready.push(job);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (ready.length > 0) {
|
|
97
|
+
await handler(ready);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
controls.changed();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
return new Proxy(client, {
|
|
106
|
+
get(target, property) {
|
|
107
|
+
if (Object.hasOwn(overrides, property)) {
|
|
108
|
+
const override: unknown = Reflect.get(overrides, property);
|
|
109
|
+
return override;
|
|
110
|
+
}
|
|
111
|
+
const value: unknown = Reflect.get(target, property);
|
|
112
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Job, PgBoss, SendOptions } from "pg-boss";
|
|
2
|
+
|
|
3
|
+
export function testingExpirations(client: PgBoss) {
|
|
4
|
+
const snapshots = new Map<string, Promise<number | undefined>>();
|
|
5
|
+
return {
|
|
6
|
+
attemptsFor: (job: Job<unknown>) => snapshots.get(`${job.name}:${job.id}`),
|
|
7
|
+
send(name: string, data: object | null | undefined, options: SendOptions, attempts: number | undefined) {
|
|
8
|
+
if (attempts === undefined && options.id === undefined) {
|
|
9
|
+
return client.send(name, data, options);
|
|
10
|
+
}
|
|
11
|
+
const id = options.id ?? crypto.randomUUID();
|
|
12
|
+
const key = `${name}:${id}`;
|
|
13
|
+
const previous = snapshots.get(key);
|
|
14
|
+
const sending = client.send(name, data, { ...options, id });
|
|
15
|
+
// A worker may claim the row before send returns. Wait for its result before applying the snapshot.
|
|
16
|
+
const snapshot = sending
|
|
17
|
+
.then(
|
|
18
|
+
(sent) => (sent === id ? attempts : previous),
|
|
19
|
+
() => previous,
|
|
20
|
+
)
|
|
21
|
+
.then((value) => {
|
|
22
|
+
if (value === undefined && snapshots.get(key) === snapshot) {
|
|
23
|
+
snapshots.delete(key);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
});
|
|
27
|
+
snapshots.set(key, snapshot);
|
|
28
|
+
return sending;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import type { FindJobsOptions, PgBoss } from "pg-boss";
|
|
3
|
+
import type { JobPayloadSchema, QueueDefinition } from "#definition.ts";
|
|
4
|
+
import { PgBossPayloadError, PgBossTestingError } from "#error.ts";
|
|
5
|
+
import { payloadOf } from "#internal/tracing.ts";
|
|
6
|
+
|
|
7
|
+
export function testingOperation<A>(operation: string, run: () => Promise<A>, queue?: string) {
|
|
8
|
+
return Effect.tryPromise({
|
|
9
|
+
catch: (original) => new PgBossTestingError({ operation, original: Redacted.make(original), queue }),
|
|
10
|
+
try: run,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function testingReadout(client: () => PgBoss) {
|
|
15
|
+
return <TName extends string, TPayload extends JobPayloadSchema>(queue: QueueDefinition<TName, TPayload>, options?: FindJobsOptions) =>
|
|
16
|
+
testingOperation("jobs", () => client().findJobs<unknown>(queue.name, options), queue.name).pipe(
|
|
17
|
+
Effect.flatMap((jobs) =>
|
|
18
|
+
Effect.forEach(jobs, (job) =>
|
|
19
|
+
Schema.decodeUnknownEffect(queue.schema)(payloadOf(job.data)).pipe(
|
|
20
|
+
Effect.mapError((original) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(original), queue: queue.name })),
|
|
21
|
+
Effect.map((data) => ({ ...job, data })),
|
|
22
|
+
),
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
export function testingSignal() {
|
|
4
|
+
let revision = 0;
|
|
5
|
+
const listeners = new Set<(revision: number) => void>();
|
|
6
|
+
return {
|
|
7
|
+
changed() {
|
|
8
|
+
revision += 1;
|
|
9
|
+
for (const listener of listeners) {
|
|
10
|
+
listener(revision);
|
|
11
|
+
}
|
|
12
|
+
listeners.clear();
|
|
13
|
+
},
|
|
14
|
+
revision: Effect.sync(() => revision),
|
|
15
|
+
waitForChange: (after: number): Effect.Effect<number> =>
|
|
16
|
+
Effect.callback((resume) => {
|
|
17
|
+
if (revision !== after) {
|
|
18
|
+
resume(Effect.succeed(revision));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
function listener(current: number) {
|
|
22
|
+
resume(Effect.succeed(current));
|
|
23
|
+
}
|
|
24
|
+
listeners.add(listener);
|
|
25
|
+
return Effect.sync(() => listeners.delete(listener));
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Effect, Redacted } from "effect";
|
|
2
|
+
import { PgBoss } from "pg-boss";
|
|
3
|
+
import type { PgBossClientFactory } from "#client.ts";
|
|
4
|
+
import type { QueueDefinition } from "#definition.ts";
|
|
5
|
+
import { PgBossTestingError } from "#error.ts";
|
|
6
|
+
import { testingClient } from "#internal/testingClient.ts";
|
|
7
|
+
import { testingOperation, testingReadout } from "#internal/testingReadout.ts";
|
|
8
|
+
import { testingSignal } from "#internal/testingSignal.ts";
|
|
9
|
+
|
|
10
|
+
export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
|
|
11
|
+
|
|
12
|
+
export interface PgBossTestingOptions {
|
|
13
|
+
readonly retryPolicy?: typeof testRetryPolicy;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function makePgBossTesting(options: PgBossTestingOptions = {}) {
|
|
17
|
+
let current: PgBoss | undefined;
|
|
18
|
+
const signal = testingSignal();
|
|
19
|
+
const controls = {
|
|
20
|
+
changed: signal.changed,
|
|
21
|
+
expirations: new Map<string, number>(),
|
|
22
|
+
failures: new Set<string>(),
|
|
23
|
+
retryPolicy: options.retryPolicy ?? {},
|
|
24
|
+
};
|
|
25
|
+
function client(): PgBoss {
|
|
26
|
+
if (current === undefined) {
|
|
27
|
+
throw new Error("Provide the testing clientFactory to a jobs Layer first");
|
|
28
|
+
}
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
const clientFactory: PgBossClientFactory = (clientOptions) => {
|
|
32
|
+
if (current !== undefined) {
|
|
33
|
+
throw new Error("A testing controller owns one jobs client; create a controller per runtime");
|
|
34
|
+
}
|
|
35
|
+
current = new PgBoss({ ...clientOptions, "__test__enableSpies": true });
|
|
36
|
+
return testingClient(current, clientOptions.schema ?? "pgboss", controls);
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
checkBatchWindow: (queue: QueueDefinition, millisecondsPerJob: number) =>
|
|
40
|
+
testingOperation(
|
|
41
|
+
"checkBatchWindow",
|
|
42
|
+
async () => {
|
|
43
|
+
const stored = await client().getQueue(queue.name);
|
|
44
|
+
if (stored === null) {
|
|
45
|
+
throw new Error("Queue is not registered");
|
|
46
|
+
}
|
|
47
|
+
if (stored.expireInSeconds === undefined) {
|
|
48
|
+
throw new Error("Queue has no expiry window");
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(millisecondsPerJob) || millisecondsPerJob < 0) {
|
|
51
|
+
throw new RangeError("Job duration must be nonnegative and finite");
|
|
52
|
+
}
|
|
53
|
+
const required = (queue.workerOptions.batchSize ?? 10) * millisecondsPerJob;
|
|
54
|
+
if (required >= stored.expireInSeconds * 1000) {
|
|
55
|
+
throw new Error("Sequential batch work must fit within the queue expiry window");
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
queue.name,
|
|
59
|
+
),
|
|
60
|
+
checkSchemaVersion: (expected: number) =>
|
|
61
|
+
testingOperation("checkSchemaVersion", async () => {
|
|
62
|
+
if ((await client().schemaVersion()) !== expected) {
|
|
63
|
+
throw new Error(`Expected pg-boss schema version ${expected}`);
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
clientFactory,
|
|
67
|
+
expireAfter: (queue: QueueDefinition, attempts: number) =>
|
|
68
|
+
Effect.try({
|
|
69
|
+
catch: (original) => new PgBossTestingError({ operation: "expireAfter", original: Redacted.make(original), queue: queue.name }),
|
|
70
|
+
try: () => {
|
|
71
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1) {
|
|
72
|
+
throw new RangeError("Attempts must be a positive integer");
|
|
73
|
+
}
|
|
74
|
+
controls.expirations.set(queue.name, attempts);
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
failNextSend: (queue: QueueDefinition) => Effect.sync(() => controls.failures.add(queue.name)).pipe(Effect.asVoid),
|
|
78
|
+
jobs: testingReadout(client),
|
|
79
|
+
revision: signal.revision,
|
|
80
|
+
schemaVersion: testingOperation("schemaVersion", () => client().schemaVersion()),
|
|
81
|
+
waitForChange: signal.waitForChange,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type PgBossTesting = ReturnType<typeof makePgBossTesting>;
|