@shivaedev/jobs 0.35.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +5 -85
  3. package/dist/server/adapters/inspectionChanges.d.ts.map +1 -1
  4. package/dist/server/adapters/inspectionChanges.js +17 -2
  5. package/dist/server/adapters/inspectionChanges.js.map +1 -1
  6. package/dist/server/adapters/listJobs.d.ts.map +1 -1
  7. package/dist/server/adapters/listJobs.js +17 -7
  8. package/dist/server/adapters/listJobs.js.map +1 -1
  9. package/dist/server/adapters/localInspectionChanges.d.ts +3 -0
  10. package/dist/server/adapters/localInspectionChanges.d.ts.map +1 -0
  11. package/dist/server/adapters/localInspectionChanges.js +2 -0
  12. package/dist/server/adapters/localInspectionChanges.js.map +1 -0
  13. package/dist/server/adapters/settleDeferral.d.ts.map +1 -1
  14. package/dist/server/adapters/settleDeferral.js +4 -2
  15. package/dist/server/adapters/settleDeferral.js.map +1 -1
  16. package/dist/server/error.d.ts +1 -20
  17. package/dist/server/error.d.ts.map +1 -1
  18. package/dist/server/error.js +1 -8
  19. package/dist/server/error.js.map +1 -1
  20. package/dist/server/internal/engine.d.ts +3 -7
  21. package/dist/server/internal/engine.d.ts.map +1 -1
  22. package/dist/server/internal/engine.js +0 -2
  23. package/dist/server/internal/engine.js.map +1 -1
  24. package/dist/server/internal/jobListings.d.ts.map +1 -1
  25. package/dist/server/internal/jobListings.js +13 -7
  26. package/dist/server/internal/jobListings.js.map +1 -1
  27. package/package.json +13 -10
  28. package/src/server/adapters/inspectionChanges.ts +17 -2
  29. package/src/server/adapters/listJobs.ts +18 -10
  30. package/src/server/adapters/localInspectionChanges.ts +3 -0
  31. package/src/server/adapters/settleDeferral.ts +4 -2
  32. package/src/server/error.ts +1 -17
  33. package/src/server/internal/engine.ts +3 -14
  34. package/src/server/internal/jobListings.ts +20 -9
  35. package/dist/server/adapters/expireAttempt.d.ts +0 -4
  36. package/dist/server/adapters/expireAttempt.d.ts.map +0 -1
  37. package/dist/server/adapters/expireAttempt.js +0 -17
  38. package/dist/server/adapters/expireAttempt.js.map +0 -1
  39. package/dist/server/adapters/testingClient.d.ts +0 -13
  40. package/dist/server/adapters/testingClient.d.ts.map +0 -1
  41. package/dist/server/adapters/testingClient.js +0 -101
  42. package/dist/server/adapters/testingClient.js.map +0 -1
  43. package/dist/server/adapters/testingController.d.ts +0 -80
  44. package/dist/server/adapters/testingController.d.ts.map +0 -1
  45. package/dist/server/adapters/testingController.js +0 -120
  46. package/dist/server/adapters/testingController.js.map +0 -1
  47. package/dist/server/adapters/testingDueNow.d.ts +0 -7
  48. package/dist/server/adapters/testingDueNow.d.ts.map +0 -1
  49. package/dist/server/adapters/testingDueNow.js +0 -26
  50. package/dist/server/adapters/testingDueNow.js.map +0 -1
  51. package/dist/server/adapters/testingEngine.d.ts +0 -5
  52. package/dist/server/adapters/testingEngine.d.ts.map +0 -1
  53. package/dist/server/adapters/testingEngine.js +0 -12
  54. package/dist/server/adapters/testingEngine.js.map +0 -1
  55. package/dist/server/adapters/testingExpirations.d.ts +0 -8
  56. package/dist/server/adapters/testingExpirations.d.ts.map +0 -1
  57. package/dist/server/adapters/testingExpirations.js +0 -27
  58. package/dist/server/adapters/testingExpirations.js.map +0 -1
  59. package/dist/server/adapters/testingInspectionClient.d.ts +0 -5
  60. package/dist/server/adapters/testingInspectionClient.d.ts.map +0 -1
  61. package/dist/server/adapters/testingInspectionClient.js +0 -52
  62. package/dist/server/adapters/testingInspectionClient.js.map +0 -1
  63. package/dist/server/adapters/testingTime.d.ts +0 -10
  64. package/dist/server/adapters/testingTime.d.ts.map +0 -1
  65. package/dist/server/adapters/testingTime.js +0 -18
  66. package/dist/server/adapters/testingTime.js.map +0 -1
  67. package/dist/server/internal/adapters/deferralChanges.d.ts +0 -3
  68. package/dist/server/internal/adapters/deferralChanges.d.ts.map +0 -1
  69. package/dist/server/internal/adapters/deferralChanges.js +0 -2
  70. package/dist/server/internal/adapters/deferralChanges.js.map +0 -1
  71. package/dist/server/internal/adapters/testingFetches.d.ts +0 -7
  72. package/dist/server/internal/adapters/testingFetches.d.ts.map +0 -1
  73. package/dist/server/internal/adapters/testingFetches.js +0 -30
  74. package/dist/server/internal/adapters/testingFetches.js.map +0 -1
  75. package/dist/server/internal/testingIdle.d.ts +0 -17
  76. package/dist/server/internal/testingIdle.d.ts.map +0 -1
  77. package/dist/server/internal/testingIdle.js +0 -18
  78. package/dist/server/internal/testingIdle.js.map +0 -1
  79. package/dist/server/internal/testingOperation.d.ts +0 -5
  80. package/dist/server/internal/testingOperation.d.ts.map +0 -1
  81. package/dist/server/internal/testingOperation.js +0 -12
  82. package/dist/server/internal/testingOperation.js.map +0 -1
  83. package/dist/server/internal/testingSchedules.d.ts +0 -14
  84. package/dist/server/internal/testingSchedules.d.ts.map +0 -1
  85. package/dist/server/internal/testingSchedules.js +0 -20
  86. package/dist/server/internal/testingSchedules.js.map +0 -1
  87. package/dist/server/internal/testingWaits.d.ts +0 -20
  88. package/dist/server/internal/testingWaits.d.ts.map +0 -1
  89. package/dist/server/internal/testingWaits.js +0 -53
  90. package/dist/server/internal/testingWaits.js.map +0 -1
  91. package/dist/server/testing.d.ts +0 -89
  92. package/dist/server/testing.d.ts.map +0 -1
  93. package/dist/server/testing.js +0 -12
  94. package/dist/server/testing.js.map +0 -1
  95. package/src/server/adapters/expireAttempt.ts +0 -22
  96. package/src/server/adapters/testingClient.ts +0 -123
  97. package/src/server/adapters/testingController.ts +0 -137
  98. package/src/server/adapters/testingDueNow.ts +0 -32
  99. package/src/server/adapters/testingEngine.ts +0 -15
  100. package/src/server/adapters/testingExpirations.ts +0 -33
  101. package/src/server/adapters/testingInspectionClient.ts +0 -70
  102. package/src/server/adapters/testingTime.ts +0 -25
  103. package/src/server/internal/adapters/deferralChanges.ts +0 -3
  104. package/src/server/internal/adapters/testingFetches.ts +0 -36
  105. package/src/server/internal/testingIdle.ts +0 -36
  106. package/src/server/internal/testingOperation.ts +0 -18
  107. package/src/server/internal/testingSchedules.ts +0 -35
  108. package/src/server/internal/testingWaits.ts +0 -94
  109. package/src/server/testing.ts +0 -31
