duron 0.1.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 (82) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +140 -0
  3. package/dist/action-job.d.ts +24 -0
  4. package/dist/action-job.d.ts.map +1 -0
  5. package/dist/action-job.js +108 -0
  6. package/dist/action-manager.d.ts +21 -0
  7. package/dist/action-manager.d.ts.map +1 -0
  8. package/dist/action-manager.js +78 -0
  9. package/dist/action.d.ts +129 -0
  10. package/dist/action.d.ts.map +1 -0
  11. package/dist/action.js +87 -0
  12. package/dist/adapters/adapter.d.ts +92 -0
  13. package/dist/adapters/adapter.d.ts.map +1 -0
  14. package/dist/adapters/adapter.js +424 -0
  15. package/dist/adapters/postgres/drizzle.config.d.ts +3 -0
  16. package/dist/adapters/postgres/drizzle.config.d.ts.map +1 -0
  17. package/dist/adapters/postgres/drizzle.config.js +10 -0
  18. package/dist/adapters/postgres/pglite.d.ts +13 -0
  19. package/dist/adapters/postgres/pglite.d.ts.map +1 -0
  20. package/dist/adapters/postgres/pglite.js +36 -0
  21. package/dist/adapters/postgres/postgres.d.ts +51 -0
  22. package/dist/adapters/postgres/postgres.d.ts.map +1 -0
  23. package/dist/adapters/postgres/postgres.js +867 -0
  24. package/dist/adapters/postgres/schema.d.ts +581 -0
  25. package/dist/adapters/postgres/schema.d.ts.map +1 -0
  26. package/dist/adapters/postgres/schema.default.d.ts +577 -0
  27. package/dist/adapters/postgres/schema.default.d.ts.map +1 -0
  28. package/dist/adapters/postgres/schema.default.js +3 -0
  29. package/dist/adapters/postgres/schema.js +87 -0
  30. package/dist/adapters/schemas.d.ts +516 -0
  31. package/dist/adapters/schemas.d.ts.map +1 -0
  32. package/dist/adapters/schemas.js +184 -0
  33. package/dist/client.d.ts +85 -0
  34. package/dist/client.d.ts.map +1 -0
  35. package/dist/client.js +416 -0
  36. package/dist/constants.d.ts +14 -0
  37. package/dist/constants.d.ts.map +1 -0
  38. package/dist/constants.js +22 -0
  39. package/dist/errors.d.ts +43 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +75 -0
  42. package/dist/index.d.ts +8 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +6 -0
  45. package/dist/server.d.ts +1193 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +516 -0
  48. package/dist/step-manager.d.ts +46 -0
  49. package/dist/step-manager.d.ts.map +1 -0
  50. package/dist/step-manager.js +216 -0
  51. package/dist/utils/checksum.d.ts +2 -0
  52. package/dist/utils/checksum.d.ts.map +1 -0
  53. package/dist/utils/checksum.js +6 -0
  54. package/dist/utils/p-retry.d.ts +19 -0
  55. package/dist/utils/p-retry.d.ts.map +1 -0
  56. package/dist/utils/p-retry.js +130 -0
  57. package/dist/utils/wait-for-abort.d.ts +5 -0
  58. package/dist/utils/wait-for-abort.d.ts.map +1 -0
  59. package/dist/utils/wait-for-abort.js +32 -0
  60. package/migrations/postgres/0000_lethal_speed_demon.sql +64 -0
  61. package/migrations/postgres/meta/0000_snapshot.json +606 -0
  62. package/migrations/postgres/meta/_journal.json +13 -0
  63. package/package.json +88 -0
  64. package/src/action-job.ts +201 -0
  65. package/src/action-manager.ts +166 -0
  66. package/src/action.ts +247 -0
  67. package/src/adapters/adapter.ts +969 -0
  68. package/src/adapters/postgres/drizzle.config.ts +11 -0
  69. package/src/adapters/postgres/pglite.ts +86 -0
  70. package/src/adapters/postgres/postgres.ts +1346 -0
  71. package/src/adapters/postgres/schema.default.ts +5 -0
  72. package/src/adapters/postgres/schema.ts +119 -0
  73. package/src/adapters/schemas.ts +320 -0
  74. package/src/client.ts +859 -0
  75. package/src/constants.ts +37 -0
  76. package/src/errors.ts +205 -0
  77. package/src/index.ts +14 -0
  78. package/src/server.ts +718 -0
  79. package/src/step-manager.ts +471 -0
  80. package/src/utils/checksum.ts +7 -0
  81. package/src/utils/p-retry.ts +213 -0
  82. package/src/utils/wait-for-abort.ts +40 -0
