@shivaedev/jobs 0.31.0 → 0.32.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 (191) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +57 -26
  3. package/dist/server/adapters/inspection.d.ts +5 -0
  4. package/dist/server/adapters/inspection.d.ts.map +1 -0
  5. package/dist/server/adapters/inspection.js +25 -0
  6. package/dist/server/adapters/inspection.js.map +1 -0
  7. package/dist/server/adapters/inspectionChanges.d.ts +7 -0
  8. package/dist/server/adapters/inspectionChanges.d.ts.map +1 -0
  9. package/dist/server/adapters/inspectionChanges.js +23 -0
  10. package/dist/server/adapters/inspectionChanges.js.map +1 -0
  11. package/dist/server/adapters/inspectionDeferrals.d.ts +10 -0
  12. package/dist/server/adapters/inspectionDeferrals.d.ts.map +1 -0
  13. package/dist/server/adapters/inspectionDeferrals.js +13 -0
  14. package/dist/server/adapters/inspectionDeferrals.js.map +1 -0
  15. package/dist/server/adapters/inspectionJobs.d.ts +50 -0
  16. package/dist/server/adapters/inspectionJobs.d.ts.map +1 -0
  17. package/dist/server/adapters/inspectionJobs.js +21 -0
  18. package/dist/server/adapters/inspectionJobs.js.map +1 -0
  19. package/dist/server/adapters/inspectionOperation.d.ts +3 -0
  20. package/dist/server/adapters/inspectionOperation.d.ts.map +1 -0
  21. package/dist/server/adapters/inspectionOperation.js +6 -0
  22. package/dist/server/adapters/inspectionOperation.js.map +1 -0
  23. package/dist/server/adapters/inspectionPlan.d.ts +3 -0
  24. package/dist/server/adapters/inspectionPlan.d.ts.map +1 -0
  25. package/dist/server/adapters/inspectionPlan.js +20 -0
  26. package/dist/server/adapters/inspectionPlan.js.map +1 -0
  27. package/dist/server/adapters/inspectionReads.d.ts +59 -0
  28. package/dist/server/adapters/inspectionReads.d.ts.map +1 -0
  29. package/dist/server/adapters/inspectionReads.js +44 -0
  30. package/dist/server/adapters/inspectionReads.js.map +1 -0
  31. package/dist/server/adapters/inspectionSnapshot.d.ts +6 -0
  32. package/dist/server/adapters/inspectionSnapshot.d.ts.map +1 -0
  33. package/dist/server/{internal/testingSnapshot.js → adapters/inspectionSnapshot.js} +9 -7
  34. package/dist/server/adapters/inspectionSnapshot.js.map +1 -0
  35. package/dist/server/adapters/jobEngine.d.ts.map +1 -1
  36. package/dist/server/adapters/jobEngine.js +0 -4
  37. package/dist/server/adapters/jobEngine.js.map +1 -1
  38. package/dist/server/adapters/payloadIdentity.d.ts +3 -0
  39. package/dist/server/adapters/payloadIdentity.d.ts.map +1 -0
  40. package/dist/server/adapters/payloadIdentity.js +14 -0
  41. package/dist/server/adapters/payloadIdentity.js.map +1 -0
  42. package/dist/server/{internal/testingSignal.d.ts → adapters/queueSignal.d.ts} +2 -2
  43. package/dist/server/adapters/queueSignal.d.ts.map +1 -0
  44. package/dist/server/{internal/testingSignal.js → adapters/queueSignal.js} +2 -2
  45. package/dist/server/adapters/queueSignal.js.map +1 -0
  46. package/dist/server/adapters/testingClient.d.ts +1 -2
  47. package/dist/server/adapters/testingClient.d.ts.map +1 -1
  48. package/dist/server/adapters/testingClient.js +23 -45
  49. package/dist/server/adapters/testingClient.js.map +1 -1
  50. package/dist/server/adapters/testingController.d.ts +57 -12
  51. package/dist/server/adapters/testingController.d.ts.map +1 -1
  52. package/dist/server/adapters/testingController.js +28 -18
  53. package/dist/server/adapters/testingController.js.map +1 -1
  54. package/dist/server/adapters/testingDueNow.js +1 -1
  55. package/dist/server/adapters/testingDueNow.js.map +1 -1
  56. package/dist/server/adapters/testingEngine.d.ts.map +1 -1
  57. package/dist/server/adapters/testingEngine.js +0 -11
  58. package/dist/server/adapters/testingEngine.js.map +1 -1
  59. package/dist/server/adapters/testingInspectionClient.d.ts +5 -0
  60. package/dist/server/adapters/testingInspectionClient.d.ts.map +1 -0
  61. package/dist/server/adapters/testingInspectionClient.js +52 -0
  62. package/dist/server/adapters/testingInspectionClient.js.map +1 -0
  63. package/dist/server/client.d.ts +3 -0
  64. package/dist/server/client.d.ts.map +1 -1
  65. package/dist/server/clientOptions.d.ts +0 -1
  66. package/dist/server/clientOptions.d.ts.map +1 -1
  67. package/dist/server/definition.d.ts +12 -11
  68. package/dist/server/definition.d.ts.map +1 -1
  69. package/dist/server/definition.js +2 -1
  70. package/dist/server/definition.js.map +1 -1
  71. package/dist/server/engineClient.d.ts +0 -5
  72. package/dist/server/engineClient.d.ts.map +1 -1
  73. package/dist/server/enqueueOptions.d.ts +6 -0
  74. package/dist/server/enqueueOptions.d.ts.map +1 -1
  75. package/dist/server/error.d.ts +1 -1
  76. package/dist/server/error.d.ts.map +1 -1
  77. package/dist/server/error.js.map +1 -1
  78. package/dist/server/inspection.d.ts +102 -0
  79. package/dist/server/inspection.d.ts.map +1 -0
  80. package/dist/server/inspection.js +30 -0
  81. package/dist/server/inspection.js.map +1 -0
  82. package/dist/server/internal/adapters/testingFetches.d.ts +2 -2
  83. package/dist/server/internal/adapters/testingFetches.d.ts.map +1 -1
  84. package/dist/server/internal/adapters/testingFetches.js.map +1 -1
  85. package/dist/server/internal/deferral.d.ts +0 -2
  86. package/dist/server/internal/deferral.d.ts.map +1 -1
  87. package/dist/server/internal/deferral.js +0 -1
  88. package/dist/server/internal/deferral.js.map +1 -1
  89. package/dist/server/internal/engine.d.ts +0 -5
  90. package/dist/server/internal/engine.d.ts.map +1 -1
  91. package/dist/server/internal/engine.js.map +1 -1
  92. package/dist/server/internal/enqueueFor.d.ts.map +1 -1
  93. package/dist/server/internal/enqueueFor.js +11 -1
  94. package/dist/server/internal/enqueueFor.js.map +1 -1
  95. package/dist/server/internal/jobStorePlans.d.ts.map +1 -1
  96. package/dist/server/internal/jobStorePlans.js +2 -1
  97. package/dist/server/internal/jobStorePlans.js.map +1 -1
  98. package/dist/server/internal/makeService.d.ts.map +1 -1
  99. package/dist/server/internal/makeService.js +7 -6
  100. package/dist/server/internal/makeService.js.map +1 -1
  101. package/dist/server/internal/register.d.ts.map +1 -1
  102. package/dist/server/internal/register.js +0 -4
  103. package/dist/server/internal/register.js.map +1 -1
  104. package/dist/server/internal/testingIdle.d.ts +3 -2
  105. package/dist/server/internal/testingIdle.d.ts.map +1 -1
  106. package/dist/server/internal/testingIdle.js +5 -25
  107. package/dist/server/internal/testingIdle.js.map +1 -1
  108. package/dist/server/internal/testingOperation.d.ts +5 -0
  109. package/dist/server/internal/testingOperation.d.ts.map +1 -0
  110. package/dist/server/internal/testingOperation.js +12 -0
  111. package/dist/server/internal/testingOperation.js.map +1 -0
  112. package/dist/server/internal/testingSchedules.d.ts +8 -5
  113. package/dist/server/internal/testingSchedules.d.ts.map +1 -1
  114. package/dist/server/internal/testingSchedules.js +14 -9
  115. package/dist/server/internal/testingSchedules.js.map +1 -1
  116. package/dist/server/internal/testingWaits.d.ts +7 -4
  117. package/dist/server/internal/testingWaits.d.ts.map +1 -1
  118. package/dist/server/internal/testingWaits.js +5 -6
  119. package/dist/server/internal/testingWaits.js.map +1 -1
  120. package/dist/server/schema.d.ts.map +1 -1
  121. package/dist/server/schema.js +5 -0
  122. package/dist/server/schema.js.map +1 -1
  123. package/dist/server/service.d.ts +4 -4
  124. package/dist/server/service.d.ts.map +1 -1
  125. package/dist/server/service.js +1 -0
  126. package/dist/server/service.js.map +1 -1
  127. package/dist/server/testing.d.ts +60 -25
  128. package/dist/server/testing.d.ts.map +1 -1
  129. package/dist/server/testing.js.map +1 -1
  130. package/package.json +6 -6
  131. package/src/server/adapters/inspection.ts +31 -0
  132. package/src/server/adapters/inspectionChanges.ts +31 -0
  133. package/src/server/{internal/testingDeferrals.ts → adapters/inspectionDeferrals.ts} +10 -12
  134. package/src/server/adapters/inspectionJobs.ts +28 -0
  135. package/src/server/adapters/inspectionOperation.ts +6 -0
  136. package/src/server/adapters/inspectionPlan.ts +21 -0
  137. package/src/server/adapters/inspectionReads.ts +55 -0
  138. package/src/server/{internal/testingSnapshot.ts → adapters/inspectionSnapshot.ts} +15 -17
  139. package/src/server/adapters/jobEngine.ts +0 -4
  140. package/src/server/adapters/payloadIdentity.ts +15 -0
  141. package/src/server/{internal/testingSignal.ts → adapters/queueSignal.ts} +1 -1
  142. package/src/server/adapters/testingClient.ts +9 -30
  143. package/src/server/adapters/testingController.ts +37 -21
  144. package/src/server/adapters/testingDueNow.ts +1 -1
  145. package/src/server/adapters/testingEngine.ts +0 -13
  146. package/src/server/adapters/testingInspectionClient.ts +70 -0
  147. package/src/server/client.ts +3 -0
  148. package/src/server/clientOptions.ts +0 -1
  149. package/src/server/definition.ts +18 -12
  150. package/src/server/engineClient.ts +0 -2
  151. package/src/server/enqueueOptions.ts +5 -0
  152. package/src/server/error.ts +1 -0
  153. package/src/server/inspection.ts +50 -0
  154. package/src/server/internal/adapters/testingFetches.ts +2 -2
  155. package/src/server/internal/deferral.ts +0 -2
  156. package/src/server/internal/engine.ts +0 -6
  157. package/src/server/internal/enqueueFor.ts +14 -1
  158. package/src/server/internal/jobStorePlans.ts +2 -1
  159. package/src/server/internal/makeService.ts +7 -6
  160. package/src/server/internal/register.ts +0 -4
  161. package/src/server/internal/testingIdle.ts +7 -30
  162. package/src/server/internal/testingOperation.ts +18 -0
  163. package/src/server/internal/testingSchedules.ts +28 -16
  164. package/src/server/internal/testingWaits.ts +10 -11
  165. package/src/server/schema.ts +5 -0
  166. package/src/server/service.ts +5 -2
  167. package/src/server/testing.ts +1 -12
  168. package/dist/server/adapters/observeTestingSettlements.d.ts +0 -6
  169. package/dist/server/adapters/observeTestingSettlements.d.ts.map +0 -1
  170. package/dist/server/adapters/observeTestingSettlements.js +0 -35
  171. package/dist/server/adapters/observeTestingSettlements.js.map +0 -1
  172. package/dist/server/internal/testingDeferrals.d.ts +0 -10
  173. package/dist/server/internal/testingDeferrals.d.ts.map +0 -1
  174. package/dist/server/internal/testingDeferrals.js +0 -15
  175. package/dist/server/internal/testingDeferrals.js.map +0 -1
  176. package/dist/server/internal/testingReadout.d.ts +0 -10
  177. package/dist/server/internal/testingReadout.d.ts.map +0 -1
  178. package/dist/server/internal/testingReadout.js +0 -13
  179. package/dist/server/internal/testingReadout.js.map +0 -1
  180. package/dist/server/internal/testingRegistrations.d.ts +0 -4
  181. package/dist/server/internal/testingRegistrations.d.ts.map +0 -1
  182. package/dist/server/internal/testingRegistrations.js +0 -8
  183. package/dist/server/internal/testingRegistrations.js.map +0 -1
  184. package/dist/server/internal/testingSignal.d.ts.map +0 -1
  185. package/dist/server/internal/testingSignal.js.map +0 -1
  186. package/dist/server/internal/testingSnapshot.d.ts +0 -6
  187. package/dist/server/internal/testingSnapshot.d.ts.map +0 -1
  188. package/dist/server/internal/testingSnapshot.js.map +0 -1
  189. package/src/server/adapters/observeTestingSettlements.ts +0 -36
  190. package/src/server/internal/testingReadout.ts +0 -25
  191. package/src/server/internal/testingRegistrations.ts +0 -10
