@shivaedev/jobs 0.0.0 → 0.19.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 +319 -0
- package/LICENSE +21 -0
- package/README.md +527 -1
- package/dist/server/client.d.ts +25 -0
- package/dist/server/client.d.ts.map +1 -0
- package/dist/server/client.js +3 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/definition.d.ts +96 -0
- package/dist/server/definition.d.ts.map +1 -0
- package/dist/server/definition.js +40 -0
- package/dist/server/definition.js.map +1 -0
- package/dist/server/error.d.ts +51 -0
- package/dist/server/error.d.ts.map +1 -0
- package/dist/server/error.js +37 -0
- package/dist/server/error.js.map +1 -0
- package/dist/server/health.d.ts +51 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +4 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/internal/batchHandler.d.ts +5 -0
- package/dist/server/internal/batchHandler.d.ts.map +1 -0
- package/dist/server/internal/batchHandler.js +51 -0
- package/dist/server/internal/batchHandler.js.map +1 -0
- package/dist/server/internal/clientCache.d.ts +8 -0
- package/dist/server/internal/clientCache.d.ts.map +1 -0
- package/dist/server/internal/clientCache.js +61 -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/countsFor.d.ts +5 -0
- package/dist/server/internal/countsFor.d.ts.map +1 -0
- package/dist/server/internal/countsFor.js +28 -0
- package/dist/server/internal/countsFor.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/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 +35 -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/enqueueFor.d.ts +4 -0
- package/dist/server/internal/enqueueFor.d.ts.map +1 -0
- package/dist/server/internal/enqueueFor.js +23 -0
- package/dist/server/internal/enqueueFor.js.map +1 -0
- package/dist/server/internal/ensureQueue.d.ts +4 -0
- package/dist/server/internal/ensureQueue.d.ts.map +1 -0
- package/dist/server/internal/ensureQueue.js +44 -0
- package/dist/server/internal/ensureQueue.js.map +1 -0
- package/dist/server/internal/expireAttempt.d.ts +3 -0
- package/dist/server/internal/expireAttempt.d.ts.map +1 -0
- package/dist/server/internal/expireAttempt.js +17 -0
- package/dist/server/internal/expireAttempt.js.map +1 -0
- package/dist/server/internal/healthFor.d.ts +5 -0
- package/dist/server/internal/healthFor.d.ts.map +1 -0
- package/dist/server/internal/healthFor.js +27 -0
- package/dist/server/internal/healthFor.js.map +1 -0
- package/dist/server/internal/jobControl.d.ts +5 -0
- package/dist/server/internal/jobControl.d.ts.map +1 -0
- package/dist/server/internal/jobControl.js +35 -0
- package/dist/server/internal/jobControl.js.map +1 -0
- package/dist/server/internal/lifecycle.d.ts +17 -0
- package/dist/server/internal/lifecycle.d.ts.map +1 -0
- package/dist/server/internal/lifecycle.js +55 -0
- package/dist/server/internal/lifecycle.js.map +1 -0
- package/dist/server/internal/listJobs.d.ts +5 -0
- package/dist/server/internal/listJobs.d.ts.map +1 -0
- package/dist/server/internal/listJobs.js +69 -0
- package/dist/server/internal/listJobs.js.map +1 -0
- package/dist/server/internal/makeService.d.ts +4 -0
- package/dist/server/internal/makeService.d.ts.map +1 -0
- package/dist/server/internal/makeService.js +17 -0
- package/dist/server/internal/makeService.js.map +1 -0
- package/dist/server/internal/nativeClient.d.ts +3 -0
- package/dist/server/internal/nativeClient.d.ts.map +1 -0
- package/dist/server/internal/nativeClient.js +28 -0
- package/dist/server/internal/nativeClient.js.map +1 -0
- package/dist/server/internal/observeTestingSettlements.d.ts +6 -0
- package/dist/server/internal/observeTestingSettlements.d.ts.map +1 -0
- package/dist/server/internal/observeTestingSettlements.js +35 -0
- package/dist/server/internal/observeTestingSettlements.js.map +1 -0
- package/dist/server/internal/register.d.ts +8 -0
- package/dist/server/internal/register.d.ts.map +1 -0
- package/dist/server/internal/register.js +98 -0
- package/dist/server/internal/register.js.map +1 -0
- package/dist/server/internal/registerReleasedJobs.d.ts +5 -0
- package/dist/server/internal/registerReleasedJobs.d.ts.map +1 -0
- package/dist/server/internal/registerReleasedJobs.js +35 -0
- package/dist/server/internal/registerReleasedJobs.js.map +1 -0
- package/dist/server/internal/registerWorker.d.ts +5 -0
- package/dist/server/internal/registerWorker.d.ts.map +1 -0
- package/dist/server/internal/registerWorker.js +33 -0
- package/dist/server/internal/registerWorker.js.map +1 -0
- package/dist/server/internal/runSchemaPlan.d.ts +7 -0
- package/dist/server/internal/runSchemaPlan.d.ts.map +1 -0
- package/dist/server/internal/runSchemaPlan.js +25 -0
- package/dist/server/internal/runSchemaPlan.js.map +1 -0
- 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/schemaIndexBuilds.d.ts +4 -0
- package/dist/server/internal/schemaIndexBuilds.d.ts.map +1 -0
- package/dist/server/internal/schemaIndexBuilds.js +34 -0
- package/dist/server/internal/schemaIndexBuilds.js.map +1 -0
- 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 +48 -0
- package/dist/server/internal/settleDeferral.js.map +1 -0
- package/dist/server/internal/storedRows.d.ts +6 -0
- package/dist/server/internal/storedRows.d.ts.map +1 -0
- package/dist/server/internal/storedRows.js +10 -0
- package/dist/server/internal/storedRows.js.map +1 -0
- package/dist/server/internal/testingClient.d.ts +13 -0
- package/dist/server/internal/testingClient.d.ts.map +1 -0
- package/dist/server/internal/testingClient.js +117 -0
- package/dist/server/internal/testingClient.js.map +1 -0
- 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 +29 -0
- package/dist/server/internal/testingDeferrals.js.map +1 -0
- package/dist/server/internal/testingExpirations.d.ts +6 -0
- package/dist/server/internal/testingExpirations.d.ts.map +1 -0
- package/dist/server/internal/testingExpirations.js +27 -0
- package/dist/server/internal/testingExpirations.js.map +1 -0
- package/dist/server/internal/testingIdle.d.ts +16 -0
- package/dist/server/internal/testingIdle.d.ts.map +1 -0
- package/dist/server/internal/testingIdle.js +31 -0
- package/dist/server/internal/testingIdle.js.map +1 -0
- package/dist/server/internal/testingReadout.d.ts +44 -0
- package/dist/server/internal/testingReadout.d.ts.map +1 -0
- package/dist/server/internal/testingReadout.js +13 -0
- package/dist/server/internal/testingReadout.js.map +1 -0
- package/dist/server/internal/testingSignal.d.ts +7 -0
- package/dist/server/internal/testingSignal.d.ts.map +1 -0
- package/dist/server/internal/testingSignal.js +29 -0
- package/dist/server/internal/testingSignal.js.map +1 -0
- package/dist/server/internal/testingSnapshot.d.ts +6 -0
- package/dist/server/internal/testingSnapshot.d.ts.map +1 -0
- package/dist/server/internal/testingSnapshot.js +32 -0
- package/dist/server/internal/testingSnapshot.js.map +1 -0
- package/dist/server/internal/tracing.d.ts +6 -0
- package/dist/server/internal/tracing.d.ts.map +1 -0
- package/dist/server/internal/tracing.js +37 -0
- package/dist/server/internal/tracing.js.map +1 -0
- package/dist/server/internal/waitForBatchExpiry.d.ts +6 -0
- package/dist/server/internal/waitForBatchExpiry.d.ts.map +1 -0
- package/dist/server/internal/waitForBatchExpiry.js +28 -0
- package/dist/server/internal/waitForBatchExpiry.js.map +1 -0
- package/dist/server/schema.d.ts +10 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +67 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/service.d.ts +29 -0
- package/dist/server/service.d.ts.map +1 -0
- package/dist/server/service.js +33 -0
- package/dist/server/service.js.map +1 -0
- package/dist/server/testing.d.ts +77 -0
- package/dist/server/testing.d.ts.map +1 -0
- package/dist/server/testing.js +90 -0
- package/dist/server/testing.js.map +1 -0
- package/package.json +71 -4
- package/src/server/client.ts +40 -0
- package/src/server/definition.ts +140 -0
- package/src/server/error.ts +73 -0
- package/src/server/health.ts +60 -0
- package/src/server/internal/batchHandler.ts +71 -0
- package/src/server/internal/clientCache.ts +73 -0
- package/src/server/internal/clientErrors.ts +64 -0
- package/src/server/internal/countsFor.ts +34 -0
- package/src/server/internal/deadlineOf.ts +8 -0
- package/src/server/internal/deferrableAttempt.ts +41 -0
- package/src/server/internal/deferral.ts +19 -0
- package/src/server/internal/deferralStorePlan.ts +35 -0
- package/src/server/internal/definitionOptions.ts +14 -0
- package/src/server/internal/enqueueFor.ts +39 -0
- package/src/server/internal/ensureQueue.ts +47 -0
- package/src/server/internal/expireAttempt.ts +21 -0
- package/src/server/internal/healthFor.ts +35 -0
- package/src/server/internal/jobControl.ts +44 -0
- package/src/server/internal/lifecycle.ts +68 -0
- package/src/server/internal/listJobs.ts +98 -0
- package/src/server/internal/makeService.ts +19 -0
- package/src/server/internal/nativeClient.ts +30 -0
- package/src/server/internal/observeTestingSettlements.ts +36 -0
- package/src/server/internal/register.ts +145 -0
- package/src/server/internal/registerReleasedJobs.ts +44 -0
- package/src/server/internal/registerWorker.ts +56 -0
- package/src/server/internal/runSchemaPlan.ts +32 -0
- package/src/server/internal/scheduleDefinition.ts +21 -0
- package/src/server/internal/schemaIndexBuilds.ts +44 -0
- package/src/server/internal/settleDeferral.ts +54 -0
- package/src/server/internal/storedRows.ts +17 -0
- package/src/server/internal/testingClient.ts +134 -0
- package/src/server/internal/testingDeferrals.ts +44 -0
- package/src/server/internal/testingExpirations.ts +31 -0
- package/src/server/internal/testingIdle.ts +52 -0
- package/src/server/internal/testingReadout.ts +19 -0
- package/src/server/internal/testingSignal.ts +30 -0
- package/src/server/internal/testingSnapshot.ts +53 -0
- package/src/server/internal/tracing.ts +44 -0
- package/src/server/internal/waitForBatchExpiry.ts +30 -0
- package/src/server/schema.ts +84 -0
- package/src/server/service.ts +100 -0
- package/src/server/testing.ts +118 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { STATELY_DEFERRAL_POLICY } from "#internal/deferral.ts";
|
|
5
|
+
import { quotedSchema } from "#internal/storedRows.ts";
|
|
6
|
+
|
|
7
|
+
const Settlement = Schema.Struct({ claimed: Schema.Boolean, deferred: Schema.Boolean });
|
|
8
|
+
|
|
9
|
+
export async function settleDeferral(client: PgBossClient, schema: string, job: Job<unknown>, at: string, reason: string, maxDeferrals: number) {
|
|
10
|
+
if (client.getDb === undefined) {
|
|
11
|
+
throw new TypeError("This pg-boss client does not support getDb for RetryAt");
|
|
12
|
+
}
|
|
13
|
+
const quoted = quotedSchema(schema);
|
|
14
|
+
// Native failure settlement deletes and reinserts jobs, so a cascading job foreign key would erase the counter.
|
|
15
|
+
const { rows } = await client.getDb().executeSql(
|
|
16
|
+
`WITH claimed AS MATERIALIZED (
|
|
17
|
+
SELECT name, id, created_on,
|
|
18
|
+
GREATEST(keep_until, $4::timestamptz + (keep_until - start_after)) AS keep_until
|
|
19
|
+
FROM ${quoted}.job
|
|
20
|
+
WHERE name = $1 AND id = $2 AND state = 'active' AND retry_count = $3
|
|
21
|
+
AND date_trunc('milliseconds', started_on) = $6::timestamptz
|
|
22
|
+
FOR UPDATE
|
|
23
|
+
), counted AS (
|
|
24
|
+
INSERT INTO ${quoted}.effect_job_deferral AS previous (name, id, created_on, count, keep_until)
|
|
25
|
+
SELECT name, id, created_on, 1, keep_until FROM claimed
|
|
26
|
+
ON CONFLICT (name, id) DO UPDATE SET
|
|
27
|
+
count = CASE WHEN previous.created_on = EXCLUDED.created_on THEN previous.count + 1 ELSE 1 END,
|
|
28
|
+
created_on = EXCLUDED.created_on, keep_until = EXCLUDED.keep_until
|
|
29
|
+
WHERE previous.created_on <> EXCLUDED.created_on OR previous.count < $7
|
|
30
|
+
RETURNING name, id, keep_until
|
|
31
|
+
), deferred AS (
|
|
32
|
+
UPDATE ${quoted}.job AS job SET state = 'retry', start_after = $4::timestamptz,
|
|
33
|
+
policy = CASE WHEN job.policy = 'stately' THEN '${STATELY_DEFERRAL_POLICY}' ELSE job.policy END,
|
|
34
|
+
keep_until = counted.keep_until, started_on = NULL, heartbeat_on = NULL,
|
|
35
|
+
output = jsonb_build_object('jobDeferral', jsonb_build_object('at', $4::text, 'reason', $5::text))
|
|
36
|
+
FROM counted WHERE job.name = counted.name AND job.id = counted.id
|
|
37
|
+
RETURNING job.id
|
|
38
|
+
), cleaned AS (
|
|
39
|
+
DELETE FROM ${quoted}.effect_job_deferral WHERE (name, id) IN (
|
|
40
|
+
SELECT stale.name, stale.id FROM ${quoted}.effect_job_deferral AS stale
|
|
41
|
+
WHERE EXISTS (SELECT 1 FROM counted) AND stale.keep_until < ${quoted}.job_now()
|
|
42
|
+
AND (stale.name, stale.id) <> ($1, $2)
|
|
43
|
+
AND NOT EXISTS (
|
|
44
|
+
SELECT 1 FROM ${quoted}.job AS live
|
|
45
|
+
WHERE live.name = stale.name AND live.id = stale.id AND live.created_on = stale.created_on
|
|
46
|
+
)
|
|
47
|
+
ORDER BY stale.name, stale.id LIMIT 100 FOR UPDATE OF stale SKIP LOCKED
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
SELECT EXISTS (SELECT 1 FROM claimed) AS claimed, EXISTS (SELECT 1 FROM deferred) AS deferred`,
|
|
51
|
+
[job.name, job.id, job.retryCount, at, reason, Reflect.get(job, "startedOn"), maxDeferrals],
|
|
52
|
+
);
|
|
53
|
+
return Schema.decodeUnknownSync(Settlement)(rows[0]);
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import type { JobPayloadSchema, QueueDefinition } from "#definition.ts";
|
|
3
|
+
import { PgBossPayloadError } from "#error.ts";
|
|
4
|
+
import { payloadOf } from "#internal/tracing.ts";
|
|
5
|
+
|
|
6
|
+
export function quotedSchema(schema: string): string {
|
|
7
|
+
return `"${schema.replaceAll('"', '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function decodeStoredData<const TName extends string, const TPayload extends JobPayloadSchema>(
|
|
11
|
+
queue: Pick<QueueDefinition<TName, TPayload>, "name" | "schema">,
|
|
12
|
+
data: unknown,
|
|
13
|
+
): Effect.Effect<TPayload["Type"], PgBossPayloadError, TPayload["DecodingServices"]> {
|
|
14
|
+
return Schema.decodeUnknownEffect(queue.schema)(payloadOf(data)).pipe(
|
|
15
|
+
Effect.mapError((error) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(error), queue: queue.name })),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Job, PgBoss, SendOptions, UpdateOptions, UpdateRequest, UpdateResponse } from "pg-boss";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
import { deferralObservers } from "#internal/deferral.ts";
|
|
4
|
+
import { expireAttempt } from "#internal/expireAttempt.ts";
|
|
5
|
+
import { observeTestingSettlements } from "#internal/observeTestingSettlements.ts";
|
|
6
|
+
import { testingExpirations } from "#internal/testingExpirations.ts";
|
|
7
|
+
import type { TestingBatch, TestingWork } from "#internal/testingIdle.ts";
|
|
8
|
+
|
|
9
|
+
export interface TestingControls {
|
|
10
|
+
readonly changed: () => void;
|
|
11
|
+
readonly expirations: Map<string, number>;
|
|
12
|
+
readonly expirySeconds: Map<string, number>;
|
|
13
|
+
readonly failures: Set<string>;
|
|
14
|
+
readonly retryPolicy: Pick<SendOptions, "retryBackoff" | "retryDelay">;
|
|
15
|
+
readonly work: TestingWork;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function testingClient(client: PgBoss, schema: string, controls: TestingControls): PgBossClient {
|
|
19
|
+
const settlements = observeTestingSettlements(client, (name) => {
|
|
20
|
+
for (const batch of controls.work.batches) {
|
|
21
|
+
if (batch.name === name) {
|
|
22
|
+
batch.settled = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
controls.changed();
|
|
26
|
+
});
|
|
27
|
+
const expirations = testingExpirations(client);
|
|
28
|
+
function update(request: UpdateRequest): Promise<UpdateResponse>;
|
|
29
|
+
function update(name: string, data: object | null | undefined, options?: UpdateOptions): Promise<UpdateResponse>;
|
|
30
|
+
async function update(request: string | UpdateRequest, data?: object | null, options?: UpdateOptions): Promise<UpdateResponse> {
|
|
31
|
+
const result = typeof request === "string" ? await client.update(request, data, options) : await client.update(request);
|
|
32
|
+
controls.changed();
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const overrides: Partial<PgBossClient> = {
|
|
36
|
+
complete: async (...args) => {
|
|
37
|
+
const result = await client.complete(...args);
|
|
38
|
+
controls.changed();
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
createQueue: (name, options) => {
|
|
42
|
+
const { retryDelayMax, ...rest } = options ?? {};
|
|
43
|
+
return client.createQueue(name, {
|
|
44
|
+
...rest,
|
|
45
|
+
...(controls.retryPolicy.retryBackoff === false || retryDelayMax === undefined ? {} : { retryDelayMax }),
|
|
46
|
+
...controls.retryPolicy,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
fail: async (...args) => {
|
|
50
|
+
const result = await client.fail(...args);
|
|
51
|
+
controls.changed();
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
retry: async (...args) => {
|
|
55
|
+
const result = await client.retry(...args);
|
|
56
|
+
controls.changed();
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
59
|
+
send: async (name, data, options) => {
|
|
60
|
+
if (controls.failures.delete(name)) {
|
|
61
|
+
throw new Error("Injected pg-boss send failure");
|
|
62
|
+
}
|
|
63
|
+
const attempts = controls.expirations.get(name);
|
|
64
|
+
const expireInSeconds = controls.expirySeconds.get(name);
|
|
65
|
+
const { retryDelayMax, ...rest } = options ?? {};
|
|
66
|
+
const id = await expirations.send(
|
|
67
|
+
name,
|
|
68
|
+
data,
|
|
69
|
+
{
|
|
70
|
+
...rest,
|
|
71
|
+
...(controls.retryPolicy.retryBackoff === false || retryDelayMax === undefined ? {} : { retryDelayMax }),
|
|
72
|
+
...controls.retryPolicy,
|
|
73
|
+
...(attempts === undefined ? {} : { retryLimit: attempts - 1 }),
|
|
74
|
+
...(expireInSeconds === undefined ? {} : { expireInSeconds }),
|
|
75
|
+
},
|
|
76
|
+
attempts,
|
|
77
|
+
);
|
|
78
|
+
controls.changed();
|
|
79
|
+
return id;
|
|
80
|
+
},
|
|
81
|
+
stop: async (options) => {
|
|
82
|
+
try {
|
|
83
|
+
await client.stop(options);
|
|
84
|
+
} finally {
|
|
85
|
+
settlements.close();
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
update,
|
|
89
|
+
updateQueue: (name, options) =>
|
|
90
|
+
client.updateQueue(name, {
|
|
91
|
+
...options,
|
|
92
|
+
...controls.retryPolicy,
|
|
93
|
+
...(controls.retryPolicy.retryBackoff === false ? { retryDelayMax: null } : {}),
|
|
94
|
+
}),
|
|
95
|
+
work: (name, options, handler) => {
|
|
96
|
+
settlements.watch(name);
|
|
97
|
+
controls.work.queues.add(name);
|
|
98
|
+
return client.work(name, options, async (jobs: Job<unknown>[]) => {
|
|
99
|
+
const batch: TestingBatch = { name, settled: false };
|
|
100
|
+
controls.work.batches.add(batch);
|
|
101
|
+
controls.changed();
|
|
102
|
+
try {
|
|
103
|
+
const ready: Job<unknown>[] = [];
|
|
104
|
+
for (const job of jobs) {
|
|
105
|
+
const attempts = await expirations.attemptsFor(job);
|
|
106
|
+
if (attempts !== undefined && job.retryCount + 1 >= attempts) {
|
|
107
|
+
await expireAttempt(client, schema, job);
|
|
108
|
+
} else {
|
|
109
|
+
ready.push(job);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (ready.length > 0) {
|
|
113
|
+
await handler(ready);
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
controls.work.batches.delete(batch);
|
|
117
|
+
controls.changed();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const wrapped = new Proxy(client, {
|
|
123
|
+
get(target, property) {
|
|
124
|
+
if (Object.hasOwn(overrides, property)) {
|
|
125
|
+
const override: unknown = Reflect.get(overrides, property);
|
|
126
|
+
return override;
|
|
127
|
+
}
|
|
128
|
+
const value: unknown = Reflect.get(target, property);
|
|
129
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
deferralObservers.set(wrapped, controls.changed);
|
|
133
|
+
return wrapped;
|
|
134
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DateTime } from "effect";
|
|
2
|
+
import type { PgBoss } from "pg-boss";
|
|
3
|
+
import type { QueueDefinition } from "#definition.ts";
|
|
4
|
+
import { deferralOf } from "#internal/deferral.ts";
|
|
5
|
+
import { quotedSchema } from "#internal/storedRows.ts";
|
|
6
|
+
import { testingOperation } from "#internal/testingReadout.ts";
|
|
7
|
+
|
|
8
|
+
export function testingDeferrals(client: () => PgBoss, schema: () => string, changed: () => void) {
|
|
9
|
+
return {
|
|
10
|
+
deferred: (queue: Pick<QueueDefinition, "name">) =>
|
|
11
|
+
testingOperation(
|
|
12
|
+
"deferred",
|
|
13
|
+
async () => {
|
|
14
|
+
const jobs = await client().findJobs(queue.name);
|
|
15
|
+
return jobs.flatMap((job) => {
|
|
16
|
+
const deferral = job.state === "retry" && job.startedOn === null ? deferralOf(job) : undefined;
|
|
17
|
+
return deferral === undefined ? [] : [{ at: DateTime.makeUnsafe(job.startAfter), reason: deferral.reason }];
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
queue.name,
|
|
21
|
+
),
|
|
22
|
+
runDeferredNow: (queue: Pick<QueueDefinition, "name">) =>
|
|
23
|
+
testingOperation(
|
|
24
|
+
"runDeferredNow",
|
|
25
|
+
async () => {
|
|
26
|
+
const quoted = quotedSchema(schema());
|
|
27
|
+
await client()
|
|
28
|
+
.getDb()
|
|
29
|
+
.executeSql(
|
|
30
|
+
`UPDATE ${quoted}.job SET start_after = ${quoted}.job_now()
|
|
31
|
+
WHERE name = $1 AND state = 'retry' AND output ? 'jobDeferral' AND started_on IS NULL`,
|
|
32
|
+
[queue.name],
|
|
33
|
+
);
|
|
34
|
+
changed();
|
|
35
|
+
for (const worker of client()
|
|
36
|
+
.getWipData()
|
|
37
|
+
.filter((candidate) => candidate.name === queue.name)) {
|
|
38
|
+
client().notifyWorker(worker.id);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
queue.name,
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Job, PgBoss, SendOptions } from "pg-boss";
|
|
2
|
+
|
|
3
|
+
export function testingExpirations(client: PgBoss) {
|
|
4
|
+
const snapshots = new Map<string, Promise<number | undefined>>();
|
|
5
|
+
return {
|
|
6
|
+
attemptsFor: (job: Job<unknown>) => snapshots.get(`${job.name}:${job.id}`),
|
|
7
|
+
send(name: string, data: object | null | undefined, options: SendOptions, attempts: number | undefined) {
|
|
8
|
+
if (attempts === undefined && options.id === undefined) {
|
|
9
|
+
return client.send(name, data, options);
|
|
10
|
+
}
|
|
11
|
+
const id = options.id ?? crypto.randomUUID();
|
|
12
|
+
const key = `${name}:${id}`;
|
|
13
|
+
const previous = snapshots.get(key);
|
|
14
|
+
const sending = client.send(name, data, { ...options, id });
|
|
15
|
+
// A worker may claim the row before send returns. Wait for its result before applying the snapshot.
|
|
16
|
+
const snapshot = sending
|
|
17
|
+
.then(
|
|
18
|
+
(sent) => (sent === id ? attempts : previous),
|
|
19
|
+
() => previous,
|
|
20
|
+
)
|
|
21
|
+
.then((value) => {
|
|
22
|
+
if (value === undefined && snapshots.get(key) === snapshot) {
|
|
23
|
+
snapshots.delete(key);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
});
|
|
27
|
+
snapshots.set(key, snapshot);
|
|
28
|
+
return sending;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { PgBoss } from "pg-boss";
|
|
3
|
+
import type { PgBossTestingError } from "#error.ts";
|
|
4
|
+
import { quotedSchema } from "#internal/storedRows.ts";
|
|
5
|
+
import { testingOperation } from "#internal/testingReadout.ts";
|
|
6
|
+
|
|
7
|
+
export interface TestingBatch {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
settled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TestingWork {
|
|
13
|
+
readonly batches: Set<TestingBatch>;
|
|
14
|
+
readonly queues: Set<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Busy = Schema.Struct({ busy: Schema.Boolean });
|
|
18
|
+
|
|
19
|
+
export function testingIdle(
|
|
20
|
+
client: () => PgBoss,
|
|
21
|
+
schema: () => string,
|
|
22
|
+
work: TestingWork,
|
|
23
|
+
signal: { readonly revision: Effect.Effect<number>; readonly waitForChange: (after: number) => Effect.Effect<number> },
|
|
24
|
+
): Effect.Effect<void, PgBossTestingError> {
|
|
25
|
+
const busy = testingOperation("waitForIdle", async () => {
|
|
26
|
+
const quoted = quotedSchema(schema());
|
|
27
|
+
const { rows } = await client()
|
|
28
|
+
.getDb()
|
|
29
|
+
.executeSql(
|
|
30
|
+
`/* jobs testing idle */ SELECT EXISTS (
|
|
31
|
+
SELECT 1 FROM ${quoted}.job
|
|
32
|
+
WHERE name = ANY($1::text[]) AND (
|
|
33
|
+
(state < 'active' AND NOT blocked AND start_after <= ${quoted}.job_now())
|
|
34
|
+
OR state = 'active'
|
|
35
|
+
)
|
|
36
|
+
) AS busy`,
|
|
37
|
+
[[...work.queues]],
|
|
38
|
+
);
|
|
39
|
+
return (await Schema.decodeUnknownPromise(Busy)(rows[0])).busy;
|
|
40
|
+
});
|
|
41
|
+
return Effect.gen(function* () {
|
|
42
|
+
while (true) {
|
|
43
|
+
const revision = yield* signal.revision;
|
|
44
|
+
// A batch with no settlement since its fetch still holds active jobs, so it is busy without a read.
|
|
45
|
+
const unsettled = [...work.batches].some((batch) => !batch.settled);
|
|
46
|
+
if (work.queues.size === 0 || !(unsettled || (yield* busy))) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
yield* signal.waitForChange(revision);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect, Redacted } from "effect";
|
|
2
|
+
import type { FindJobsOptions, PgBoss } from "pg-boss";
|
|
3
|
+
import type { JobPayloadSchema, QueueDefinition } from "#definition.ts";
|
|
4
|
+
import { PgBossTestingError } from "#error.ts";
|
|
5
|
+
import { decodeStoredData } from "#internal/storedRows.ts";
|
|
6
|
+
|
|
7
|
+
export function testingOperation<A>(operation: string, run: () => Promise<A>, queue?: string) {
|
|
8
|
+
return Effect.tryPromise({
|
|
9
|
+
catch: (original) => new PgBossTestingError({ operation, original: Redacted.make(original), queue }),
|
|
10
|
+
try: run,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function testingReadout(client: () => PgBoss) {
|
|
15
|
+
return <TName extends string, TPayload extends JobPayloadSchema>(queue: QueueDefinition<TName, TPayload>, options?: FindJobsOptions) =>
|
|
16
|
+
testingOperation("jobs", () => client().findJobs<unknown>(queue.name, options), queue.name).pipe(
|
|
17
|
+
Effect.flatMap((jobs) => Effect.forEach(jobs, (job) => decodeStoredData(queue, job.data).pipe(Effect.map((data) => ({ ...job, data }))))),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
export function testingSignal() {
|
|
4
|
+
let revision = 0;
|
|
5
|
+
const listeners = new Set<(revision: number) => void>();
|
|
6
|
+
return {
|
|
7
|
+
changed() {
|
|
8
|
+
revision += 1;
|
|
9
|
+
// A resumed waiter can wait again synchronously; it must wait for the next change, not this one.
|
|
10
|
+
const waiting = [...listeners];
|
|
11
|
+
listeners.clear();
|
|
12
|
+
for (const listener of waiting) {
|
|
13
|
+
listener(revision);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
revision: Effect.sync(() => revision),
|
|
17
|
+
waitForChange: (after: number): Effect.Effect<number> =>
|
|
18
|
+
Effect.callback((resume) => {
|
|
19
|
+
if (revision !== after) {
|
|
20
|
+
resume(Effect.succeed(revision));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
function listener(current: number) {
|
|
24
|
+
resume(Effect.succeed(current));
|
|
25
|
+
}
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return Effect.sync(() => listeners.delete(listener));
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { PgBoss } from "pg-boss";
|
|
3
|
+
import type { PgBossPayloadError, PgBossTestingError } from "#error.ts";
|
|
4
|
+
import { decodeStoredData, quotedSchema } from "#internal/storedRows.ts";
|
|
5
|
+
import { testingOperation } from "#internal/testingReadout.ts";
|
|
6
|
+
import type { SnapshotQueue, UnfinishedJob } from "#testing.ts";
|
|
7
|
+
|
|
8
|
+
const Rows = Schema.Array(
|
|
9
|
+
Schema.Struct({
|
|
10
|
+
data: Schema.Unknown,
|
|
11
|
+
id: Schema.String,
|
|
12
|
+
queue: Schema.String,
|
|
13
|
+
startAfter: Schema.DateTimeUtcFromDate,
|
|
14
|
+
state: Schema.Literals(["created", "retry", "active", "failed"]),
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export function testingSnapshot(client: () => PgBoss, schema: () => string) {
|
|
19
|
+
return <const TQueues extends readonly SnapshotQueue[]>(
|
|
20
|
+
queues: TQueues,
|
|
21
|
+
): Effect.Effect<
|
|
22
|
+
readonly UnfinishedJob<TQueues[number]>[],
|
|
23
|
+
PgBossPayloadError | PgBossTestingError,
|
|
24
|
+
TQueues[number]["schema"]["DecodingServices"]
|
|
25
|
+
> => {
|
|
26
|
+
const byName = new Map<string, TQueues[number]>(queues.map((queue) => [queue.name, queue]));
|
|
27
|
+
return testingOperation("snapshot", async () => {
|
|
28
|
+
const quoted = quotedSchema(schema());
|
|
29
|
+
const { rows } = await client()
|
|
30
|
+
.getDb()
|
|
31
|
+
.executeSql(
|
|
32
|
+
`SELECT id, name AS queue, state, data, start_after AS "startAfter"
|
|
33
|
+
FROM ${quoted}.job
|
|
34
|
+
WHERE name = ANY($1::text[]) AND state IN ('created', 'retry', 'active', 'failed')
|
|
35
|
+
ORDER BY created_on, id`,
|
|
36
|
+
[[...byName.keys()]],
|
|
37
|
+
);
|
|
38
|
+
return Schema.decodeUnknownPromise(Rows)(rows);
|
|
39
|
+
}).pipe(
|
|
40
|
+
Effect.flatMap((rows) =>
|
|
41
|
+
Effect.forEach(rows, ({ data, queue: name, ...row }) => {
|
|
42
|
+
const queue = byName.get(name);
|
|
43
|
+
if (queue === undefined) {
|
|
44
|
+
return Effect.die(new Error(`The snapshot returned a job of an unrequested queue ${name}`));
|
|
45
|
+
}
|
|
46
|
+
return decodeStoredData<TQueues[number]["name"], TQueues[number]["schema"]>(queue, data).pipe(
|
|
47
|
+
Effect.map((decoded): UnfinishedJob<TQueues[number]> => ({ ...row, data: decoded, queue: queue.name })),
|
|
48
|
+
);
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect, Option, Schema, Tracer } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
|
|
4
|
+
const TracedPayload = Schema.Struct({
|
|
5
|
+
_tag: Schema.Literal("@shivaedev/jobs/TracedPayload"),
|
|
6
|
+
payload: Schema.Unknown,
|
|
7
|
+
span: Schema.Struct({ sampled: Schema.Boolean, spanId: Schema.String, traceId: Schema.String }),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const isTracedPayload = Schema.is(TracedPayload);
|
|
11
|
+
|
|
12
|
+
export function capturePayload(payload: object): Effect.Effect<object> {
|
|
13
|
+
return Effect.map(Effect.option(Effect.currentParentSpan), (span) =>
|
|
14
|
+
Option.isNone(span)
|
|
15
|
+
? payload
|
|
16
|
+
: ({
|
|
17
|
+
_tag: "@shivaedev/jobs/TracedPayload",
|
|
18
|
+
payload,
|
|
19
|
+
span: { sampled: span.value.sampled, spanId: span.value.spanId, traceId: span.value.traceId },
|
|
20
|
+
} satisfies typeof TracedPayload.Type),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function payloadOf(data: unknown): unknown {
|
|
25
|
+
return isTracedPayload(data) ? data.payload : data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function traceJob<A, E, R>(effect: Effect.Effect<A, E, R>, job: Job<unknown>, queue: string) {
|
|
29
|
+
return Effect.withSpan(effect, queue, {
|
|
30
|
+
attributes: {
|
|
31
|
+
"job.attempt": job.retryCount + 1,
|
|
32
|
+
"job.id": job.id,
|
|
33
|
+
"job.name": job.name,
|
|
34
|
+
"job.queue": queue,
|
|
35
|
+
"job.retry_count": job.retryCount,
|
|
36
|
+
"messaging.destination.name": queue,
|
|
37
|
+
"messaging.message.id": job.id,
|
|
38
|
+
"messaging.system": "pg-boss",
|
|
39
|
+
},
|
|
40
|
+
kind: "consumer",
|
|
41
|
+
links: isTracedPayload(job.data) ? [{ attributes: {}, span: Tracer.externalSpan(job.data.span) }] : [],
|
|
42
|
+
root: true,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
import type { Job } from "pg-boss";
|
|
3
|
+
import type { PgBossClient } from "#client.ts";
|
|
4
|
+
import { type PgBossError, toPgBossError } from "#error.ts";
|
|
5
|
+
import { deadlineOf } from "#internal/deadlineOf.ts";
|
|
6
|
+
|
|
7
|
+
export function waitForBatchExpiry(client: PgBossClient, name: string, jobs: readonly Job<unknown>[]): Effect.Effect<void, PgBossError> {
|
|
8
|
+
return Effect.gen(function* () {
|
|
9
|
+
const findJobs = client.findJobs?.bind(client);
|
|
10
|
+
if (findJobs === undefined) {
|
|
11
|
+
return yield* Effect.fail(new TypeError("This pg-boss client does not support reading interrupted claims"));
|
|
12
|
+
}
|
|
13
|
+
const deadline = Math.max(0, ...jobs.map(deadlineOf)) + 1000;
|
|
14
|
+
let delay = 25;
|
|
15
|
+
const poll = Effect.gen(function* () {
|
|
16
|
+
for (const job of jobs) {
|
|
17
|
+
while (true) {
|
|
18
|
+
const current = (yield* Effect.tryPromise({ catch: (error) => error, try: () => findJobs(name, { id: job.id }) }))[0];
|
|
19
|
+
if (current?.state !== "active" || current.retryCount !== job.retryCount) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
// Aborting the signal precedes pg-boss's failure write; returning early would complete this claim.
|
|
23
|
+
yield* Effect.sleep(delay);
|
|
24
|
+
delay = Math.min(delay * 2, 500);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return yield* poll.pipe(Effect.timeout(Math.max(0, deadline - (yield* Clock.currentTimeMillis))));
|
|
29
|
+
}).pipe(Effect.mapError((error) => toPgBossError("settle", error, name)));
|
|
30
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql";
|
|
3
|
+
import type { Connection } from "effect/sql/SqlConnection";
|
|
4
|
+
import { getConstructionPlans, getMigrationPlans, type MigrationPartition, PgBoss } from "pg-boss";
|
|
5
|
+
import pgBossPackage from "pg-boss/package.json" with { type: "json" };
|
|
6
|
+
import type { SchemaComponent } from "@shivaedev/sql/SchemaComponent.ts";
|
|
7
|
+
import { type PgBossError, toPgBossError } from "#error.ts";
|
|
8
|
+
import { deferralStorePlan } from "#internal/deferralStorePlan.ts";
|
|
9
|
+
import { runSchemaPlan } from "#internal/runSchemaPlan.ts";
|
|
10
|
+
import { resumeSchemaIndexes } from "#internal/schemaIndexBuilds.ts";
|
|
11
|
+
|
|
12
|
+
export interface PgBossSchemaOptions {
|
|
13
|
+
readonly schema?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function installSchema(
|
|
17
|
+
connection: Connection,
|
|
18
|
+
options: PgBossSchemaOptions,
|
|
19
|
+
run: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const schema = options.schema ?? "pgboss";
|
|
22
|
+
const construction = getConstructionPlans(schema);
|
|
23
|
+
const client = new PgBoss({
|
|
24
|
+
db: { executeSql: async (text, values = []) => ({ rows: Array.from(await run(connection.executeUnprepared(text, values, undefined))) }) },
|
|
25
|
+
schema,
|
|
26
|
+
});
|
|
27
|
+
if (!(await client.isInstalled())) {
|
|
28
|
+
await runSchemaPlan(connection, construction, run);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const version = await client.schemaVersion();
|
|
32
|
+
if (version === null) {
|
|
33
|
+
throw new Error("The pg-boss version table is empty");
|
|
34
|
+
}
|
|
35
|
+
if (version > pgBossPackage.pgboss.schema) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (version >= 27) {
|
|
39
|
+
await resumeSchemaIndexes(client, schema);
|
|
40
|
+
}
|
|
41
|
+
if (version < pgBossPackage.pgboss.schema) {
|
|
42
|
+
const partitionTables: MigrationPartition[] = Array.from(
|
|
43
|
+
await run(connection.executeUnprepared(`SELECT table_name AS "tableName", policy FROM ${schema}.queue WHERE partition = true`, [], undefined)),
|
|
44
|
+
);
|
|
45
|
+
await runSchemaPlan(connection, getMigrationPlans(schema, version, { partitionTables }), run, { schema, version: pgBossPackage.pgboss.schema });
|
|
46
|
+
await resumeSchemaIndexes(client, schema);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function installPgBossSchema(options: PgBossSchemaOptions = {}): Effect.Effect<void, PgBossError, SqlClient.SqlClient> {
|
|
51
|
+
return Effect.scoped(
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const sql = yield* SqlClient.SqlClient;
|
|
54
|
+
const connection = yield* sql.reserve;
|
|
55
|
+
const run = Effect.runPromiseWith(yield* Effect.context<never>());
|
|
56
|
+
yield* Effect.tryPromise({
|
|
57
|
+
catch: (error) => error,
|
|
58
|
+
try: async () => {
|
|
59
|
+
await installSchema(connection, options, run);
|
|
60
|
+
await run(connection.executeRaw(deferralStorePlan(options.schema ?? "pgboss"), []));
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}).pipe(Effect.mapError((error) => toPgBossError("install", error))),
|
|
64
|
+
).pipe(Effect.uninterruptible);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function pgBossSchema(options: PgBossSchemaOptions = {}): SchemaComponent {
|
|
68
|
+
const schema = options.schema ?? "pgboss";
|
|
69
|
+
return {
|
|
70
|
+
apply: installPgBossSchema(options),
|
|
71
|
+
name: `pg-boss:${schema}`,
|
|
72
|
+
pending: Effect.gen(function* () {
|
|
73
|
+
const sql = yield* SqlClient.SqlClient;
|
|
74
|
+
const table = sql`${sql(schema)}.${sql("version")}`;
|
|
75
|
+
const [installed] = yield* sql<{ name: string | null }>`select to_regclass(${table.compile()[0]})::text as name`.withoutTransform;
|
|
76
|
+
const [row] =
|
|
77
|
+
installed === undefined || installed.name === null ? [] : yield* sql<{ version: number }>`select version from ${table}`.withoutTransform;
|
|
78
|
+
const required = pgBossPackage.pgboss.schema;
|
|
79
|
+
return row === undefined || row.version < required
|
|
80
|
+
? `schema ${schema}, installed version ${row?.version ?? "missing"}, required version ${required}`
|
|
81
|
+
: undefined;
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}
|