@pgflow/edge-worker 0.0.5 → 0.0.7-prealpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +10 -4
  2. package/.envrc +0 -2
  3. package/CHANGELOG.md +0 -10
  4. package/deno.lock +0 -336
  5. package/deno.test.json +0 -32
  6. package/dist/LICENSE.md +0 -660
  7. package/dist/README.md +0 -46
  8. package/dist/index.js +0 -972
  9. package/dist/index.js.map +0 -7
  10. package/mod.ts +0 -7
  11. package/pkgs/edge-worker/dist/index.js +0 -953
  12. package/pkgs/edge-worker/dist/index.js.map +0 -7
  13. package/pkgs/edge-worker/dist/pkgs/edge-worker/LICENSE.md +0 -660
  14. package/pkgs/edge-worker/dist/pkgs/edge-worker/README.md +0 -46
  15. package/project.json +0 -164
  16. package/scripts/concatenate-migrations.sh +0 -22
  17. package/scripts/wait-for-localhost +0 -17
  18. package/sql/990_active_workers.sql +0 -11
  19. package/sql/991_inactive_workers.sql +0 -12
  20. package/sql/992_spawn_worker.sql +0 -68
  21. package/sql/benchmarks/max_concurrency.sql +0 -32
  22. package/sql/queries/debug_connections.sql +0 -0
  23. package/sql/queries/debug_processing_gaps.sql +0 -115
  24. package/src/EdgeWorker.ts +0 -172
  25. package/src/core/BatchProcessor.ts +0 -38
  26. package/src/core/ExecutionController.ts +0 -51
  27. package/src/core/Heartbeat.ts +0 -23
  28. package/src/core/Logger.ts +0 -42
  29. package/src/core/Queries.ts +0 -44
  30. package/src/core/Worker.ts +0 -102
  31. package/src/core/WorkerLifecycle.ts +0 -93
  32. package/src/core/WorkerState.ts +0 -85
  33. package/src/core/types.ts +0 -47
  34. package/src/flow/FlowWorkerLifecycle.ts +0 -81
  35. package/src/flow/StepTaskExecutor.ts +0 -87
  36. package/src/flow/StepTaskPoller.ts +0 -51
  37. package/src/flow/createFlowWorker.ts +0 -105
  38. package/src/flow/types.ts +0 -1
  39. package/src/index.ts +0 -15
  40. package/src/queue/MessageExecutor.ts +0 -105
  41. package/src/queue/Queue.ts +0 -92
  42. package/src/queue/ReadWithPollPoller.ts +0 -35
  43. package/src/queue/createQueueWorker.ts +0 -145
  44. package/src/queue/types.ts +0 -14
  45. package/src/spawnNewEdgeFunction.ts +0 -33
  46. package/supabase/call +0 -23
  47. package/supabase/cli +0 -3
  48. package/supabase/config.toml +0 -42
  49. package/supabase/functions/cpu_intensive/index.ts +0 -20
  50. package/supabase/functions/creating_queue/index.ts +0 -5
  51. package/supabase/functions/failing_always/index.ts +0 -13
  52. package/supabase/functions/increment_sequence/index.ts +0 -14
  53. package/supabase/functions/max_concurrency/index.ts +0 -17
  54. package/supabase/functions/serial_sleep/index.ts +0 -16
  55. package/supabase/functions/utils.ts +0 -13
  56. package/supabase/seed.sql +0 -2
  57. package/tests/db/compose.yaml +0 -20
  58. package/tests/db.ts +0 -71
  59. package/tests/e2e/README.md +0 -54
  60. package/tests/e2e/_helpers.ts +0 -135
  61. package/tests/e2e/performance.test.ts +0 -60
  62. package/tests/e2e/restarts.test.ts +0 -56
  63. package/tests/helpers.ts +0 -22
  64. package/tests/integration/_helpers.ts +0 -43
  65. package/tests/integration/creating_queue.test.ts +0 -32
  66. package/tests/integration/flow/minimalFlow.test.ts +0 -121
  67. package/tests/integration/maxConcurrent.test.ts +0 -76
  68. package/tests/integration/retries.test.ts +0 -78
  69. package/tests/integration/starting_worker.test.ts +0 -35
  70. package/tests/sql.ts +0 -46
  71. package/tests/unit/WorkerState.test.ts +0 -74
  72. package/tsconfig.lib.json +0 -23
