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.
- package/LICENSE +7 -0
- package/README.md +140 -0
- package/dist/action-job.d.ts +24 -0
- package/dist/action-job.d.ts.map +1 -0
- package/dist/action-job.js +108 -0
- package/dist/action-manager.d.ts +21 -0
- package/dist/action-manager.d.ts.map +1 -0
- package/dist/action-manager.js +78 -0
- package/dist/action.d.ts +129 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +87 -0
- package/dist/adapters/adapter.d.ts +92 -0
- package/dist/adapters/adapter.d.ts.map +1 -0
- package/dist/adapters/adapter.js +424 -0
- package/dist/adapters/postgres/drizzle.config.d.ts +3 -0
- package/dist/adapters/postgres/drizzle.config.d.ts.map +1 -0
- package/dist/adapters/postgres/drizzle.config.js +10 -0
- package/dist/adapters/postgres/pglite.d.ts +13 -0
- package/dist/adapters/postgres/pglite.d.ts.map +1 -0
- package/dist/adapters/postgres/pglite.js +36 -0
- package/dist/adapters/postgres/postgres.d.ts +51 -0
- package/dist/adapters/postgres/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres/postgres.js +867 -0
- package/dist/adapters/postgres/schema.d.ts +581 -0
- package/dist/adapters/postgres/schema.d.ts.map +1 -0
- package/dist/adapters/postgres/schema.default.d.ts +577 -0
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -0
- package/dist/adapters/postgres/schema.default.js +3 -0
- package/dist/adapters/postgres/schema.js +87 -0
- package/dist/adapters/schemas.d.ts +516 -0
- package/dist/adapters/schemas.d.ts.map +1 -0
- package/dist/adapters/schemas.js +184 -0
- package/dist/client.d.ts +85 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +416 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +22 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +1193 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +516 -0
- package/dist/step-manager.d.ts +46 -0
- package/dist/step-manager.d.ts.map +1 -0
- package/dist/step-manager.js +216 -0
- package/dist/utils/checksum.d.ts +2 -0
- package/dist/utils/checksum.d.ts.map +1 -0
- package/dist/utils/checksum.js +6 -0
- package/dist/utils/p-retry.d.ts +19 -0
- package/dist/utils/p-retry.d.ts.map +1 -0
- package/dist/utils/p-retry.js +130 -0
- package/dist/utils/wait-for-abort.d.ts +5 -0
- package/dist/utils/wait-for-abort.d.ts.map +1 -0
- package/dist/utils/wait-for-abort.js +32 -0
- package/migrations/postgres/0000_lethal_speed_demon.sql +64 -0
- package/migrations/postgres/meta/0000_snapshot.json +606 -0
- package/migrations/postgres/meta/_journal.json +13 -0
- package/package.json +88 -0
- package/src/action-job.ts +201 -0
- package/src/action-manager.ts +166 -0
- package/src/action.ts +247 -0
- package/src/adapters/adapter.ts +969 -0
- package/src/adapters/postgres/drizzle.config.ts +11 -0
- package/src/adapters/postgres/pglite.ts +86 -0
- package/src/adapters/postgres/postgres.ts +1346 -0
- package/src/adapters/postgres/schema.default.ts +5 -0
- package/src/adapters/postgres/schema.ts +119 -0
- package/src/adapters/schemas.ts +320 -0
- package/src/client.ts +859 -0
- package/src/constants.ts +37 -0
- package/src/errors.ts +205 -0
- package/src/index.ts +14 -0
- package/src/server.ts +718 -0
- package/src/step-manager.ts +471 -0
- package/src/utils/checksum.ts +7 -0
- package/src/utils/p-retry.ts +213 -0
- package/src/utils/wait-for-abort.ts +40 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { Logger } from 'pino'
|
|
2
|
+
|
|
3
|
+
import type { Action } from './action.js'
|
|
4
|
+
import type { Adapter } from './adapters/adapter.js'
|
|
5
|
+
import { ActionCancelError, ActionTimeoutError, isCancelError, StepTimeoutError, serializeError } from './errors.js'
|
|
6
|
+
import { StepManager } from './step-manager.js'
|
|
7
|
+
import waitForAbort from './utils/wait-for-abort.js'
|
|
8
|
+
|
|
9
|
+
export interface ActionJobOptions<TAction extends Action<any, any, any>> {
|
|
10
|
+
job: { id: string; input: any; groupKey: string; timeoutMs: number; actionName: string }
|
|
11
|
+
action: TAction
|
|
12
|
+
database: Adapter
|
|
13
|
+
variables: Record<string, unknown>
|
|
14
|
+
logger: Logger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ActionJob represents a single job execution for an action.
|
|
19
|
+
* Manages the execution lifecycle, timeout handling, and cancellation.
|
|
20
|
+
*
|
|
21
|
+
* @template TAction - The action type being executed
|
|
22
|
+
*/
|
|
23
|
+
export class ActionJob<TAction extends Action<any, any, any>> {
|
|
24
|
+
#job: { id: string; input: any; groupKey: string; timeoutMs: number; actionName: string }
|
|
25
|
+
#action: TAction
|
|
26
|
+
#database: Adapter
|
|
27
|
+
#variables: Record<string, unknown>
|
|
28
|
+
#logger: Logger
|
|
29
|
+
#stepManager: StepManager
|
|
30
|
+
#abortController: AbortController
|
|
31
|
+
#timeoutId: NodeJS.Timeout | null = null
|
|
32
|
+
#done: Promise<void>
|
|
33
|
+
#resolve: (() => void) | null = null
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Constructor
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a new ActionJob instance.
|
|
41
|
+
*
|
|
42
|
+
* @param options - Configuration options for the action job
|
|
43
|
+
*/
|
|
44
|
+
constructor(options: ActionJobOptions<TAction>) {
|
|
45
|
+
this.#job = options.job
|
|
46
|
+
this.#action = options.action
|
|
47
|
+
this.#database = options.database
|
|
48
|
+
this.#variables = options.variables
|
|
49
|
+
this.#logger = options.logger
|
|
50
|
+
this.#abortController = new AbortController()
|
|
51
|
+
|
|
52
|
+
// Create StepManager for this job
|
|
53
|
+
this.#stepManager = new StepManager({
|
|
54
|
+
jobId: options.job.id,
|
|
55
|
+
actionName: options.job.actionName,
|
|
56
|
+
adapter: options.database,
|
|
57
|
+
logger: options.logger,
|
|
58
|
+
concurrencyLimit: options.action.concurrency,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
this.#done = new Promise((resolve) => {
|
|
62
|
+
this.#resolve = resolve
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Public API Methods
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute the action job.
|
|
72
|
+
* Creates the action context, sets up timeout, executes the handler,
|
|
73
|
+
* validates output, and marks the job as completed or failed.
|
|
74
|
+
*
|
|
75
|
+
* @returns Promise resolving to the action result
|
|
76
|
+
* @throws ActionTimeoutError if the job times out
|
|
77
|
+
* @throws ActionCancelError if the job is cancelled
|
|
78
|
+
* @throws Error if the job fails or output validation fails
|
|
79
|
+
*/
|
|
80
|
+
async execute() {
|
|
81
|
+
try {
|
|
82
|
+
// Create a child logger for this job
|
|
83
|
+
const jobLogger = this.#logger.child({
|
|
84
|
+
jobId: this.#job.id,
|
|
85
|
+
actionName: this.#action.name,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Create action context with step manager
|
|
89
|
+
const ctx = this.#stepManager.createActionContext(
|
|
90
|
+
this.#job,
|
|
91
|
+
this.#action,
|
|
92
|
+
this.#variables as any,
|
|
93
|
+
this.#abortController.signal,
|
|
94
|
+
jobLogger,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
this.#timeoutId = setTimeout(() => {
|
|
98
|
+
const timeoutError = new ActionTimeoutError(this.#action.name, this.#job.timeoutMs)
|
|
99
|
+
this.#abortController.abort(timeoutError)
|
|
100
|
+
}, this.#job.timeoutMs)
|
|
101
|
+
|
|
102
|
+
this.#timeoutId?.unref?.()
|
|
103
|
+
|
|
104
|
+
// Execute handler with timeout - race between handler and abort signal
|
|
105
|
+
const abortWaiter = waitForAbort(this.#abortController.signal)
|
|
106
|
+
let result: any = null
|
|
107
|
+
await Promise.race([
|
|
108
|
+
this.#action
|
|
109
|
+
.handler(ctx)
|
|
110
|
+
.then((res) => {
|
|
111
|
+
if (res !== undefined) {
|
|
112
|
+
result = res
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.finally(() => {
|
|
116
|
+
abortWaiter.release()
|
|
117
|
+
}),
|
|
118
|
+
abortWaiter.promise,
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
// Validate output if schema is provided
|
|
122
|
+
if (this.#action.output) {
|
|
123
|
+
result = this.#action.output.parse(result, {
|
|
124
|
+
error: () => 'Error parsing action output',
|
|
125
|
+
reportInput: true,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Complete job
|
|
130
|
+
const completed = await this.#database.completeJob({ jobId: this.#job.id, output: result })
|
|
131
|
+
if (!completed) {
|
|
132
|
+
throw new Error('Job not completed')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Log action completion
|
|
136
|
+
this.#logger.debug(
|
|
137
|
+
{ jobId: this.#job.id, actionName: this.#action.name },
|
|
138
|
+
'[ActionJob] Action finished executing',
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (
|
|
144
|
+
isCancelError(error) ||
|
|
145
|
+
(error instanceof Error && error.name === 'AbortError' && isCancelError(error.cause))
|
|
146
|
+
) {
|
|
147
|
+
this.#logger.warn({ jobId: this.#job.id, actionName: this.#action.name }, '[ActionJob] Job cancelled')
|
|
148
|
+
await this.#database.cancelJob({ jobId: this.#job.id })
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const message =
|
|
153
|
+
error instanceof ActionTimeoutError
|
|
154
|
+
? '[ActionJob] Job timed out'
|
|
155
|
+
: error instanceof StepTimeoutError
|
|
156
|
+
? '[ActionJob] Step timed out'
|
|
157
|
+
: '[ActionJob] Job failed'
|
|
158
|
+
|
|
159
|
+
this.#logger.error({ jobId: this.#job.id, actionName: this.#action.name }, message)
|
|
160
|
+
await this.#database.failJob({ jobId: this.#job.id, error: serializeError(error) })
|
|
161
|
+
throw error
|
|
162
|
+
} finally {
|
|
163
|
+
this.#clear()
|
|
164
|
+
this.#resolve?.()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wait for the job execution to complete.
|
|
170
|
+
* Returns a promise that resolves when the job finishes (successfully or with error).
|
|
171
|
+
*
|
|
172
|
+
* @returns Promise that resolves when the job is done
|
|
173
|
+
*/
|
|
174
|
+
waitForDone(): Promise<void> {
|
|
175
|
+
return this.#done
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Cancel the job execution.
|
|
180
|
+
* Clears the timeout and aborts the action handler.
|
|
181
|
+
*/
|
|
182
|
+
cancel() {
|
|
183
|
+
this.#clear()
|
|
184
|
+
const cancelError = new ActionCancelError(this.#action.name, this.#job.id)
|
|
185
|
+
this.#abortController.abort(cancelError)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Private Methods
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Clear the timeout timer.
|
|
194
|
+
*/
|
|
195
|
+
#clear() {
|
|
196
|
+
if (this.#timeoutId) {
|
|
197
|
+
clearTimeout(this.#timeoutId)
|
|
198
|
+
this.#timeoutId = null
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fastq from 'fastq'
|
|
2
|
+
import type { Logger } from 'pino'
|
|
3
|
+
|
|
4
|
+
import type { Action } from './action.js'
|
|
5
|
+
import { ActionJob } from './action-job.js'
|
|
6
|
+
import type { Adapter, Job } from './adapters/adapter.js'
|
|
7
|
+
|
|
8
|
+
export interface ActionManagerOptions<TAction extends Action<any, any, any>> {
|
|
9
|
+
action: TAction
|
|
10
|
+
database: Adapter
|
|
11
|
+
variables: Record<string, unknown>
|
|
12
|
+
logger: Logger
|
|
13
|
+
concurrencyLimit: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ActionManager manages the execution of jobs for a specific action.
|
|
18
|
+
* Uses a fastq queue to control concurrency and process jobs.
|
|
19
|
+
*
|
|
20
|
+
* @template TAction - The action type being managed
|
|
21
|
+
*/
|
|
22
|
+
export class ActionManager<TAction extends Action<any, any, any>> {
|
|
23
|
+
#action: TAction
|
|
24
|
+
#database: Adapter
|
|
25
|
+
#variables: Record<string, unknown>
|
|
26
|
+
#logger: Logger
|
|
27
|
+
#queue: fastq.queueAsPromised<Job, void>
|
|
28
|
+
#concurrencyLimit: number
|
|
29
|
+
#activeJobs = new Map<string, ActionJob<TAction>>()
|
|
30
|
+
#stopped: boolean = false
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Constructor
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new ActionManager instance.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration options for the action manager
|
|
40
|
+
*/
|
|
41
|
+
constructor(options: ActionManagerOptions<TAction>) {
|
|
42
|
+
this.#action = options.action
|
|
43
|
+
this.#database = options.database
|
|
44
|
+
this.#variables = options.variables
|
|
45
|
+
this.#logger = options.logger
|
|
46
|
+
this.#concurrencyLimit = options.concurrencyLimit
|
|
47
|
+
|
|
48
|
+
// Create fastq queue with action concurrency limit
|
|
49
|
+
this.#queue = fastq.promise(async (job: Job) => {
|
|
50
|
+
if (this.#stopped) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await this.#executeJob(job)
|
|
55
|
+
}, this.#concurrencyLimit)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Public API Methods
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Queue a job for execution.
|
|
64
|
+
*
|
|
65
|
+
* @param job - The job to queue
|
|
66
|
+
* @returns Promise that resolves when the job is queued
|
|
67
|
+
*/
|
|
68
|
+
async push(job: Job): Promise<void> {
|
|
69
|
+
return this.#queue.push(job)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Cancel a specific job by ID.
|
|
74
|
+
*
|
|
75
|
+
* @param jobId - The ID of the job to cancel
|
|
76
|
+
* @returns If the manager has the job, it will be cancelled and true will be returned. Otherwise, false will be returned.
|
|
77
|
+
*/
|
|
78
|
+
cancelJob(jobId: string) {
|
|
79
|
+
const actionJob = this.#activeJobs.get(jobId)
|
|
80
|
+
if (actionJob) {
|
|
81
|
+
actionJob.cancel()
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Cancel all active jobs.
|
|
89
|
+
*/
|
|
90
|
+
abortAll(): void {
|
|
91
|
+
for (const actionJob of this.#activeJobs.values()) {
|
|
92
|
+
actionJob.cancel()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if the queue is idle (no jobs being processed).
|
|
98
|
+
*
|
|
99
|
+
* @returns Promise resolving to `true` if idle, `false` otherwise
|
|
100
|
+
*/
|
|
101
|
+
async idle(): Promise<boolean> {
|
|
102
|
+
return this.#queue.idle()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Wait for the queue to drain (all jobs completed).
|
|
107
|
+
*
|
|
108
|
+
* @returns Promise that resolves when the queue is drained
|
|
109
|
+
*/
|
|
110
|
+
async drain(): Promise<void> {
|
|
111
|
+
return this.#queue.drain()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stop the action manager.
|
|
116
|
+
* Aborts all active jobs and waits for the queue to drain.
|
|
117
|
+
*
|
|
118
|
+
* @returns Promise that resolves when the action manager is stopped
|
|
119
|
+
*/
|
|
120
|
+
async stop(): Promise<void> {
|
|
121
|
+
if (this.#stopped) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.#stopped = true
|
|
126
|
+
this.abortAll()
|
|
127
|
+
await this.#queue.killAndDrain()
|
|
128
|
+
await Promise.all(Array.from(this.#activeJobs.values()).map((actionJob) => actionJob.waitForDone()))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Private Methods
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Execute a job by creating an ActionJob and running it.
|
|
137
|
+
*
|
|
138
|
+
* @param job - The job to execute
|
|
139
|
+
*/
|
|
140
|
+
async #executeJob(job: Job) {
|
|
141
|
+
// Create ActionJob for this job
|
|
142
|
+
const actionJob = new ActionJob({
|
|
143
|
+
job: {
|
|
144
|
+
id: job.id,
|
|
145
|
+
input: job.input,
|
|
146
|
+
groupKey: job.groupKey,
|
|
147
|
+
timeoutMs: job.timeoutMs,
|
|
148
|
+
actionName: job.actionName,
|
|
149
|
+
},
|
|
150
|
+
action: this.#action,
|
|
151
|
+
database: this.#database,
|
|
152
|
+
variables: this.#variables,
|
|
153
|
+
logger: this.#logger,
|
|
154
|
+
})
|
|
155
|
+
this.#activeJobs.set(job.id, actionJob)
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Execute the job - all error handling is done inside ActionJob.execute()
|
|
159
|
+
await actionJob.execute()
|
|
160
|
+
} finally {
|
|
161
|
+
// Always cleanup, even if the job failed or was cancelled
|
|
162
|
+
// Errors are already handled in ActionJob.execute() (logging, failing job, etc.)
|
|
163
|
+
this.#activeJobs.delete(job.id)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/action.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { Logger } from 'pino'
|
|
2
|
+
import * as z from 'zod'
|
|
3
|
+
|
|
4
|
+
import generateChecksum from './utils/checksum.js'
|
|
5
|
+
|
|
6
|
+
export type RetryOptions = z.infer<typeof RetryOptionsSchema>
|
|
7
|
+
|
|
8
|
+
export type StepOptions = z.infer<typeof StepOptionsSchema>
|
|
9
|
+
|
|
10
|
+
export interface ActionHandlerContext<TInput extends z.ZodObject, TVariables = Record<string, unknown>> {
|
|
11
|
+
input: z.infer<TInput>
|
|
12
|
+
jobId: string
|
|
13
|
+
groupKey: string
|
|
14
|
+
var: TVariables
|
|
15
|
+
logger: Logger
|
|
16
|
+
step: <TResult>(
|
|
17
|
+
name: string,
|
|
18
|
+
cb: (ctx: StepHandlerContext) => Promise<TResult>,
|
|
19
|
+
options?: z.input<typeof StepOptionsSchema>,
|
|
20
|
+
) => Promise<TResult>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StepHandlerContext {
|
|
24
|
+
signal: AbortSignal
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ConcurrencyHandlerContext<TInput extends z.ZodObject, TVariables = Record<string, unknown>> {
|
|
28
|
+
input: z.infer<TInput>
|
|
29
|
+
var: TVariables
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ActionDefinition<
|
|
33
|
+
TInput extends z.ZodObject,
|
|
34
|
+
TOutput extends z.ZodObject,
|
|
35
|
+
TVariables = Record<string, unknown>,
|
|
36
|
+
> = z.input<ReturnType<typeof createActionDefinitionSchema<TInput, TOutput, TVariables>>>
|
|
37
|
+
|
|
38
|
+
export type Action<
|
|
39
|
+
TInput extends z.ZodObject,
|
|
40
|
+
TOutput extends z.ZodObject,
|
|
41
|
+
TVariables = Record<string, unknown>,
|
|
42
|
+
> = z.infer<ReturnType<typeof createActionDefinitionSchema<TInput, TOutput, TVariables>>>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Retry configuration options for actions and steps.
|
|
46
|
+
*/
|
|
47
|
+
export const RetryOptionsSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
/**
|
|
50
|
+
* Maximum number of retry attempts.
|
|
51
|
+
*
|
|
52
|
+
* @default 4
|
|
53
|
+
*/
|
|
54
|
+
limit: z.number().default(4),
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Exponential backoff factor.
|
|
58
|
+
* The delay between retries is calculated as: minTimeout * (factor ^ attemptNumber)
|
|
59
|
+
*
|
|
60
|
+
* @default 2
|
|
61
|
+
*/
|
|
62
|
+
factor: z.number().default(2),
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Minimum delay in milliseconds before the first retry.
|
|
66
|
+
*
|
|
67
|
+
* @default 1000
|
|
68
|
+
*/
|
|
69
|
+
minTimeout: z.number().default(1000),
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Maximum delay in milliseconds between retries.
|
|
73
|
+
* The calculated delay will be capped at this value.
|
|
74
|
+
*
|
|
75
|
+
* @default 30000
|
|
76
|
+
*/
|
|
77
|
+
maxTimeout: z.number().default(30000),
|
|
78
|
+
})
|
|
79
|
+
.default({ limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 })
|
|
80
|
+
.describe('The retry options')
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Options for configuring a step within an action.
|
|
84
|
+
*/
|
|
85
|
+
export const StepOptionsSchema = z.object({
|
|
86
|
+
/**
|
|
87
|
+
* Retry configuration for this step.
|
|
88
|
+
* If not provided, uses the default retry options from the action or Duron instance.
|
|
89
|
+
*/
|
|
90
|
+
retry: RetryOptionsSchema,
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Timeout in milliseconds for this step.
|
|
94
|
+
* Steps that exceed this timeout will be cancelled.
|
|
95
|
+
*
|
|
96
|
+
* @default 300000 (5 minutes)
|
|
97
|
+
*/
|
|
98
|
+
expire: z
|
|
99
|
+
.number()
|
|
100
|
+
.default(5 * 60 * 1000)
|
|
101
|
+
.describe('The expire time for the step (milliseconds)'),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates a Zod schema for validating action definitions.
|
|
106
|
+
*
|
|
107
|
+
* @template TInput - Zod schema for the action input
|
|
108
|
+
* @template TOutput - Zod schema for the action output
|
|
109
|
+
* @template TVariables - Type of variables available to the action
|
|
110
|
+
* @returns Zod schema for action definitions
|
|
111
|
+
*/
|
|
112
|
+
export function createActionDefinitionSchema<
|
|
113
|
+
TInput extends z.ZodObject,
|
|
114
|
+
TOutput extends z.ZodObject,
|
|
115
|
+
TVariables = Record<string, unknown>,
|
|
116
|
+
>() {
|
|
117
|
+
return z
|
|
118
|
+
.object({
|
|
119
|
+
/**
|
|
120
|
+
* Unique name for this action.
|
|
121
|
+
* Used as the queue name and must be unique across all actions.
|
|
122
|
+
* Required.
|
|
123
|
+
*/
|
|
124
|
+
name: z.string().describe('The name of the action'),
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Version of the action.
|
|
128
|
+
* Used to track changes to the action and generate the checksum.
|
|
129
|
+
*/
|
|
130
|
+
version: z.string().describe('The version of the action').optional(),
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Zod schema for validating the action input.
|
|
134
|
+
* If provided, input will be validated before the handler is called.
|
|
135
|
+
* If not provided, any input will be accepted.
|
|
136
|
+
*/
|
|
137
|
+
input: z
|
|
138
|
+
.custom<TInput>((val: any) => {
|
|
139
|
+
return !val || ('_zod' in val && 'type' in val && val.type === 'object')
|
|
140
|
+
})
|
|
141
|
+
.optional(),
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Zod schema for validating the action output.
|
|
145
|
+
* If provided, output will be validated after the handler completes.
|
|
146
|
+
* If not provided, any output will be accepted.
|
|
147
|
+
*/
|
|
148
|
+
output: z
|
|
149
|
+
.custom<TOutput>((val: any) => {
|
|
150
|
+
return !val || ('_zod' in val && 'type' in val && val.type === 'object')
|
|
151
|
+
})
|
|
152
|
+
.optional(),
|
|
153
|
+
|
|
154
|
+
groups: z
|
|
155
|
+
.object({
|
|
156
|
+
/**
|
|
157
|
+
* Function to determine the group key for a job.
|
|
158
|
+
* Jobs with the same group key will respect the group concurrency limit.
|
|
159
|
+
* If not provided, all jobs for this action will use the '@default' group key.
|
|
160
|
+
*
|
|
161
|
+
* @param ctx - Context containing the input and variables
|
|
162
|
+
* @returns Promise resolving to the group key string
|
|
163
|
+
*/
|
|
164
|
+
groupKey: z
|
|
165
|
+
.custom<(ctx: ConcurrencyHandlerContext<TInput, TVariables>) => Promise<string>>((val) => {
|
|
166
|
+
return !val || val instanceof Function
|
|
167
|
+
})
|
|
168
|
+
.optional(),
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Function to determine the concurrency limit for a job.
|
|
172
|
+
* The concurrency limit is stored with each job and used during fetch operations.
|
|
173
|
+
* When fetching jobs, the latest job's concurrency limit is used for each groupKey.
|
|
174
|
+
* If not provided, defaults to 10.
|
|
175
|
+
*
|
|
176
|
+
* @param ctx - Context containing the input and variables
|
|
177
|
+
* @returns Promise resolving to the concurrency limit number
|
|
178
|
+
*/
|
|
179
|
+
concurrency: z
|
|
180
|
+
.custom<(ctx: ConcurrencyHandlerContext<TInput, TVariables>) => Promise<number>>((val) => {
|
|
181
|
+
return !val || val instanceof Function
|
|
182
|
+
})
|
|
183
|
+
.optional(),
|
|
184
|
+
})
|
|
185
|
+
.optional(),
|
|
186
|
+
|
|
187
|
+
steps: z
|
|
188
|
+
.object({
|
|
189
|
+
/**
|
|
190
|
+
* Function to determine the concurrency limit for a step.
|
|
191
|
+
* The concurrency limit is stored with each step and used during fetch operations.
|
|
192
|
+
* When fetching steps, the latest step's concurrency limit is used for each stepKey.
|
|
193
|
+
* If not provided, defaults to 10.
|
|
194
|
+
*/
|
|
195
|
+
concurrency: z.number().default(10).describe('How many steps can run concurrently for this action'),
|
|
196
|
+
retry: RetryOptionsSchema.describe('How to retry on failure for the steps of this action'),
|
|
197
|
+
expire: z
|
|
198
|
+
.number()
|
|
199
|
+
.default(5 * 60 * 1000)
|
|
200
|
+
.describe('How long a step can run for (milliseconds)'),
|
|
201
|
+
})
|
|
202
|
+
.default({
|
|
203
|
+
concurrency: 10,
|
|
204
|
+
retry: { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 },
|
|
205
|
+
expire: 5 * 60 * 1000,
|
|
206
|
+
}),
|
|
207
|
+
|
|
208
|
+
concurrency: z.number().default(100).describe('How many jobs can run concurrently for this action'),
|
|
209
|
+
|
|
210
|
+
expire: z
|
|
211
|
+
.number()
|
|
212
|
+
.default(15 * 60 * 1000)
|
|
213
|
+
.describe('How long a job can run for (milliseconds)'),
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The handler function that executes the action logic.
|
|
217
|
+
* Receives a context object with input, variables, and a step function.
|
|
218
|
+
* Must return a Promise that resolves to the action output.
|
|
219
|
+
* Required.
|
|
220
|
+
*
|
|
221
|
+
* @param ctx - Action handler context
|
|
222
|
+
* @returns Promise resolving to the action output
|
|
223
|
+
*/
|
|
224
|
+
handler: z
|
|
225
|
+
.custom<(ctx: ActionHandlerContext<TInput, TVariables>) => Promise<z.infer<TOutput>>>((val) => {
|
|
226
|
+
return val instanceof Function
|
|
227
|
+
})
|
|
228
|
+
.describe('The handler for the action'),
|
|
229
|
+
})
|
|
230
|
+
.transform((def) => {
|
|
231
|
+
const checksum = [def.name, def.version, def.handler.toString()].filter(Boolean).join(':')
|
|
232
|
+
return {
|
|
233
|
+
...def,
|
|
234
|
+
checksum: generateChecksum(checksum),
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const defineAction = <TVariables = Record<string, unknown>>() => {
|
|
240
|
+
return <TInput extends z.ZodObject, TOutput extends z.ZodObject>(
|
|
241
|
+
def: ActionDefinition<TInput, TOutput, TVariables>,
|
|
242
|
+
) => {
|
|
243
|
+
return createActionDefinitionSchema<TInput, TOutput, TVariables>().parse(def, {
|
|
244
|
+
reportInput: true,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|