@shivaedev/jobs 0.31.0 → 0.34.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 (205) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +75 -30
  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/settleDeferral.d.ts.map +1 -1
  47. package/dist/server/adapters/settleDeferral.js +8 -2
  48. package/dist/server/adapters/settleDeferral.js.map +1 -1
  49. package/dist/server/adapters/testingClient.d.ts +1 -2
  50. package/dist/server/adapters/testingClient.d.ts.map +1 -1
  51. package/dist/server/adapters/testingClient.js +25 -45
  52. package/dist/server/adapters/testingClient.js.map +1 -1
  53. package/dist/server/adapters/testingController.d.ts +58 -12
  54. package/dist/server/adapters/testingController.d.ts.map +1 -1
  55. package/dist/server/adapters/testingController.js +40 -18
  56. package/dist/server/adapters/testingController.js.map +1 -1
  57. package/dist/server/adapters/testingDueNow.js +1 -1
  58. package/dist/server/adapters/testingDueNow.js.map +1 -1
  59. package/dist/server/adapters/testingEngine.d.ts.map +1 -1
  60. package/dist/server/adapters/testingEngine.js +0 -11
  61. package/dist/server/adapters/testingEngine.js.map +1 -1
  62. package/dist/server/adapters/testingInspectionClient.d.ts +5 -0
  63. package/dist/server/adapters/testingInspectionClient.d.ts.map +1 -0
  64. package/dist/server/adapters/testingInspectionClient.js +52 -0
  65. package/dist/server/adapters/testingInspectionClient.js.map +1 -0
  66. package/dist/server/adapters/testingTime.d.ts +10 -0
  67. package/dist/server/adapters/testingTime.d.ts.map +1 -0
  68. package/dist/server/adapters/testingTime.js +18 -0
  69. package/dist/server/adapters/testingTime.js.map +1 -0
  70. package/dist/server/client.d.ts +3 -0
  71. package/dist/server/client.d.ts.map +1 -1
  72. package/dist/server/clientOptions.d.ts +0 -1
  73. package/dist/server/clientOptions.d.ts.map +1 -1
  74. package/dist/server/definition.d.ts +12 -11
  75. package/dist/server/definition.d.ts.map +1 -1
  76. package/dist/server/definition.js +2 -1
  77. package/dist/server/definition.js.map +1 -1
  78. package/dist/server/engineClient.d.ts +0 -5
  79. package/dist/server/engineClient.d.ts.map +1 -1
  80. package/dist/server/enqueueOptions.d.ts +6 -0
  81. package/dist/server/enqueueOptions.d.ts.map +1 -1
  82. package/dist/server/error.d.ts +1 -1
  83. package/dist/server/error.d.ts.map +1 -1
  84. package/dist/server/error.js.map +1 -1
  85. package/dist/server/inspection.d.ts +102 -0
  86. package/dist/server/inspection.d.ts.map +1 -0
  87. package/dist/server/inspection.js +30 -0
  88. package/dist/server/inspection.js.map +1 -0
  89. package/dist/server/internal/adapters/deferralChanges.d.ts +3 -0
  90. package/dist/server/internal/adapters/deferralChanges.d.ts.map +1 -0
  91. package/dist/server/internal/adapters/deferralChanges.js +2 -0
  92. package/dist/server/internal/adapters/deferralChanges.js.map +1 -0
  93. package/dist/server/internal/adapters/testingFetches.d.ts +2 -2
  94. package/dist/server/internal/adapters/testingFetches.d.ts.map +1 -1
  95. package/dist/server/internal/adapters/testingFetches.js.map +1 -1
  96. package/dist/server/internal/deferral.d.ts +0 -2
  97. package/dist/server/internal/deferral.d.ts.map +1 -1
  98. package/dist/server/internal/deferral.js +0 -1
  99. package/dist/server/internal/deferral.js.map +1 -1
  100. package/dist/server/internal/engine.d.ts +0 -5
  101. package/dist/server/internal/engine.d.ts.map +1 -1
  102. package/dist/server/internal/engine.js.map +1 -1
  103. package/dist/server/internal/enqueueFor.d.ts.map +1 -1
  104. package/dist/server/internal/enqueueFor.js +11 -1
  105. package/dist/server/internal/enqueueFor.js.map +1 -1
  106. package/dist/server/internal/jobStorePlans.d.ts.map +1 -1
  107. package/dist/server/internal/jobStorePlans.js +2 -1
  108. package/dist/server/internal/jobStorePlans.js.map +1 -1
  109. package/dist/server/internal/makeService.d.ts.map +1 -1
  110. package/dist/server/internal/makeService.js +7 -6
  111. package/dist/server/internal/makeService.js.map +1 -1
  112. package/dist/server/internal/register.d.ts.map +1 -1
  113. package/dist/server/internal/register.js +0 -4
  114. package/dist/server/internal/register.js.map +1 -1
  115. package/dist/server/internal/testingIdle.d.ts +4 -3
  116. package/dist/server/internal/testingIdle.d.ts.map +1 -1
  117. package/dist/server/internal/testingIdle.js +7 -26
  118. package/dist/server/internal/testingIdle.js.map +1 -1
  119. package/dist/server/internal/testingOperation.d.ts +5 -0
  120. package/dist/server/internal/testingOperation.d.ts.map +1 -0
  121. package/dist/server/internal/testingOperation.js +12 -0
  122. package/dist/server/internal/testingOperation.js.map +1 -0
  123. package/dist/server/internal/testingSchedules.d.ts +8 -5
  124. package/dist/server/internal/testingSchedules.d.ts.map +1 -1
  125. package/dist/server/internal/testingSchedules.js +14 -9
  126. package/dist/server/internal/testingSchedules.js.map +1 -1
  127. package/dist/server/internal/testingWaits.d.ts +7 -4
  128. package/dist/server/internal/testingWaits.d.ts.map +1 -1
  129. package/dist/server/internal/testingWaits.js +7 -10
  130. package/dist/server/internal/testingWaits.js.map +1 -1
  131. package/dist/server/schema.d.ts.map +1 -1
  132. package/dist/server/schema.js +5 -0
  133. package/dist/server/schema.js.map +1 -1
  134. package/dist/server/service.d.ts +4 -4
  135. package/dist/server/service.d.ts.map +1 -1
  136. package/dist/server/service.js +1 -0
  137. package/dist/server/service.js.map +1 -1
  138. package/dist/server/testing.d.ts +63 -25
  139. package/dist/server/testing.d.ts.map +1 -1
  140. package/dist/server/testing.js.map +1 -1
  141. package/package.json +7 -7
  142. package/src/server/adapters/inspection.ts +31 -0
  143. package/src/server/adapters/inspectionChanges.ts +31 -0
  144. package/src/server/{internal/testingDeferrals.ts → adapters/inspectionDeferrals.ts} +10 -12
  145. package/src/server/adapters/inspectionJobs.ts +28 -0
  146. package/src/server/adapters/inspectionOperation.ts +6 -0
  147. package/src/server/adapters/inspectionPlan.ts +21 -0
  148. package/src/server/adapters/inspectionReads.ts +55 -0
  149. package/src/server/{internal/testingSnapshot.ts → adapters/inspectionSnapshot.ts} +15 -17
  150. package/src/server/adapters/jobEngine.ts +0 -4
  151. package/src/server/adapters/payloadIdentity.ts +15 -0
  152. package/src/server/{internal/testingSignal.ts → adapters/queueSignal.ts} +1 -1
  153. package/src/server/adapters/settleDeferral.ts +8 -2
  154. package/src/server/adapters/testingClient.ts +11 -30
  155. package/src/server/adapters/testingController.ts +49 -21
  156. package/src/server/adapters/testingDueNow.ts +1 -1
  157. package/src/server/adapters/testingEngine.ts +0 -13
  158. package/src/server/adapters/testingInspectionClient.ts +70 -0
  159. package/src/server/adapters/testingTime.ts +25 -0
  160. package/src/server/client.ts +3 -0
  161. package/src/server/clientOptions.ts +0 -1
  162. package/src/server/definition.ts +18 -12
  163. package/src/server/engineClient.ts +0 -2
  164. package/src/server/enqueueOptions.ts +5 -0
  165. package/src/server/error.ts +1 -0
  166. package/src/server/inspection.ts +50 -0
  167. package/src/server/internal/adapters/deferralChanges.ts +3 -0
  168. package/src/server/internal/adapters/testingFetches.ts +2 -2
  169. package/src/server/internal/deferral.ts +0 -2
  170. package/src/server/internal/engine.ts +0 -6
  171. package/src/server/internal/enqueueFor.ts +14 -1
  172. package/src/server/internal/jobStorePlans.ts +2 -1
  173. package/src/server/internal/makeService.ts +7 -6
  174. package/src/server/internal/register.ts +0 -4
  175. package/src/server/internal/testingIdle.ts +10 -31
  176. package/src/server/internal/testingOperation.ts +18 -0
  177. package/src/server/internal/testingSchedules.ts +28 -16
  178. package/src/server/internal/testingWaits.ts +11 -16
  179. package/src/server/schema.ts +5 -0
  180. package/src/server/service.ts +5 -2
  181. package/src/server/testing.ts +3 -12
  182. package/dist/server/adapters/observeTestingSettlements.d.ts +0 -6
  183. package/dist/server/adapters/observeTestingSettlements.d.ts.map +0 -1
  184. package/dist/server/adapters/observeTestingSettlements.js +0 -35
  185. package/dist/server/adapters/observeTestingSettlements.js.map +0 -1
  186. package/dist/server/internal/testingDeferrals.d.ts +0 -10
  187. package/dist/server/internal/testingDeferrals.d.ts.map +0 -1
  188. package/dist/server/internal/testingDeferrals.js +0 -15
  189. package/dist/server/internal/testingDeferrals.js.map +0 -1
  190. package/dist/server/internal/testingReadout.d.ts +0 -10
  191. package/dist/server/internal/testingReadout.d.ts.map +0 -1
  192. package/dist/server/internal/testingReadout.js +0 -13
  193. package/dist/server/internal/testingReadout.js.map +0 -1
  194. package/dist/server/internal/testingRegistrations.d.ts +0 -4
  195. package/dist/server/internal/testingRegistrations.d.ts.map +0 -1
  196. package/dist/server/internal/testingRegistrations.js +0 -8
  197. package/dist/server/internal/testingRegistrations.js.map +0 -1
  198. package/dist/server/internal/testingSignal.d.ts.map +0 -1
  199. package/dist/server/internal/testingSignal.js.map +0 -1
  200. package/dist/server/internal/testingSnapshot.d.ts +0 -6
  201. package/dist/server/internal/testingSnapshot.d.ts.map +0 -1
  202. package/dist/server/internal/testingSnapshot.js.map +0 -1
  203. package/src/server/adapters/observeTestingSettlements.ts +0 -36
  204. package/src/server/internal/testingReadout.ts +0 -25
  205. package/src/server/internal/testingRegistrations.ts +0 -10
