@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
JavaScript
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;