@@ -1,32 +1,33 @@
1
- import { Duration, Effect, Redacted } from "effect";
1
+ import { Duration, Effect, Redacted, type Schema } from "effect";
2
2
  import { defaultClientFactory } from "#server/adapters/defaultClientFactory.ts";
3
3
  import { testingClient } from "#server/adapters/testingClient.ts";
4
4
  import { testingDueNow } from "#server/adapters/testingDueNow.ts";
5
- import type { PgBossClientFactory } from "#server/client.ts";
6
- import type { QueueDefinition } from "#server/definition.ts";
5
+ import type { JobReadOptions, PgBossClientFactory } from "#server/client.ts";
6
+ import type { JobPayloadSchema, QueueDefinition } from "#server/definition.ts";
7
7
  import type { EngineClient } from "#server/engineClient.ts";
8
8
  import { PgBossTestingError } from "#server/error.ts";
9
+ import type { PgBossInspectionService, SnapshotQueue } from "#server/inspection.ts";
9
10
  import { type TestingFetches, testingFetches } from "#server/internal/adapters/testingFetches.ts";
10
- import { testingDeferrals } from "#server/internal/testingDeferrals.ts";
11
11
  import { type TestingWork, testingIdle } from "#server/internal/testingIdle.ts";
12
- import { testingOperation, testingReadout } from "#server/internal/testingReadout.ts";
12
+ import { testingInspection, testingOperation } from "#server/internal/testingOperation.ts";
13
13
  import { testingSchedules } from "#server/internal/testingSchedules.ts";
