duron 0.2.2 → 0.3.0-beta.1
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/dist/action-job.d.ts +2 -0
- package/dist/action-job.d.ts.map +1 -1
- package/dist/action-job.js +20 -1
- package/dist/action-manager.d.ts +2 -0
- package/dist/action-manager.d.ts.map +1 -1
- package/dist/action-manager.js +3 -0
- package/dist/action.d.ts +27 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +9 -0
- package/dist/adapters/adapter.d.ts +10 -2
- package/dist/adapters/adapter.d.ts.map +1 -1
- package/dist/adapters/adapter.js +59 -1
- package/dist/adapters/postgres/base.d.ts +9 -4
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +269 -19
- package/dist/adapters/postgres/schema.d.ts +249 -105
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.d.ts +249 -106
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.js +2 -2
- package/dist/adapters/postgres/schema.js +29 -1
- package/dist/adapters/schemas.d.ts +140 -7
- package/dist/adapters/schemas.d.ts.map +1 -1
- package/dist/adapters/schemas.js +52 -4
- package/dist/client.d.ts +8 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/server.d.ts +220 -16
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -8
- package/dist/step-manager.d.ts +8 -2
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +174 -15
- package/dist/telemetry/adapter.d.ts +85 -0
- package/dist/telemetry/adapter.d.ts.map +1 -0
- package/dist/telemetry/adapter.js +128 -0
- package/dist/telemetry/index.d.ts +5 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +4 -0
- package/dist/telemetry/local.d.ts +21 -0
- package/dist/telemetry/local.d.ts.map +1 -0
- package/dist/telemetry/local.js +180 -0
- package/dist/telemetry/noop.d.ts +16 -0
- package/dist/telemetry/noop.d.ts.map +1 -0
- package/dist/telemetry/noop.js +39 -0
- package/dist/telemetry/opentelemetry.d.ts +24 -0
- package/dist/telemetry/opentelemetry.d.ts.map +1 -0
- package/dist/telemetry/opentelemetry.js +202 -0
- package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +3 -0
- package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +988 -0
- package/migrations/postgres/20260118202533_wealthy_mysterio/migration.sql +24 -0
- package/migrations/postgres/20260118202533_wealthy_mysterio/snapshot.json +1362 -0
- package/package.json +6 -4
- package/src/action-job.ts +35 -0
- package/src/action-manager.ts +5 -0
- package/src/action.ts +199 -0
- package/src/adapters/adapter.ts +151 -0
- package/src/adapters/postgres/base.ts +342 -23
- package/src/adapters/postgres/schema.default.ts +2 -2
- package/src/adapters/postgres/schema.ts +49 -1
- package/src/adapters/schemas.ts +81 -5
- package/src/client.ts +78 -0
- package/src/errors.ts +45 -1
- package/src/index.ts +10 -2
- package/src/server.ts +163 -8
- package/src/step-manager.ts +293 -13
- package/src/telemetry/adapter.ts +468 -0
- package/src/telemetry/index.ts +17 -0
- package/src/telemetry/local.ts +336 -0
- package/src/telemetry/noop.ts +95 -0
- package/src/telemetry/opentelemetry.ts +310 -0
package/src/adapters/schemas.ts
CHANGED
|
@@ -55,6 +55,8 @@ export const JobSchema = z.object({
|
|
|
55
55
|
export const JobStepSchema = z.object({
|
|
56
56
|
id: z.string(),
|
|
57
57
|
jobId: z.string(),
|
|
58
|
+
parentStepId: z.string().nullable().default(null),
|
|
59
|
+
parallel: z.boolean().default(false),
|
|
58
60
|
name: z.string(),
|
|
59
61
|
output: z.any().nullable().default(null),
|
|
60
62
|
status: StepStatusSchema,
|
|
@@ -113,8 +115,6 @@ export const GetJobsOptionsSchema = z.object({
|
|
|
113
115
|
|
|
114
116
|
export const GetJobStepsOptionsSchema = z.object({
|
|
115
117
|
jobId: z.string(),
|
|
116
|
-
page: z.number().int().positive().optional(),
|
|
117
|
-
pageSize: z.number().int().positive().optional(),
|
|
118
118
|
search: z.string().optional(),
|
|
119
119
|
updatedAfter: DateSchema.optional(),
|
|
120
120
|
})
|
|
@@ -183,6 +183,13 @@ export const DeleteJobOptionsSchema = z.object({
|
|
|
183
183
|
|
|
184
184
|
export const DeleteJobsOptionsSchema = GetJobsOptionsSchema.optional()
|
|
185
185
|
|
|
186
|
+
export const TimeTravelJobOptionsSchema = z.object({
|
|
187
|
+
/** The ID of the job to time travel */
|
|
188
|
+
jobId: z.string(),
|
|
189
|
+
/** The ID of the step to restart from */
|
|
190
|
+
stepId: z.string(),
|
|
191
|
+
})
|
|
192
|
+
|
|
186
193
|
// ============================================================================
|
|
187
194
|
// Step Option Schemas
|
|
188
195
|
// ============================================================================
|
|
@@ -190,6 +197,10 @@ export const DeleteJobsOptionsSchema = GetJobsOptionsSchema.optional()
|
|
|
190
197
|
export const CreateOrRecoverJobStepOptionsSchema = z.object({
|
|
191
198
|
/** The ID of the job this step belongs to */
|
|
192
199
|
jobId: z.string(),
|
|
200
|
+
/** The ID of the parent step (null for root steps) */
|
|
201
|
+
parentStepId: z.string().nullable().default(null),
|
|
202
|
+
/** Whether this step runs in parallel (independent from siblings during time travel) */
|
|
203
|
+
parallel: z.boolean().default(false),
|
|
193
204
|
/** The name of the step */
|
|
194
205
|
name: z.string(),
|
|
195
206
|
/** Timeout in milliseconds for the step */
|
|
@@ -258,8 +269,6 @@ export const GetJobsResultSchema = z.object({
|
|
|
258
269
|
export const GetJobStepsResultSchema = z.object({
|
|
259
270
|
steps: z.array(JobStepWithoutOutputSchema),
|
|
260
271
|
total: z.number().int().nonnegative(),
|
|
261
|
-
page: z.number().int().positive(),
|
|
262
|
-
pageSize: z.number().int().positive(),
|
|
263
272
|
})
|
|
264
273
|
|
|
265
274
|
export const ActionStatsSchema = z.object({
|
|
@@ -285,6 +294,63 @@ export const JobStepStatusResultSchema = z.object({
|
|
|
285
294
|
updatedAt: DateSchema,
|
|
286
295
|
})
|
|
287
296
|
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// Metrics Schemas
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
export const MetricTypeSchema = z.enum(['metric', 'span_event', 'span_attribute'])
|
|
302
|
+
|
|
303
|
+
export const MetricSchema = z.object({
|
|
304
|
+
id: z.string(),
|
|
305
|
+
jobId: z.string(),
|
|
306
|
+
stepId: z.string().nullable(),
|
|
307
|
+
name: z.string(),
|
|
308
|
+
value: z.number(),
|
|
309
|
+
attributes: z.record(z.string(), z.any()),
|
|
310
|
+
type: MetricTypeSchema,
|
|
311
|
+
timestamp: DateSchema,
|
|
312
|
+
createdAt: DateSchema,
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
export const MetricSortFieldSchema = z.enum(['name', 'value', 'timestamp', 'createdAt'])
|
|
316
|
+
|
|
317
|
+
export const MetricSortSchema = z.object({
|
|
318
|
+
field: MetricSortFieldSchema,
|
|
319
|
+
order: SortOrderSchema,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
export const MetricFiltersSchema = z.object({
|
|
323
|
+
name: z.union([z.string(), z.array(z.string())]).optional(),
|
|
324
|
+
type: z.union([MetricTypeSchema, z.array(MetricTypeSchema)]).optional(),
|
|
325
|
+
attributesFilter: z.record(z.string(), z.any()).optional(),
|
|
326
|
+
timestampRange: z.array(DateSchema).length(2).optional(),
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
export const InsertMetricOptionsSchema = z.object({
|
|
330
|
+
jobId: z.string(),
|
|
331
|
+
stepId: z.string().optional(),
|
|
332
|
+
name: z.string(),
|
|
333
|
+
value: z.number(),
|
|
334
|
+
attributes: z.record(z.string(), z.any()).optional(),
|
|
335
|
+
type: MetricTypeSchema,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
export const GetMetricsOptionsSchema = z.object({
|
|
339
|
+
jobId: z.string().optional(),
|
|
340
|
+
stepId: z.string().optional(),
|
|
341
|
+
filters: MetricFiltersSchema.optional(),
|
|
342
|
+
sort: MetricSortSchema.optional(),
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
export const GetMetricsResultSchema = z.object({
|
|
346
|
+
metrics: z.array(MetricSchema),
|
|
347
|
+
total: z.number().int().nonnegative(),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
export const DeleteMetricsOptionsSchema = z.object({
|
|
351
|
+
jobId: z.string(),
|
|
352
|
+
})
|
|
353
|
+
|
|
288
354
|
// ============================================================================
|
|
289
355
|
// Type Exports
|
|
290
356
|
// ============================================================================
|
|
@@ -313,9 +379,19 @@ export type CancelJobOptions = z.infer<typeof CancelJobOptionsSchema>
|
|
|
313
379
|
export type RetryJobOptions = z.infer<typeof RetryJobOptionsSchema>
|
|
314
380
|
export type DeleteJobOptions = z.infer<typeof DeleteJobOptionsSchema>
|
|
315
381
|
export type DeleteJobsOptions = z.infer<typeof DeleteJobsOptionsSchema>
|
|
316
|
-
export type CreateOrRecoverJobStepOptions = z.
|
|
382
|
+
export type CreateOrRecoverJobStepOptions = z.input<typeof CreateOrRecoverJobStepOptionsSchema>
|
|
317
383
|
export type CompleteJobStepOptions = z.infer<typeof CompleteJobStepOptionsSchema>
|
|
318
384
|
export type FailJobStepOptions = z.infer<typeof FailJobStepOptionsSchema>
|
|
319
385
|
export type DelayJobStepOptions = z.infer<typeof DelayJobStepOptionsSchema>
|
|
320
386
|
export type CancelJobStepOptions = z.infer<typeof CancelJobStepOptionsSchema>
|
|
321
387
|
export type CreateOrRecoverJobStepResult = z.infer<typeof CreateOrRecoverJobStepResultSchema>
|
|
388
|
+
export type TimeTravelJobOptions = z.infer<typeof TimeTravelJobOptionsSchema>
|
|
389
|
+
export type MetricType = z.infer<typeof MetricTypeSchema>
|
|
390
|
+
export type Metric = z.infer<typeof MetricSchema>
|
|
391
|
+
export type MetricSortField = z.infer<typeof MetricSortFieldSchema>
|
|
392
|
+
export type MetricSort = z.infer<typeof MetricSortSchema>
|
|
393
|
+
export type MetricFilters = z.infer<typeof MetricFiltersSchema>
|
|
394
|
+
export type InsertMetricOptions = z.infer<typeof InsertMetricOptionsSchema>
|
|
395
|
+
export type GetMetricsOptions = z.infer<typeof GetMetricsOptionsSchema>
|
|
396
|
+
export type GetMetricsResult = z.infer<typeof GetMetricsResultSchema>
|
|
397
|
+
export type DeleteMetricsOptions = z.infer<typeof DeleteMetricsOptionsSchema>
|
package/src/client.ts
CHANGED
|
@@ -11,11 +11,14 @@ import type {
|
|
|
11
11
|
GetJobStepsResult,
|
|
12
12
|
GetJobsOptions,
|
|
13
13
|
GetJobsResult,
|
|
14
|
+
GetMetricsOptions,
|
|
15
|
+
GetMetricsResult,
|
|
14
16
|
Job,
|
|
15
17
|
JobStep,
|
|
16
18
|
} from './adapters/adapter.js'
|
|
17
19
|
import type { JobStatusResult, JobStepStatusResult } from './adapters/schemas.js'
|
|
18
20
|
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'
|
|
19
22
|
|
|
20
23
|
const BaseOptionsSchema = z.object({
|
|
21
24
|
/**
|
|
@@ -136,6 +139,17 @@ export interface ClientOptions<
|
|
|
136
139
|
* These can be accessed in action handlers using `ctx.var`.
|
|
137
140
|
*/
|
|
138
141
|
variables?: TVariables
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Optional telemetry adapter for observability.
|
|
145
|
+
* When provided, traces job and step execution with spans and allows recording custom metrics.
|
|
146
|
+
*
|
|
147
|
+
* Available adapters:
|
|
148
|
+
* - `openTelemetryAdapter()` - Export traces to external systems (Jaeger, OTLP, etc.)
|
|
149
|
+
* - `localTelemetryAdapter({ database })` - Store metrics in the Duron database
|
|
150
|
+
* - `noopTelemetryAdapter()` - No-op adapter (default)
|
|
151
|
+
*/
|
|
152
|
+
telemetry?: TelemetryAdapter
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
interface FetchOptions {
|
|
@@ -157,6 +171,7 @@ export class Client<
|
|
|
157
171
|
#id: string
|
|
158
172
|
#actions: TActions | null
|
|
159
173
|
#database: Adapter
|
|
174
|
+
#telemetry: TelemetryAdapter
|
|
160
175
|
#variables: Record<string, unknown>
|
|
161
176
|
#logger: Logger
|
|
162
177
|
#started: boolean = false
|
|
@@ -190,11 +205,14 @@ export class Client<
|
|
|
190
205
|
this.#options = BaseOptionsSchema.parse(options)
|
|
191
206
|
this.#id = options.id ?? globalThis.crypto.randomUUID()
|
|
192
207
|
this.#database = options.database
|
|
208
|
+
this.#telemetry = options.telemetry ?? noopTelemetryAdapter()
|
|
193
209
|
this.#actions = options.actions ?? null
|
|
194
210
|
this.#variables = options?.variables ?? {}
|
|
195
211
|
this.#logger = this.#normalizeLogger(options?.logger)
|
|
196
212
|
this.#database.setId(this.#id)
|
|
197
213
|
this.#database.setLogger(this.#logger)
|
|
214
|
+
this.#telemetry.setLogger(this.#logger)
|
|
215
|
+
this.#telemetry.setClient(this)
|
|
198
216
|
}
|
|
199
217
|
|
|
200
218
|
#normalizeLogger(logger?: Logger | 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent'): Logger {
|
|
@@ -217,6 +235,28 @@ export class Client<
|
|
|
217
235
|
return this.#logger
|
|
218
236
|
}
|
|
219
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Get the telemetry adapter instance.
|
|
240
|
+
*/
|
|
241
|
+
get telemetry(): TelemetryAdapter {
|
|
242
|
+
return this.#telemetry
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get the database adapter instance.
|
|
247
|
+
*/
|
|
248
|
+
get database(): Adapter {
|
|
249
|
+
return this.#database
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if local telemetry is enabled.
|
|
254
|
+
* Returns true if using LocalTelemetryAdapter.
|
|
255
|
+
*/
|
|
256
|
+
get metricsEnabled(): boolean {
|
|
257
|
+
return this.#telemetry instanceof LocalTelemetryAdapter
|
|
258
|
+
}
|
|
259
|
+
|
|
220
260
|
/**
|
|
221
261
|
* Get the current configuration of this Duron instance.
|
|
222
262
|
*
|
|
@@ -361,6 +401,21 @@ export class Client<
|
|
|
361
401
|
return this.#database.retryJob({ jobId })
|
|
362
402
|
}
|
|
363
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Time travel a job to restart from a specific step.
|
|
406
|
+
* The job must be in completed, failed, or cancelled status.
|
|
407
|
+
* Resets the job and ancestor steps to active status, deletes subsequent steps,
|
|
408
|
+
* and preserves completed parallel siblings.
|
|
409
|
+
*
|
|
410
|
+
* @param jobId - The ID of the job to time travel
|
|
411
|
+
* @param stepId - The ID of the step to restart from
|
|
412
|
+
* @returns Promise resolving to `true` if time travel succeeded, `false` otherwise
|
|
413
|
+
*/
|
|
414
|
+
async timeTravelJob(jobId: string, stepId: string): Promise<boolean> {
|
|
415
|
+
await this.start()
|
|
416
|
+
return this.#database.timeTravelJob({ jobId, stepId })
|
|
417
|
+
}
|
|
418
|
+
|
|
364
419
|
/**
|
|
365
420
|
* Delete a job by its ID.
|
|
366
421
|
* Active jobs cannot be deleted.
|
|
@@ -548,6 +603,22 @@ export class Client<
|
|
|
548
603
|
return this.#database.getActions()
|
|
549
604
|
}
|
|
550
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Get metrics for a job or step.
|
|
608
|
+
* Only available when using LocalTelemetryAdapter.
|
|
609
|
+
*
|
|
610
|
+
* @param options - Query options including jobId/stepId, filters, sort, and pagination
|
|
611
|
+
* @returns Promise resolving to metrics result with pagination info
|
|
612
|
+
* @throws Error if not using LocalTelemetryAdapter
|
|
613
|
+
*/
|
|
614
|
+
async getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult> {
|
|
615
|
+
await this.start()
|
|
616
|
+
if (!this.metricsEnabled) {
|
|
617
|
+
throw new Error('Metrics are only available when using LocalTelemetryAdapter')
|
|
618
|
+
}
|
|
619
|
+
return this.#database.getMetrics(options)
|
|
620
|
+
}
|
|
621
|
+
|
|
551
622
|
/**
|
|
552
623
|
* Get action metadata including input schemas and mock data.
|
|
553
624
|
* This is useful for generating UI forms or mock data.
|
|
@@ -610,6 +681,9 @@ export class Client<
|
|
|
610
681
|
return false
|
|
611
682
|
}
|
|
612
683
|
|
|
684
|
+
// Start telemetry adapter
|
|
685
|
+
await this.#telemetry.start()
|
|
686
|
+
|
|
613
687
|
if (this.#actions) {
|
|
614
688
|
if (this.#options.recoverJobsOnStart) {
|
|
615
689
|
await this.#database.recoverJobs({
|
|
@@ -680,6 +754,9 @@ export class Client<
|
|
|
680
754
|
}),
|
|
681
755
|
)
|
|
682
756
|
|
|
757
|
+
// Stop telemetry adapter
|
|
758
|
+
await this.#telemetry.stop()
|
|
759
|
+
|
|
683
760
|
const dbStopped = await this.#database.stop()
|
|
684
761
|
if (!dbStopped) {
|
|
685
762
|
return false
|
|
@@ -795,6 +872,7 @@ export class Client<
|
|
|
795
872
|
actionManager = new ActionManager({
|
|
796
873
|
action,
|
|
797
874
|
database: this.#database,
|
|
875
|
+
telemetry: this.#telemetry,
|
|
798
876
|
variables: this.#variables,
|
|
799
877
|
logger: this.#logger,
|
|
800
878
|
concurrencyLimit: this.#options.actionConcurrencyLimit,
|
package/src/errors.ts
CHANGED
|
@@ -126,6 +126,38 @@ export class ActionCancelError extends DuronError {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Error thrown when a parent step completes with unhandled (non-awaited) child steps.
|
|
131
|
+
*
|
|
132
|
+
* This error indicates a bug in the action handler where child steps were started
|
|
133
|
+
* but not properly awaited. All child steps must be awaited before the parent returns.
|
|
134
|
+
*/
|
|
135
|
+
export class UnhandledChildStepsError extends NonRetriableError {
|
|
136
|
+
/**
|
|
137
|
+
* The name of the parent step that completed with unhandled children.
|
|
138
|
+
*/
|
|
139
|
+
public readonly stepName: string
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The number of unhandled child steps.
|
|
143
|
+
*/
|
|
144
|
+
public readonly pendingCount: number
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a new UnhandledChildStepsError.
|
|
148
|
+
*
|
|
149
|
+
* @param stepName - The name of the parent step
|
|
150
|
+
* @param pendingCount - The number of unhandled child steps
|
|
151
|
+
*/
|
|
152
|
+
constructor(stepName: string, pendingCount: number) {
|
|
153
|
+
super(
|
|
154
|
+
`Parent step "${stepName}" completed with ${pendingCount} unhandled child step(s). All child steps must be awaited before the parent returns.`,
|
|
155
|
+
)
|
|
156
|
+
this.stepName = stepName
|
|
157
|
+
this.pendingCount = pendingCount
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
/**
|
|
130
162
|
* Checks if an error is a DuronError instance.
|
|
131
163
|
*/
|
|
@@ -137,7 +169,19 @@ export function isDuronError(error: unknown): error is DuronError {
|
|
|
137
169
|
* Checks if an error is a NonRetriableError instance.
|
|
138
170
|
*/
|
|
139
171
|
export function isNonRetriableError(error: unknown): error is NonRetriableError {
|
|
140
|
-
return
|
|
172
|
+
return (
|
|
173
|
+
error instanceof NonRetriableError ||
|
|
174
|
+
error instanceof ActionCancelError ||
|
|
175
|
+
error instanceof ActionTimeoutError ||
|
|
176
|
+
error instanceof UnhandledChildStepsError
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Checks if an error is an UnhandledChildStepsError instance.
|
|
182
|
+
*/
|
|
183
|
+
export function isUnhandledChildStepsError(error: unknown): error is UnhandledChildStepsError {
|
|
184
|
+
return error instanceof UnhandledChildStepsError
|
|
141
185
|
}
|
|
142
186
|
|
|
143
187
|
/**
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { Action } from './action.js'
|
|
2
2
|
import { Client, type ClientOptions } from './client.js'
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
createStep,
|
|
6
|
+
defineAction,
|
|
7
|
+
type StepDefinition,
|
|
8
|
+
type StepDefinitionHandlerContext,
|
|
9
|
+
type StepDefinitionInput,
|
|
10
|
+
} from './action.js'
|
|
11
|
+
export * from './client.js'
|
|
5
12
|
export * from './constants.js'
|
|
6
|
-
export { NonRetriableError } from './errors.js'
|
|
13
|
+
export { NonRetriableError, UnhandledChildStepsError } from './errors.js'
|
|
7
14
|
export * from './server.js'
|
|
15
|
+
export * from './telemetry/index.js'
|
|
8
16
|
|
|
9
17
|
export const duron = <
|
|
10
18
|
TActions extends Record<string, Action<any, any, TVariables>>,
|
package/src/server.ts
CHANGED
|
@@ -2,17 +2,20 @@ import { Elysia } from 'elysia'
|
|
|
2
2
|
import { jwtVerify, SignJWT } from 'jose'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
|
|
5
|
-
import type { GetJobStepsOptions, GetJobsOptions } from './adapters/adapter.js'
|
|
5
|
+
import type { GetJobStepsOptions, GetJobsOptions, GetMetricsOptions } from './adapters/adapter.js'
|
|
6
6
|
import {
|
|
7
7
|
GetActionsResultSchema,
|
|
8
8
|
GetJobStepsResultSchema,
|
|
9
9
|
GetJobsResultSchema,
|
|
10
|
+
GetMetricsResultSchema,
|
|
10
11
|
JobSchema,
|
|
11
12
|
JobSortFieldSchema,
|
|
12
13
|
JobStatusResultSchema,
|
|
13
14
|
JobStatusSchema,
|
|
14
15
|
JobStepSchema,
|
|
15
16
|
JobStepStatusResultSchema,
|
|
17
|
+
MetricSortFieldSchema,
|
|
18
|
+
MetricTypeSchema,
|
|
16
19
|
SortOrderSchema,
|
|
17
20
|
} from './adapters/schemas.js'
|
|
18
21
|
import type { Client } from './client.js'
|
|
@@ -61,14 +64,10 @@ export class UnauthorizedError extends Error {
|
|
|
61
64
|
|
|
62
65
|
export const GetJobStepsQuerySchema = z
|
|
63
66
|
.object({
|
|
64
|
-
page: z.coerce.number().int().min(1).optional(),
|
|
65
|
-
pageSize: z.coerce.number().int().min(1).max(1000).optional(),
|
|
66
67
|
search: z.string().optional(),
|
|
67
68
|
fUpdatedAfter: z.coerce.date().optional(),
|
|
68
69
|
})
|
|
69
70
|
.transform((data) => ({
|
|
70
|
-
page: data.page,
|
|
71
|
-
pageSize: data.pageSize,
|
|
72
71
|
search: data.search,
|
|
73
72
|
updatedAfter: data.fUpdatedAfter,
|
|
74
73
|
}))
|
|
@@ -177,6 +176,50 @@ export const GetActionsMetadataResponseSchema = z.array(
|
|
|
177
176
|
export type GetJobsQueryInput = z.input<typeof GetJobsQuerySchema>
|
|
178
177
|
export type GetJobStepsQueryInput = z.input<typeof GetJobStepsQuerySchema>
|
|
179
178
|
|
|
179
|
+
// Metrics query schema
|
|
180
|
+
export const GetMetricsQuerySchema = z
|
|
181
|
+
.object({
|
|
182
|
+
// Filters
|
|
183
|
+
fName: z.union([z.string(), z.array(z.string())]).optional(),
|
|
184
|
+
fType: z.union([MetricTypeSchema, z.array(MetricTypeSchema)]).optional(),
|
|
185
|
+
fTimestampRange: z.array(z.coerce.date()).length(2).optional(),
|
|
186
|
+
fAttributesFilter: z.record(z.string(), z.any()).optional(),
|
|
187
|
+
|
|
188
|
+
// Sort - format: "field:asc" or "field:desc"
|
|
189
|
+
sort: z.string().optional(),
|
|
190
|
+
})
|
|
191
|
+
.transform((data) => {
|
|
192
|
+
const filters: any = {}
|
|
193
|
+
|
|
194
|
+
if (data.fName) filters.name = data.fName
|
|
195
|
+
if (data.fType) filters.type = data.fType
|
|
196
|
+
if (data.fTimestampRange) filters.timestampRange = data.fTimestampRange
|
|
197
|
+
if (data.fAttributesFilter) filters.attributesFilter = data.fAttributesFilter
|
|
198
|
+
|
|
199
|
+
// Parse sort string: "field:asc" -> { field: 'field', order: 'asc' }
|
|
200
|
+
let sort: { field: z.infer<typeof MetricSortFieldSchema>; order: z.infer<typeof SortOrderSchema> } | undefined
|
|
201
|
+
if (data.sort) {
|
|
202
|
+
const [field, order] = data.sort.split(':').map((s) => s.trim())
|
|
203
|
+
if (field && order) {
|
|
204
|
+
const fieldResult = MetricSortFieldSchema.safeParse(field)
|
|
205
|
+
const orderResult = SortOrderSchema.safeParse(order.toLowerCase())
|
|
206
|
+
if (fieldResult.success && orderResult.success) {
|
|
207
|
+
sort = {
|
|
208
|
+
field: fieldResult.data,
|
|
209
|
+
order: orderResult.data,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
217
|
+
sort,
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
export type GetMetricsQueryInput = z.input<typeof GetMetricsQuerySchema>
|
|
222
|
+
|
|
180
223
|
export const ErrorResponseSchema = z.object({
|
|
181
224
|
error: z.string(),
|
|
182
225
|
message: z.string().optional(),
|
|
@@ -201,6 +244,15 @@ export const RetryJobResponseSchema = z.object({
|
|
|
201
244
|
newJobId: z.string(),
|
|
202
245
|
})
|
|
203
246
|
|
|
247
|
+
export const TimeTravelJobBodySchema = z.object({
|
|
248
|
+
stepId: z.uuid(),
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
export const TimeTravelJobResponseSchema = z.object({
|
|
252
|
+
success: z.boolean(),
|
|
253
|
+
message: z.string(),
|
|
254
|
+
})
|
|
255
|
+
|
|
204
256
|
// ============================================================================
|
|
205
257
|
// Server Factory
|
|
206
258
|
// ============================================================================
|
|
@@ -216,6 +268,14 @@ export interface CreateServerOptions<P extends string> {
|
|
|
216
268
|
*/
|
|
217
269
|
prefix?: P
|
|
218
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Enable metrics endpoints (/jobs/:id/metrics, /steps/:id/metrics).
|
|
273
|
+
* Only works when client is configured with LocalTelemetryAdapter.
|
|
274
|
+
* When true, enables the dashboard to show metrics buttons.
|
|
275
|
+
* @default auto-detected from client.metricsEnabled
|
|
276
|
+
*/
|
|
277
|
+
metricsEnabled?: boolean
|
|
278
|
+
|
|
219
279
|
login?: {
|
|
220
280
|
onLogin: (body: { email: string; password: string }) => Promise<boolean>
|
|
221
281
|
jwtSecret: string | Uint8Array
|
|
@@ -237,12 +297,15 @@ export interface CreateServerOptions<P extends string> {
|
|
|
237
297
|
* @param options - Configuration options
|
|
238
298
|
* @returns Elysia server instance
|
|
239
299
|
*/
|
|
240
|
-
export function createServer<P extends string>({ client, prefix, login }: CreateServerOptions<P>) {
|
|
300
|
+
export function createServer<P extends string>({ client, prefix, login, metricsEnabled }: CreateServerOptions<P>) {
|
|
241
301
|
// Convert string secret to Uint8Array if needed
|
|
242
302
|
const secretKey = typeof login?.jwtSecret === 'string' ? new TextEncoder().encode(login?.jwtSecret) : login?.jwtSecret
|
|
243
303
|
|
|
244
304
|
const routePrefix = (prefix ?? '/api') as P
|
|
245
305
|
|
|
306
|
+
// Auto-detect metricsEnabled from client if not explicitly set
|
|
307
|
+
const isMetricsEnabled = metricsEnabled ?? client.metricsEnabled
|
|
308
|
+
|
|
246
309
|
return new Elysia({
|
|
247
310
|
prefix: routePrefix,
|
|
248
311
|
})
|
|
@@ -345,8 +408,6 @@ export function createServer<P extends string>({ client, prefix, login }: Create
|
|
|
345
408
|
async ({ params, query }) => {
|
|
346
409
|
const options: GetJobStepsOptions = {
|
|
347
410
|
jobId: params.id,
|
|
348
|
-
page: query.page,
|
|
349
|
-
pageSize: query.pageSize,
|
|
350
411
|
search: query.search,
|
|
351
412
|
updatedAfter: query.updatedAfter,
|
|
352
413
|
}
|
|
@@ -490,6 +551,32 @@ export function createServer<P extends string>({ client, prefix, login }: Create
|
|
|
490
551
|
auth: true,
|
|
491
552
|
},
|
|
492
553
|
)
|
|
554
|
+
.post(
|
|
555
|
+
'/jobs/:id/time-travel',
|
|
556
|
+
async ({ params, body }) => {
|
|
557
|
+
const success = await client.timeTravelJob(params.id, body.stepId)
|
|
558
|
+
if (!success) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
`Could not time travel job ${params.id}. The job may not be in a terminal state or the step may not exist.`,
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
message: `Job ${params.id} has been time traveled to step ${body.stepId}`,
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
params: JobIdParamsSchema,
|
|
570
|
+
body: TimeTravelJobBodySchema,
|
|
571
|
+
response: {
|
|
572
|
+
200: TimeTravelJobResponseSchema,
|
|
573
|
+
400: ErrorResponseSchema,
|
|
574
|
+
500: ErrorResponseSchema,
|
|
575
|
+
401: ErrorResponseSchema,
|
|
576
|
+
},
|
|
577
|
+
auth: true,
|
|
578
|
+
},
|
|
579
|
+
)
|
|
493
580
|
.delete(
|
|
494
581
|
'/jobs/:id',
|
|
495
582
|
async ({ params }) => {
|
|
@@ -579,6 +666,74 @@ export function createServer<P extends string>({ client, prefix, login }: Create
|
|
|
579
666
|
auth: true,
|
|
580
667
|
},
|
|
581
668
|
)
|
|
669
|
+
.get(
|
|
670
|
+
'/config',
|
|
671
|
+
async () => {
|
|
672
|
+
return {
|
|
673
|
+
metricsEnabled: isMetricsEnabled,
|
|
674
|
+
authEnabled: !!login,
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
response: {
|
|
679
|
+
200: z.object({
|
|
680
|
+
metricsEnabled: z.boolean(),
|
|
681
|
+
authEnabled: z.boolean(),
|
|
682
|
+
}),
|
|
683
|
+
500: ErrorResponseSchema,
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
)
|
|
687
|
+
.get(
|
|
688
|
+
'/jobs/:id/metrics',
|
|
689
|
+
async ({ params, query }) => {
|
|
690
|
+
if (!isMetricsEnabled) {
|
|
691
|
+
throw new Error('Metrics are not enabled. Use LocalTelemetryAdapter to enable metrics.')
|
|
692
|
+
}
|
|
693
|
+
const options: GetMetricsOptions = {
|
|
694
|
+
jobId: params.id,
|
|
695
|
+
filters: query.filters,
|
|
696
|
+
sort: query.sort,
|
|
697
|
+
}
|
|
698
|
+
return client.getMetrics(options)
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
params: JobIdParamsSchema,
|
|
702
|
+
query: GetMetricsQuerySchema,
|
|
703
|
+
response: {
|
|
704
|
+
200: GetMetricsResultSchema,
|
|
705
|
+
400: ErrorResponseSchema,
|
|
706
|
+
500: ErrorResponseSchema,
|
|
707
|
+
401: ErrorResponseSchema,
|
|
708
|
+
},
|
|
709
|
+
auth: true,
|
|
710
|
+
},
|
|
711
|
+
)
|
|
712
|
+
.get(
|
|
713
|
+
'/steps/:id/metrics',
|
|
714
|
+
async ({ params, query }) => {
|
|
715
|
+
if (!isMetricsEnabled) {
|
|
716
|
+
throw new Error('Metrics are not enabled. Use LocalTelemetryAdapter to enable metrics.')
|
|
717
|
+
}
|
|
718
|
+
const options: GetMetricsOptions = {
|
|
719
|
+
stepId: params.id,
|
|
720
|
+
filters: query.filters,
|
|
721
|
+
sort: query.sort,
|
|
722
|
+
}
|
|
723
|
+
return client.getMetrics(options)
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
params: StepIdParamsSchema,
|
|
727
|
+
query: GetMetricsQuerySchema,
|
|
728
|
+
response: {
|
|
729
|
+
200: GetMetricsResultSchema,
|
|
730
|
+
400: ErrorResponseSchema,
|
|
731
|
+
500: ErrorResponseSchema,
|
|
732
|
+
401: ErrorResponseSchema,
|
|
733
|
+
},
|
|
734
|
+
auth: true,
|
|
735
|
+
},
|
|
736
|
+
)
|
|
582
737
|
.post(
|
|
583
738
|
'/actions/:actionName/run',
|
|
584
739
|
async ({ params, body }) => {
|