@shivaedev/effect-pg-boss 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +117 -0
  4. package/dist/definition.d.ts +61 -0
  5. package/dist/definition.d.ts.map +1 -0
  6. package/dist/definition.js +39 -0
  7. package/dist/definition.js.map +1 -0
  8. package/dist/error.d.ts +23 -0
  9. package/dist/error.d.ts.map +1 -0
  10. package/dist/error.js +11 -0
  11. package/dist/error.js.map +1 -0
  12. package/dist/health.d.ts +18 -0
  13. package/dist/health.d.ts.map +1 -0
  14. package/dist/health.js +2 -0
  15. package/dist/health.js.map +1 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +5 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/internal/client.d.ts +16 -0
  21. package/dist/internal/client.d.ts.map +1 -0
  22. package/dist/internal/client.js +3 -0
  23. package/dist/internal/client.js.map +1 -0
  24. package/dist/internal/health.d.ts +5 -0
  25. package/dist/internal/health.d.ts.map +1 -0
  26. package/dist/internal/health.js +28 -0
  27. package/dist/internal/health.js.map +1 -0
  28. package/dist/internal/lifecycle.d.ts +16 -0
  29. package/dist/internal/lifecycle.d.ts.map +1 -0
  30. package/dist/internal/lifecycle.js +83 -0
  31. package/dist/internal/lifecycle.js.map +1 -0
  32. package/dist/internal/register.d.ts +6 -0
  33. package/dist/internal/register.d.ts.map +1 -0
  34. package/dist/internal/register.js +72 -0
  35. package/dist/internal/register.js.map +1 -0
  36. package/dist/internal/service.d.ts +4 -0
  37. package/dist/internal/service.d.ts.map +1 -0
  38. package/dist/internal/service.js +24 -0
  39. package/dist/internal/service.js.map +1 -0
  40. package/dist/service.d.ts +27 -0
  41. package/dist/service.d.ts.map +1 -0
  42. package/dist/service.js +43 -0
  43. package/dist/service.js.map +1 -0
  44. package/package.json +59 -0
  45. package/src/definition.ts +144 -0
  46. package/src/error.ts +31 -0
  47. package/src/health.ts +20 -0
  48. package/src/index.ts +33 -0
  49. package/src/internal/client.ts +45 -0
  50. package/src/internal/health.ts +47 -0
  51. package/src/internal/lifecycle.ts +121 -0
  52. package/src/internal/register.ts +135 -0
  53. package/src/internal/service.ts +51 -0
  54. package/src/service.ts +148 -0
