duron 0.3.0-beta.9 → 0.3.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 (91) hide show
  1. package/dist/action-job.d.ts +33 -2
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +88 -23
  4. package/dist/action-manager.d.ts +44 -2
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +64 -3
  7. package/dist/action.d.ts +388 -7
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +44 -23
  10. package/dist/adapters/adapter.d.ts +365 -8
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +221 -15
  13. package/dist/adapters/postgres/base.d.ts +184 -6
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +436 -75
  16. package/dist/adapters/postgres/pglite.d.ts +37 -0
  17. package/dist/adapters/postgres/pglite.d.ts.map +1 -1
  18. package/dist/adapters/postgres/pglite.js +38 -0
  19. package/dist/adapters/postgres/postgres.d.ts +35 -0
  20. package/dist/adapters/postgres/postgres.d.ts.map +1 -1
  21. package/dist/adapters/postgres/postgres.js +42 -0
  22. package/dist/adapters/postgres/schema.d.ts +150 -37
  23. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  24. package/dist/adapters/postgres/schema.default.d.ts +151 -38
  25. package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
  26. package/dist/adapters/postgres/schema.default.js +2 -2
  27. package/dist/adapters/postgres/schema.js +60 -23
  28. package/dist/adapters/schemas.d.ts +124 -80
  29. package/dist/adapters/schemas.d.ts.map +1 -1
  30. package/dist/adapters/schemas.js +139 -26
  31. package/dist/client.d.ts +426 -22
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +370 -20
  34. package/dist/constants.js +6 -0
  35. package/dist/errors.d.ts +140 -3
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +152 -9
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/server.d.ts +99 -37
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +84 -25
  43. package/dist/step-manager.d.ts +111 -4
  44. package/dist/step-manager.d.ts.map +1 -1
  45. package/dist/step-manager.js +403 -75
  46. package/dist/telemetry/index.d.ts +1 -4
  47. package/dist/telemetry/index.d.ts.map +1 -1
  48. package/dist/telemetry/index.js +2 -4
  49. package/dist/telemetry/local-span-exporter.d.ts +56 -0
  50. package/dist/telemetry/local-span-exporter.d.ts.map +1 -0
  51. package/dist/telemetry/local-span-exporter.js +118 -0
  52. package/dist/utils/p-retry.d.ts +5 -0
  53. package/dist/utils/p-retry.d.ts.map +1 -1
  54. package/dist/utils/p-retry.js +8 -0
  55. package/dist/utils/wait-for-abort.d.ts +1 -0
  56. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  57. package/dist/utils/wait-for-abort.js +1 -0
  58. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/migration.sql +32 -20
  59. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/snapshot.json +241 -66
  60. package/package.json +42 -26
  61. package/src/action-job.ts +33 -29
  62. package/src/action-manager.ts +5 -5
  63. package/src/action.ts +317 -149
  64. package/src/adapters/adapter.ts +54 -54
  65. package/src/adapters/postgres/base.ts +266 -86
  66. package/src/adapters/postgres/schema.default.ts +2 -2
  67. package/src/adapters/postgres/schema.ts +52 -24
  68. package/src/adapters/schemas.ts +91 -36
  69. package/src/client.ts +322 -68
  70. package/src/errors.ts +84 -12
  71. package/src/index.ts +2 -0
  72. package/src/server.ts +39 -37
  73. package/src/step-manager.ts +246 -95
  74. package/src/telemetry/index.ts +2 -20
  75. package/src/telemetry/local-span-exporter.ts +148 -0
  76. package/dist/telemetry/adapter.d.ts +0 -107
  77. package/dist/telemetry/adapter.d.ts.map +0 -1
  78. package/dist/telemetry/adapter.js +0 -134
  79. package/dist/telemetry/local.d.ts +0 -22
  80. package/dist/telemetry/local.d.ts.map +0 -1
  81. package/dist/telemetry/local.js +0 -243
  82. package/dist/telemetry/noop.d.ts +0 -17
  83. package/dist/telemetry/noop.d.ts.map +0 -1
  84. package/dist/telemetry/noop.js +0 -66
  85. package/dist/telemetry/opentelemetry.d.ts +0 -25
  86. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  87. package/dist/telemetry/opentelemetry.js +0 -312
  88. package/src/telemetry/adapter.ts +0 -642
  89. package/src/telemetry/local.ts +0 -429
  90. package/src/telemetry/noop.ts +0 -141
  91. package/src/telemetry/opentelemetry.ts +0 -453
