Skip to content

Commit 88c6785

Browse files
authored
added nested steps and several other features (#2)
Core Features Nested Steps Steps can now create child steps with proper parent-child tracking. Child steps inherit abort signals from their parent. If a parent completes with unawaited children, Duron throws an UnhandledChildStepsError to prevent orphaned processes. Time Travel Jobs can be restarted from a specific step, preserving completed sibling branches. Includes time offset adjustment so preserved steps align with the current timeline. Supports parallel step marking so independent branches are preserved during time travel. Reusable Step Definitions New createStep() + ctx.run() API for defining modular, reusable steps. Step definitions can be composed — nested definitions can call other definitions. Inline steps can also call reusable definitions. Telemetry & Observability - Built-in OpenTelemetry tracing with telemetry: { local: true } (stores spans in DB) or external exporters. - ctx.telemetry context for recording custom metrics, creating spans, and adding attributes/events. - Tracer is context-aware — child spans automatically link to the current trace hierarchy. - Spans API endpoints for jobs and steps, with recursive descendant span querying. - Batch metric insertion with configurable flushDelayMs. Enhanced Error Handling - New error codes for better programmatic error detection (isTimeoutError, isCancelError). - ErrorMetadata interface with job ID and contextual info on errors. - Proper abort and settle of running steps when errors occur. Typed Job Results waitForJob and runActionAndWait now return structured JobResult / TypedJobResult with proper type safety instead of raw job objects. Dynamic Job Descriptions Actions can define a description function to generate contextual descriptions at job creation time. Jobs are filterable by description in the API and dashboard. Concurrency Step Limit New concurrencyStepLimit option on job creation for fine-grained control over how many steps can run concurrently within a single job. Dynamic Step Names Step names can be generated dynamically using full context (input, variables, jobId, parentStepId) via the StepNameContext interface. --- Dashboard - Resizable panels — drag-to-resize layout for jobs, details, and steps sections using react-resizable-panels. - Timeline view — integrated directly into StepList with toggle between list/timeline views. - Nested step visualization — color-coded nesting depth indicators. - Column resizing & visibility — customizable jobs table columns with pinning support. - Monaco JSON viewer — replaced @uiw/react-json-view with @monaco-editor/react for interactive JSON viewing. - Spans visualization — view OpenTelemetry spans with JSONata filtering for advanced queries. - Duration display — formatted durations (hh:mm:ss.mmm) on jobs and steps. - Theme improvements — configurable theme prop (light/dark/system) with localStorage persistence. - Configurable polling — PollingProvider for custom polling intervals. - Custom fetch — ApiProvider accepts a custom fetch function. - Description filtering — search/filter jobs by description.
1 parent c772d70 commit 88c6785

92 files changed

Lines changed: 12814 additions & 1447 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3232

3333
# Finder (MacOS) folder config
3434
.DS_Store
35+
.claude/settings.local.json

CLAUDE.md

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ Duron is a modern, type-safe background job processing system built with TypeScr
1212

1313
- **Type-Safe Actions** - Define actions with Zod schemas for input/output validation
1414
- **Step-Based Execution** - Break down complex workflows into manageable, retryable steps
15+
- **Nested Steps** - Steps can create child steps with proper parent-child tracking and abort signal propagation
1516
- **Intelligent Retry Logic** - Configurable exponential backoff with per-action and per-step options
1617
- **Flexible Sync Patterns** - Pull, push, hybrid, or manual job fetching
1718
- **Advanced Concurrency Control** - Per-action, per-group, and dynamic concurrency limits
1819
- **Multi-Process Support** - Run multiple worker processes sharing the same database
1920
- **Database Adapters** - PostgreSQL (production) and PGLite (development/testing)
2021
- **REST API Server** - Built-in Elysia-based API with advanced filtering and pagination
2122
- **Dashboard UI** - Beautiful React dashboard for real-time job monitoring
23+
- **Telemetry & Observability** - Built-in support for metrics, tracing, and custom observability with pluggable adapters
2224

2325
## Runtime Environment
2426

@@ -85,6 +87,10 @@ The main library providing:
8587
- **Adapters**:
8688
- `duron/adapters/postgres` - PostgreSQL adapter for production
8789
- `duron/adapters/pglite` - PGLite adapter for development/testing
90+
- **Telemetry** - Configured via `telemetry` option on client:
91+
- `telemetry: { local: true }` - Store spans in the database
92+
- `telemetry: { traceExporter }` - Export to OpenTelemetry backends
93+
- No config = telemetry disabled (default)
8894

8995
**Key Dependencies:**
9096
- `zod` - Schema validation
@@ -93,6 +99,7 @@ The main library providing:
9399
- `pino` - Logging
94100
- `fastq` - Queue implementation
95101
- `jose` - JWT handling
102+
- `@opentelemetry/api` - OpenTelemetry integration (optional)
96103

97104
### `duron-dashboard` (React Dashboard)
98105

@@ -198,6 +205,49 @@ const sendEmail = defineAction<typeof variables>()({
198205
})
199206
```
200207

208+
### Nested Steps
209+
210+
Steps can create child steps using the `step()` method available in the step handler context. Child steps share abort signals with their parent and are tracked with `parentStepId` in the database.
211+
212+
```typescript
213+
const processOrder = defineAction<typeof variables>()({
214+
name: 'process-order',
215+
input: z.object({ orderId: z.string() }),
216+
output: z.object({ success: z.boolean() }),
217+
handler: async (ctx) => {
218+
const result = await ctx.step('process', async ({ step, signal, stepId }) => {
219+
// stepId is available for the current step
220+
console.log('Processing step:', stepId)
221+
222+
// Create child steps - they inherit the parent's abort signal
223+
const validation = await step('validate', async ({ parentStepId }) => {
224+
// parentStepId links back to the 'process' step
225+
return { valid: true }
226+
})
227+
228+
// Child steps can also be nested further
229+
const payment = await step('charge', async ({ step: nestedStep }) => {
230+
const auth = await nestedStep('authorize', async () => {
231+
return { authCode: '123' }
232+
})
233+
return { charged: true, authCode: auth.authCode }
234+
})
235+
236+
return { success: validation.valid && payment.charged }
237+
})
238+
239+
return result
240+
},
241+
})
242+
```
243+
244+
**Important:** All child steps MUST be awaited before the parent step returns. If a parent step completes with unawaited children, Duron will:
245+
1. Abort all pending child steps
246+
2. Wait for them to settle
247+
3. Throw an `UnhandledChildStepsError`
248+
249+
This prevents orphaned processes and ensures proper async patterns.
250+
201251
### Creating a Client
202252

203253
```typescript
@@ -228,6 +278,75 @@ const jobId = await client.runAction('send-email', {
228278
const job = await client.waitForJob(jobId)
229279
```
230280

281+
### Telemetry & Observability
282+
283+
Duron provides built-in OpenTelemetry support for tracing:
284+
285+
```typescript
286+
import { duron } from 'duron'
287+
import { postgresAdapter } from 'duron/adapters/postgres'
288+
289+
const client = duron({
290+
database: postgresAdapter({
291+
connection: process.env.DATABASE_URL,
292+
}),
293+
// Enable local telemetry - stores spans in the database
294+
telemetry: { local: true },
295+
actions: { sendEmail },
296+
})
297+
```
298+
299+
**Telemetry Configuration Options:**
300+
301+
- `local: true | { flushDelayMs?: number }` - Store spans in the Duron database
302+
- `traceExporter: SpanExporter` - Export to OpenTelemetry-compatible backends (Jaeger, OTLP, etc.)
303+
- `spanProcessors: SpanProcessor[]` - Add custom span processors
304+
- `serviceName: string` - Service name for OpenTelemetry resource (default: `'duron'`)
305+
306+
**Recording Custom Metrics:**
307+
308+
The `telemetry` context is available in action and step handlers for recording custom metrics:
309+
310+
```typescript
311+
const processAI = defineAction()({
312+
name: 'process-ai',
313+
handler: async (ctx) => {
314+
const startTime = Date.now()
315+
316+
// Record job-level metrics
317+
ctx.telemetry.recordMetric('ai.request.start', 1)
318+
const span = ctx.telemetry.getActiveSpan()
319+
span?.setAttribute('model', 'gpt-4')
320+
span?.addEvent('processing.started')
321+
322+
const result = await ctx.step('call-api', async ({ telemetry }) => {
323+
const response = await callAI(ctx.input)
324+
325+
// Record step-level metrics
326+
telemetry.recordMetric('ai.tokens.input', response.inputTokens)
327+
telemetry.recordMetric('ai.tokens.output', response.outputTokens)
328+
telemetry.recordMetric('ai.latency.ms', Date.now() - startTime)
329+
telemetry.getActiveSpan()?.addEvent('api.call.complete', { status: 'success' })
330+
331+
return response
332+
})
333+
334+
return result
335+
},
336+
})
337+
```
338+
339+
**Accessing Metrics via API:**
340+
341+
When using `telemetry: { local: true }`, spans are stored in the database and accessible via the REST API:
342+
343+
```
344+
GET /api/jobs/:id/spans
345+
GET /api/steps/:id/spans
346+
```
347+
348+
The dashboard also shows metrics when local telemetry is enabled.
349+
231350
### Creating a Server with Dashboard
232351

233352
```typescript
@@ -340,6 +459,8 @@ Uses Bun's bundler mode with:
340459
| `packages/duron/src/server.ts` | REST API server |
341460
| `packages/duron/src/adapters/adapter.ts` | Base adapter class |
342461
| `packages/duron/src/adapters/postgres/` | PostgreSQL adapter |
462+
| `packages/duron/src/step-manager.ts` | Step execution and nested step handling |
463+
| `packages/duron/src/telemetry/` | Telemetry adapters (local, opentelemetry, noop) |
343464
| `packages/duron-dashboard/src/DuronDashboard.tsx` | Dashboard root |
344465
| `packages/duron-dashboard/src/views/` | Dashboard pages |
345466
| `packages/examples/basic/start.ts` | Basic example |
@@ -368,15 +489,22 @@ Uses Bun's bundler mode with:
368489
## Error Handling
369490

370491
- Use `NonRetriableError` for errors that should not be retried
492+
- Use `UnhandledChildStepsError` is thrown when parent steps complete with unawaited children
371493
- Steps have built-in retry logic with exponential backoff
372494
- Jobs have timeout/expiration settings
373495

374496
```typescript
375-
import { NonRetriableError } from 'duron'
497+
import { NonRetriableError, UnhandledChildStepsError } from 'duron'
376498

499+
// For errors that should not be retried
377500
if (!apiKey) {
378501
throw new NonRetriableError('API key is required')
379502
}
503+
504+
// UnhandledChildStepsError is thrown automatically when:
505+
// - A parent step returns before all child steps are awaited
506+
// - The parent step's callback completes but children are still pending
507+
// This error is non-retriable and will fail the entire job
380508
```
381509

382510
## Environment Variables
@@ -395,3 +523,5 @@ if (!apiKey) {
395523
4. Follow existing code patterns
396524
5. Use TypeScript strict mode
397525
6. Document public APIs with JSDoc
526+
527+
Always use Context7 MCP when I need library/API documentation, code generation, setup or configuration steps without me having to explicitly ask.

0 commit comments

Comments
 (0)