duron 0.3.0-beta.12 → 0.3.0-beta.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duron",
3
- "version": "0.3.0-beta.12",
3
+ "version": "0.3.0-beta.14",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/action.ts CHANGED
@@ -183,95 +183,274 @@ export interface ConcurrencyHandlerContext<TInput extends z.ZodObject, TVariable
183
183
  var: TVariables
184
184
  }
185
185
 
186
- export type ActionDefinition<
187
- TInput extends z.ZodObject,
188
- TOutput extends z.ZodObject,
189
- TVariables = Record<string, unknown>,
190
- > = z.input<ReturnType<typeof createActionDefinitionSchema<TInput, TOutput, TVariables>>>
191
-
192
- export type Action<
193
- TInput extends z.ZodObject,
194
- TOutput extends z.ZodObject,
195
- TVariables = Record<string, unknown>,
196
- > = z.infer<ReturnType<typeof createActionDefinitionSchema<TInput, TOutput, TVariables>>>
197
-
198
186
  /**
199
187
  * Retry configuration options for actions and steps.
188
+ * Controls how failed operations are retried with exponential backoff.
200
189
  */
201
- export const RetryOptionsSchema = z
202
- .object({
203
- /**
204
- * Maximum number of retry attempts.
205
- *
206
- * @default 4
207
- */
208
- limit: z.number().default(4),
190
+ export interface RetryOptionsInput {
191
+ /**
192
+ * Maximum number of retry attempts.
193
+ * Set to 0 to disable retries.
194
+ *
195
+ * @default 4
196
+ */
197
+ limit?: number
209
198
 
210
- /**
211
- * Exponential backoff factor.
212
- * The delay between retries is calculated as: minTimeout * (factor ^ attemptNumber)
213
- *
214
- * @default 2
215
- */
216
- factor: z.number().default(2),
199
+ /**
200
+ * Exponential backoff factor.
201
+ * The delay between retries is calculated as: `minTimeout * (factor ^ attemptNumber)`
202
+ *
203
+ * @default 2
204
+ */
205
+ factor?: number
217
206
 
218
- /**
219
- * Minimum delay in milliseconds before the first retry.
220
- *
221
- * @default 1000
222
- */
223
- minTimeout: z.number().default(1000),
207
+ /**
208
+ * Minimum delay in milliseconds before the first retry.
209
+ * This is the base delay that gets multiplied by the factor.
210
+ *
211
+ * @default 1000
212
+ */
213
+ minTimeout?: number
224
214
 
225
- /**
226
- * Maximum delay in milliseconds between retries.
227
- * The calculated delay will be capped at this value.
228
- *
229
- * @default 30000
230
- */
231
- maxTimeout: z.number().default(30000),
232
- })
233
- .default({ limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 })
234
- .describe('The retry options')
215
+ /**
216
+ * Maximum delay in milliseconds between retries.
217
+ * The calculated delay will be capped at this value to prevent
218
+ * excessively long wait times.
219
+ *
220
+ * @default 30000
221
+ */
222
+ maxTimeout?: number
223
+ }
235
224
 
236
225
  /**
237
- * Options for configuring a step within an action.
226
+ * Configuration options for steps within an action.
227
+ * Controls concurrency, retries, and timeouts for step execution.
238
228
  */
