import { InputSpec } from "./input/builder" import { ExtractInputSpecType, ExtractPartialInputSpecType, } from "./input/builder/inputSpec" import * as T from "../types" import { once } from "../util" export type Run< A extends | Record | InputSpec, any> | InputSpec, never>, > = (options: { effects: T.Effects input: ExtractInputSpecType & Record }) => Promise export type GetInput< A extends | Record | InputSpec, any> | InputSpec, never>, > = (options: { effects: T.Effects }) => Promise< | null | void | undefined | (ExtractPartialInputSpecType & Record) > export type MaybeFn = T | ((options: { effects: T.Effects }) => Promise) function callMaybeFn( maybeFn: MaybeFn, options: { effects: T.Effects }, ): Promise { if (maybeFn instanceof Function) { return maybeFn(options) } else { return Promise.resolve(maybeFn) } } function mapMaybeFn( maybeFn: MaybeFn, map: (value: T) => U, ): MaybeFn { if (maybeFn instanceof Function) { return async (...args) => map(await maybeFn(...args)) } else { return map(maybeFn) } } export class Action< Id extends T.ActionId, Store, InputSpecType extends | Record | InputSpec | InputSpec, > { private constructor( readonly id: Id, private readonly metadataFn: MaybeFn, private readonly inputSpec: InputSpecType, private readonly getInputFn: GetInput>, private readonly runFn: Run>, ) {} static withInput< Id extends T.ActionId, Store, InputSpecType extends | Record | InputSpec | InputSpec, >( id: Id, metadata: MaybeFn>, inputSpec: InputSpecType, getInput: GetInput>, run: Run>, ): Action { return new Action( id, mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })), inputSpec, getInput, run, ) } static withoutInput( id: Id, metadata: MaybeFn>, run: Run<{}>, ): Action { return new Action( id, mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })), {}, async () => null, run, ) } async exportMetadata(options: { effects: T.Effects }): Promise { const metadata = await callMaybeFn(this.metadataFn, options) await options.effects.action.export({ id: this.id, metadata }) return metadata } async getInput(options: { effects: T.Effects }): Promise { return { spec: await this.inputSpec.build(options), value: (await this.getInputFn(options)) || null, } } async run(options: { effects: T.Effects input: ExtractInputSpecType }): Promise { return (await this.runFn(options)) || null } } export class Actions< Store, AllActions extends Record>, > { private constructor(private readonly actions: AllActions) {} static of(): Actions { return new Actions({}) } addAction>( action: A, ): Actions { return new Actions({ ...this.actions, [action.id]: action }) } async update(options: { effects: T.Effects }): Promise { options.effects = { ...options.effects, constRetry: once(() => { this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem }), } for (let action of Object.values(this.actions)) { await action.exportMetadata(options) } await options.effects.action.clear({ except: Object.keys(this.actions) }) return null } get(actionId: Id): AllActions[Id] { return this.actions[actionId] } }