@@ -0,0 +1,5 @@
1
+ import createSchema from './schema.js'
2
+
3
+ const { schema, jobsTable, jobStepsTable } = createSchema('duron')
4
+
5
+ export { schema, jobsTable, jobStepsTable }
@@ -0,0 +1,119 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import { check, index, integer, jsonb, pgSchema, text, timestamp, unique, uuid } from 'drizzle-orm/pg-core'
3
+
4
+ import { JOB_STATUSES, type JobStatus, STEP_STATUS_ACTIVE, STEP_STATUSES, type StepStatus } from '../../constants.js'
5
+ import type { SerializableError } from '../../errors.js'
6
+
7
+ export default function createSchema(schemaName: string) {
8
+ const schema = pgSchema(schemaName)
9
+
10
+ const jobsTable = schema.table(
11
+ 'jobs',
12
+ {
13
+ id: uuid('id').primaryKey().defaultRandom(),
14
+ action_name: text('action_name').notNull(),
15
+ group_key: text('group_key').notNull(),
16
+ status: text('status').$type<JobStatus>().notNull().default('created'),
17
+ checksum: text('checksum').notNull(),
18
+ input: jsonb('input').notNull().default({}),
19
+ output: jsonb('output'),
20
+ error: jsonb('error').$type<SerializableError>(),
21
+ timeout_ms: integer('timeout_ms').notNull(),
22
+ expires_at: timestamp('expires_at', { withTimezone: true }),
23
+ started_at: timestamp('started_at', { withTimezone: true }),
24
+ finished_at: timestamp('finished_at', { withTimezone: true }),
25
+ owner_id: text('owner_id'),
26
+ concurrency_limit: integer('concurrency_limit').notNull().default(10),
27
+ created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
28
+ updated_at: timestamp('updated_at', { withTimezone: true })
29
+ .notNull()
30
+ .defaultNow()
31
+ .$onUpdateFn(
32
+ () =>
33
+ ({
34
+ toISOString: () => sql`now()` as any,
35
+ }) as any,
36
+ ),
37
+ },
38
+ (table) => [
39
+ // Single column indexes
40
+ index('idx_jobs_action_name').on(table.action_name),
41
+ index('idx_jobs_status').on(table.status),
42
+ index('idx_jobs_group_key').on(table.group_key),
43
+ index('idx_jobs_started_at').on(table.started_at),
44
+ index('idx_jobs_finished_at').on(table.finished_at),
45
+ index('idx_jobs_expires_at').on(table.expires_at),
46
+ index('idx_jobs_owner_id').on(table.owner_id),
47
+ index('idx_jobs_checksum').on(table.checksum),
48
+ index('idx_jobs_concurrency_limit').on(table.concurrency_limit),
49
+ // Composite indexes
50
+ index('idx_jobs_action_status').on(table.action_name, table.status),
51
+ index('idx_jobs_action_group').on(table.action_name, table.group_key),
52
+ // GIN indexes for full-text search
53
+ index('idx_jobs_input_fts').using('gin', sql`to_tsvector('english', ${table.input}::text)`),
54
+ index('idx_jobs_output_fts').using('gin', sql`to_tsvector('english', ${table.output}::text)`),
55
+ check(
56
+ 'jobs_status_check',
57
+ sql`${table.status} IN ${sql.raw(`(${JOB_STATUSES.map((s) => `'${s}'`).join(',')})`)}`,
58
+ ),
59
+ ],
60
+ )
61
+
62
+ const jobStepsTable = schema.table(
63
+ 'job_steps',
64
+ {
65
+ id: uuid('id').primaryKey().defaultRandom(),
66
+ job_id: uuid('job_id')
67
+ .notNull()
68
+ .references(() => jobsTable.id, { onDelete: 'cascade' }),
69
+ name: text('name').notNull(),
70
+ status: text('status').$type<StepStatus>().notNull().default(STEP_STATUS_ACTIVE),
71
+ output: jsonb('output'),
72
+ error: jsonb('error').$type<SerializableError>(),
73
+ started_at: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
74
+ finished_at: timestamp('finished_at', { withTimezone: true }),
75
+ timeout_ms: integer('timeout_ms').notNull(),
76
+ expires_at: timestamp('expires_at', { withTimezone: true }),
77
+ retries_limit: integer('retries_limit').notNull().default(0),
78
+ retries_count: integer('retries_count').notNull().default(0),
79
+ delayed_ms: integer('delayed_ms'),
80
+ history_failed_attempts: jsonb('history_failed_attempts')
81
+ .$type<Record<string, { failedAt: Date; error: SerializableError; delayedMs: number }>>()
82
+ .notNull()
83
+ .default({}),
84
+ created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
85
+ updated_at: timestamp('updated_at', { withTimezone: true })
86
+ .notNull()
87
+ .defaultNow()
88
+ .$onUpdateFn(
89
+ () =>
90
+ ({
91
+ toISOString: () => sql`now()` as any,
92
+ }) as any,
93
+ ),
94
+ },
95
+ (table) => [
96
+ // Single column indexes
97
+ index('idx_job_steps_job_id').on(table.job_id),
98
+ index('idx_job_steps_status').on(table.status),
99
+ index('idx_job_steps_name').on(table.name),
100
+ index('idx_job_steps_expires_at').on(table.expires_at),
101
+ // Composite indexes
102
+ index('idx_job_steps_job_status').on(table.job_id, table.status),
103
+ index('idx_job_steps_job_name').on(table.job_id, table.name),
104
+ index('idx_job_steps_output_fts').using('gin', sql`to_tsvector('english', ${table.output}::text)`),
105
+ // Unique constraint
106
+ unique('unique_job_step_name').on(table.job_id, table.name),
107
+ check(
108
+ 'job_steps_status_check',
109
+ sql`${table.status} IN ${sql.raw(`(${STEP_STATUSES.map((s) => `'${s}'`).join(',')})`)}`,
110
+ ),
111
+ ],
112
+ )
113
+
114
+ return {
115
+ schema,
116
+ jobsTable,
117
+ jobStepsTable,
118
+ }
119
+ }
@@ -0,0 +1,320 @@
1
+ import { z } from 'zod'
2
+
3
+ import { JOB_STATUSES, STEP_STATUSES } from '../constants.js'
4
+
5
+ // ============================================================================
6
+ // Status Enums
7
+ // ============================================================================
8
+
9
+ export const JobStatusSchema = z.enum(JOB_STATUSES)
10
+ export const StepStatusSchema = z.enum(STEP_STATUSES)
11
+
12
+ // ============================================================================
13
+ // Date Schema
14
+ // ============================================================================
15
+
16
+ const DateSchema = z.union([
17
+ z.date(),
18
+ z.string().transform((str) => new Date(str)),
19
+ z.number().transform((num) => new Date(num)),
20
+ ])
21
+
22
+ export const SerializableErrorSchema = z.object({
23
+ name: z.string(),
24
+ message: z.string(),
25
+ cause: z.any().optional(),
26
+ stack: z.string().optional(),
27
+ })
28
+
29
+ // ============================================================================
30
+ // Job Schema
31
+ // ============================================================================
32
+
33
+ export const JobSchema = z.object({
34
+ id: z.string(),
35
+ actionName: z.string(),
36
+ groupKey: z.string(),
37
+ input: z.any(),
38
+ output: z.any().nullable(),
39
+ error: z.any().nullable(),
40
+ status: JobStatusSchema,
41
+ timeoutMs: z.coerce.number(),
42
+ expiresAt: DateSchema.nullable(),
43
+ startedAt: DateSchema.nullable().default(null),
44
+ finishedAt: DateSchema.nullable().default(null),
45
+ createdAt: DateSchema,
46
+ updatedAt: DateSchema,
47
+ concurrencyLimit: z.coerce.number(),
48
+ })
49
+
50
+ // ============================================================================
51
+ // JobStep Schema
52
+ // ============================================================================
53
+
54
+ export const JobStepSchema = z.object({
55
+ id: z.string(),
56
+ jobId: z.string(),
57
+ name: z.string(),
58
+ output: z.any().nullable().default(null),
59
+ status: StepStatusSchema,
60
+ error: z.any().nullable().default(null),
61
+ startedAt: DateSchema,
62
+ finishedAt: DateSchema.nullable().default(null),
63
+ timeoutMs: z.coerce.number(),
64
+ expiresAt: DateSchema.nullable().default(null),
65
+ retriesLimit: z.coerce.number(),
66
+ retriesCount: z.coerce.number(),
67
+ delayedMs: z.coerce.number().nullable().default(null),
68
+ historyFailedAttempts: z.record(
69
+ z.string(),
70
+ z.object({ failedAt: DateSchema, error: SerializableErrorSchema, delayedMs: z.coerce.number() }),
71
+ ),
72
+ createdAt: DateSchema,
73
+ updatedAt: DateSchema,
74
+ })
75
+
76
+ // JobStep without output (for getJobSteps)
77
+ export const JobStepWithoutOutputSchema = JobStepSchema.omit({ output: true })
78
+
79
+ // ============================================================================
80
+ // Query Option Schemas
81
+ // ============================================================================
82
+
83
+ export const SortOrderSchema = z.enum(['asc', 'desc'])
84
+
85
+ export const JobSortFieldSchema = z.enum(['createdAt', 'startedAt', 'finishedAt', 'status', 'actionName', 'expiresAt'])
86
+
87
+ export const JobSortSchema = z.object({
88
+ field: JobSortFieldSchema,
89
+ order: SortOrderSchema,
90
+ })
91
+
92
+ export const JobFiltersSchema = z.object({
93
+ status: z.union([JobStatusSchema, z.array(JobStatusSchema)]).optional(),
94
+ actionName: z.union([z.string(), z.array(z.string())]).optional(),
95
+ groupKey: z.union([z.string(), z.array(z.string())]).optional(),
96
+ ownerId: z.union([z.string(), z.array(z.string())]).optional(),
97
+ createdAt: z.union([DateSchema, z.array(DateSchema).length(2)]).optional(),
98
+ startedAt: z.union([DateSchema, z.array(DateSchema).length(2)]).optional(),
99
+ finishedAt: z.union([DateSchema, z.array(DateSchema).length(2)]).optional(),
100
+ updatedAfter: DateSchema.optional(),
101
+ inputFilter: z.record(z.string(), z.any()).optional(),
102
+ outputFilter: z.record(z.string(), z.any()).optional(),
103
+ search: z.string().optional(),
104
+ })
105
+
106
+ export const GetJobsOptionsSchema = z.object({
107
+ page: z.number().int().positive().optional(),
108
+ pageSize: z.number().int().positive().optional(),
109
+ filters: JobFiltersSchema.optional(),
110
+ sort: z.union([JobSortSchema, z.array(JobSortSchema)]).optional(),
111
+ })
112
+
113
+ export const GetJobStepsOptionsSchema = z.object({
114
+ jobId: z.string(),
115
+ page: z.number().int().positive().optional(),
116
+ pageSize: z.number().int().positive().optional(),
117
+ search: z.string().optional(),
118
+ updatedAfter: DateSchema.optional(),
119
+ })
120
+
121
+ // ============================================================================
122
+ // Job Option Schemas
123
+ // ============================================================================
124
+
125
+ export const CreateJobOptionsSchema = z.object({
126
+ /** The queue name (action name) */
127
+ queue: z.string(),
128
+ /** The group key for concurrency control */
129
+ groupKey: z.string(),
130
+ /** The checksum of the action */
131
+ checksum: z.string(),
132
+ /** The job input data */
133
+ input: z.any(),
134
+ /** Timeout in milliseconds for the job */
135
+ timeoutMs: z.number(),
136
+ /** The concurrency limit for this job's group */
137
+ concurrencyLimit: z.number(),
138
+ })
139
+
140
+ export const RecoverJobsOptionsSchema = z.object({
141
+ /** The action checksums to recover jobs for */
142
+ checksums: z.array(z.string()),
143
+ /** Whether to ping other processes before recovering their jobs */
144
+ multiProcessMode: z.boolean().optional(),
145
+ /** Timeout in milliseconds to wait for process ping responses */
146
+ processTimeout: z.number().optional(),
147
+ })
148
+
149
+ export const FetchOptionsSchema = z.object({
150
+ /** Maximum number of jobs to fetch in this batch */
151
+ batch: z.number(),
152
+ })
153
+
154
+ export const CompleteJobOptionsSchema = z.object({
155
+ /** The ID of the job to complete */
156
+ jobId: z.string(),
157
+ /** The job output data */
158
+ output: z.any(),
159
+ })
160
+
161
+ export const FailJobOptionsSchema = z.object({
162
+ /** The ID of the job to fail */
163
+ jobId: z.string(),
164
+ /** The error data */
165
+ error: z.any(),
166
+ })
167
+
168
+ export const CancelJobOptionsSchema = z.object({
169
+ /** The ID of the job to cancel */
170
+ jobId: z.string(),
171
+ })
172
+
173
+ export const RetryJobOptionsSchema = z.object({
174
+ /** The ID of the job to retry */
175
+ jobId: z.string(),
176
+ })
177
+
178
+ export const DeleteJobOptionsSchema = z.object({
179
+ /** The ID of the job to delete */
180
+ jobId: z.string(),
181
+ })
182
+
183
+ export const DeleteJobsOptionsSchema = GetJobsOptionsSchema.optional()
184
+
185
+ // ============================================================================
186
+ // Step Option Schemas
187
+ // ============================================================================
188
+
189
+ export const CreateOrRecoverJobStepOptionsSchema = z.object({
190
+ /** The ID of the job this step belongs to */
191
+ jobId: z.string(),
192
+ /** The name of the step */
193
+ name: z.string(),
194
+ /** Timeout in milliseconds for the step */
195
+ timeoutMs: z.number(),
196
+ /** Maximum number of retries for the step */
197
+ retriesLimit: z.number(),
198
+ })
199
+
200
+ export const CompleteJobStepOptionsSchema = z.object({
201
+ /** The ID of the step to complete */
202
+ stepId: z.string(),
203
+ /** The step output data */
204
+ output: z.any(),
205
+ })
206
+
207
+ export const FailJobStepOptionsSchema = z.object({
208
+ /** The ID of the step to fail */
209
+ stepId: z.string(),
210
+ /** The error data */
211
+ error: z.any(),
212
+ })
213
+
214
+ export const DelayJobStepOptionsSchema = z.object({
215
+ /** The ID of the step to delay */
216
+ stepId: z.string(),
217
+ /** The delay in milliseconds */
218
+ delayMs: z.number(),
219
+ /** The error data */
220
+ error: z.any(),
221
+ })
222
+
223
+ export const CancelJobStepOptionsSchema = z.object({
224
+ /** The ID of the step to cancel */
225
+ stepId: z.string(),
226
+ })
227
+
228
+ export const CreateOrRecoverJobStepResultSchema = z.object({
229
+ id: z.string(),
230
+ status: StepStatusSchema,
231
+ retriesLimit: z.number(),
232
+ retriesCount: z.number(),
233
+ timeoutMs: z.number(),
234
+ error: z.any().nullable(),
235
+ output: z.any().nullable(),
236
+ isNew: z.boolean(),
237
+ })
238
+
239
+ // ============================================================================
240
+ // Response Schemas
241
+ // ============================================================================
242
+
243
+ // Simple response schemas
244
+ export const JobIdResultSchema = z.union([z.string(), z.null()])
245
+ export const BooleanResultSchema = z.boolean()
246
+ export const NumberResultSchema = z.number()
247
+ export const JobsArrayResultSchema = z.array(JobSchema)
248
+ export const CreateOrRecoverJobStepResultNullableSchema = z.union([CreateOrRecoverJobStepResultSchema, z.null()])
249
+
250
+ export const GetJobsResultSchema = z.object({
251
+ jobs: z.array(JobSchema),
252
+ total: z.number().int().nonnegative(),
253
+ page: z.number().int().positive(),
254
+ pageSize: z.number().int().positive(),
255
+ })
256
+
257
+ export const GetJobStepsResultSchema = z.object({
258
+ steps: z.array(JobStepWithoutOutputSchema),
259
+ total: z.number().int().nonnegative(),
260
+ page: z.number().int().positive(),
261
+ pageSize: z.number().int().positive(),
262
+ })
263
+
264
+ export const ActionStatsSchema = z.object({
265
+ name: z.string(),
266
+ lastJobCreated: DateSchema.nullable(),
267
+ active: z.number().int().nonnegative(),
268
+ completed: z.number().int().nonnegative(),
269
+ failed: z.number().int().nonnegative(),
270
+ cancelled: z.number().int().nonnegative(),
271
+ })
272
+
273
+ export const GetActionsResultSchema = z.object({
274
+ actions: z.array(ActionStatsSchema),
275
+ })
276
+
277
+ export const JobStatusResultSchema = z.object({
278
+ status: JobStatusSchema,
279
+ updatedAt: DateSchema,
280
+ })
281
+
282
+ export const JobStepStatusResultSchema = z.object({
283
+ status: StepStatusSchema,
284
+ updatedAt: DateSchema,
285
+ })
286
+
287
+ // ============================================================================
288
+ // Type Exports
289
+ // ============================================================================
290
+
291
+ export type Job = z.infer<typeof JobSchema>
292
+ export type JobStep = z.infer<typeof JobStepSchema>
293
+ export type JobStepWithoutOutput = z.infer<typeof JobStepWithoutOutputSchema>
294
+ export type SortOrder = z.infer<typeof SortOrderSchema>
295
+ export type JobSortField = z.infer<typeof JobSortFieldSchema>
296
+ export type JobSort = z.infer<typeof JobSortSchema>
297
+ export type JobFilters = z.infer<typeof JobFiltersSchema>
298
+ export type GetJobsOptions = z.infer<typeof GetJobsOptionsSchema>
299
+ export type GetJobStepsOptions = z.infer<typeof GetJobStepsOptionsSchema>
300
+ export type GetJobsResult = z.infer<typeof GetJobsResultSchema>
301
+ export type GetJobStepsResult = z.infer<typeof GetJobStepsResultSchema>
302
+ export type ActionStats = z.infer<typeof ActionStatsSchema>
303
+ export type GetActionsResult = z.infer<typeof GetActionsResultSchema>
304
+ export type JobStatusResult = z.infer<typeof JobStatusResultSchema>
305
+ export type JobStepStatusResult = z.infer<typeof JobStepStatusResultSchema>
306
+ export type CreateJobOptions = z.infer<typeof CreateJobOptionsSchema>
307
+ export type RecoverJobsOptions = z.infer<typeof RecoverJobsOptionsSchema>
308
+ export type FetchOptions = z.infer<typeof FetchOptionsSchema>
309
+ export type CompleteJobOptions = z.infer<typeof CompleteJobOptionsSchema>
310
+ export type FailJobOptions = z.infer<typeof FailJobOptionsSchema>
311
+ export type CancelJobOptions = z.infer<typeof CancelJobOptionsSchema>
312
+ export type RetryJobOptions = z.infer<typeof RetryJobOptionsSchema>
313
+ export type DeleteJobOptions = z.infer<typeof DeleteJobOptionsSchema>
314
+ export type DeleteJobsOptions = z.infer<typeof DeleteJobsOptionsSchema>
315
+ export type CreateOrRecoverJobStepOptions = z.infer<typeof CreateOrRecoverJobStepOptionsSchema>
316
+ export type CompleteJobStepOptions = z.infer<typeof CompleteJobStepOptionsSchema>
317
+ export type FailJobStepOptions = z.infer<typeof FailJobStepOptionsSchema>
318
+ export type DelayJobStepOptions = z.infer<typeof DelayJobStepOptionsSchema>
319
+ export type CancelJobStepOptions = z.infer<typeof CancelJobStepOptionsSchema>
320
+ export type CreateOrRecoverJobStepResult = z.infer<typeof CreateOrRecoverJobStepResultSchema>