package/src/client.ts CHANGED
@@ -1,3 +1,8 @@
1
+ import { type Span, type Tracer, trace } from '@opentelemetry/api'
2
+ import { resourceFromAttributes } from '@opentelemetry/resources'
3
+ import { BatchSpanProcessor, type SpanExporter, type SpanProcessor } from '@opentelemetry/sdk-trace-base'
4
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
5
+ import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
1
6
  import pino, { type Logger } from 'pino'
2
7
  import { zocker } from 'zocker'
3
8
  import * as z from 'zod'
@@ -11,14 +16,14 @@ import type {
11
16
  GetJobStepsResult,
12
17
  GetJobsOptions,
13
18
  GetJobsResult,
14
- GetMetricsOptions,
15
- GetMetricsResult,
19
+ GetSpansOptions,
20
+ GetSpansResult,
16
21
  Job,
17
22
  JobStep,
18
23
  } from './adapters/adapter.js'
19
24
  import type { JobStatusResult, JobStepStatusResult } from './adapters/schemas.js'
20
25
  import { JOB_STATUS_CANCELLED, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, type JobStatus } from './constants.js'
21
- import { LocalTelemetryAdapter, noopTelemetryAdapter, type TelemetryAdapter } from './telemetry/index.js'
26
+ import { LocalSpanExporter } from './telemetry/local-span-exporter.js'
22
27
 
23
28
  /**
24
29
  * Extracts the inferred type from an action's input/output schema.
@@ -30,10 +35,11 @@ type InferActionSchema<T> = T extends z.ZodTypeAny ? z.infer<T> : Record<string,
30
35
  * Result returned from waitForJob with untyped input and output.
31
36
  */