239
- export const StepOptionsSchema = z.object({
229
+ export interface StepsConfigInput {
240
230
  /**
241
- * Retry configuration for this step.
242
- * If not provided, uses the default retry options from the action or Duron instance.
231
+ * Maximum number of steps that can run concurrently within this action.
232
+ * Higher values allow more parallelism but may increase resource usage.
233
+ *
234
+ * @default 100
243
235
  */
244
- retry: RetryOptionsSchema,
236
+ concurrency?: number
245
237
 
246
238
  /**
247
- * Timeout in milliseconds for this step.
248
- * Steps that exceed this timeout will be cancelled.
239
+ * Retry configuration for steps.
240
+ * These settings apply to all steps unless overridden at the step level.
241
+ *
242
+ * @default { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 }
243
+ */
244
+ retry?: RetryOptionsInput
245
+
246
+ /**
247
+ * Timeout in milliseconds for each step.
248
+ * Steps that exceed this timeout will be cancelled and may be retried.
249
249
  *
250
250
  * @default 300000 (5 minutes)
251
251
  */
252
- expire: z
253
- .number()
254
- .default(5 * 60 * 1000)
255
- .describe('The expire time for the step (milliseconds)'),
252
+ expire?: number
253
+ }
256
254
 
255
+ /**
256
+ * Group configuration for concurrency control.
257
+ * Allows grouping jobs by key and controlling concurrency per group.
258
+ */
259
+ export interface GroupsConfigInput<TInput extends z.ZodObject, TVariables = Record<string, unknown>> {
257
260
  /**
258
- * Whether this step runs in parallel with siblings.
259
- * Parallel steps are independent from siblings during time travel.
260
- * When time traveling to a step, completed parallel siblings are preserved.
261
+ * Function to determine the group key for a job.
262
+ * Jobs with the same group key will respect the group concurrency limit.
263
+ * Use this to limit concurrent processing of related jobs (e.g., per user, per tenant).
264
+ *
265
+ * If not provided, all jobs for this action will use the '@default' group key.
261
266
  *
262
- * @default false
267
+ * @param ctx - Context containing the validated input and variables
268
+ * @returns Promise resolving to the group key string
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * groupKey: async (ctx) => `user:${ctx.input.userId}`
273
+ * ```
263
274
  */
264
- parallel: z.boolean().default(false).describe('Whether this step runs in parallel (independent from siblings)'),
265
- })
275
+ groupKey?: (ctx: ConcurrencyHandlerContext<TInput, TVariables>) => Promise<string>
276
+
277
+ /**
278
+ * Function to dynamically determine the concurrency limit for a job's group.
279
+ * The concurrency limit is stored with each job and used during fetch operations.
280
+ * This allows different groups to have different concurrency limits.
281
+ *
282
+ * If not provided, uses the global `groupConcurrencyLimit` from the client options.
283
+ *
284
+ * @param ctx - Context containing the validated input and variables
285
+ * @returns Promise resolving to the concurrency limit number
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * concurrency: async (ctx) => ctx.input.priority === 'high' ? 10 : 2
290
+ * ```
291
+ */
292
+ concurrency?: (ctx: ConcurrencyHandlerContext<TInput, TVariables>) => Promise<number>
293
+ }
266
294
 
267
295
  /**
268
- * Creates a Zod schema for validating action definitions.
296
+ * Definition for creating a Duron action.
297
+ * Actions are type-safe, durable job handlers with built-in retry logic,
298
+ * step-based execution, and concurrency control.
269
299
  *
270
- * @template TInput - Zod schema for the action input
271
- * @template TOutput - Zod schema for the action output
272
- * @template TVariables - Type of variables available to the action
273
- * @returns Zod schema for action definitions
300
+ * @template TInput - Zod schema type for validating the action input
301
+ * @template TOutput - Zod schema type for validating the action output
302
+ * @template TVariables - Type of variables available to the action handler
274
303
  */