@@ -1,38 +0,0 @@
1
- import type { ExecutionController } from './ExecutionController.ts';
2
- import type { IMessage, IPoller } from './types.ts';
3
- import { getLogger } from './Logger.ts';
4
-
5
- export class BatchProcessor<TMessage extends IMessage> {
6
- private logger = getLogger('BatchProcessor');
7
-
8
- constructor(
9
- private executionController: ExecutionController<TMessage>,
10
- private poller: IPoller<TMessage>,
11
- private signal: AbortSignal
12
- ) {
13
- this.executionController = executionController;
14
- this.signal = signal;
15
- this.poller = poller;
16
- }
17
-
18
- async processBatch() {
19
- this.logger.debug('Polling for new batch of messages...');
20
- const messageRecords = await this.poller.poll();
21
-
22
- if (this.signal.aborted) {
23
- this.logger.info('Discarding messageRecords because worker is stopping');
24
- return;
25
- }
26
-
27
- this.logger.debug(`Starting ${messageRecords.length} messages`);
28
-
29
- const startPromises = messageRecords.map(
30
- (message) => this.executionController.start(message)
31
- );
32
- await Promise.all(startPromises);
33
- }
34
-
35
- async awaitCompletion() {
36
- return await this.executionController.awaitCompletion();
37
- }
38
- }
@@ -1,51 +0,0 @@
1
- import { newQueue, type Queue as PromiseQueue } from '@henrygd/queue';
2
- import type { IExecutor, IMessage } from './types.ts';
3
- import { getLogger } from './Logger.ts';
4
-
5
- export interface ExecutionConfig {
6
- maxConcurrent: number;
7
- }
8
-
9
- export class ExecutionController<TMessage extends IMessage> {
10
- private logger = getLogger('ExecutionController');
11
- private promiseQueue: PromiseQueue;
12
- private signal: AbortSignal;
13
- private createExecutor: (record: TMessage, signal: AbortSignal) => IExecutor;
14
-
15
- constructor(
16
- executorFactory: (record: TMessage, signal: AbortSignal) => IExecutor,
17
- abortSignal: AbortSignal,
18
- config: ExecutionConfig
19
- ) {
20
- this.signal = abortSignal;
21
- this.createExecutor = executorFactory;
22
- this.promiseQueue = newQueue(config.maxConcurrent);
23
- }
24
-
25
- async start(record: TMessage) {
26
- const executor = this.createExecutor(record, this.signal);
27
-
28
- this.logger.info(`Scheduling execution of task ${executor.msgId}`);
29
-
30
- return await this.promiseQueue.add(async () => {
31
- try {
32
- this.logger.debug(`Executing task ${executor.msgId}...`);
33
- await executor.execute();
34
- this.logger.debug(`Execution successful for ${executor.msgId}`);
35
- } catch (error) {
36
- this.logger.error(`Execution failed for ${executor.msgId}:`, error);
37
- throw error;
38
- }
39
- });
40
- }
41
-
42
- async awaitCompletion() {
43
- const active = this.promiseQueue.active();
44
- const all = this.promiseQueue.size();
45
-
46
- this.logger.debug(
47
- `Awaiting completion of all tasks... (active/all: ${active}}/${all})`
48
- );
49
- await this.promiseQueue.done();
50
- }
51
- }
@@ -1,23 +0,0 @@
1
- import type { Queries } from './Queries.ts';
2
- import type { WorkerRow } from './types.ts';
3
- import { getLogger } from './Logger.ts';
4
-
5
- export class Heartbeat {
6
- private logger = getLogger('Heartbeat');
7
- private lastHeartbeat = 0;
8
-
9
- constructor(
10
- private interval: number,
11
- private queries: Queries,
12
- private workerRow: WorkerRow
13
- ) {}
14
-
15
- async send(): Promise<void> {
16
- const now = Date.now();
17
- if (now - this.lastHeartbeat >= this.interval) {
18
- await this.queries.sendHeartbeat(this.workerRow);
19
- this.logger.debug('OK');
20
- this.lastHeartbeat = now;
21
- }
22
- }
23
- }
@@ -1,42 +0,0 @@
1
- import pino from 'pino';
2
-
3
- function getLogLevelFromEnv(): pino.LevelWithSilent {
4
- const validLevels = [
5
- 'DEBUG',
6
- 'INFO',
7
- 'ERROR',
8
- ];
9
- const logLevel = Deno.env.get('EDGE_WORKER_LOG_LEVEL')?.toUpperCase();
10
-
11
- if (logLevel && !validLevels.includes(logLevel)) {
12
- console.warn(`Invalid log level "${logLevel}". Using "INFO" instead.`);
13
- return 'info';
14
- }
15
-
16
- return (logLevel?.toLowerCase() as pino.LevelWithSilent) || 'info';
17
- }
18
-
19
- export function setupLogger(workerId: string) {
20
- const level = getLogLevelFromEnv();
21
-
22
- const loggerOptions: pino.LoggerOptions = {
23
- level,
24
- formatters: {
25
- bindings: () => ({ worker_id: workerId }),
26
- },
27
- serializers: pino.stdSerializers,
28
- transport: {
29
- target: 'pino-pretty',
30
- options: {
31
- colorize: true,
32
- messageFormat: '[{module}] {msg}',
33
- }
34
- }
35
- };
36
-
37
- pino(loggerOptions);
38
- }
39
-
40
- export function getLogger(module: string) {
41
- return pino().child({ module });
42
- }
@@ -1,44 +0,0 @@
1
- import type postgres from 'postgres';
2
- import type { WorkerRow } from './types.ts';
3
-
4
- export class Queries {
5
- constructor(private readonly sql: postgres.Sql) {}
6
-
7
- async onWorkerStarted({
8
- queueName,
9
- workerId,
10
- edgeFunctionName,
11
- }: {
12
- queueName: string;
13
- workerId: string;
14
- edgeFunctionName: string;
15
- }): Promise<WorkerRow> {
16
- const [worker] = await this.sql<WorkerRow[]>`
17
- INSERT INTO edge_worker.workers (queue_name, worker_id, function_name)
18
- VALUES (${queueName}, ${workerId}, ${edgeFunctionName})
19
- RETURNING *;
20
- `;
21
-
22
- return worker;
23
- }
24
-
25
- async onWorkerStopped(workerRow: WorkerRow): Promise<WorkerRow> {
26
- const [worker] = await this.sql<WorkerRow[]>`
27
- UPDATE edge_worker.workers AS w
28
- SET stopped_at = clock_timestamp(), last_heartbeat_at = clock_timestamp()
29
- WHERE w.worker_id = ${workerRow.worker_id}
30
- RETURNING *;
31
- `;
32
-
33
- return worker;
34
- }
35
-
36
- async sendHeartbeat(workerRow: WorkerRow): Promise<void> {
37
- await this.sql<WorkerRow[]>`
38
- UPDATE edge_worker.workers AS w
39
- SET last_heartbeat_at = clock_timestamp()
40
- WHERE w.worker_id = ${workerRow.worker_id}
41
- RETURNING *;
42
- `;
43
- }
44
- }
@@ -1,102 +0,0 @@
1
- import type postgres from 'postgres';
2
- import type { IBatchProcessor, ILifecycle, WorkerBootstrap } from './types.ts';
3
- import { getLogger, setupLogger } from './Logger.ts';
4
-
5
- export class Worker {
6
- private lifecycle: ILifecycle;
7
- private logger = getLogger('Worker');
8
- private abortController = new AbortController();
9
-
10
- private batchProcessor: IBatchProcessor;
11
- private sql: postgres.Sql;
12
-
13
- constructor(
14
- batchProcessor: IBatchProcessor,
15
- lifecycle: ILifecycle,
16
- sql: postgres.Sql,
17
- ) {
18
- this.sql = sql;
19
-
20
- this.lifecycle = lifecycle;
21
-
22
- this.batchProcessor = batchProcessor;
23
- }
24
-
25
- async startOnlyOnce(workerBootstrap: WorkerBootstrap) {
26
- if (this.lifecycle.isRunning) {
27
- this.logger.debug('Worker already running, ignoring start request');
28
- return;
29
- }
30
-
31
- await this.start(workerBootstrap);
32
- }
33
-
34
- private async start(workerBootstrap: WorkerBootstrap) {
35
- setupLogger(workerBootstrap.workerId);
36
-
37
- try {
38
- await this.lifecycle.acknowledgeStart(workerBootstrap);
39
-
40
- while (this.isMainLoopActive) {
41
- try {
42
- await this.lifecycle.sendHeartbeat();
43
- } catch (error: unknown) {
44
- this.logger.error(`Error sending heartbeat: ${error}`);
45
- // Continue execution - a failed heartbeat shouldn't stop processing
46
- }
47
-
48
- try {
49
- await this.batchProcessor.processBatch();
50
- } catch (error: unknown) {
51
- this.logger.error(`Error processing batch: ${error}`);
52
- // Continue to next iteration - failed batch shouldn't stop the worker
53
- }
54
- }
55
- } catch (error) {
56
- this.logger.error(`Error in worker main loop: ${error}`);
57
- throw error;
58
- }
59
- }
60
-
61
- async stop() {
62
- // If the worker is already stopping or stopped, do nothing
63
- if (this.lifecycle.isStopping || this.lifecycle.isStopped) {
64
- return;
65
- }
66
-
67
- this.lifecycle.transitionToStopping();
68
-
69
- try {
70
- this.logger.info('-> Stopped accepting new messages');
71
- this.abortController.abort();
72
-
73
- this.logger.info('-> Waiting for pending tasks to complete...');
74
- await this.batchProcessor.awaitCompletion();
75
- this.logger.info('-> Pending tasks completed!');
76
-
77
- this.lifecycle.acknowledgeStop();
78
-
79
- this.logger.info('-> Closing SQL connection...');
80
- await this.sql.end();
81
- this.logger.info('-> SQL connection closed!');
82
- } catch (error) {
83
- this.logger.info(`Error during worker stop: ${error}`);
84
- throw error;
85
- }
86
- }
87
-
88
- get edgeFunctionName() {
89
- return this.lifecycle.edgeFunctionName;
90
- }
91
-
92
- /**
93
- * Returns true if worker state is Running and worker was not stopped
94
- */
95
- private get isMainLoopActive() {
96
- return this.lifecycle.isRunning && !this.isAborted;
97
- }
98
-
99
- private get isAborted() {
100
- return this.abortController.signal.aborted;
101
- }
102
- }
@@ -1,93 +0,0 @@
1
- import { Heartbeat } from './Heartbeat.ts';
2
- import { getLogger } from './Logger.ts';
3
- import type { Queries } from './Queries.ts';
4
- import type { Queue } from '../queue/Queue.ts';
5
- import type { ILifecycle, Json, WorkerBootstrap, WorkerRow } from './types.ts';
6
- import { States, WorkerState } from './WorkerState.ts';
7
-
8
- export interface LifecycleConfig {
9
- queueName: string;
10
- }
11
-
12
- export class WorkerLifecycle<IMessage extends Json> implements ILifecycle {
13
- private workerState: WorkerState = new WorkerState();
14
- private heartbeat?: Heartbeat;
15
- private logger = getLogger('WorkerLifecycle');
16
- private queries: Queries;
17
- private queue: Queue<IMessage>;
18
- private workerRow?: WorkerRow;
19
-
20
- constructor(queries: Queries, queue: Queue<IMessage>) {
21
- this.queries = queries;
22
- this.queue = queue;
23
- }
24
-
25
- async acknowledgeStart(workerBootstrap: WorkerBootstrap): Promise<void> {
26
- this.workerState.transitionTo(States.Starting);
27
-
28
- this.logger.info(`Ensuring queue '${this.queue.queueName}' exists...`);
29
- await this.queue.safeCreate();
30
-
31
- this.workerRow = await this.queries.onWorkerStarted({
32
- queueName: this.queueName,
33
- ...workerBootstrap,
34
- });
35
-
36
- this.heartbeat = new Heartbeat(5000, this.queries, this.workerRow);
37
-
38
- this.workerState.transitionTo(States.Running);
39
- }
40
-
41
- acknowledgeStop() {
42
- this.workerState.transitionTo(States.Stopping);
43
-
44
- if (!this.workerRow) {
45
- throw new Error('Cannot stop worker: workerRow not set');
46
- }
47
-
48
- try {
49
- this.logger.debug('Acknowledging worker stop...');
50
-
51
- // TODO: commented out because we can live without this
52
- // but it is causing problems with DbHandler - workes does not have
53
- // enough time to fire this query before hard-terimnated
54
- // We can always check the heartbeat to see if it is still running
55
- //
56
- // await this.queries.onWorkerStopped(this.workerRow);
57
-
58
- this.workerState.transitionTo(States.Stopped);
59
- this.logger.debug('Worker stop acknowledged');
60
- } catch (error) {
61
- this.logger.debug(`Error acknowledging worker stop: ${error}`);
62
- throw error;
63
- }
64
- }
65
-
66
- get edgeFunctionName() {
67
- return this.workerRow?.function_name;
68
- }
69
-
70
- get queueName() {
71
- return this.queue.queueName;
72
- }
73
-
74
- async sendHeartbeat() {
75
- await this.heartbeat?.send();
76
- }
77
-
78
- get isRunning() {
79
- return this.workerState.isRunning;
80
- }
81
-
82
- get isStopping() {
83
- return this.workerState.isStopping;
84
- }
85
-
86
- get isStopped() {
87
- return this.workerState.isStopped;
88
- }
89
-
90
- transitionToStopping() {
91
- this.workerState.transitionTo(States.Stopping);
92
- }
93
- }
@@ -1,85 +0,0 @@
1
- import { getLogger } from './Logger.ts';
2
-
3
- export enum States {
4
- /** The worker has been created but has not yet started. */
5
- Created = 'created',
6
-
7
- /** The worker is starting but has not yet started processing messages. */
8
- Starting = 'starting',
9
-
10
- /** The worker is processing messages. */
11
- Running = 'running',
12
-
13
- /** The worker stopped processing messages but is still releasing resources. */
14
- Stopping = 'stopping',
15
-
16
- /** The worker has stopped processing messages and released resources
17
- * and can be discarded. */
18
- Stopped = 'stopped',
19
- }
20
-
21
- export const Transitions: Record<States, States[]> = {
22
- [States.Created]: [States.Starting],
23
- [States.Starting]: [States.Running],
24
- [States.Running]: [States.Stopping],
25
- [States.Stopping]: [States.Stopped],
26
- [States.Stopped]: [], // Terminal state - no valid transitions from here
27
- };
28
-
29
- export class TransitionError extends Error {
30
- constructor(options: { from: States; to: States }) {
31
- super(`Cannot transition from ${options.from} to ${options.to}`);
32
- }
33
- }
34
-
35
- /**
36
- * Represents the state of a worker and exposes method for doing allowed transitions
37
- */
38
- export class WorkerState {
39
- private logger = getLogger('WorkerState');
40
- private state: States = States.Created;
41
-
42
- get current() {
43
- return this.state;
44
- }
45
-
46
- get isCreated() {
47
- return this.state === States.Created;
48
- }
49
-
50
- get isStarting() {
51
- return this.state === States.Starting;
52
- }
53
-
54
- get isRunning() {
55
- return this.state === States.Running;
56
- }
57
-
58
- get isStopping() {
59
- return this.state === States.Stopping;
60
- }
61
-
62
- get isStopped() {
63
- return this.state === States.Stopped;
64
- }
65
-
66
- transitionTo(state: States) {
67
- this.logger.debug(
68
- `[WorkerState] Starting transition to '${state}' (current state: ${this.state})`
69
- );
70
-
71
- if (this.state === state) {
72
- return;
73
- }
74
-
75
- if (Transitions[this.state].includes(state)) {
76
- this.state = state;
77
- this.logger.debug(`[WorkerState] Transitioned to '${state}'`);
78
- } else {
79
- throw new TransitionError({
80
- from: this.state,
81
- to: state,
82
- });
83
- }
84
- }
85
- }
package/src/core/types.ts DELETED
@@ -1,47 +0,0 @@
1
- export type { Json } from '../../../core/src/types.ts';
2
-
3
- export interface IPoller<IMessage> {
4
- poll(): Promise<IMessage[]>;
5
- }
6
-
7
- export interface IExecutor {
8
- get msgId(): number;
9
- execute(): Promise<unknown>;
10
- }
11
-
12
- export interface IMessage {
13
- msg_id: number;
14
- }
15
-
16
- export interface ILifecycle {
17
- acknowledgeStart(workerBootstrap: WorkerBootstrap): Promise<void>;
18
- acknowledgeStop(): void;
19
- sendHeartbeat(): Promise<void>;
20
-
21
- get edgeFunctionName(): string | undefined;
22
- get queueName(): string;
23
- get isRunning(): boolean;
24
- get isStopping(): boolean;
25
- get isStopped(): boolean;
26
-
27
- transitionToStopping(): void;
28
- }
29
-
30
- export interface IBatchProcessor {
31
- processBatch(): Promise<void>;
32
- awaitCompletion(): Promise<void>;
33
- }
34
-
35
- export type WorkerRow = {
36
- last_heartbeat_at: string;
37
- queue_name: string;
38
- started_at: string;
39
- stopped_at: string | null;
40
- worker_id: string;
41
- function_name: string;
42
- };
43
-
44
- export interface WorkerBootstrap {
45
- edgeFunctionName: string;
46
- workerId: string;
47
- }
@@ -1,81 +0,0 @@
1
- import { Heartbeat } from '../core/Heartbeat.ts';
2
- import { getLogger } from '../core/Logger.ts';
3
- import type { Queries } from '../core/Queries.ts';
4
- import type { ILifecycle, WorkerBootstrap, WorkerRow } from '../core/types.ts';
5
- import { States, WorkerState } from '../core/WorkerState.ts';
6
- import type { AnyFlow } from '@pgflow/dsl';
7
-
8
- /**
9
- * A specialized WorkerLifecycle for Flow-based workers that is aware of the Flow's step types
10
- */
11
- export class FlowWorkerLifecycle<TFlow extends AnyFlow> implements ILifecycle {
12
- private workerState: WorkerState = new WorkerState();
13
- private heartbeat?: Heartbeat;
14
- private logger = getLogger('FlowWorkerLifecycle');
15
- private queries: Queries;
16
- private workerRow?: WorkerRow;
17
- private flow: TFlow;
18
-
19
- constructor(queries: Queries, flow: TFlow) {
20
- this.queries = queries;
21
- this.flow = flow;
22
- }
23
-
24
- async acknowledgeStart(workerBootstrap: WorkerBootstrap): Promise<void> {
25
- this.workerState.transitionTo(States.Starting);
26
-
27
- this.workerRow = await this.queries.onWorkerStarted({
28
- queueName: this.queueName,
29
- ...workerBootstrap,
30
- });
31
-
32
- this.heartbeat = new Heartbeat(5000, this.queries, this.workerRow);
33
-
34
- this.workerState.transitionTo(States.Running);
35
- }
36
-
37
- acknowledgeStop() {
38
- this.workerState.transitionTo(States.Stopping);
39
-
40
- if (!this.workerRow) {
41
- throw new Error('Cannot stop worker: workerRow not set');
42
- }
43
-
44
- try {
45
- this.logger.debug('Acknowledging worker stop...');
46
- this.workerState.transitionTo(States.Stopped);
47
- this.logger.debug('Worker stop acknowledged');
48
- } catch (error) {
49
- this.logger.debug(`Error acknowledging worker stop: ${error}`);
50
- throw error;
51
- }
52
- }
53
-
54
- get edgeFunctionName() {
55
- return this.workerRow?.function_name;
56
- }
57
-
58
- get queueName() {
59
- return this.flow.slug;
60
- }
61
-
62
- async sendHeartbeat() {
63
- await this.heartbeat?.send();
64
- }
65
-
66
- get isRunning() {
67
- return this.workerState.isRunning;
68
- }
69
-
70
- get isStopping() {
71
- return this.workerState.isStopping;
72
- }
73
-
74
- get isStopped() {
75
- return this.workerState.isStopped;
76
- }
77
-
78
- transitionToStopping() {
79
- this.workerState.transitionTo(States.Stopping);
80
- }
81
- }
@@ -1,87 +0,0 @@
1
- import type { AnyFlow } from '@pgflow/dsl';
2
- import type { StepTaskRecord, IPgflowClient } from './types.ts';
3
- import type { IExecutor } from '../core/types.ts';
4
- import { getLogger } from '../core/Logger.ts';
5
-
6
- class AbortError extends Error {
7
- constructor() {
8
- super('Operation aborted');
9
- this.name = 'AbortError';
10
- }
11
- }
12
-
13
- /**
14
- * An executor that processes step tasks using an IPgflowClient
15
- * with strong typing for the flow's step handlers
16
- */
17
- export class StepTaskExecutor<TFlow extends AnyFlow> implements IExecutor {
18
- private logger = getLogger('StepTaskExecutor');
19
-
20
- constructor(
21
- private readonly flow: TFlow,
22
- private readonly task: StepTaskRecord<TFlow>,
23
- private readonly adapter: IPgflowClient<TFlow>,
24
- private readonly signal: AbortSignal
25
- ) {}
26
-
27
- get msgId() {
28
- return this.task.msg_id;
29
- }
30
-
31
- async execute(): Promise<void> {
32
- try {
33
- if (this.signal.aborted) {
34
- throw new AbortError();
35
- }
36
-
37
- // Check if already aborted before starting
38
- this.signal.throwIfAborted();
39
-
40
- const stepSlug = this.task.step_slug;
41
- this.logger.debug(
42
- `Executing step task ${this.task.msg_id} for step ${stepSlug}`
43
- );
44
-
45
- // Get the step handler from the flow with proper typing
46
- const stepDef = this.flow.getStepDefinition(stepSlug);
47
-
48
- if (!stepDef) {
49
- throw new Error(`No step definition found for slug=${stepSlug}`);
50
- }
51
-
52
- // !!! HANDLER EXECUTION !!!
53
- const result = await stepDef.handler(this.task.input);
54
- // !!! HANDLER EXECUTION !!!
55
-
56
- this.logger.debug(
57
- `step task ${this.task.msg_id} completed successfully, marking as complete`
58
- );
59
- await this.adapter.completeTask(this.task, result);
60
-
61
- this.logger.debug(`step task ${this.task.msg_id} marked as complete`);
62
- } catch (error) {
63
- await this.handleExecutionError(error);
64
- }
65
- }
66
-
67
- /**
68
- * Handles the error that occurred during execution.
69
- *
70
- * If the error is an AbortError, it means that the worker was aborted and stopping,
71
- * the task will be picked up by another worker later.
72
- *
73
- * Otherwise, it marks the task as failed.
74
- */
75
- private async handleExecutionError(error: unknown) {
76
- if (error instanceof Error && error.name === 'AbortError') {
77
- this.logger.debug(`Aborted execution for step task ${this.task.msg_id}`);
78
- // Do not mark as failed - the worker was aborted and stopping,
79
- // the task will be picked up by another worker later
80
- } else {
81
- this.logger.error(
82
- `step task ${this.task.msg_id} failed with error: ${error}`
83
- );
84
- await this.adapter.failTask(this.task, error);
85
- }
86
- }
87
- }