@shivaedev/effect-pg-boss 0.7.1 → 0.8.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.
- package/CHANGELOG.md +22 -0
- package/README.md +40 -3
- package/dist/server/definition.d.ts +2 -1
- package/dist/server/definition.d.ts.map +1 -1
- package/dist/server/definition.js +3 -0
- package/dist/server/definition.js.map +1 -1
- package/dist/server/error.d.ts +1 -0
- package/dist/server/error.d.ts.map +1 -1
- package/dist/server/error.js +12 -0
- package/dist/server/error.js.map +1 -1
- package/dist/server/internal/batchHandler.d.ts.map +1 -1
- package/dist/server/internal/batchHandler.js +15 -2
- package/dist/server/internal/batchHandler.js.map +1 -1
- package/dist/server/internal/clientCache.d.ts +7 -0
- package/dist/server/internal/clientCache.d.ts.map +1 -0
- package/dist/server/internal/clientCache.js +51 -0
- package/dist/server/internal/clientCache.js.map +1 -0
- package/dist/server/internal/clientErrors.d.ts +8 -0
- package/dist/server/internal/clientErrors.d.ts.map +1 -0
- package/dist/server/internal/clientErrors.js +49 -0
- package/dist/server/internal/clientErrors.js.map +1 -0
- package/dist/server/internal/deadlineOf.d.ts +3 -0
- package/dist/server/internal/deadlineOf.d.ts.map +1 -0
- package/dist/server/internal/deadlineOf.js +7 -0
- package/dist/server/internal/deadlineOf.js.map +1 -0
- package/dist/server/internal/ensureQueue.d.ts.map +1 -1
- package/dist/server/internal/ensureQueue.js +19 -2
- package/dist/server/internal/ensureQueue.js.map +1 -1
- package/dist/server/internal/lifecycle.d.ts +2 -2
- package/dist/server/internal/lifecycle.d.ts.map +1 -1
- package/dist/server/internal/lifecycle.js +36 -79
- package/dist/server/internal/lifecycle.js.map +1 -1
- package/dist/server/internal/register.d.ts +2 -2
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +10 -5
- package/dist/server/internal/register.js.map +1 -1
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +4 -12
- package/dist/server/service.js.map +1 -1
- package/docs/roadmap.md +6 -5
- package/package.json +6 -5
- package/src/server/definition.ts +4 -1
- package/src/server/error.ts +15 -1
- package/src/server/internal/batchHandler.ts +15 -2
- package/src/server/internal/clientCache.ts +61 -0
- package/src/server/internal/clientErrors.ts +64 -0
- package/src/server/internal/deadlineOf.ts +8 -0
- package/src/server/internal/ensureQueue.ts +20 -2
- package/src/server/internal/lifecycle.ts +37 -97
- package/src/server/internal/register.ts +10 -6
- package/src/server/service.ts +5 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../../src/server/internal/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAA+C,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../../src/server/internal/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAA+C,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAcxE,KAAK,UAAU,WAAW,CAAC,OAA6B,EAAE,MAA8B;IACvF,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,oBAAoB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpF,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACR,2EAA2E;QAC5E,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAA6B;IAC1D,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACxC,SAAS,MAAM,CAAC,KAAY;YAC3B,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5B,CAAC;QACD,OAAO,MAAM,CAAC,UAAU,CAAC;YACxB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC;YAC/C,GAAG,EAAE,KAAK,IAAI,EAAE;gBACf,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC;gBACxC,OAAO,QAAQ,KAAK,SAAS;oBAC5B,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE;oBAC/D,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,mBAAmB,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/F,CAAC;SACD,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAwB,EAAE,WAAyB;IAChF,OAAO,MAAM,CAAC,UAAU,CAAC;QACxB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC;QAC9C,GAAG,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,mBAAmB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAChH,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAChC,CAAC;QACF,CAAC;KACD,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Context, Effect } from "effect";
|
|
2
2
|
import type { PgBossClient } from "#client.ts";
|
|
3
3
|
import { type JobRegistration } from "#definition.ts";
|
|
4
|
-
import { PgBossQueueOptionsError } from "#error.ts";
|
|
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,
|
|
7
|
+
export declare const registerJobs: <R>(client: PgBossClient, registrations: readonly JobRegistration[], context: Context.Context<R>, replaceWorkers: boolean) => Effect.Effect<void, PgBossError | PgBossQueueOptionsError>;
|
|
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,EAAsB,uBAAuB,EAAiB,MAAM,WAAW,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,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,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Redacted, Schema } from "effect";
|
|
2
2
|
import { jobContext, } from "#definition.ts";
|
|
3
|
-
import { PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
3
|
+
import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
4
4
|
import { batchHandler } from "#internal/batchHandler.ts";
|
|
5
5
|
import { ensureQueue } from "#internal/ensureQueue.ts";
|
|
6
6
|
import { payloadOf, traceJob } from "#internal/tracing.ts";
|
|
@@ -36,7 +36,12 @@ async function prepareQueues(client, registrations, replaceWorkers) {
|
|
|
36
36
|
}
|
|
37
37
|
for (const registration of registrations) {
|
|
38
38
|
const definition = registrationDefinition(registration);
|
|
39
|
-
|
|
39
|
+
try {
|
|
40
|
+
await ensureQueue(client, definition.name, definition.queueOptions);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error, definition.name);
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
function workerContext(context) {
|
|
@@ -55,11 +60,11 @@ async function registerQueue(client, registration, context) {
|
|
|
55
60
|
if (registration._tag === "QueueDefinition") {
|
|
56
61
|
return;
|
|
57
62
|
}
|
|
58
|
-
await client.work(name, queue.workerOptions, batchHandler(client, name, (job) => runQueueWorker(registration, job, context)));
|
|
63
|
+
await client.work(name, { ...queue.workerOptions, includeMetadata: true }, batchHandler(client, name, (job) => runQueueWorker(registration, job, context)));
|
|
59
64
|
}
|
|
60
65
|
async function registerSchedule(client, worker, context) {
|
|
61
66
|
const { schedule } = worker;
|
|
62
|
-
await client.work(schedule.name, schedule.workerOptions, batchHandler(client, schedule.name, (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context)));
|
|
67
|
+
await client.work(schedule.name, { ...schedule.workerOptions, includeMetadata: true }, batchHandler(client, schedule.name, (job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context)));
|
|
63
68
|
await client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions);
|
|
64
69
|
}
|
|
65
70
|
export const registrationNames = (registrations) => {
|
|
@@ -70,7 +75,7 @@ export const registrationNames = (registrations) => {
|
|
|
70
75
|
return names;
|
|
71
76
|
};
|
|
72
77
|
export const registerJobs = (client, registrations, context, replaceWorkers) => Effect.tryPromise({
|
|
73
|
-
catch: (error) => (error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error)),
|
|
78
|
+
catch: (error) => (error instanceof PgBossQueueOptionsError || error instanceof PgBossError ? error : toPgBossError("register", error)),
|
|
74
79
|
try: async () => {
|
|
75
80
|
registrationNames(registrations);
|
|
76
81
|
await prepareQueues(client, registrations, replaceWorkers);
|
|
@@ -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,kBAAkB,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/server/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,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAC1F,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"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"}
|
package/dist/server/service.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { logClientError, subscribeClientErrors } from "#internal/clientErrors.ts";
|
|
2
3
|
import { acquireClient, releaseClient } from "#internal/lifecycle.ts";
|
|
3
4
|
import { registerJobs, registrationName } from "#internal/register.ts";
|
|
4
5
|
import { makeService } from "#internal/service.ts";
|
|
5
|
-
function logClientError(error) {
|
|
6
|
-
return Effect.logError({ event: "pg_boss_error", message: error.message });
|
|
7
|
-
}
|
|
8
6
|
export const makePgBoss = (identifier) => {
|
|
9
7
|
const Service = Context.Service(identifier);
|
|
10
8
|
const layer = (options) => {
|
|
@@ -23,16 +21,10 @@ export const makePgBoss = (identifier) => {
|
|
|
23
21
|
const errorListener = (error) => {
|
|
24
22
|
void Effect.runPromise(Effect.exit(Effect.provide(reportError(error), context)));
|
|
25
23
|
};
|
|
26
|
-
acquired.client
|
|
24
|
+
const unsubscribe = subscribeClientErrors(acquired.client, errorListener);
|
|
27
25
|
const registration = registerJobs(acquired.client, jobs, context, acquired.reused);
|
|
28
|
-
return yield* registration.pipe(Effect.as({ acquired,
|
|
29
|
-
|
|
30
|
-
return releaseClient(acquired, stop);
|
|
31
|
-
}));
|
|
32
|
-
}), (resource) => {
|
|
33
|
-
resource.acquired.client.off("error", resource.errorListener);
|
|
34
|
-
return releaseClient(resource.acquired, stop);
|
|
35
|
-
});
|
|
26
|
+
return yield* registration.pipe(Effect.as({ acquired, service, unsubscribe }), Effect.onError(() => releaseClient(acquired, stop).pipe(Effect.ensuring(Effect.sync(unsubscribe)))));
|
|
27
|
+
}), (resource) => releaseClient(resource.acquired, stop).pipe(Effect.ensuring(Effect.sync(resource.unsubscribe))));
|
|
36
28
|
return Layer.effect(Service, Effect.map(acquire, ({ service }) => service));
|
|
37
29
|
};
|
|
38
30
|
return Object.assign(Service, { layer });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAA4B,MAAM,QAAQ,CAAC;AAM1E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"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"}
|
package/docs/roadmap.md
CHANGED
|
@@ -4,10 +4,10 @@ This file owns the package's implementation inventory and open API choices. The
|
|
|
4
4
|
|
|
5
5
|
## Built
|
|
6
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/service.typecheck.test.ts).
|
|
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
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
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.
|
|
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
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
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
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.
|
|
@@ -15,9 +15,10 @@ This file owns the package's implementation inventory and open API choices. The
|
|
|
15
15
|
- [x] Scoped startup and shutdown, including cleanup when startup or registration fails.
|
|
16
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
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
|
|
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
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
|
|
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).
|
|
21
22
|
- [x] Public defining-module imports and an installed-package type fixture.
|
|
22
23
|
|
|
23
24
|
## Next
|
|
@@ -31,4 +32,4 @@ Enqueueing is separate from application SQL transactions. The package supplies n
|
|
|
31
32
|
## Open questions
|
|
32
33
|
|
|
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.
|
|
34
|
-
-
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-pg-boss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": {
|
|
@@ -43,17 +43,18 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@shivaedev/
|
|
47
|
-
"@
|
|
46
|
+
"@shivaedev/effect-test-sql": "0.5.3",
|
|
47
|
+
"@shivaedev/quality": "0.23.0",
|
|
48
|
+
"@types/node": "24.19.2",
|
|
48
49
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
49
50
|
"effect": "4.0.4",
|
|
50
|
-
"pg-boss": "12.37.
|
|
51
|
+
"pg-boss": "12.37.1",
|
|
51
52
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
52
53
|
"vitest": "5.0.3"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"effect": "4.0.4",
|
|
56
|
-
"pg-boss": "12.37.
|
|
57
|
+
"pg-boss": "12.37.1"
|
|
57
58
|
},
|
|
58
59
|
"engines": {
|
|
59
60
|
"node": ">=24"
|
package/src/server/definition.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Clock, Duration, Effect, type Schema } from "effect";
|
|
2
2
|
import type { Job, Queue, ScheduleOptions, WorkOptions } from "pg-boss";
|
|
3
|
+
import { deadlineOf } from "#internal/deadlineOf.ts";
|
|
3
4
|
|
|
4
5
|
export interface JobPayloadSchema extends Schema.Top {
|
|
5
6
|
readonly Encoded: object;
|
|
@@ -11,6 +12,7 @@ export interface JobContext {
|
|
|
11
12
|
readonly heartbeatSeconds: number | null;
|
|
12
13
|
readonly id: string;
|
|
13
14
|
readonly name: string;
|
|
15
|
+
readonly remainingBudget: Effect.Effect<Duration.Duration>;
|
|
14
16
|
readonly signal: AbortSignal;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -116,5 +118,6 @@ export const jobContext = (job: Job<unknown>): JobContext => ({
|
|
|
116
118
|
heartbeatSeconds: job.heartbeatSeconds,
|
|
117
119
|
id: job.id,
|
|
118
120
|
name: job.name,
|
|
121
|
+
remainingBudget: Effect.map(Clock.currentTimeMillis, (now) => Duration.millis(Math.max(0, deadlineOf(job) - now))),
|
|
119
122
|
signal: job.signal,
|
|
120
123
|
});
|
package/src/server/error.ts
CHANGED
|
@@ -6,7 +6,21 @@ export class PgBossError extends Data.TaggedError("PgBossError")<{
|
|
|
6
6
|
readonly operation: PgBossOperation;
|
|
7
7
|
readonly queue?: string | undefined;
|
|
8
8
|
readonly original: Redacted.Redacted<unknown>;
|
|
9
|
-
}> {
|
|
9
|
+
}> {
|
|
10
|
+
// The original's message can quote payloads or connection strings, so only its class and code are shown.
|
|
11
|
+
override get message(): string {
|
|
12
|
+
const target = this.queue === undefined ? "" : ` for queue ${this.queue}`;
|
|
13
|
+
return `pg-boss ${this.operation}${target} failed: ${originalKind(Redacted.value(this.original))}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function originalKind(original: unknown): string {
|
|
18
|
+
if (!(original instanceof Error)) {
|
|
19
|
+
return typeof original;
|
|
20
|
+
}
|
|
21
|
+
const code: unknown = Reflect.get(original, "code");
|
|
22
|
+
return typeof code === "string" ? `${original.constructor.name} ${code}` : original.constructor.name;
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
export class PgBossQueueOptionsError extends Data.TaggedError("PgBossQueueOptionsError")<{
|
|
12
26
|
readonly queue: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Cause, Effect, Exit } from "effect";
|
|
1
|
+
import { Cause, Clock, Effect, Exit } from "effect";
|
|
2
2
|
import type { Job } from "pg-boss";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { deadlineOf } from "#internal/deadlineOf.ts";
|
|
4
5
|
|
|
5
6
|
function failureOutput(cause: Cause.Cause<unknown>): object {
|
|
6
7
|
const error = Cause.squash(cause);
|
|
@@ -14,9 +15,18 @@ export function batchHandler(client: PgBossClient, name: string, run: (job: Job<
|
|
|
14
15
|
throw new TypeError("This pg-boss client does not support complete and fail");
|
|
15
16
|
}
|
|
16
17
|
return async (jobs: readonly Job<unknown>[]): Promise<void> => {
|
|
18
|
+
let expired = false;
|
|
17
19
|
for (const job of jobs) {
|
|
20
|
+
const deadline = deadlineOf(job);
|
|
21
|
+
if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
|
|
22
|
+
expired = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
18
25
|
const exit = await Effect.runPromiseExit(
|
|
19
|
-
Effect.
|
|
26
|
+
Effect.flatMap(Clock.currentTimeMillis, (now) => {
|
|
27
|
+
const remaining = deadline - now;
|
|
28
|
+
return remaining <= 0 ? Effect.interrupt : run(job).pipe(Effect.timeoutOrElse({ duration: remaining, orElse: () => Effect.interrupt }));
|
|
29
|
+
}),
|
|
20
30
|
{ signal: job.signal },
|
|
21
31
|
);
|
|
22
32
|
if (job.signal.aborted || (Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause))) {
|
|
@@ -29,5 +39,8 @@ export function batchHandler(client: PgBossClient, name: string, run: (job: Job<
|
|
|
29
39
|
await fail(name, job, failureOutput(exit.cause));
|
|
30
40
|
}
|
|
31
41
|
}
|
|
42
|
+
if (expired) {
|
|
43
|
+
return new Promise<never>(() => undefined);
|
|
44
|
+
}
|
|
32
45
|
};
|
|
33
46
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PgBossClient } from "#client.ts";
|
|
2
|
+
|
|
3
|
+
interface CachedClient {
|
|
4
|
+
readonly client: Promise<PgBossClient>;
|
|
5
|
+
references: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const cacheSymbol = Symbol.for("@shivaedev/effect-pg-boss/client-cache");
|
|
9
|
+
const shared: unknown = Reflect.get(globalThis, cacheSymbol);
|
|
10
|
+
const clients: Map<string | symbol, unknown> = shared instanceof Map ? shared : new Map<string | symbol, unknown>();
|
|
11
|
+
Reflect.set(globalThis, cacheSymbol, clients);
|
|
12
|
+
|
|
13
|
+
function entryFor(key: string | symbol): CachedClient | undefined {
|
|
14
|
+
const value = clients.get(key);
|
|
15
|
+
return isCachedClient(value) ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isCachedClient(value: unknown): value is CachedClient {
|
|
19
|
+
return (
|
|
20
|
+
typeof value === "object"
|
|
21
|
+
&& value !== null
|
|
22
|
+
&& Reflect.get(value, "client") instanceof Promise
|
|
23
|
+
&& typeof Reflect.get(value, "references") === "number"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function acquireCachedClient(key: string | symbol, start: () => Promise<PgBossClient>) {
|
|
28
|
+
const existing = entryFor(key);
|
|
29
|
+
if (existing !== undefined) {
|
|
30
|
+
existing.references += 1;
|
|
31
|
+
try {
|
|
32
|
+
return { client: await existing.client, reused: true };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
existing.references -= 1;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const entry: CachedClient = { client: start(), references: 1 };
|
|
39
|
+
clients.set(key, entry);
|
|
40
|
+
try {
|
|
41
|
+
return { client: await entry.client, reused: false };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (clients.get(key) === entry) {
|
|
44
|
+
clients.delete(key);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function releaseCachedClient(key: string | symbol): Promise<PgBossClient> | undefined {
|
|
51
|
+
const entry = entryFor(key);
|
|
52
|
+
if (entry === undefined) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
entry.references -= 1;
|
|
56
|
+
if (entry.references > 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
clients.delete(key);
|
|
60
|
+
return entry.client;
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
|
|
4
|
+
type Listener = (error: Error) => void;
|
|
5
|
+
|
|
6
|
+
interface Observer {
|
|
7
|
+
readonly dispose: () => void;
|
|
8
|
+
readonly listeners: Set<Listener>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const key = Symbol.for("@shivaedev/effect-pg-boss/error-observers");
|
|
12
|
+
const shared: unknown = Reflect.get(globalThis, key);
|
|
13
|
+
const observers: WeakMap<WeakKey, unknown> = shared instanceof WeakMap ? shared : new WeakMap<WeakKey, unknown>();
|
|
14
|
+
Reflect.set(globalThis, key, observers);
|
|
15
|
+
|
|
16
|
+
export function logClientError(error: Error): Effect.Effect<void> {
|
|
17
|
+
const message = { event: "pg_boss_error", message: error.message };
|
|
18
|
+
return Reflect.get(error, "code") === "57P01" ? Effect.logDebug(message) : Effect.logError(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function observeClientErrors(client: PgBossClient, fallback: Listener): void {
|
|
22
|
+
closeClientErrors(client);
|
|
23
|
+
const listeners = new Set<Listener>();
|
|
24
|
+
const listener: Listener = (error) => {
|
|
25
|
+
if (listeners.size === 0) {
|
|
26
|
+
fallback(error);
|
|
27
|
+
} else {
|
|
28
|
+
for (const report of listeners) {
|
|
29
|
+
report(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// Pool shutdown can resolve before its clients finish disconnecting.
|
|
34
|
+
client.on("error", listener);
|
|
35
|
+
observers.set(client, { dispose: () => client.off("error", listener), listeners });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function subscribeClientErrors(client: PgBossClient, listener: Listener): () => void {
|
|
39
|
+
const observer = observerFor(client);
|
|
40
|
+
if (observer === undefined) {
|
|
41
|
+
throw new Error("pg-boss client has no error observer");
|
|
42
|
+
}
|
|
43
|
+
observer.listeners.add(listener);
|
|
44
|
+
return () => observer.listeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function closeClientErrors(client: PgBossClient): void {
|
|
48
|
+
observerFor(client)?.dispose();
|
|
49
|
+
observers.delete(client);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function observerFor(client: PgBossClient): Observer | undefined {
|
|
53
|
+
const value = observers.get(client);
|
|
54
|
+
return isObserver(value) ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isObserver(value: unknown): value is Observer {
|
|
58
|
+
return (
|
|
59
|
+
typeof value === "object"
|
|
60
|
+
&& value !== null
|
|
61
|
+
&& typeof Reflect.get(value, "dispose") === "function"
|
|
62
|
+
&& Reflect.get(value, "listeners") instanceof Set
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Job } from "pg-boss";
|
|
2
|
+
|
|
3
|
+
export function deadlineOf(job: Job<unknown>): number {
|
|
4
|
+
if (!("startedOn" in job && job.startedOn instanceof Date && Number.isFinite(job.startedOn.getTime()))) {
|
|
5
|
+
throw new TypeError("Worker jobs must include pg-boss startedOn metadata");
|
|
6
|
+
}
|
|
7
|
+
return job.startedOn.getTime() + job.expireInSeconds * 1000;
|
|
8
|
+
}
|
|
@@ -3,11 +3,29 @@ import type { PgBossClient } from "#client.ts";
|
|
|
3
3
|
import { PgBossQueueOptionsError } from "#error.ts";
|
|
4
4
|
import { deadLetterQueueName } from "#health.ts";
|
|
5
5
|
|
|
6
|
+
const DEADLOCK_DETECTED = "40P01";
|
|
7
|
+
const CREATE_QUEUE_ATTEMPTS = 6;
|
|
8
|
+
|
|
9
|
+
// A partitioned queue attaches its table to pg-boss's job table, which can deadlock with job traffic on other queues.
|
|
10
|
+
// PostgreSQL rolls the whole creation back, so trying it again is safe.
|
|
11
|
+
async function createQueue(client: PgBossClient, name: string, options?: Omit<Queue, "name">): Promise<void> {
|
|
12
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
13
|
+
try {
|
|
14
|
+
return await client.createQueue(name, options);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (attempt === CREATE_QUEUE_ATTEMPTS || !(error instanceof Error) || Reflect.get(error, "code") !== DEADLOCK_DETECTED) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
export async function ensureQueue(client: PgBossClient, name: string, options: Omit<Queue, "name" | "deadLetter">): Promise<void> {
|
|
7
25
|
const deadLetter = deadLetterQueueName(name);
|
|
8
26
|
const desired = { retryBackoff: true, retryLimit: 3, ...options, deadLetter };
|
|
9
|
-
await
|
|
10
|
-
await
|
|
27
|
+
await createQueue(client, deadLetter);
|
|
28
|
+
await createQueue(client, name, desired);
|
|
11
29
|
const existing = await client.getQueue(name);
|
|
12
30
|
if (existing === null) {
|
|
13
31
|
return;
|