304
+ export interface ActionDefinitionInput<
305
+ TInput extends z.ZodObject,
306
+ TOutput extends z.ZodObject,
307
+ TVariables = Record<string, unknown>,
308
+ > {
309
+ /**
310
+ * Unique name for this action.
311
+ * Used as the queue name and must be unique across all actions registered with a client.
312
+ * This name is used for job routing, logging, and dashboard display.
313
+ *
314
+ * @example 'send-email', 'process-payment', 'sync-user-data'
315
+ */
316
+ name: string
317
+
318
+ /**
319
+ * Optional version string for the action.
320
+ * Used to track changes to the action and included in the checksum calculation.
321
+ * Changing the version will cause existing jobs to be treated as having a different checksum.
322
+ *
323
+ * @example '1.0.0', '2024-01-15', 'v2'
324
+ */
325
+ version?: string
326
+
327
+ /**
328
+ * Zod schema for validating the action input.
329
+ * If provided, input will be validated before the handler is called.
330
+ * Invalid input will throw a validation error and the job will fail.
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * input: z.object({
335
+ * email: z.string().email(),
336
+ * subject: z.string().min(1),
337
+ * })
338
+ * ```
339
+ */
340
+ input?: TInput
341
+
342
+ /**
343
+ * Zod schema for validating the action output.
344
+ * If provided, output will be validated after the handler completes.
345
+ * Invalid output will cause the job to fail.
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * output: z.object({
350
+ * success: z.boolean(),
351
+ * messageId: z.string().optional(),
352
+ * })
353
+ * ```
354
+ */
355
+ output?: TOutput
356
+
357
+ /**
358
+ * Group configuration for concurrency control.
359
+ * Allows grouping jobs by a dynamic key and controlling concurrency per group.
360
+ * Useful for rate limiting per user, tenant, or resource.
361
+ */
362
+ groups?: GroupsConfigInput<TInput, TVariables>
363
+
364
+ /**
365
+ * Configuration for steps within this action.
366
+ * Steps are retryable units of work that can be executed within the action handler.
367
+ * These settings apply to all steps unless overridden at the step level.
368
+ *
369
+ * @default { concurrency: 100, retry: { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 }, expire: 300000 }
370
+ */
371
+ steps?: StepsConfigInput
372
+
373
+ /**
374
+ * Maximum number of jobs for this action that can run concurrently across all workers.
375
+ * This limit is enforced per action, regardless of group.
376
+ *
377
+ * @default 100
378
+ */
379
+ concurrency?: number
380
+
381
+ /**
382
+ * Timeout in milliseconds for the entire action.
383
+ * Jobs that exceed this timeout will be cancelled.
384
+ * Make sure this is greater than the expected total execution time including all steps.
385
+ *
386
+ * @default 900000 (15 minutes)
387
+ */
388
+ expire?: number
389
+
390
+ /**
391
+ * The handler function that executes the action logic.
392
+ * Receives a context object with validated input, variables, logger, and step functions.
393
+ * Must return a Promise that resolves to the action output (matching the output schema if provided).
394
+ *
395
+ * @param ctx - Action handler context with input, variables, and step functions
396
+ * @returns Promise resolving to the action output
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * handler: async (ctx) => {
401
+ * const result = await ctx.step('send', async () => {
402
+ * return await sendEmail(ctx.input.email, ctx.input.subject)
403
+ * })
404
+ * return { success: true, messageId: result.id }
405
+ * }
406
+ * ```
407
+ */
408
+ handler: (ctx: ActionHandlerContext<TInput, TVariables>) => Promise<z.infer<TOutput>>
409
+ }
410
+
411
+ // Type alias for backwards compatibility (inferred from Zod schema)
412
+ export type ActionDefinition<
413
+ TInput extends z.ZodObject,
414
+ TOutput extends z.ZodObject,
415
+ TVariables = Record<string, unknown>,
416
+ > = ActionDefinitionInput<TInput, TOutput, TVariables>
417
+
418
+ // Compile-time check: ensure ActionDefinitionInput is assignable to the Zod schema's input type
419
+ type _EnsureActionDefinitionCompatible<
420
+ TInput extends z.ZodObject,
421
+ TOutput extends z.ZodObject,
422
+ TVariables,
423
+ > = ActionDefinitionInput<TInput, TOutput, TVariables> extends z.input<
424
+ ReturnType<typeof createActionDefinitionSchema<TInput, TOutput, TVariables>>
425
+ >
426
+ ? true
427
+ : 'ERROR: ActionDefinitionInput does not match Zod schema input type'
428
+
429
+ // This will cause a compile error if the types diverge
430
+ declare const _actionDefCheck: _EnsureActionDefinitionCompatible<z.ZodObject<any>, z.ZodObject<any>, any>
431
+ const _checkActionDef: _EnsureActionDefinitionCompatible<z.ZodObject<any>, z.ZodObject<any>, any> = true
432
+
433
+ export type Action<
434
+ TInput extends z.ZodObject,
435
+ TOutput extends z.ZodObject,
436
+ TVariables = Record<string, unknown>,
437
+ > = z.infer<ReturnType<typeof createActionDefinitionSchema<TInput, TOutput, TVariables>>>
438
+
439
+ export const RetryOptionsSchema = z
440
+ .object({
441
+ limit: z.number().default(4),
442
+ factor: z.number().default(2),
443
+ minTimeout: z.number().default(1000),
444
+ maxTimeout: z.number().default(30000),
445
+ })
446
+ .default({ limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 })
447
+
448
+ export const StepOptionsSchema = z.object({
449
+ retry: RetryOptionsSchema,
450
+ expire: z.number().default(5 * 60 * 1000),
451
+ parallel: z.boolean().default(false),
452
+ })
453
+
275
454
  export function createActionDefinitionSchema<
276
455
  TInput extends z.ZodObject,
277
456
  TOutput extends z.ZodObject,
