@shivaedev/effect-pg-boss 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -2
  3. package/dist/server/client.d.ts +1 -2
  4. package/dist/server/client.d.ts.map +1 -1
  5. package/dist/server/client.js +2 -2
  6. package/dist/server/client.js.map +1 -1
  7. package/dist/server/internal/countsFor.d.ts.map +1 -1
  8. package/dist/server/internal/countsFor.js +3 -2
  9. package/dist/server/internal/countsFor.js.map +1 -1
  10. package/dist/server/internal/deferralStorePlan.d.ts.map +1 -1
  11. package/dist/server/internal/deferralStorePlan.js +3 -2
  12. package/dist/server/internal/deferralStorePlan.js.map +1 -1
  13. package/dist/server/internal/expireAttempt.d.ts.map +1 -1
  14. package/dist/server/internal/expireAttempt.js +2 -1
  15. package/dist/server/internal/expireAttempt.js.map +1 -1
  16. package/dist/server/internal/jobControl.d.ts.map +1 -1
  17. package/dist/server/internal/jobControl.js +4 -4
  18. package/dist/server/internal/jobControl.js.map +1 -1
  19. package/dist/server/internal/listJobs.js +6 -6
  20. package/dist/server/internal/listJobs.js.map +1 -1
  21. package/dist/server/internal/nativeClient.d.ts +3 -0
  22. package/dist/server/internal/nativeClient.d.ts.map +1 -0
  23. package/dist/server/internal/nativeClient.js +28 -0
  24. package/dist/server/internal/nativeClient.js.map +1 -0
  25. package/dist/server/internal/observeTestingSettlements.d.ts +1 -1
  26. package/dist/server/internal/observeTestingSettlements.d.ts.map +1 -1
  27. package/dist/server/internal/observeTestingSettlements.js +2 -2
  28. package/dist/server/internal/observeTestingSettlements.js.map +1 -1
  29. package/dist/server/internal/settleDeferral.d.ts.map +1 -1
  30. package/dist/server/internal/settleDeferral.js +3 -2
  31. package/dist/server/internal/settleDeferral.js.map +1 -1
  32. package/dist/server/internal/storedRows.d.ts +6 -0
  33. package/dist/server/internal/storedRows.d.ts.map +1 -0
  34. package/dist/server/internal/storedRows.js +10 -0
  35. package/dist/server/internal/storedRows.js.map +1 -0
  36. package/dist/server/internal/testingClient.d.ts +2 -0
  37. package/dist/server/internal/testingClient.d.ts.map +1 -1
  38. package/dist/server/internal/testingClient.js +12 -1
  39. package/dist/server/internal/testingClient.js.map +1 -1
  40. package/dist/server/internal/testingDeferrals.d.ts.map +1 -1
  41. package/dist/server/internal/testingDeferrals.js +3 -2
  42. package/dist/server/internal/testingDeferrals.js.map +1 -1
  43. package/dist/server/internal/testingIdle.d.ts +16 -0
  44. package/dist/server/internal/testingIdle.d.ts.map +1 -0
  45. package/dist/server/internal/testingIdle.js +31 -0
  46. package/dist/server/internal/testingIdle.js.map +1 -0
  47. package/dist/server/internal/testingReadout.d.ts +2 -2
  48. package/dist/server/internal/testingReadout.d.ts.map +1 -1
  49. package/dist/server/internal/testingReadout.js +4 -4
  50. package/dist/server/internal/testingReadout.js.map +1 -1
  51. package/dist/server/internal/testingSignal.d.ts.map +1 -1
  52. package/dist/server/internal/testingSignal.js +4 -2
  53. package/dist/server/internal/testingSignal.js.map +1 -1
  54. package/dist/server/internal/testingSnapshot.d.ts +6 -0
  55. package/dist/server/internal/testingSnapshot.d.ts.map +1 -0
  56. package/dist/server/internal/testingSnapshot.js +32 -0
  57. package/dist/server/internal/testingSnapshot.js.map +1 -0
  58. package/dist/server/testing.d.ts +14 -4
  59. package/dist/server/testing.d.ts.map +1 -1
  60. package/dist/server/testing.js +8 -2
  61. package/dist/server/testing.js.map +1 -1
  62. package/package.json +4 -4
  63. package/src/server/client.ts +3 -2
  64. package/src/server/internal/countsFor.ts +3 -2
  65. package/src/server/internal/deferralStorePlan.ts +3 -2
  66. package/src/server/internal/expireAttempt.ts +2 -1
  67. package/src/server/internal/jobControl.ts +4 -7
  68. package/src/server/internal/listJobs.ts +6 -7
  69. package/src/server/internal/nativeClient.ts +30 -0
  70. package/src/server/internal/observeTestingSettlements.ts +2 -2
  71. package/src/server/internal/settleDeferral.ts +3 -2
  72. package/src/server/internal/storedRows.ts +17 -0
  73. package/src/server/internal/testingClient.ts +14 -1
  74. package/src/server/internal/testingDeferrals.ts +3 -2
  75. package/src/server/internal/testingIdle.ts +52 -0
  76. package/src/server/internal/testingReadout.ts +4 -11
  77. package/src/server/internal/testingSignal.ts +4 -2
  78. package/src/server/internal/testingSnapshot.ts +53 -0
  79. package/src/server/testing.ts +21 -4