@@ -0,0 +1,72 @@
1
+ import { Effect, Redacted, Schema } from "effect";
2
+ import { jobContext, } from "../definition.js";
3
+ import { PgBossPayloadError, toPgBossError } from "../error.js";
4
+ import { deadLetterQueueName } from "../health.js";
5
+ const queueOptions = (options) => ({
6
+ retryBackoff: true,
7
+ retryLimit: 3,
8
+ ...options,
9
+ });
10
+ const runQueueWorker = (worker, job, context) => Effect.runPromise(Schema.decodeUnknownEffect(worker.queue.schema)(job.data).pipe(Effect.mapError((error) => new PgBossPayloadError({
11
+ direction: "decode",
12
+ original: Redacted.make(error),
13
+ queue: worker.queue.name,
14
+ })), Effect.flatMap((payload) => worker.handler(payload, jobContext(job))), Effect.provide(context)), { signal: job.signal });
15
+ const registerQueue = async (client, worker, context, replaceWorker) => {
16
+ const name = worker.queue.name;
17
+ const deadLetter = deadLetterQueueName(name);
18
+ if (replaceWorker)
19
+ await client.offWork(name);
20
+ await client.createQueue(deadLetter);
21
+ await client.createQueue(name, {
22
+ ...queueOptions(worker.queue.queueOptions),
23
+ deadLetter,
24
+ });
25
+ await client.work(name, worker.queue.workerOptions, async (jobs) => {
26
+ for (const job of jobs)
27
+ await runQueueWorker(worker, job, context);
28
+ });
29
+ };
30
+ const registerSchedule = async (client, worker, context, replaceWorker) => {
31
+ const { schedule } = worker;
32
+ const deadLetter = deadLetterQueueName(schedule.name);
33
+ if (replaceWorker)
34
+ await client.offWork(schedule.name);
35
+ await client.createQueue(deadLetter);
36
+ await client.createQueue(schedule.name, {
37
+ ...queueOptions(schedule.queueOptions),
38
+ deadLetter,
39
+ });
40
+ await client.work(schedule.name, schedule.workerOptions, async (jobs) => {
41
+ for (const job of jobs) {
42
+ await Effect.runPromise(Effect.provide(worker.effect, context), {
43
+ signal: job.signal,
44
+ });
45
+ }
46
+ });
47
+ await client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions);
48
+ };
49
+ export const registrationNames = (registrations) => {
50
+ const names = registrations.map((registration) => registration._tag === "QueueWorker"
51
+ ? registration.queue.name
52
+ : registration.schedule.name);
53
+ if (new Set(names).size !== names.length) {
54
+ throw new TypeError("pg-boss job names must be unique");
55
+ }
56
+ return names;
57
+ };
58
+ export const registerJobs = (client, registrations, context, replaceWorkers) => Effect.tryPromise({
59
+ try: async () => {
60
+ registrationNames(registrations);
61
+ for (const registration of registrations) {
62
+ if (registration._tag === "QueueWorker") {
63
+ await registerQueue(client, registration, context, replaceWorkers);
64
+ }
65
+ else {
66
+ await registerSchedule(client, registration, context, replaceWorkers);
67
+ }
68
+ }
69
+ },
70
+ catch: (error) => toPgBossError("register", error),
71
+ });
72
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/internal/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { PgBossService } from "../service.js";
2
+ import type { PgBossClient } from "./client.js";
3
+ export declare const makeService: (client: PgBossClient, names: readonly string[]) => PgBossService;
4
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/internal/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,eAAO,MAAM,WAAW,GACvB,QAAQ,YAAY,EACpB,OAAO,SAAS,MAAM,EAAE,KACtB,aAqCD,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { Effect, Option, Redacted, Schema } from "effect";
2
+ import { PgBossPayloadError, toPgBossError, } from "../error.js";
3
+ import { healthFor } from "./health.js";
4
+ export const makeService = (client, names) => ({
5
+ enqueue: (queue, payload, options) => Schema.encodeUnknownEffect(queue.schema)(payload).pipe(Effect.mapError((error) => new PgBossPayloadError({
6
+ direction: "encode",
7
+ original: Redacted.make(error),
8
+ queue: queue.name,
9
+ })), Effect.flatMap((encoded) => {
10
+ if (typeof encoded !== "object" || encoded === null) {
11
+ return Effect.fail(new PgBossPayloadError({
12
+ direction: "encode",
13
+ original: Redacted.make(new TypeError("pg-boss payloads must encode to objects")),
14
+ queue: queue.name,
15
+ }));
16
+ }
17
+ return Effect.tryPromise({
18
+ try: () => client.send(queue.name, encoded, options),
19
+ catch: (error) => toPgBossError("enqueue", error, queue.name),
20
+ }).pipe(Effect.map(Option.fromNullishOr));
21
+ })),
22
+ health: healthFor(client, names),
23
+ });
24
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/internal/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAEN,kBAAkB,EAClB,aAAa,GACb,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,CAAC,MAAM,WAAW,GAAG,CAC1B,MAAoB,EACpB,KAAwB,EACR,EAAE,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CACpC,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CACrD,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,KAAK,CAAC,IAAI;KACjB,CAAC,CACH,EACD,MAAM,CAAC,OAAO,CACb,CACC,OAAO,EAIN,EAAE;QACH,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrD,OAAO,MAAM,CAAC,IAAI,CACjB,IAAI,kBAAkB,CAAC;gBACtB,SAAS,EAAE,QAAQ;gBACnB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CACtB,IAAI,SAAS,CAAC,yCAAyC,CAAC,CACxD;gBACD,KAAK,EAAE,KAAK,CAAC,IAAI;aACjB,CAAC,CACF,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC,UAAU,CAAC;YACxB,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC;YACpD,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC;SAC7D,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;IAC3C,CAAC,CACD,CACD;IACF,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC;CAChC,CAAC,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Layer, type Option, type Schema } from "effect";
2
+ import type { ConstructorOptions, SendOptions, StopOptions } from "pg-boss";
3
+ import type { JobPayloadSchema, JobRegistration, QueueDefinition, RegistrationRequirements } from "./definition.js";
4
+ import type { PgBossError, PgBossPayloadError } from "./error.js";
5
+ import type { JobsHealth } from "./health.js";
6
+ import type { PgBossClientFactory } from "./internal/client.js";
7
+ export interface PgBossService {
8
+ readonly enqueue: <const Name extends string, const Payload extends JobPayloadSchema>(queue: QueueDefinition<Name, Payload>, payload: Schema.Schema.Type<Payload>, options?: SendOptions) => Effect.Effect<Option.Option<string>, PgBossError | PgBossPayloadError, Payload["EncodingServices"]>;
9
+ readonly health: Effect.Effect<JobsHealth, PgBossError>;
10
+ }
11
+ interface PgBossIdentifier {
12
+ readonly _pgBossIdentifier: unique symbol;
13
+ }
14
+ export type PgBossLayerOptions<Registrations extends readonly JobRegistration[], ErrorRequirements> = ConstructorOptions & {
15
+ /** Override client construction for compatible clients or deterministic tests. */
16
+ readonly clientFactory?: PgBossClientFactory;
17
+ readonly developmentCacheKey?: string | symbol;
18
+ readonly jobs: Registrations;
19
+ readonly onError?: (error: Error) => Effect.Effect<unknown, never, ErrorRequirements>;
20
+ readonly stop?: StopOptions;
21
+ };
22
+ export interface PgBossDefinition extends Context.Service<PgBossIdentifier, PgBossService> {
23
+ readonly layer: <const Registrations extends readonly JobRegistration[], ErrorRequirements = never>(options: PgBossLayerOptions<Registrations, ErrorRequirements>) => Layer.Layer<PgBossIdentifier, PgBossError, RegistrationRequirements<Registrations> | ErrorRequirements>;
24
+ }
25
+ export declare const makePgBoss: (identifier: string) => PgBossDefinition;
26
+ export {};
27
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,KAAK,EACX,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,wBAAwB,EACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAKhE,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,OAAO,EAAE,CACjB,KAAK,CAAC,IAAI,SAAS,MAAM,EACzB,KAAK,CAAC,OAAO,SAAS,gBAAgB,EAEtC,KAAK,EAAE,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,EACrC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EACpC,OAAO,CAAC,EAAE,WAAW,KACjB,MAAM,CAAC,MAAM,CACjB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EACrB,WAAW,GAAG,kBAAkB,EAChC,OAAO,CAAC,kBAAkB,CAAC,CAC3B,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;CACxD;AAED,UAAU,gBAAgB;IACzB,QAAQ,CAAC,iBAAiB,EAAE,OAAO,MAAM,CAAC;CAC1C;AAED,MAAM,MAAM,kBAAkB,CAC7B,aAAa,SAAS,SAAS,eAAe,EAAE,EAChD,iBAAiB,IACd,kBAAkB,GAAG;IACxB,kFAAkF;IAClF,QAAQ,CAAC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IAC7C,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAClB,KAAK,EAAE,KAAK,KACR,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;IACtD,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;CAC5B,CAAC;AAEF,MAAM,WAAW,gBAChB,SAAQ,OAAO,CAAC,OAAO,CAAC,gBAAgB,EAAE,aAAa,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,CACf,KAAK,CAAC,aAAa,SAAS,SAAS,eAAe,EAAE,EACtD,iBAAiB,GAAG,KAAK,EAEzB,OAAO,EAAE,kBAAkB,CAAC,aAAa,EAAE,iBAAiB,CAAC,KACzD,KAAK,CAAC,KAAK,CACf,gBAAgB,EAChB,WAAW,EACX,wBAAwB,CAAC,aAAa,CAAC,GAAG,iBAAiB,CAC3D,CAAC;CACF;AAED,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,KAAG,gBAoF/C,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { acquireClient, releaseClient } from "./internal/lifecycle.js";
3
+ import { registerJobs } from "./internal/register.js";
4
+ import { makeService } from "./internal/service.js";
5
+ export const makePgBoss = (identifier) => {
6
+ const Service = Context.Service(identifier);
7
+ const layer = (options) => {
8
+ const names = options.jobs.map((registration) => registration._tag === "QueueWorker"
9
+ ? registration.queue.name
10
+ : registration.schedule
11
+ .name);
12
+ const { clientFactory, developmentCacheKey, jobs, onError, stop, ...constructorOptions } = options;
13
+ const acquire = Effect.acquireRelease(Effect.gen(function* () {
14
+ const context = yield* Effect.context();
15
+ const acquired = yield* acquireClient({
16
+ clientFactory,
17
+ constructor: constructorOptions,
18
+ developmentCacheKey,
19
+ });
20
+ const reportError = onError === undefined
21
+ ? (error) => Effect.logError({
22
+ event: "pg_boss_error",
23
+ message: error.message,
24
+ })
25
+ : onError;
26
+ const errorListener = (error) => {
27
+ void Effect.runPromise(Effect.exit(Effect.provide(reportError(error), context)));
28
+ };
29
+ acquired.client.on("error", errorListener);
30
+ const registration = registerJobs(acquired.client, jobs, context, acquired.reused);
31
+ return yield* registration.pipe(Effect.as({ acquired, context, errorListener }), Effect.onError(() => {
32
+ acquired.client.off("error", errorListener);
33
+ return releaseClient(acquired, stop);
34
+ }));
35
+ }), (resource) => {
36
+ resource.acquired.client.off("error", resource.errorListener);
37
+ return releaseClient(resource.acquired, stop);
38
+ });
39
+ return Layer.effect(Service, Effect.map(acquire, ({ acquired }) => makeService(acquired.client, names)));
40
+ };
41
+ return Object.assign(Service, { layer });
42
+ };
43
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@shivaedev/effect-pg-boss",
3
+ "version": "0.0.0",
4
+ "description": "Effect-native jobs and lifecycle management for pg-boss",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ShivaeDev/platform.git",
10
+ "directory": "packages/effect-pg-boss"
11
+ },
12
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-pg-boss#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/ShivaeDev/platform/issues"
15
+ },
16
+ "engines": {
17
+ "node": ">=24"
18
+ },
19
+ "sideEffects": false,
20
+ "files": [
21
+ "dist",
22
+ "src",
23
+ "CHANGELOG.md",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "source": "./src/index.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "peerDependencies": {
39
+ "effect": "4.0.0-beta.102",
40
+ "pg-boss": "12.23.1"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "24.10.1",
44
+ "@typescript/native": "npm:typescript@7.0.2",
45
+ "effect": "4.0.0-beta.102",
46
+ "pg-boss": "12.23.1",
47
+ "typescript": "npm:@typescript/typescript6@6.0.2",
48
+ "vitest": "4.1.9"
49
+ },
50
+ "scripts": {
51
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
52
+ "check": "biome check .",
53
+ "test": "vitest run",
54
+ "test:package": "node scripts/test-package.mjs",
55
+ "typecheck": "tsc --noEmit",
56
+ "typecheck:compat": "tsc6 --noEmit",
57
+ "ready": "pnpm check && pnpm typecheck && pnpm typecheck:compat && pnpm test && pnpm build && pnpm test:package"
58
+ }
59
+ }
@@ -0,0 +1,144 @@
1
+ import type { Effect, Schema } from "effect";
2
+ import type { Job, QueueOptions, ScheduleOptions, WorkOptions } from "pg-boss";
3
+
4
+ export interface JobPayloadSchema extends Schema.Top {
5
+ readonly Encoded: object;
6
+ readonly Type: object;
7
+ }
8
+
9
+ export interface JobContext {
10
+ readonly id: string;
11
+ readonly name: string;
12
+ readonly signal: AbortSignal;
13
+ readonly expireInSeconds: number;
14
+ readonly heartbeatSeconds: number | null;
15
+ }
16
+
17
+ export interface QueueDefinition<
18
+ Name extends string = string,
19
+ Payload extends JobPayloadSchema = JobPayloadSchema,
20
+ > {
21
+ readonly _tag: "QueueDefinition";
22
+ readonly name: Name;
23
+ readonly schema: Payload;
24
+ readonly queueOptions: Omit<QueueOptions, "deadLetter">;
25
+ readonly workerOptions: WorkOptions;
26
+ readonly handle: <E, R>(
27
+ handler: (
28
+ payload: Schema.Schema.Type<Payload>,
29
+ job: JobContext,
30
+ ) => Effect.Effect<unknown, E, R>,
31
+ ) => QueueWorker<Payload, R>;
32
+ }
33
+
34
+ export interface QueueWorker<
35
+ Payload extends JobPayloadSchema = JobPayloadSchema,
36
+ Requirements = unknown,
37
+ > {
38
+ readonly _tag: "QueueWorker";
39
+ readonly queue: QueueDefinition<string, Payload>;
40
+ readonly handler: (
41
+ payload: Schema.Schema.Type<Payload>,
42
+ job: JobContext,
43
+ ) => Effect.Effect<unknown, unknown, Requirements>;
44
+ }
45
+
46
+ export interface JobRegistration {
47
+ readonly _tag: "QueueWorker" | "ScheduledWorker";
48
+ }
49
+
50
+ export interface ScheduleDefinition<Name extends string = string> {
51
+ readonly _tag: "ScheduleDefinition";
52
+ readonly cron: string;
53
+ readonly name: Name;
54
+ readonly queueOptions: Omit<QueueOptions, "deadLetter">;
55
+ readonly scheduleOptions: ScheduleOptions;
56
+ readonly workerOptions: WorkOptions;
57
+ readonly run: <E, R>(
58
+ effect: Effect.Effect<unknown, E, R>,
59
+ ) => ScheduledWorker<R>;
60
+ }
61
+
62
+ export interface ScheduledWorker<Requirements = unknown> {
63
+ readonly _tag: "ScheduledWorker";
64
+ readonly schedule: ScheduleDefinition;
65
+ readonly effect: Effect.Effect<unknown, unknown, Requirements>;
66
+ }
67
+
68
+ export type RegistrationRequirements<
69
+ Registrations extends readonly JobRegistration[],
70
+ > = Registrations[number] extends infer Registration
71
+ ? Registration extends QueueWorker<infer Payload, infer Requirements>
72
+ ? Requirements | Payload["DecodingServices"]
73
+ : Registration extends ScheduledWorker<infer Requirements>
74
+ ? Requirements
75
+ : never
76
+ : never;
77
+
78
+ export interface DefineQueueOptions<
79
+ Name extends string,
80
+ Payload extends JobPayloadSchema,
81
+ > {
82
+ readonly name: Name;
83
+ readonly schema: Payload;
84
+ readonly queue?: Omit<QueueOptions, "deadLetter">;
85
+ readonly worker?: WorkOptions;
86
+ }
87
+
88
+ export const defineQueue = <
89
+ const Name extends string,
90
+ const Payload extends JobPayloadSchema,
91
+ >(
92
+ options: DefineQueueOptions<Name, Payload>,
93
+ ): QueueDefinition<Name, Payload> => {
94
+ const definition: QueueDefinition<Name, Payload> = {
95
+ _tag: "QueueDefinition",
96
+ name: options.name,
97
+ queueOptions: Object.freeze({ ...options.queue }),
98
+ schema: options.schema,
99
+ workerOptions: Object.freeze({ batchSize: 1, ...options.worker }),
100
+ handle: (handler) =>
101
+ Object.freeze({
102
+ _tag: "QueueWorker" as const,
103
+ handler,
104
+ queue: definition,
105
+ }),
106
+ };
107
+ return Object.freeze(definition);
108
+ };
109
+
110
+ export interface DefineScheduleOptions<Name extends string> {
111
+ readonly cron: string;
112
+ readonly name: Name;
113
+ readonly queue?: Omit<QueueOptions, "deadLetter">;
114
+ readonly schedule?: ScheduleOptions;
115
+ readonly worker?: WorkOptions;
116
+ }
117
+
118
+ export const defineSchedule = <const Name extends string>(
119
+ options: DefineScheduleOptions<Name>,
120
+ ): ScheduleDefinition<Name> => {
121
+ const definition: ScheduleDefinition<Name> = {
122
+ _tag: "ScheduleDefinition",
123
+ cron: options.cron,
124
+ name: options.name,
125
+ queueOptions: Object.freeze({ ...options.queue }),
126
+ scheduleOptions: Object.freeze({ tz: "UTC", ...options.schedule }),
127
+ workerOptions: Object.freeze({ batchSize: 1, ...options.worker }),
128
+ run: (effect) =>
129
+ Object.freeze({
130
+ _tag: "ScheduledWorker" as const,
131
+ effect,
132
+ schedule: definition,
133
+ }),
134
+ };
135
+ return Object.freeze(definition);
136
+ };
137
+
138
+ export const jobContext = (job: Job<unknown>): JobContext => ({
139
+ expireInSeconds: job.expireInSeconds,
140
+ heartbeatSeconds: job.heartbeatSeconds,
141
+ id: job.id,
142
+ name: job.name,
143
+ signal: job.signal,
144
+ });
package/src/error.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { Data, Redacted } from "effect";
2
+
3
+ export type PgBossOperation =
4
+ | "enqueue"
5
+ | "health"
6
+ | "register"
7
+ | "start"
8
+ | "stop";
9
+
10
+ export class PgBossError extends Data.TaggedError("PgBossError")<{
11
+ readonly operation: PgBossOperation;
12
+ readonly queue?: string;
13
+ readonly original: Redacted.Redacted<unknown>;
14
+ }> {}
15
+
16
+ export class PgBossPayloadError extends Data.TaggedError("PgBossPayloadError")<{
17
+ readonly direction: "decode" | "encode";
18
+ readonly queue: string;
19
+ readonly original: Redacted.Redacted<unknown>;
20
+ }> {}
21
+
22
+ export const toPgBossError = (
23
+ operation: PgBossOperation,
24
+ error: unknown,
25
+ queue?: string,
26
+ ): PgBossError =>
27
+ new PgBossError({
28
+ operation,
29
+ queue,
30
+ original: Redacted.make(error),
31
+ });
package/src/health.ts ADDED
@@ -0,0 +1,20 @@
1
+ export interface QueueHealth {
2
+ readonly activeCount: number;
3
+ readonly deadLetteredCount: number;
4
+ readonly failedCount: number;
5
+ readonly name: string;
6
+ readonly queuedCount: number;
7
+ readonly readyCount: number;
8
+ }
9
+
10
+ export interface JobsHealth {
11
+ readonly activeTotal: number;
12
+ readonly deadLetteredTotal: number;
13
+ readonly failedTotal: number;
14
+ readonly jobs: readonly QueueHealth[];
15
+ readonly queuedTotal: number;
16
+ readonly readyTotal: number;
17
+ }
18
+
19
+ export const deadLetterQueueName = (queueName: string): string =>
20
+ `${queueName}-dlq`;
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export {
2
+ type DefineQueueOptions,
3
+ type DefineScheduleOptions,
4
+ defineQueue,
5
+ defineSchedule,
6
+ type JobContext,
7
+ type JobPayloadSchema,
8
+ type JobRegistration,
9
+ type QueueDefinition,
10
+ type QueueWorker,
11
+ type ScheduleDefinition,
12
+ type ScheduledWorker,
13
+ } from "./definition.js";
14
+ export {
15
+ PgBossError,
16
+ type PgBossOperation,
17
+ PgBossPayloadError,
18
+ } from "./error.js";
19
+ export {
20
+ deadLetterQueueName,
21
+ type JobsHealth,
22
+ type QueueHealth,
23
+ } from "./health.js";
24
+ export type {
25
+ PgBossClient,
26
+ PgBossClientFactory,
27
+ } from "./internal/client.js";
28
+ export {
29
+ makePgBoss,
30
+ type PgBossDefinition,
31
+ type PgBossLayerOptions,
32
+ type PgBossService,
33
+ } from "./service.js";
@@ -0,0 +1,45 @@
1
+ import type {
2
+ ConstructorOptions,
3
+ Job,
4
+ Queue,
5
+ QueueResult,
6
+ ScheduleOptions,
7
+ SendOptions,
8
+ StopOptions,
9
+ WorkOptions,
10
+ } from "pg-boss";
11
+ import { PgBoss } from "pg-boss";
12
+
13
+ export interface PgBossClient {
14
+ readonly createQueue: (
15
+ name: string,
16
+ options?: Omit<Queue, "name">,
17
+ ) => Promise<void>;
18
+ readonly getQueue: (name: string) => Promise<QueueResult | null>;
19
+ readonly offWork: (name: string) => Promise<void>;
20
+ readonly on: (event: "error", listener: (error: Error) => void) => unknown;
21
+ readonly off: (event: "error", listener: (error: Error) => void) => unknown;
22
+ readonly schedule: (
23
+ name: string,
24
+ cron: string,
25
+ data?: object | null,
26
+ options?: ScheduleOptions,
27
+ ) => Promise<void>;
28
+ readonly send: (
29
+ name: string,
30
+ data?: object | null,
31
+ options?: SendOptions,
32
+ ) => Promise<string | null>;
33
+ readonly start: () => Promise<unknown>;
34
+ readonly stop: (options?: StopOptions) => Promise<void>;
35
+ readonly work: <Payload>(
36
+ name: string,
37
+ options: WorkOptions,
38
+ handler: (jobs: readonly Job<Payload>[]) => Promise<unknown>,
39
+ ) => Promise<string>;
40
+ }
41
+
42
+ export type PgBossClientFactory = (options: ConstructorOptions) => PgBossClient;
43
+
44
+ export const defaultClientFactory: PgBossClientFactory = (options) =>
45
+ new PgBoss(options);
@@ -0,0 +1,47 @@
1
+ import { Effect } from "effect";
2
+ import { toPgBossError } from "../error.js";
3
+ import {
4
+ deadLetterQueueName,
5
+ type JobsHealth,
6
+ type QueueHealth,
7
+ } from "../health.js";
8
+ import type { PgBossClient } from "./client.js";
9
+
10
+ export const healthFor = (
11
+ client: PgBossClient,
12
+ names: readonly string[],
13
+ ): Effect.Effect<JobsHealth, import("../error.js").PgBossError> =>
14
+ Effect.forEach(
15
+ names,
16
+ (name) =>
17
+ Effect.tryPromise({
18
+ try: async (): Promise<QueueHealth> => {
19
+ const [queue, deadLetter] = await Promise.all([
20
+ client.getQueue(name),
21
+ client.getQueue(deadLetterQueueName(name)),
22
+ ]);
23
+ return {
24
+ activeCount: queue?.activeCount ?? 0,
25
+ deadLetteredCount: deadLetter?.queuedCount ?? 0,
26
+ failedCount: queue?.failedCount ?? 0,
27
+ name,
28
+ queuedCount: queue?.queuedCount ?? 0,
29
+ readyCount: queue?.readyCount ?? 0,
30
+ };
31
+ },
32
+ catch: (error) => toPgBossError("health", error, name),
33
+ }),
34
+ { concurrency: "unbounded" },
35
+ ).pipe(
36
+ Effect.map((jobs) => ({
37
+ activeTotal: jobs.reduce((sum, job) => sum + job.activeCount, 0),
38
+ deadLetteredTotal: jobs.reduce(
39
+ (sum, job) => sum + job.deadLetteredCount,
40
+ 0,
41
+ ),
42
+ failedTotal: jobs.reduce((sum, job) => sum + job.failedCount, 0),
43
+ jobs,
44
+ queuedTotal: jobs.reduce((sum, job) => sum + job.queuedCount, 0),
45
+ readyTotal: jobs.reduce((sum, job) => sum + job.readyCount, 0),
46
+ })),
47
+ );