mobx-keystone-mindreframer
Version:
A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more
386 lines (350 loc) • 10.3 kB
text/typescript
import type { O } from "ts-toolbelt"
import { checkModelDecoratorArgs } from "../modelShared/checkModelDecoratorArgs"
import { decorateWrapMethodOrField, failure } from "../utils"
import { getActionNameAndContextOverride } from "./actionDecoratorUtils"
import { ActionContext, ActionContextActionType, ActionContextAsyncStepType } from "./context"
import { wrapInAction, WrapInActionOverrideContextFn } from "./wrapInAction"
const modelFlowSymbol = Symbol("modelFlow")
/**
* @ignore
* @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>(function (resolve, reject) {
function onFulfilled(res: any): void {
let ret
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: any): void {
let ret
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
}
/**
* @ignore
* @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: any) {
return typeof fn === "function" && fn[modelFlowSymbol]
}
/**
* Decorator that turns a function generator into a model flow.
*
* @param target
* @param propertyKey
* @param [baseDescriptor]
* @returns
*/
export function modelFlow(
target: any,
propertyKey: string,
baseDescriptor?: PropertyDescriptor
): void {
const { actionName, overrideContext } = getActionNameAndContextOverride(target, propertyKey)
return decorateWrapMethodOrField(
"modelFlow",
{
target,
propertyKey,
baseDescriptor,
},
(data, fn) => {
if (isModelFlow(fn)) {
return fn
} else {
checkModelFlowArgs(data.target, data.propertyKey, fn)
return flow({ nameOrNameFn: actionName, generator: fn, overrideContext })
}
}
)
}
function checkModelFlowArgs(target: any, propertyKey: string, value: any) {
if (typeof value !== "function") {
throw failure("modelFlow has to be used over functions")
}
checkModelDecoratorArgs("modelFlow", target, propertyKey)
}
/**
* Tricks the TS compiler into thinking that a model flow generator function can be awaited
* (is a promise).
*
* @typeparam A Function arguments.
* @typeparam 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*.
*
* @typeparam T Promise return type.
* @param promise Promise.
* @returns
*/
export function _await<T>(promise: Promise<T>): Generator<Promise<T>, T, unknown> {
return promiseGenerator.call(promise)
}
/*
function* promiseGenerator<T>(
this: Promise<T>
) {
const ret: T = yield this
return ret
}
*/
// above code but compiled by TS for ES5
// so we don't include a dependency to regenerator runtime
const __generator = function (thisArg: any, body: any) {
let _: any = {
label: 0,
sent: function () {
if (t[0] & 1) throw t[1]
return t[1]
},
trys: [],
ops: [],
},
f: any,
y: any,
t: any,
g: any
return (
(g = { next: verb(0), throw: verb(1), return: verb(2) }),
typeof Symbol === "function" &&
(g[Symbol.iterator] = function () {
return this
}),
g
)
function verb(n: any) {
return function (v: any) {
return step([n, v])
}
}
function step(op: any) {
if (f) throw new TypeError("Generator is already executing.")
while (_)
try {
if (
((f = 1),
y &&
(t =
op[0] & 2
? y["return"]
: op[0]
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
: y.next) &&
!(t = t.call(y, op[1])).done)
)
return t
if (((y = 0), t)) op = [op[0] & 2, t.value]
switch (op[0]) {
case 0:
case 1:
t = op
break
case 4:
_.label++
return { value: op[1], done: false }
case 5:
_.label++
y = op[1]
op = [0]
continue
case 7:
op = _.ops.pop()
_.trys.pop()
continue
default:
if (
!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) &&
(op[0] === 6 || op[0] === 2)
) {
_ = 0
continue
}
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
_.label = op[1]
break
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1]
t = op
break
}
if (t && _.label < t[2]) {
_.label = t[2]
_.ops.push(op)
break
}
if (t[2]) _.ops.pop()
_.trys.pop()
continue
}
op = body.call(thisArg, _)
} catch (e) {
op = [6, e]
y = 0
} finally {
f = t = 0
}
if (op[0] & 5) throw op[1]
return { value: op[0] ? op[1] : void 0, done: true }
}
}
function promiseGenerator(this: Promise<any>) {
let ret
return __generator(this, function (this: any, _a: any) {
switch (_a.label) {
case 0:
return [4 /*yield*/, this]
case 1:
ret = _a.sent()
return [2 /*return*/, ret]
default:
return
}
})
}