UNPKG

@grlt-hub/app-compose

Version:

<p align="center"> <a href="https://grlt-hub.github.io/app-compose/"> <img src="https://github.com/user-attachments/assets/b9f87cf8-5af1-410b-8125-28689e668d47" height="150"> </a> </p> <h1 align="center"> App Compose </h1> <p align="center"> Compose modul

339 lines (338 loc) 9.58 kB
const LIBRARY_NAME = "[app-compose]"; const UNKNOWN_NAME = "<unknown>"; const path = (value, path$1) => path$1.reduce((curr, key) => curr?.[key], value); const isObject = (x) => Object.prototype.toString.call(x) === "[object Object]"; const T = () => true; const tap = (fn) => (arg) => (fn(arg), arg); const difference = (left, right) => { const out = new Set(left); for (const item of right) out.delete(item); return out; }; const union = (left, right) => { const out = new Set(left); for (const item of right) out.add(item); return out; }; const RefID$ = Symbol("$lens.id"); const RefPath$ = Symbol("$lens.path"); const Self$ = Symbol(); const raise = () => { throw new Error(`${LIBRARY_NAME} Modifying a Reference is not allowed.`); }; const get = (target, property, recv) => typeof property == "symbol" ? property === Self$ ? target : Reflect.get(target, property, recv) : proxy({ ...target, [RefPath$]: target[RefPath$].concat(property) }); const set = (target, property, recv) => typeof property == "symbol" ? Reflect.set(target, property, recv) : raise(); const proxy = (ref) => new Proxy(ref, { get, set }); const lens = (spot, id) => proxy({ ...spot, [RefPath$]: [], [RefID$]: id }); lens.is = (ref) => RefID$ in ref; lens.modify = (ref, key, value) => proxy({ ...ref[Self$], [key]: value }); const Kind$ = Symbol("$kind"); const Optional$ = Symbol("$optional"); const Literal$ = Symbol("$literal"); const literal = (value) => ({ [Kind$]: "literal", [Literal$]: value }); const optional = (spot) => lens.is(spot) ? lens.modify(spot, Optional$, true) : { ...spot, [Optional$]: true }; const reference = (id) => { return lens({ [Kind$]: "reference", [Optional$]: false }, id); }; const Binding$ = Symbol("$binding"); const bind = (tag, value) => ({ [Binding$]: { id: tag[RefID$], value } }); const Tag$ = Symbol("$tag"); const createTag = (config = {}) => { const ref = reference(config.id ? Symbol(`Tag[${config.id}]`) : Symbol()); ref[Tag$] = true; return ref; }; const Task$ = Symbol("$task"); const createTask = (config) => { const id = { value: config.id ? Symbol(`Task[${config.id}]`) : Symbol(), status: config.id ? Symbol(`Task[${config.id}]::status`) : Symbol() }; const task = reference(id.value); const context = config.run.context === void 0 ? literal(void 0) : config.run.context; task[Task$] = { run: config.run.fn, enabled: config.enabled, context, id }; return task; }; const status = (task) => reference(task[Task$].id.status); const createCompiler = (repo) => { const read = (spot) => { switch (spot[Kind$]) { case "literal": return spot[Literal$]; case "reference": return path(repo.get(spot[RefID$]), spot[RefPath$]); default: } }; const record = (shape) => { const out = {}; for (const key of Object.keys(shape)) out[key] = anything(shape[key]); return out; }; const anything = (thing) => { if (isObject(thing)) if (Kind$ in thing) return read(thing); else return record(thing); else if (Array.isArray(thing)) return thing.map(anything); else throw new Error(`${LIBRARY_NAME} Literal value found in context: ${String(thing)}.`); }; return { build: anything }; }; const createDispatch = (registry) => { const binding = ({ id }) => (result) => { switch (result.status) { case "done": registry.set(id, result.value); return; case "skip": case "fail": return; } }; const task = ({ id }) => (result) => { switch (result.status) { case "done": registry.set(id.value, result.value); registry.set(id.status, { name: "done" }); return; case "skip": registry.set(id.status, { name: "skip" }); return; case "fail": registry.set(id.status, { name: "fail", error: result.error }); return; } }; return { task, binding }; }; const NameMap = { task: "Task", binding: "Binding" }; const notify = { duplicate: ({ type, name, index }) => { const message = `${LIBRARY_NAME} A duplicate ${NameMap[type]} found with ID: ${name} on stage #${index + 1}.`; throw new Error(message); }, notSatisfied: ({ type, name, index, missing: set$1 }) => { const list = Array.from(set$1).map((id) => id.description ?? UNKNOWN_NAME).join(", "); const message = `${LIBRARY_NAME} Unsatisfied dependencies found for ${NameMap[type]} with ID: ${name} on stage #${index + 1}: missing ${list}.`; throw new Error(message); } }; const createGuard = (resolver) => { const toID = (step) => { switch (true) { case Task$ in step: return { type: "task", name: step[Task$].id.value.description ?? UNKNOWN_NAME, writes: [step[Task$].id.value, step[Task$].id.status] }; case Binding$ in step: return { type: "binding", name: step[Binding$].id.description ?? UNKNOWN_NAME, writes: [step[Binding$].id] }; default: throw new Error(`${LIBRARY_NAME} Unknown step type found: ${String(step)}.`); } }; const toContext = (step) => { switch (true) { case Task$ in step: return step[Task$].context; case Binding$ in step: return step[Binding$].value; default: throw new Error(`${LIBRARY_NAME} Unknown step type found: ${String(step)}.`); } }; return (stages) => { let registry = /* @__PURE__ */ new Set(); for (const [index, stage] of stages.entries()) { const willCompute = /* @__PURE__ */ new Set(); for (const step of stage) { const { type, name, writes } = toID(step); const context = toContext(step); const dependencies = resolver.dependenciesOf(context); if (writes.some((id) => registry.has(id) || willCompute.has(id))) notify.duplicate({ type, name, index }); const missing = difference(dependencies.required, registry); if (missing.size > 0) notify.notSatisfied({ type, name, index, missing }); writes.forEach((id) => willCompute.add(id)); } registry = union(registry, willCompute); } }; }; const fallback = { onTaskFail: ({ id, error }) => { const name = id.description ?? UNKNOWN_NAME; console.warn(`${LIBRARY_NAME} A Task with ID: ${name} has failed to run.`, error); }, onTaskSkip: ({ id, reason }) => {} }; const createLogger = (config = {}) => { const { onTaskFail = fallback.onTaskFail } = config; const task = (task$1) => (result) => { switch (result.status) { case "fail": onTaskFail?.({ id: task$1.id.value, error: result.error }); return; } }; return { task }; }; function* flatten(thing) { if (isObject(thing)) if (Kind$ in thing) yield thing; else for (const key of Object.keys(thing)) yield* flatten(thing[key]); else if (Array.isArray(thing)) for (const item of thing) yield* flatten(item); else throw new Error(`${LIBRARY_NAME} Literal value found in context: ${String(thing)}.`); } const resolve = (context) => { const required = /* @__PURE__ */ new Set(); const optional$1 = /* @__PURE__ */ new Set(); for (const spot of flatten(context)) switch (spot[Kind$]) { case "reference": if (spot[Optional$]) optional$1.add(spot[RefID$]); else required.add(spot[RefID$]); } return { required, optional: difference(optional$1, required) }; }; const createResolver = (repo) => { const deps = /* @__PURE__ */ new Map(); const dependenciesOf = (context) => { if (!deps.has(context)) deps.set(context, resolve(context)); return deps.get(context); }; const satisfies = (context) => { return Array.from(dependenciesOf(context).required).every((id) => repo.has(id)); }; return { dependenciesOf, satisfies }; }; const createRunner = ({ compiler, resolver }) => { const binding = async (binding$1) => { if (!resolver.satisfies(binding$1.value)) return { status: "skip" }; return { status: "done", value: compiler.build(binding$1.value) }; }; const task = async (task$1) => { if (!resolver.satisfies(task$1.context)) return { status: "skip" }; const context = compiler.build(task$1.context); try { if (!await (task$1.enabled ?? T)(context)) return { status: "skip" }; } catch (error) { return { status: "fail", error }; } try { return { status: "done", value: await task$1.run(context) }; } catch (error) { return { status: "fail", error }; } }; return { binding, task }; }; const run = async (config, stages) => { const registry = /* @__PURE__ */ new Map(); const resolver = createResolver(registry); const compiler = createCompiler(registry); const dispatch = createDispatch(registry); const logger = createLogger(config.log); createGuard(resolver)(stages); const run$1 = createRunner({ compiler, resolver }); const execute = async (step) => { switch (true) { case Task$ in step: { const task = step[Task$]; return run$1.task(task).then(tap(logger.task(task))).then(dispatch.task(task)); } case Binding$ in step: { const binding = step[Binding$]; return run$1.binding(binding).then(dispatch.binding(binding)); } default: throw new Error(`${LIBRARY_NAME} Unknown step type found: ${String(step)}.`); } }; for (const stage of stages) { const queue = stage.map(execute); await Promise.all(queue); } return { get: (task) => registry.get(task[RefID$]) }; }; const compose = (config = {}) => { const stages = []; const composer = { stage: (...input) => (stages.push(...input), composer), run: () => run(config, stages), guard: () => null }; return composer; }; exports.bind = bind; exports.compose = compose; exports.createTag = createTag; exports.createTask = createTask; exports.literal = literal; exports.optional = optional; exports.status = status;