Skip to content

Commit

Permalink
Merge pull request #8 from retro/dev
Browse files Browse the repository at this point in the history
feat: implement onExecute handler for Task
  • Loading branch information
retro authored Jul 9, 2023
2 parents ea62e11 + 893c945 commit 6910540
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"build:main": "swc ./src -d ./build",
"clean": "rimraf build coverage nyc_output",
"type:dts": "tsc --emitDeclarationOnly --project tsconfig.build.json",
"type:check": "tsc --noEmit",
"type:check": "vitest typecheck",
"format": "prettier \"src/**/*.ts\" --write",
"format:check": "prettier \"src/**/*.ts\" --check",
"lint": "eslint src --ext .ts --fix",
Expand Down
102 changes: 102 additions & 0 deletions src/__tests__/interpreter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3542,3 +3542,105 @@ it('can supports workflow cancellation', () => {

Effect.runSync(program);
});

it('supports task execution', () => {
const workflowDefinition = Builder.workflow('name')
.startCondition('start')
.task('A', (t) =>
t.onExecute(({ input, completeTask }) =>
Effect.gen(function* ($) {
if (input === 'COMPLETE') {
yield* $(completeTask());
} else {
return input;
}
})
)
)
.endCondition('end')
.connectCondition('start', (to) => to.task('A'))
.connectTask('A', (to) => to.condition('end'));

const program = Effect.gen(function* ($) {
const idGenerator = makeIdGenerator();
const stateManager = yield* $(
createMemory(),
Effect.provideService(IdGenerator, idGenerator)
);

const workflow = yield* $(
workflowDefinition.build(),
Effect.provideService(StateManager, stateManager),
Effect.provideService(IdGenerator, idGenerator)
);

const interpreter = yield* $(
Interpreter.make(workflow, {}),
Effect.provideService(StateManager, stateManager)
);

yield* $(interpreter.start('starting'));

const res1 = yield* $(interpreter.getWorkflowState());

expect(res1).toEqual({
id: 'workflow-1',
name: 'name',
state: 'running',
tasks: { 'task-1': { id: 'task-1', name: 'A', state: 'enabled' } },
conditions: {
'condition-1': { id: 'condition-1', name: 'start', marking: 1 },
},
});

yield* $(interpreter.activateTask('A'));

const res2 = yield* $(interpreter.getWorkflowState());

expect(res2).toEqual({
id: 'workflow-1',
name: 'name',
state: 'running',
tasks: { 'task-1': { id: 'task-1', name: 'A', state: 'active' } },
conditions: {
'condition-1': { id: 'condition-1', name: 'start', marking: 0 },
},
});

const executeRes1 = yield* $(interpreter.executeTask('A', 'foo'));
expect(executeRes1).toEqual('foo');

const executeRes2 = yield* $(interpreter.executeTask('A', 'bar'));
expect(executeRes2).toEqual('bar');

const res3 = yield* $(interpreter.getWorkflowState());

expect(res3).toEqual({
id: 'workflow-1',
name: 'name',
state: 'running',
tasks: { 'task-1': { id: 'task-1', name: 'A', state: 'active' } },
conditions: {
'condition-1': { id: 'condition-1', name: 'start', marking: 0 },
},
});

const executeRes3 = yield* $(interpreter.executeTask('A', 'COMPLETE'));
expect(executeRes3).toEqual(undefined);

const res4 = yield* $(interpreter.getWorkflowState());

expect(res4).toEqual({
id: 'workflow-1',
name: 'name',
state: 'done',
tasks: { 'task-1': { id: 'task-1', name: 'A', state: 'completed' } },
conditions: {
'condition-1': { id: 'condition-1', name: 'start', marking: 0 },
'condition-2': { id: 'condition-2', name: 'end', marking: 1 },
},
});
});

Effect.runSync(program);
});
4 changes: 2 additions & 2 deletions src/__tests__/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
EndConditionDoesNotExist,
StartConditionDoesNotExist,
TaskDoesNotExist,
TaskNotActivatedError,
TaskNotActiveError,
TaskNotEnabledError,
WorkflowNotInitialized,
} from '../errors.js';
Expand Down Expand Up @@ -94,7 +94,7 @@ it('can correctly infer predicate types', () => {
| StartConditionDoesNotExist
| EndConditionDoesNotExist
| TaskNotEnabledError
| TaskNotActivatedError,
| TaskNotActiveError,
void
>
>(start);
Expand Down
22 changes: 22 additions & 0 deletions src/builder/TaskBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TaskOnCompletePayload,
TaskOnDisablePayload,
TaskOnEnablePayload,
TaskOnExecutePayload,
} from '../types.js';
import { IdProvider } from './IdProvider.js';

Expand Down Expand Up @@ -103,6 +104,7 @@ export type InitializedTaskBuilder<C extends object = object> = TaskBuilder<
export interface ActivitiesReturnType {
onActivate: unknown;
onComplete: unknown;
onExecute: unknown;
}

export class TaskBuilder<
Expand Down Expand Up @@ -204,6 +206,26 @@ export class TaskBuilder<
return this;
}