32
37
  export interface JobResult {
33
- jobId: string
38
+ id: string
34
39
  actionName: string
35
40
  status: JobStatus
36
41
  groupKey: string
42
+ description: string | null
37
43
  input: unknown
38
44
  output: unknown
39
45
  error: Job['error']
@@ -43,102 +49,228 @@ export interface JobResult {
43
49
  * Result returned from runActionAndWait with typed input and output based on the action's Zod schemas.
44
50
  */
45
51
  export interface TypedJobResult<TAction extends Action<any, any, any>> {
46
- jobId: string
52
+ id: string
47
53
  actionName: string
48
54
  status: JobStatus
49
55
  groupKey: string
56
+ description: string | null
50
57
  input: InferActionSchema<NonNullable<TAction['input']>>
51
58
  output: InferActionSchema<NonNullable<TAction['output']>>
52
59
  error: Job['error']
53
60
  }
54
61
 
55
- const BaseOptionsSchema = z.object({
62
+ /**
63
+ * Telemetry context provided to action and step handlers.
64
+ * Provides access to OpenTelemetry APIs for recording traces and metrics.
65
+ */
66
+ export interface TelemetryContext {
67
+ /**
68
+ * Get the active OpenTelemetry span for the current job/step.
69
+ * Use standard OTel Span methods: setAttribute, addEvent, recordException, etc.
70
+ */
71
+ getActiveSpan(): Span
72
+
73
+ /**
74
+ * Get an OpenTelemetry tracer for creating custom spans.
75
+ *
76
+ * @param name - The name of the tracer (typically your service or library name)
77
+ */
78
+ getTracer(name: string): Tracer
79
+
80
+ /**
81
+ * Record a custom metric as a span event.
82
+ * This is a convenience method that stores metrics as span events
83
+ * which can be queried from the local database when telemetry.local is enabled.
84
+ *
85
+ * @param name - The metric name (e.g., 'tokens.input', 'latency.ms')
86
+ * @param value - The metric value
87
+ * @param attributes - Optional attributes for the metric
88
+ */
89
+ recordMetric(name: string, value: number, attributes?: Record<string, any>): void
90
+ }
91
+
92
+ /**
93
+ * Options for local telemetry storage.
94
+ */
95
+ export interface LocalTelemetryOptions {
96
+ /**
97
+ * Delay in milliseconds before flushing spans to the database.
98
+ * Uses BatchSpanProcessor with this delay.
99
+ * @default 5000
100
+ */
101
+ flushDelayMs?: number
102
+ }
103
+
104
+ /**
105
+ * Telemetry configuration options.
106
+ * Uses OpenTelemetry SDK for tracing.
107
+ */
108
+ export interface TelemetryOptions {
109
+ /**
110
+ * Enable local span storage in the database.
111
+ * When enabled, spans are stored in the database and can be queried via getSpans().
112
+ * Set to true for default options, or provide LocalTelemetryOptions for custom config.
113
+ */
114
+ local?: LocalTelemetryOptions | boolean
115
+
116
+ /**
117
+ * Additional span processors to add to the tracer provider.
118
+ * These are merged with the local processor (if enabled).
119
+ */
120
+ spanProcessors?: SpanProcessor[]
121
+
122
+ /**
123
+ * Additional span exporter to use.
124
+ * Will be wrapped in a BatchSpanProcessor and merged with other processors.
125
+ */
126
+ traceExporter?: SpanExporter
127
+
128
+ /**
129
+ * Service name for OpenTelemetry resource.
130
+ * @default 'duron'
131
+ */
132
+ serviceName?: string
133
+ }
134
+
135
+ /**
136
+ * Base configuration options for a Duron client instance.
137
+ * These options control job fetching, concurrency, and recovery behavior.
138
+ */
139
+ export interface BaseOptionsInput {
56
140
  /**
57
141
  * Unique identifier for this Duron instance.
58
142
  * Used for multi-process coordination and job ownership.
59
- * Defaults to a random UUID if not provided.
143
+ * If not provided, a random UUID will be generated.
144
+ *
145
+ * @example 'worker-1', 'api-server', 'background-processor'
60
146
  */
61
- id: z.string().optional(),
147
+ id?: string
62
148
 
63
149
  /**
64
- * Synchronization pattern for fetching jobs.
65
- * - `'pull'`: Periodically poll the database for new jobs
66
- * - `'push'`: Listen for database notifications when jobs are available
67
- * - `'hybrid'`: Use both pull and push patterns (recommended)
68
- * - `false`: Disable automatic job fetching (manual fetching only)
150
+ * Synchronization pattern for fetching jobs from the database.
151
+ *
152
+ * - `'pull'`: Periodically poll the database for new jobs at `pullInterval`
153
+ * - `'push'`: Listen for database notifications when jobs are available (real-time)
154
+ * - `'hybrid'`: Use both pull and push patterns (recommended for reliability)
155
+ * - `false`: Disable automatic job fetching (use `fetch()` manually)
69
156
  *
70
157
  * @default 'hybrid'
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * // Real-time job processing with fallback polling
162
+ * syncPattern: 'hybrid'
163
+ *
164
+ * // Disable auto-fetching for API-only servers
165
+ * syncPattern: false
166
+ * ```
71
167
  */
72
- syncPattern: z.union([z.literal('pull'), z.literal('push'), z.literal('hybrid'), z.literal(false)]).default('hybrid'),
168
+ syncPattern?: 'pull' | 'push' | 'hybrid' | false
73
169
 
74
170
  /**
75
- * Interval in milliseconds between pull operations when using pull or hybrid sync pattern.
171
+ * Interval in milliseconds between pull operations when using `'pull'` or `'hybrid'` sync pattern.
172
+ * Lower values mean faster job pickup but more database queries.
76
173
  *
77
174
  * @default 5000
78
175
  */
79
- pullInterval: z.number().default(5_000),
176
+ pullInterval?: number
80
177
 
81
178
  /**
82
- * Maximum number of jobs to fetch in a single batch.
179
+ * Maximum number of jobs to fetch in a single batch from the database.
180
+ * Higher values reduce database round-trips but may increase memory usage.
83
181
  *
84
182
  * @default 10
85
183
  */
86
- batchSize: z.number().default(10),
184
+ batchSize?: number
87
185
 
88
186
  /**
89
187
  * Maximum number of jobs that can run concurrently per action.
90
- * This controls the concurrency limit for the action's fastq queue.
188
+ * This controls the concurrency limit for each action's internal queue.
189
+ * Use this to prevent any single action from consuming all resources.
91
190
  *
92
191
  * @default 100
93
192
  */
94
- actionConcurrencyLimit: z.number().default(100),
193
+ actionConcurrencyLimit?: number
95
194
 
96
195
  /**
97
196
  * Maximum number of jobs that can run concurrently per group key.
98
197
  * Jobs with the same group key will respect this limit.
99
- * This can be overridden using action -> groups -> concurrency.
198
+ * This is the default value; it can be overridden per-job using `action.groups.concurrency`.
100
199
  *
101
200
  * @default 10
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * // Limit concurrent jobs per user to 2
205
+ * groupConcurrencyLimit: 2
206
+ * ```
102
207
  */
103
- groupConcurrencyLimit: z.number().default(10),
208
+ groupConcurrencyLimit?: number
104
209
 
105
210
  /**
106
211
  * Whether to run database migrations on startup.
107
212
  * When enabled, Duron will automatically apply pending migrations when the adapter starts.
213
+ * Disable this if you manage migrations separately or use a read-only database connection.
108
214
  *
109
215
  * @default true
110
216
  */
111
- migrateOnStart: z.boolean().default(true),
217
+ migrateOnStart?: boolean
112
218
 
113
219
  /**
114
220
  * Whether to recover stuck jobs on startup.
115
221
  * Stuck jobs are jobs that were marked as active but the process that owned them
116
- * is no longer running.
222
+ * is no longer running (e.g., after a crash or restart).
223
+ * These jobs will be reset to 'created' status so they can be picked up again.
117
224
  *
118
225
  * @default true
119
226
  */
120
- recoverJobsOnStart: z.boolean().default(true),
227
+ recoverJobsOnStart?: boolean
121
228
 
122
229
  /**
123
230
  * Enable multi-process mode for job recovery.
124
231
  * When enabled, Duron will ping other processes to check if they're alive
125
- * before recovering their jobs.
232
+ * before recovering their jobs. This prevents recovering jobs from processes
233
+ * that are still running but slow to respond.
234
+ *
235
+ * Only enable this if you're running multiple Duron instances sharing the same database.
126
236
  *
127
237
  * @default false
128
238
  */
129
- multiProcessMode: z.boolean().default(false),
239
+ multiProcessMode?: boolean
130
240
 
131
241
  /**
132
242
  * Timeout in milliseconds to wait for process ping responses in multi-process mode.
133
243
  * Processes that don't respond within this timeout will have their jobs recovered.
244
+ * Increase this value if your processes may be temporarily unresponsive under load.
134
245
  *
135
- * @default 5000 (5 seconds)
246
+ * @default 5000
136
247
  */
137
- processTimeout: z.number().default(5 * 1000), // 5 seconds
248
+ processTimeout?: number
249
+ }
250
+
251
+ const BaseOptionsSchema = z.object({
252
+ id: z.string().optional(),
253
+ syncPattern: z.union([z.literal('pull'), z.literal('push'), z.literal('hybrid'), z.literal(false)]).default('hybrid'),
254
+ pullInterval: z.number().default(5_000),
255
+ batchSize: z.number().default(10),
256
+ actionConcurrencyLimit: z.number().default(100),
257
+ groupConcurrencyLimit: z.number().default(10),
258
+ migrateOnStart: z.boolean().default(true),
259
+ recoverJobsOnStart: z.boolean().default(true),
260
+ multiProcessMode: z.boolean().default(false),
261
+ processTimeout: z.number().default(5 * 1000),
138
262
  })
139
263
 
264
+ // Compile-time check: ensure BaseOptionsInput is assignable to the Zod schema's input type
265
+ type _EnsureBaseOptionsCompatible = BaseOptionsInput extends z.input<typeof BaseOptionsSchema>
266
+ ? true
267
+ : 'ERROR: BaseOptionsInput does not match Zod schema input type'
268
+
269
+ declare const _baseOptionsCheck: _EnsureBaseOptionsCompatible
270
+ const _checkOptions: _EnsureBaseOptionsCompatible = true
271
+
140
272
  /**
141
- * Options for configuring a Duron instance.
273
+ * Options for configuring a Duron client instance.
142
274
  *
143
275
  * @template TActions - Record of action definitions keyed by action name
144
276
  * @template TVariables - Type of variables available to actions
@@ -146,7 +278,7 @@ const BaseOptionsSchema = z.object({
146
278
  export interface ClientOptions<
147
279
  TActions extends Record<string, Action<any, any, TVariables>>,
148
280
  TVariables = Record<string, unknown>,
149
- > extends z.input<typeof BaseOptionsSchema> {
281
+ > extends BaseOptionsInput {
150
282
  /**
151
283
  * The database adapter to use for storing jobs and steps.
152
284
  * Required.
@@ -173,15 +305,25 @@ export interface ClientOptions<
173
305
  variables?: TVariables
174
306
 
175
307
  /**
176
- * Optional telemetry adapter for observability.
177
- * When provided, traces job and step execution with spans and allows recording custom metrics.
308
+ * Optional telemetry configuration for observability.
309
+ * Uses OpenTelemetry SDK for tracing.
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * // Enable local span storage (stored in the database)
314
+ * telemetry: { local: true }
315
+ *
316
+ * // Enable local storage with custom flush delay
317
+ * telemetry: { local: { flushDelayMs: 10000 } }
178
318
  *
179
- * Available adapters:
180
- * - `openTelemetryAdapter()` - Export traces to external systems (Jaeger, OTLP, etc.)
181
- * - `localTelemetryAdapter({ database })` - Store metrics in the Duron database
182
- * - `noopTelemetryAdapter()` - No-op adapter (default)
319
+ * // Export to external systems (e.g., OTLP)
320
+ * telemetry: { traceExporter: new OTLPTraceExporter() }
321
+ *
322
+ * // Both local storage and external export
323
+ * telemetry: { local: true, traceExporter: new OTLPTraceExporter() }
324
+ * ```
183
325
  */
184
- telemetry?: TelemetryAdapter
326
+ telemetry?: TelemetryOptions
185
327
  }
186
328
 
187
329
  interface FetchOptions {
@@ -203,7 +345,10 @@ export class Client<
203
345
  #id: string
204
346
  #actions: TActions | null
205
347
  #database: Adapter
206
- #telemetry: TelemetryAdapter
348
+ #tracerProvider: NodeTracerProvider | null = null
349
+ #tracer: Tracer
350
+ #telemetryOptions: TelemetryOptions | null = null
351
+ #localSpansEnabled: boolean = false
207
352
  #variables: Record<string, unknown>
208
353
  #logger: Logger
209
354
  #started: boolean = false
@@ -237,14 +382,66 @@ export class Client<
237
382
  this.#options = BaseOptionsSchema.parse(options)
238
383
  this.#id = options.id ?? globalThis.crypto.randomUUID()
239
384
  this.#database = options.database
240
- this.#telemetry = options.telemetry ?? noopTelemetryAdapter()
385
+ this.#telemetryOptions = options.telemetry ?? null
241
386
  this.#actions = options.actions ?? null
242
387
  this.#variables = options?.variables ?? {}
243
388
  this.#logger = this.#normalizeLogger(options?.logger)
244
389
  this.#database.setId(this.#id)
245
390
  this.#database.setLogger(this.#logger)
246
- this.#telemetry.setLogger(this.#logger)
247
- this.#telemetry.setClient(this)
391
+
392
+ // Initialize OpenTelemetry TracerProvider if telemetry options are provided
393
+ // When no options are provided, the tracer will be a no-op (from OpenTelemetry API)
394
+ if (this.#telemetryOptions) {
395
+ this.#initTelemetry(this.#telemetryOptions)
396
+ }
397
+
398
+ // Get tracer from our provider if configured, otherwise use global no-op tracer
399
+ // This keeps telemetry scoped to this client instance rather than globally registered
400
+ this.#tracer = this.#tracerProvider?.getTracer('duron') ?? trace.getTracer('duron')
401
+ }
402
+
403
+ /**
404
+ * Initialize OpenTelemetry TracerProvider with configured processors.
405
+ */
406
+ #initTelemetry(options: TelemetryOptions): void {
407
+ const serviceName = options.serviceName ?? 'duron'
408
+ const processors: SpanProcessor[] = []
409
+
410
+ // Add local span exporter if enabled
411
+ if (options.local) {
412
+ const localOptions = typeof options.local === 'boolean' ? {} : options.local
413
+ const flushDelayMs = localOptions.flushDelayMs ?? 5000
414
+
415
+ const localExporter = new LocalSpanExporter({ adapter: this.#database })
416
+ processors.push(
417
+ new BatchSpanProcessor(localExporter, {
418
+ scheduledDelayMillis: flushDelayMs,
419
+ }),
420
+ )
421
+ this.#localSpansEnabled = true
422
+ }
423
+
424
+ // Add custom span processors
425
+ if (options.spanProcessors) {
426
+ processors.push(...options.spanProcessors)
427
+ }
428
+
429
+ // Add custom trace exporter wrapped in BatchSpanProcessor
430
+ if (options.traceExporter) {
431
+ processors.push(new BatchSpanProcessor(options.traceExporter))
432
+ }
433
+
434
+ // Only create TracerProvider if we have processors
435
+ if (processors.length > 0) {
436
+ this.#tracerProvider = new NodeTracerProvider({
437
+ resource: resourceFromAttributes({
438
+ [ATTR_SERVICE_NAME]: serviceName,
439
+ }),
440
+ spanProcessors: processors,
441
+ })
442
+ // Note: We do NOT call .register() here to avoid global state pollution
443
+ // The tracer is obtained directly from this provider instance
444
+ }
248
445
  }
249
446
 
250
447
  #normalizeLogger(logger?: Logger | 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent'): Logger {
@@ -268,10 +465,11 @@ export class Client<
268
465
  }
269
466
 
270
467
  /**
271
- * Get the telemetry adapter instance.
468
+ * Get the OpenTelemetry tracer for creating custom spans.
469
+ * Always returns a tracer - it's a no-op tracer when no SDK is configured.
272
470
  */
273
- get telemetry(): TelemetryAdapter {
274
- return this.#telemetry
471
+ get tracer(): Tracer {
472
+ return this.#tracer
275
473
  }
276
474
 
277
475
  /**
@@ -282,11 +480,21 @@ export class Client<
282
480
  }
283
481
 
284
482
  /**
285
- * Check if local telemetry is enabled.
286
- * Returns true if using LocalTelemetryAdapter.
483
+ * Check if local span storage is enabled.
484
+ * Returns true if telemetry.local is enabled.
485
+ */
486
+ get spansEnabled(): boolean {
487
+ return this.#localSpansEnabled
488
+ }
489
+
490
+ /**
491
+ * Force flush any pending telemetry data.
492
+ * Useful in tests or when you need to ensure spans are exported before querying.
287
493
  */
288
- get metricsEnabled(): boolean {
289
- return this.#telemetry instanceof LocalTelemetryAdapter
494
+ async flushTelemetry(): Promise<void> {
495
+ if (this.#tracerProvider) {
496
+ await this.#tracerProvider.forceFlush()
497
+ }
290
498
  }
291
499
 
292
500
  /**
@@ -348,6 +556,12 @@ export class Client<
348
556
  concurrencyLimit = await action.groups.concurrency(concurrencyCtx)
349
557
  }
350
558
 
559
+ // Calculate description if provided
560
+ let description: string | null = null
561
+ if (action.description) {
562
+ description = await action.description(concurrencyCtx)
563
+ }
564
+
351
565
  // Create job in database
352
566
  const jobId = await this.#database.createJob({
353
567
  queue: action.name,
@@ -356,6 +570,8 @@ export class Client<
356
570
  timeoutMs: action.expire,
357
571
  checksum: action.checksum,
358
572
  concurrencyLimit,
573
+ concurrencyStepLimit: action.steps.concurrency,
574
+ description,
359
575
  })
360
576
 
361
577
  if (!jobId) {
@@ -497,10 +713,10 @@ export class Client<
497
713
  * Fetch and process jobs from the database.
498
714
  * Concurrency limits are determined from the latest job created for each groupKey.
499
715
  *
500
- * @param options - Fetch options including batch size
716
+ * @param [options.batchSize] - Maximum number of jobs to fetch in this batch (defaults to `batchSize` from client options)
501
717
  * @returns Promise resolving to the array of fetched jobs
502
718
  */
503
- async fetch(options: FetchOptions) {
719
+ async fetch(options: FetchOptions = {}) {
504
720
  await this.start()
505
721
 
506
722
  if (!this.#actions) {
@@ -705,10 +921,11 @@ export class Client<
705
921
  return null
706
922
  }
707
923
  return {
708
- jobId: job.id,
924
+ id: job.id,
709
925
  actionName: job.actionName,
710
926
  status: job.status,
711
927
  groupKey: job.groupKey,
928
+ description: job.description,
712
929
  input: job.input,
713
930
  output: job.output,
714
931
  error: job.error,
@@ -770,19 +987,19 @@ export class Client<
770
987
  }
771
988
 
772
989
  /**
773
- * Get metrics for a job or step.
774
- * Only available when using LocalTelemetryAdapter.
990
+ * Get spans for a job or step.
991
+ * Only available when telemetry.local is enabled.
775
992
  *
776
- * @param options - Query options including jobId/stepId, filters, sort, and pagination
777
- * @returns Promise resolving to metrics result with pagination info
778
- * @throws Error if not using LocalTelemetryAdapter
993
+ * @param options - Query options including jobId/stepId, filters, and sort
994
+ * @returns Promise resolving to spans result
995
+ * @throws Error if local telemetry is not enabled
779
996
  */
780
- async getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult> {
997
+ async getSpans(options: GetSpansOptions): Promise<GetSpansResult> {
781
998
  await this.start()
782
- if (!this.metricsEnabled) {
783
- throw new Error('Metrics are only available when using LocalTelemetryAdapter')
999
+ if (!this.spansEnabled) {
1000
+ throw new Error('Spans are only available when telemetry.local is enabled')
784
1001
  }
785
- return this.#database.getMetrics(options)
1002
+ return this.#database.getSpans(options)
786
1003
  }
787
1004
 
788
1005
  /**
@@ -806,6 +1023,43 @@ export class Client<
806
1023
  action.name,
807
1024
  zocker(action.input as z.ZodObject)
808
1025
  .override(z.ZodString, 'string')
1026
+ .override(z.ZodBigInt, '4000' as any) // Convert BigInt to string for JSON serialization
1027
+ .override(z.ZodNumber, (schema, _ctx) => {
1028
+ const greaterThan = schema.def.checks?.find((check) => check._zod.def.check === 'greater_than')?._zod
1029
+ .def as unknown as { value: number; inclusive: boolean }
1030
+ const lessThan = schema.def.checks?.find((check) => check._zod.def.check === 'less_than')?._zod
1031
+ .def as unknown as { value: number; inclusive: boolean }
1032
+
1033
+ if (greaterThan && lessThan) {
1034
+ const min = greaterThan.inclusive ? greaterThan.value : greaterThan.value + 1
1035
+ // For inclusive lessThan, we want to include the value, so max should be value + 1
1036
+ // For exclusive lessThan, we want to exclude the value, so max is the value itself
1037
+ const max = lessThan.inclusive ? lessThan.value + 1 : lessThan.value
1038
+ // Ensure min < max
1039
+ if (min >= max) {
1040
+ return Math.floor(min)
1041
+ }
1042
+ return Math.floor(Math.random() * (max - min) + min)
1043
+ }
1044
+
1045
+ if (greaterThan) {
1046
+ const min = greaterThan.inclusive ? greaterThan.value : greaterThan.value + 1
1047
+ const max = min + 1000 // Use 1000 as default range
1048
+ return Math.floor(Math.random() * (max - min) + min)
1049
+ }
1050
+
1051
+ if (lessThan) {
1052
+ // For inclusive lessThan, we want to include the value, so max should be value + 1
1053
+ // For exclusive lessThan, we want to exclude the value, so max is the value itself
1054
+ const max = lessThan.inclusive ? lessThan.value + 1 : lessThan.value
1055
+ return Math.floor(Math.random() * max)
1056
+ }
1057
+
1058
+ return Math.floor(Math.random() * 1000)
1059
+ })
1060
+ .number({
1061
+ extreme_value_chance: 0.01,
1062
+ })
809
1063
  .generate(),
810
1064
  )
811
1065
  }
@@ -847,9 +1101,6 @@ export class Client<
847
1101
  return false
848
1102
  }
849
1103
 
850
- // Start telemetry adapter
851
- await this.#telemetry.start()
852
-
853
1104
  if (this.#actions) {
854
1105
  if (this.#options.recoverJobsOnStart) {
855
1106
  await this.#database.recoverJobs({
@@ -920,8 +1171,10 @@ export class Client<
920
1171
  }),
921
1172
  )
922
1173
 
923
- // Stop telemetry adapter
924
- await this.#telemetry.stop()
1174
+ // Shutdown TracerProvider if configured
1175
+ if (this.#tracerProvider) {
1176
+ await this.#tracerProvider.shutdown()
1177
+ }
925
1178
 
926
1179
  const dbStopped = await this.#database.stop()
927
1180
  if (!dbStopped) {
@@ -965,10 +1218,11 @@ export class Client<
965
1218
  // Transform to JobResult
966
1219
  const result: JobResult | null = job
967
1220
  ? {
968
- jobId: job.id,
1221
+ id: job.id,
969
1222
  actionName: job.actionName,
970
1223
  status: job.status,
971
1224
  groupKey: job.groupKey,
1225
+ description: job.description,
972
1226
  input: job.input,
973
1227
  output: job.output,
974
1228
  error: job.error,
@@ -1051,7 +1305,7 @@ export class Client<
1051
1305
  actionManager = new ActionManager({
1052
1306
  action,
1053
1307
  database: this.#database,
1054
- telemetry: this.#telemetry,
1308
+ tracer: this.#tracer,
1055
1309
  variables: this.#variables,
1056
1310
  logger: this.#logger,
1057
1311
  concurrencyLimit: this.#options.actionConcurrencyLimit,