UNPKG

mobx-keystone

Version:

A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more

239 lines (209 loc) 6.97 kB
import type { O } from "ts-toolbelt" import { failure } from "../utils" import { ActionContext, ActionContextActionType, ActionContextAsyncStepType } from "./context" import { WrapInActionOverrideContextFn, wrapInAction } from "./wrapInAction" import { decorateWrapMethodOrField } from "../utils/decorators" import { promiseGenerator } from "./modelFlowPromiseGenerator" const modelFlowSymbol = Symbol("modelFlow") /** * @internal */ export function flow<R, Args extends any[]>({ nameOrNameFn, generator, overrideContext, }: { nameOrNameFn: string | (() => string) generator: (...args: Args) => IterableIterator<any> overrideContext?: WrapInActionOverrideContextFn }): (...args: Args) => Promise<any> { // Implementation based on https://github.com/tj/co/blob/master/index.js const flowFn = function (this: any, ...args: any[]) { const name = typeof nameOrNameFn === "function" ? nameOrNameFn() : nameOrNameFn const target = this let previousAsyncStepContext: ActionContext | undefined const ctxOverride = (stepType: ActionContextAsyncStepType): WrapInActionOverrideContextFn => { return (ctx: O.Writable<ActionContext>, self) => { if (overrideContext) { overrideContext(ctx, self) } ctx.previousAsyncStepContext = previousAsyncStepContext ctx.spawnAsyncStepContext = previousAsyncStepContext ? previousAsyncStepContext.spawnAsyncStepContext : ctx ctx.asyncStepType = stepType ctx.args = args previousAsyncStepContext = ctx } } let generatorRun = false const gen = wrapInAction({ nameOrNameFn: name, fn: () => { generatorRun = true return generator.apply(target, args as Args) }, actionType: ActionContextActionType.Async, overrideContext: ctxOverride(ActionContextAsyncStepType.Spawn), }).apply(target) if (!generatorRun) { // maybe it got overridden into a sync action return gen instanceof Promise ? gen : Promise.resolve(gen) } // use bound functions to fix es6 compilation const genNext = gen.next.bind(gen) const genThrow = gen.throw!.bind(gen) const promise = new Promise<R>((resolve, reject) => { function onFulfilled(res: any): void { let ret: unknown try { ret = wrapInAction({ nameOrNameFn: name, fn: genNext, actionType: ActionContextActionType.Async, overrideContext: ctxOverride(ActionContextAsyncStepType.Resume), }).call(target, res) } catch (e) { wrapInAction({ nameOrNameFn: name, fn: (err: any) => { // we use a flow finisher to allow middlewares to tweak the return value before resolution return { value: err, resolution: "reject", accepter: resolve, rejecter: reject, } as FlowFinisher }, actionType: ActionContextActionType.Async, overrideContext: ctxOverride(ActionContextAsyncStepType.Throw), isFlowFinisher: true, }).call(target, e) return } next(ret) } function onRejected(err: unknown): void { let ret: unknown try { ret = wrapInAction({ nameOrNameFn: name, fn: genThrow, actionType: ActionContextActionType.Async, overrideContext: ctxOverride(ActionContextAsyncStepType.ResumeError), }).call(target, err) } catch (e) { wrapInAction({ nameOrNameFn: name, fn: (err: any) => { // we use a flow finisher to allow middlewares to tweak the return value before resolution return { value: err, resolution: "reject", accepter: resolve, rejecter: reject, } as FlowFinisher }, actionType: ActionContextActionType.Async, overrideContext: ctxOverride(ActionContextAsyncStepType.Throw), isFlowFinisher: true, }).call(target, e) return } next(ret) } function next(ret: any): void { if (ret && typeof ret.then === "function") { // an async iterator ret.then(next, reject) } else if (ret.done) { // done wrapInAction({ nameOrNameFn: name, fn: (val: any) => { // we use a flow finisher to allow middlewares to tweak the return value before resolution return { value: val, resolution: "accept", accepter: resolve, rejecter: reject, } as FlowFinisher }, actionType: ActionContextActionType.Async, overrideContext: ctxOverride(ActionContextAsyncStepType.Return), isFlowFinisher: true, }).call(target, ret.value) } else { // continue Promise.resolve(ret.value).then(onFulfilled, onRejected) } } onFulfilled(undefined) // kick off the process }) return promise } ;(flowFn as any)[modelFlowSymbol] = true return flowFn } /** * @internal */ export interface FlowFinisher { value: any resolution: "accept" | "reject" accepter(value: any): void rejecter(value: any): void } /** * Returns if the given function is a model flow or not. * * @param fn Function to check. * @returns */ export function isModelFlow(fn: unknown) { return typeof fn === "function" && modelFlowSymbol in fn } /** * Decorator that turns a function generator into a model flow. */ export function modelFlow(...args: any[]): void { // biome-ignore lint/correctness/noVoidTypeReturn: proper way to declare a decorator return decorateWrapMethodOrField("modelFlow", args, (data, fn) => { if (isModelFlow(fn)) { return fn } else { if (typeof fn !== "function") { throw failure("modelFlow has to be used over functions") } return flow({ nameOrNameFn: data.actionName, generator: fn, overrideContext: data.overrideContext, }) } }) } /** * Tricks the TS compiler into thinking that a model flow generator function can be awaited * (is a promise). * * @template A Function arguments. * @template R Return value. * @param fn Flow function. * @returns */ export function _async<A extends any[], R>( fn: (...args: A) => Generator<any, R, any> ): (...args: A) => Promise<R> { return fn as any } /** * Makes a promise a flow, so it can be awaited with yield*. * * @template T Promise return type. * @param promise Promise. * @returns */ export function _await<T>(promise: Promise<T>): Generator<Promise<T>, T, unknown> { return promiseGenerator.call(promise) }