onExecute<
F extends (
payload: TaskOnExecutePayload<C>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Effect.Effect<any, any, any>
>(
f: F
): TaskBuilder<
C,
TA,
JT,
ST,
ART & { onExecute: Effect.Effect.Success<ReturnType<F>> },
R | Effect.Effect.Context<ReturnType<F>>,
E | Effect.Effect.Error<ReturnType<F>>
> {
this.activities.onExecute = f;
return this;
}

onComplete<
F extends (
payload: TaskOnCompletePayload<C>
Expand Down
3 changes: 2 additions & 1 deletion src/builder/WorkflowBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type IsXorOrOrJoinSplit<T> = T extends never

type TasksActivitiesOutputs = Record<
string,
{ onComplete: unknown; onActivate: unknown }
{ onComplete: unknown; onActivate: unknown; onExecute: unknown }
>;
// TODO: implement invariant checking
export class WorkflowBuilder<
Expand Down Expand Up @@ -413,6 +413,7 @@ export class WorkflowBuilder<
[K in WBTasks & string]: {
onActivate: WBTasksActivitiesOutputs[K]['onActivate'];
onComplete: WBTasksActivitiesOutputs[K]['onComplete'];
onExecute: WBTasksActivitiesOutputs[K]['onExecute'];
};
},
OnStartReturnType
Expand Down
25 changes: 24 additions & 1 deletion src/elements/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function isValidTransition(
}
// TODO: handle case where task is completed and prev condition(s)
// have positive marking, so it should transition to enabled again
// TODO: add onExecute

export class Task {
readonly workflow: Workflow;
readonly preSet: Record<string, Condition> = {};
Expand Down Expand Up @@ -219,6 +219,29 @@ export class Task {
});
}

execute(context: object, input: unknown = undefined) {
const self = this;
return Effect.gen(function* ($) {
const state = yield* $(self.getState());
if (state === 'active') {
const activityContext = self.getActivityContext();
const taskActionsService = yield* $(TaskActionsService);
const completeTask = (input?: unknown) => {
return taskActionsService.completeTask(self.name, input);
};

return yield* $(
self.activities.onExecute({
...activityContext,
context,
input,
completeTask,
}) as Effect.Effect<never, never, unknown>
);
}
});
}

