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,216 @@
|
|
|
1
|
+
import fastq from 'fastq';
|
|
2
|
+
import { StepOptionsSchema, } from './action.js';
|
|
3
|
+
import { STEP_STATUS_CANCELLED, STEP_STATUS_COMPLETED, STEP_STATUS_FAILED } from './constants.js';
|
|
4
|
+
import { ActionCancelError, isCancelError, isNonRetriableError, NonRetriableError, StepAlreadyExecutedError, StepTimeoutError, serializeError, } from './errors.js';
|
|
5
|
+
import pRetry from './utils/p-retry.js';
|
|
6
|
+
import waitForAbort from './utils/wait-for-abort.js';
|
|
7
|
+
export class StepStore {
|
|
8
|
+
#adapter;
|
|
9
|
+
constructor(adapter) {
|
|
10
|
+
this.#adapter = adapter;
|
|
11
|
+
}
|
|
12
|
+
async getOrCreate(jobId, name, timeoutMs, retriesLimit) {
|
|
13
|
+
try {
|
|
14
|
+
return await this.#adapter.createOrRecoverJobStep({
|
|
15
|
+
jobId,
|
|
16
|
+
name,
|
|
17
|
+
timeoutMs,
|
|
18
|
+
retriesLimit,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new NonRetriableError(`Failed to get or create step "${name}" for job "${jobId}"`, { cause: error });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async updateStatus(stepId, status, output, error) {
|
|
26
|
+
if (status === STEP_STATUS_COMPLETED) {
|
|
27
|
+
return this.#adapter.completeJobStep({ stepId, output });
|
|
28
|
+
}
|
|
29
|
+
else if (status === STEP_STATUS_FAILED) {
|
|
30
|
+
return this.#adapter.failJobStep({ stepId, error });
|
|
31
|
+
}
|
|
32
|
+
else if (status === STEP_STATUS_CANCELLED) {
|
|
33
|
+
return this.#adapter.cancelJobStep({ stepId });
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
async delay(stepId, delayMs, error) {
|
|
38
|
+
return this.#adapter.delayJobStep({ stepId, delayMs, error });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class StepManager {
|
|
42
|
+
#jobId;
|
|
43
|
+
#actionName;
|
|
44
|
+
#stepStore;
|
|
45
|
+
#queue;
|
|
46
|
+
#logger;
|
|
47
|
+
#historySteps = new Set();
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.#jobId = options.jobId;
|
|
50
|
+
this.#actionName = options.actionName;
|
|
51
|
+
this.#logger = options.logger;
|
|
52
|
+
this.#stepStore = new StepStore(options.adapter);
|
|
53
|
+
this.#queue = fastq.promise(async (task) => {
|
|
54
|
+
if (this.#historySteps.has(task.name)) {
|
|
55
|
+
throw new StepAlreadyExecutedError(task.name, this.#jobId, this.#actionName);
|
|
56
|
+
}
|
|
57
|
+
this.#historySteps.add(task.name);
|
|
58
|
+
return this.#executeStep(task.name, task.cb, task.options, task.abortSignal);
|
|
59
|
+
}, options.concurrencyLimit);
|
|
60
|
+
}
|
|
61
|
+
createActionContext(job, action, variables, abortSignal, logger) {
|
|
62
|
+
return new ActionContext(this, job, action, variables, abortSignal, logger);
|
|
63
|
+
}
|
|
64
|
+
async push(task) {
|
|
65
|
+
return this.#queue.push(task);
|
|
66
|
+
}
|
|
67
|
+
async drain() {
|
|
68
|
+
await this.#queue.drain();
|
|
69
|
+
}
|
|
70
|
+
async #executeStep(name, cb, options, abortSignal) {
|
|
71
|
+
const expire = options.expire;
|
|
72
|
+
const retryOptions = options.retry;
|
|
73
|
+
let step = null;
|
|
74
|
+
const executeStep = async () => {
|
|
75
|
+
if (!step) {
|
|
76
|
+
if (abortSignal.aborted) {
|
|
77
|
+
throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled before create step' });
|
|
78
|
+
}
|
|
79
|
+
const newStep = await this.#stepStore.getOrCreate(this.#jobId, name, expire, retryOptions.limit);
|
|
80
|
+
if (!newStep) {
|
|
81
|
+
throw new NonRetriableError(`Failed to create step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`, { cause: 'step not created' });
|
|
82
|
+
}
|
|
83
|
+
step = newStep;
|
|
84
|
+
if (abortSignal.aborted) {
|
|
85
|
+
throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled after create step' });
|
|
86
|
+
}
|
|
87
|
+
if (step.status === STEP_STATUS_COMPLETED) {
|
|
88
|
+
this.#logger.debug({ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id }, '[StepManager] Step recovered (already completed)');
|
|
89
|
+
return step.output;
|
|
90
|
+
}
|
|
91
|
+
else if (step.status === STEP_STATUS_FAILED) {
|
|
92
|
+
throw new NonRetriableError(`Cannot recover a failed step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`, {
|
|
93
|
+
cause: step.error,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else if (step.status === STEP_STATUS_CANCELLED) {
|
|
97
|
+
throw new NonRetriableError(`Cannot recover a cancelled step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`, { cause: step.error });
|
|
98
|
+
}
|
|
99
|
+
this.#logger.debug({ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id }, '[StepManager] Step started executing');
|
|
100
|
+
}
|
|
101
|
+
const stepAbortController = new AbortController();
|
|
102
|
+
const timeoutId = setTimeout(() => {
|
|
103
|
+
const timeoutError = new StepTimeoutError(name, this.#jobId, expire);
|
|
104
|
+
stepAbortController.abort(timeoutError);
|
|
105
|
+
}, expire);
|
|
106
|
+
timeoutId?.unref?.();
|
|
107
|
+
const signal = AbortSignal.any([abortSignal, stepAbortController.signal]);
|
|
108
|
+
try {
|
|
109
|
+
const abortPromise = waitForAbort(signal);
|
|
110
|
+
const callbackPromise = cb({ signal });
|
|
111
|
+
let result = null;
|
|
112
|
+
await Promise.race([
|
|
113
|
+
abortPromise.promise,
|
|
114
|
+
callbackPromise
|
|
115
|
+
.then((res) => {
|
|
116
|
+
if (res !== undefined && res !== null) {
|
|
117
|
+
result = res;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
abortPromise.release();
|
|
122
|
+
}),
|
|
123
|
+
]);
|
|
124
|
+
const completed = await this.#stepStore.updateStatus(step.id, 'completed', result);
|
|
125
|
+
if (!completed) {
|
|
126
|
+
throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`);
|
|
127
|
+
}
|
|
128
|
+
this.#logger.debug({ jobId: this.#jobId, actionName: this.#actionName, stepName: name, stepId: step.id }, '[StepManager] Step finished executing');
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
clearTimeout(timeoutId);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return pRetry(executeStep, {
|
|
136
|
+
retries: retryOptions.limit,
|
|
137
|
+
factor: retryOptions.factor,
|
|
138
|
+
randomize: false,
|
|
139
|
+
signal: abortSignal,
|
|
140
|
+
minTimeout: retryOptions.minTimeout,
|
|
141
|
+
maxTimeout: retryOptions.maxTimeout,
|
|
142
|
+
onFailedAttempt: async (ctx) => {
|
|
143
|
+
const error = ctx.error;
|
|
144
|
+
if (isNonRetriableError(error) ||
|
|
145
|
+
(error.cause && isNonRetriableError(error.cause)) ||
|
|
146
|
+
(error instanceof Error && error.name === 'AbortError' && isNonRetriableError(error.cause))) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (ctx.retriesLeft > 0 && step) {
|
|
150
|
+
const delayed = await this.#stepStore.delay(step.id, ctx.finalDelay, serializeError(error));
|
|
151
|
+
if (!delayed) {
|
|
152
|
+
throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
}).catch(async (error) => {
|
|
157
|
+
if (step) {
|
|
158
|
+
if (isCancelError(error)) {
|
|
159
|
+
await this.#stepStore.updateStatus(step.id, 'cancelled');
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
await this.#stepStore.updateStatus(step.id, STEP_STATUS_FAILED, null, serializeError(error));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
class ActionContext {
|
|
170
|
+
#stepManager;
|
|
171
|
+
#variables;
|
|
172
|
+
#abortSignal;
|
|
173
|
+
#logger;
|
|
174
|
+
#input;
|
|
175
|
+
#jobId;
|
|
176
|
+
#groupKey = '@default';
|
|
177
|
+
#action;
|
|
178
|
+
constructor(stepManager, job, action, variables, abortSignal, logger) {
|
|
179
|
+
this.#stepManager = stepManager;
|
|
180
|
+
this.#variables = variables;
|
|
181
|
+
this.#abortSignal = abortSignal;
|
|
182
|
+
this.#logger = logger;
|
|
183
|
+
this.#action = action;
|
|
184
|
+
this.#jobId = job.id;
|
|
185
|
+
this.#groupKey = job.groupKey ?? '@default';
|
|
186
|
+
if (action.input) {
|
|
187
|
+
this.#input = action.input.parse(job.input, {
|
|
188
|
+
error: () => 'Error parsing action input',
|
|
189
|
+
reportInput: true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
this.#input = job.input ?? {};
|
|
193
|
+
}
|
|
194
|
+
get input() {
|
|
195
|
+
return this.#input;
|
|
196
|
+
}
|
|
197
|
+
get jobId() {
|
|
198
|
+
return this.#jobId;
|
|
199
|
+
}
|
|
200
|
+
get groupKey() {
|
|
201
|
+
return this.#groupKey;
|
|
202
|
+
}
|
|
203
|
+
get var() {
|
|
204
|
+
return this.#variables;
|
|
205
|
+
}
|
|
206
|
+
get logger() {
|
|
207
|
+
return this.#logger;
|
|
208
|
+
}
|
|
209
|
+
async step(name, cb, options = {}) {
|
|
210
|
+
const parsedOptions = StepOptionsSchema.parse({
|
|
211
|
+
...this.#action.steps,
|
|
212
|
+
...options,
|
|
213
|
+
});
|
|
214
|
+
return this.#stepManager.push({ name, cb, options: parsedOptions, abortSignal: this.#abortSignal });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checksum.d.ts","sourceRoot":"","sources":["../../src/utils/checksum.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,IAAI,EAAE,MAAM,UAIpD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface RetryOptions {
|
|
2
|
+
retries: number;
|
|
3
|
+
factor: number;
|
|
4
|
+
minTimeout: number;
|
|
5
|
+
maxTimeout: number;
|
|
6
|
+
maxRetryTime?: number;
|
|
7
|
+
randomize: boolean;
|
|
8
|
+
signal: AbortSignal;
|
|
9
|
+
onFailedAttempt: (context: {
|
|
10
|
+
error: Error;
|
|
11
|
+
attemptNumber: number;
|
|
12
|
+
retriesLeft: number;
|
|
13
|
+
retriesConsumed: number;
|
|
14
|
+
finalDelay: number;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export default function pRetry<TResult>(input: (attemptNumber: number) => Promise<TResult>, options: RetryOptions): Promise<TResult>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=p-retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"p-retry.d.ts","sourceRoot":"","sources":["../../src/utils/p-retry.ts"],"names":[],"mappings":"AAMA,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,WAAW,CAAA;IACnB,eAAe,EAAE,CAAC,OAAO,EAAE;QACzB,KAAK,EAAE,KAAK,CAAA;QACZ,aAAa,EAAE,MAAM,CAAA;QACrB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB;AA6HD,wBAA8B,MAAM,CAAC,OAAO,EAC1C,KAAK,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAClD,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,OAAO,CAAC,CA+DlB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
function validateRetries(retries) {
|
|
2
|
+
if (typeof retries === 'number') {
|
|
3
|
+
if (retries < 0) {
|
|
4
|
+
throw new TypeError('Expected `retries` to be a non-negative number.');
|
|
5
|
+
}
|
|
6
|
+
if (Number.isNaN(retries)) {
|
|
7
|
+
throw new TypeError('Expected `retries` to be a valid number or Infinity, got NaN.');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
else if (retries !== undefined) {
|
|
11
|
+
throw new TypeError('Expected `retries` to be a number or Infinity.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function validateNumberOption(name, value, { min = 0, allowInfinity = false } = {}) {
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
19
|
+
throw new TypeError(`Expected \`${name}\` to be a number${allowInfinity ? ' or Infinity' : ''}.`);
|
|
20
|
+
}
|
|
21
|
+
if (!allowInfinity && !Number.isFinite(value)) {
|
|
22
|
+
throw new TypeError(`Expected \`${name}\` to be a finite number.`);
|
|
23
|
+
}
|
|
24
|
+
if (value < min) {
|
|
25
|
+
throw new TypeError(`Expected \`${name}\` to be \u2265 ${min}.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function calculateDelay(retriesConsumed, options) {
|
|
29
|
+
const attempt = Math.max(1, retriesConsumed + 1);
|
|
30
|
+
const random = options.randomize ? Math.random() + 1 : 1;
|
|
31
|
+
let timeout = Math.round(random * options.minTimeout * options.factor ** (attempt - 1));
|
|
32
|
+
timeout = Math.min(timeout, options.maxTimeout);
|
|
33
|
+
return timeout;
|
|
34
|
+
}
|
|
35
|
+
function calculateRemainingTime(start, max) {
|
|
36
|
+
if (!Number.isFinite(max)) {
|
|
37
|
+
return max;
|
|
38
|
+
}
|
|
39
|
+
return max - (performance.now() - start);
|
|
40
|
+
}
|
|
41
|
+
async function onAttemptFailure({ error, attemptNumber, retriesConsumed, startTime, options, }) {
|
|
42
|
+
const normalizedError = error instanceof Error ? error : new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
|
|
43
|
+
const retriesLeft = Number.isFinite(options.retries)
|
|
44
|
+
? Math.max(0, options.retries - retriesConsumed)
|
|
45
|
+
: options.retries;
|
|
46
|
+
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
|
|
47
|
+
const delayTime = calculateDelay(retriesConsumed, options);
|
|
48
|
+
const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
|
|
49
|
+
const finalDelay = Math.min(delayTime, remainingTime);
|
|
50
|
+
if (remainingTime <= 0) {
|
|
51
|
+
throw normalizedError;
|
|
52
|
+
}
|
|
53
|
+
const context = Object.freeze({
|
|
54
|
+
error: normalizedError,
|
|
55
|
+
attemptNumber,
|
|
56
|
+
retriesLeft,
|
|
57
|
+
retriesConsumed,
|
|
58
|
+
finalDelay,
|
|
59
|
+
});
|
|
60
|
+
await options.onFailedAttempt(context);
|
|
61
|
+
if (remainingTime <= 0 || retriesLeft <= 0) {
|
|
62
|
+
throw normalizedError;
|
|
63
|
+
}
|
|
64
|
+
if (normalizedError instanceof TypeError) {
|
|
65
|
+
options.signal?.throwIfAborted();
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (finalDelay > 0) {
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
const onAbort = () => {
|
|
71
|
+
clearTimeout(timeoutToken);
|
|
72
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
73
|
+
reject(options.signal.reason);
|
|
74
|
+
};
|
|
75
|
+
const timeoutToken = setTimeout(() => {
|
|
76
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
77
|
+
resolve(undefined);
|
|
78
|
+
}, finalDelay);
|
|
79
|
+
timeoutToken.unref?.();
|
|
80
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
options.signal?.throwIfAborted();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
export default async function pRetry(input, options) {
|
|
87
|
+
options = { ...options };
|
|
88
|
+
validateRetries(options.retries);
|
|
89
|
+
if (Object.hasOwn(options, 'forever')) {
|
|
90
|
+
throw new Error('The `forever` option is no longer supported. For many use-cases, you can set `retries: Infinity` instead.');
|
|
91
|
+
}
|
|
92
|
+
options.retries ??= 10;
|
|
93
|
+
options.factor ??= 2;
|
|
94
|
+
options.minTimeout ??= 1000;
|
|
95
|
+
options.maxTimeout ??= Number.POSITIVE_INFINITY;
|
|
96
|
+
options.maxRetryTime ??= Number.POSITIVE_INFINITY;
|
|
97
|
+
options.randomize ??= false;
|
|
98
|
+
validateNumberOption('factor', options.factor, { min: 0, allowInfinity: false });
|
|
99
|
+
validateNumberOption('minTimeout', options.minTimeout, { min: 0, allowInfinity: false });
|
|
100
|
+
validateNumberOption('maxTimeout', options.maxTimeout, { min: 0, allowInfinity: true });
|
|
101
|
+
validateNumberOption('maxRetryTime', options.maxRetryTime, { min: 0, allowInfinity: true });
|
|
102
|
+
if (!(options.factor > 0)) {
|
|
103
|
+
options.factor = 1;
|
|
104
|
+
}
|
|
105
|
+
options.signal?.throwIfAborted();
|
|
106
|
+
let attemptNumber = 0;
|
|
107
|
+
let retriesConsumed = 0;
|
|
108
|
+
const startTime = performance.now();
|
|
109
|
+
while (Number.isFinite(options.retries) ? retriesConsumed <= options.retries : true) {
|
|
110
|
+
attemptNumber++;
|
|
111
|
+
try {
|
|
112
|
+
options.signal?.throwIfAborted();
|
|
113
|
+
const result = await input(attemptNumber);
|
|
114
|
+
options.signal?.throwIfAborted();
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (await onAttemptFailure({
|
|
119
|
+
error: error,
|
|
120
|
+
attemptNumber,
|
|
121
|
+
retriesConsumed,
|
|
122
|
+
startTime,
|
|
123
|
+
options,
|
|
124
|
+
})) {
|
|
125
|
+
retriesConsumed++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new Error('Retry attempts exhausted without throwing an error.');
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wait-for-abort.d.ts","sourceRoot":"","sources":["../../src/utils/wait-for-abort.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,MAAM,EAAE,WAAW;;;EAqCvD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default function waitForAbort(signal) {
|
|
2
|
+
let done = false;
|
|
3
|
+
let globalResolve = null;
|
|
4
|
+
const promise = new Promise((resolve, reject) => {
|
|
5
|
+
if (done) {
|
|
6
|
+
resolve(undefined);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (signal.aborted) {
|
|
10
|
+
done = true;
|
|
11
|
+
reject(signal.reason);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
globalResolve = resolve;
|
|
15
|
+
signal.addEventListener('abort', () => {
|
|
16
|
+
done = true;
|
|
17
|
+
reject(signal.reason);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
promise,
|
|
22
|
+
release: () => {
|
|
23
|
+
if (done) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
globalResolve?.();
|
|
28
|
+
}, 0);
|
|
29
|
+
done = true;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
CREATE SCHEMA IF NOT EXISTS "duron";
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
CREATE TABLE "duron"."job_steps" (
|
|
4
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
5
|
+
"job_id" uuid NOT NULL,
|
|
6
|
+
"name" text NOT NULL,
|
|
7
|
+
"status" text DEFAULT 'active' NOT NULL,
|
|
8
|
+
"output" jsonb,
|
|
9
|
+
"error" jsonb,
|
|
10
|
+
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
11
|
+
"finished_at" timestamp with time zone,
|
|
12
|
+
"timeout_ms" integer NOT NULL,
|
|
13
|
+
"expires_at" timestamp with time zone,
|
|
14
|
+
"retries_limit" integer DEFAULT 0 NOT NULL,
|
|
15
|
+
"retries_count" integer DEFAULT 0 NOT NULL,
|
|
16
|
+
"delayed_ms" integer,
|
|
17
|
+
"history_failed_attempts" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
18
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
19
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
20
|
+
CONSTRAINT "unique_job_step_name" UNIQUE("job_id","name"),
|
|
21
|
+
CONSTRAINT "job_steps_status_check" CHECK ("duron"."job_steps"."status" IN ('active','completed','failed','cancelled'))
|
|
22
|
+
);
|
|
23
|
+
--> statement-breakpoint
|
|
24
|
+
CREATE TABLE "duron"."jobs" (
|
|
25
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
26
|
+
"action_name" text NOT NULL,
|
|
27
|
+
"group_key" text NOT NULL,
|
|
28
|
+
"status" text DEFAULT 'created' NOT NULL,
|
|
29
|
+
"checksum" text NOT NULL,
|
|
30
|
+
"input" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
31
|
+
"output" jsonb,
|
|
32
|
+
"error" jsonb,
|
|
33
|
+
"timeout_ms" integer NOT NULL,
|
|
34
|
+
"expires_at" timestamp with time zone,
|
|
35
|
+
"started_at" timestamp with time zone,
|
|
36
|
+
"finished_at" timestamp with time zone,
|
|
37
|
+
"owner_id" text,
|
|
38
|
+
"concurrency_limit" integer DEFAULT 10 NOT NULL,
|
|
39
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
40
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
41
|
+
CONSTRAINT "jobs_status_check" CHECK ("duron"."jobs"."status" IN ('created','active','completed','failed','cancelled'))
|
|
42
|
+
);
|
|
43
|
+
--> statement-breakpoint
|
|
44
|
+
ALTER TABLE "duron"."job_steps" ADD CONSTRAINT "job_steps_job_id_jobs_id_fk" FOREIGN KEY ("job_id") REFERENCES "duron"."jobs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
45
|
+
CREATE INDEX "idx_job_steps_job_id" ON "duron"."job_steps" USING btree ("job_id");--> statement-breakpoint
|
|
46
|
+
CREATE INDEX "idx_job_steps_status" ON "duron"."job_steps" USING btree ("status");--> statement-breakpoint
|
|
47
|
+
CREATE INDEX "idx_job_steps_name" ON "duron"."job_steps" USING btree ("name");--> statement-breakpoint
|
|
48
|
+
CREATE INDEX "idx_job_steps_expires_at" ON "duron"."job_steps" USING btree ("expires_at");--> statement-breakpoint
|
|
49
|
+
CREATE INDEX "idx_job_steps_job_status" ON "duron"."job_steps" USING btree ("job_id","status");--> statement-breakpoint
|
|
50
|
+
CREATE INDEX "idx_job_steps_job_name" ON "duron"."job_steps" USING btree ("job_id","name");--> statement-breakpoint
|
|
51
|
+
CREATE INDEX "idx_job_steps_output_fts" ON "duron"."job_steps" USING gin (to_tsvector('english', "output"::text));--> statement-breakpoint
|
|
52
|
+
CREATE INDEX "idx_jobs_action_name" ON "duron"."jobs" USING btree ("action_name");--> statement-breakpoint
|
|
53
|
+
CREATE INDEX "idx_jobs_status" ON "duron"."jobs" USING btree ("status");--> statement-breakpoint
|
|
54
|
+
CREATE INDEX "idx_jobs_group_key" ON "duron"."jobs" USING btree ("group_key");--> statement-breakpoint
|
|
55
|
+
CREATE INDEX "idx_jobs_started_at" ON "duron"."jobs" USING btree ("started_at");--> statement-breakpoint
|
|
56
|
+
CREATE INDEX "idx_jobs_finished_at" ON "duron"."jobs" USING btree ("finished_at");--> statement-breakpoint
|
|
57
|
+
CREATE INDEX "idx_jobs_expires_at" ON "duron"."jobs" USING btree ("expires_at");--> statement-breakpoint
|
|
58
|
+
CREATE INDEX "idx_jobs_owner_id" ON "duron"."jobs" USING btree ("owner_id");--> statement-breakpoint
|
|
59
|
+
CREATE INDEX "idx_jobs_checksum" ON "duron"."jobs" USING btree ("checksum");--> statement-breakpoint
|
|
60
|
+
CREATE INDEX "idx_jobs_concurrency_limit" ON "duron"."jobs" USING btree ("concurrency_limit");--> statement-breakpoint
|
|
61
|
+
CREATE INDEX "idx_jobs_action_status" ON "duron"."jobs" USING btree ("action_name","status");--> statement-breakpoint
|
|
62
|
+
CREATE INDEX "idx_jobs_action_group" ON "duron"."jobs" USING btree ("action_name","group_key");--> statement-breakpoint
|
|
63
|
+
CREATE INDEX "idx_jobs_input_fts" ON "duron"."jobs" USING gin (to_tsvector('english', "input"::text));--> statement-breakpoint
|
|
64
|
+
CREATE INDEX "idx_jobs_output_fts" ON "duron"."jobs" USING gin (to_tsvector('english', "output"::text));
|