import { Shape, Skip, Any } from 'shape' import { getx } from './basic' type PointCtx = { async: boolean log: LogEntry[] data: Record depth: number, sys: (() => { now: () => number, print: (...s: any) => void }) & { plog?: string[] } } type LogEntry = { note: string when: number depth: number args: any } abstract class Point implements Point { id: string name?: string args?: any constructor(id: string, name?: string) { this.id = id this.name = name } async runner(pctx: PointCtx): Promise { const suffix = (null == this.name || '' === this.name) ? '' : ':' + this.name this.logger(pctx, { note: this.constructor.name + ':before:' + this.id + suffix, args: this.args }) if (pctx.async) { await this.run(pctx) } else { this.run(pctx) } this.logger(pctx, { note: this.constructor.name + ':after:' + this.id + suffix, args: this.args }) } logger(pctx: PointCtx, entry: Partial) { entry.note = null == entry.note ? 'none' : entry.note entry.when = pctx.sys().now() entry.depth = pctx.depth if (undefined === entry.args) { delete entry.args } pctx.log.push(entry as LogEntry) } abstract run(pctx: PointCtx): Promise } class SerialPoint extends Point { points: Point[] constructor(id: string) { super(id) this.points = [] } add(p: Point) { this.points.push(p) } async run(pctx: PointCtx): Promise { let childctx: PointCtx = { ...pctx } childctx.depth++ for (let p of this.points) { if (pctx.async) { await p.runner(childctx) } else { p.runner(childctx) } } } } class RootPoint extends SerialPoint { points: Point[] constructor(id: string) { super(id) this.points = [] } direct(data?: Record, sys?: any): PointCtx { const pctx = this.makePointCtx(false, data, sys) this.runner(pctx) return pctx } async start(data?: Record, sys?: any): Promise { const pctx = this.makePointCtx(true, data, sys) await this.runner(pctx) return pctx } makePointCtx(async: boolean, data?: Record, sys?: any): PointCtx { const pctx: PointCtx = { async, log: [], data: data || {}, depth: 0, sys: sys || (() => ({ now: () => Date.now(), print: (...s: any) => { if (null != s[0] && 'string' === typeof s[0]) { console.dir(s[0], s[1] || { depth: null }) } else { console.log(...s) } } })) } return pctx } } class ParallelPoint extends Point { points: Point[] constructor(id: string) { super(id) this.points = [] } add(p: Point) { this.points.push(p) } async run(pctx: PointCtx): Promise { let childctx: PointCtx = { ...pctx } childctx.depth++ await Promise.all(this.points.map(p => p.runner(childctx))) } } class FuncPoint extends Point { func: (pctx: PointCtx) => any constructor(id: string, func: (pctx: PointCtx) => any) { super(id, func.name) this.func = func } async run(pctx: PointCtx): Promise { if (pctx.async) { await this.func(pctx) } else { this.func(pctx) } } } class PrintPoint extends Point { path?: string constructor(id: string, path?: string) { super(id) this.path = path } async run(pctx: PointCtx): Promise { let print = pctx.sys().print let d = pctx.data if (null != this.path) { d = getx(d, this.path) } if (null != d && 'object' === typeof d) { print('POINTCTX:') print(d) } else { print('POINTCTX: ' + d) } } } const PointDefShape = Shape({ k: Skip(String), n: Skip(String), // `p` holds child PointDefs. `Skip([])` describes the runtime check // exactly -- absent is legal, present must be an array -- but an empty // array literal infers as `never[]`, which no child can be assigned to. // The cast is the same idiom FragmentShape uses to state the intended TS // type beside the runtime one; it cannot be `PointDef[]` because PointDef // is derived from this shape. p: Skip([]) as unknown as any[], a: Any(), m: {} }) type PointDef = Partial> type MakePoint = (id: () => string, pdef: PointDef) => Point function buildPoints(pdef: PointDef, pm: Record, id?: () => string): Point { let idi = 0 id = id || (() => (++idi) + '') let p: Point | undefined = undefined // TODO: fix point kind resolution to be more extensible pdef = PointDefShape(pdef) let isSerial = 'Serial' === pdef.k || Array.isArray(pdef.p) // `k` is Skip(String), so absent is a legal PointDef. Indexing with it // yielded undefined at runtime and fell through to the Root branch below; // this says so rather than relying on JS tolerating an undefined key. const mp = null == pdef.k ? undefined : pm[pdef.k] if (null != mp) { p = mp(id, pdef) } else if (null == pdef.k || 'Root' === pdef.k) { const rp = new RootPoint(id()) const cp = pdef.p || [] for (let c of cp) { rp.add(buildPoints(c, pm, id)) } p = rp isSerial = false } else if ('Parallel' === pdef.k) { const sp = new ParallelPoint(id()) const cp = pdef.p || [] for (let c of cp) { sp.add(buildPoints(c, pm, id)) } p = sp isSerial = false } if (isSerial) { const sp = (p || new SerialPoint(id())) as SerialPoint const cp = pdef.p || [] for (let c of cp) { sp.add(buildPoints(c, pm, id)) } p = sp } if (null == p) { throw new Error('Unknown point kind: ' + JSON.stringify(pdef)) } return p } function makeFuncDef(fd: (pdef: PointDef) => (pctx: PointCtx) => any) { return (id: () => string, pdef: PointDef) => { const fp = new FuncPoint(id(), fd(pdef)) fp.args = pdef.a return fp } } export type { PointCtx, MakePoint, PointDef, } export { Point, RootPoint, SerialPoint, ParallelPoint, FuncPoint, PrintPoint, buildPoints, makeFuncDef, }