@@ -279,66 +458,25 @@ export function createActionDefinitionSchema<
279
458
  >() {
280
459
  return z
281
460
  .object({
282
- /**
283
- * Unique name for this action.
284
- * Used as the queue name and must be unique across all actions.
285
- * Required.
286
- */
287
- name: z.string().describe('The name of the action'),
288
-
289
- /**
290
- * Version of the action.
291
- * Used to track changes to the action and generate the checksum.
292
- */
293
- version: z.string().describe('The version of the action').optional(),
294
-
295
- /**
296
- * Zod schema for validating the action input.
297
- * If provided, input will be validated before the handler is called.
298
- * If not provided, any input will be accepted.
299
- */
461
+ name: z.string(),
462
+ version: z.string().optional(),
300
463
  input: z
301
464
  .custom<TInput>((val: any) => {
302
465
  return !val || ('_zod' in val && 'type' in val && val.type === 'object')
303
466
  })
304
467
  .optional(),
305
-
306
- /**
307
- * Zod schema for validating the action output.
308
- * If provided, output will be validated after the handler completes.
309
- * If not provided, any output will be accepted.
310
- */
311
468
  output: z
312
469
  .custom<TOutput>((val: any) => {
313
470
  return !val || ('_zod' in val && 'type' in val && val.type === 'object')
314
471
  })
315
472
  .optional(),
316
-
317
473
  groups: z
318
474
  .object({
319
- /**
320
- * Function to determine the group key for a job.
321
- * Jobs with the same group key will respect the group concurrency limit.
322
- * If not provided, all jobs for this action will use the '@default' group key.
323
- *
324
- * @param ctx - Context containing the input and variables
325
- * @returns Promise resolving to the group key string
326
- */
327
475
  groupKey: z
328
476
  .custom<(ctx: ConcurrencyHandlerContext<TInput, TVariables>) => Promise<string>>((val) => {
329
477
  return !val || val instanceof Function
330
478
  })
331
479
  .optional(),
332
-
333
- /**
334
- * Function to determine the concurrency limit for a job.
335
- * The concurrency limit is stored with each job and used during fetch operations.
336
- * When fetching jobs, the latest job's concurrency limit is used for each groupKey.
337
- * If not provided, defaults to 10.
338
- *
339
- * @param ctx - Context containing the input and variables
340
- * @returns Promise resolving to the concurrency limit number
341
- */
342
480
  concurrency: z
343
481
  .custom<(ctx: ConcurrencyHandlerContext<TInput, TVariables>) => Promise<number>>((val) => {
344
482
  return !val || val instanceof Function
@@ -346,49 +484,22 @@ export function createActionDefinitionSchema<
346
484
  .optional(),
347
485
  })
348
486
  .optional(),
349
-
350
487
  steps: z
351
488
  .object({
352
- /**
353
- * Function to determine the concurrency limit for a step.
354
- * The concurrency limit is stored with each step and used during fetch operations.
355
- * When fetching steps, the latest step's concurrency limit is used for each stepKey.
356
- * If not provided, defaults to 100.
357
- */
358
- concurrency: z.number().default(100).describe('How many steps can run concurrently for this action'),
359
- retry: RetryOptionsSchema.describe('How to retry on failure for the steps of this action'),
360
- expire: z
361
- .number()
362
- .default(5 * 60 * 1000)
363
- .describe('How long a step can run for (milliseconds)'),
489
+ concurrency: z.number().default(100),
490
+ retry: RetryOptionsSchema,
491
+ expire: z.number().default(5 * 60 * 1000),
364
492
  })
365
493
  .default({
366
494
  concurrency: 100,
367
495
  retry: { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 },
368
496
  expire: 5 * 60 * 1000,
369
497
  }),
370
-
371
- concurrency: z.number().default(100).describe('How many jobs can run concurrently for this action'),
372
-
373
- expire: z
374
- .number()
375
- .default(15 * 60 * 1000)
376
- .describe('How long a job can run for (milliseconds)'),
377
-
378
- /**
379
- * The handler function that executes the action logic.
380
- * Receives a context object with input, variables, and a step function.
381
- * Must return a Promise that resolves to the action output.
382
- * Required.
383
- *
384
- * @param ctx - Action handler context
385
- * @returns Promise resolving to the action output
386
- */
387
- handler: z
388
- .custom<(ctx: ActionHandlerContext<TInput, TVariables>) => Promise<z.infer<TOutput>>>((val) => {
389
- return val instanceof Function
390
- })
391
- .describe('The handler for the action'),
498
+ concurrency: z.number().default(100),
499
+ expire: z.number().default(15 * 60 * 1000),
500
+ handler: z.custom<(ctx: ActionHandlerContext<TInput, TVariables>) => Promise<z.infer<TOutput>>>((val) => {
501
+ return val instanceof Function
502
+ }),
392
503
  })
393
504
  .transform((def) => {
394
505
  const checksum = [def.name, def.version, def.handler.toString()].filter(Boolean).join(':')