14
- import { testingSignal } from "#server/internal/testingSignal.ts";
15
- import { testingSnapshot } from "#server/internal/testingSnapshot.ts";
16
14
  import { testingWaits } from "#server/internal/testingWaits.ts";
17
15
  import type { PgBossTestingOptions } from "#server/testing.ts";
18
- export function testingController(options: PgBossTestingOptions = {}) {
16
+ import { queueSignal } from "./queueSignal.ts";
17
+ import { testingInspectionClient } from "./testingInspectionClient.ts";
18
+ export function testingController(settings: PgBossTestingOptions = {}) {
19
19
  let current: EngineClient | undefined;
20
20
  let fetches: TestingFetches | undefined;
21
21
  let schema = "pgboss";
22
- const signal = testingSignal();
22
+ const signal = queueSignal();
23
+ let observed: PgBossInspectionService | undefined;
23
24
  const work: TestingWork = { batches: new Set(), queues: new Set() };
24
25
  const controls = {
25
26
  changed: signal.changed,
26
27
  expirations: new Map<string, number>(),
27
28
  expirySeconds: new Map<string, number>(),
28
29
  failures: new Set<string>(),
29
- retryPolicy: options.retryPolicy ?? {},
30
+ retryPolicy: settings.retryPolicy ?? {},
30
31
  work,
31
32
  };
32
33
  function client(): EngineClient {
@@ -35,25 +36,34 @@ export function testingController(options: PgBossTestingOptions = {}) {
35
36
  }
36
37
  return current;
37
38
  }
39
+ function inspection(): PgBossInspectionService {
40
+ if (observed === undefined) {
41
+ throw new Error("Provide the testing clientFactory to a jobs Layer first");
42
+ }
43
+ return observed;
44
+ }
38
45
  const clientFactory: PgBossClientFactory = (clientOptions) => {
39
46
  if (current !== undefined) {
40
47
  throw new Error("A testing controller owns one jobs client; create a controller per runtime");
41
48
  }
42
49
  schema = clientOptions.schema ?? "pgboss";
43
- current = defaultClientFactory({ ...clientOptions, "__test__enableSpies": true });
50
+ current = defaultClientFactory(clientOptions);
44
51
  fetches = testingFetches(current.getDb(), signal.changed);
45
- return testingClient(current, clientOptions.schema ?? "pgboss", controls);
52
+ current = testingClient(current, schema, controls);
53
+ return testingInspectionClient(current, schema, work, signal.changed, (reads) => {
54
+ observed = reads;
55
+ });
46
56
  };
47
57
  return {
48
- ...testingDeferrals(client),
58
+ deferred: (queue: Pick<QueueDefinition, "name">) => Effect.suspend(() => testingInspection("deferred", inspection().deferred(queue), queue.name)),
49
59
  ...testingDueNow(client, () => schema, signal.changed),
50
- ...testingSchedules(client, signal.changed),
51
- ...testingWaits(client, () => fetches, signal),
60
+ ...testingSchedules(client, inspection),
61
+ ...testingWaits(inspection, () => fetches, signal),
52
62
  checkBatchWindow: (queue: Pick<QueueDefinition, "name" | "workerOptions">, millisecondsPerJob: number) =>
53
63
  testingOperation(
54
64
  "checkBatchWindow",
55
65
  async () => {
56
- const stored = await client().getQueue(queue.name);
66
+ const stored = await Effect.runPromise(inspection().queue(queue));
57
67
  if (stored === null) {
58
68
  throw new Error("Queue is not registered");
59
69
  }
@@ -72,7 +82,7 @@ export function testingController(options: PgBossTestingOptions = {}) {
72
82
  ),
73
83
  checkSchemaVersion: (expected: number) =>
74
84
  testingOperation("checkSchemaVersion", async () => {
75
- if ((await client().schemaVersion()) !== expected) {
85
+ if ((await Effect.runPromise(inspection().schemaVersion())) !== expected) {
76
86
  throw new Error(`Expected pg-boss schema version ${expected}`);
77
87
  }
78
88
  }),
@@ -99,11 +109,17 @@ export function testingController(options: PgBossTestingOptions = {}) {
99
109
  },
100
110
  }),
101
111
  failNextSend: (queue: Pick<QueueDefinition, "name">) => Effect.sync(() => controls.failures.add(queue.name)).pipe(Effect.asVoid),
102
- jobs: testingReadout(client),
112
+ jobs: <TName extends string, TPayload extends JobPayloadSchema | typeof Schema.Null>(
113
+ queue: { readonly name: TName; readonly schema: TPayload },
114
+ readOptions?: JobReadOptions,
115
+ ) => Effect.suspend(() => testingInspection("jobs", inspection().jobs(queue, readOptions), queue.name)),
116
+ refreshQueueStats: (queue?: Pick<QueueDefinition, "name">) =>
117
+ testingOperation("refreshQueueStats", () => client().supervise(queue?.name), queue?.name),
103
118
  revision: signal.revision,
104
- schemaVersion: testingOperation("schemaVersion", () => client().schemaVersion()),
105
- snapshot: testingSnapshot(client, () => schema),
119
+ schemaVersion: Effect.suspend(() => testingInspection("schemaVersion", inspection().schemaVersion())),
120
+ snapshot: <const TQueues extends readonly SnapshotQueue[]>(queues: TQueues) =>
121
+ Effect.suspend(() => testingInspection("snapshot", inspection().snapshot(queues))),
106
122
  waitForChange: signal.waitForChange,
107
- waitForIdle: testingIdle(client, () => schema, work, signal),
123
+ waitForIdle: testingIdle(inspection, work, signal),
108
124
  };
109
125
  }
@@ -1,7 +1,7 @@
1
1
  import type { QueueDefinition } from "#server/definition.ts";
2
2
  import type { EngineClient } from "#server/engineClient.ts";
3
3
  import { quotedSchema } from "#server/internal/storedRows.ts";
4
- import { testingOperation } from "#server/internal/testingReadout.ts";
4
+ import { testingOperation } from "#server/internal/testingOperation.ts";
5
5
 
6
6
  export function testingDueNow(client: () => EngineClient, schema: () => string, changed: () => void) {
7
7
  function makeDue(queue: Pick<QueueDefinition, "name">, deferredOnly: boolean) {
@@ -3,7 +3,6 @@ import type { EngineClient } from "#server/engineClient.ts";
3
3
  import { PgBossTestingError } from "#server/error.ts";
4
4
  import type { TestingEngine } from "#server/internal/engine.ts";
5
5
  import { expireAttempt } from "./expireAttempt.ts";
6
- import { observeTestingSettlements } from "./observeTestingSettlements.ts";
7
6
 
8
7
  export function testingEngine(client: EngineClient, schema: string): Context.Service.Shape<typeof TestingEngine> {
9
8
  return {
@@ -12,17 +11,5 @@ export function testingEngine(client: EngineClient, schema: string): Context.Ser
12
11
  catch: (original) => new PgBossTestingError({ operation: "expireAttempt", original: Redacted.make(original), queue: job.name }),
13
12
  try: () => expireAttempt(client, schema, job, cause),
14
13
  }),
15
- observeSettlements: (settled) =>
16
- Effect.sync(() => {
17
- const observer = observeTestingSettlements(client, settled);
18
- return {
19
- close: Effect.sync(() => observer.close()),
20
- watch: (name: string) =>
21
- Effect.try({
22
- catch: (original) => new PgBossTestingError({ operation: "observeSettlements", original: Redacted.make(original), queue: name }),
23
- try: () => observer.watch(name),
24
- }),
25
- };
26
- }),
27
14
  };
28
15
  }
@@ -0,0 +1,70 @@
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,5 +1,6 @@
1
1
  import type { ClientDatabase } from "#server/ClientDatabase.ts";
2
2
  import type { ClientOptions, StopOptions } from "#server/clientOptions.ts";
3
+ import type { EngineSchedule } from "#server/engineClient.ts";
3
4
  import type { EnqueueOptions, ScheduleOptions } from "#server/enqueueOptions.ts";
4
5
  import type { Job, WorkerJob } from "#server/job.ts";
5
6
  import type { QueueOptions } from "#server/queueOptions.ts";
@@ -62,6 +63,7 @@ export interface PgBossClient {
62
63
  readonly getDb?: () => ClientDatabase;
63
64
  readonly getQueue: (name: string) => Promise<ClientQueue | null>;
64
65
  readonly getSchedule?: (name: string, key?: string) => Promise<{ readonly name: string; readonly key: string } | null>;
66
+ readonly getSchedules?: (name?: string, key?: string) => Promise<EngineSchedule[]>;
65
67
  readonly getWipData?: (options?: { includeInternal?: boolean }) => { id: string; workId: string; name: string; count: number }[];
66
68
  readonly notifyWorker?: (workerId: string) => void;
67
69
  readonly off: (event: "error", listener: (error: Error) => void) => unknown;
@@ -69,6 +71,7 @@ export interface PgBossClient {
69
71
  readonly on: (event: "error", listener: (error: Error) => void) => unknown;
70
72
  readonly retry?: (name: string, id: string | string[], options?: ClientConnectionOptions) => Promise<object>;
71
73
  readonly schedule: (name: string, cron: string, data?: object | null, options?: ScheduleOptions) => Promise<void>;
74
+ readonly schemaVersion?: () => Promise<number | null>;
72
75
  readonly send: (name: string, data?: object | null, options?: EnqueueOptions) => Promise<string | null>;
73
76
  readonly start: () => Promise<unknown>;
74
77
  readonly stop: (options?: StopOptions) => Promise<void>;
@@ -1,7 +1,6 @@
1
1
  import type { ClientDatabase } from "#server/ClientDatabase.ts";
2
2
 
3
3
  export interface ClientOptions {
4
- "__test__enableSpies"?: boolean;
5
4
  allowSchemaCaseVariant?: boolean;
6
5
  "application_name"?: string;
7
6
 
@@ -32,7 +32,8 @@ export interface QueueDefinition<TName extends string = string, TPayload extends
32
32
  readonly handle: <E, R>(
33
33
  handler: (payload: Schema.Schema.Type<TPayload>, job: JobContext) => Effect.Effect<unknown, E, R>,
34
34
  ) => QueueWorker<TPayload, R>;
35
- readonly key?: ((payload: Schema.Schema.Type<TPayload>) => string) | undefined;
35
+ readonly id?: readonly string[] | undefined;
36
+ readonly key?: readonly string[] | undefined;
36
37
  readonly maxDeferrals: number;
37
38
  readonly name: TName;
38
39
  readonly queueOptions: Omit<QueueOptions, "maxDeferrals">;
@@ -76,32 +77,37 @@ export type RegistrationRequirements<TRegistrations extends readonly JobRegistra
76
77
  : never
77
78
  : never;
78
79
 
80
+ export type PayloadParts<TPayload extends JobPayloadSchema> = readonly {
81
+ [TField in Extract<keyof TPayload["Type"], string>]: TPayload["Type"][TField] extends string | number | boolean | null ? TField : never;
82
+ }[Extract<keyof TPayload["Type"], string>][];
83
+
79
84
  export type DefineQueueOptions<TName extends string, TPayload extends JobPayloadSchema> = {
85
+ readonly id?: PayloadParts<NoInfer<TPayload>>;
80
86
  readonly label?: string;
81
87
  readonly name: TName;
82
88
  readonly schema: TPayload;
83
89
  readonly subject?: readonly SubjectColumn<NoInfer<TPayload>>[];
84
90
  readonly worker?: WorkerOptions;
85
91
  } & (
86
- | { readonly key: (payload: Schema.Schema.Type<TPayload>) => string; readonly queue: QueueOptions & { readonly policy: "singleton" } }
92
+ | { readonly key: PayloadParts<NoInfer<TPayload>>; readonly queue: QueueOptions & { readonly policy: "singleton" } }
87
93
  | { readonly key?: never; readonly queue?: QueueOptions & { readonly policy?: Exclude<QueuePolicy, "singleton"> } }
88
94
  );
89
95
 
90
- export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
91
- options: DefineQueueOptions<TName, TPayload> & { readonly key: (payload: Schema.Schema.Type<TPayload>) => string },
92
- ): QueueDefinition<TName, TPayload> & { readonly key: (payload: Schema.Schema.Type<TPayload>) => string };
93
- export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
94
- options: DefineQueueOptions<TName, TPayload> & { readonly key?: never },
95
- ): QueueDefinition<TName, TPayload> & { readonly key?: never };
96
- export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
97
- options: DefineQueueOptions<TName, TPayload>,
98
- ): QueueDefinition<TName, TPayload>;
96
+ export function defineQueue<
97
+ const TName extends string,
98
+ const TPayload extends JobPayloadSchema,
99
+ const TId extends readonly string[] | undefined = undefined,
100
+ const TKey extends readonly string[] | undefined = undefined,
101
+ >(
102
+ options: DefineQueueOptions<TName, TPayload> & { readonly id?: TId; readonly key?: TKey },
103
+ ): QueueDefinition<TName, TPayload> & { readonly id?: TId; readonly key?: TKey };
99
104
  export function defineQueue<const TName extends string, const TPayload extends JobPayloadSchema>(
100
105
  options: DefineQueueOptions<TName, TPayload>,
101
106
  ): QueueDefinition<TName, TPayload> {
102
107
  const definition: QueueDefinition<TName, TPayload> = {
103
108
  _tag: "QueueDefinition",
104
- ...(options.key === undefined ? {} : { key: options.key }),
109
+ id: options.id === undefined ? undefined : Object.freeze([...options.id]),
110
+ ...(options.key === undefined ? {} : { key: Object.freeze([...options.key]) }),
105
111
  ...(options.label === undefined ? {} : { label: options.label }),
106
112
  ...(options.subject === undefined ? {} : { subject: Object.freeze(options.subject.map((column) => Object.freeze({ ...column }))) }),
107
113
  handle: (handler) =>
@@ -16,9 +16,7 @@ export interface EngineClient extends Required<PgBossClient> {
16
16
  readonly emit: (event: "error", error: Error) => boolean;
17
17
  readonly findJobs: <T = unknown>(name: string, options?: JobReadOptions) => Promise<Job<T>[]>;
18
18
  readonly getDb: () => ClientDatabase;
19
- readonly getJobById: <T = unknown>(name: string, id: string) => Promise<Job<T> | null>;
20
19
  readonly getSchedules: (name?: string, key?: string) => Promise<EngineSchedule[]>;
21
- readonly getSpy: (name: string) => { clear: () => void; waitForJob: (selector: () => boolean, state: "completed" | "failed") => Promise<unknown> };
22
20
  readonly schemaVersion: () => Promise<number | null>;
23
21
  readonly supervise: (name?: string) => Promise<void>;
24
22
  }
@@ -1,4 +1,5 @@
1
1
  import type { ClientDatabase } from "#server/ClientDatabase.ts";
2
+ import type { QueueDefinition } from "#server/definition.ts";
2
3
  import type { RetryOptions } from "#server/queueOptions.ts";
3
4
 
4
5
  export interface EnqueueOptions extends RetryOptions {
@@ -13,6 +14,10 @@ export interface EnqueueOptions extends RetryOptions {
13
14
  readonly startAfter?: number | string | Date;
14
15
  }
15
16
 
17
+ export type QueueEnqueueOptions<TQueue extends QueueDefinition> = Omit<EnqueueOptions, "id" | "singletonKey"> &
18
+ (TQueue["id"] extends undefined ? Pick<EnqueueOptions, "id"> : { readonly id?: never }) &
19
+ (TQueue["key"] extends undefined ? Pick<EnqueueOptions, "singletonKey"> : { readonly singletonKey?: never });
20
+
16
21
  export interface ScheduleOptions extends EnqueueOptions {
17
22
  readonly key?: string;
18
23
  readonly missed?: "skip" | "once";
@@ -10,6 +10,7 @@ export type PgBossOperation =
10
10
  | "getJob"
11
11
  | "health"
12
12
  | "install"
13
+ | "inspect"
13
14
  | "register"
14
15
  | "refreshQueueStats"
15
16
  | "retry"
@@ -0,0 +1,50 @@
1
+ import { type DateTime, Effect } from "effect";
2
+ import { defineService } from "@shivaedev/service/defineService.ts";
3
+ import { genericMethod } from "@shivaedev/service/genericMethod.ts";
4
+ import { inspection } from "#server/adapters/inspection.ts";
5
+ import type { inspectionChanges } from "#server/adapters/inspectionChanges.ts";
6
+ import type { inspectionReads } from "#server/adapters/inspectionReads.ts";
7
+ import type { JobPayloadSchema, QueueDefinition } from "#server/definition.ts";
8
+ import { PgBoss } from "#server/PgBoss.ts";
9
+ import type { PgBossService } from "#server/service.ts";
10
+
11
+ export type SnapshotQueue = Pick<QueueDefinition<string, JobPayloadSchema>, "name" | "schema">;
12
+
13
+ export interface UnfinishedJob<TQueue extends SnapshotQueue> {
14
+ readonly data: TQueue["schema"]["Type"];
15
+ readonly id: string;
16
+ readonly queue: TQueue["name"];
17
+ readonly startAfter: DateTime.Utc;
18
+ readonly state: "active" | "created" | "deferred" | "failed" | "retry";
19
+ }
20
+
21
+ export const PgBossInspection = defineService({
22
+ id: "@shivaedev/jobs/PgBossInspection",
23
+ initialize: Effect.gen(function* () {
24
+ const acquired = yield* PgBoss;
25
+ return yield* inspection(acquired.client, acquired.schema);
26
+ }),
27
+ methods: (service) => ({
28
+ busy: service.busy,
29
+ changes: service.changes,
30
+ counts: service.counts,
31
+ deadLetteredJobs: genericMethod(service.deadLetteredJobs),
32
+ deferred: service.deferred,
33
+ failedJobs: genericMethod(service.failedJobs),
34
+ getJob: genericMethod(service.getJob),
35
+ health: service.health,
36
+ jobs: genericMethod(service.jobs),
37
+ queue: service.queue,
38
+ schedules: service.schedules,
39
+ schemaVersion: service.schemaVersion,
40
+ snapshot: genericMethod(service.snapshot),
41
+ storedJobs: service.storedJobs,
42
+ }),
43
+ requires: [PgBoss] as const,
44
+ });
45
+
46
+ export type PgBossInspectionService = ReturnType<typeof inspectionReads> &
47
+ Pick<PgBossService, "counts" | "deadLetteredJobs" | "failedJobs" | "getJob"> & {
48
+ readonly health: (queues: readonly { readonly name: string }[]) => ReturnType<PgBossService["health"]>;
49
+ readonly changes: (queue?: { readonly name: string }) => ReturnType<typeof inspectionChanges>;
50
+ };
@@ -1,4 +1,4 @@
1
- import type { EngineClient } from "#server/engineClient.ts";
1
+ import type { ClientDatabase } from "#server/ClientDatabase.ts";
2
2
 
3
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
4
  const claim = /UPDATE\s+\S+\s+j\s+SET\s+state\s*=\s*'active'/u;
@@ -9,7 +9,7 @@ export interface TestingFetches {
9
9
  readonly passedSince: (queue: string, begun: number) => number;
10
10
  }
11
11
 
12
- export function testingFetches(db: ReturnType<EngineClient["getDb"]>, changed: () => void): TestingFetches {
12
+ export function testingFetches(db: ClientDatabase, changed: () => void): TestingFetches {
13
13
  const begun = new Map<string, number>();
14
14
  const passed = new Map<string, number[]>();
15
15
  const executeSql = db.executeSql.bind(db);
@@ -1,9 +1,7 @@
1
1
  import { Schema } from "effect";
2
- import type { PgBossClient } from "#server/client.ts";
3
2
  import type { WorkerJob } from "#server/job.ts";
4
3
 
5
4
  export const deferredAttempt = Symbol("deferredAttempt");
6
- export const deferralObservers = new WeakMap<PgBossClient, () => void>();
7
5
  export const STATELY_DEFERRAL_POLICY = "effect-stately-deferred";
8
6
 
9
7
  export const Deferral = Schema.Struct({
@@ -27,15 +27,9 @@ export class JobEngine extends Context.Service<
27
27
  }
28
28
  >()("@shivaedev/jobs/JobEngine") {}
29
29
 
30
- export interface TestingSettlements {
31
- readonly close: Effect.Effect<void>;
32
- readonly watch: (name: string) => Effect.Effect<void, PgBossTestingError>;
33
- }
34
-
35
30
  export class TestingEngine extends Context.Service<
36
31
  TestingEngine,
37
32
  {
38
33
  readonly expireAttempt: (job: JobAttempt, cause?: "expiry" | "heartbeat") => Effect.Effect<void, PgBossTestingError>;
39
- readonly observeSettlements: (settled: (name: string) => void) => Effect.Effect<TestingSettlements>;
40
34
  }
41
35
  >()("@shivaedev/jobs/TestingEngine") {}
@@ -1,10 +1,23 @@
1
1
  import { Effect, Option, Redacted, Schema } from "effect";
2
+ import { payloadJobId, payloadKey } from "#server/adapters/payloadIdentity.ts";
2
3
  import { transactionDb } from "#server/adapters/transactionDb.ts";
3
4
  import type { PgBossClient } from "#server/client.ts";
5
+ import type { QueueDefinition } from "#server/definition.ts";
6
+ import type { EnqueueOptions } from "#server/enqueueOptions.ts";
4
7
  import { PgBossPayloadError, toPgBossError } from "#server/error.ts";
5
8
  import { capturePayload } from "#server/internal/tracing.ts";
6
9
  import type { PgBossService } from "#server/service.ts";
7
10
 
11
+ function identityOptions(queue: Pick<QueueDefinition, "id" | "key" | "name">, payload: object, options?: EnqueueOptions) {
12
+ return queue.id === undefined && queue.key === undefined
13
+ ? options
14
+ : {
15
+ ...options,
16
+ ...(queue.id === undefined ? {} : { id: payloadJobId(queue.name, queue.id, payload) }),
17
+ ...(queue.key === undefined ? {} : { singletonKey: payloadKey(queue.key, payload) }),
18
+ };
19
+ }
20
+
8
21
  export function enqueueFor(client: PgBossClient): PgBossService["enqueue"] {
9
22
  return (queue, payload, options) =>
10
23
  Effect.gen(function* () {
@@ -20,7 +33,7 @@ export function enqueueFor(client: PgBossClient): PgBossService["enqueue"] {
20
33
  }
21
34
  const data = yield* capturePayload(encoded);
22
35
  const db = yield* transactionDb;
23
- const sendOptions = queue.key === undefined ? options : { ...options, singletonKey: queue.key(payload) };
36
+ const sendOptions = identityOptions(queue, payload, options);
24
37
  const send = Effect.tryPromise({
25
38
  catch: (error) => toPgBossError("enqueue", error, queue.name),
26
39
  try: () => client.send(queue.name, data, db === undefined ? sendOptions : { ...sendOptions, db }),
@@ -1,6 +1,7 @@
1
+ import { inspectionPlan } from "#server/adapters/inspectionPlan.ts";
1
2
  import { deadLetterResolutionPlan } from "#server/internal/deadLetterResolutionPlan.ts";
2
3
  import { deferralStorePlan } from "#server/internal/deferralStorePlan.ts";
3
4
 
4
5
  export function jobStorePlans(schema: string): readonly string[] {
5
- return [deferralStorePlan(schema), deadLetterResolutionPlan(schema)];
6
+ return [deferralStorePlan(schema), deadLetterResolutionPlan(schema), inspectionPlan(schema)];
6
7
  }
@@ -1,11 +1,9 @@
1
1
  import { Effect } from "effect";
2
+ import { inspection } from "#server/adapters/inspection.ts";
2
3
  import type { PgBossClient } from "#server/client.ts";
3
- import { countsFor } from "#server/internal/countsFor.ts";
4
4
  import { JobEngine } from "#server/internal/engine.ts";
5
5
  import { enqueueFor } from "#server/internal/enqueueFor.ts";
6
- import { healthFor } from "#server/internal/healthFor.ts";
7
6
  import { jobControl } from "#server/internal/jobControl.ts";
8
- import { jobListings } from "#server/internal/jobListings.ts";
9
7
  import type { PgBossService } from "#server/service.ts";
10
8
 
11
9
  export const makeService = Effect.fnUntraced(function* (
@@ -14,13 +12,16 @@ export const makeService = Effect.fnUntraced(function* (
14
12
  schema = "pgboss",
15
13
  ): Effect.fn.Return<PgBossService, never, JobEngine> {
16
14
  const engine = yield* JobEngine;
15
+ const reads = yield* inspection(client, schema);
17
16
  return {
18
- counts: (queue) => countsFor(client, schema, queue.name),
17
+ counts: (queue) => reads.counts(queue),
18
+ deadLetteredJobs: reads.deadLetteredJobs,
19
19
  deleteQueue: engine.deleteQueue,
20
- ...(yield* jobListings),
21
20
  enqueue: enqueueFor(client),
21
+ failedJobs: reads.failedJobs,
22
+ inspection: () => Effect.succeed(reads),
22
23
  ...(yield* jobControl),
23
- health: (queues) => healthFor(client, queues === undefined ? names : queues.map((queue) => queue.name)),
24
+ health: (queues) => reads.health(queues ?? names.map((name) => ({ name }))),
24
25
  refreshQueueStats: engine.refreshQueueStats,
25
26
  unschedule: engine.unschedule,
26
27
  };
@@ -7,7 +7,6 @@ import type { JobPayloadSchema, JobRegistration, QueueDefinition, QueueWorker, S
7
7
  import { PgBossQueueOptionsError, toPgBossError } from "#server/error.ts";
8
8
  import { ensureQueue } from "#server/internal/ensureQueue.ts";
9
9
  import { runQueueWorker } from "#server/internal/runQueueWorker.ts";
10
- import { registerTestingQueue } from "#server/internal/testingRegistrations.ts";
11
10
 
12
11
  function registrationDefinition(registration: JobRegistration): QueueDefinition | ScheduleDefinition {
13
12
  if (registration._tag === "QueueDefinition") {
@@ -107,9 +106,6 @@ export const registerJobs = Effect.fnUntraced(function* <R>(
107
106
  const scope = yield* Scope.fork(yield* Effect.scope, "parallel");
108
107
  yield* Effect.try({ catch: (error) => toPgBossError("register", error), try: () => registrationNames(registrations) });
109
108
  yield* prepareQueues(client, registrations, replaceWorkers);
110
- for (const registration of registrations) {
111
- registerTestingQueue(client, registrationName(registration));
112
- }
113
109
  if (!startWorkers) {
114
110
  return;
115
111
  }
@@ -1,10 +1,10 @@
1
- import { Clock, Effect, Schema } from "effect";
2
- import type { EngineClient } from "#server/engineClient.ts";
1
+ import { Clock, Effect } from "effect";
3
2
  import type { PgBossTestingError } from "#server/error.ts";
4
- import { quotedSchema } from "#server/internal/storedRows.ts";
5
- import { testingOperation } from "#server/internal/testingReadout.ts";
3
+ import type { PgBossInspectionService } from "#server/inspection.ts";
4
+ import { testingInspection } from "#server/internal/testingOperation.ts";
6
5
 
7
6
  export interface TestingBatch {
7
+ readonly jobs: ReadonlyMap<string, number>;
8
8
  readonly name: string;
9
9
  settled: boolean;
10
10
  }
@@ -14,30 +14,12 @@ export interface TestingWork {
14
14
  readonly queues: Set<string>;
15
15
  }
16
16
 
17
- const Busy = Schema.Struct({ busy: Schema.Boolean });
18
-
19
17
  export function testingIdle(
20
- client: () => EngineClient,
21
- schema: () => string,
18
+ inspection: () => PgBossInspectionService,
22
19
  work: TestingWork,
23
20
  signal: { readonly revision: Effect.Effect<number>; readonly waitForChange: (after: number) => Effect.Effect<number> },
24
21
  ): 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
- });
22
+ const busy = Effect.suspend(() => testingInspection("waitForIdle", inspection().busy([...work.queues].map((name) => ({ name })))));
41
23
  return Effect.gen(function* () {
42
24
  while (true) {
43
25
  const revision = yield* signal.revision;
@@ -46,12 +28,7 @@ export function testingIdle(
46
28
  if (work.queues.size === 0 || !(unsettled || (yield* busy))) {
47
29
  return;
48
30
  }
49
- if (unsettled) {
50
- yield* signal.waitForChange(revision);
51
- } else {
52
- // Other clients have no local callback; PostgreSQL remains the source of idle state.
53
- yield* Effect.raceFirst(signal.waitForChange(revision), Effect.sleep("50 millis"));
54
- }
31
+ yield* signal.waitForChange(revision);
55
32
  }
56
33
  }).pipe(Effect.provideService(Clock.Clock, Clock.Clock.defaultValue()));
57
34
  }
@@ -0,0 +1,18 @@
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
+ }