scrapme-flow 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +126 -2
- package/dist/src/commands/commands.models.d.ts +41 -0
- package/dist/src/commands/commands.models.js +2 -0
- package/dist/src/commands/commands.models.js.map +1 -0
- package/dist/src/commands/save-document.command.d.ts +9 -0
- package/dist/src/commands/save-document.command.js +33 -0
- package/dist/src/commands/save-document.command.js.map +1 -0
- package/dist/src/commands/scrape-page.command.d.ts +20 -0
- package/dist/src/commands/scrape-page.command.js +93 -0
- package/dist/src/commands/scrape-page.command.js.map +1 -0
- package/dist/src/commands/send-mail.command.d.ts +11 -0
- package/dist/src/commands/send-mail.command.js +39 -0
- package/dist/src/commands/send-mail.command.js.map +1 -0
- package/dist/src/config/config-builder.d.ts +24 -0
- package/dist/src/config/config-builder.js +129 -0
- package/dist/src/config/config-builder.js.map +1 -0
- package/dist/src/config/config-loader.d.ts +10 -0
- package/dist/src/config/config-loader.js +79 -0
- package/dist/src/config/config-loader.js.map +1 -0
- package/dist/src/config/config.models.d.ts +144 -0
- package/dist/src/config/config.models.js +2 -0
- package/dist/src/config/config.models.js.map +1 -0
- package/dist/src/config/config.schema.d.ts +75 -0
- package/dist/src/config/config.schema.js +87 -0
- package/dist/src/config/config.schema.js.map +1 -0
- package/dist/src/config/dsn-check.d.ts +5 -0
- package/dist/src/config/dsn-check.js +12 -0
- package/dist/src/config/dsn-check.js.map +1 -0
- package/dist/src/di/container.d.ts +13 -0
- package/dist/src/di/container.js +67 -0
- package/dist/src/di/container.js.map +1 -0
- package/dist/src/di/types.d.ts +27 -0
- package/dist/src/di/types.js +28 -0
- package/dist/src/di/types.js.map +1 -0
- package/dist/src/flow/command-executor.d.ts +18 -0
- package/dist/src/flow/command-executor.js +87 -0
- package/dist/src/flow/command-executor.js.map +1 -0
- package/dist/src/flow/command-resolver.d.ts +11 -0
- package/dist/src/flow/command-resolver.js +35 -0
- package/dist/src/flow/command-resolver.js.map +1 -0
- package/dist/src/flow/flow-catalog.d.ts +12 -0
- package/dist/src/flow/flow-catalog.js +31 -0
- package/dist/src/flow/flow-catalog.js.map +1 -0
- package/dist/src/flow/flow-errors.d.ts +66 -0
- package/dist/src/flow/flow-errors.js +119 -0
- package/dist/src/flow/flow-errors.js.map +1 -0
- package/dist/src/flow/flow-job-processor.d.ts +33 -0
- package/dist/src/flow/flow-job-processor.js +208 -0
- package/dist/src/flow/flow-job-processor.js.map +1 -0
- package/dist/src/flow/flow-params.d.ts +9 -0
- package/dist/src/flow/flow-params.js +52 -0
- package/dist/src/flow/flow-params.js.map +1 -0
- package/dist/src/flow/flow-runner.d.ts +36 -0
- package/dist/src/flow/flow-runner.js +209 -0
- package/dist/src/flow/flow-runner.js.map +1 -0
- package/dist/src/flow/flow-scheduler.d.ts +24 -0
- package/dist/src/flow/flow-scheduler.js +96 -0
- package/dist/src/flow/flow-scheduler.js.map +1 -0
- package/dist/src/flow/flow-validator.d.ts +29 -0
- package/dist/src/flow/flow-validator.js +141 -0
- package/dist/src/flow/flow-validator.js.map +1 -0
- package/dist/src/flow/flow.d.ts +32 -0
- package/dist/src/flow/flow.js +50 -0
- package/dist/src/flow/flow.js.map +1 -0
- package/dist/src/flow/flow.models.d.ts +288 -0
- package/dist/src/flow/flow.models.js +38 -0
- package/dist/src/flow/flow.models.js.map +1 -0
- package/dist/src/flow/retry-policy.d.ts +9 -0
- package/dist/src/flow/retry-policy.js +15 -0
- package/dist/src/flow/retry-policy.js.map +1 -0
- package/dist/src/flow/state-path.d.ts +21 -0
- package/dist/src/flow/state-path.js +37 -0
- package/dist/src/flow/state-path.js.map +1 -0
- package/dist/src/flow/step-input-builder.d.ts +15 -0
- package/dist/src/flow/step-input-builder.js +63 -0
- package/dist/src/flow/step-input-builder.js.map +1 -0
- package/dist/src/flows/scrape.flow.d.ts +6 -0
- package/dist/src/flows/scrape.flow.js +31 -0
- package/dist/src/flows/scrape.flow.js.map +1 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +50 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logging/logger.d.ts +17 -0
- package/dist/src/logging/logger.js +55 -0
- package/dist/src/logging/logger.js.map +1 -0
- package/dist/src/logging/logger.models.d.ts +25 -0
- package/dist/src/logging/logger.models.js +7 -0
- package/dist/src/logging/logger.models.js.map +1 -0
- package/dist/src/mail/mail.models.d.ts +15 -0
- package/dist/src/mail/mail.models.js +2 -0
- package/dist/src/mail/mail.models.js.map +1 -0
- package/dist/src/mail/smtp-mailer.d.ts +13 -0
- package/dist/src/mail/smtp-mailer.js +70 -0
- package/dist/src/mail/smtp-mailer.js.map +1 -0
- package/dist/src/mail/unconfigured-mailer.d.ts +6 -0
- package/dist/src/mail/unconfigured-mailer.js +20 -0
- package/dist/src/mail/unconfigured-mailer.js.map +1 -0
- package/dist/src/models.d.ts +11 -0
- package/dist/src/models.js +10 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/monitor/monitor-api.d.ts +18 -0
- package/dist/src/monitor/monitor-api.js +62 -0
- package/dist/src/monitor/monitor-api.js.map +1 -0
- package/dist/src/monitor/monitor-server.d.ts +33 -0
- package/dist/src/monitor/monitor-server.js +142 -0
- package/dist/src/monitor/monitor-server.js.map +1 -0
- package/dist/src/monitor/monitor.models.d.ts +27 -0
- package/dist/src/monitor/monitor.models.js +2 -0
- package/dist/src/monitor/monitor.models.js.map +1 -0
- package/dist/src/queue/pg-boss-flow-queue.d.ts +22 -0
- package/dist/src/queue/pg-boss-flow-queue.js +102 -0
- package/dist/src/queue/pg-boss-flow-queue.js.map +1 -0
- package/dist/src/queue/queue.models.d.ts +38 -0
- package/dist/src/queue/queue.models.js +2 -0
- package/dist/src/queue/queue.models.js.map +1 -0
- package/dist/src/reporting/lighty-error-reporter.d.ts +24 -0
- package/dist/src/reporting/lighty-error-reporter.js +96 -0
- package/dist/src/reporting/lighty-error-reporter.js.map +1 -0
- package/dist/src/reporting/log-error-reporter.d.ts +12 -0
- package/dist/src/reporting/log-error-reporter.js +35 -0
- package/dist/src/reporting/log-error-reporter.js.map +1 -0
- package/dist/src/reporting/reporting.models.d.ts +27 -0
- package/dist/src/reporting/reporting.models.js +2 -0
- package/dist/src/reporting/reporting.models.js.map +1 -0
- package/dist/src/scrapme.d.ts +61 -0
- package/dist/src/scrapme.js +133 -0
- package/dist/src/scrapme.js.map +1 -0
- package/dist/src/scrapme.models.d.ts +18 -0
- package/dist/src/scrapme.models.js +2 -0
- package/dist/src/scrapme.models.js.map +1 -0
- package/dist/src/storage/couch-connection.d.ts +19 -0
- package/dist/src/storage/couch-connection.js +67 -0
- package/dist/src/storage/couch-connection.js.map +1 -0
- package/dist/src/storage/couch-database.d.ts +44 -0
- package/dist/src/storage/couch-database.js +118 -0
- package/dist/src/storage/couch-database.js.map +1 -0
- package/dist/src/storage/couch-document-store.d.ts +14 -0
- package/dist/src/storage/couch-document-store.js +36 -0
- package/dist/src/storage/couch-document-store.js.map +1 -0
- package/dist/src/storage/couch-error.d.ts +12 -0
- package/dist/src/storage/couch-error.js +19 -0
- package/dist/src/storage/couch-error.js.map +1 -0
- package/dist/src/storage/couch-flow-run-store.d.ts +24 -0
- package/dist/src/storage/couch-flow-run-store.js +66 -0
- package/dist/src/storage/couch-flow-run-store.js.map +1 -0
- package/dist/src/storage/couch-run-store.d.ts +15 -0
- package/dist/src/storage/couch-run-store.js +52 -0
- package/dist/src/storage/couch-run-store.js.map +1 -0
- package/dist/src/storage/run-doc-ids.d.ts +9 -0
- package/dist/src/storage/run-doc-ids.js +15 -0
- package/dist/src/storage/run-doc-ids.js.map +1 -0
- package/dist/src/storage/storage.models.d.ts +95 -0
- package/dist/src/storage/storage.models.js +5 -0
- package/dist/src/storage/storage.models.js.map +1 -0
- package/dist/src/storage/unconfigured-document-store.d.ts +8 -0
- package/dist/src/storage/unconfigured-document-store.js +26 -0
- package/dist/src/storage/unconfigured-document-store.js.map +1 -0
- package/dist/src/util/preview.d.ts +10 -0
- package/dist/src/util/preview.js +44 -0
- package/dist/src/util/preview.js.map +1 -0
- package/dist/src/util/text.d.ts +8 -0
- package/dist/src/util/text.js +16 -0
- package/dist/src/util/text.js.map +1 -0
- package/dist/src/util/timing.d.ts +10 -0
- package/dist/src/util/timing.js +59 -0
- package/dist/src/util/timing.js.map +1 -0
- package/dist/src/util/url.d.ts +5 -0
- package/dist/src/util/url.js +11 -0
- package/dist/src/util/url.js.map +1 -0
- package/package.json +75 -4
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { inject, injectable } from 'inversify';
|
|
11
|
+
import { TYPES } from '../di/types.js';
|
|
12
|
+
import { Timing } from '../util/timing.js';
|
|
13
|
+
import { CommandTimeoutError, FlowCancelledError, FlowErrors } from './flow-errors.js';
|
|
14
|
+
import { ATTEMPT_OUTCOME, } from './flow.models.js';
|
|
15
|
+
import { RetryPolicy } from './retry-policy.js';
|
|
16
|
+
/**
|
|
17
|
+
* Runs one step inside a flow attempt: attempts with a timeout each, retried with backoff while the error is
|
|
18
|
+
* retryable. Never throws. (The whole flow is retried by pg-boss; this is the per-command retry inside it.)
|
|
19
|
+
*/
|
|
20
|
+
let CommandExecutor = class CommandExecutor {
|
|
21
|
+
config;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
/** step settings > command.options > defaults (Scrapme.create({ defaults })). */
|
|
26
|
+
policy(step, command) {
|
|
27
|
+
const d = this.config.flow;
|
|
28
|
+
const c = command.options ?? {};
|
|
29
|
+
const s = step.options;
|
|
30
|
+
return {
|
|
31
|
+
maxAttempts: 1 + (s.retries ?? c.retries ?? d.retries),
|
|
32
|
+
timeoutMs: s.timeoutMs ?? c.timeoutMs ?? d.timeoutMs,
|
|
33
|
+
retryDelayMs: s.retryDelayMs ?? c.retryDelayMs ?? d.retryDelayMs,
|
|
34
|
+
maxRetryDelayMs: s.maxRetryDelayMs ?? c.maxRetryDelayMs ?? d.maxRetryDelayMs,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async execute(step, command, input, base, runSignal, listeners) {
|
|
38
|
+
const policy = this.policy(step, command);
|
|
39
|
+
const retry = new RetryPolicy(policy);
|
|
40
|
+
const attempts = [];
|
|
41
|
+
const started = Date.now();
|
|
42
|
+
for (let attempt = 1;; attempt++) {
|
|
43
|
+
const startedAt = new Date();
|
|
44
|
+
await listeners.started?.(attempt);
|
|
45
|
+
try {
|
|
46
|
+
if (runSignal?.aborted)
|
|
47
|
+
throw new FlowCancelledError(runSignal.reason);
|
|
48
|
+
const value = await Timing.withTimeout((signal) => command.execute(input, { ...base, attempt, maxAttempts: policy.maxAttempts, signal, logger: base.logger.child({ attempt }) }), policy.timeoutMs, () => new CommandTimeoutError(step.key, policy.timeoutMs), runSignal);
|
|
49
|
+
attempts.push(this.report(attempt, base.flowAttempt, startedAt, ATTEMPT_OUTCOME.SUCCEEDED));
|
|
50
|
+
return { ok: true, value, attempts, durationMs: Date.now() - started };
|
|
51
|
+
}
|
|
52
|
+
catch (caught) {
|
|
53
|
+
const error = runSignal?.aborted && !(caught instanceof FlowCancelledError) ? new FlowCancelledError(runSignal.reason) : caught;
|
|
54
|
+
const final = !this.shouldRetry(command, error) || attempt >= policy.maxAttempts;
|
|
55
|
+
const report = {
|
|
56
|
+
...this.report(attempt, base.flowAttempt, startedAt, error instanceof CommandTimeoutError ? ATTEMPT_OUTCOME.TIMEOUT : ATTEMPT_OUTCOME.FAILED),
|
|
57
|
+
error: FlowErrors.serialize(error),
|
|
58
|
+
...(final ? {} : { retryInMs: retry.delayAfter(attempt) }),
|
|
59
|
+
};
|
|
60
|
+
attempts.push(report);
|
|
61
|
+
await listeners.failed(report, error, final);
|
|
62
|
+
if (final)
|
|
63
|
+
return { ok: false, error, attempts, durationMs: Date.now() - started };
|
|
64
|
+
try {
|
|
65
|
+
await Timing.sleep(report.retryInMs ?? 0, runSignal);
|
|
66
|
+
}
|
|
67
|
+
catch (reason) {
|
|
68
|
+
return { ok: false, error: new FlowCancelledError(reason), attempts, durationMs: Date.now() - started };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
report(attempt, flowAttempt, startedAt, outcome) {
|
|
74
|
+
return { attempt, flowAttempt, startedAt: startedAt.toISOString(), durationMs: Date.now() - startedAt.getTime(), outcome };
|
|
75
|
+
}
|
|
76
|
+
shouldRetry(command, error) {
|
|
77
|
+
if (error instanceof FlowCancelledError)
|
|
78
|
+
return false;
|
|
79
|
+
return command.isRetryable ? command.isRetryable(error) : FlowErrors.isRetryable(error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
CommandExecutor = __decorate([
|
|
83
|
+
injectable(),
|
|
84
|
+
__param(0, inject(TYPES.Config))
|
|
85
|
+
], CommandExecutor);
|
|
86
|
+
export { CommandExecutor };
|
|
87
|
+
//# sourceMappingURL=command-executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-executor.js","sourceRoot":"","sources":["../../../src/flow/command-executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Container } from 'inversify';
|
|
2
|
+
import type { Command, CommandRef } from './flow.models.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turns a step's command into an instance: classes come from scrapme's container (autobind; its parent is your
|
|
5
|
+
* app's container, so a command's `@inject(...)` gets your services), instances are used as they are.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CommandResolver {
|
|
8
|
+
private readonly container;
|
|
9
|
+
constructor(container: Container);
|
|
10
|
+
resolve(ref: CommandRef): Command;
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { inject, injectable } from 'inversify';
|
|
11
|
+
import { TYPES } from '../di/types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Turns a step's command into an instance: classes come from scrapme's container (autobind; its parent is your
|
|
14
|
+
* app's container, so a command's `@inject(...)` gets your services), instances are used as they are.
|
|
15
|
+
*/
|
|
16
|
+
let CommandResolver = class CommandResolver {
|
|
17
|
+
container;
|
|
18
|
+
constructor(container) {
|
|
19
|
+
this.container = container;
|
|
20
|
+
}
|
|
21
|
+
resolve(ref) {
|
|
22
|
+
if (typeof ref !== 'function')
|
|
23
|
+
return ref;
|
|
24
|
+
const command = this.container.get(ref);
|
|
25
|
+
if (!command || typeof command.execute !== 'function')
|
|
26
|
+
throw new TypeError(`${ref.name} has no execute method`);
|
|
27
|
+
return command;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
CommandResolver = __decorate([
|
|
31
|
+
injectable(),
|
|
32
|
+
__param(0, inject(TYPES.Container))
|
|
33
|
+
], CommandResolver);
|
|
34
|
+
export { CommandResolver };
|
|
35
|
+
//# sourceMappingURL=command-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-resolver.js","sourceRoot":"","sources":["../../../src/flow/command-resolver.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAkB,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC;;;GAGG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACmC;IAA7D,YAA6D,SAAoB;QAApB,cAAS,GAAT,SAAS,CAAW;IAAG,CAAC;IAE9E,OAAO,CAAC,GAAe;QAC5B,IAAI,OAAO,GAAG,KAAK,UAAU;YAAE,OAAO,GAAG,CAAC;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,GAAmC,CAAC,CAAC;QACjF,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU;YAAE,MAAM,IAAI,SAAS,CAAC,GAAG,GAAG,CAAC,IAAI,wBAAwB,CAAC,CAAC;QAChH,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAA;AATY,eAAe;IAD3B,UAAU,EAAE;IAES,WAAA,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;GADhC,eAAe,CAS3B"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Flow } from './flow.js';
|
|
2
|
+
import type { FlowConfig } from './flow.models.js';
|
|
3
|
+
/** The flows this app runs, by name — a queued job only carries the name. Names must be unique. */
|
|
4
|
+
export declare class FlowCatalog {
|
|
5
|
+
private readonly flows;
|
|
6
|
+
constructor(flows: readonly (Flow | FlowConfig)[]);
|
|
7
|
+
has(name: string): boolean;
|
|
8
|
+
/** Throws FlowDefinitionError for a flow that wasn't given to Scrapme.create. */
|
|
9
|
+
get(name: string): Flow;
|
|
10
|
+
names(): string[];
|
|
11
|
+
all(): Flow[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FlowDefinitionError } from './flow-errors.js';
|
|
2
|
+
import { Flow } from './flow.js';
|
|
3
|
+
/** The flows this app runs, by name — a queued job only carries the name. Names must be unique. */
|
|
4
|
+
export class FlowCatalog {
|
|
5
|
+
flows = new Map();
|
|
6
|
+
constructor(flows) {
|
|
7
|
+
for (const given of flows) {
|
|
8
|
+
const flow = given instanceof Flow ? given : new Flow(given);
|
|
9
|
+
if (this.flows.has(flow.name))
|
|
10
|
+
throw new FlowDefinitionError('two flows have this name (names must be unique)', flow.name);
|
|
11
|
+
this.flows.set(flow.name, flow);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
has(name) {
|
|
15
|
+
return this.flows.has(name);
|
|
16
|
+
}
|
|
17
|
+
/** Throws FlowDefinitionError for a flow that wasn't given to Scrapme.create. */
|
|
18
|
+
get(name) {
|
|
19
|
+
const flow = this.flows.get(name);
|
|
20
|
+
if (!flow)
|
|
21
|
+
throw new FlowDefinitionError(`not registered - add it to Scrapme.create({ flows: [...] }) (registered: ${this.names().join(', ') || 'none'})`, name);
|
|
22
|
+
return flow;
|
|
23
|
+
}
|
|
24
|
+
names() {
|
|
25
|
+
return [...this.flows.keys()].sort();
|
|
26
|
+
}
|
|
27
|
+
all() {
|
|
28
|
+
return [...this.flows.values()];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=flow-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-catalog.js","sourceRoot":"","sources":["../../../src/flow/flow-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,mGAAmG;AACnG,MAAM,OAAO,WAAW;IACL,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAC;IAEjD,YAAmB,KAAqC;QACtD,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;YAC7D,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM,IAAI,mBAAmB,CAAC,iDAAiD,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3H,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAEM,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,iFAAiF;IAC1E,GAAG,CAAC,IAAY;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,mBAAmB,CAAC,4EAA4E,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,EAAE,IAAI,CAAC,CAAC;QACjK,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,KAAK;QACV,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,CAAC;IAEM,GAAG;QACR,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type SerializedError } from './flow.models.js';
|
|
2
|
+
/** An error retrying cannot fix (bad input, page without the expected data, rejected e-mail address…). */
|
|
3
|
+
export declare class PermanentError extends Error {
|
|
4
|
+
readonly retryable = false;
|
|
5
|
+
readonly code: string;
|
|
6
|
+
constructor(message: string, options?: {
|
|
7
|
+
code?: string;
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/** A command needs a service that wasn't given to Scrapme.create — fix the options, a retry won't help. */
|
|
12
|
+
export declare class NotConfiguredError extends PermanentError {
|
|
13
|
+
constructor(service: string, neededBy: string);
|
|
14
|
+
}
|
|
15
|
+
/** One attempt took longer than its timeoutMs (retryable). */
|
|
16
|
+
export declare class CommandTimeoutError extends Error {
|
|
17
|
+
readonly step: string;
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
readonly retryable = true;
|
|
20
|
+
readonly code: "COMMAND_TIMEOUT";
|
|
21
|
+
constructor(step: string, timeoutMs: number);
|
|
22
|
+
}
|
|
23
|
+
/** The run was cancelled through RunOptions.signal (never retried). */
|
|
24
|
+
export declare class FlowCancelledError extends Error {
|
|
25
|
+
readonly retryable = false;
|
|
26
|
+
readonly code: "FLOW_CANCELLED";
|
|
27
|
+
constructor(reason?: unknown);
|
|
28
|
+
private static describe;
|
|
29
|
+
}
|
|
30
|
+
/** A run that failed for good; `cause` is the step's last error (for apps that want to throw after waitFor). */
|
|
31
|
+
export declare class FlowFailedError extends Error {
|
|
32
|
+
readonly flow: string;
|
|
33
|
+
readonly step: string;
|
|
34
|
+
readonly runId: string;
|
|
35
|
+
readonly code: "FLOW_FAILED";
|
|
36
|
+
constructor(flow: string, step: string, runId: string, cause: unknown);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A flow can't be used as written: wrong shape, bad reference, duplicate key, not a command. Thrown by
|
|
40
|
+
* new Flow (so at import time of the `*.flow.ts` file) or Scrapme.create — never in the middle of a run.
|
|
41
|
+
*/
|
|
42
|
+
export declare class FlowDefinitionError extends Error {
|
|
43
|
+
readonly flow: string;
|
|
44
|
+
readonly retryable = false;
|
|
45
|
+
readonly code: "FLOW_DEFINITION";
|
|
46
|
+
constructor(message: string, flow: string);
|
|
47
|
+
}
|
|
48
|
+
/** The run input doesn't fit the flow's `<param>`s (missing required, wrong type). Thrown before the run starts. */
|
|
49
|
+
export declare class FlowInputError extends Error {
|
|
50
|
+
readonly flow: string;
|
|
51
|
+
readonly problems: readonly string[];
|
|
52
|
+
readonly retryable = false;
|
|
53
|
+
readonly code: "FLOW_INPUT";
|
|
54
|
+
constructor(flow: string, problems: readonly string[]);
|
|
55
|
+
}
|
|
56
|
+
/** What scrapme needs to know about any error: retry it? how to store it? */
|
|
57
|
+
export declare class FlowErrors {
|
|
58
|
+
private static readonly MAX_CAUSE_DEPTH;
|
|
59
|
+
/**
|
|
60
|
+
* Default retry decision: PermanentError and anything with `retryable === false` (e.g. client errors of
|
|
61
|
+
* scrap-anything-client) are final, cancellations are final, everything else is retried.
|
|
62
|
+
*/
|
|
63
|
+
static isRetryable(error: unknown): boolean;
|
|
64
|
+
/** JSON-safe form for storage and reports (follows `cause` 3 levels deep). */
|
|
65
|
+
static serialize(error: unknown, depth?: number): SerializedError;
|
|
66
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ERROR_CODE } from './flow.models.js';
|
|
2
|
+
/** An error retrying cannot fix (bad input, page without the expected data, rejected e-mail address…). */
|
|
3
|
+
export class PermanentError extends Error {
|
|
4
|
+
retryable = false;
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, options = {}) {
|
|
7
|
+
super(message, { cause: options.cause });
|
|
8
|
+
this.name = 'PermanentError';
|
|
9
|
+
this.code = options.code ?? ERROR_CODE.PERMANENT;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** A command needs a service that wasn't given to Scrapme.create — fix the options, a retry won't help. */
|
|
13
|
+
export class NotConfiguredError extends PermanentError {
|
|
14
|
+
constructor(service, neededBy) {
|
|
15
|
+
super(`${neededBy} needs "${service}" in Scrapme.create({ ${service}: { … } })`, { code: ERROR_CODE.NOT_CONFIGURED });
|
|
16
|
+
this.name = 'NotConfiguredError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** One attempt took longer than its timeoutMs (retryable). */
|
|
20
|
+
export class CommandTimeoutError extends Error {
|
|
21
|
+
step;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
retryable = true;
|
|
24
|
+
code = ERROR_CODE.COMMAND_TIMEOUT;
|
|
25
|
+
constructor(step, timeoutMs) {
|
|
26
|
+
super(`step "${step}" timed out after ${timeoutMs} ms`);
|
|
27
|
+
this.step = step;
|
|
28
|
+
this.timeoutMs = timeoutMs;
|
|
29
|
+
this.name = 'CommandTimeoutError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The run was cancelled through RunOptions.signal (never retried). */
|
|
33
|
+
export class FlowCancelledError extends Error {
|
|
34
|
+
retryable = false;
|
|
35
|
+
code = ERROR_CODE.FLOW_CANCELLED;
|
|
36
|
+
constructor(reason) {
|
|
37
|
+
super(`flow cancelled${FlowCancelledError.describe(reason)}`, { cause: reason });
|
|
38
|
+
this.name = 'FlowCancelledError';
|
|
39
|
+
}
|
|
40
|
+
static describe(reason) {
|
|
41
|
+
if (reason instanceof Error)
|
|
42
|
+
return `: ${reason.message}`;
|
|
43
|
+
return reason ? `: ${String(reason)}` : '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** A run that failed for good; `cause` is the step's last error (for apps that want to throw after waitFor). */
|
|
47
|
+
export class FlowFailedError extends Error {
|
|
48
|
+
flow;
|
|
49
|
+
step;
|
|
50
|
+
runId;
|
|
51
|
+
code = ERROR_CODE.FLOW_FAILED;
|
|
52
|
+
constructor(flow, step, runId, cause) {
|
|
53
|
+
super(`flow "${flow}" failed at step "${step}" (run ${runId}): ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
54
|
+
this.flow = flow;
|
|
55
|
+
this.step = step;
|
|
56
|
+
this.runId = runId;
|
|
57
|
+
this.name = 'FlowFailedError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A flow can't be used as written: wrong shape, bad reference, duplicate key, not a command. Thrown by
|
|
62
|
+
* new Flow (so at import time of the `*.flow.ts` file) or Scrapme.create — never in the middle of a run.
|
|
63
|
+
*/
|
|
64
|
+
export class FlowDefinitionError extends Error {
|
|
65
|
+
flow;
|
|
66
|
+
retryable = false;
|
|
67
|
+
code = ERROR_CODE.FLOW_DEFINITION;
|
|
68
|
+
constructor(message, flow) {
|
|
69
|
+
super(`flow "${flow}": ${message}`);
|
|
70
|
+
this.flow = flow;
|
|
71
|
+
this.name = 'FlowDefinitionError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** The run input doesn't fit the flow's `<param>`s (missing required, wrong type). Thrown before the run starts. */
|
|
75
|
+
export class FlowInputError extends Error {
|
|
76
|
+
flow;
|
|
77
|
+
problems;
|
|
78
|
+
retryable = false;
|
|
79
|
+
code = ERROR_CODE.FLOW_INPUT;
|
|
80
|
+
constructor(flow, problems) {
|
|
81
|
+
super(`flow "${flow}": ${problems.join('; ')}`);
|
|
82
|
+
this.flow = flow;
|
|
83
|
+
this.problems = problems;
|
|
84
|
+
this.name = 'FlowInputError';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** What scrapme needs to know about any error: retry it? how to store it? */
|
|
88
|
+
export class FlowErrors {
|
|
89
|
+
static MAX_CAUSE_DEPTH = 3;
|
|
90
|
+
/**
|
|
91
|
+
* Default retry decision: PermanentError and anything with `retryable === false` (e.g. client errors of
|
|
92
|
+
* scrap-anything-client) are final, cancellations are final, everything else is retried.
|
|
93
|
+
*/
|
|
94
|
+
static isRetryable(error) {
|
|
95
|
+
if (error instanceof FlowCancelledError || error instanceof PermanentError)
|
|
96
|
+
return false;
|
|
97
|
+
if (error && typeof error === 'object' && 'retryable' in error && typeof error.retryable === 'boolean')
|
|
98
|
+
return error.retryable;
|
|
99
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
100
|
+
return false;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/** JSON-safe form for storage and reports (follows `cause` 3 levels deep). */
|
|
104
|
+
static serialize(error, depth = 0) {
|
|
105
|
+
if (!(error instanceof Error)) {
|
|
106
|
+
return { name: 'NonError', message: typeof error === 'string' ? error : (JSON.stringify(error) ?? String(error)), retryable: true };
|
|
107
|
+
}
|
|
108
|
+
const code = error.code;
|
|
109
|
+
return {
|
|
110
|
+
name: error.name,
|
|
111
|
+
message: error.message,
|
|
112
|
+
...(typeof code === 'string' || typeof code === 'number' ? { code: String(code) } : {}),
|
|
113
|
+
retryable: FlowErrors.isRetryable(error),
|
|
114
|
+
...(error.stack ? { stack: error.stack } : {}),
|
|
115
|
+
...(error.cause !== undefined && depth < FlowErrors.MAX_CAUSE_DEPTH ? { cause: FlowErrors.serialize(error.cause, depth + 1) } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=flow-errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-errors.js","sourceRoot":"","sources":["../../../src/flow/flow-errors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ScrapmeConfig } from '../config/config.models.js';
|
|
2
|
+
import type { Logger } from '../logging/logger.js';
|
|
3
|
+
import type { FlowJob } from '../queue/queue.models.js';
|
|
4
|
+
import type { ErrorReporter } from '../reporting/reporting.models.js';
|
|
5
|
+
import type { RunStore } from '../storage/storage.models.js';
|
|
6
|
+
import type { CommandExecutor } from './command-executor.js';
|
|
7
|
+
import type { CommandResolver } from './command-resolver.js';
|
|
8
|
+
import type { Flow } from './flow.js';
|
|
9
|
+
import type { StepInputBuilder } from './step-input-builder.js';
|
|
10
|
+
/**
|
|
11
|
+
* Runs one attempt of a flow job: every step from the first one, each command with its own retries and timeout
|
|
12
|
+
* (CommandExecutor). A command that fails for good fails the attempt:
|
|
13
|
+
* - retryable error, flow attempts left → run "retrying", throw → pg-boss runs the whole flow again later;
|
|
14
|
+
* - last flow attempt, or a permanent error (PermanentError, retryable: false) → run "failed", reported once.
|
|
15
|
+
* Every change is written to the run document right away — that is what the monitor shows.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FlowJobProcessor {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly store;
|
|
20
|
+
private readonly resolver;
|
|
21
|
+
private readonly executor;
|
|
22
|
+
private readonly inputs;
|
|
23
|
+
private readonly reporter;
|
|
24
|
+
private readonly logger;
|
|
25
|
+
constructor(config: ScrapmeConfig, logger: Logger, store: RunStore, resolver: CommandResolver, executor: CommandExecutor, inputs: StepInputBuilder, reporter: ErrorReporter);
|
|
26
|
+
process(flow: Flow, job: FlowJob): Promise<void>;
|
|
27
|
+
private runStep;
|
|
28
|
+
private stepFailed;
|
|
29
|
+
/** The run failed for good: record it and report it once. */
|
|
30
|
+
private finishFailed;
|
|
31
|
+
private static endAttempt;
|
|
32
|
+
private static stepOf;
|
|
33
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
var FlowJobProcessor_1;
|
|
11
|
+
import { inject, injectable } from 'inversify';
|
|
12
|
+
import { TYPES } from '../di/types.js';
|
|
13
|
+
import { LINE_LEVEL } from '../logging/logger.models.js';
|
|
14
|
+
import { Preview } from '../util/preview.js';
|
|
15
|
+
import { FlowErrors } from './flow-errors.js';
|
|
16
|
+
import { ATTEMPT_OUTCOME, FINAL_RUN_STATUSES, RUN_STATUS, STEP_STATUS } from './flow.models.js';
|
|
17
|
+
/**
|
|
18
|
+
* Runs one attempt of a flow job: every step from the first one, each command with its own retries and timeout
|
|
19
|
+
* (CommandExecutor). A command that fails for good fails the attempt:
|
|
20
|
+
* - retryable error, flow attempts left → run "retrying", throw → pg-boss runs the whole flow again later;
|
|
21
|
+
* - last flow attempt, or a permanent error (PermanentError, retryable: false) → run "failed", reported once.
|
|
22
|
+
* Every change is written to the run document right away — that is what the monitor shows.
|
|
23
|
+
*/
|
|
24
|
+
let FlowJobProcessor = FlowJobProcessor_1 = class FlowJobProcessor {
|
|
25
|
+
config;
|
|
26
|
+
store;
|
|
27
|
+
resolver;
|
|
28
|
+
executor;
|
|
29
|
+
inputs;
|
|
30
|
+
reporter;
|
|
31
|
+
logger;
|
|
32
|
+
constructor(config, logger, store, resolver, executor, inputs, reporter) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.store = store;
|
|
35
|
+
this.resolver = resolver;
|
|
36
|
+
this.executor = executor;
|
|
37
|
+
this.inputs = inputs;
|
|
38
|
+
this.reporter = reporter;
|
|
39
|
+
this.logger = logger.child({ component: 'flow' });
|
|
40
|
+
}
|
|
41
|
+
async process(flow, job) {
|
|
42
|
+
const { runId } = job.data;
|
|
43
|
+
const run = await this.store.get(runId);
|
|
44
|
+
if (!run) {
|
|
45
|
+
this.logger.error('flow job without a run document - dropped', { runId, jobId: job.id });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (FINAL_RUN_STATUSES.includes(run.status)) {
|
|
49
|
+
this.logger.warn('run is finished already - job ignored', { runId, status: run.status });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const flowAttempt = job.retryCount + 1;
|
|
53
|
+
const maxFlowAttempts = job.retryLimit + 1;
|
|
54
|
+
const log = this.logger.child({ flow: flow.name, runId, flowAttempt });
|
|
55
|
+
const startedAt = new Date().toISOString();
|
|
56
|
+
await this.store.update(runId, (d) => {
|
|
57
|
+
d.status = RUN_STATUS.RUNNING;
|
|
58
|
+
d.maxFlowAttempts = maxFlowAttempts;
|
|
59
|
+
d.startedAt ??= startedAt;
|
|
60
|
+
d.currentStep = null;
|
|
61
|
+
d.flowAttempts.push({ attempt: flowAttempt, jobId: job.id, startedAt });
|
|
62
|
+
d.steps = flow.steps.map((step) => ({ key: step.key, command: step.commandName, status: STEP_STATUS.PENDING, attempts: d.steps.find((s) => s.key === step.key)?.attempts ?? [] }));
|
|
63
|
+
});
|
|
64
|
+
log.info('flow started', { attempt: flowAttempt + '/' + maxFlowAttempts });
|
|
65
|
+
this.reporter.log('flow ' + flow.name + ' attempt ' + flowAttempt + '/' + maxFlowAttempts, { runId });
|
|
66
|
+
let state = { ...run.input };
|
|
67
|
+
for (const step of flow.steps) {
|
|
68
|
+
const outcome = await this.runStep(flow, step, runId, state, job, flowAttempt, log);
|
|
69
|
+
if (!outcome.ok) {
|
|
70
|
+
const permanent = !job.signal.aborted && !FlowErrors.isRetryable(outcome.error);
|
|
71
|
+
if (permanent || flowAttempt >= maxFlowAttempts) {
|
|
72
|
+
await this.finishFailed(run, job, step.key, outcome.error);
|
|
73
|
+
if (permanent)
|
|
74
|
+
return; // the job completes: running it again can't help
|
|
75
|
+
throw outcome.error; // pg-boss marks the job failed (no attempts left)
|
|
76
|
+
}
|
|
77
|
+
await this.store.update(runId, (d) => {
|
|
78
|
+
d.status = RUN_STATUS.RETRYING;
|
|
79
|
+
FlowJobProcessor_1.endAttempt(d, ATTEMPT_OUTCOME.FAILED, outcome.error);
|
|
80
|
+
});
|
|
81
|
+
log.warn('flow attempt failed, pg-boss will run the flow again', { step: step.key, error: outcome.error });
|
|
82
|
+
throw outcome.error;
|
|
83
|
+
}
|
|
84
|
+
state = { ...state, [step.key]: outcome.value };
|
|
85
|
+
}
|
|
86
|
+
const finished = new Date();
|
|
87
|
+
await this.store.update(runId, (d) => {
|
|
88
|
+
d.status = RUN_STATUS.SUCCEEDED;
|
|
89
|
+
d.currentStep = null;
|
|
90
|
+
d.finishedAt = finished.toISOString();
|
|
91
|
+
d.durationMs = finished.getTime() - Date.parse(d.startedAt ?? d.createdAt);
|
|
92
|
+
delete d.error;
|
|
93
|
+
FlowJobProcessor_1.endAttempt(d, ATTEMPT_OUTCOME.SUCCEEDED);
|
|
94
|
+
});
|
|
95
|
+
log.info('flow succeeded');
|
|
96
|
+
}
|
|
97
|
+
async runStep(flow, step, runId, state, job, flowAttempt, log) {
|
|
98
|
+
const stepLog = log.child({ step: step.key, command: step.commandName });
|
|
99
|
+
let command;
|
|
100
|
+
let input;
|
|
101
|
+
try {
|
|
102
|
+
command = this.resolver.resolve(step.command);
|
|
103
|
+
input = this.inputs.build(step.input, state, step.key);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// a missing value, a throwing field function, an unresolvable command: the same state gives the same error
|
|
107
|
+
stepLog.error('step failed before it ran', { error });
|
|
108
|
+
const report = { attempt: 1, flowAttempt, startedAt: new Date().toISOString(), durationMs: 0, outcome: ATTEMPT_OUTCOME.FAILED, error: FlowErrors.serialize(error) };
|
|
109
|
+
await this.stepFailed(runId, step, report, error, true);
|
|
110
|
+
return { ok: false, error };
|
|
111
|
+
}
|
|
112
|
+
const maxAttempts = this.executor.policy(step, command).maxAttempts;
|
|
113
|
+
const execution = await this.executor.execute(step, command, input, { runId, flow: flow.name, step: step.key, flowAttempt, logger: stepLog, state: Object.freeze({ ...state }) }, job.signal, {
|
|
114
|
+
started: async (attempt) => {
|
|
115
|
+
stepLog.info(attempt === 1 ? 'step started' : 'step attempt started', { attempt, maxAttempts });
|
|
116
|
+
await this.store.update(runId, (d) => {
|
|
117
|
+
const s = FlowJobProcessor_1.stepOf(d, step);
|
|
118
|
+
s.status = STEP_STATUS.RUNNING;
|
|
119
|
+
s.startedAt ??= new Date().toISOString();
|
|
120
|
+
d.currentStep = step.key;
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
failed: async (report, error, final) => {
|
|
124
|
+
stepLog[final ? LINE_LEVEL.ERROR : LINE_LEVEL.WARN](final ? 'step failed' : 'step attempt failed, retrying in ' + report.retryInMs + ' ms', { attempt: report.attempt, maxAttempts, error });
|
|
125
|
+
this.reporter.log('step ' + step.key + ' attempt ' + report.attempt + '/' + maxAttempts + ' failed', { error: report.error?.message }, final ? LINE_LEVEL.ERROR : LINE_LEVEL.WARN);
|
|
126
|
+
await this.stepFailed(runId, step, report, error, final);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (!execution.ok)
|
|
130
|
+
return { ok: false, error: execution.error };
|
|
131
|
+
const finished = new Date();
|
|
132
|
+
await this.store.update(runId, (d) => {
|
|
133
|
+
const s = FlowJobProcessor_1.stepOf(d, step);
|
|
134
|
+
s.status = STEP_STATUS.SUCCEEDED;
|
|
135
|
+
s.attempts.push(execution.attempts.at(-1));
|
|
136
|
+
s.finishedAt = finished.toISOString();
|
|
137
|
+
s.durationMs = finished.getTime() - Date.parse(s.startedAt ?? finished.toISOString());
|
|
138
|
+
s.output = Preview.of(execution.value, this.config.flow.maxStoredChars);
|
|
139
|
+
});
|
|
140
|
+
stepLog.info('step succeeded', { attempts: execution.attempts.length, ms: execution.durationMs });
|
|
141
|
+
return { ok: true, value: execution.value };
|
|
142
|
+
}
|
|
143
|
+
async stepFailed(runId, step, report, error, final) {
|
|
144
|
+
await this.store.update(runId, (d) => {
|
|
145
|
+
const s = FlowJobProcessor_1.stepOf(d, step);
|
|
146
|
+
s.attempts.push(report);
|
|
147
|
+
if (final) {
|
|
148
|
+
s.status = STEP_STATUS.FAILED;
|
|
149
|
+
s.error = report.error ?? FlowErrors.serialize(error);
|
|
150
|
+
s.finishedAt = new Date().toISOString();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** The run failed for good: record it and report it once. */
|
|
155
|
+
async finishFailed(run, job, stepKey, error) {
|
|
156
|
+
const eventId = this.reporter.report(error, {
|
|
157
|
+
flow: run.flow,
|
|
158
|
+
step: stepKey,
|
|
159
|
+
command: run.steps.find((s) => s.key === stepKey)?.command ?? '',
|
|
160
|
+
runId: run.runId,
|
|
161
|
+
attempt: job.retryCount + 1,
|
|
162
|
+
maxAttempts: job.retryLimit + 1,
|
|
163
|
+
tags: { ...run.tags },
|
|
164
|
+
extra: { flowAttempt: job.retryCount + 1 },
|
|
165
|
+
});
|
|
166
|
+
const finished = new Date();
|
|
167
|
+
await this.store.update(run.runId, (d) => {
|
|
168
|
+
d.status = RUN_STATUS.FAILED;
|
|
169
|
+
d.finishedAt = finished.toISOString();
|
|
170
|
+
d.durationMs = finished.getTime() - Date.parse(d.startedAt ?? d.createdAt);
|
|
171
|
+
d.error = { ...FlowErrors.serialize(error), step: stepKey };
|
|
172
|
+
if (eventId)
|
|
173
|
+
d.reportEventId = eventId;
|
|
174
|
+
FlowJobProcessor_1.endAttempt(d, ATTEMPT_OUTCOME.FAILED, error);
|
|
175
|
+
});
|
|
176
|
+
await this.reporter.flush();
|
|
177
|
+
this.logger.error('flow failed', { flow: run.flow, runId: run.runId, step: stepKey, eventId: eventId || undefined });
|
|
178
|
+
}
|
|
179
|
+
static endAttempt(d, outcome, error) {
|
|
180
|
+
const attempt = d.flowAttempts.at(-1);
|
|
181
|
+
if (!attempt || attempt.finishedAt)
|
|
182
|
+
return;
|
|
183
|
+
attempt.finishedAt = new Date().toISOString();
|
|
184
|
+
attempt.outcome = outcome;
|
|
185
|
+
if (error !== undefined)
|
|
186
|
+
attempt.error = FlowErrors.serialize(error);
|
|
187
|
+
}
|
|
188
|
+
static stepOf(d, step) {
|
|
189
|
+
let report = d.steps.find((s) => s.key === step.key);
|
|
190
|
+
if (!report) {
|
|
191
|
+
report = { key: step.key, command: step.commandName, status: STEP_STATUS.PENDING, attempts: [] };
|
|
192
|
+
d.steps.push(report);
|
|
193
|
+
}
|
|
194
|
+
return report;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
FlowJobProcessor = FlowJobProcessor_1 = __decorate([
|
|
198
|
+
injectable(),
|
|
199
|
+
__param(0, inject(TYPES.Config)),
|
|
200
|
+
__param(1, inject(TYPES.Logger)),
|
|
201
|
+
__param(2, inject(TYPES.RunStore)),
|
|
202
|
+
__param(3, inject(TYPES.CommandResolver)),
|
|
203
|
+
__param(4, inject(TYPES.CommandExecutor)),
|
|
204
|
+
__param(5, inject(TYPES.StepInputBuilder)),
|
|
205
|
+
__param(6, inject(TYPES.ErrorReporter))
|
|
206
|
+
], FlowJobProcessor);
|
|
207
|
+
export { FlowJobProcessor };
|
|
208
|
+
//# sourceMappingURL=flow-job-processor.js.map
|