@shivaedev/effect-pg-boss 0.10.1 → 0.12.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 +19 -0
- package/README.md +51 -4
- package/dist/server/definition.d.ts +59 -25
- package/dist/server/definition.d.ts.map +1 -1
- package/dist/server/definition.js +25 -27
- package/dist/server/definition.js.map +1 -1
- package/dist/server/internal/batchHandler.d.ts.map +1 -1
- package/dist/server/internal/batchHandler.js +13 -7
- package/dist/server/internal/batchHandler.js.map +1 -1
- package/dist/server/internal/deferrableAttempt.d.ts +7 -0
- package/dist/server/internal/deferrableAttempt.d.ts.map +1 -0
- package/dist/server/internal/deferrableAttempt.js +28 -0
- package/dist/server/internal/deferrableAttempt.js.map +1 -0
- package/dist/server/internal/deferral.d.ts +15 -0
- package/dist/server/internal/deferral.d.ts.map +1 -0
- package/dist/server/internal/deferral.js +14 -0
- package/dist/server/internal/deferral.js.map +1 -0
- package/dist/server/internal/deferralStorePlan.d.ts +2 -0
- package/dist/server/internal/deferralStorePlan.d.ts.map +1 -0
- package/dist/server/internal/deferralStorePlan.js +34 -0
- package/dist/server/internal/deferralStorePlan.js.map +1 -0
- package/dist/server/internal/definitionOptions.d.ts +17 -0
- package/dist/server/internal/definitionOptions.d.ts.map +1 -0
- package/dist/server/internal/definitionOptions.js +12 -0
- package/dist/server/internal/definitionOptions.js.map +1 -0
- package/dist/server/internal/expireAttempt.d.ts +1 -1
- package/dist/server/internal/expireAttempt.d.ts.map +1 -1
- package/dist/server/internal/expireAttempt.js +4 -2
- package/dist/server/internal/expireAttempt.js.map +1 -1
- package/dist/server/internal/lifecycle.d.ts.map +1 -1
- package/dist/server/internal/lifecycle.js +4 -0
- package/dist/server/internal/lifecycle.js.map +1 -1
- package/dist/server/internal/register.d.ts +1 -1
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +10 -10
- package/dist/server/internal/register.js.map +1 -1
- package/dist/server/internal/registerWorker.d.ts +2 -2
- package/dist/server/internal/registerWorker.d.ts.map +1 -1
- package/dist/server/internal/registerWorker.js +7 -2
- package/dist/server/internal/registerWorker.js.map +1 -1
- package/dist/server/internal/scheduleDefinition.d.ts +3 -0
- package/dist/server/internal/scheduleDefinition.d.ts.map +1 -0
- package/dist/server/internal/scheduleDefinition.js +19 -0
- package/dist/server/internal/scheduleDefinition.js.map +1 -0
- package/dist/server/internal/service.js +1 -1
- package/dist/server/internal/service.js.map +1 -1
- package/dist/server/internal/settleDeferral.d.ts +7 -0
- package/dist/server/internal/settleDeferral.d.ts.map +1 -0
- package/dist/server/internal/settleDeferral.js +47 -0
- package/dist/server/internal/settleDeferral.js.map +1 -0
- package/dist/server/internal/testingClient.d.ts.map +1 -1
- package/dist/server/internal/testingClient.js +4 -1
- package/dist/server/internal/testingClient.js.map +1 -1
- package/dist/server/internal/testingDeferrals.d.ts +11 -0
- package/dist/server/internal/testingDeferrals.d.ts.map +1 -0
- package/dist/server/internal/testingDeferrals.js +28 -0
- package/dist/server/internal/testingDeferrals.js.map +1 -0
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +8 -1
- package/dist/server/schema.js.map +1 -1
- package/dist/server/service.d.ts +5 -3
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +1 -1
- package/dist/server/service.js.map +1 -1
- package/dist/server/testing.d.ts +8 -3
- package/dist/server/testing.d.ts.map +1 -1
- package/dist/server/testing.js +4 -0
- package/dist/server/testing.js.map +1 -1
- package/package.json +5 -5
- package/src/server/definition.ts +75 -58
- package/src/server/internal/batchHandler.ts +21 -6
- package/src/server/internal/deferrableAttempt.ts +41 -0
- package/src/server/internal/deferral.ts +19 -0
- package/src/server/internal/deferralStorePlan.ts +34 -0
- package/src/server/internal/definitionOptions.ts +14 -0
- package/src/server/internal/expireAttempt.ts +4 -2
- package/src/server/internal/lifecycle.ts +4 -0
- package/src/server/internal/register.ts +15 -15
- package/src/server/internal/registerWorker.ts +39 -26
- package/src/server/internal/scheduleDefinition.ts +21 -0
- package/src/server/internal/service.ts +1 -1
- package/src/server/internal/settleDeferral.ts +53 -0
- package/src/server/internal/testingClient.ts +4 -1
- package/src/server/internal/testingDeferrals.ts +43 -0
- package/src/server/schema.ts +8 -1
- package/src/server/service.ts +7 -7
- package/src/server/testing.ts +7 -3
package/dist/server/testing.js
CHANGED
|
@@ -2,11 +2,13 @@ import { Effect, Redacted } from "effect";
|
|
|
2
2
|
import { PgBoss } from "pg-boss";
|
|
3
3
|
import { PgBossTestingError } from "#error.ts";
|
|
4
4
|
import { testingClient } from "#internal/testingClient.ts";
|
|
5
|
+
import { testingDeferrals } from "#internal/testingDeferrals.ts";
|
|
5
6
|
import { testingOperation, testingReadout } from "#internal/testingReadout.ts";
|
|
6
7
|
import { testingSignal } from "#internal/testingSignal.ts";
|
|
7
8
|
export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
|
|
8
9
|
export function makePgBossTesting(options = {}) {
|
|
9
10
|
let current;
|
|
11
|
+
let schema = "pgboss";
|
|
10
12
|
const signal = testingSignal();
|
|
11
13
|
const controls = {
|
|
12
14
|
changed: signal.changed,
|
|
@@ -24,10 +26,12 @@ export function makePgBossTesting(options = {}) {
|
|
|
24
26
|
if (current !== undefined) {
|
|
25
27
|
throw new Error("A testing controller owns one jobs client; create a controller per runtime");
|
|
26
28
|
}
|
|
29
|
+
schema = clientOptions.schema ?? "pgboss";
|
|
27
30
|
current = new PgBoss({ ...clientOptions, "__test__enableSpies": true });
|
|
28
31
|
return testingClient(current, clientOptions.schema ?? "pgboss", controls);
|
|
29
32
|
};
|
|
30
33
|
return {
|
|
34
|
+
...testingDeferrals(client, () => schema, signal.changed),
|
|
31
35
|
checkBatchWindow: (queue, millisecondsPerJob) => testingOperation("checkBatchWindow", async () => {
|
|
32
36
|
const stored = await client().getQueue(queue.name);
|
|
33
37
|
if (stored === null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;AAMrF,MAAM,UAAU,iBAAiB,CAAC,OAAO,GAAyB,EAAE;IACnE,IAAI,OAA2B,CAAC;IAChC,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG;QAChB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,WAAW,EAAE,IAAI,GAAG,EAAkB;QACtC,QAAQ,EAAE,IAAI,GAAG,EAAU;QAC3B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;KACtC,CAAC;IACF,SAAS,MAAM;QACd,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC;IACD,MAAM,aAAa,GAAwB,CAAC,aAAa,EAAE,EAAE;QAC5D,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAC/F,CAAC;QACD,OAAO,GAAG,IAAI,MAAM,CAAC,EAAE,GAAG,aAAa,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,OAAO,aAAa,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,IAAI,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3E,CAAC,CAAC;IACF,OAAO;QACN,gBAAgB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;AAMrF,MAAM,UAAU,iBAAiB,CAAC,OAAO,GAAyB,EAAE;IACnE,IAAI,OAA2B,CAAC;IAChC,IAAI,MAAM,GAAG,QAAQ,CAAC;IACtB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG;QAChB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,WAAW,EAAE,IAAI,GAAG,EAAkB;QACtC,QAAQ,EAAE,IAAI,GAAG,EAAU;QAC3B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;KACtC,CAAC;IACF,SAAS,MAAM;QACd,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC;IACD,MAAM,aAAa,GAAwB,CAAC,aAAa,EAAE,EAAE;QAC5D,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAC;QAC/F,CAAC;QACD,MAAM,GAAG,aAAa,CAAC,MAAM,IAAI,QAAQ,CAAC;QAC1C,OAAO,GAAG,IAAI,MAAM,CAAC,EAAE,GAAG,aAAa,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,OAAO,aAAa,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,IAAI,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3E,CAAC,CAAC;IACF,OAAO;QACN,GAAG,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC;QACzD,gBAAgB,EAAE,CAAC,KAAsD,EAAE,kBAA0B,EAAE,EAAE,CACxG,gBAAgB,CACf,kBAAkB,EAClB,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,GAAG,MAAM,MAAM,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACnD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACrB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;YAC5C,CAAC;YACD,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;YAC/C,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,kBAAkB,GAAG,CAAC,EAAE,CAAC;gBACpE,MAAM,IAAI,UAAU,CAAC,6CAA6C,CAAC,CAAC;YACrE,CAAC;YACD,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,SAAS,IAAI,EAAE,CAAC,GAAG,kBAAkB,CAAC;YAC5E,IAAI,QAAQ,IAAI,MAAM,CAAC,eAAe,GAAG,IAAI,EAAE,CAAC;gBAC/C,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;YAClF,CAAC;QACF,CAAC,EACD,KAAK,CAAC,IAAI,CACV;QACF,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE,CACxC,gBAAgB,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;YACjD,IAAI,CAAC,MAAM,MAAM,EAAE,CAAC,aAAa,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACnD,MAAM,IAAI,KAAK,CAAC,mCAAmC,QAAQ,EAAE,CAAC,CAAC;YAChE,CAAC;QACF,CAAC,CAAC;QACH,aAAa;QACb,WAAW,EAAE,CAAC,KAAoC,EAAE,QAAgB,EAAE,EAAE,CACvE,MAAM,CAAC,GAAG,CAAC;YACV,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;YAC/H,GAAG,EAAE,GAAG,EAAE;gBACT,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;oBACrD,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAC;gBAC7D,CAAC;gBACD,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAChD,CAAC;SACD,CAAC;QACH,YAAY,EAAE,CAAC,KAAoC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QAChI,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC;QAC5B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,gBAAgB,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,aAAa,EAAE,CAAC;QAChF,aAAa,EAAE,MAAM,CAAC,aAAa;KACnC,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-pg-boss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@shivaedev/effect-test": "0.
|
|
45
|
-
"@shivaedev/effect-test-sql": "0.6.
|
|
46
|
-
"@shivaedev/quality": "0.
|
|
47
|
-
"@shivaedev/test-story": "0.
|
|
44
|
+
"@shivaedev/effect-test": "0.7.1",
|
|
45
|
+
"@shivaedev/effect-test-sql": "0.6.1",
|
|
46
|
+
"@shivaedev/quality": "0.28.0",
|
|
47
|
+
"@shivaedev/test-story": "0.12.0",
|
|
48
48
|
"@types/node": "24.19.2",
|
|
49
49
|
"effect": "4.0.4",
|
|
50
50
|
"pg-boss": "12.37.1",
|
package/src/server/definition.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import { Clock, Duration, Effect,
|
|
2
|
-
import type { Job, Queue, ScheduleOptions, WorkOptions } from "pg-boss";
|
|
1
|
+
import { Clock, Duration, Effect, Schema } from "effect";
|
|
2
|
+
import type { Job, policies, Queue, ScheduleOptions, WorkOptions } from "pg-boss";
|
|
3
3
|
import { deadlineOf } from "#internal/deadlineOf.ts";
|
|
4
|
+
import { maxDeferrals, nativeQueueOptions } from "#internal/definitionOptions.ts";
|
|
5
|
+
import { scheduleDefinition } from "#internal/scheduleDefinition.ts";
|
|
6
|
+
|
|
7
|
+
export class RetryAt extends Schema.TaggedError<RetryAt>()("RetryAt", {
|
|
8
|
+
at: Schema.DateTimeUtc,
|
|
9
|
+
reason: Schema.String,
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export type QueuePolicy = (typeof policies)[keyof typeof policies];
|
|
13
|
+
export type QueueOptions = Omit<Queue, "name" | "deadLetter" | "policy"> & {
|
|
14
|
+
readonly maxDeferrals?: number;
|
|
15
|
+
readonly policy?: QueuePolicy;
|
|
16
|
+
};
|
|
4
17
|
|
|
5
18
|
export interface JobPayloadSchema extends Schema.Top {
|
|
6
19
|
readonly Encoded: object;
|
|
@@ -16,108 +29,112 @@ export interface JobContext {
|
|
|
16
29
|
readonly signal: AbortSignal;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
|
-
export interface QueueDefinition<
|
|
32
|
+
export interface QueueDefinition<TName extends string = string, TPayload extends JobPayloadSchema = JobPayloadSchema> {
|
|
20
33
|
readonly _tag: "QueueDefinition";
|
|
21
34
|
readonly handle: <E, R>(
|
|
22
|
-
handler: (payload: Schema.Schema.Type<
|
|
23
|
-
) => QueueWorker<
|
|
24
|
-
readonly
|
|
35
|
+
handler: (payload: Schema.Schema.Type<TPayload>, job: JobContext) => Effect.Effect<unknown, E, R>,
|
|
36
|
+
) => QueueWorker<TPayload, R>;
|
|
37
|
+
readonly key?: ((payload: Schema.Schema.Type<TPayload>) => string) | undefined;
|
|
38
|
+
readonly maxDeferrals: number;
|
|
39
|
+
readonly name: TName;
|
|
25
40
|
readonly queueOptions: Omit<Queue, "name" | "deadLetter">;
|
|
26
|
-
readonly schema:
|
|
41
|
+
readonly schema: TPayload;
|
|
27
42
|
readonly workerOptions: WorkOptions;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
export interface QueueWorker<
|
|
45
|
+
export interface QueueWorker<TPayload extends JobPayloadSchema = JobPayloadSchema, TRequirements = unknown> {
|
|
31
46
|
readonly _tag: "QueueWorker";
|
|
32
|
-
readonly handler: (payload: Schema.Schema.Type<
|
|
33
|
-
readonly queue: QueueDefinition<string,
|
|
47
|
+
readonly handler: (payload: Schema.Schema.Type<TPayload>, job: JobContext) => Effect.Effect<unknown, unknown, TRequirements>;
|
|
48
|
+
readonly queue: QueueDefinition<string, TPayload>;
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
export interface JobRegistration {
|
|
37
52
|
readonly _tag: "QueueDefinition" | "QueueWorker" | "ScheduledWorker";
|
|
38
53
|
}
|
|
39
54
|
|
|
40
|
-
export interface ScheduleDefinition<
|
|
55
|
+
export interface ScheduleDefinition<TName extends string = string> {
|
|
41
56
|
readonly _tag: "ScheduleDefinition";
|
|
42
57
|
readonly cron: string;
|
|
43
|
-
readonly
|
|
58
|
+
readonly maxDeferrals: number;
|
|
59
|
+
readonly name: TName;
|
|
44
60
|
readonly queueOptions: Omit<Queue, "name" | "deadLetter">;
|
|
45
61
|
readonly run: <E, R>(effect: Effect.Effect<unknown, E, R>) => ScheduledWorker<R>;
|
|
46
62
|
readonly scheduleOptions: ScheduleOptions;
|
|
47
63
|
readonly workerOptions: WorkOptions;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
export interface ScheduledWorker<
|
|
66
|
+
export interface ScheduledWorker<TRequirements = unknown> {
|
|
51
67
|
readonly _tag: "ScheduledWorker";
|
|
52
|
-
readonly effect: Effect.Effect<unknown, unknown,
|
|
68
|
+
readonly effect: Effect.Effect<unknown, unknown, TRequirements>;
|
|
53
69
|
readonly schedule: ScheduleDefinition;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
export type RegistrationRequirements<
|
|
57
|
-
?
|
|
58
|
-
?
|
|
59
|
-
:
|
|
60
|
-
?
|
|
72
|
+
export type RegistrationRequirements<TRegistrations extends readonly JobRegistration[]> = TRegistrations[number] extends infer TRegistration
|
|
73
|
+
? TRegistration extends QueueWorker<infer TPayload, infer TRequirements>
|
|
74
|
+
? TRequirements | TPayload["DecodingServices"]
|
|
75
|
+
: TRegistration extends ScheduledWorker<infer TRequirements>
|
|
76
|
+
? TRequirements
|
|
61
77
|
: never
|
|
62
78
|
: never;
|
|
63
79
|
|
|
64
|
-
export
|
|
65
|
-
readonly name:
|
|
66
|
-
readonly
|
|
67
|
-
readonly schema: Payload;
|
|
80
|
+
export type DefineQueueOptions<TName extends string, TPayload extends JobPayloadSchema> = {
|
|
81
|
+
readonly name: TName;
|
|
82
|
+
readonly schema: TPayload;
|
|
68
83
|
readonly worker?: WorkOptions;
|
|
69
|
-
}
|
|
84
|
+
} & (
|
|
85
|
+
| { readonly key: (payload: Schema.Schema.Type<TPayload>) => string; readonly queue: QueueOptions & { readonly policy: "singleton" } }
|
|
86
|
+
| { readonly key?: never; readonly queue?: QueueOptions & { readonly policy?: Exclude<QueuePolicy, "singleton"> } }
|
|
87
|
+
);
|
|
70
88
|
|
|
71
|
-
export
|
|
72
|
-
options: DefineQueueOptions<
|
|
73
|
-
): QueueDefinition<
|
|
74
|
-
|
|
89
|
+
export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
|
|
90
|
+
options: DefineQueueOptions<TName, TPayload> & { readonly key: (payload: Schema.Schema.Type<TPayload>) => string },
|
|
91
|
+
): QueueDefinition<TName, TPayload> & { readonly key: (payload: Schema.Schema.Type<TPayload>) => string };
|
|
92
|
+
export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
|
|
93
|
+
options: DefineQueueOptions<TName, TPayload> & { readonly key?: never },
|
|
94
|
+
): QueueDefinition<TName, TPayload> & { readonly key?: never };
|
|
95
|
+
export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
|
|
96
|
+
options: DefineQueueOptions<TName, TPayload>,
|
|
97
|
+
): QueueDefinition<TName, TPayload>;
|
|
98
|
+
export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
|
|
99
|
+
options: DefineQueueOptions<TName, TPayload>,
|
|
100
|
+
): QueueDefinition<TName, TPayload> {
|
|
101
|
+
const definition: QueueDefinition<TName, TPayload> = {
|
|
75
102
|
_tag: "QueueDefinition",
|
|
103
|
+
...(options.key === undefined ? {} : { key: options.key }),
|
|
76
104
|
handle: (handler) =>
|
|
77
105
|
Object.freeze({
|
|
78
106
|
_tag: "QueueWorker" as const,
|
|
79
107
|
handler,
|
|
80
108
|
queue: definition,
|
|
81
109
|
}),
|
|
110
|
+
maxDeferrals: maxDeferrals(options.queue),
|
|
82
111
|
name: options.name,
|
|
83
|
-
queueOptions:
|
|
112
|
+
queueOptions: nativeQueueOptions(options.queue),
|
|
84
113
|
schema: options.schema,
|
|
85
114
|
workerOptions: Object.freeze({ batchSize: 10, burstWhenBatchFull: true, ...options.worker }),
|
|
86
115
|
};
|
|
87
116
|
return Object.freeze(definition);
|
|
88
|
-
}
|
|
117
|
+
}
|
|
89
118
|
|
|
90
|
-
export interface DefineScheduleOptions<
|
|
119
|
+
export interface DefineScheduleOptions<TName extends string> {
|
|
91
120
|
readonly cron: string;
|
|
92
|
-
readonly name:
|
|
93
|
-
readonly queue?:
|
|
121
|
+
readonly name: TName;
|
|
122
|
+
readonly queue?: QueueOptions;
|
|
94
123
|
readonly schedule?: ScheduleOptions;
|
|
95
124
|
readonly worker?: WorkOptions;
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cron: options.cron,
|
|
102
|
-
name: options.name,
|
|
103
|
-
queueOptions: Object.freeze({ ...options.queue }),
|
|
104
|
-
run: (effect) =>
|
|
105
|
-
Object.freeze({
|
|
106
|
-
_tag: "ScheduledWorker" as const,
|
|
107
|
-
effect,
|
|
108
|
-
schedule: definition,
|
|
109
|
-
}),
|
|
110
|
-
scheduleOptions: Object.freeze({ tz: "UTC", ...options.schedule }),
|
|
111
|
-
workerOptions: Object.freeze({ batchSize: 10, burstWhenBatchFull: true, ...options.worker }),
|
|
112
|
-
};
|
|
113
|
-
return Object.freeze(definition);
|
|
114
|
-
};
|
|
127
|
+
export function defineSchedule<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
|
|
128
|
+
return scheduleDefinition(options);
|
|
129
|
+
}
|
|
115
130
|
|
|
116
|
-
export
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
export function jobContext(job: Job<unknown>): JobContext {
|
|
132
|
+
return {
|
|
133
|
+
expireInSeconds: job.expireInSeconds,
|
|
134
|
+
heartbeatSeconds: job.heartbeatSeconds,
|
|
135
|
+
id: job.id,
|
|
136
|
+
name: job.name,
|
|
137
|
+
remainingBudget: Effect.map(Clock.currentTimeMillis, (now) => Duration.millis(Math.max(0, deadlineOf(job) - now))),
|
|
138
|
+
signal: job.signal,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -2,12 +2,31 @@ import { Cause, Clock, Effect, Exit } from "effect";
|
|
|
2
2
|
import type { Job } from "pg-boss";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
4
|
import { deadlineOf } from "#internal/deadlineOf.ts";
|
|
5
|
+
import { deferredAttempt } from "#internal/deferral.ts";
|
|
5
6
|
|
|
6
7
|
function failureOutput(cause: Cause.Cause<unknown>): object {
|
|
7
8
|
const error = Cause.squash(cause);
|
|
8
9
|
return typeof error === "object" && error !== null ? error : { message: String(error) };
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
async function settleAttempt(
|
|
13
|
+
complete: NonNullable<PgBossClient["complete"]>,
|
|
14
|
+
fail: NonNullable<PgBossClient["fail"]>,
|
|
15
|
+
name: string,
|
|
16
|
+
job: Job<unknown>,
|
|
17
|
+
exit: Exit.Exit<unknown, unknown>,
|
|
18
|
+
afterFailure?: (job: Job<unknown>) => Promise<void>,
|
|
19
|
+
) {
|
|
20
|
+
if (Exit.isSuccess(exit)) {
|
|
21
|
+
if (exit.value !== deferredAttempt) {
|
|
22
|
+
await complete(name, job);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
await fail(name, job, failureOutput(exit.cause));
|
|
26
|
+
await afterFailure?.(job);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
export function batchHandler(
|
|
12
31
|
client: PgBossClient,
|
|
13
32
|
name: string,
|
|
@@ -19,6 +38,7 @@ export function batchHandler(
|
|
|
19
38
|
if (complete === undefined || fail === undefined) {
|
|
20
39
|
throw new TypeError("This pg-boss client does not support complete and fail");
|
|
21
40
|
}
|
|
41
|
+
|
|
22
42
|
return async (jobs: readonly Job<unknown>[]): Promise<void> => {
|
|
23
43
|
let expired = false;
|
|
24
44
|
for (const job of jobs) {
|
|
@@ -38,12 +58,7 @@ export function batchHandler(
|
|
|
38
58
|
// Returning would let pg-boss complete the unprocessed jobs; its expiry owns them instead.
|
|
39
59
|
return new Promise<never>(() => undefined);
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
await complete(name, job);
|
|
43
|
-
} else {
|
|
44
|
-
await fail(name, job, failureOutput(exit.cause));
|
|
45
|
-
await afterFailure?.(job);
|
|
46
|
-
}
|
|
61
|
+
await settleAttempt(complete, fail, name, job, exit, afterFailure);
|
|
47
62
|
}
|
|
48
63
|
if (expired) {
|
|
49
64
|
return new Promise<never>(() => undefined);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Clock, DateTime, Effect, Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { RetryAt } from "#definition.ts";
|
|
5
|
+
import { deferralObservers, deferredAttempt } from "#internal/deferral.ts";
|
|
6
|
+
import { settleDeferral } from "#internal/settleDeferral.ts";
|
|
7
|
+
|
|
8
|
+
const recordDeferral = Effect.fnUntraced(function* (at: string, reason: string) {
|
|
9
|
+
const span = yield* Effect.currentSpan.pipe(Effect.orDie);
|
|
10
|
+
span.event("job.deferred", yield* Clock.currentTimeNanos, { at, reason });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function deferrableAttempt<A, E, R>(
|
|
14
|
+
effect: Effect.Effect<A, E, R>,
|
|
15
|
+
client: PgBossClient,
|
|
16
|
+
schema: string,
|
|
17
|
+
job: Job<unknown>,
|
|
18
|
+
maxDeferrals: number,
|
|
19
|
+
) {
|
|
20
|
+
return effect.pipe(
|
|
21
|
+
Effect.catchIf(Schema.is(RetryAt), (error) => {
|
|
22
|
+
if (maxDeferrals === 0) {
|
|
23
|
+
return Effect.fail(error);
|
|
24
|
+
}
|
|
25
|
+
const at = DateTime.formatIso(error.at);
|
|
26
|
+
return Effect.promise(() => settleDeferral(client, schema, job, at, error.reason, maxDeferrals)).pipe(
|
|
27
|
+
Effect.flatMap((settled) => {
|
|
28
|
+
if (settled.claimed && !settled.deferred) {
|
|
29
|
+
return Effect.fail(error);
|
|
30
|
+
}
|
|
31
|
+
// Native batch settlement must not match a sibling's new claim at the unchanged retry count.
|
|
32
|
+
job.retryCount = Number.MAX_SAFE_INTEGER;
|
|
33
|
+
if (settled.deferred) {
|
|
34
|
+
deferralObservers.get(client)?.();
|
|
35
|
+
}
|
|
36
|
+
return (settled.deferred ? recordDeferral(at, error.reason) : Effect.void).pipe(Effect.as(deferredAttempt));
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
|
|
5
|
+
export const deferredAttempt = Symbol("deferredAttempt");
|
|
6
|
+
export const deferralObservers = new WeakMap<PgBossClient, () => void>();
|
|
7
|
+
export const STATELY_DEFERRAL_POLICY = "effect-stately-deferred";
|
|
8
|
+
|
|
9
|
+
export const Deferral = Schema.Struct({
|
|
10
|
+
at: Schema.String,
|
|
11
|
+
reason: Schema.String,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const Output = Schema.Struct({ jobDeferral: Deferral });
|
|
15
|
+
|
|
16
|
+
export function deferralOf(job: Job<unknown>) {
|
|
17
|
+
const output: unknown = Reflect.get(job, "output");
|
|
18
|
+
return Schema.is(Output)(output) ? output.jobDeferral : undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { STATELY_DEFERRAL_POLICY } from "./deferral.ts";
|
|
2
|
+
|
|
3
|
+
export function deferralStorePlan(schema: string): string {
|
|
4
|
+
const quoted = `"${schema.replaceAll('"', '""')}"`;
|
|
5
|
+
const jobTable = `${quoted}.job`.replaceAll("'", "''");
|
|
6
|
+
const key = `${schema}.effect_job_deferral`.replaceAll("'", "''");
|
|
7
|
+
const body = `BEGIN
|
|
8
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('${key}', 0));
|
|
9
|
+
CREATE TABLE IF NOT EXISTS ${quoted}.effect_job_deferral (
|
|
10
|
+
name text NOT NULL,
|
|
11
|
+
id uuid NOT NULL,
|
|
12
|
+
created_on timestamptz NOT NULL,
|
|
13
|
+
count bigint NOT NULL,
|
|
14
|
+
keep_until timestamptz NOT NULL,
|
|
15
|
+
PRIMARY KEY (name, id)
|
|
16
|
+
);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS effect_job_deferral_keep_until ON ${quoted}.effect_job_deferral (keep_until);
|
|
18
|
+
CREATE OR REPLACE FUNCTION ${quoted}.effect_stately_deferral() RETURNS trigger LANGUAGE plpgsql AS $claim$
|
|
19
|
+
BEGIN
|
|
20
|
+
IF NEW.policy = '${STATELY_DEFERRAL_POLICY}' AND NEW.state = 'active' THEN
|
|
21
|
+
NEW.policy := 'stately';
|
|
22
|
+
END IF;
|
|
23
|
+
RETURN NEW;
|
|
24
|
+
END
|
|
25
|
+
$claim$;
|
|
26
|
+
IF NOT EXISTS (
|
|
27
|
+
SELECT 1 FROM pg_trigger WHERE tgrelid = '${jobTable}'::regclass AND tgname = 'effect_stately_deferral'
|
|
28
|
+
) THEN
|
|
29
|
+
CREATE TRIGGER effect_stately_deferral BEFORE UPDATE OF state, policy ON ${quoted}.job
|
|
30
|
+
FOR EACH ROW EXECUTE FUNCTION ${quoted}.effect_stately_deferral();
|
|
31
|
+
END IF;
|
|
32
|
+
END`;
|
|
33
|
+
return `DO '${body.replaceAll("'", "''")}'`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { QueueOptions } from "#definition.ts";
|
|
2
|
+
|
|
3
|
+
export function maxDeferrals(options: QueueOptions | undefined): number {
|
|
4
|
+
const limit = options?.maxDeferrals ?? 10;
|
|
5
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
6
|
+
throw new RangeError("maxDeferrals must be a nonnegative safe integer");
|
|
7
|
+
}
|
|
8
|
+
return limit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function nativeQueueOptions(options: QueueOptions | undefined) {
|
|
12
|
+
const { maxDeferrals: _limit, ...native } = options ?? {};
|
|
13
|
+
return Object.freeze(native);
|
|
14
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Job, PgBoss } from "pg-boss";
|
|
2
2
|
|
|
3
|
-
export async function expireAttempt(client: PgBoss, schema: string, job: Job<unknown
|
|
3
|
+
export async function expireAttempt(client: PgBoss, schema: string, job: Job<unknown>, cause: "expiry" | "heartbeat" = "expiry"): Promise<void> {
|
|
4
4
|
if ((await client.schemaVersion()) !== 45) {
|
|
5
5
|
throw new Error("Testing expiry requires pg-boss schema version 45");
|
|
6
6
|
}
|
|
7
7
|
const quoted = `"${schema.replaceAll('"', '""')}"`;
|
|
8
|
+
const timestamp = cause === "expiry" ? "started_on" : "heartbeat_on";
|
|
9
|
+
const seconds = cause === "expiry" ? "expire_seconds" : "heartbeat_seconds";
|
|
8
10
|
await client.getDb().executeSql(
|
|
9
|
-
`UPDATE ${quoted}.job SET
|
|
11
|
+
`UPDATE ${quoted}.job SET ${timestamp} = ${quoted}.job_now() - (${seconds} + 1) * interval '1 second'
|
|
10
12
|
WHERE name = $1 AND id = $2 AND retry_count = $3 AND state = 'active'`,
|
|
11
13
|
[job.name, job.id, job.retryCount],
|
|
12
14
|
);
|
|
@@ -2,6 +2,7 @@ import { Effect } from "effect";
|
|
|
2
2
|
import type { ConstructorOptions, StopOptions } from "pg-boss";
|
|
3
3
|
import { defaultClientFactory, type PgBossClient, type PgBossClientFactory } from "#client.ts";
|
|
4
4
|
import { toPgBossError } from "#error.ts";
|
|
5
|
+
import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
|
|
5
6
|
import { acquireCachedClient, releaseCachedClient } from "./clientCache.ts";
|
|
6
7
|
import { logClientError, observeClientErrors } from "./clientErrors.ts";
|
|
7
8
|
|
|
@@ -22,6 +23,9 @@ async function startClient(options: AcquireClientOptions, report: (error: Error)
|
|
|
22
23
|
observeClientErrors(client, report);
|
|
23
24
|
try {
|
|
24
25
|
await client.start();
|
|
26
|
+
if (options.constructor.migrate !== false && client.getDb !== undefined) {
|
|
27
|
+
await client.getDb().executeSql(deferralStorePlan(options.constructor.schema ?? "pgboss"));
|
|
28
|
+
}
|
|
25
29
|
return client;
|
|
26
30
|
} catch (error) {
|
|
27
31
|
try {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
13
13
|
import { ensureQueue } from "#internal/ensureQueue.ts";
|
|
14
14
|
import { registerWorker } from "#internal/registerWorker.ts";
|
|
15
|
-
import { payloadOf
|
|
15
|
+
import { payloadOf } from "#internal/tracing.ts";
|
|
16
16
|
|
|
17
17
|
function isQueueWorker(registration: JobRegistration): registration is QueueWorker {
|
|
18
18
|
return registration._tag === "QueueWorker";
|
|
@@ -65,7 +65,7 @@ function workerContext(context: unknown): unknown {
|
|
|
65
65
|
return context;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function runQueueWorker(worker: QueueWorker, job: Job<unknown
|
|
68
|
+
function runQueueWorker(worker: QueueWorker, job: Job<unknown>) {
|
|
69
69
|
return Schema.decodeUnknownEffect(worker.queue.schema)(payloadOf(job.data)).pipe(
|
|
70
70
|
Effect.mapError(
|
|
71
71
|
(error) =>
|
|
@@ -76,8 +76,6 @@ function runQueueWorker(worker: QueueWorker, job: Job<unknown>, context: Context
|
|
|
76
76
|
}),
|
|
77
77
|
),
|
|
78
78
|
Effect.flatMap((payload) => worker.handler(payload, jobContext(job))),
|
|
79
|
-
(effect) => traceJob(effect, job, worker.queue.name),
|
|
80
|
-
Effect.provide(context),
|
|
81
79
|
);
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -86,24 +84,25 @@ async function registerQueue(
|
|
|
86
84
|
registration: QueueWorker | QueueDefinition,
|
|
87
85
|
context: Context.Context<unknown>,
|
|
88
86
|
scope: Scope.Scope,
|
|
87
|
+
schema: string,
|
|
89
88
|
): Promise<void> {
|
|
90
89
|
const queue = registration._tag === "QueueWorker" ? registration.queue : registration;
|
|
91
90
|
const name = queue.name;
|
|
92
91
|
if (registration._tag === "QueueDefinition") {
|
|
93
92
|
return;
|
|
94
93
|
}
|
|
95
|
-
await registerWorker(client, name, queue.workerOptions, (job) => runQueueWorker(registration, job,
|
|
94
|
+
await registerWorker(client, name, queue.workerOptions, (job) => runQueueWorker(registration, job), scope, schema, queue.maxDeferrals, context);
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
async function registerSchedule(
|
|
97
|
+
async function registerSchedule(
|
|
98
|
+
client: PgBossClient,
|
|
99
|
+
worker: ScheduledWorker,
|
|
100
|
+
context: Context.Context<unknown>,
|
|
101
|
+
scope: Scope.Scope,
|
|
102
|
+
schema: string,
|
|
103
|
+
): Promise<void> {
|
|
99
104
|
const { schedule } = worker;
|
|
100
|
-
await registerWorker(
|
|
101
|
-
client,
|
|
102
|
-
schedule.name,
|
|
103
|
-
schedule.workerOptions,
|
|
104
|
-
(job) => Effect.provide(traceJob(worker.effect, job, schedule.name), context),
|
|
105
|
-
scope,
|
|
106
|
-
);
|
|
105
|
+
await registerWorker(client, schedule.name, schedule.workerOptions, () => worker.effect, scope, schema, schedule.maxDeferrals, context);
|
|
107
106
|
await client.schedule(schedule.name, schedule.cron, null, schedule.scheduleOptions);
|
|
108
107
|
}
|
|
109
108
|
|
|
@@ -120,6 +119,7 @@ export const registerJobs = <R>(
|
|
|
120
119
|
registrations: readonly JobRegistration[],
|
|
121
120
|
context: Context.Context<R>,
|
|
122
121
|
replaceWorkers: boolean,
|
|
122
|
+
schema = "pgboss",
|
|
123
123
|
): Effect.Effect<void, PgBossError | PgBossQueueOptionsError, Scope.Scope> =>
|
|
124
124
|
Effect.flatMap(Effect.scope, (scope) =>
|
|
125
125
|
Effect.tryPromise({
|
|
@@ -130,9 +130,9 @@ export const registerJobs = <R>(
|
|
|
130
130
|
const provided = workerContext(context);
|
|
131
131
|
for (const registration of registrations) {
|
|
132
132
|
if (isQueueWorker(registration) || isQueueDefinition(registration)) {
|
|
133
|
-
await registerQueue(client, registration, provided, scope);
|
|
133
|
+
await registerQueue(client, registration, provided, scope, schema);
|
|
134
134
|
} else if (isScheduledWorker(registration)) {
|
|
135
|
-
await registerSchedule(client, registration, provided, scope);
|
|
135
|
+
await registerSchedule(client, registration, provided, scope, schema);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
},
|
|
@@ -1,42 +1,55 @@
|
|
|
1
|
-
import { Clock, Effect, type Scope } from "effect";
|
|
1
|
+
import { Clock, type Context, Effect, type Scope } from "effect";
|
|
2
2
|
import type { Job, WorkOptions } from "pg-boss";
|
|
3
3
|
import type { PgBossClient } from "#client.ts";
|
|
4
4
|
import { batchHandler } from "#internal/batchHandler.ts";
|
|
5
|
+
import { deferrableAttempt } from "#internal/deferrableAttempt.ts";
|
|
6
|
+
import { traceJob } from "#internal/tracing.ts";
|
|
5
7
|
|
|
6
8
|
export function registerWorker(
|
|
7
9
|
client: PgBossClient,
|
|
8
10
|
name: string,
|
|
9
11
|
options: WorkOptions,
|
|
10
|
-
run: (job: Job<unknown>) => Effect.Effect<unknown, unknown>,
|
|
12
|
+
run: (job: Job<unknown>) => Effect.Effect<unknown, unknown, unknown>,
|
|
11
13
|
scope: Scope.Scope,
|
|
14
|
+
schema: string,
|
|
15
|
+
maxDeferrals: number,
|
|
16
|
+
context: Context.Context<unknown>,
|
|
12
17
|
): Promise<string> {
|
|
13
18
|
const registration = client.work(
|
|
14
19
|
name,
|
|
15
20
|
{ ...options, includeMetadata: true },
|
|
16
|
-
batchHandler(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
batchHandler(
|
|
22
|
+
client,
|
|
23
|
+
name,
|
|
24
|
+
(job) => {
|
|
25
|
+
const attempt = traceJob(deferrableAttempt(run(job), client, schema, job, maxDeferrals), job, name);
|
|
26
|
+
return Effect.provide(attempt, context);
|
|
27
|
+
},
|
|
28
|
+
async (attempt) => {
|
|
29
|
+
if (client.findJobs === undefined || client.notifyWorker === undefined) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const job = (await client.findJobs(name, { id: attempt.id }))[0];
|
|
33
|
+
if (job?.state !== "retry") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const workId = await registration;
|
|
37
|
+
await Effect.runPromise(
|
|
38
|
+
Effect.gen(function* () {
|
|
39
|
+
const now = yield* Clock.currentTimeMillis;
|
|
40
|
+
// PostgreSQL timestamps retain sub-millisecond precision that Date drops.
|
|
41
|
+
yield* Effect.sleep(Math.max(0, job.startAfter.getTime() + 1 - now));
|
|
42
|
+
const workerIds = client
|
|
43
|
+
.getWipData?.()
|
|
44
|
+
.filter((worker) => worker.workId === workId)
|
|
45
|
+
.map((worker) => worker.id) ?? [workId];
|
|
46
|
+
for (const workerId of workerIds) {
|
|
47
|
+
client.notifyWorker?.(workerId);
|
|
48
|
+
}
|
|
49
|
+
}).pipe(Effect.forkIn(scope)),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
),
|
|
40
53
|
);
|
|
41
54
|
return registration;
|
|
42
55
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DefineScheduleOptions, ScheduleDefinition } from "#definition.ts";
|
|
2
|
+
import { maxDeferrals, nativeQueueOptions } from "#internal/definitionOptions.ts";
|
|
3
|
+
|
|
4
|
+
export function scheduleDefinition<const TName extends string>(options: DefineScheduleOptions<TName>): ScheduleDefinition<TName> {
|
|
5
|
+
const definition: ScheduleDefinition<TName> = {
|
|
6
|
+
_tag: "ScheduleDefinition",
|
|
7
|
+
cron: options.cron,
|
|
8
|
+
maxDeferrals: maxDeferrals(options.queue),
|
|
9
|
+
name: options.name,
|
|
10
|
+
queueOptions: nativeQueueOptions(options.queue),
|
|
11
|
+
run: (effect) =>
|
|
12
|
+
Object.freeze({
|
|
13
|
+
_tag: "ScheduledWorker" as const,
|
|
14
|
+
effect,
|
|
15
|
+
schedule: definition,
|
|
16
|
+
}),
|
|
17
|
+
scheduleOptions: Object.freeze({ tz: "UTC", ...options.schedule }),
|
|
18
|
+
workerOptions: Object.freeze({ batchSize: 10, burstWhenBatchFull: true, ...options.worker }),
|
|
19
|
+
};
|
|
20
|
+
return Object.freeze(definition);
|
|
21
|
+
}
|