mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
Move the Engine band, extension host, fixtures and shared test support into codewhale-engine; wire TUI/CLI to that owner. Preserve human-decision construction boundaries and lower reusable test helpers. Remove unused copied dependencies and repair path-based guards, scripts and crate maps. Correct the blocking scanner external-module scope leak; raised counts equal the corrected pre-move source. Refresh embedded built-in hashes after path-only bundle regeneration. Restore the provider-free prompt budget to the exact pre-b984e31290 macOS bytes and representative identities; tool catalogs are unchanged. Portable prompt-fixture pinning remains follow-up work. Local validation: Engine/TUI/CLI clippy --tests --no-deps -D warnings passed with the existing allow-list; production CLI check passed; fmt passed. Focused native run: 705 passed / 83 failed initially; after hash/path repairs, 81 affected cases plus 2 existing build-relocation cases passed (83/83). Thus 786/788 original cases pass; 2 compiled-release-image cases lack input and are not qualified. Frozen input inventories unchanged. macOS test-link unwind-table warnings remain. Four metric tests pass; all 55 budgets pass. Host Node: 413 pass, 7 skip; Bun: 414 pass, 6 skip. Existing scanner checks: 34 + 33 pass. Compiled-delivery synthetic fixtures: 7 pass. Web: 42 pass. Import, blocking, dead-code, coarse-map, version and facts checks pass. CW-7701. No hosted CI, provider call, release build or publication. Stage A part 2 and the old App Server/ratatui dependencies remain separate.
2018 lines
70 KiB
JavaScript
2018 lines
70 KiB
JavaScript
// generated by crates/engine/extension-host/build.mjs from src/shims/skills.ts; do not edit.
|
|
|
|
// node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
function isNullable(value) {
|
|
return value === null || value === void 0;
|
|
}
|
|
function defineProperty(object, key, value) {
|
|
return Object.defineProperty(object, key, {
|
|
writable: true,
|
|
value,
|
|
enumerable: false
|
|
});
|
|
}
|
|
function is(type, value) {
|
|
if (arguments.length === 1) return (value2) => is(type, value2);
|
|
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
}
|
|
function isArrayBufferLike(value) {
|
|
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
}
|
|
function isArrayBufferSource(value) {
|
|
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
}
|
|
var Binary;
|
|
(function(Binary2) {
|
|
Binary2.is = isArrayBufferLike;
|
|
Binary2.isSource = isArrayBufferSource;
|
|
function fromSource(source) {
|
|
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
else return source;
|
|
}
|
|
Binary2.fromSource = fromSource;
|
|
function toBase64(source) {
|
|
source = fromSource(source);
|
|
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
let binary = "";
|
|
const bytes = new Uint8Array(source);
|
|
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
return btoa(binary);
|
|
}
|
|
Binary2.toBase64 = toBase64;
|
|
function fromBase64(source) {
|
|
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
}
|
|
Binary2.fromBase64 = fromBase64;
|
|
function toHex(source) {
|
|
source = fromSource(source);
|
|
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
Binary2.toHex = toHex;
|
|
function fromHex(source) {
|
|
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
const buffer = [];
|
|
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
return Uint8Array.from(buffer).buffer;
|
|
}
|
|
Binary2.fromHex = fromHex;
|
|
})(Binary || (Binary = {}));
|
|
var base64ToArrayBuffer = Binary.fromBase64;
|
|
var arrayBufferToBase64 = Binary.toBase64;
|
|
var hexToArrayBuffer = Binary.fromHex;
|
|
var arrayBufferToHex = Binary.toHex;
|
|
function tokenize(source, delimiters, delimiter) {
|
|
const output = [];
|
|
let state = 0;
|
|
for (let i = 0; i < source.length; i++) {
|
|
const code = source.charCodeAt(i);
|
|
if (code >= 65 && code <= 90) {
|
|
if (state === 1) {
|
|
const next = source.charCodeAt(i + 1);
|
|
if (next >= 97 && next <= 122) output.push(delimiter);
|
|
output.push(code + 32);
|
|
} else {
|
|
if (state !== 0) output.push(delimiter);
|
|
output.push(code + 32);
|
|
}
|
|
state = 1;
|
|
} else if (code >= 97 && code <= 122) {
|
|
output.push(code);
|
|
state = 2;
|
|
} else if (delimiters.includes(code)) {
|
|
if (state !== 0) output.push(delimiter);
|
|
state = 0;
|
|
} else output.push(code);
|
|
}
|
|
return String.fromCharCode(...output);
|
|
}
|
|
function paramCase(source) {
|
|
return tokenize(source, [45, 95], 45);
|
|
}
|
|
var hyphenate = paramCase;
|
|
var Time;
|
|
(function(Time2) {
|
|
Time2.millisecond = 1;
|
|
Time2.second = 1e3;
|
|
Time2.minute = Time2.second * 60;
|
|
Time2.hour = Time2.minute * 60;
|
|
Time2.day = Time2.hour * 24;
|
|
Time2.week = Time2.day * 7;
|
|
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
function setTimezoneOffset(offset) {
|
|
timezoneOffset = offset;
|
|
}
|
|
Time2.setTimezoneOffset = setTimezoneOffset;
|
|
function getTimezoneOffset() {
|
|
return timezoneOffset;
|
|
}
|
|
Time2.getTimezoneOffset = getTimezoneOffset;
|
|
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
if (typeof date === "number") date = new Date(date);
|
|
if (offset === void 0) offset = timezoneOffset;
|
|
return Math.floor((date.valueOf() / Time2.minute - offset) / 1440);
|
|
}
|
|
Time2.getDateNumber = getDateNumber;
|
|
function fromDateNumber(value, offset) {
|
|
const date = new Date(value * Time2.day);
|
|
if (offset === void 0) offset = timezoneOffset;
|
|
return new Date(+date + offset * Time2.minute);
|
|
}
|
|
Time2.fromDateNumber = fromDateNumber;
|
|
const numeric = /\d+(?:\.\d+)?/.source;
|
|
const timeRegExp = new RegExp(`^${[
|
|
"w(?:eek(?:s)?)?",
|
|
"d(?:ay(?:s)?)?",
|
|
"h(?:our(?:s)?)?",
|
|
"m(?:in(?:ute)?(?:s)?)?",
|
|
"s(?:ec(?:ond)?(?:s)?)?"
|
|
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
function parseTime(source) {
|
|
const capture = timeRegExp.exec(source);
|
|
if (!capture) return 0;
|
|
return (parseFloat(capture[1]) * Time2.week || 0) + (parseFloat(capture[2]) * Time2.day || 0) + (parseFloat(capture[3]) * Time2.hour || 0) + (parseFloat(capture[4]) * Time2.minute || 0) + (parseFloat(capture[5]) * Time2.second || 0);
|
|
}
|
|
Time2.parseTime = parseTime;
|
|
function parseDate(date) {
|
|
const parsed = parseTime(date);
|
|
if (parsed) date = Date.now() + parsed;
|
|
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
}
|
|
Time2.parseDate = parseDate;
|
|
function format(ms) {
|
|
const abs = Math.abs(ms);
|
|
if (abs >= Time2.day - Time2.hour / 2) return Math.round(ms / Time2.day) + "d";
|
|
else if (abs >= Time2.hour - Time2.minute / 2) return Math.round(ms / Time2.hour) + "h";
|
|
else if (abs >= Time2.minute - Time2.second / 2) return Math.round(ms / Time2.minute) + "m";
|
|
else if (abs >= Time2.second) return Math.round(ms / Time2.second) + "s";
|
|
return ms + "ms";
|
|
}
|
|
Time2.format = format;
|
|
function toDigits(source, length = 2) {
|
|
return source.toString().padStart(length, "0");
|
|
}
|
|
Time2.toDigits = toDigits;
|
|
function template(template2, time = /* @__PURE__ */ new Date()) {
|
|
return template2.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
}
|
|
Time2.template = template;
|
|
})(Time || (Time = {}));
|
|
|
|
// node_modules/@deepseek-ai/cordis/lib/index.js
|
|
var DisposableList = class {
|
|
sn = 0;
|
|
map = /* @__PURE__ */ new Map();
|
|
weak = /* @__PURE__ */ new WeakMap();
|
|
get length() {
|
|
return this.map.size;
|
|
}
|
|
push(value) {
|
|
const sn = ++this.sn;
|
|
this.map.set(sn, value);
|
|
this.weak.set(value, sn);
|
|
return () => this.map.delete(sn);
|
|
}
|
|
delete(value) {
|
|
const sn = this.weak.get(value);
|
|
if (!sn) return false;
|
|
return this.map.delete(sn);
|
|
}
|
|
clear() {
|
|
const values = [...this.map.values()];
|
|
this.map.clear();
|
|
return values.reverse();
|
|
}
|
|
[Symbol.iterator]() {
|
|
return this.map.values();
|
|
}
|
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
return [...this];
|
|
}
|
|
};
|
|
var symbols = {
|
|
shadow: /* @__PURE__ */ Symbol.for("cordis.shadow"),
|
|
receiver: /* @__PURE__ */ Symbol.for("cordis.receiver"),
|
|
original: /* @__PURE__ */ Symbol.for("cordis.original"),
|
|
metadata: /* @__PURE__ */ Symbol.for("cordis.metadata"),
|
|
initHooks: /* @__PURE__ */ Symbol.for("cordis.initHooks"),
|
|
checkProto: /* @__PURE__ */ Symbol.for("cordis.checkProto"),
|
|
effect: /* @__PURE__ */ Symbol.for("cordis.effect"),
|
|
filter: /* @__PURE__ */ Symbol.for("cordis.filter"),
|
|
isolate: /* @__PURE__ */ Symbol.for("cordis.isolate"),
|
|
intercept: /* @__PURE__ */ Symbol.for("cordis.intercept"),
|
|
init: /* @__PURE__ */ Symbol.for("cordis.init"),
|
|
check: /* @__PURE__ */ Symbol.for("cordis.check"),
|
|
config: /* @__PURE__ */ Symbol.for("cordis.config"),
|
|
invoke: /* @__PURE__ */ Symbol.for("cordis.invoke"),
|
|
extend: /* @__PURE__ */ Symbol.for("cordis.extend"),
|
|
tracker: /* @__PURE__ */ Symbol.for("cordis.tracker"),
|
|
resolveConfig: /* @__PURE__ */ Symbol.for("cordis.resolveConfig")
|
|
};
|
|
var GeneratorFunction = function* () {
|
|
}.constructor;
|
|
var AsyncGeneratorFunction = async function* () {
|
|
}.constructor;
|
|
function isConstructor(func) {
|
|
if (!func.prototype) return false;
|
|
if (func instanceof GeneratorFunction) return false;
|
|
if (AsyncGeneratorFunction !== Function && func instanceof AsyncGeneratorFunction) return false;
|
|
return true;
|
|
}
|
|
function joinPrototype(proto1, proto2) {
|
|
if (proto1 === Object.prototype) return proto2;
|
|
const result = Object.create(joinPrototype(Object.getPrototypeOf(proto1), proto2));
|
|
for (const key of Reflect.ownKeys(proto1)) Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(proto1, key));
|
|
return result;
|
|
}
|
|
function isObject(value) {
|
|
return value && (typeof value === "object" || typeof value === "function");
|
|
}
|
|
function getPropertyDescriptor(target, prop) {
|
|
let proto = target;
|
|
while (proto) {
|
|
const desc = Reflect.getOwnPropertyDescriptor(proto, prop);
|
|
if (desc) return desc;
|
|
proto = Object.getPrototypeOf(proto);
|
|
}
|
|
}
|
|
function getTraceable(ctx, value) {
|
|
if (!isObject(value)) return value;
|
|
if (Object.hasOwn(value, symbols.shadow)) return Object.getPrototypeOf(value);
|
|
const tracker = value[symbols.tracker];
|
|
if (!tracker) return value;
|
|
return createTraceable(ctx, value, tracker);
|
|
}
|
|
function withProps(target, props) {
|
|
if (!props) return target;
|
|
return new Proxy(target, {
|
|
get: (target2, prop, receiver) => {
|
|
if (prop in props && prop !== "constructor") return Reflect.get(props, prop, receiver);
|
|
return Reflect.get(target2, prop, receiver);
|
|
},
|
|
set: (target2, prop, value, receiver) => {
|
|
if (prop in props && prop !== "constructor") return Reflect.set(props, prop, value, receiver);
|
|
return Reflect.set(target2, prop, value, receiver);
|
|
}
|
|
});
|
|
}
|
|
function withProp(target, prop, value) {
|
|
return withProps(target, Object.defineProperty(/* @__PURE__ */ Object.create(null), prop, {
|
|
value,
|
|
writable: false
|
|
}));
|
|
}
|
|
function createShadow(ctx, target, property, receiver) {
|
|
if (!property) return receiver;
|
|
const origin = Reflect.getOwnPropertyDescriptor(target, property)?.value;
|
|
if (!origin) return receiver;
|
|
return withProp(receiver, property, ctx.extend({ [symbols.shadow]: origin }));
|
|
}
|
|
function createShadowMethod(ctx, value, outer, shadow) {
|
|
return new Proxy(value, { apply: (target, thisArg, args) => {
|
|
if (thisArg === outer) thisArg = shadow;
|
|
return getTraceable(ctx, Reflect.apply(target, thisArg, args));
|
|
} });
|
|
}
|
|
function createTraceable(ctx, value, tracker) {
|
|
if (ctx[symbols.shadow] && !tracker.noShadow) ctx = Object.getPrototypeOf(ctx);
|
|
const proxy = new Proxy(value, {
|
|
get: (target, prop, receiver) => {
|
|
if (prop === symbols.original) return target;
|
|
if (prop === tracker.property) return ctx;
|
|
if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
|
|
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) return Reflect.get(ctx, `${tracker.associate}.${prop}`, withProp(ctx, symbols.receiver, receiver));
|
|
let shadow, innerValue;
|
|
const desc = getPropertyDescriptor(target, prop);
|
|
if (desc && "value" in desc) innerValue = desc.value;
|
|
else {
|
|
shadow = createShadow(ctx, target, tracker.property, receiver);
|
|
innerValue = Reflect.get(target, prop, shadow);
|
|
}
|
|
const innerTracker = innerValue?.[symbols.tracker];
|
|
if (innerTracker) return createTraceable(ctx, innerValue, innerTracker);
|
|
else if (!tracker.noShadow && typeof innerValue === "function") {
|
|
shadow ??= createShadow(ctx, target, tracker.property, receiver);
|
|
return createShadowMethod(ctx, innerValue, receiver, shadow);
|
|
} else return innerValue;
|
|
},
|
|
set: (target, prop, value2, receiver) => {
|
|
if (prop === symbols.original) return false;
|
|
if (prop === tracker.property) return false;
|
|
if (typeof prop === "symbol") return Reflect.set(target, prop, value2, receiver);
|
|
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) return Reflect.set(ctx, `${tracker.associate}.${prop}`, value2, withProp(ctx, symbols.receiver, receiver));
|
|
const shadow = createShadow(ctx, target, tracker.property, receiver);
|
|
return Reflect.set(target, prop, value2, shadow);
|
|
},
|
|
apply: (target, thisArg, args) => {
|
|
return applyTraceable(proxy, target, thisArg, args);
|
|
}
|
|
});
|
|
return proxy;
|
|
}
|
|
function applyTraceable(proxy, value, thisArg, args) {
|
|
if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args);
|
|
return value[symbols.invoke].apply(proxy, args);
|
|
}
|
|
function createCallable(name, proto, tracker) {
|
|
const self = function(...args) {
|
|
return applyTraceable(createTraceable(self["ctx"], self, tracker), self, this, args);
|
|
};
|
|
defineProperty(self, "name", name);
|
|
return Object.setPrototypeOf(self, proto);
|
|
}
|
|
function handleError(info, reason, getOuterStack) {
|
|
const innerLines = info.error.stack.split("\n");
|
|
if (typeof reason?.stack !== "string") {
|
|
const outerError = new Error(reason);
|
|
const lines2 = outerError.stack.split("\n");
|
|
lines2.splice(1, Infinity, ...getOuterStack());
|
|
outerError.stack = lines2.join("\n");
|
|
throw outerError;
|
|
}
|
|
const lines = reason.stack.split("\n");
|
|
let index = lines.indexOf(innerLines[2]);
|
|
if (index === -1) throw reason;
|
|
index -= info.offset;
|
|
while (index > 0) {
|
|
if (!lines[index - 1].endsWith(" (<anonymous>)")) break;
|
|
index -= 1;
|
|
}
|
|
lines.splice(index, Infinity, ...getOuterStack());
|
|
reason.stack = lines.join("\n");
|
|
throw reason;
|
|
}
|
|
function composeError(callback, getOuterStack = buildOuterStack()) {
|
|
const info = {
|
|
offset: 1,
|
|
error: /* @__PURE__ */ new Error()
|
|
};
|
|
try {
|
|
const result = callback(info);
|
|
if (isObject(result) && "then" in result) return result.then(void 0, (reason) => handleError(info, reason, getOuterStack));
|
|
else return result;
|
|
} catch (reason) {
|
|
handleError(info, reason, getOuterStack);
|
|
}
|
|
}
|
|
function buildOuterStack(offset = 0) {
|
|
const outerError = /* @__PURE__ */ new Error();
|
|
return () => outerError.stack.split("\n").slice(3 + offset);
|
|
}
|
|
function isBailed(value) {
|
|
return value !== null && value !== false && value !== void 0;
|
|
}
|
|
var EventsService = class {
|
|
ctx;
|
|
_hooks = {};
|
|
constructor(ctx) {
|
|
this.ctx = ctx;
|
|
defineProperty(this, symbols.tracker, {
|
|
property: "ctx",
|
|
noShadow: true
|
|
});
|
|
this.on("internal/listener", function(name, listener, options) {
|
|
if (name === "internal/update" && !options.global) return (this.fiber._hooks["internal/update"] ??= new DisposableList())[options.prepend ? "unshift" : "push"](listener);
|
|
});
|
|
this.on("internal/update", function(config, noSave, next) {
|
|
const cbs = [...this._hooks["internal/update"] || []];
|
|
const _next = () => {
|
|
return (cbs.shift() ?? next).call(this, config, noSave, _next);
|
|
};
|
|
return _next();
|
|
}, {
|
|
global: true,
|
|
prepend: true
|
|
});
|
|
}
|
|
/**
|
|
* Resolve listeners for one dispatch and apply context filtering.
|
|
*
|
|
* @param type — the dispatch mode, reported on `internal/dispatch`.
|
|
* @param args — the raw dispatch arguments; consumed up to the event name.
|
|
* @returns the matching listener callbacks, bound to the dispatch `this`.
|
|
*/
|
|
dispatch(type, args) {
|
|
const thisArg = typeof args[0] === "object" || typeof args[0] === "function" ? args.shift() : null;
|
|
const name = args.shift();
|
|
if (!name.startsWith("internal/")) this.emit("internal/dispatch", type, name, args, thisArg);
|
|
const filter = thisArg?.[Context.filter];
|
|
return (this._hooks[name] || []).filter((hook) => hook.global || !filter || filter.call(thisArg, hook.ctx)).map((hook) => hook.callback.bind(thisArg));
|
|
}
|
|
/**
|
|
* Run listeners concurrently and wait for all of them.
|
|
*
|
|
* @param args — optional `this`, the event name, then listener arguments.
|
|
* @returns a promise resolving once every listener has settled.
|
|
*/
|
|
async parallel(...args) {
|
|
const errors = (await Promise.allSettled(this.dispatch("emit", args).map(async (cb) => cb(...args)))).filter((result) => result.status === "rejected");
|
|
if (errors.length) throw new AggregateError(errors.map((error) => error.reason));
|
|
}
|
|
/**
|
|
* Run listeners synchronously without waiting for returned promises.
|
|
*
|
|
* @param args — optional `this`, the event name, then listener arguments.
|
|
*/
|
|
emit(...args) {
|
|
this.dispatch("emit", args).map((cb) => cb(...args));
|
|
}
|
|
/**
|
|
* Run listeners in order, awaiting each, until one returns a bail value.
|
|
*
|
|
* @param args — optional `this`, the event name, then listener arguments.
|
|
* @returns the first bail value (see {@link isBailed}), if any.
|
|
*/
|
|
async serial(...args) {
|
|
for (const cb of this.dispatch("serial", args)) {
|
|
const result = await cb(...args);
|
|
if (isBailed(result)) return result;
|
|
}
|
|
}
|
|
/**
|
|
* Run listeners synchronously until one returns a bail value.
|
|
*
|
|
* @param args — optional `this`, the event name, then listener arguments.
|
|
* @returns the first bail value (see {@link isBailed}), if any.
|
|
*/
|
|
bail(...args) {
|
|
for (const cb of this.dispatch("bail", args)) {
|
|
const result = cb(...args);
|
|
if (isBailed(result)) return result;
|
|
}
|
|
}
|
|
/**
|
|
* Compose listeners around the final `next` callback.
|
|
*
|
|
* The last dispatch argument is treated as the innermost `next`. Listeners
|
|
* run outermost-first; a listener that does not call `next()` vetoes the
|
|
* rest of the chain, including the built-in behavior.
|
|
*
|
|
* @param args — optional `this`, the event name, listener arguments, then `next`.
|
|
* @returns the outermost listener's return value.
|
|
*/
|
|
waterfall(...args) {
|
|
const cbs = this.dispatch("waterfall", args);
|
|
const inner = args.pop();
|
|
const next = () => {
|
|
return (cbs.shift() ?? inner)(...args);
|
|
};
|
|
args.push(next);
|
|
return next();
|
|
}
|
|
/**
|
|
* Store a listener record as an effect on the current fiber.
|
|
*
|
|
* @param label — effect label shown in fiber diagnostics.
|
|
* @param hooks — the listener list for one event.
|
|
* @param callback — the listener to store.
|
|
* @param options — placement and filtering options.
|
|
* @returns a disposer that unregisters the listener.
|
|
*/
|
|
register(label, hooks, callback, options) {
|
|
const method = options.prepend ? "unshift" : "push";
|
|
return this.ctx.fiber.effect(() => {
|
|
hooks[method]({
|
|
ctx: this.ctx,
|
|
callback,
|
|
...options
|
|
});
|
|
return () => this.unregister(hooks, callback);
|
|
}, label);
|
|
}
|
|
/**
|
|
* Remove a stored listener record.
|
|
*
|
|
* @param hooks — the listener list for one event.
|
|
* @param callback — the listener to remove.
|
|
* @returns `true` if the listener was found and removed.
|
|
*/
|
|
unregister(hooks, callback) {
|
|
const index = hooks.findIndex((hook) => hook.callback === callback);
|
|
if (index >= 0) {
|
|
hooks.splice(index, 1);
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* Register an event listener owned by the current fiber.
|
|
*
|
|
* The listener is removed automatically when the fiber unloads. Throws
|
|
* `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed.
|
|
*
|
|
* @param name — the event name to listen for.
|
|
* @param listener — called with the dispatch arguments.
|
|
* @param options — listener options; a boolean is shorthand for `prepend`.
|
|
* @returns a disposer removing the listener; `true` if it was still registered.
|
|
*/
|
|
on(name, listener, options) {
|
|
if (typeof options !== "object") options = { prepend: options };
|
|
this.ctx.fiber.assertActive();
|
|
listener = this.ctx.reflect.bind(listener);
|
|
const result = this.bail(this.ctx, "internal/listener", name, listener, options);
|
|
if (result) return result;
|
|
const hooks = this._hooks[name] ||= [];
|
|
const label = `ctx.on(${typeof name === "string" ? JSON.stringify(name) : name.toString()})`;
|
|
return this.register(label, hooks, listener, options);
|
|
}
|
|
/**
|
|
* Register an event listener that disposes itself after the first call.
|
|
*
|
|
* @param name — the event name to listen for.
|
|
* @param listener — called at most once with the dispatch arguments.
|
|
* @param options — listener options; a boolean is shorthand for `prepend`.
|
|
* @returns a disposer removing the listener; `true` if it was still registered.
|
|
*/
|
|
once(name, listener, options) {
|
|
const dispose = this.on(name, function(...args) {
|
|
dispose();
|
|
return listener.apply(this, args);
|
|
}, options);
|
|
return dispose;
|
|
}
|
|
};
|
|
var defaultFormatters = {
|
|
s: (value) => String(value),
|
|
d: (value) => Math.trunc(Number(value)),
|
|
i: (value) => Math.trunc(Number(value)),
|
|
f: (value) => Number(value),
|
|
o: (value) => JSON.stringify(value),
|
|
O: (value) => JSON.stringify(value),
|
|
c: () => "",
|
|
C: (value, exporter, message) => {
|
|
return Logger.color(exporter, Logger.code(message.name, exporter.colors), value);
|
|
}
|
|
};
|
|
function isAggregateError(error) {
|
|
return error instanceof Error && Array.isArray(error["errors"]);
|
|
}
|
|
var Logger = class {
|
|
service;
|
|
static color(exporter, code, value, decoration = "") {
|
|
if (!exporter.colors) return "" + value;
|
|
return `\x1B[3${code < 8 ? code : "8;5;" + code}${exporter.colors >= 2 ? decoration : ""}m${value}\x1B[0m`;
|
|
}
|
|
static code(name, level) {
|
|
let hash = 0;
|
|
for (let i = 0; i < name.length; i++) {
|
|
hash = (hash << 3) - hash + name.charCodeAt(i) + 13;
|
|
hash |= 0;
|
|
}
|
|
const colors = !level ? [] : level >= 2 ? c256 : c16;
|
|
return colors[Math.abs(hash) % colors.length];
|
|
}
|
|
static format(exporter, message) {
|
|
const args = message.args.slice();
|
|
if (args[0] instanceof Error) {
|
|
args[0] = args[0].stack || args[0].message;
|
|
args.unshift("%s");
|
|
} else if (typeof args[0] !== "string") args.unshift("%o");
|
|
let format = args.shift();
|
|
format = format.replace(/%([a-zA-Z%])/g, (match, char) => {
|
|
if (match === "%%") return "%";
|
|
const formatter = exporter.formatters?.[char] ?? defaultFormatters[char];
|
|
if (typeof formatter === "function") return formatter(args.shift(), exporter, message);
|
|
return match;
|
|
});
|
|
const oFormatter = exporter.formatters?.o ?? defaultFormatters.o;
|
|
for (let arg of args) {
|
|
if (typeof arg === "object" && arg) arg = oFormatter(arg, exporter, message);
|
|
format += " " + arg;
|
|
}
|
|
const { maxLength = 10240 } = exporter;
|
|
return format.split(/\r?\n/g).map((line) => {
|
|
return line.slice(0, maxLength) + (line.length > maxLength ? "..." : "");
|
|
}).join("\n");
|
|
}
|
|
constructor(options, service) {
|
|
this.service = service;
|
|
Object.assign(this, options);
|
|
this.error = this._method("error", 0);
|
|
this.info = this._method("info", 1);
|
|
this.warn = this._method("warn", 2);
|
|
this.debug = this._method("debug", 3);
|
|
}
|
|
_method(type, level) {
|
|
return (...args) => {
|
|
if (args.length === 1 && args[0] instanceof Error) {
|
|
if (args[0].cause) this[type](args[0].cause);
|
|
else if (isAggregateError(args[0])) {
|
|
args[0].errors.forEach((error) => this[type](error));
|
|
return;
|
|
}
|
|
}
|
|
const sn = ++this.service._snMessage;
|
|
const ts = Date.now();
|
|
for (const exporter of this.service.exporters.values()) {
|
|
if ((exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? 1) < level) continue;
|
|
const message = {
|
|
sn,
|
|
ts,
|
|
type,
|
|
level,
|
|
name: this.name,
|
|
...this.meta,
|
|
args
|
|
};
|
|
exporter.export(message);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
var c16 = [
|
|
6,
|
|
2,
|
|
3,
|
|
4,
|
|
5,
|
|
1
|
|
];
|
|
var c256 = [
|
|
20,
|
|
21,
|
|
26,
|
|
27,
|
|
32,
|
|
33,
|
|
38,
|
|
39,
|
|
40,
|
|
41,
|
|
42,
|
|
43,
|
|
44,
|
|
45,
|
|
56,
|
|
57,
|
|
62,
|
|
63,
|
|
68,
|
|
69,
|
|
74,
|
|
75,
|
|
76,
|
|
77,
|
|
78,
|
|
79,
|
|
80,
|
|
81,
|
|
92,
|
|
93,
|
|
98,
|
|
99,
|
|
112,
|
|
113,
|
|
129,
|
|
134,
|
|
135,
|
|
148,
|
|
149,
|
|
160,
|
|
161,
|
|
162,
|
|
163,
|
|
164,
|
|
165,
|
|
166,
|
|
167,
|
|
168,
|
|
169,
|
|
170,
|
|
171,
|
|
172,
|
|
173,
|
|
178,
|
|
179,
|
|
184,
|
|
185,
|
|
196,
|
|
197,
|
|
198,
|
|
199,
|
|
200,
|
|
201,
|
|
202,
|
|
203,
|
|
204,
|
|
205,
|
|
206,
|
|
207,
|
|
208,
|
|
209,
|
|
214,
|
|
215,
|
|
220,
|
|
221
|
|
];
|
|
var LoggerService = class LoggerService2 {
|
|
bufferSize = 1e3;
|
|
buffer = [];
|
|
ctx;
|
|
_snMessage = 0;
|
|
_snExporter = 0;
|
|
exporters = /* @__PURE__ */ new Map();
|
|
constructor(ctx) {
|
|
const tracker = {
|
|
property: "ctx",
|
|
noShadow: true
|
|
};
|
|
const self = createCallable("logger", joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker);
|
|
Object.assign(self, this);
|
|
self.ctx = ctx;
|
|
defineProperty(self, symbols.tracker, tracker);
|
|
self.exporter({
|
|
colors: 3,
|
|
export: (message) => {
|
|
self.buffer.push(message);
|
|
if (self.buffer.length > self.bufferSize) self.buffer = self.buffer.slice(-self.bufferSize);
|
|
}
|
|
});
|
|
return self;
|
|
}
|
|
/**
|
|
* Register an exporter and dispose it with the current fiber.
|
|
*
|
|
* @param exporter — the sink that receives structured log messages.
|
|
* @returns a disposer that removes the exporter.
|
|
*/
|
|
exporter(exporter) {
|
|
return this.ctx.effect(() => {
|
|
const id = ++this._snExporter;
|
|
this.exporters.set(id, exporter);
|
|
return () => this.exporters.delete(id);
|
|
}, "ctx.logger.exporter()");
|
|
}
|
|
_resolveConfig() {
|
|
let intercept = this.ctx[symbols.intercept];
|
|
const configs = [];
|
|
while ("logger" in intercept) {
|
|
if (Object.hasOwn(intercept, "logger")) configs.unshift(intercept["logger"]);
|
|
intercept = Object.getPrototypeOf(intercept);
|
|
}
|
|
return Object.assign({}, ...configs);
|
|
}
|
|
[symbols.invoke](name) {
|
|
const config = this._resolveConfig();
|
|
const fiber = (this.ctx[symbols.shadow] ?? this.ctx).fiber;
|
|
name ??= config.name;
|
|
name ??= hyphenate(fiber.name);
|
|
return new Logger({
|
|
name,
|
|
level: config.level,
|
|
meta: { fiber: new WeakRef(fiber) }
|
|
}, this);
|
|
}
|
|
static {
|
|
for (const type of [
|
|
"error",
|
|
"info",
|
|
"warn",
|
|
"debug"
|
|
]) LoggerService2.prototype[type] = function(...args) {
|
|
return this()[type](...args);
|
|
};
|
|
}
|
|
};
|
|
function enhanceError(error) {
|
|
const lines = error.stack.split("\n");
|
|
lines.splice(0, 2, `Error: ${error.message}`);
|
|
error.stack = lines.join("\n");
|
|
return error;
|
|
}
|
|
var RESERVED_WORDS = ["prototype", "then"];
|
|
function isSpecialProperty(prop) {
|
|
return typeof prop === "symbol" || RESERVED_WORDS.includes(prop) || parseInt(prop).toString() === prop || prop.startsWith("_");
|
|
}
|
|
var ReflectService = class {
|
|
ctx;
|
|
/** Proxy traps implementing service resolution for every context object. */
|
|
static handler = {
|
|
get: (target, prop, ctx) => {
|
|
if (isSpecialProperty(prop)) return Reflect.get(target, prop, ctx);
|
|
if (Reflect.has(target, prop)) return getTraceable(ctx, Reflect.get(target, prop, ctx));
|
|
const error = /* @__PURE__ */ new Error(`cannot get property "${prop}" without inject`);
|
|
try {
|
|
const def = target.reflect.props[prop];
|
|
if (def?.type === "accessor") return def.get.call(ctx, ctx[symbols.receiver], error);
|
|
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false);
|
|
return ctx.events.waterfall("internal/get", ctx, prop, error, () => {
|
|
const key = target[symbols.isolate][prop];
|
|
let fiber = (ctx[symbols.shadow] ?? ctx).fiber;
|
|
while (true) {
|
|
const impl = fiber.store?.[prop];
|
|
if (impl) return getTraceable(ctx, impl.value);
|
|
if (prop in fiber.inject) {
|
|
error.message = `cannot get required service "${prop}" in inactive context`;
|
|
throw error;
|
|
}
|
|
if (!fiber.runtime) throw error;
|
|
if (fiber.parent[symbols.isolate][prop] !== key) throw error;
|
|
fiber = fiber.parent.fiber;
|
|
}
|
|
});
|
|
} catch (e) {
|
|
throw e === error ? enhanceError(e) : e;
|
|
}
|
|
},
|
|
set: (target, prop, value, ctx) => {
|
|
if (isSpecialProperty(prop)) return Reflect.set(target, prop, value, ctx);
|
|
const error = /* @__PURE__ */ new Error(`cannot set property "${prop}" without provide`);
|
|
const def = target.reflect.props[prop];
|
|
if (!def) {
|
|
if (!ctx.fiber.runtime) return Reflect.set(target, prop, value, ctx);
|
|
throw enhanceError(error);
|
|
}
|
|
try {
|
|
if (def.type === "accessor") {
|
|
if (!def.set) return false;
|
|
return def.set.call(ctx, value, ctx[symbols.receiver], error);
|
|
}
|
|
return ctx.events.waterfall("internal/set", ctx, prop, value, error, () => {
|
|
return ctx.reflect.set(prop, value, error);
|
|
});
|
|
} catch (e) {
|
|
throw e === error ? enhanceError(e) : e;
|
|
}
|
|
},
|
|
has: (target, prop) => {
|
|
if (isSpecialProperty(prop)) return Reflect.has(target, prop);
|
|
if (Reflect.has(target, prop)) return true;
|
|
return !!target.reflect.props[prop];
|
|
}
|
|
};
|
|
/** Service implementations, keyed by isolation label. */
|
|
store = /* @__PURE__ */ Object.create(null);
|
|
/** Declared context properties (services and accessors), by name. */
|
|
props = /* @__PURE__ */ Object.create(null);
|
|
constructor(ctx) {
|
|
this.ctx = ctx;
|
|
defineProperty(this, symbols.tracker, {
|
|
property: "ctx",
|
|
noShadow: true
|
|
});
|
|
this.mixin("reflect", [
|
|
"get",
|
|
"set",
|
|
"provide",
|
|
"accessor",
|
|
"mixin"
|
|
]);
|
|
this.mixin("fiber", ["runtime", "effect"]);
|
|
this.mixin("registry", ["inject", "plugin"]);
|
|
this.mixin("events", [
|
|
"on",
|
|
"once",
|
|
"parallel",
|
|
"emit",
|
|
"serial",
|
|
"bail",
|
|
"waterfall"
|
|
]);
|
|
}
|
|
/**
|
|
* Read a service from the store without the inject requirement.
|
|
*
|
|
* @param name — the service name.
|
|
* @param strict — when `true`, only return implementations whose providing
|
|
* fiber is currently active.
|
|
* @returns the service value, or `undefined` when not (yet) provided.
|
|
*/
|
|
get(name, strict = true) {
|
|
return getTraceable(this.ctx, this._getImpl(name, strict)?.value);
|
|
}
|
|
_getImpl(name, strict = true) {
|
|
const key = this.ctx[symbols.isolate][name];
|
|
const impl = key && this.store[key];
|
|
if (!impl) return;
|
|
if (strict && impl.fiber.state !== 2) return;
|
|
return impl;
|
|
}
|
|
/**
|
|
* Overwrite a provided service's value.
|
|
*
|
|
* @param name — the service name.
|
|
* @param value — the new service value.
|
|
* @param error — carrier for the caller stack in diagnostics.
|
|
* @returns `true` on success.
|
|
* @throws when `name` was never provided, or was provided by another fiber.
|
|
*/
|
|
set(name, value, error) {
|
|
const key = this.ctx[symbols.isolate][name];
|
|
const impl = this.store[key];
|
|
if (!impl) throw new Error(`cannot set property "${name}" without provide`);
|
|
if (impl.fiber !== this.ctx.fiber) throw new Error(`cannot set property "${name}" in multiple fibers`);
|
|
impl.value = value;
|
|
return true;
|
|
}
|
|
/**
|
|
* Register a service implementation owned by the current fiber.
|
|
*
|
|
* See the `ctx.provide()` overload above for the full contract.
|
|
*
|
|
* @param name — the service name.
|
|
* @param value — the service value.
|
|
* @param check — optional availability predicate for dependents.
|
|
* @returns a disposer that unregisters the service.
|
|
*/
|
|
provide(name, value, check) {
|
|
return this.ctx.fiber.effect(() => {
|
|
if (!this.props[name]) this.props[name] ??= { type: "service" };
|
|
else if (this.props[name].type !== "service") throw new Error(`property "${name}" is already declared as ${this.props[name].type}`);
|
|
this.props[name] = { type: "service" };
|
|
this.ctx.root[symbols.isolate][name] ??= Symbol(name);
|
|
const key = this.ctx[symbols.isolate][name];
|
|
const impl = {
|
|
name,
|
|
value,
|
|
fiber: this.ctx.fiber,
|
|
check
|
|
};
|
|
if (this.store[key]) throw new Error(`service "${name}" has been registered at <${this.store[key].fiber.name}>`);
|
|
this.store[key] = impl;
|
|
this.ctx.fiber.store[name] = impl;
|
|
if (this.ctx.fiber.state === 2) this.notify([name]);
|
|
return async () => {
|
|
delete this.store[key];
|
|
const fibers = this.notify([name]);
|
|
await Promise.allSettled(fibers.map((fiber) => fiber.await()));
|
|
delete this.ctx.fiber.store[name];
|
|
};
|
|
}, `ctx.provide(${JSON.stringify(name)})`);
|
|
}
|
|
/**
|
|
* Re-evaluate every fiber that requires one of the given services.
|
|
*
|
|
* @param names — the service names that changed.
|
|
* @param filter — restricts notification to matching isolation scopes.
|
|
* @returns the fibers whose dependency state was refreshed.
|
|
*/
|
|
notify(names, filter = (ctx, name) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
|
|
const fibers = [];
|
|
for (const runtime of this.ctx.registry.values()) for (const fiber of runtime.fibers) {
|
|
let hasUpdate = false;
|
|
for (const name of names) {
|
|
if (!(name in fiber.inject)) continue;
|
|
if (!filter(fiber.ctx, name)) continue;
|
|
hasUpdate = true;
|
|
fiber._checkImpl(name);
|
|
}
|
|
if (!hasUpdate) continue;
|
|
fiber._refresh();
|
|
fibers.push(fiber);
|
|
}
|
|
for (const name of names) {
|
|
const self = Object.create(this.ctx);
|
|
self[symbols.filter] = (target) => filter(target, name);
|
|
this.ctx.events.emit(self, "internal/service", name, this._getImpl(name, false)?.value);
|
|
}
|
|
return fibers;
|
|
}
|
|
/**
|
|
* Define a computed context property backed by get/set hooks.
|
|
*
|
|
* @param name — the context property name.
|
|
* @param options — the `get` hook and optional `set` hook.
|
|
* @returns a disposer that removes the accessor.
|
|
*/
|
|
accessor(name, options) {
|
|
return this.ctx.fiber.effect(() => {
|
|
if (name in this.props) throw new Error(`property "${name}" is already declared as ${this.props[name].type}`);
|
|
this.props[name] = {
|
|
type: "accessor",
|
|
...options
|
|
};
|
|
return () => delete this.props[name];
|
|
}, `ctx.accessor(${JSON.stringify(name)})`);
|
|
}
|
|
/**
|
|
* Expose selected members of a service directly on `ctx`.
|
|
*
|
|
* See the `ctx.mixin()` overload above for the full contract.
|
|
*
|
|
* @param source — a context property name or a source object.
|
|
* @param mixins — keys to forward, or a source-key → ctx-key map.
|
|
* @returns a disposer that removes all created accessors.
|
|
*/
|
|
mixin(source, mixins) {
|
|
const self = this;
|
|
return this.ctx.fiber.effect(function* () {
|
|
const entries = Array.isArray(mixins) ? mixins.map((key) => [key, key]) : Object.entries(mixins);
|
|
const getTarget = (ctx, error) => {
|
|
return ctx[source];
|
|
};
|
|
for (const [key, value] of entries) yield self.accessor(value, {
|
|
get(receiver, error) {
|
|
const service = getTarget(this, error);
|
|
if (isNullable(service)) return service;
|
|
const mixin = receiver ? withProps(receiver, service) : service;
|
|
const value2 = Reflect.get(service, key, mixin);
|
|
if (typeof value2 !== "function") return value2;
|
|
return value2.bind(mixin ?? service);
|
|
},
|
|
set(value2, receiver, error) {
|
|
const service = getTarget(this, error);
|
|
const mixin = receiver ? withProps(receiver, service) : service;
|
|
return Reflect.set(service, key, value2, mixin);
|
|
}
|
|
});
|
|
}, `ctx.mixin(${JSON.stringify(source)})`);
|
|
}
|
|
/**
|
|
* Attach this context's tracing wrapper to a value.
|
|
*
|
|
* @param value — the value to wrap.
|
|
* @returns the traceable wrapper (or the value itself when not applicable).
|
|
*/
|
|
trace(value) {
|
|
return getTraceable(this.ctx, value);
|
|
}
|
|
/**
|
|
* Wrap a callback so calls trace `this` and arguments to this context.
|
|
*
|
|
* @param callback — the function to wrap.
|
|
* @returns a proxy delegating to `callback` with traced values.
|
|
*/
|
|
bind(callback) {
|
|
return new Proxy(callback, {
|
|
apply: (target, thisArg, args) => {
|
|
return Reflect.apply(target, this.trace(thisArg), args.map((arg) => this.trace(arg)));
|
|
},
|
|
construct: (target, args, newTarget) => {
|
|
return Reflect.construct(target, args.map((arg) => this.trace(arg)), newTarget);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
var kValidationError = /* @__PURE__ */ Symbol.for("ValidationError");
|
|
var ValidationError = class extends TypeError {
|
|
name = "ValidationError";
|
|
/**
|
|
* Build the aggregated message from schema issues.
|
|
*
|
|
* @param issues — the standard-schema issues, one message line each.
|
|
*/
|
|
constructor(issues) {
|
|
super(`invalid config:
|
|
` + issues.map((issue) => {
|
|
if (issue.path) return ` - ${issue.message} (at ${issue.path.join(".")})`;
|
|
else return ` - ${issue.message}`;
|
|
}).join("\n"));
|
|
}
|
|
};
|
|
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
function resolveConfig(runtime, config) {
|
|
if (!runtime.Config) return config;
|
|
const result = runtime.Config["~standard"].validate(config);
|
|
if ("then" in result) throw new TypeError("Async config validation is not supported");
|
|
if (result.issues) throw new ValidationError(result.issues);
|
|
else return result.value;
|
|
}
|
|
var effectInertia = /* @__PURE__ */ new WeakMap();
|
|
function runDisposable(dispose) {
|
|
const result = dispose();
|
|
return effectInertia.get(dispose)?.() ?? result;
|
|
}
|
|
function emitPluginDisposed(context, fiber) {
|
|
const args = ["internal/plugin", fiber];
|
|
let callbacks;
|
|
try {
|
|
callbacks = context.events.dispatch("emit", args);
|
|
} catch (error) {
|
|
context.logger.error(error);
|
|
return;
|
|
}
|
|
for (const callback of callbacks) try {
|
|
const returned = callback(...args);
|
|
Promise.resolve(returned).catch((error) => context.logger.error(error));
|
|
} catch (error) {
|
|
context.logger.error(error);
|
|
}
|
|
}
|
|
var CordisError = class CordisError2 extends Error {
|
|
code;
|
|
/**
|
|
* @param code — the stable error code; also the default message.
|
|
* @param message — optional human-readable override.
|
|
*/
|
|
constructor(code, message) {
|
|
super(message ?? CordisError2.Code[code]);
|
|
this.code = code;
|
|
}
|
|
};
|
|
(function(CordisError3) {
|
|
CordisError3.Code = { INACTIVE_EFFECT: "cannot create effect on inactive context" };
|
|
})(CordisError || (CordisError = {}));
|
|
var INACTIVE = "__INACTIVE__";
|
|
var Fiber = class {
|
|
parent;
|
|
inject;
|
|
runtime;
|
|
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
|
|
uid;
|
|
/** The context this fiber's plugin runs in (extends the parent context). */
|
|
ctx;
|
|
/** The validated plugin config (updated by `update()`). */
|
|
config;
|
|
/** The raw plugin config, re-resolved before each activation. */
|
|
_config;
|
|
/** Current lifecycle state; transitions emit `internal/status`. */
|
|
state = 0;
|
|
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
|
|
dispose;
|
|
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
|
|
store;
|
|
/** The in-flight load/unload transition, if one is currently running. */
|
|
inertia;
|
|
_hooks = /* @__PURE__ */ Object.create(null);
|
|
_disposables = new DisposableList();
|
|
context;
|
|
_error;
|
|
_runner;
|
|
_store = /* @__PURE__ */ Object.create(null);
|
|
/**
|
|
* Create a fiber. Plugin authors normally obtain fibers from `ctx.plugin()`
|
|
* rather than constructing them directly.
|
|
*
|
|
* @param parent — the context the plugin was loaded from.
|
|
* @param config — raw config, validated against the runtime's schema.
|
|
* @param inject — resolved dependency map (service name → intercept config).
|
|
* @param runtime — the shared plugin runtime, or `null` for the root fiber.
|
|
* @param getOuterStack — captures the caller stack for effect diagnostics.
|
|
*/
|
|
constructor(parent, config, inject, runtime, getOuterStack) {
|
|
this.parent = parent;
|
|
this.inject = inject;
|
|
this.runtime = runtime;
|
|
this._config = config;
|
|
const collect = (dispose) => {
|
|
this._disposables.push(dispose);
|
|
};
|
|
if (runtime) {
|
|
this.uid = parent.registry.counter;
|
|
this.ctx = this.context = parent.extend({ fiber: this });
|
|
const injectEntries = Object.entries(this.inject);
|
|
if (injectEntries.length) {
|
|
this.ctx[Context.intercept] = Object.create(parent[Context.intercept]);
|
|
for (const [name, config2] of injectEntries) {
|
|
if (isNullable(config2)) continue;
|
|
this.ctx[Context.intercept][name] = config2;
|
|
}
|
|
}
|
|
this._runner = {
|
|
epoch: INACTIVE,
|
|
getOuterStack,
|
|
execute: function() {
|
|
if (isConstructor(runtime.callback)) {
|
|
const instance = new runtime.callback(this.ctx, this.config);
|
|
for (const hook of instance?.[symbols.initHooks] ?? []) hook();
|
|
return instance?.[symbols.init]?.();
|
|
} else return runtime.callback(this.ctx, this.config);
|
|
},
|
|
collect
|
|
};
|
|
this.dispose = parent.fiber.effect(() => {
|
|
const remove = runtime.fibers.push(this);
|
|
return async () => {
|
|
this.uid = null;
|
|
emitPluginDisposed(this.context, this);
|
|
if (this.ctx.registry.has(runtime.callback)) {
|
|
remove();
|
|
if (!runtime.fibers.length) this.ctx.registry.delete(runtime.callback);
|
|
}
|
|
this._setEpoch(INACTIVE);
|
|
if (!this.inertia) this._updateState(() => {
|
|
this.inertia = this._unload();
|
|
return 5;
|
|
});
|
|
while (this.inertia) await this.inertia;
|
|
};
|
|
}, "ctx.plugin()");
|
|
try {
|
|
this.context.emit("internal/plugin", this);
|
|
} catch (error) {
|
|
Promise.resolve(this.dispose()).catch((reason) => this.ctx.logger.error(reason));
|
|
throw error;
|
|
}
|
|
if (this.uid !== null && parent.fiber.state !== 5) {
|
|
for (const name of Object.keys(this.inject)) this._checkImpl(name);
|
|
this._refresh();
|
|
}
|
|
} else {
|
|
this.uid = 0;
|
|
this.ctx = this.context = parent;
|
|
this.state = 2;
|
|
this.store = /* @__PURE__ */ Object.create(null);
|
|
this._runner = {
|
|
epoch: "",
|
|
getOuterStack,
|
|
execute: () => {
|
|
},
|
|
collect
|
|
};
|
|
this.dispose = () => this.restart();
|
|
}
|
|
}
|
|
/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
|
|
get name() {
|
|
let fiber = this;
|
|
do {
|
|
if (fiber.runtime?.name) return fiber.runtime.name;
|
|
fiber = fiber.parent.fiber;
|
|
} while (fiber !== fiber.parent.fiber);
|
|
return "root";
|
|
}
|
|
/**
|
|
* Throw if the fiber has already been disposed.
|
|
*
|
|
* @returns nothing when the fiber is still active.
|
|
* @throws {CordisError} `INACTIVE_EFFECT` when the fiber's uid has been cleared.
|
|
*/
|
|
assertActive() {
|
|
if (this.uid !== null) return;
|
|
throw new CordisError("INACTIVE_EFFECT");
|
|
}
|
|
_execute(runner) {
|
|
const oldEpoch = runner.epoch;
|
|
return composeError((info) => {
|
|
const safeCollect = (dispose) => {
|
|
if (typeof dispose === "function") runner.collect(dispose);
|
|
else if (!isNullable(dispose)) throw new TypeError("Invalid effect");
|
|
};
|
|
const effect = runner.execute.call(this);
|
|
if (typeof effect === "function") return runner.collect(effect);
|
|
else if (isNullable(effect)) {
|
|
} else if (!isObject(effect)) throw new TypeError("Invalid effect");
|
|
else if ("then" in effect) return effect.then(safeCollect);
|
|
else if (Symbol.iterator in effect) {
|
|
info.error = /* @__PURE__ */ new Error();
|
|
const iter = effect[Symbol.iterator]();
|
|
while (true) {
|
|
const result = iter.next();
|
|
safeCollect(result.value);
|
|
if (result.done) return;
|
|
}
|
|
} else if (Symbol.asyncIterator in effect) {
|
|
const iter = effect[Symbol.asyncIterator]();
|
|
return (async () => {
|
|
await Promise.resolve();
|
|
info.error = /* @__PURE__ */ new Error();
|
|
while (true) {
|
|
if (runner.epoch !== oldEpoch) return;
|
|
const result = await iter.next();
|
|
safeCollect(result.value);
|
|
if (result.done) return;
|
|
}
|
|
})();
|
|
} else throw new TypeError("Invalid effect");
|
|
}, runner.getOuterStack);
|
|
}
|
|
effect(execute, label = "anonymous") {
|
|
this.assertActive();
|
|
if (this.state === 5) throw new CordisError("INACTIVE_EFFECT");
|
|
const disposables = [];
|
|
let disposing = false;
|
|
let disposalTask;
|
|
const dispose = () => {
|
|
if (disposing) return disposalTask;
|
|
disposing = true;
|
|
let task2;
|
|
for (const disposable of disposables.splice(0).reverse()) if (task2) task2 = task2.then(() => runDisposable(disposable));
|
|
else {
|
|
const result = runDisposable(disposable);
|
|
if (isObject(result) && "then" in result) task2 = result;
|
|
}
|
|
return disposalTask = task2;
|
|
};
|
|
const meta = {
|
|
label,
|
|
children: []
|
|
};
|
|
const runner = {
|
|
execute,
|
|
epoch: true,
|
|
collect: (dispose2) => {
|
|
disposables.push(dispose2);
|
|
this._disposables.delete(dispose2);
|
|
if (dispose2[symbols.effect]) meta.children.push(dispose2[symbols.effect]);
|
|
},
|
|
getOuterStack: buildOuterStack()
|
|
};
|
|
let task;
|
|
let executing = true;
|
|
let resolveSetup;
|
|
let rejectSetup;
|
|
let setupBarrier;
|
|
let setupFailed = false;
|
|
let inFlight;
|
|
let removeWrapper = () => false;
|
|
const waitForSetup = () => {
|
|
setupBarrier ??= new Promise((resolve, reject) => {
|
|
resolveSetup = resolve;
|
|
rejectSetup = reject;
|
|
});
|
|
return setupBarrier;
|
|
};
|
|
const disposeAfter = (setup) => {
|
|
return Promise.resolve(setup).then(() => dispose(), async (reason) => {
|
|
await dispose();
|
|
throw reason;
|
|
});
|
|
};
|
|
const finalizeDisposal = (callback) => {
|
|
let result;
|
|
try {
|
|
result = callback();
|
|
} catch (error) {
|
|
removeWrapper();
|
|
throw error;
|
|
}
|
|
if (isObject(result) && "then" in result) {
|
|
const pending = Promise.resolve(result).finally(() => {
|
|
removeWrapper();
|
|
if (inFlight === pending) inFlight = void 0;
|
|
});
|
|
return inFlight = pending;
|
|
}
|
|
removeWrapper();
|
|
return result;
|
|
};
|
|
const wrapper = defineProperty(() => {
|
|
if (!runner.epoch) return setupFailed ? inFlight : void 0;
|
|
runner.epoch = false;
|
|
return finalizeDisposal(() => {
|
|
if (executing) return disposeAfter(waitForSetup());
|
|
return task ? disposeAfter(task) : dispose();
|
|
});
|
|
}, symbols.effect, meta);
|
|
effectInertia.set(wrapper, () => inFlight);
|
|
removeWrapper = this._disposables.push(wrapper);
|
|
try {
|
|
task = this._execute(runner);
|
|
} catch (reason) {
|
|
executing = false;
|
|
setupFailed = true;
|
|
runner.epoch = false;
|
|
let cleanup;
|
|
try {
|
|
cleanup = finalizeDisposal(dispose);
|
|
} finally {
|
|
rejectSetup?.(reason);
|
|
}
|
|
if (isObject(cleanup) && "then" in cleanup) cleanup.catch((error) => this.ctx.logger.error(error));
|
|
throw reason;
|
|
}
|
|
executing = false;
|
|
if (setupBarrier) Promise.resolve(task).then(resolveSetup, rejectSetup);
|
|
task?.catch(() => {
|
|
if (!runner.epoch) return dispose();
|
|
return finalizeDisposal(dispose);
|
|
}).catch((error) => this.ctx.logger.error(error));
|
|
const disposeAsync = () => {
|
|
if (!runner.epoch) return;
|
|
runner.epoch = false;
|
|
return finalizeDisposal(dispose);
|
|
};
|
|
wrapper.then = async (onFulfilled, onRejected) => {
|
|
return Promise.resolve(task).then(() => disposeAsync).then(onFulfilled, onRejected);
|
|
};
|
|
return wrapper;
|
|
}
|
|
/**
|
|
* Return metadata for currently registered effects.
|
|
*
|
|
* @returns one {@link EffectMeta} tree per labeled live effect.
|
|
*/
|
|
getEffects() {
|
|
return [...this._disposables].map((dispose) => dispose[symbols.effect]).filter(Boolean);
|
|
}
|
|
_getState() {
|
|
if (this.uid === null) return 4;
|
|
if (this._error) return 3;
|
|
if (this._runner.epoch !== INACTIVE) return 2;
|
|
return 0;
|
|
}
|
|
_updateState(callback) {
|
|
const oldState = this.state;
|
|
this.state = callback() ?? this._getState();
|
|
if (oldState === this.state) return;
|
|
this.context.emit("internal/status", this, oldState);
|
|
if (oldState !== 2 && this.state !== 2) return;
|
|
for (const key of Reflect.ownKeys(this.ctx.reflect.store)) {
|
|
const impl = this.ctx.reflect.store[key];
|
|
if (impl.fiber !== this) continue;
|
|
this.ctx.reflect.notify([impl.name]);
|
|
}
|
|
}
|
|
_checkImpl(name) {
|
|
const impl = this.ctx.reflect._getImpl(name, true);
|
|
if (!impl) return delete this._store[name];
|
|
try {
|
|
if (impl.check && !impl.check.call(getTraceable(this.ctx, impl.value))) return delete this._store[name];
|
|
} catch (error) {
|
|
impl.fiber.ctx.logger.error(error);
|
|
return delete this._store[name];
|
|
}
|
|
this._store[name] = impl;
|
|
}
|
|
_refresh() {
|
|
let epoch = false;
|
|
epoch = "";
|
|
for (const name of Object.keys(this.inject)) {
|
|
const impl = this._store[name];
|
|
if (!impl) {
|
|
epoch = INACTIVE;
|
|
break;
|
|
}
|
|
epoch += ":" + impl.fiber.uid;
|
|
}
|
|
this._setEpoch(epoch);
|
|
}
|
|
_setEpoch(epoch) {
|
|
const oldEpoch = this._runner.epoch;
|
|
if (epoch === oldEpoch) return;
|
|
this._runner.epoch = epoch;
|
|
if (this.inertia) return;
|
|
this._updateState(() => {
|
|
if (epoch !== INACTIVE && oldEpoch === INACTIVE) {
|
|
this.inertia = this._reload();
|
|
return 1;
|
|
} else {
|
|
this.inertia = this._unload();
|
|
return 5;
|
|
}
|
|
});
|
|
}
|
|
_resolveConfig(config) {
|
|
config = this.context.waterfall(this, "internal/config", config, () => config);
|
|
return this.runtime ? resolveConfig(this.runtime, config) : config;
|
|
}
|
|
async _reload() {
|
|
this.store = { ...this._store };
|
|
const oldEpoch = this._runner.epoch;
|
|
try {
|
|
await Promise.resolve();
|
|
if (this._runner.epoch === oldEpoch) {
|
|
this.config = this._resolveConfig(this._config);
|
|
await this._execute(this._runner);
|
|
this._error = void 0;
|
|
}
|
|
} catch (reason) {
|
|
this.ctx.logger.error(reason);
|
|
this._error = reason;
|
|
this._runner.epoch = INACTIVE;
|
|
}
|
|
this._updateState(() => {
|
|
if (this._runner.epoch === oldEpoch) this.inertia = void 0;
|
|
else {
|
|
this.inertia = this._unload();
|
|
return 5;
|
|
}
|
|
});
|
|
}
|
|
async _unload() {
|
|
await Promise.all(this._disposables.clear().map(async (dispose) => {
|
|
try {
|
|
await composeError(async (info) => {
|
|
await Promise.resolve();
|
|
info.error = /* @__PURE__ */ new Error();
|
|
await runDisposable(dispose);
|
|
}, this._runner.getOuterStack);
|
|
} catch (reason) {
|
|
this.ctx.logger.error(reason);
|
|
}
|
|
}));
|
|
this.store = void 0;
|
|
this._updateState(() => {
|
|
if (this._runner.epoch === INACTIVE) this.inertia = void 0;
|
|
else {
|
|
this.inertia = this._reload();
|
|
return 1;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Wait for current lifecycle work and rethrow startup errors.
|
|
*
|
|
* @returns this fiber, once it has settled into a stable state.
|
|
* @throws the config-validation or plugin-startup error, if any.
|
|
*/
|
|
async await() {
|
|
while (this.inertia) await this.inertia;
|
|
if (this._error) throw this._error;
|
|
return this;
|
|
}
|
|
/**
|
|
* Dispose and immediately reload this plugin with its current config.
|
|
*
|
|
* @returns a promise resolving once the reload settled.
|
|
* @throws {CordisError} `INACTIVE_EFFECT` when the fiber is already disposed.
|
|
*/
|
|
async restart() {
|
|
this.assertActive();
|
|
this._setEpoch(INACTIVE);
|
|
this._refresh();
|
|
await this.await();
|
|
}
|
|
/**
|
|
* Validate and apply new config, then restart the plugin.
|
|
*
|
|
* Runs the `internal/update` waterfall first, so update hooks (and HMR)
|
|
* can veto or replace the restart.
|
|
*
|
|
* @param config — the new raw config; validated before anything restarts.
|
|
* @param noSave — hint for persistence hooks not to write the change back.
|
|
* @returns nothing; the restart runs behind the `internal/update` waterfall.
|
|
* @throws {ValidationError} when the new config fails validation.
|
|
*/
|
|
update(config, noSave = false) {
|
|
this.assertActive();
|
|
this._config = config;
|
|
if (this.state !== 2) {
|
|
this._error = void 0;
|
|
this._setEpoch(INACTIVE);
|
|
this._refresh();
|
|
return;
|
|
}
|
|
config = this._resolveConfig(config);
|
|
this.context.waterfall(this, "internal/update", config, noSave, () => {
|
|
this.config = config;
|
|
this._error = void 0;
|
|
return this.restart();
|
|
});
|
|
}
|
|
};
|
|
function isApplicable(object) {
|
|
return object && typeof object === "object" && typeof object.apply === "function";
|
|
}
|
|
function Inject(name, config) {
|
|
return function(value, decorator) {
|
|
if (decorator.kind === "class") {
|
|
if (!Object.hasOwn(value, "inject")) {
|
|
defineProperty(value, "inject", Object.create(Object.getPrototypeOf(value).inject ?? null));
|
|
defineProperty(value.inject, symbols.checkProto, true);
|
|
}
|
|
value.inject[name] = config;
|
|
} else if (decorator.kind === "method") {
|
|
const inject = (value[symbols.metadata] ??= {}).inject ??= /* @__PURE__ */ Object.create(null);
|
|
inject[name] = config;
|
|
decorator.addInitializer(function() {
|
|
const property = this[symbols.tracker]?.property;
|
|
(this[symbols.initHooks] ??= []).push(() => {
|
|
this.ctx.inject(inject, (ctx) => {
|
|
return value.call(property ? withProps(this, { [property]: ctx }) : this);
|
|
});
|
|
});
|
|
});
|
|
} else throw new Error("@Inject() can only be used on class or class methods");
|
|
};
|
|
}
|
|
(function(Inject2) {
|
|
function resolve(inject, result = /* @__PURE__ */ Object.create(null)) {
|
|
if (!inject) return result;
|
|
if (Array.isArray(inject)) for (const name of inject) result[name] = null;
|
|
else if (Reflect.has(inject, symbols.checkProto)) {
|
|
Object.assign(result, resolve(Object.getPrototypeOf(inject)));
|
|
for (const name of Object.keys(inject)) result[name] = inject[name] ?? null;
|
|
} else for (const name of Object.keys(inject)) result[name] = inject[name] ?? null;
|
|
return result;
|
|
}
|
|
Inject2.resolve = resolve;
|
|
})(Inject || (Inject = {}));
|
|
var RegistryService = class {
|
|
ctx;
|
|
_counter = 0;
|
|
_internal = /* @__PURE__ */ new Map();
|
|
constructor(ctx) {
|
|
this.ctx = ctx;
|
|
defineProperty(this, symbols.tracker, {
|
|
property: "ctx",
|
|
noShadow: true
|
|
});
|
|
}
|
|
/** Allocate the next fiber uid (increments on every read). */
|
|
get counter() {
|
|
return ++this._counter;
|
|
}
|
|
/** Number of registered plugin runtimes. */
|
|
get size() {
|
|
return this._internal.size;
|
|
}
|
|
/**
|
|
* Resolve a supported plugin shape to its executable callback.
|
|
*
|
|
* @param plugin — a function, class, or `{ apply }` object plugin.
|
|
* @returns the callback identifying the plugin, or `undefined` if invalid.
|
|
*/
|
|
resolve(plugin) {
|
|
try {
|
|
if (typeof plugin === "function") return plugin;
|
|
if (isApplicable(plugin)) return plugin.apply;
|
|
} catch {
|
|
}
|
|
}
|
|
/**
|
|
* Look up the runtime record for a plugin.
|
|
*
|
|
* @param plugin — any supported plugin shape.
|
|
* @returns the runtime, or `undefined` when the plugin is not registered.
|
|
*/
|
|
get(plugin) {
|
|
const key = this.resolve(plugin);
|
|
return key && this._internal.get(key);
|
|
}
|
|
/**
|
|
* Check whether a plugin has a registered runtime.
|
|
*
|
|
* @param plugin — any supported plugin shape.
|
|
* @returns `true` when at least one fiber of the plugin exists.
|
|
*/
|
|
has(plugin) {
|
|
const key = this.resolve(plugin);
|
|
return !!key && this._internal.has(key);
|
|
}
|
|
/**
|
|
* Dispose every running fiber for a plugin and remove its runtime record.
|
|
*
|
|
* @param plugin — any supported plugin shape.
|
|
* @returns the removed runtime, or `undefined` when none was registered.
|
|
*/
|
|
delete(plugin) {
|
|
const key = this.resolve(plugin);
|
|
const runtime = key && this._internal.get(key);
|
|
if (!runtime) return;
|
|
this._internal.delete(key);
|
|
for (const fiber of runtime.fibers) fiber.dispose();
|
|
return runtime;
|
|
}
|
|
/** Iterate the registered plugin callbacks. */
|
|
keys() {
|
|
return this._internal.keys();
|
|
}
|
|
/** Iterate the registered plugin runtimes. */
|
|
values() {
|
|
return this._internal.values();
|
|
}
|
|
/** Iterate `[callback, runtime]` pairs. */
|
|
entries() {
|
|
return this._internal.entries();
|
|
}
|
|
/**
|
|
* Visit every registered runtime.
|
|
*
|
|
* @param callback — receives each runtime and its identifying callback.
|
|
*/
|
|
forEach(callback) {
|
|
return this._internal.forEach(callback);
|
|
}
|
|
/**
|
|
* Start a callback once the requested dependencies are available.
|
|
*
|
|
* @param inject — required services, as an array or a name → config map.
|
|
* @param callback — plugin body called with `(ctx, config)`.
|
|
* @returns the fiber; awaiting it settles once loading finished.
|
|
*/
|
|
inject(inject, callback) {
|
|
return this.plugin({
|
|
inject,
|
|
apply: callback,
|
|
name: callback.name
|
|
});
|
|
}
|
|
/**
|
|
* Start a plugin in the current context and return its fiber.
|
|
*
|
|
* Creates (or reuses) the plugin's runtime record, then starts a new fiber
|
|
* under the current context. Throws if `plugin` is not a supported shape or
|
|
* if the current fiber is already disposed.
|
|
*
|
|
* @param plugin — a function, class, or `{ apply }` object plugin.
|
|
* @param config — the plugin config, validated against its `Config` schema.
|
|
* @param getOuterStack — captures the caller stack for effect diagnostics.
|
|
* @returns the fiber; awaiting it settles once loading finished.
|
|
*/
|
|
plugin(plugin, config, getOuterStack = buildOuterStack()) {
|
|
const callback = this.resolve(plugin);
|
|
if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin);
|
|
this.ctx.fiber.assertActive();
|
|
let runtime = this._internal.get(callback);
|
|
if (!runtime) {
|
|
let name = plugin.name;
|
|
if (name === "apply") name = void 0;
|
|
runtime = {
|
|
name,
|
|
callback,
|
|
fibers: new DisposableList(),
|
|
Config: plugin.Config
|
|
};
|
|
this._internal.set(callback, runtime);
|
|
}
|
|
const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack);
|
|
const wrapped = Object.create(fiber);
|
|
wrapped.then = (onFulfilled, onRejected) => {
|
|
return fiber.await().then(onFulfilled, onRejected);
|
|
};
|
|
return wrapped;
|
|
}
|
|
};
|
|
var Context = class Context2 {
|
|
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
|
|
static effect = symbols.effect;
|
|
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
|
|
static filter = symbols.filter;
|
|
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
|
|
static isolate = symbols.isolate;
|
|
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
|
|
static intercept = symbols.intercept;
|
|
/**
|
|
* Returns true for Cordis context proxies and context prototypes.
|
|
*
|
|
* Works across realms and across multiple copies of cordis, because the
|
|
* brand is keyed by a global symbol rather than by `instanceof`.
|
|
*
|
|
* @param value — the value to test.
|
|
* @returns `true` if `value` is a Cordis context, narrowing its type.
|
|
*/
|
|
static is(value) {
|
|
return !!value?.[Context2.is];
|
|
}
|
|
static {
|
|
Context2.is[Symbol.toPrimitive] = () => /* @__PURE__ */ Symbol.for("cordis.is");
|
|
Context2.prototype[Context2.is] = true;
|
|
}
|
|
/** Create the root context and install the built-in services. */
|
|
constructor() {
|
|
this[symbols.isolate] = /* @__PURE__ */ Object.create(null);
|
|
this[symbols.intercept] = /* @__PURE__ */ Object.create(null);
|
|
const self = new Proxy(this, ReflectService.handler);
|
|
this.root = self;
|
|
this.baseUrl = void 0;
|
|
this.fiber = new Fiber(self, {}, /* @__PURE__ */ Object.create(null), null, () => []);
|
|
this.reflect = new ReflectService(self);
|
|
this.registry = new RegistryService(self);
|
|
this.events = new EventsService(self);
|
|
this.logger = new LoggerService(self);
|
|
this.fiber._disposables.clear();
|
|
return self;
|
|
}
|
|
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
return `Context <${this.fiber.name}>`;
|
|
}
|
|
/**
|
|
* Create a child context with extra metadata on top of the current scope.
|
|
*
|
|
* The child prototypally inherits every property of this context; own
|
|
* properties of `meta` shadow the inherited ones. The parent is not mutated.
|
|
*
|
|
* @param meta — own properties (including symbol keys) to define on the child.
|
|
* @returns a child context inheriting from this one.
|
|
*/
|
|
extend(meta = {}) {
|
|
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value;
|
|
const self = Object.create(getTraceable(this, this));
|
|
for (const prop of Reflect.ownKeys(meta)) Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop));
|
|
if (!shadow) return self;
|
|
return Object.assign(Object.create(self), { [symbols.shadow]: shadow });
|
|
}
|
|
/**
|
|
* Create a child context with an independent service scope for `name`.
|
|
*
|
|
* Below the returned context, reads and writes of the service `name`
|
|
* resolve against the new label instead of the parent's, so a different
|
|
* implementation can be provided without affecting the parent scope.
|
|
* Passing the same `label` to two `isolate()` calls joins their scopes.
|
|
*
|
|
* @param name — the service name to isolate.
|
|
* @param label — scope label to join; defaults to a fresh unique symbol.
|
|
* @returns a child context whose `name` service resolves in the new scope.
|
|
*/
|
|
isolate(name, label) {
|
|
const shadow = Object.create(this[symbols.isolate]);
|
|
shadow[name] = label ?? Symbol(name);
|
|
return this.extend({ [symbols.isolate]: shadow });
|
|
}
|
|
intercept(name, config) {
|
|
const intercept = Object.create(this[symbols.intercept]);
|
|
intercept[name] = config;
|
|
return this.extend({ [symbols.intercept]: intercept });
|
|
}
|
|
};
|
|
var Service = class Service2 {
|
|
ctx;
|
|
/** Symbol key of an instance method run after construction (class plugins). */
|
|
static init = symbols.init;
|
|
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
|
|
static check = symbols.check;
|
|
/** Symbol key of the phantom intercept-config type parameter. */
|
|
static config = symbols.config;
|
|
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
|
|
static invoke = symbols.invoke;
|
|
/** Symbol key of the helper deriving an extended service instance. */
|
|
static extend = symbols.extend;
|
|
/** Symbol key of the tracker metadata used for context tracing. */
|
|
static tracker = symbols.tracker;
|
|
/** Symbol key of the intercept-config resolution helper below. */
|
|
static resolveConfig = symbols.resolveConfig;
|
|
/** The service name this instance is registered under. */
|
|
name;
|
|
/**
|
|
* Register this instance as `name` in the current context.
|
|
*
|
|
* Calls `ctx.reflect.provide(name, this, this[Service.check])`, so the
|
|
* service is unregistered automatically when the owning fiber unloads.
|
|
* Services with a `[Service.invoke]` body return a callable instance.
|
|
*
|
|
* @param ctx — the context to register in (stored as `this.ctx`).
|
|
* @param name — the service name; defaults to the static `provide` field.
|
|
*/
|
|
constructor(ctx, name) {
|
|
this.ctx = ctx;
|
|
name ??= this.constructor["provide"];
|
|
let self = this;
|
|
const tracker = {
|
|
associate: name,
|
|
property: "ctx"
|
|
};
|
|
if (self[symbols.invoke]) self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker);
|
|
self.ctx = ctx;
|
|
self.name = name;
|
|
defineProperty(self, symbols.tracker, tracker);
|
|
self.ctx.reflect.provide(name, self, this[symbols.check]);
|
|
return self;
|
|
}
|
|
[symbols.filter](ctx) {
|
|
return ctx[symbols.isolate][this.name] === this.ctx[symbols.isolate][this.name];
|
|
}
|
|
[symbols.extend](props) {
|
|
let self;
|
|
if (this[Service2.invoke]) self = createCallable(this.name, this, this[symbols.tracker]);
|
|
else self = Object.create(this);
|
|
return Object.assign(self, props);
|
|
}
|
|
/**
|
|
* Merge intercept config from ancestors with optional base and head values.
|
|
*
|
|
* Entries added closer to the root apply first; `base` is prepended and
|
|
* `head` appended. Uses `Config.merge` when the service declares one,
|
|
* otherwise a shallow `Object.assign`.
|
|
*
|
|
* @param base — lowest-precedence config merged before all intercepts.
|
|
* @param head — highest-precedence config merged after all intercepts.
|
|
* @returns the merged config.
|
|
*/
|
|
[symbols.resolveConfig](base, head) {
|
|
let intercept = this.ctx[Context.intercept];
|
|
const configs = [];
|
|
while (this.name in intercept) {
|
|
if (Object.hasOwn(intercept, this.name)) configs.unshift(intercept[this.name]);
|
|
intercept = Object.getPrototypeOf(intercept);
|
|
}
|
|
if (base) configs.unshift(base);
|
|
if (head) configs.push(head);
|
|
if (this["Config"]?.merge) return this["Config"].merge(...configs);
|
|
else return Object.assign({}, ...configs);
|
|
}
|
|
static [Symbol.hasInstance](instance) {
|
|
if (!instance) return false;
|
|
let constructor = instance.constructor;
|
|
while (constructor) {
|
|
constructor = constructor.prototype?.constructor;
|
|
if (constructor === this) return true;
|
|
constructor &&= Object.getPrototypeOf(constructor);
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// src/shims/owned.ts
|
|
function describeError(error) {
|
|
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
}
|
|
var OwnedRegistrations = class {
|
|
/** Admitted entries by core handle. */
|
|
byHandle = /* @__PURE__ */ new Map();
|
|
rpc;
|
|
kind;
|
|
/** The owner's own index of this kind, for the leak report at deactivation. */
|
|
ownedBy;
|
|
warn;
|
|
constructor(rpc, kind, ownedBy, warn) {
|
|
this.rpc = rpc;
|
|
this.kind = kind;
|
|
this.ownedBy = ownedBy;
|
|
this.warn = warn;
|
|
}
|
|
/** Called inside the owner's effect; returns the effect's cleanup. */
|
|
add(entry, spec) {
|
|
const owner = entry.owner;
|
|
if (owner.state !== "activating" && owner.state !== "active") throw new Error("registration owner was disposed");
|
|
const registration = this.rpc.request("registry/register", { owner: owner.ref, ...owner.scope ? { scope: owner.scope } : {}, kind: this.kind, spec }).then(
|
|
(result) => {
|
|
if (typeof result?.handle === "number") {
|
|
entry.handle = result.handle;
|
|
if (entry.disposed || owner.state !== "activating" && owner.state !== "active") {
|
|
return this.unregister(entry);
|
|
}
|
|
this.ownedBy(owner).set(result.handle, entry);
|
|
this.byHandle.set(result.handle, entry);
|
|
} else {
|
|
const reason = typeof result?.refused === "string" ? result.refused : "refused without a reason";
|
|
owner.refusals.push(`${this.kind} \`${entry.name}\` refused: ${reason}`);
|
|
if (owner.state === "active") this.warn(`${this.kind} \`${entry.name}\` refused: ${reason}`, owner);
|
|
}
|
|
},
|
|
(error) => {
|
|
owner.refusals.push(`${this.kind} \`${entry.name}\` registration failed: ${describeError(error)}`);
|
|
}
|
|
).finally(() => owner.pendingRegistrations.delete(registration));
|
|
owner.pendingRegistrations.add(registration);
|
|
return () => {
|
|
if (entry.disposed) return;
|
|
entry.disposed = true;
|
|
if (entry.handle !== void 0) void this.unregister(entry);
|
|
};
|
|
}
|
|
/** Undo exactly this entry. The core revokes first, so a failure here is expected after revocation. */
|
|
unregister(entry) {
|
|
const handle = entry.handle;
|
|
const owner = entry.owner;
|
|
this.ownedBy(owner).delete(handle);
|
|
this.byHandle.delete(handle);
|
|
const cleanup = this.rpc.request("registry/unregister", { owner: owner.ref, handle }).then(() => void 0, () => void 0).finally(() => owner.pendingRegistrations.delete(cleanup));
|
|
owner.pendingRegistrations.add(cleanup);
|
|
return cleanup;
|
|
}
|
|
/** Drop the host's index of everything `owner` registered (after its teardown). */
|
|
forget(owner) {
|
|
for (const entry of this.ownedBy(owner).values()) this.byHandle.delete(entry.handle);
|
|
}
|
|
};
|
|
|
|
// src/shims/skills.ts
|
|
var MAX_SKILL_ROOTS_PER_OWNER = 8;
|
|
var MAX_SKILL_ROOTS_PER_HOST = 64;
|
|
function normalizeSkillRoot(value) {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).some((key) => key !== "path")) throw new TypeError("skill root supports only path");
|
|
const { path } = value;
|
|
if (typeof path !== "string" || !path || Buffer.byteLength(path, "utf8") > 512 || /[\\:\u0000-\u001f\u007f-\u009f]/u.test(path) || path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
throw new TypeError("skill root path must be a bounded bundle-relative path with normal slash-separated components");
|
|
}
|
|
return Object.freeze({ path });
|
|
}
|
|
var SkillRoots = class {
|
|
registrations;
|
|
owners = /* @__PURE__ */ new Map();
|
|
count = 0;
|
|
constructor(rpc, ownedBy, warn) {
|
|
this.registrations = new OwnedRegistrations(rpc, "skill_root", ownedBy, warn);
|
|
}
|
|
register(owner, definition) {
|
|
if (owner.state !== "activating" && owner.state !== "active") throw new Error("skill owner is not live");
|
|
const { path } = normalizeSkillRoot(definition);
|
|
const roots = this.owners.get(owner) ?? /* @__PURE__ */ new Map();
|
|
if (roots.has(path)) throw new Error("skill root is already registered; dispose it before registering it again");
|
|
if (roots.size >= MAX_SKILL_ROOTS_PER_OWNER || this.count >= MAX_SKILL_ROOTS_PER_HOST) throw new RangeError("skill root owner or host registration limit reached");
|
|
const undo = this.registrations.add({ owner, name: path, disposed: false }, { name: path, description: "" });
|
|
const dispose = () => {
|
|
if (roots.get(path) !== dispose) return;
|
|
roots.delete(path);
|
|
this.count--;
|
|
if (!roots.size) this.owners.delete(owner);
|
|
undo();
|
|
};
|
|
roots.set(path, dispose);
|
|
this.owners.set(owner, roots);
|
|
this.count++;
|
|
return dispose;
|
|
}
|
|
forget(owner) {
|
|
for (const dispose of [...this.owners.get(owner)?.values() ?? []]) dispose();
|
|
this.registrations.forget(owner);
|
|
}
|
|
};
|
|
function defineSkillsService(host) {
|
|
class SkillsShim extends Service {
|
|
constructor(ctx) {
|
|
super(ctx, "skills");
|
|
}
|
|
registerRoot(definition) {
|
|
const ctx = this.ctx;
|
|
const owner = host.ownerOf(ctx);
|
|
if (!owner) throw new Error("skills.registerRoot called outside an extension owner");
|
|
const root = normalizeSkillRoot(definition);
|
|
return ctx.effect(() => host.skillRoots.register(owner, root), `skills.registerRoot(${JSON.stringify(root.path)})`);
|
|
}
|
|
}
|
|
Object.freeze(SkillsShim.prototype);
|
|
return SkillsShim;
|
|
}
|
|
export {
|
|
MAX_SKILL_ROOTS_PER_HOST,
|
|
MAX_SKILL_ROOTS_PER_OWNER,
|
|
SkillRoots,
|
|
defineSkillsService,
|
|
normalizeSkillRoot
|
|
};
|