diff --git a/src/Service.ts b/src/Service.ts index 9a6bd1b..ab3d50f 100644 --- a/src/Service.ts +++ b/src/Service.ts @@ -670,8 +670,21 @@ export class Service< .getWorkflow(input.workflowId) .pipe(Effect.map((w) => w.context)); }, - updateWorkflowContext(context: unknown) { - return state.updateWorkflowContext(input.workflowId, context); + updateWorkflowContext(contextOrUpdater: unknown) { + return Effect.gen(function* ($) { + if (typeof contextOrUpdater === 'function') { + const workflow = yield* $(state.getWorkflow(input.workflowId)); + return yield* $( + state.updateWorkflowContext( + input.workflowId, + contextOrUpdater(workflow.context) + ) + ); + } + return yield* $( + state.updateWorkflowContext(input.workflowId, contextOrUpdater) + ); + }); }, }, queue: { diff --git a/src/__tests__/__snapshots__/update-workflow-context.test.ts.snap b/src/__tests__/__snapshots__/update-workflow-context.test.ts.snap new file mode 100644 index 0000000..b524da3 --- /dev/null +++ b/src/__tests__/__snapshots__/update-workflow-context.test.ts.snap @@ -0,0 +1,223 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`handles context update when updater function is passed 1`] = ` +{ + "conditions": [ + { + "marking": 0, + "name": "start", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "marking": 0, + "name": "end", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + ], + "tasks": [ + { + "generation": 1, + "name": "t1", + "state": "started", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + ], + "workItems": [], + "workflows": [ + { + "context": { + "count": 2, + }, + "id": "workflow-1", + "name": "activities", + "parent": null, + "state": "started", + }, + ], +} +`; + +exports[`handles context update when value is passed 1`] = ` +{ + "conditions": [ + { + "marking": 0, + "name": "start", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "marking": 0, + "name": "end", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + ], + "tasks": [ + { + "generation": 1, + "name": "t1", + "state": "started", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + ], + "workItems": [], + "workflows": [ + { + "context": { + "count": 2, + }, + "id": "workflow-1", + "name": "activities", + "parent": null, + "state": "started", + }, + ], +} +`; + +exports[`handles parent context update when updater function is passed 1`] = ` +{ + "conditions": [ + { + "marking": 0, + "name": "start", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "marking": 0, + "name": "end", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "marking": 1, + "name": "start", + "workflowId": "workflow-2", + "workflowName": "sub", + }, + { + "marking": 0, + "name": "end", + "workflowId": "workflow-2", + "workflowName": "sub", + }, + ], + "tasks": [ + { + "generation": 1, + "name": "t1", + "state": "started", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "generation": 0, + "name": "subT1", + "state": "enabled", + "workflowId": "workflow-2", + "workflowName": "sub", + }, + ], + "workItems": [], + "workflows": [ + { + "context": { + "count": 1, + }, + "id": "workflow-1", + "name": "activities", + "parent": null, + "state": "started", + }, + { + "context": undefined, + "id": "workflow-2", + "name": "sub", + "parent": { + "taskGeneration": 1, + "taskName": "t1", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + "state": "started", + }, + ], +} +`; + +exports[`handles parent context update when value is passed 1`] = ` +{ + "conditions": [ + { + "marking": 0, + "name": "start", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "marking": 0, + "name": "end", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "marking": 1, + "name": "start", + "workflowId": "workflow-2", + "workflowName": "sub", + }, + { + "marking": 0, + "name": "end", + "workflowId": "workflow-2", + "workflowName": "sub", + }, + ], + "tasks": [ + { + "generation": 1, + "name": "t1", + "state": "started", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + { + "generation": 0, + "name": "subT1", + "state": "enabled", + "workflowId": "workflow-2", + "workflowName": "sub", + }, + ], + "workItems": [], + "workflows": [ + { + "context": { + "count": 1, + }, + "id": "workflow-1", + "name": "activities", + "parent": null, + "state": "started", + }, + { + "context": undefined, + "id": "workflow-2", + "name": "sub", + "parent": { + "taskGeneration": 1, + "taskName": "t1", + "workflowId": "workflow-1", + "workflowName": "activities", + }, + "state": "started", + }, + ], +} +`; diff --git a/src/__tests__/update-workflow-context.test.ts b/src/__tests__/update-workflow-context.test.ts new file mode 100644 index 0000000..7aa7eef --- /dev/null +++ b/src/__tests__/update-workflow-context.test.ts @@ -0,0 +1,198 @@ +import { Effect } from 'effect'; +import { it } from 'vitest'; + +import { Builder, IdGenerator, Service } from '../index.js'; +import { makeIdGenerator } from './shared.js'; + +const workflowDefinition1 = Builder.workflow<{ count: number }>() + .withName('activities') + .startCondition('start') + .task('t1', (t) => + t().onEnable(({ enableTask, getWorkflowContext, updateWorkflowContext }) => + Effect.gen(function* ($) { + const { enqueueStartTask } = yield* $(enableTask()); + yield* $(enqueueStartTask()); + const workflowContext = yield* $(getWorkflowContext()); + const { count } = workflowContext; + yield* $(updateWorkflowContext({ count: count + 1 })); + }) + ) + ) + .endCondition('end') + .connectCondition('start', (to) => to.task('t1')) + .connectTask('t1', (to) => to.condition('end')) + .onStart(({ getWorkflowContext, updateWorkflowContext }) => + Effect.gen(function* ($) { + const workflowContext = yield* $(getWorkflowContext()); + const { count } = workflowContext; + yield* $(updateWorkflowContext({ count: count + 1 })); + }) + ); + +it('handles context update when value is passed', ({ expect }) => { + const program = Effect.gen(function* ($) { + const idGenerator = makeIdGenerator(); + + const service = yield* $( + workflowDefinition1.build(), + Effect.flatMap((workflow) => Service.initialize(workflow, { count: 0 })), + Effect.provideService(IdGenerator, idGenerator) + ); + + yield* $(service.start()); + const state = yield* $(service.getState()); + expect(state).toMatchSnapshot(); + expect(state); + }); + + Effect.runSync(program); +}); + +const workflowDefinition2 = Builder.workflow<{ count: number }>() + .withName('activities') + .startCondition('start') + .task('t1', (t) => + t().onEnable(({ enableTask, updateWorkflowContext }) => + Effect.gen(function* ($) { + const { enqueueStartTask } = yield* $(enableTask()); + yield* $(enqueueStartTask()); + yield* $(updateWorkflowContext(({ count }) => ({ count: count + 1 }))); + }) + ) + ) + .endCondition('end') + .connectCondition('start', (to) => to.task('t1')) + .connectTask('t1', (to) => to.condition('end')) + .onStart(({ updateWorkflowContext }) => + Effect.gen(function* ($) { + yield* $(updateWorkflowContext(({ count }) => ({ count: count + 1 }))); + }) + ); + +it('handles context update when updater function is passed', ({ expect }) => { + const program = Effect.gen(function* ($) { + const idGenerator = makeIdGenerator(); + + const service = yield* $( + workflowDefinition2.build(), + Effect.flatMap((workflow) => Service.initialize(workflow, { count: 0 })), + Effect.provideService(IdGenerator, idGenerator) + ); + + yield* $(service.start()); + + const state = yield* $(service.getState()); + expect(state).toMatchSnapshot(); + expect(state); + }); + + Effect.runSync(program); +}); + +const subWorkflowDefinition1 = Builder.workflow() + .withName('sub') + .withParentContext<{ count: number }>() + .startCondition('start') + .task('subT1') + .endCondition('end') + .connectCondition('start', (to) => to.task('subT1')) + .connectTask('subT1', (to) => to.condition('end')) + .onStart(({ updateParentWorkflowContext }) => + updateParentWorkflowContext(({ count }) => ({ count: count + 1 })) + ); + +const parentWorkflowDefinition1 = Builder.workflow<{ count: number }>() + .withName('activities') + .startCondition('start') + .compositeTask('t1', (ct) => + ct() + .withSubWorkflow(subWorkflowDefinition1) + .onStart(({ startTask }) => + Effect.gen(function* ($) { + const { initializeWorkflow, enqueueStartWorkflow } = yield* $( + startTask() + ); + const workflow = yield* $(initializeWorkflow()); + yield* $(enqueueStartWorkflow(workflow.id)); + }) + ) + ) + .endCondition('end') + .connectCondition('start', (to) => to.task('t1')) + .connectTask('t1', (to) => to.condition('end')); + +it('handles parent context update when updater function is passed', ({ + expect, +}) => { + const program = Effect.gen(function* ($) { + const idGenerator = makeIdGenerator(); + + const service = yield* $( + parentWorkflowDefinition1.build(), + Effect.flatMap((workflow) => Service.initialize(workflow, { count: 0 })), + Effect.provideService(IdGenerator, idGenerator) + ); + + yield* $(service.start()); + yield* $(service.startTask('t1')); + + const state = yield* $(service.getState()); + expect(state).toMatchSnapshot(); + expect(state); + }); + + Effect.runSync(program); +}); + +const subWorkflowDefinition2 = Builder.workflow() + .withName('sub') + .withParentContext<{ count: number }>() + .startCondition('start') + .task('subT1') + .endCondition('end') + .connectCondition('start', (to) => to.task('subT1')) + .connectTask('subT1', (to) => to.condition('end')) + .onStart(({ updateParentWorkflowContext }) => + updateParentWorkflowContext({ count: 1 }) + ); + +const parentWorkflowDefinition2 = Builder.workflow<{ count: number }>() + .withName('activities') + .startCondition('start') + .compositeTask('t1', (ct) => + ct() + .withSubWorkflow(subWorkflowDefinition2) + .onStart(({ startTask }) => + Effect.gen(function* ($) { + const { initializeWorkflow, enqueueStartWorkflow } = yield* $( + startTask() + ); + const workflow = yield* $(initializeWorkflow()); + yield* $(enqueueStartWorkflow(workflow.id)); + }) + ) + ) + .endCondition('end') + .connectCondition('start', (to) => to.task('t1')) + .connectTask('t1', (to) => to.condition('end')); + +it('handles parent context update when value is passed', ({ expect }) => { + const program = Effect.gen(function* ($) { + const idGenerator = makeIdGenerator(); + + const service = yield* $( + parentWorkflowDefinition2.build(), + Effect.flatMap((workflow) => Service.initialize(workflow, { count: 0 })), + Effect.provideService(IdGenerator, idGenerator) + ); + + yield* $(service.start()); + yield* $(service.startTask('t1')); + + const state = yield* $(service.getState()); + expect(state).toMatchSnapshot(); + expect(state); + }); + + Effect.runSync(program); +}); diff --git a/src/elements/Workflow.ts b/src/elements/Workflow.ts index 6529b8c..27c8914 100644 --- a/src/elements/Workflow.ts +++ b/src/elements/Workflow.ts @@ -387,7 +387,7 @@ export class Workflow< return yield* $(stateManager.getWorkflowContext(parent.workflowId)); }); }, - updateParentWorkflowContext(context: unknown) { + updateParentWorkflowContext(contextOrUpdater: unknown) { return Effect.gen(function* ($) { const parent = workflow.parent; if (!parent) { @@ -400,8 +400,22 @@ export class Workflow< ) ); } + if (typeof contextOrUpdater === 'function') { + const context = yield* $( + stateManager.getWorkflowContext(parent.workflowId) + ); + return yield* $( + stateManager.updateWorkflowContext( + parent.workflowId, + contextOrUpdater(context) + ) + ); + } return yield* $( - stateManager.updateWorkflowContext(parent.workflowId, context) + stateManager.updateWorkflowContext( + parent.workflowId, + contextOrUpdater + ) ); }); }, @@ -410,8 +424,21 @@ export class Workflow< return (yield* $(stateManager.getWorkflowContext(id))) as Context; }); }, - updateWorkflowContext(context: unknown) { - return stateManager.updateWorkflowContext(id, context); + updateWorkflowContext(contextOrUpdater: unknown) { + return Effect.gen(function* ($) { + if (typeof contextOrUpdater === 'function') { + const context = yield* $(stateManager.getWorkflowContext(id)); + return yield* $( + stateManager.updateWorkflowContext( + id, + contextOrUpdater(context) + ) + ); + } + return yield* $( + stateManager.updateWorkflowContext(id, contextOrUpdater) + ); + }); }, }; }); diff --git a/src/types.ts b/src/types.ts index 4a09eb1..4dd1999 100644 --- a/src/types.ts +++ b/src/types.ts @@ -65,11 +65,13 @@ export interface WorkflowBuilderDefinition { }; } +type UpdateWorkflowContext = ( + contextOrUpdater: C | ((context: C) => C) +) => Effect.Effect; + export interface DefaultTaskOrWorkItemActivityPayload { getWorkflowContext(): Effect.Effect; - updateWorkflowContext( - context: C - ): Effect.Effect; + updateWorkflowContext: UpdateWorkflowContext; } export type TaskOnDisablePayload = @@ -534,9 +536,7 @@ export interface ExecutionContext { WorkflowDoesNotExist, unknown >; - updateWorkflowContext: ( - context: unknown - ) => Effect.Effect; + updateWorkflowContext: UpdateWorkflowContext; }; queue: { offer: ( @@ -624,16 +624,14 @@ export interface DefaultWorkflowActivityPayload { updateParentWorkflowContext: PC extends never ? never : ( - context: PC + context: PC | ((context: PC) => PC) ) => Effect.Effect< never, ParentWorkflowDoesNotExist | WorkflowDoesNotExist, void >; getWorkflowContext(): Effect.Effect; - updateWorkflowContext( - context: C - ): Effect.Effect; + updateWorkflowContext: UpdateWorkflowContext; } export type WorkflowOnStartPayload = DefaultWorkflowActivityPayload<