complete(context: object, input: unknown = undefined) {
const self = this;
return Effect.gen(function* ($) {
Expand Down
4 changes: 3 additions & 1 deletion src/elements/Workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export class Workflow<
return this.stateManager.updateWorkflowState(this, 'done');
}

cancel(context: object) {
cancel(
context: object
): Effect.Effect<never, WorkflowNotInitialized, unknown> {
return pipe(
Effect.allDiscard([
Effect.allPar(
Expand Down
9 changes: 4 additions & 5 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ export const TaskNotEnabledError = Data.tagged<TaskNotEnabledError>(
'TaskNotEnabledError'
);

export interface TaskNotActivatedError extends Data.Case {
readonly _tag: 'TaskNotActivatedError';
export interface TaskNotActiveError extends Data.Case {
readonly _tag: 'TaskNotActiveError';
}
export const TaskNotActivatedError = Data.tagged<TaskNotActivatedError>(
'TaskNotActivatedError'
);
export const TaskNotActiveError =
Data.tagged<TaskNotActiveError>('TaskNotActiveError');
40 changes: 38 additions & 2 deletions src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Workflow,
WorkflowTasksActivitiesOutputs,
} from './elements/Workflow.js';
import { TaskNotActivatedError, TaskNotEnabledError } from './errors.js';
import { TaskNotActiveError, TaskNotEnabledError } from './errors.js';
import { TaskActionsService } from './types.js';

type QueueItem =
Expand Down Expand Up @@ -168,7 +168,7 @@ export class Interpreter<
const task = yield* $(self.workflow.getTask(taskName));
const isActive = yield* $(task.isActive());
if (!isActive) {
yield* $(Effect.fail(TaskNotActivatedError()));
yield* $(Effect.fail(TaskNotActiveError()));
}
const output: unknown = yield* $(
task.complete(self.context, input),
Expand Down Expand Up @@ -209,6 +209,42 @@ export class Interpreter<
>;
}

_executeTask(taskName: string, input?: unknown) {
const self = this;

return Effect.gen(function* ($) {
const task = yield* $(self.workflow.getTask(taskName));
const isActive = yield* $(task.isActive());
if (!isActive) {
yield* $(Effect.fail(TaskNotActiveError()));
}
const output: unknown = yield* $(
task.execute(self.context, input),
Effect.provideService(TaskActionsService, self.getTaskActionsService())
);

yield* $(self.runQueue());
yield* $(self.maybeEnd());

return output;
});
}

executeTask<T extends keyof TasksActivitiesOutputs, I>(
taskName: T & string,
input?: I
) {
return this._executeTask(taskName, input) as unknown as Effect.Effect<
R,
E,
unknown extends I
? undefined
: unknown extends TasksActivitiesOutputs[T]['onExecute']
? I
: TasksActivitiesOutputs[T]['onExecute']
>;
}

private getTaskActionsService() {
const { queue } = this;
return {
Expand Down
16 changes: 14 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
EndConditionDoesNotExist,
StartConditionDoesNotExist,
TaskDoesNotExist,
TaskNotActivatedError,
TaskNotActiveError,
TaskNotEnabledError,
WorkflowNotInitialized,
} from './errors.js';
Expand Down Expand Up @@ -97,7 +97,7 @@ export interface WorkflowOnStartPayload<C> {
| WorkflowNotInitialized
| TaskDoesNotExist
| TaskNotEnabledError
| TaskNotActivatedError,
| TaskNotActiveError,
void
>;
}
Expand Down Expand Up @@ -135,6 +135,15 @@ export type TaskOnActivatePayload<C extends object = object> =
>;
};

export type TaskOnExecutePayload<C extends object = object> =
DefaultTaskActivityPayload & {
context: C;
input: unknown;
completeTask: (
input?: unknown
) => Effect.Effect<never, WorkflowNotInitialized, void>;
};

export type TaskOnCompletePayload<C extends object = object> =
DefaultTaskActivityPayload & {
context: C;
Expand All @@ -158,6 +167,9 @@ export interface TaskActivities<C extends object = object> {
onActivate: (
payload: TaskOnActivatePayload<C>
) => Effect.Effect<unknown, unknown, unknown>;
onExecute: (
payload: TaskOnExecutePayload<C>
) => Effect.Effect<unknown, unknown, unknown>;
onComplete: (
payload: TaskOnCompletePayload<C>
) => Effect.Effect<unknown, unknown, unknown>;
Expand Down
Loading

0 comments on commit 6910540

Please sign in to comment.