From 4ead535405618fbe5259d16c85f25d359cac9bd5 Mon Sep 17 00:00:00 2001 From: Arturas Date: Tue, 24 Jan 2023 19:10:02 +0000 Subject: [PATCH] feat: Execute workflow action Closes #35 --- src/actions/action-id.mts | 1 + src/actions/core/delay-action.tsx | 5 +- src/actions/core/equip-item-action.tsx | 10 +- src/actions/core/exec-workflow-action.mts | 114 ++++++++++++++++++++++ src/actions/core/index.mts | 1 + src/lib/execution/workflow-execution.mts | 14 ++- src/lib/util/memo.mts | 25 +++++ src/lib/util/obj-from-array.mts | 8 ++ src/public_api/action.d.ts | 2 +- src/public_api/option.d.ts | 2 +- src/ui/ui.mts | 4 +- 11 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 src/actions/core/exec-workflow-action.mts create mode 100644 src/lib/util/memo.mts create mode 100644 src/lib/util/obj-from-array.mts diff --git a/src/actions/action-id.mts b/src/actions/action-id.mts index 53ed569..80fb8b2 100644 --- a/src/actions/action-id.mts +++ b/src/actions/action-id.mts @@ -33,6 +33,7 @@ const enum ActionId { StartSkillSummoning = 31, StartSkillThieving = 32, StartSkillWoodcutting = 33, + ExecWorkflow = 34, } export default ActionId; diff --git a/src/actions/core/delay-action.tsx b/src/actions/core/delay-action.tsx index 49a4b95..f893d1b 100644 --- a/src/actions/core/delay-action.tsx +++ b/src/actions/core/delay-action.tsx @@ -1,6 +1,5 @@ -import {noop} from 'lodash-es'; import {Fragment} from 'preact'; -import {map, timer} from 'rxjs'; +import {timer} from 'rxjs'; import {InternalCategory} from '../../lib/registries/action-registry.mjs'; import {defineLocalAction} from '../../lib/util/define-local.mjs'; import ActionId from '../action-id.mjs'; @@ -17,7 +16,7 @@ defineLocalAction({ {`${duration.toLocaleString()}ms`} ), - execute: ({duration}) => timer(duration).pipe(map(noop)), + execute: ({duration}) => timer(duration), id: ActionId.CoreDelay, label: 'Wait', media: cdnMedia('assets/media/main/timer.svg'), diff --git a/src/actions/core/equip-item-action.tsx b/src/actions/core/equip-item-action.tsx index 761a77c..2163d59 100644 --- a/src/actions/core/equip-item-action.tsx +++ b/src/actions/core/equip-item-action.tsx @@ -3,6 +3,7 @@ import {EquipSlotType} from 'melvor'; import {Fragment} from 'preact'; import {InternalCategory} from '../../lib/registries/action-registry.mjs'; import {defineLocalAction} from '../../lib/util/define-local.mjs'; +import {objectFromArray} from '../../lib/util/obj-from-array.mjs'; import {BigNum} from '../../ui/components/big-num'; import {RenderNodeMedia} from '../../ui/pages/workflows-dashboard/render-node-media'; import ActionId from '../action-id.mjs'; @@ -76,15 +77,6 @@ defineLocalAction({ ], }); -function objectFromArray(values: T[]): Record { - const out: Record = {} as any; - for (const v of values) { - out[v] = v; - } - - return out; -} - const SLOTS_WITH_QTY = new Set([ EquipSlotType.Consumable, EquipSlotType.Quiver, diff --git a/src/actions/core/exec-workflow-action.mts b/src/actions/core/exec-workflow-action.mts new file mode 100644 index 0000000..08fe8ed --- /dev/null +++ b/src/actions/core/exec-workflow-action.mts @@ -0,0 +1,114 @@ +import {takeUntil} from 'rxjs'; +import {take} from 'rxjs/operators'; +import type {WorkflowStep} from '../../lib/data/workflow-step.mjs'; +import type {Workflow} from '../../lib/data/workflow.mjs'; +import {WorkflowExecution} from '../../lib/execution/workflow-execution.mjs'; +import {InternalCategory} from '../../lib/registries/action-registry.mjs'; +import WorkflowRegistry from '../../lib/registries/workflow-registry.mjs'; +import {defineLocalAction} from '../../lib/util/define-local.mjs'; +import {memoOutput} from '../../lib/util/memo.mjs'; +import type {Obj} from '../../public_api'; +import {mainIcon} from '../../ui/ui.mjs'; +import ActionId from '../action-id.mjs'; + +interface Props { + name: string; + stop: StopCondition; +} + +const enum StopCondition { + NextStepTrigger = 's', + WorkflowCompletion = 'w', +} + +const reduceWorkflowNames = (acc: Obj, {name}: Workflow): Obj => { + acc[name] = name; + + return acc; +}; +const getWorkflowNames = memoOutput((): Obj => ( + WorkflowRegistry.inst.workflows.reduce(reduceWorkflowNames, {}) +)); + +defineLocalAction({ + category: InternalCategory.CORE, + execute({name, stop}) { + const reg = WorkflowRegistry.inst; + const workflow = reg.workflows.find(wf => wf.name === name); + + if (!workflow) { + throw new Error(`Workflow not found: ${name}`); + } + + switch (stop) { + case StopCondition.WorkflowCompletion: + return new WorkflowExecution(workflow); + case StopCondition.NextStepTrigger: { + const exec = reg.primaryExecution; + if (!exec) { + throw new Error('There is no primary execution running'); + } + + const activeIdx = exec.activeStepIdx; + const steps = exec.workflow.steps; + + let nextStep: WorkflowStep | undefined = steps[activeIdx + 1]; + if (!nextStep) { // Check for "jump to step" action + const currStep = steps[activeIdx]; + if (!currStep) { + throw new Error('Screwy primary execution: current step not found in steps array'); + } + + const jumpToStepAction = currStep.actions.find(a => ( + + // @ts-expect-error + a.action.id === ActionId.CoreSetStepIdx + )); + if (!jumpToStepAction) { + throw new Error('There is no next step'); + } + + const tryIdx: number | undefined = jumpToStepAction.opts.idx; + if (typeof tryIdx !== 'number') { + throw new Error('Can\'t resolve next step: jump to step index not a number'); + } + + nextStep = steps[tryIdx]; + if (!nextStep) { + throw new Error('Can\'t resolve next step: jump to step index leads to nothing'); + } + } + + return new WorkflowExecution(workflow).pipe( + takeUntil(nextStep.trigger.listen().pipe(take(1))) + ); + } + default: + throw new Error(`Unknown stop condition: ${stop}`); + } + }, + id: ActionId.ExecWorkflow, + initOptions: () => ({stop: StopCondition.NextStepTrigger}), + label: 'Execute workflow', + media: mainIcon, + options: [ + { + enum: getWorkflowNames, + id: 'name', + label: 'Name', + required: true, + type: String, + }, + { + description: '"Workflow completion" will execute the inner workflow until it\'s done; "Next step\'s trigger" will execute it until it completes or until the trigger for the next step fires. You CAN use a "Jump to step" action immediately following this one with the "next step trigger" option.', + enum: { + [StopCondition.NextStepTrigger]: 'Next step\'s trigger', + [StopCondition.WorkflowCompletion]: 'Workflow completion', + }, + id: 'stop', + label: 'Stop condition', + required: true, + type: String, + }, + ], +}); diff --git a/src/actions/core/index.mts b/src/actions/core/index.mts index 190af5a..8f5e671 100644 --- a/src/actions/core/index.mts +++ b/src/actions/core/index.mts @@ -2,6 +2,7 @@ import './buy-item-action'; import './delay-action'; import './equip-food-action'; import './equip-item-action'; +import './exec-workflow-action.mjs'; import './sell-item-action.mjs'; import './set-step-idx-action.mjs'; import './switch-equipment-set-action.mjs'; diff --git a/src/lib/execution/workflow-execution.mts b/src/lib/execution/workflow-execution.mts index 094d084..3b39567 100644 --- a/src/lib/execution/workflow-execution.mts +++ b/src/lib/execution/workflow-execution.mts @@ -38,6 +38,9 @@ export class WorkflowExecution extends ShareReplayLike { @AutoIncrement() public readonly id!: number; + /** Set to true when it's run as part of, e.g. the exec workflow action */ + public isEmbeddedRun = false; + /** Currently executed step index */ private readonly _activeStepIdx$: BehaviorSubject; @@ -116,6 +119,11 @@ export class WorkflowExecution extends ShareReplayLike { return this.workflow.steps[this.activeStepIdx]; } + /** Should this workflow be removed on completion? */ + private get shouldRm(): boolean { + return !this.isEmbeddedRun && Boolean(ctx.accountStorage.getItem(ConfigCheckboxKey.RM_WORKFLOW_ON_COMPLETE)); + } + /** Set the currently executed step index. */ @BoundMethod() public setActiveStepIdx(v: number): void { @@ -172,7 +180,7 @@ export class WorkflowExecution extends ShareReplayLike { } } - if (shouldRemoveWorkflowOnCompletion()) { + if (this.shouldRm) { unsetRemoveWorkflowOnCompletion(); WorkflowRegistry.inst.rmByListId(this.workflow.listId); } else { @@ -370,10 +378,6 @@ export class WorkflowExecution extends ShareReplayLike { } } -function shouldRemoveWorkflowOnCompletion(): boolean { - return Boolean(ctx.accountStorage.getItem(ConfigCheckboxKey.RM_WORKFLOW_ON_COMPLETE)); -} - function unsetRemoveWorkflowOnCompletion(): void { ctx.accountStorage.removeItem(ConfigCheckboxKey.RM_WORKFLOW_ON_COMPLETE); } diff --git a/src/lib/util/memo.mts b/src/lib/util/memo.mts new file mode 100644 index 0000000..199ffc1 --- /dev/null +++ b/src/lib/util/memo.mts @@ -0,0 +1,25 @@ +import {isEqual} from 'lodash-es'; + +type This = T extends (this: infer E) => any ? E : never; + +/** + * Returns the previously returned object if calling the function again yields the same output + * @param fn The function + * @param checkFn Function used for output equality checking + */ +export function memoOutput any>( + fn: T, + checkFn: (a: ReturnType, b: ReturnType) => boolean = isEqual +): T { + let prevReturn: ReturnType; + + return function memoOutputFn(this: This): ReturnType { + const out = fn.apply(this, arguments as unknown as Parameters); + if (checkFn(out, prevReturn)) { + return prevReturn; + } + + prevReturn = out; + return out; + } as T; +} diff --git a/src/lib/util/obj-from-array.mts b/src/lib/util/obj-from-array.mts new file mode 100644 index 0000000..4f1c09c --- /dev/null +++ b/src/lib/util/obj-from-array.mts @@ -0,0 +1,8 @@ +export function objectFromArray(values: T[]): Record { + const out: Record = {} as any; + for (const v of values) { + out[v] = v; + } + + return out; +} diff --git a/src/public_api/action.d.ts b/src/public_api/action.d.ts index 1a668fd..9c1ef48 100644 --- a/src/public_api/action.d.ts +++ b/src/public_api/action.d.ts @@ -7,5 +7,5 @@ export interface ActionNodeDefinition extends NodeDefinition; + execute(data: T, executionContext?: WorkflowExecutionCtx): void | ObservableInput; } diff --git a/src/public_api/option.d.ts b/src/public_api/option.d.ts index ebe34ae..ba83bd8 100644 --- a/src/public_api/option.d.ts +++ b/src/public_api/option.d.ts @@ -23,7 +23,7 @@ export interface StringNodeOption extends NodeOptionBase { * Render a ``. * key = model value, value = display label */ - enum?: DynamicOption | undefined>; + enum?: DynamicOption>; /** The `placeholder` attribute for the `` element */ placeholder?: string; diff --git a/src/ui/ui.mts b/src/ui/ui.mts index 6f012c7..d333afb 100644 --- a/src/ui/ui.mts +++ b/src/ui/ui.mts @@ -15,10 +15,10 @@ interface BaseCat { itemID: string; } +export const mainIcon = githubAsset('src/ui/assets/icon.png', '0.5.0'); + // Init package { - const mainIcon = githubAsset('src/ui/assets/icon.png', '0.5.0'); - const cat = (config?: T): T & BaseCat => ({ categoryID: '', itemID: startCase(namespace),