@@ -0,0 +1,55 @@
1
+ import { Schema } from "effect";
2
+ import { inspectionDeferrals } from "#server/adapters/inspectionDeferrals.ts";
3
+ import { inspectionJobs } from "#server/adapters/inspectionJobs.ts";
4
+ import { inspectionOperation } from "#server/adapters/inspectionOperation.ts";
5
+ import { inspectionSnapshot } from "#server/adapters/inspectionSnapshot.ts";
6
+ import type { PgBossClient } from "#server/client.ts";
7
+ import type { QueueDefinition } from "#server/definition.ts";
8
+ import { quotedSchema } from "#server/internal/storedRows.ts";
9
+
10
+ const Busy = Schema.Struct({ busy: Schema.Boolean });
11
+ export function inspectionReads(client: PgBossClient, schema: string) {
12
+ const quoted = quotedSchema(schema);
13
+ function query(text: string, values?: unknown[]) {
14
+ const db = client.getDb?.();
15
+ if (db === undefined) {
16
+ throw new TypeError("This pg-boss client does not support getDb");
17
+ }
18
+ return db.executeSql(text, values);
19
+ }
20
+ return {
21
+ ...inspectionDeferrals(client),
22
+ busy: (queues: readonly Pick<QueueDefinition, "name">[]) =>
23
+ inspectionOperation(async () => {
24
+ const { rows } = await query(
25
+ `/* jobs inspection busy */ SELECT EXISTS (
26
+ SELECT 1 FROM ${quoted}.job WHERE name = ANY($1::text[]) AND (
27
+ (state < 'active' AND NOT blocked AND start_after <= ${quoted}.job_now()) OR state = 'active'
28
+ )
29
+ ) AS busy`,
30
+ [queues.map(({ name }) => name)],
31
+ );
32
+ return Schema.decodeUnknownSync(Busy)(rows[0]).busy;
33
+ }),
34
+ ...inspectionJobs(client),
35
+ queue: (queue: Pick<QueueDefinition, "name">) => inspectionOperation(() => client.getQueue(queue.name), queue.name),
36
+ schedules: (name?: string, key?: string) =>
37
+ inspectionOperation(() => {
38
+ if (client.getSchedules === undefined) {
39
+ throw new TypeError("This pg-boss client does not support getSchedules");
40
+ }
41
+ return client.getSchedules(name, key);
42
+ }),
43
+ schemaVersion: () =>
44
+ inspectionOperation(() => {
45
+ if (client.schemaVersion === undefined) {
46
+ throw new TypeError("This pg-boss client does not support schemaVersion");
47
+ }
48
+ return client.schemaVersion();
49
+ }),
50
+ snapshot: inspectionSnapshot(
51
+ () => client,
52
+ () => schema,
53
+ ),
54
+ };
55
+ }
@@ -1,9 +1,9 @@
1
1
  import { Effect, Schema } from "effect";