@@ -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
+ `/* effect-pg-boss 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
+ }
@@ -1,8 +1,8 @@
1
- import { Effect, Redacted, Schema } from "effect";
1
+ import { Effect, Redacted } from "effect";
2
2
  import type { FindJobsOptions, PgBoss } from "pg-boss";
3
3
  import type { JobPayloadSchema, QueueDefinition } from "#definition.ts";
4
- import { PgBossPayloadError, PgBossTestingError } from "#error.ts";
5
- import { payloadOf } from "#internal/tracing.ts";
4
+ import { PgBossTestingError } from "#error.ts";
5
+ import { decodeStoredData } from "#internal/storedRows.ts";
6
6
 
7
7
  export function testingOperation<A>(operation: string, run: () => Promise<A>, queue?: string) {
8
8
  return Effect.tryPromise({
@@ -14,13 +14,6 @@ export function testingOperation<A>(operation: string, run: () => Promise<A>, qu
14
14
  export function testingReadout(client: () => PgBoss) {
15
15
  return <TName extends string, TPayload extends JobPayloadSchema>(queue: QueueDefinition<TName, TPayload>, options?: FindJobsOptions) =>
16
16
  testingOperation("jobs", () => client().findJobs<unknown>(queue.name, options), queue.name).pipe(
17
- Effect.flatMap((jobs) =>
18
- Effect.forEach(jobs, (job) =>
19
- Schema.decodeUnknownEffect(queue.schema)(payloadOf(job.data)).pipe(
20
- Effect.mapError((original) => new PgBossPayloadError({ direction: "decode", original: Redacted.make(original), queue: queue.name })),
21
- Effect.map((data) => ({ ...job, data })),
22
- ),
23
- ),
24
- ),
17
+ Effect.flatMap((jobs) => Effect.forEach(jobs, (job) => decodeStoredData(queue, job.data).pipe(Effect.map((data) => ({ ...job, data }))))),
25
18
  );
26
19
  }
@@ -6,10 +6,12 @@ export function testingSignal() {
6
6
  return {
7
7
  changed() {
8
8
  revision += 1;
9
- for (const listener of listeners) {
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) {
10
13
  listener(revision);
11
14
  }
12
- listeners.clear();
13
15
  },
14
16
  revision: Effect.sync(() => revision),
15
17
  waitForChange: (after: number): Effect.Effect<number> =>
@@ -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
+ }
@@ -1,12 +1,25 @@
1
- import { Effect, Redacted } from "effect";
2
- import { PgBoss } from "pg-boss";
1
+ import { type DateTime, Effect, Redacted } from "effect";
2
+ import type { PgBoss } from "pg-boss";
3
3
  import type { PgBossClientFactory } from "#client.ts";
4
- import type { QueueDefinition } from "#definition.ts";
4
+ import type { JobPayloadSchema, QueueDefinition } from "#definition.ts";
5
5
  import { PgBossTestingError } from "#error.ts";
6
+ import { nativeClient } from "#internal/nativeClient.ts";
6
7
  import { testingClient } from "#internal/testingClient.ts";
7
8
  import { testingDeferrals } from "#internal/testingDeferrals.ts";
9
+ import { type TestingWork, testingIdle } from "#internal/testingIdle.ts";
8
10
  import { testingOperation, testingReadout } from "#internal/testingReadout.ts";
9
11
  import { testingSignal } from "#internal/testingSignal.ts";
12
+ import { testingSnapshot } from "#internal/testingSnapshot.ts";
13
+
14
+ export type SnapshotQueue = Pick<QueueDefinition<string, JobPayloadSchema>, "name" | "schema">;
15
+
16
+ export interface UnfinishedJob<TQueue extends SnapshotQueue> {
17
+ readonly data: TQueue["schema"]["Type"];
18
+ readonly id: string;
19
+ readonly queue: TQueue["name"];
20
+ readonly startAfter: DateTime.Utc;
21
+ readonly state: "active" | "created" | "failed" | "retry";
22
+ }
10
23
 
11
24
  export const testRetryPolicy = Object.freeze({ retryBackoff: false, retryDelay: 0 });
12
25
 
@@ -18,11 +31,13 @@ export function makePgBossTesting(options: PgBossTestingOptions = {}) {
18
31
  let current: PgBoss | undefined;
19
32
  let schema = "pgboss";
20
33
  const signal = testingSignal();
34
+ const work: TestingWork = { batches: new Set(), queues: new Set() };
21
35
  const controls = {
22
36
  changed: signal.changed,
23
37
  expirations: new Map<string, number>(),
24
38
  failures: new Set<string>(),
25
39
  retryPolicy: options.retryPolicy ?? {},
40
+ work,
26
41
  };
27
42
  function client(): PgBoss {
28
43
  if (current === undefined) {
@@ -35,7 +50,7 @@ export function makePgBossTesting(options: PgBossTestingOptions = {}) {
35
50
  throw new Error("A testing controller owns one jobs client; create a controller per runtime");
36
51
  }
37
52
  schema = clientOptions.schema ?? "pgboss";
38
- current = new PgBoss({ ...clientOptions, "__test__enableSpies": true });
53
+ current = nativeClient({ ...clientOptions, "__test__enableSpies": true });
39
54
  return testingClient(current, clientOptions.schema ?? "pgboss", controls);
40
55
  };
41
56
  return {
@@ -82,7 +97,9 @@ export function makePgBossTesting(options: PgBossTestingOptions = {}) {
82
97
  jobs: testingReadout(client),
83
98
  revision: signal.revision,
84
99
  schemaVersion: testingOperation("schemaVersion", () => client().schemaVersion()),
100
+ snapshot: testingSnapshot(client, () => schema),
85
101
  waitForChange: signal.waitForChange,
102
+ waitForIdle: testingIdle(client, () => schema, work, signal),
86
103
  };
87
104
  }
88
105