@@ -1,12 +0,0 @@
1
- import { Effect } from "effect";
2
- import { testingController } from "#server/adapters/testingController.ts";
3
- export function makePgBossTesting(options = {}) {
4
- return testingController(options);
5
- }
6
- export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
7
- export function linkedWorkerSpans(sender, spans) {
8
- return Effect.sync(() => spans.filter((span) => span.kind === "consumer"
9
- && span.attributes.get("messaging.system") === "pg-boss"
10
- && span.links.some((link) => link.span.spanId === sender.spanId && link.span.traceId === sender.traceId)));
11
- }
12
- //# sourceMappingURL=testing.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/server/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,MAAM,EAAe,MAAM,QAAQ,CAAC;AAE5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAE1E,MAAM,UAAU,iBAAiB,CAAC,OAAO,GAAyB,EAAE;IACnE,OAAO,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAMD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;AAOrF,MAAM,UAAU,iBAAiB,CAAC,MAAkD,EAAE,KAA6B;IAClH,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CACvB,KAAK,CAAC,MAAM,CACX,CAAC,IAAI,EAAE,EAAE,CACR,IAAI,CAAC,IAAI,KAAK,UAAU;WACrB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,kBAAkB,CAAC,KAAK,SAAS;WACrD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,CAAC,CACzG,CACD,CAAC;AACH,CAAC"}
@@ -1,22 +0,0 @@
1
- import type { EngineClient } from "#server/engineClient.ts";
2
- import type { JobAttempt } from "#server/internal/engine.ts";
3
- import { quotedSchema } from "#server/internal/storedRows.ts";
4
-
5
- export async function expireAttempt(client: EngineClient, schema: string, job: JobAttempt, cause: "expiry" | "heartbeat" = "expiry"): Promise<void> {
6
- if ((await client.schemaVersion()) !== 46) {
7
- throw new Error("Testing expiry requires pg-boss schema version 46");
8
- }
9
- const quoted = quotedSchema(schema);
10
- const timestamp = cause === "expiry" ? "started_on" : "heartbeat_on";
11
- const seconds = cause === "expiry" ? "expire_seconds" : "heartbeat_seconds";
12
- await client.getDb().executeSql(
13
- `UPDATE ${quoted}.job SET ${timestamp} = ${quoted}.job_now() - (${seconds} + 1) * interval '1 second'
14
- WHERE name = $1 AND id = $2 AND retry_count = $3 AND state = 'active'`,
15
- [job.name, job.id, job.retryCount],
16
- );
17
- // Supervision owns expiry and dead lettering; make its claim due with the job's deadline.
18
- await client
19
- .getDb()
20
- .executeSql(`UPDATE ${quoted}.queue SET monitor_claim_on = ${quoted}.job_now() - interval '1 week' WHERE name = $1`, [job.name]);
21
- await client.supervise(job.name);
22
- }
@@ -1,123 +0,0 @@
1
- import { Effect } from "effect";
2
- import { testingEngine } from "#server/adapters/testingEngine.ts";
3
- import { testingExpirations } from "#server/adapters/testingExpirations.ts";
4
- import type { ClientUpdateOptions, ClientUpdateRequest, ClientUpdateResult, PgBossClient } from "#server/client.ts";
5
- import type { EngineClient } from "#server/engineClient.ts";
6
- import type { EnqueueOptions } from "#server/enqueueOptions.ts";
7
- import { deferralChanges } from "#server/internal/adapters/deferralChanges.ts";
8
- import type { TestingBatch, TestingWork } from "#server/internal/testingIdle.ts";
9
- import type { WorkerJob } from "#server/job.ts";
10
-
11
- export interface TestingControls {
12
- readonly changed: () => void;
13
- readonly expirations: Map<string, number>;
14
- readonly expirySeconds: Map<string, number>;
15
- readonly failures: Set<string>;
16
- readonly retryPolicy: Pick<EnqueueOptions, "retryBackoff" | "retryDelay">;
17
- readonly work: TestingWork;
18
- }
19
-
20
- export function testingClient(client: EngineClient, schema: string, controls: TestingControls): EngineClient {
21
- const engine = testingEngine(client, schema);
22
- deferralChanges.set(client.getDb(), controls.changed);
23
-
24
- const expirations = testingExpirations(client);
25
- function update(request: ClientUpdateRequest): Promise<ClientUpdateResult>;
26
- function update(name: string, data: object | null | undefined, options?: ClientUpdateOptions): Promise<ClientUpdateResult>;
27
- async function update(request: string | ClientUpdateRequest, data?: object | null, options?: ClientUpdateOptions): Promise<ClientUpdateResult> {
28
- const result = typeof request === "string" ? await client.update(request, data, options) : await client.update(request);
29
- controls.changed();
30
- return result;
31
- }
32
- const overrides: Partial<PgBossClient> = {
33
- complete: async (...args) => {
34
- const result = await client.complete(...args);
35
- controls.changed();
36
- return result;
37
- },
38
- createQueue: (name, options) => {
39
- if (options?.deadLetter !== undefined) {
40
- controls.work.queues.add(name);
41
- }
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
- update,
82
- updateQueue: (name, options) =>
83
- client.updateQueue(name, {
84
- ...options,
85
- ...controls.retryPolicy,
86
- ...(controls.retryPolicy.retryBackoff === false ? { retryDelayMax: null } : {}),
87
- }),
88
- work: async (name, options, handler) =>
89
- client.work(name, options, async (jobs: readonly WorkerJob<unknown>[]) => {
90
- const batch: TestingBatch = { jobs: new Map(jobs.map((job) => [job.id, job.retryCount])), name, settled: false };
91
- controls.work.batches.add(batch);
92
- controls.changed();
93
- try {
94
- const ready: WorkerJob<unknown>[] = [];
95
- for (const job of jobs) {
96
- const attempts = await expirations.attemptsFor(job);
97
- if (attempts !== undefined && job.retryCount + 1 >= attempts) {
98
- await Effect.runPromise(engine.expireAttempt(job));
99
- } else {
100
- ready.push(job);
101
- }
102
- }
103
- if (ready.length > 0) {
104
- await handler(ready);
105
- }
106
- } finally {
107
- controls.work.batches.delete(batch);
108
- controls.changed();
109
- }
110
- }),
111
- };
112
- const wrapped = new Proxy(client, {
113
- get(target, property) {
114
- if (Object.hasOwn(overrides, property)) {
115
- const override: unknown = Reflect.get(overrides, property);
116
- return override;
117
- }
118
- const value: unknown = Reflect.get(target, property);
119
- return typeof value === "function" ? value.bind(target) : value;
120
- },
121
- });
122
- return wrapped;
123
- }
@@ -1,137 +0,0 @@
1
- import { Duration, Effect, Redacted, type Schema } from "effect";
2
- import { TestClock as PgBossTestClock } from "pg-boss";
3
- import { defaultClientFactory } from "#server/adapters/defaultClientFactory.ts";
4
- import { testingClient } from "#server/adapters/testingClient.ts";
5
- import { testingDueNow } from "#server/adapters/testingDueNow.ts";
6
- import { testingTime } from "#server/adapters/testingTime.ts";
7
- import type { JobReadOptions, PgBossClientFactory } from "#server/client.ts";
8
- import type { JobPayloadSchema, QueueDefinition } from "#server/definition.ts";
9
- import type { EngineClient } from "#server/engineClient.ts";
10
- import { PgBossTestingError } from "#server/error.ts";
11
- import type { PgBossInspectionService, SnapshotQueue } from "#server/inspection.ts";
12
- import { type TestingFetches, testingFetches } from "#server/internal/adapters/testingFetches.ts";
13
- import { type TestingWork, testingIdle } from "#server/internal/testingIdle.ts";
14
- import { testingInspection, testingOperation } from "#server/internal/testingOperation.ts";
15
- import { testingSchedules } from "#server/internal/testingSchedules.ts";
16
- import { testingWaits } from "#server/internal/testingWaits.ts";
17
- import type { PgBossTestingOptions } from "#server/testing.ts";
18
- import { queueSignal } from "./queueSignal.ts";
19
- import { testingInspectionClient } from "./testingInspectionClient.ts";
20
- export function testingController(settings: PgBossTestingOptions = {}) {
21
- const clock = settings.clock === undefined ? undefined : new PgBossTestClock(settings.clock.currentTimeMillisUnsafe());
22
- let current: EngineClient | undefined;
23
- let fetches: TestingFetches | undefined;
24
- let schema = "pgboss";
25
- const signal = queueSignal();
26
- let observed: PgBossInspectionService | undefined;
27
- const work: TestingWork = { batches: new Set(), queues: new Set() };
28
- const controls = {
29
- changed: signal.changed,
30
- expirations: new Map<string, number>(),
31
- expirySeconds: new Map<string, number>(),
32
- failures: new Set<string>(),
33
- retryPolicy: settings.retryPolicy ?? {},
34
- work,
35
- };
36
- function client(): EngineClient {
37
- if (current === undefined) {
38
- throw new Error("Provide the testing clientFactory to a jobs Layer first");
39
- }
40
- return current;
41
- }
42
- function inspection(): PgBossInspectionService {
43
- if (observed === undefined) {
44
- throw new Error("Provide the testing clientFactory to a jobs Layer first");
45
- }
46
- return observed;
47
- }
48
- const clientFactory: PgBossClientFactory = (clientOptions) => {
49
- if (current !== undefined) {
50
- throw new Error("A testing controller owns one jobs client; create a controller per runtime");
51
- }
52
- schema = clientOptions.schema ?? "pgboss";
53
- current = defaultClientFactory({ ...clientOptions, ...(clock === undefined ? {} : { clock }) });
54
- fetches = testingFetches(current.getDb(), signal.changed);
55
- current = testingClient(current, schema, controls);
56
- return testingInspectionClient(current, schema, work, signal.changed, (reads) => {
57
- observed = reads;
58
- });
59
- };
60
- const wake = Effect.sync(() => {
61
- if (clock !== undefined) {
62
- for (const worker of client().getWipData()) {
63
- client().notifyWorker(worker.id);
64
- }
65
- }
66
- });
67
- const idle = testingIdle(inspection, work, signal, wake);
68
- return {
69
- deferred: (queue: Pick<QueueDefinition, "name">) => Effect.suspend(() => testingInspection("deferred", inspection().deferred(queue), queue.name)),
70
- ...testingDueNow(client, () => schema, signal.changed),
71
- ...testingSchedules(client, inspection),
72
- ...testingWaits(inspection, () => fetches, signal),
73
- checkBatchWindow: (queue: Pick<QueueDefinition, "name" | "workerOptions">, millisecondsPerJob: number) =>
74
- testingOperation(
75
- "checkBatchWindow",
76
- async () => {
77
- const stored = await Effect.runPromise(inspection().queue(queue));
78
- if (stored === null) {
79
- throw new Error("Queue is not registered");
80
- }
81
- if (stored.expireInSeconds === undefined) {
82
- throw new Error("Queue has no expiry window");
83
- }
84
- if (!Number.isFinite(millisecondsPerJob) || millisecondsPerJob < 0) {
85
- throw new RangeError("Job duration must be nonnegative and finite");
86
- }
87
- const required = (queue.workerOptions.batchSize ?? 10) * millisecondsPerJob;
88
- if (required >= stored.expireInSeconds * 1000) {
89
- throw new Error("Sequential batch work must fit within the queue expiry window");
90
- }
91
- },
92
- queue.name,
93
- ),
94
- checkSchemaVersion: (expected: number) =>
95
- testingOperation("checkSchemaVersion", async () => {
96
- if ((await Effect.runPromise(inspection().schemaVersion())) !== expected) {
97
- throw new Error(`Expected pg-boss schema version ${expected}`);
98
- }
99
- }),
100
- clientFactory,
101
- expireAfter: (queue: Pick<QueueDefinition, "name">, attempts: number) =>
102
- Effect.try({
103
- catch: (original) => new PgBossTestingError({ operation: "expireAfter", original: Redacted.make(original), queue: queue.name }),
104
- try: () => {
105
- if (!Number.isSafeInteger(attempts) || attempts < 1) {
106
- throw new RangeError("Attempts must be a positive integer");
107
- }
108
- controls.expirations.set(queue.name, attempts);
109
- },
110
- }),
111
- expireIn: (queue: Pick<QueueDefinition, "name">, duration: Duration.Input) =>
112
- Effect.try({
113
- catch: (original) => new PgBossTestingError({ operation: "expireIn", original: Redacted.make(original), queue: queue.name }),
114
- try: () => {
115
- const milliseconds = Duration.toMillis(Duration.fromInputUnsafe(duration));
116
- if (!Number.isFinite(milliseconds) || milliseconds < 1000) {
117
- throw new RangeError("Expiry must be finite and at least one second");
118
- }
119
- controls.expirySeconds.set(queue.name, Math.ceil(milliseconds / 1000));
120
- },
121
- }),
122
- failNextSend: (queue: Pick<QueueDefinition, "name">) => Effect.sync(() => controls.failures.add(queue.name)).pipe(Effect.asVoid),
123
- jobs: <TName extends string, TPayload extends JobPayloadSchema | typeof Schema.Null>(
124
- queue: { readonly name: TName; readonly schema: TPayload },
125
- readOptions?: JobReadOptions,
126
- ) => Effect.suspend(() => testingInspection("jobs", inspection().jobs(queue, readOptions), queue.name)),
127
- refreshQueueStats: (queue?: Pick<QueueDefinition, "name">) =>
128
- testingOperation("refreshQueueStats", () => client().supervise(queue?.name), queue?.name),
129
- revision: signal.revision,
130
- schemaVersion: Effect.suspend(() => testingInspection("schemaVersion", inspection().schemaVersion())),
131
- snapshot: <const TQueues extends readonly SnapshotQueue[]>(queues: TQueues) =>
132
- Effect.suspend(() => testingInspection("snapshot", inspection().snapshot(queues))),
133
- waitForChange: signal.waitForChange,
134
- waitForIdle: idle,
135
- ...testingTime(settings.clock, clock, idle),
136
- };
137
- }
@@ -1,32 +0,0 @@
1
- import type { QueueDefinition } from "#server/definition.ts";
2
- import type { EngineClient } from "#server/engineClient.ts";
3
- import { quotedSchema } from "#server/internal/storedRows.ts";
4
- import { testingOperation } from "#server/internal/testingOperation.ts";
5
-
6
- export function testingDueNow(client: () => EngineClient, schema: () => string, changed: () => void) {
7
- function makeDue(queue: Pick<QueueDefinition, "name">, deferredOnly: boolean) {
8
- return testingOperation(
9
- deferredOnly ? "runDeferredNow" : "runDueNow",
10
- async () => {
11
- const quoted = quotedSchema(schema());
12
- const condition = deferredOnly
13
- ? "state = 'retry' AND output ? 'jobDeferral' AND started_on IS NULL"
14
- : `state IN ('created', 'retry') AND start_after > ${quoted}.job_now()`;
15
- await client()
16
- .getDb()
17
- .executeSql(`UPDATE ${quoted}.job SET start_after = ${quoted}.job_now() WHERE name = $1 AND ${condition}`, [queue.name]);
18
- changed();
19
- for (const worker of client()
20
- .getWipData()
21
- .filter((candidate) => candidate.name === queue.name)) {
22
- client().notifyWorker(worker.id);
23
- }
24
- },
25
- queue.name,
26
- );
27
- }
28
- return {
29
- runDeferredNow: (queue: Pick<QueueDefinition, "name">) => makeDue(queue, true),
30
- runDueNow: (queue: Pick<QueueDefinition, "name">) => makeDue(queue, false),
31
- };
32
- }
@@ -1,15 +0,0 @@
1
- import { type Context, Effect, Redacted } from "effect";
2
- import type { EngineClient } from "#server/engineClient.ts";
3
- import { PgBossTestingError } from "#server/error.ts";
4
- import type { TestingEngine } from "#server/internal/engine.ts";
5
- import { expireAttempt } from "./expireAttempt.ts";
6
-
7
- export function testingEngine(client: EngineClient, schema: string): Context.Service.Shape<typeof TestingEngine> {
8
- return {
9
- expireAttempt: (job, cause) =>
10
- Effect.tryPromise({
11
- catch: (original) => new PgBossTestingError({ operation: "expireAttempt", original: Redacted.make(original), queue: job.name }),
12
- try: () => expireAttempt(client, schema, job, cause),
13
- }),
14
- };
15
- }
@@ -1,33 +0,0 @@
1
- import type { EngineClient } from "#server/engineClient.ts";
2
- import type { EnqueueOptions } from "#server/enqueueOptions.ts";
3
- import type { WorkerJob } from "#server/job.ts";
4
-
5
- export function testingExpirations(client: EngineClient) {
6
- const snapshots = new Map<string, Promise<number | undefined>>();
7
- return {
8
- attemptsFor: (job: WorkerJob<unknown>) => snapshots.get(`${job.name}:${job.id}`),
9
- send(name: string, data: object | null | undefined, options: EnqueueOptions, attempts: number | undefined) {
10
- if (attempts === undefined && options.id === undefined) {
11
- return client.send(name, data, options);
12
- }
13
- const id = options.id ?? crypto.randomUUID();
14
- const key = `${name}:${id}`;
15
- const previous = snapshots.get(key);
16
- const sending = client.send(name, data, { ...options, id });
17
- // A worker may claim the row before send returns. Wait for its result before applying the snapshot.
18
- const snapshot = sending
19
- .then(
20
- (sent) => (sent === id ? attempts : previous),
21
- () => previous,
22
- )
23
- .then((value) => {
24
- if (value === undefined && snapshots.get(key) === snapshot) {
25
- snapshots.delete(key);
26
- }
27
- return value;
28
- });
29
- snapshots.set(key, snapshot);
30
- return sending;
31
- },
32
- };
33
- }
@@ -1,70 +0,0 @@
1
- import { Context, Effect, Exit, Layer, Scope } from "effect";
2
- import type { EngineClient } from "#server/engineClient.ts";
3
- import { PgBossInspection, type PgBossInspectionService } from "#server/inspection.ts";
4
- import type { TestingWork } from "#server/internal/testingIdle.ts";
5
- import { PgBoss } from "#server/PgBoss.ts";
6
-
7
- const settleBatches = Effect.fnUntraced(function* (inspection: PgBossInspectionService, work: TestingWork) {
8
- for (const batch of work.batches) {
9
- if (batch.settled) {
10
- continue;
11
- }
12
- const stored = yield* inspection.storedJobs({ name: batch.name });
13
- batch.settled = !stored.some((job) => job.state === "active" && batch.jobs.get(job.id) === job.retryCount);
14
- }
15
- });
16
-
17
- export function testingInspectionClient(
18
- source: EngineClient,
19
- schema: string,
20
- work: TestingWork,
21
- changed: () => void,
22
- observe: (inspection: PgBossInspectionService) => void,
23
- ): EngineClient {
24
- const scope = Effect.runSync(Scope.make());
25
- const start = source.start.bind(source);
26
- const stop = source.stop.bind(source);
27
- async function startObserved() {
28
- const result = await start();
29
- const context = await Effect.runPromise(
30
- Layer.buildWithScope(
31
- PgBossInspection.layer.pipe(Layer.provide(Layer.succeed(PgBoss, { client: source, reused: false, schema, stop: undefined }))),
32
- scope,
33
- ),
34
- );
35
- const inspection = Context.get(context, PgBossInspection);
36
- observe(inspection);
37
- await Effect.runPromise(
38
- Effect.gen(function* () {
39
- const changes = yield* inspection.changes();
40
- const from = yield* changes.revision;
41
- yield* Effect.forkScoped(
42
- Effect.gen(function* () {
43
- let revision = from;
44
- while (true) {
45
- revision = yield* changes.waitForChange(revision);
46
- yield* settleBatches(inspection, work);
47
- changed();
48
- }
49
- }),
50
- );
51
- }).pipe(Effect.provideService(Scope.Scope, scope)),
52
- );
53
- return result;
54
- }
55
- const stopObserved: EngineClient["stop"] = async (options) => {
56
- await Promise.all([Effect.runPromise(Scope.close(scope, Exit.void)), stop(options)]);
57
- };
58
- return new Proxy(source, {
59
- get(target, property) {
60
- if (property === "start") {
61
- return startObserved;
62
- }
63
- if (property === "stop") {
64
- return stopObserved;
65
- }
66
- const value: unknown = Reflect.get(target, property);
67
- return typeof value === "function" ? value.bind(target) : value;
68
- },
69
- });
70
- }
@@ -1,25 +0,0 @@
1
- import { Duration, Effect } from "effect";
2
- import type { TestClock } from "effect/testing";
3
- import type { PgBossTestingError } from "#server/error.ts";
4
- import { testingOperation } from "#server/internal/testingOperation.ts";
5
-
6
- export function testingTime(
7
- clock: TestClock.TestClock | undefined,
8
- jobs: { readonly setTime: (time: number) => Promise<void>; readonly tick: (milliseconds: number) => Promise<void> } | undefined,
9
- idle: Effect.Effect<void, PgBossTestingError>,
10
- ) {
11
- return {
12
- timePasses: (duration: Duration.Input) =>
13
- Effect.gen(function* () {
14
- if (clock === undefined || jobs === undefined) {
15
- return yield* Effect.die(new Error("Provide the story's TestClock to makePgBossTesting before passing time"));
16
- }
17
- yield* idle;
18
- const next = clock.currentTimeMillisUnsafe() + Duration.toMillis(Duration.fromInputUnsafe(duration));
19
- yield* testingOperation("timePasses", () => jobs.setTime(next));
20
- yield* clock.adjust(duration);
21
- yield* testingOperation("timePasses", () => jobs.tick(0));
22
- yield* idle;
23
- }),
24
- };
25
- }
@@ -1,3 +0,0 @@
1
- import type { ClientDatabase } from "#server/ClientDatabase.ts";
2
-
3
- export const deferralChanges = new WeakMap<ClientDatabase, () => void>();
@@ -1,36 +0,0 @@
1
- import type { ClientDatabase } from "#server/ClientDatabase.ts";
2
-
3
- // pg-boss offers no hook for a worker fetch that finds nothing, so the controller recognizes its claim statement on the client's database.
4
- const claim = /UPDATE\s+\S+\s+j\s+SET\s+state\s*=\s*'active'/u;
5
- const claimedQueue = /\bj\.name\s*=\s*'(?<queue>[^']+)'/u;
6
-
7
- export interface TestingFetches {
8
- readonly begun: (queue: string) => number;
9
- readonly passedSince: (queue: string, begun: number) => number;
10
- }
11
-
12
- export function testingFetches(db: ClientDatabase, changed: () => void): TestingFetches {
13
- const begun = new Map<string, number>();
14
- const passed = new Map<string, number[]>();
15
- const executeSql = db.executeSql.bind(db);
16
- db.executeSql = async (text: string, values?: unknown[]) => {
17
- const queue = claim.test(text) ? claimedQueue.exec(text)?.groups?.queue : undefined;
18
- if (queue === undefined) {
19
- return executeSql(text, values);
20
- }
21
- const ordinal = (begun.get(queue) ?? 0) + 1;
22
- begun.set(queue, ordinal);
23
- try {
24
- return await executeSql(text, values);
25
- } finally {
26
- const ordinals = passed.get(queue) ?? [];
27
- ordinals.push(ordinal);
28
- passed.set(queue, ordinals);
29
- changed();
30
- }
31
- };
32
- return {
33
- begun: (queue) => begun.get(queue) ?? 0,
34
- passedSince: (queue, mark) => (passed.get(queue) ?? []).filter((ordinal) => ordinal > mark).length,
35
- };
36
- }
@@ -1,36 +0,0 @@
1
- import { Effect } from "effect";
2
- import type { PgBossTestingError } from "#server/error.ts";
3
- import type { PgBossInspectionService } from "#server/inspection.ts";
4
- import { testingInspection } from "#server/internal/testingOperation.ts";
5
-
6
- export interface TestingBatch {
7
- readonly jobs: ReadonlyMap<string, number>;
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
- export function testingIdle(
18
- inspection: () => PgBossInspectionService,
19
- work: TestingWork,
20
- signal: { readonly revision: Effect.Effect<number>; readonly waitForChange: (after: number) => Effect.Effect<number> },
21
- wake: Effect.Effect<void> = Effect.void,
22
- ): Effect.Effect<void, PgBossTestingError> {
23
- const busy = Effect.suspend(() => testingInspection("waitForIdle", inspection().busy([...work.queues].map((name) => ({ name })))));
24
- return Effect.gen(function* () {
25
- while (true) {
26
- const revision = yield* signal.revision;
27
- // A batch with no settlement since its fetch still holds active jobs, so it is busy without a read.
28
- const unsettled = [...work.batches].some((batch) => !batch.settled);
29
- if (work.queues.size === 0 || !(unsettled || (yield* busy))) {
30
- return;
31
- }
32
- yield* wake;
33
- yield* signal.waitForChange(revision);
34
- }
35
- });
36
- }
@@ -1,18 +0,0 @@
1
- import { Effect, Redacted } from "effect";
2
- import { PgBossError, PgBossTestingError } from "#server/error.ts";
3
-
4
- export function testingOperation<A>(operation: string, run: () => Promise<A>, queue?: string) {
5
- return Effect.tryPromise({
6
- catch: (original) => new PgBossTestingError({ operation, original: Redacted.make(original), queue }),
7
- try: run,
8
- });
9
- }
10
-
11
- export function testingInspection<A, E, R>(operation: string, effect: Effect.Effect<A, E | PgBossError, R>, queue?: string) {
12
- return effect.pipe(
13
- Effect.catchIf(
14
- (error): error is PgBossError => error instanceof PgBossError,
15
- (error) => Effect.fail(new PgBossTestingError({ operation, original: error.original, queue: queue ?? error.queue })),
16
- ),
17
- );
18
- }
@@ -1,35 +0,0 @@
1
- import { Effect, Redacted } from "effect";
2
- import type { PgBossClient } from "#server/client.ts";
3
- import type { ScheduleDefinition } from "#server/definition.ts";
4
- import { PgBossTestingError } from "#server/error.ts";
5
- import type { PgBossInspectionService } from "#server/inspection.ts";
6
- import { testingInspection, testingOperation } from "#server/internal/testingOperation.ts";
7
-
8
- export function testingSchedules(client: () => PgBossClient, inspection: () => PgBossInspectionService) {
9
- return {
10
- schedules: () =>
11
- Effect.suspend(() => testingInspection("schedules", inspection().schedules())).pipe(
12
- Effect.map((schedules) => schedules.map(({ name, cron, timezone }) => ({ cron, name, timezone }))),
13
- ),
14
- trigger: (schedule: Pick<ScheduleDefinition, "name" | "scheduleOptions">) =>
15
- Effect.gen(function* () {
16
- const [registered] = yield* testingInspection(
17
- "trigger",
18
- inspection().schedules(schedule.name, schedule.scheduleOptions.key ?? ""),
19
- schedule.name,
20
- );
21
- if (registered === undefined) {
22
- return yield* new PgBossTestingError({
23
- operation: "trigger",
24
- original: Redacted.make(new Error("Schedule is not registered")),
25
- queue: schedule.name,
26
- });
27
- }
28
- return yield* testingOperation(
29
- "trigger",
30
- () => client().send(registered.name, registered.data, { ...registered.options, startAfter: 0 }),
31
- schedule.name,
32
- );
33
- }),
34
- };
35
- }