2
- import type { EngineClient } from "#server/engineClient.ts";
3
- import type { PgBossPayloadError, PgBossTestingError } from "#server/error.ts";
2
+ import { inspectionOperation } from "#server/adapters/inspectionOperation.ts";
3
+ import type { PgBossClient } from "#server/client.ts";
4
+ import type { PgBossError, PgBossPayloadError } from "#server/error.ts";
5
+ import type { SnapshotQueue, UnfinishedJob } from "#server/inspection.ts";
4
6
  import { decodeStoredData, quotedSchema } from "#server/internal/storedRows.ts";
5
- import { testingOperation } from "#server/internal/testingReadout.ts";
6
- import type { SnapshotQueue, UnfinishedJob } from "#server/testing.ts";
7
7
 
8
8
  const Rows = Schema.Array(
9
9
  Schema.Struct({
@@ -15,29 +15,27 @@ const Rows = Schema.Array(
15
15
  }),
16
16
  );
17
17
 
18
- export function testingSnapshot(client: () => EngineClient, schema: () => string) {
18
+ export function inspectionSnapshot(client: () => PgBossClient, schema: () => string) {
19
19
  return <const TQueues extends readonly SnapshotQueue[]>(
20
20
  queues: TQueues,
21
- ): Effect.Effect<
22
- readonly UnfinishedJob<TQueues[number]>[],
23
- PgBossPayloadError | PgBossTestingError,
24
- TQueues[number]["schema"]["DecodingServices"]
25
- > => {
21
+ ): Effect.Effect<readonly UnfinishedJob<TQueues[number]>[], PgBossPayloadError | PgBossError, TQueues[number]["schema"]["DecodingServices"]> => {
26
22
  const byName = new Map<string, TQueues[number]>(queues.map((queue) => [queue.name, queue]));
27
- return testingOperation("snapshot", async () => {
23
+ return inspectionOperation(async () => {
28
24
  const quoted = quotedSchema(schema());
29
- const { rows } = await client()
30
- .getDb()
31
- .executeSql(
32
- `SELECT id, name AS queue, data, start_after AS "startAfter",
25
+ const db = client().getDb?.();
26
+ if (db === undefined) {
27
+ throw new TypeError("This pg-boss client does not support getDb");
28
+ }
29
+ const { rows } = await db.executeSql(
30
+ `SELECT id, name AS queue, data, start_after AS "startAfter",
33
31
  CASE WHEN state = 'retry' AND started_on IS NULL AND start_after > ${quoted}.job_now()
34
32
  AND output ? 'jobDeferral'
35
33
  THEN 'deferred' ELSE state::text END AS state
36
34
  FROM ${quoted}.job
37
35
  WHERE name = ANY($1::text[]) AND state IN ('created', 'retry', 'active', 'failed')
38
36
  ORDER BY created_on, id`,
39
- [[...byName.keys()]],
40
- );
37
+ [[...byName.keys()]],
38
+ );
41
39
  return Schema.decodeUnknownPromise(Rows)(rows);
42
40
  }).pipe(
43
41
  Effect.flatMap((rows) =>
@@ -1,7 +1,6 @@
1
1
  import { type Context, Effect } from "effect";
2
2
  import type { PgBossClient } from "#server/client.ts";
3
3
  import { toPgBossError } from "#server/error.ts";
4
- import { deferralObservers } from "#server/internal/deferral.ts";
5
4
  import type { JobEngine } from "#server/internal/engine.ts";
6
5
  import { listJobs } from "./listJobs.ts";
7
6
  import { queueAdministration } from "./queueAdministration.ts";
@@ -49,9 +48,6 @@ export function jobEngine(client: PgBossClient, schema = "pgboss"): Context.Serv
49
48
  // Native batch settlement must not match a sibling's new claim at the unchanged retry count.
50
49
  job.retryCount = Number.MAX_SAFE_INTEGER;
51
50
  }
52
- if (settled.deferred) {
53
- deferralObservers.get(client)?.();
54
- }
55
51
  }),
56
52
  ),
57
53
  ),
@@ -0,0 +1,15 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function payloadKey(parts: readonly string[], payload: object): string {
4
+ return JSON.stringify(parts.map((part) => [part, typeof Reflect.get(payload, part), String(Reflect.get(payload, part))]));
5
+ }
6
+
7
+ export function payloadJobId(queue: string, parts: readonly string[], payload: object): string {
8
+ const bytes = createHash("sha256")
9
+ .update(JSON.stringify([queue, payloadKey(parts, payload)]))
10
+ .digest();
11
+ bytes.writeUInt8((bytes.readUInt8(6) & 0x0f) | 0x80, 6);
12
+ bytes.writeUInt8((bytes.readUInt8(8) & 0x3f) | 0x80, 8);
13
+ const hex = bytes.subarray(0, 16).toString("hex");
14
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
15
+ }
@@ -1,6 +1,6 @@
1
1
  import { Effect } from "effect";
2
2
 
3
- export function testingSignal() {
3
+ export function queueSignal() {
4
4
  let revision = 0;
5
5
  const listeners = new Set<(revision: number) => void>();
6
6
  return {
@@ -1,5 +1,6 @@
1
1
  import { Schema } from "effect";
2
2
  import type { PgBossClient } from "#server/client.ts";
3
+ import { deferralChanges } from "#server/internal/adapters/deferralChanges.ts";
3
4
  import { STATELY_DEFERRAL_POLICY } from "#server/internal/deferral.ts";
4
5
  import type { JobAttempt } from "#server/internal/engine.ts";
5
6
  import { quotedSchema } from "#server/internal/storedRows.ts";
@@ -11,8 +12,9 @@ export async function settleDeferral(client: PgBossClient, schema: string, job:
11
12
  throw new TypeError("This pg-boss client does not support getDb for RetryAt");
12
13
  }
13
14
  const quoted = quotedSchema(schema);
15
+ const db = client.getDb();
14
16
  // Native failure settlement deletes and reinserts jobs, so a cascading job foreign key would erase the counter.
15
- const { rows } = await client.getDb().executeSql(
17
+ const { rows } = await db.executeSql(
16
18
  `WITH claimed AS MATERIALIZED (
17
19
  SELECT name, id, created_on,
18
20
  GREATEST(keep_until, $4::timestamptz + (keep_until - start_after)) AS keep_until
@@ -50,5 +52,9 @@ export async function settleDeferral(client: PgBossClient, schema: string, job:
50
52
  SELECT EXISTS (SELECT 1 FROM claimed) AS claimed, EXISTS (SELECT 1 FROM deferred) AS deferred`,
51
53
  [job.name, job.id, job.retryCount, at, reason, Reflect.get(job, "startedOn"), maxDeferrals],
52
54
  );
53
- return Schema.decodeUnknownSync(Settlement)(rows[0]);
55
+ const settled = Schema.decodeUnknownSync(Settlement)(rows[0]);
56
+ if (settled.deferred) {
57
+ deferralChanges.get(db)?.();
58
+ }
59
+ return settled;
54
60
  }
@@ -4,9 +4,8 @@ import { testingExpirations } from "#server/adapters/testingExpirations.ts";
4
4
  import type { ClientUpdateOptions, ClientUpdateRequest, ClientUpdateResult, PgBossClient } from "#server/client.ts";
5
5
  import type { EngineClient } from "#server/engineClient.ts";
6
6
  import type { EnqueueOptions } from "#server/enqueueOptions.ts";
7
- import { deferralObservers } from "#server/internal/deferral.ts";
7
+ import { deferralChanges } from "#server/internal/adapters/deferralChanges.ts";
8
8
  import type { TestingBatch, TestingWork } from "#server/internal/testingIdle.ts";
9
- import { testingRegistration } from "#server/internal/testingRegistrations.ts";
10
9
  import type { WorkerJob } from "#server/job.ts";
11
10
 
12
11
  export interface TestingControls {
@@ -18,18 +17,10 @@ export interface TestingControls {
18
17
  readonly work: TestingWork;
19
18
  }
20
19
 
21
- export function testingClient(client: EngineClient, schema: string, controls: TestingControls): PgBossClient {
20
+ export function testingClient(client: EngineClient, schema: string, controls: TestingControls): EngineClient {
22
21
  const engine = testingEngine(client, schema);
23
- const settlements = Effect.runSync(
24
- engine.observeSettlements((name) => {
25
- for (const batch of controls.work.batches) {
26
- if (batch.name === name) {
27
- batch.settled = true;
28
- }
29
- }
30
- controls.changed();
31
- }),
32
- );
22
+ deferralChanges.set(client.getDb(), controls.changed);
23
+
33
24
  const expirations = testingExpirations(client);
34
25
  function update(request: ClientUpdateRequest): Promise<ClientUpdateResult>;
35
26
  function update(name: string, data: object | null | undefined, options?: ClientUpdateOptions): Promise<ClientUpdateResult>;
@@ -45,6 +36,9 @@ export function testingClient(client: EngineClient, schema: string, controls: Te
45
36
  return result;
46
37
  },
47
38
  createQueue: (name, options) => {
39
+ if (options?.deadLetter !== undefined) {
40
+ controls.work.queues.add(name);
41
+ }
48
42
  const { retryDelayMax, ...rest } = options ?? {};
49
43
  return client.createQueue(name, {
50
44
  ...rest,
@@ -84,13 +78,6 @@ export function testingClient(client: EngineClient, schema: string, controls: Te
84
78
  controls.changed();
85
79
  return id;
86
80
  },
87
- stop: async (options) => {
88
- try {
89
- await client.stop(options);
90
- } finally {
91
- await Effect.runPromise(settlements.close);
92
- }
93
- },
94
81
  update,
95
82
  updateQueue: (name, options) =>
96
83
  client.updateQueue(name, {
@@ -98,10 +85,9 @@ export function testingClient(client: EngineClient, schema: string, controls: Te
98
85
  ...controls.retryPolicy,
99
86
  ...(controls.retryPolicy.retryBackoff === false ? { retryDelayMax: null } : {}),
100
87
  }),
101
- work: async (name, options, handler) => {
102
- await Effect.runPromise(settlements.watch(name));
103
- return client.work(name, options, async (jobs: readonly WorkerJob<unknown>[]) => {
104
- const batch: TestingBatch = { name, settled: false };
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 };
105
91
  controls.work.batches.add(batch);
106
92
  controls.changed();
107
93
  try {
@@ -121,14 +107,10 @@ export function testingClient(client: EngineClient, schema: string, controls: Te
121
107
  controls.work.batches.delete(batch);
122
108
  controls.changed();
123
109
  }
124
- });
125
- },
110
+ }),
126
111
  };
127
112
  const wrapped = new Proxy(client, {
128
113
  get(target, property) {
129
- if (property === testingRegistration) {
130
- return (name: string) => controls.work.queues.add(name);
131
- }
132
114
  if (Object.hasOwn(overrides, property)) {
133
115
  const override: unknown = Reflect.get(overrides, property);
134
116
  return override;
@@ -137,6 +119,5 @@ export function testingClient(client: EngineClient, schema: string, controls: Te
137
119
  return typeof value === "function" ? value.bind(target) : value;
138
120
  },
139
121
  });
140
- deferralObservers.set(wrapped, controls.changed);
141
122
  return wrapped;
142
123
  }
@@ -1,32 +1,36 @@
1
- import { Duration, Effect, Redacted } from "effect";
1
+ import { Duration, Effect, Redacted, type Schema } from "effect";
2
+ import { TestClock as PgBossTestClock } from "pg-boss";
2
3
  import { defaultClientFactory } from "#server/adapters/defaultClientFactory.ts";
3
4
  import { testingClient } from "#server/adapters/testingClient.ts";
4
5
  import { testingDueNow } from "#server/adapters/testingDueNow.ts";
5
- import type { PgBossClientFactory } from "#server/client.ts";
6
- import type { QueueDefinition } from "#server/definition.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";
7
9
  import type { EngineClient } from "#server/engineClient.ts";
8
10
  import { PgBossTestingError } from "#server/error.ts";
11
+ import type { PgBossInspectionService, SnapshotQueue } from "#server/inspection.ts";
9
12
  import { type TestingFetches, testingFetches } from "#server/internal/adapters/testingFetches.ts";
10
- import { testingDeferrals } from "#server/internal/testingDeferrals.ts";
11
13
  import { type TestingWork, testingIdle } from "#server/internal/testingIdle.ts";
12
- import { testingOperation, testingReadout } from "#server/internal/testingReadout.ts";
14
+ import { testingInspection, testingOperation } from "#server/internal/testingOperation.ts";
13
15
  import { testingSchedules } from "#server/internal/testingSchedules.ts";
14
- import { testingSignal } from "#server/internal/testingSignal.ts";
15
- import { testingSnapshot } from "#server/internal/testingSnapshot.ts";
16
16
  import { testingWaits } from "#server/internal/testingWaits.ts";
17
17
  import type { PgBossTestingOptions } from "#server/testing.ts";
18
- export function testingController(options: PgBossTestingOptions = {}) {
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());
19
22
  let current: EngineClient | undefined;
20
23
  let fetches: TestingFetches | undefined;
21
24
  let schema = "pgboss";
22
- const signal = testingSignal();
25
+ const signal = queueSignal();
26
+ let observed: PgBossInspectionService | undefined;
23
27
  const work: TestingWork = { batches: new Set(), queues: new Set() };
24
28
  const controls = {
25
29
  changed: signal.changed,
26
30
  expirations: new Map<string, number>(),
27
31
  expirySeconds: new Map<string, number>(),
28
32
  failures: new Set<string>(),
29
- retryPolicy: options.retryPolicy ?? {},
33
+ retryPolicy: settings.retryPolicy ?? {},
30
34
  work,
31
35
  };
32
36
  function client(): EngineClient {
@@ -35,25 +39,42 @@ export function testingController(options: PgBossTestingOptions = {}) {
35
39
  }
36
40
  return current;
37
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
+ }
38
48
  const clientFactory: PgBossClientFactory = (clientOptions) => {
39
49
  if (current !== undefined) {
40
50
  throw new Error("A testing controller owns one jobs client; create a controller per runtime");
41
51
  }
42
52
  schema = clientOptions.schema ?? "pgboss";
43
- current = defaultClientFactory({ ...clientOptions, "__test__enableSpies": true });
53
+ current = defaultClientFactory({ ...clientOptions, ...(clock === undefined ? {} : { clock }) });
44
54
  fetches = testingFetches(current.getDb(), signal.changed);
45
- return testingClient(current, clientOptions.schema ?? "pgboss", controls);
55
+ current = testingClient(current, schema, controls);
56
+ return testingInspectionClient(current, schema, work, signal.changed, (reads) => {
57
+ observed = reads;
58
+ });
46
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);
47
68
  return {
48
- ...testingDeferrals(client),
69
+ deferred: (queue: Pick<QueueDefinition, "name">) => Effect.suspend(() => testingInspection("deferred", inspection().deferred(queue), queue.name)),
49
70
  ...testingDueNow(client, () => schema, signal.changed),
50
- ...testingSchedules(client, signal.changed),
51
- ...testingWaits(client, () => fetches, signal),
71
+ ...testingSchedules(client, inspection),
72
+ ...testingWaits(inspection, () => fetches, signal),
52
73
  checkBatchWindow: (queue: Pick<QueueDefinition, "name" | "workerOptions">, millisecondsPerJob: number) =>
53
74
  testingOperation(
54
75
  "checkBatchWindow",
55
76
  async () => {
56
- const stored = await client().getQueue(queue.name);
77
+ const stored = await Effect.runPromise(inspection().queue(queue));
57
78
  if (stored === null) {
58
79
  throw new Error("Queue is not registered");
59
80
  }
@@ -72,7 +93,7 @@ export function testingController(options: PgBossTestingOptions = {}) {
72
93
  ),
73
94
  checkSchemaVersion: (expected: number) =>
74
95
  testingOperation("checkSchemaVersion", async () => {
75
- if ((await client().schemaVersion()) !== expected) {
96
+ if ((await Effect.runPromise(inspection().schemaVersion())) !== expected) {
76
97
  throw new Error(`Expected pg-boss schema version ${expected}`);
77
98
  }
78
99
  }),
@@ -99,11 +120,18 @@ export function testingController(options: PgBossTestingOptions = {}) {
99
120
  },
100
121
  }),
101
122
  failNextSend: (queue: Pick<QueueDefinition, "name">) => Effect.sync(() => controls.failures.add(queue.name)).pipe(Effect.asVoid),
102
- jobs: testingReadout(client),
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),
103
129
  revision: signal.revision,
104
- schemaVersion: testingOperation("schemaVersion", () => client().schemaVersion()),
105
- snapshot: testingSnapshot(client, () => schema),
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))),
106
133
  waitForChange: signal.waitForChange,
107
- waitForIdle: testingIdle(client, () => schema, work, signal),
134
+ waitForIdle: idle,
135
+ ...testingTime(settings.clock, clock, idle),
108
136
  };
109
137
  }
@@ -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
+ }
@@ -0,0 +1,25 @@
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,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
  }