import { InputSpec } from './input/builder' import { ExtractInputSpecType } from './input/builder/inputSpec' import * as T from '../types' import { once } from '../util' import { InitScript } from '../inits' import { Parser } from 'ts-matches' type MaybeInputSpec = {} extends Type ? null : InputSpec export type Run> = (options: { effects: T.Effects input: A spec: T.inputSpecTypes.InputSpec }) => Promise<(T.ActionResult & { version: '1' }) | null | void | undefined> export type GetInput> = (options: { effects: T.Effects }) => Promise> 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 interface ActionInfo< Id extends T.ActionId, Type extends Record, > { readonly id: Id readonly _INPUT: Type } export class Action> implements ActionInfo { readonly _INPUT: Type = null as any as Type private prevInputSpec: Record< string, { spec: T.inputSpecTypes.InputSpec; validator: Parser } > = {} private constructor( readonly id: Id, private readonly metadataFn: MaybeFn, private readonly inputSpec: MaybeInputSpec, private readonly getInputFn: GetInput, private readonly runFn: Run, ) {} static withInput< Id extends T.ActionId, InputSpecType extends InputSpec>, >( id: Id, metadata: MaybeFn>, inputSpec: InputSpecType, getInput: GetInput>, run: Run>, ): Action> { return new Action>( id, mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })), inputSpec as any, getInput, run, ) } static withoutInput( id: Id, metadata: MaybeFn>, run: Run<{}>, ): Action { return new Action( id, mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })), null, async () => null, run, ) } async exportMetadata(options: { effects: T.Effects }): Promise { const childEffects = options.effects.child(`setupActions/${this.id}`) childEffects.constRetry = once(() => { this.exportMetadata(options) }) const metadata = await callMaybeFn(this.metadataFn, { effects: childEffects, }) await options.effects.action.export({ id: this.id, metadata }) return metadata } async getInput(options: { effects: T.Effects }): Promise { let spec = {} if (this.inputSpec) { const built = await this.inputSpec.build(options) this.prevInputSpec[options.effects.eventId!] = built spec = built.spec } return { eventId: options.effects.eventId!, spec, value: ((await this.getInputFn(options)) as | Record | null | undefined) || null, } } async run(options: { effects: T.Effects input: Type }): Promise { let spec = {} if (this.inputSpec) { const prevInputSpec = this.prevInputSpec[options.effects.eventId!] if (!prevInputSpec) { throw new Error( `getActionInput has not been called for EventID ${options.effects.eventId}`, ) } options.input = prevInputSpec.validator.unsafeCast(options.input) spec = prevInputSpec.spec } return ( (await this.runFn({ effects: options.effects, input: options.input, spec, })) ?? null ) } } export class Actions< AllActions extends Record>, > implements InitScript { private constructor(private readonly actions: AllActions) {} static of(): Actions<{}> { return new Actions({}) } addAction>( action: A, // TODO: prevent duplicates ): Actions { return new Actions({ ...this.actions, [action.id]: action }) } async init(effects: T.Effects): Promise { for (let action of Object.values(this.actions)) { const fn = async () => { let res: (value?: undefined) => void = () => {} const complete = new Promise((resolve) => { res = resolve }) const e: T.Effects = effects.child(action.id) e.constRetry = once(() => complete.then(() => fn()).catch(console.error), ) try { await action.exportMetadata({ effects: e }) } finally { res() } } await fn() } await effects.action.clear({ except: Object.keys(this.actions) }) } get(actionId: Id): AllActions[Id] { return this.actions[actionId] } }