正在写deepseek透传的相关事宜

This commit is contained in:
2026-08-18 10:12:26 +08:00
parent 5368da3be7
commit 08df19bde6
3219 changed files with 426272 additions and 0 deletions
@@ -0,0 +1 @@
../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,43 @@
# @cordisjs/plugin-include
File-backed loader tree for Cordis. The include plugin reads a YAML or JSON
file, turns it into loader entries, and writes updates back when the file is
writable.
## Usage
```ts
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
const root = new Context()
await root.plugin(Loader, { baseUrl: import.meta.url })
await root.plugin(Include, {
path: './cordis.yml',
initial: [],
enableLogs: true,
})
```
Example `cordis.yml`:
```yaml
- id: timer
name: '@cordisjs/plugin-timer'
- id: app
name: ./plugins/app
config:
message: hello
```
## Config
| Field | Description |
| --- | --- |
| `path` | YAML or JSON file path resolved from `ctx.baseUrl`. |
| `initial` | Entry list written when the file is missing. |
| `patches` | Runtime patches applied after reading the file. |
| `enableLogs` | Enables loader apply, reload, and unload logs. |
Patches can insert entries or override fields on entries with a matching `id`.
@@ -0,0 +1,284 @@
import { EntryGroup, EntryTree, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
import { Service } from "@deepseek-ai/cordis";
import { extname } from "node:path";
import { access, constants, readFile, rename, writeFile } from "node:fs/promises";
import { setTimeout as setTimeout$1 } from "node:timers/promises";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as yaml from "js-yaml";
//#region lib/types/index.js
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
});
return path;
};
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
kind: "scalar",
resolve: (data) => typeof data === "string",
construct: (data) => ({ __jsExpr: data }),
predicate: isJsExpr,
represent: (data) => data["__jsExpr"]
});
/**
* The entry-list YAML dialect: `!!js` scalars round-trip as expression nodes
* the Loader evaluates at entry activation. Exported so config tooling
* (`dsh --dump-config`) parses and prints exactly the dialect this include
* mounts.
*/
const entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr);
const schema = entryListSchema;
const writable = {
".json": "application/json",
".yaml": "application/yaml",
".yml": "application/yaml"
};
const supported = new Set(Object.keys(writable));
const WRITE_RETRY_LIMIT = 10;
const WRITE_RETRY_DELAY_MS = 50;
function retryableWriteError(error) {
const code = error?.code;
return code === "EACCES" || code === "EBUSY" || code === "EPERM";
}
/**
* Apply patch lists to an entry list — THE patch semantics of this include,
* shared by mounting (`applyPatches`) and offline config tooling
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
* is never mutated and the result is always detached from it (even with no
* patches): patching or mounting shared entry objects would bake earlier
* values into the cached parse, so repeated application (config hot-reloads)
* could never revert a removed or changed patch. Inserted entries are indexed
* as they are added, so a later patch in the same list can target a row an
* earlier patch inserted. A patch that matches nothing warns and is skipped.
* @param data - the parsed entry list (JSON-safe plain data).
* @param patches - the patch list to apply, in order.
* @param warn - sink for skipped-patch diagnostics (printf-style, `%C` = code).
* @returns a detached entry list with every applicable patch applied.
*/
function applyEntryPatches(data, patches, warn) {
data = structuredClone(data);
if (!patches?.length) return data;
const entryMap = /* @__PURE__ */ new Map();
const buildMap = (entries) => {
for (const entry of entries) {
if (entry.id) entryMap.set(entry.id, entry);
if (entry.group && Array.isArray(entry.config)) buildMap(entry.config);
}
};
buildMap(data);
for (const patch of patches) {
const { id, insert, name, ...overrides } = patch;
if (insert) {
if (id) {
const target = entryMap.get(id);
if (!target) {
warn("patch insert: entry %C not found", id);
continue;
}
if (!target.group) {
warn("patch insert: entry %C is not a group", id);
continue;
}
if (!Array.isArray(target.config)) target.config = [];
target.config.push(...insert);
} else data.push(...insert);
buildMap(insert);
continue;
}
if (!id) {
warn("patch: id is required for non-insert patches");
continue;
}
const target = entryMap.get(id);
if (!target) {
warn("patch: entry %C not found", id);
continue;
}
if (name && name !== target.name) {
warn("patch: name mismatch for %C (expected %C, got %C), skipping", id, target.name, name);
continue;
}
for (const [key, value] of Object.entries(overrides)) {
if (key === "id") continue;
target[key] = value;
}
}
return data;
}
var ConfigFileError = class extends Error {
stage;
constructor(stage, path, cause) {
super(`failed to ${stage} config file ${path}`, { cause });
this.stage = stage;
this.name = "ConfigFileError";
}
};
/** Loader entry tree backed by a YAML or JSON file. */
var Include = class extends EntryTree {
config;
static inject = ["loader"];
static [EntryGroup.key] = true;
filename;
type;
readonly;
content;
data;
writeTask;
pendingWrite;
writeQueue = Promise.resolve();
applyQueue = Promise.resolve();
constructor(ctx, config) {
super(ctx);
this.config = config;
this.enableLogs = config.enableLogs ?? ctx.fiber.entry?.parent.tree.enableLogs ?? false;
this.filename = fileURLToPath(new URL(this.config.path, this.ctx.baseUrl));
const ext = extname(this.filename);
if (!supported.has(ext)) throw new Error(`extension "${ext}" not supported`);
this.type = writable[ext];
this.readonly = !this.type;
this.ctx.baseUrl = new URL(".", pathToFileURL(this.filename)).href;
ctx.on("internal/update", async (config, _, next) => {
if (config.path !== this.config.path) return next();
await this.enqueue(async () => {
const data = this.applyPatches(this.data, config.patches);
await this.root.update(data);
this.config = config;
});
});
}
/**
* Serialize one child-tree mutation behind every earlier one. The group's
* transactional `update` is not reentrant: two concurrent applies (the init
* apply racing an HMR-triggered refresh from the watcher's initial scan)
* interleave create and rollback on the same entries and strand the include
* fiber without settling, so every apply path funnels through this queue.
* A predecessor's failure is its own caller's outcome and never gates the
* next task.
*/
enqueue(task) {
const run = this.applyQueue.then(task, task);
this.applyQueue = run.then(() => {}, () => {});
return run;
}
async checkAccess() {
if (!this.type) return;
try {
await access(this.filename, constants.W_OK);
} catch {
this.readonly = true;
}
}
async read(forced = false) {
let content;
try {
content = await readFile(this.filename, "utf8");
} catch (error) {
throw new ConfigFileError("read", this.filename, error);
}
if (!forced && this.content === content) return;
let data;
try {
if (this.type === "application/yaml") data = yaml.load(content, { schema });
else if (this.type === "application/json") data = JSON.parse(content);
else {
const module = await import(__rewriteRelativeImportExtension(
/* @vite-ignore */
this.filename
));
data = module.default || module;
}
} catch (error) {
throw new ConfigFileError("parse", this.filename, error);
}
if (!Array.isArray(data)) throw new ConfigFileError("validate", this.filename, /* @__PURE__ */ new TypeError("config file must be a top-level array"));
return {
content,
data
};
}
applyPatches(data, patches) {
return applyEntryPatches(data, patches, (message, ...args) => {
this.ctx.root.logger?.("loader").warn(message, ...args);
});
}
async *[Service.init]() {
let candidate;
try {
candidate = await this.read(true);
} catch (error) {
if (!(error instanceof ConfigFileError) || error.stage !== "read" || error.cause?.code !== "ENOENT") throw error;
if (this.config.initial) {
await this._writeFile(this.config.initial);
candidate = await this.read(true);
} else throw new Error(`config file not found: ${this.filename}`);
}
yield () => this.stop();
await this.apply(candidate);
}
async stop() {
await this.root.stop();
await this.flushWrite();
}
/**
* Re-read the file and transactionally refresh child entries when content changed.
* @returns a promise resolving after the new tree commits, or immediately when unchanged.
* @throws when reading, parsing, validation, application, or rollback fails; the last good tree remains active when rollback succeeds.
*/
async refresh() {
await this.enqueue(async () => {
const candidate = await this.read();
if (!candidate) return;
await this._apply(candidate);
});
}
apply(candidate) {
return this.enqueue(() => this._apply(candidate));
}
async _apply(candidate) {
const data = this.applyPatches(candidate.data, this.config.patches);
await this.root.update(data);
this.content = candidate.content;
this.data = candidate.data;
await this.checkAccess();
}
async _writeFile(config) {
if (this.readonly) throw new Error(`cannot overwrite readonly config`);
if (this.type === "application/yaml") this.content = yaml.dump(config, { schema });
else if (this.type === "application/json") this.content = JSON.stringify(config, null, 2);
await writeFile(this.filename + ".tmp", this.content);
for (let retry = 0;; retry++) try {
await rename(this.filename + ".tmp", this.filename);
return;
} catch (error) {
if (!retryableWriteError(error) || retry >= WRITE_RETRY_LIMIT) throw error;
await setTimeout$1((retry + 1) * WRITE_RETRY_DELAY_MS);
}
}
writeFile(config) {
clearTimeout(this.writeTask);
this.pendingWrite = config;
this.writeTask = setTimeout(() => {
this.flushWrite();
}, 0);
}
flushWrite() {
clearTimeout(this.writeTask);
this.writeTask = void 0;
const config = this.pendingWrite;
this.pendingWrite = void 0;
if (config === void 0) return this.writeQueue;
const run = this.writeQueue.then(() => this._writeFile(config), () => this._writeFile(config));
this.writeQueue = run;
run.catch((error) => {
this.ctx.root.logger?.("loader").warn("failed to write config file %C", this.filename);
this.ctx.root.logger?.("loader").warn(error);
});
return run;
}
/** Schedule a write of the current root entry data. */
write() {
this.context.emit("loader/config-update");
return this.writeFile(this.root.data);
}
};
//#endregion
export { Include, Include as default, applyEntryPatches, entryListSchema };
@@ -0,0 +1,99 @@
import { EntryGroup, EntryTree, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader';
import { Context, Service } from '@deepseek-ai/cordis';
import * as yaml from 'js-yaml';
/**
* The entry-list YAML dialect: `!!js` scalars round-trip as expression nodes
* the Loader evaluates at entry activation. Exported so config tooling
* (`dsh --dump-config`) parses and prints exactly the dialect this include
* mounts.
*/
export declare const entryListSchema: yaml.Schema;
/**
* Apply patch lists to an entry list — THE patch semantics of this include,
* shared by mounting (`applyPatches`) and offline config tooling
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
* is never mutated and the result is always detached from it (even with no
* patches): patching or mounting shared entry objects would bake earlier
* values into the cached parse, so repeated application (config hot-reloads)
* could never revert a removed or changed patch. Inserted entries are indexed
* as they are added, so a later patch in the same list can target a row an
* earlier patch inserted. A patch that matches nothing warns and is skipped.
* @param data - the parsed entry list (JSON-safe plain data).
* @param patches - the patch list to apply, in order.
* @param warn - sink for skipped-patch diagnostics (printf-style, `%C` = code).
* @returns a detached entry list with every applicable patch applied.
*/
export declare function applyEntryPatches(data: EntryOptions[], patches: PatchOptions[] | undefined, warn: (message: string, ...args: any[]) => void): EntryOptions[];
/** Runtime patch applied to entries loaded from an included config file. */
export interface PatchOptions {
id?: string;
insert?: EntryOptions[];
name?: string;
config?: any;
group?: boolean | null;
disabled?: boolean | null;
inject?: any;
intercept?: any;
isolate?: any;
[key: string]: any;
}
/** Config namespace for the file-backed include loader. */
export declare namespace Include {
/** Config for a file-backed loader subtree. */
interface Config {
/** YAML or JSON path resolved from `ctx.baseUrl`. */
path: string;
/** Entry list written when the file does not already exist. */
initial?: any[];
/** Runtime patches applied after reading the file. */
patches?: PatchOptions[];
/** Enables loader apply/reload/unload logs for this subtree. */
enableLogs?: boolean;
}
}
/** Loader entry tree backed by a YAML or JSON file. */
export declare class Include extends EntryTree {
config: Include.Config;
static inject: string[];
static readonly [EntryGroup.key] = true;
filename: string;
private type?;
private readonly;
private content?;
private data?;
private writeTask?;
private pendingWrite?;
private writeQueue;
private applyQueue;
constructor(ctx: Context, config: Include.Config);
/**
* Serialize one child-tree mutation behind every earlier one. The group's
* transactional `update` is not reentrant: two concurrent applies (the init
* apply racing an HMR-triggered refresh from the watcher's initial scan)
* interleave create and rollback on the same entries and strand the include
* fiber without settling, so every apply path funnels through this queue.
* A predecessor's failure is its own caller's outcome and never gates the
* next task.
*/
private enqueue;
private checkAccess;
private read;
private applyPatches;
[Service.init](): AsyncGenerator<() => Promise<void>, void, unknown>;
stop(): Promise<void>;
/**
* Re-read the file and transactionally refresh child entries when content changed.
* @returns a promise resolving after the new tree commits, or immediately when unchanged.
* @throws when reading, parsing, validation, application, or rollback fails; the last good tree remains active when rollback succeeds.
*/
refresh(): Promise<void>;
private apply;
private _apply;
private _writeFile;
private writeFile;
private flushWrite;
/** Schedule a write of the current root entry data. */
write(): void;
}
export default Include;
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAY,KAAK,YAAY,EAAE,MAAM,mCAAmC,CAAA;AACtG,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKtD,OAAO,KAAK,IAAI,MAAM,SAAS,CAAA;AAU/B;;;;;GAKG;AACH,eAAO,MAAM,eAAe,aAAkC,CAAA;AAoB9D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,EAAE,EACpB,OAAO,EAAE,YAAY,EAAE,GAAG,SAAS,EACnC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAC9C,YAAY,EAAE,CAkEhB;AAgBD,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,YAAY,EAAE,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,SAAS,CAAC,EAAE,GAAG,CAAA;IACf,OAAO,CAAC,EAAE,GAAG,CAAA;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,2DAA2D;AAC3D,yBAAiB,OAAO,CAAC;IACvB,+CAA+C;IAC/C,UAAiB,MAAM;QACrB,qDAAqD;QACrD,IAAI,EAAE,MAAM,CAAA;QACZ,+DAA+D;QAC/D,OAAO,CAAC,EAAE,GAAG,EAAE,CAAA;QACf,sDAAsD;QACtD,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;QACxB,gEAAgE;QAChE,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB;CACF;AAED,uDAAuD;AACvD,qBAAa,OAAQ,SAAQ,SAAS;IAoBH,MAAM,EAAE,OAAO,CAAC,MAAM;IAnBvD,MAAM,CAAC,MAAM,WAAa;IAO1B,MAAM,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,QAAO;IAEhC,QAAQ,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,IAAI,CAAC,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAC,CAAQ;IACxB,OAAO,CAAC,IAAI,CAAC,CAAgB;IAC7B,OAAO,CAAC,SAAS,CAAC,CAA4B;IAC9C,OAAO,CAAC,YAAY,CAAC,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,UAAU,CAAsC;gBAE5C,GAAG,EAAE,OAAO,EAAS,MAAM,EAAE,OAAO,CAAC,MAAM;IAsBvD;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO;YAMD,WAAW;YASX,IAAI;IA2BlB,OAAO,CAAC,YAAY;IAMb,CAAC,OAAO,CAAC,IAAI,CAAC;IAkBf,IAAI;IAKV;;;;OAIG;IACG,OAAO;IAUb,OAAO,CAAC,KAAK;YAIC,MAAM;YAQN,UAAU;IAqBxB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,UAAU;IAkBlB,uDAAuD;IACvD,KAAK;CAIN;AAED,eAAe,OAAO,CAAA"}
@@ -0,0 +1,55 @@
#!/bin/sh
# Resolve $0 through symlinks so basedir is the shim's real directory.
# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim.
link="$0"
hops=0
while [ -L "$link" ] && [ "$hops" -lt 40 ]; do
hops=$((hops+1))
target=$(readlink "$link")
case "$target" in
/*) link="$target" ;;
*) link="$(dirname "$link")/$target" ;;
esac
done
basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')")
basedir_win="$basedir"
exe=""
msys=""
case `uname -a` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir_win=`cygpath -w "$basedir"`
fi
exe=".exe"
msys="true"
;;
*WSL2*)
if command -v wslpath > /dev/null 2>&1; then
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
basedir_win="$basedir"
else
exe=".exe"
fi
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
exec "$basedir/node.exe" "$basedir_win/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
elif [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
elif command -v node >/dev/null 2>&1; then
exec node "$basedir/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
exec node.exe "$basedir_win/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
else
exec node "$basedir/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
fi
# cmd-shim-target=/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js
@@ -0,0 +1,55 @@
#!/bin/sh
# Resolve $0 through symlinks so basedir is the shim's real directory.
# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim.
link="$0"
hops=0
while [ -L "$link" ] && [ "$hops" -lt 40 ]; do
hops=$((hops+1))
target=$(readlink "$link")
case "$target" in
/*) link="$target" ;;
*) link="$(dirname "$link")/$target" ;;
esac
done
basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')")
basedir_win="$basedir"
exe=""
msys=""
case `uname -a` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir_win=`cygpath -w "$basedir"`
fi
exe=".exe"
msys="true"
;;
*WSL2*)
if command -v wslpath > /dev/null 2>&1; then
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
basedir_win="$basedir"
else
exe=".exe"
fi
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/js-yaml@4.3.1/node_modules/js-yaml/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/js-yaml@4.3.1/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/js-yaml@4.3.1/node_modules/js-yaml/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/js-yaml@4.3.1/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
exec "$basedir/node.exe" "$basedir_win/../../../../../../js-yaml@4.3.1/node_modules/js-yaml/bin/js-yaml.js" "$@"
elif [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../../../../../js-yaml@4.3.1/node_modules/js-yaml/bin/js-yaml.js" "$@"
elif command -v node >/dev/null 2>&1; then
exec node "$basedir/../../../../../../js-yaml@4.3.1/node_modules/js-yaml/bin/js-yaml.js" "$@"
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
exec node.exe "$basedir_win/../../../../../../js-yaml@4.3.1/node_modules/js-yaml/bin/js-yaml.js" "$@"
else
exec node "$basedir/../../../../../../js-yaml@4.3.1/node_modules/js-yaml/bin/js-yaml.js" "$@"
fi
# cmd-shim-target=/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/js-yaml@4.3.1/node_modules/js-yaml/bin/js-yaml.js
@@ -0,0 +1,40 @@
{
"name": "@deepseek-ai/cordis-plugin-include",
"description": "Include files in cordis configurations",
"version": "1.0.6",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "vendor/include"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
"@deepseek-ai/cordis": "^4.0.1"
},
"dependencies": {
"js-yaml": "^4.1.0",
"@deepseek-ai/cosmokit": "^1.8.2"
}
}
@@ -0,0 +1,377 @@
import { EntryGroup, EntryTree, isJsExpr, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import { Context, Service } from '@deepseek-ai/cordis'
import { extname } from 'node:path'
import { access, constants, readFile, rename, writeFile } from 'node:fs/promises'
import { setTimeout as delay } from 'node:timers/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
import * as yaml from 'js-yaml'
const JsExpr = new yaml.Type('tag:yaml.org,2002:js', {
kind: 'scalar',
resolve: (data) => typeof data === 'string',
construct: (data) => ({ __jsExpr: data }),
predicate: isJsExpr,
represent: (data) => data['__jsExpr'],
})
/**
* The entry-list YAML dialect: `!!js` scalars round-trip as expression nodes
* the Loader evaluates at entry activation. Exported so config tooling
* (`dsh --dump-config`) parses and prints exactly the dialect this include
* mounts.
*/
export const entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr)
const schema = entryListSchema
const writable: Record<string, string> = {
'.json': 'application/json',
'.yaml': 'application/yaml',
'.yml': 'application/yaml',
}
const supported = new Set(Object.keys(writable))
const WRITE_RETRY_LIMIT = 10
const WRITE_RETRY_DELAY_MS = 50
function retryableWriteError(error: unknown): boolean {
const code = (error as NodeJS.ErrnoException | null)?.code
return code === 'EACCES' || code === 'EBUSY' || code === 'EPERM'
}
/**
* Apply patch lists to an entry list — THE patch semantics of this include,
* shared by mounting (`applyPatches`) and offline config tooling
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
* is never mutated and the result is always detached from it (even with no
* patches): patching or mounting shared entry objects would bake earlier
* values into the cached parse, so repeated application (config hot-reloads)
* could never revert a removed or changed patch. Inserted entries are indexed
* as they are added, so a later patch in the same list can target a row an
* earlier patch inserted. A patch that matches nothing warns and is skipped.
* @param data - the parsed entry list (JSON-safe plain data).
* @param patches - the patch list to apply, in order.
* @param warn - sink for skipped-patch diagnostics (printf-style, `%C` = code).
* @returns a detached entry list with every applicable patch applied.
*/
export function applyEntryPatches(
data: EntryOptions[],
patches: PatchOptions[] | undefined,
warn: (message: string, ...args: any[]) => void,
): EntryOptions[] {
data = structuredClone(data)
if (!patches?.length) return data
const entryMap = new Map<string, EntryOptions>()
const buildMap = (entries: EntryOptions[]) => {
for (const entry of entries) {
if (entry.id) entryMap.set(entry.id, entry)
if (entry.group && Array.isArray(entry.config)) {
buildMap(entry.config)
}
}
}
buildMap(data)
for (const patch of patches) {
const { id, insert, name, ...overrides } = patch
if (insert) {
if (id) {
const target = entryMap.get(id)
if (!target) {
warn('patch insert: entry %C not found', id)
continue
}
if (!target.group) {
warn('patch insert: entry %C is not a group', id)
continue
}
if (!Array.isArray(target.config)) target.config = []
target.config.push(...insert)
} else {
data.push(...insert)
}
// Index what this patch added so a LATER patch in the same list can
// target it. Patch lists compose one layer per source (each bundle
// layer, then the user's, then `--patch` overlays), and a layer must be
// able to configure or disable a row an earlier layer inserted; without
// this, inserted rows were silently unpatchable.
buildMap(insert)
continue
}
if (!id) {
warn('patch: id is required for non-insert patches')
continue
}
const target = entryMap.get(id)
if (!target) {
warn('patch: entry %C not found', id)
continue
}
if (name && name !== target.name) {
warn('patch: name mismatch for %C (expected %C, got %C), skipping', id, target.name, name)
continue
}
for (const [key, value] of Object.entries(overrides)) {
if (key === 'id') continue
target[key] = value
}
}
return data
}
type ConfigUpdateStage = 'read' | 'parse' | 'validate'
interface ReadCandidate {
content: string
data: EntryOptions[]
}
class ConfigFileError extends Error {
constructor(public readonly stage: ConfigUpdateStage, path: string, cause: unknown) {
super(`failed to ${stage} config file ${path}`, { cause })
this.name = 'ConfigFileError'
}
}
/** Runtime patch applied to entries loaded from an included config file. */
export interface PatchOptions {
id?: string
insert?: EntryOptions[]
name?: string
config?: any
group?: boolean | null
disabled?: boolean | null
inject?: any
intercept?: any
isolate?: any
[key: string]: any
}
/** Config namespace for the file-backed include loader. */
export namespace Include {
/** Config for a file-backed loader subtree. */
export interface Config {
/** YAML or JSON path resolved from `ctx.baseUrl`. */
path: string
/** Entry list written when the file does not already exist. */
initial?: any[]
/** Runtime patches applied after reading the file. */
patches?: PatchOptions[]
/** Enables loader apply/reload/unload logs for this subtree. */
enableLogs?: boolean
}
}
/** Loader entry tree backed by a YAML or JSON file. */
export class Include extends EntryTree {
static inject = ['loader']
// Tree-carrier marker (the Group plugin declares the same): this config is
// entry and patch lists, so the Loader's `internal/config` interpolation
// keeps it literal — a `!!js` expression inside a nested row's config
// belongs to that row's fiber, resolving lazily in the row's own context.
// Include's own fields (`path`, `enableLogs`) therefore stay literal too.
static readonly [EntryGroup.key] = true
public filename: string
private type?: string
private readonly: boolean
private content?: string
private data?: EntryOptions[]
private writeTask?: NodeJS.Timeout | undefined
private pendingWrite?: EntryOptions[]
private writeQueue: Promise<void> = Promise.resolve()
private applyQueue: Promise<unknown> = Promise.resolve()
constructor(ctx: Context, public config: Include.Config) {
super(ctx)
this.enableLogs = config.enableLogs ?? ctx.fiber.entry?.parent.tree.enableLogs ?? false
this.filename = fileURLToPath(new URL(this.config.path, this.ctx.baseUrl))
const ext = extname(this.filename)
if (!supported.has(ext)) {
throw new Error(`extension "${ext}" not supported`)
}
this.type = writable[ext]
this.readonly = !this.type
this.ctx.baseUrl = new URL('.', pathToFileURL(this.filename)).href
ctx.on('internal/update', async (config, _, next) => {
if (config.path !== this.config.path) return next()
await this.enqueue(async () => {
const data = this.applyPatches(this.data!, config.patches)
await this.root.update(data)
this.config = config
})
})
}
/**
* Serialize one child-tree mutation behind every earlier one. The group's
* transactional `update` is not reentrant: two concurrent applies (the init
* apply racing an HMR-triggered refresh from the watcher's initial scan)
* interleave create and rollback on the same entries and strand the include
* fiber without settling, so every apply path funnels through this queue.
* A predecessor's failure is its own caller's outcome and never gates the
* next task.
*/
private enqueue<T>(task: () => Promise<T>): Promise<T> {
const run = this.applyQueue.then(task, task)
this.applyQueue = run.then(() => {}, () => {})
return run
}
private async checkAccess() {
if (!this.type) return
try {
await access(this.filename, constants.W_OK)
} catch {
this.readonly = true
}
}
private async read(forced = false): Promise<ReadCandidate | undefined> {
let content: string
try {
content = await readFile(this.filename, 'utf8')
} catch (error) {
throw new ConfigFileError('read', this.filename, error)
}
if (!forced && this.content === content) return
let data: any
try {
if (this.type === 'application/yaml') {
data = yaml.load(content, { schema })
} else if (this.type === 'application/json') {
data = JSON.parse(content)
} else {
const module = await import(/* @vite-ignore */ this.filename)
data = module.default || module
}
} catch (error) {
throw new ConfigFileError('parse', this.filename, error)
}
if (!Array.isArray(data)) {
throw new ConfigFileError('validate', this.filename, new TypeError('config file must be a top-level array'))
}
return { content, data }
}
private applyPatches(data: EntryOptions[], patches?: PatchOptions[]): EntryOptions[] {
return applyEntryPatches(data, patches, (message, ...args) => {
this.ctx.root.logger?.('loader').warn(message, ...args)
})
}
async* [Service.init]() {
let candidate: ReadCandidate
try {
candidate = (await this.read(true))!
} catch (error) {
if (!(error instanceof ConfigFileError) || error.stage !== 'read' || (error.cause as NodeJS.ErrnoException)?.code !== 'ENOENT') throw error
if (this.config.initial) {
await this._writeFile(this.config.initial as any)
candidate = (await this.read(true))!
} else {
throw new Error(`config file not found: ${this.filename}`)
}
}
yield () => this.stop()
await this.apply(candidate)
}
async stop() {
await this.root.stop()
await this.flushWrite()
}
/**
* Re-read the file and transactionally refresh child entries when content changed.
* @returns a promise resolving after the new tree commits, or immediately when unchanged.
* @throws when reading, parsing, validation, application, or rollback fails; the last good tree remains active when rollback succeeds.
*/
async refresh() {
// Read inside the queue so the changed-content check compares against the
// predecessor's committed state, not a mid-apply snapshot.
await this.enqueue(async () => {
const candidate = await this.read()
if (!candidate) return
await this._apply(candidate)
})
}
private apply(candidate: ReadCandidate) {
return this.enqueue(() => this._apply(candidate))
}
private async _apply(candidate: ReadCandidate) {
const data = this.applyPatches(candidate.data, this.config.patches)
await this.root.update(data)
this.content = candidate.content
this.data = candidate.data
await this.checkAccess()
}
private async _writeFile(config: EntryOptions[]) {
if (this.readonly) {
throw new Error(`cannot overwrite readonly config`)
}
if (this.type === 'application/yaml') {
this.content = yaml.dump(config, { schema })
} else if (this.type === 'application/json') {
this.content = JSON.stringify(config, null, 2)
}
await writeFile(this.filename + '.tmp', this.content!)
for (let retry = 0; ; retry++) {
try {
await rename(this.filename + '.tmp', this.filename)
return
} catch (error) {
if (!retryableWriteError(error) || retry >= WRITE_RETRY_LIMIT) throw error
await delay((retry + 1) * WRITE_RETRY_DELAY_MS)
}
}
}
private writeFile(config: EntryOptions[]) {
clearTimeout(this.writeTask)
this.pendingWrite = config
this.writeTask = setTimeout(() => {
void this.flushWrite()
}, 0)
}
private flushWrite(): Promise<void> {
clearTimeout(this.writeTask)
this.writeTask = undefined
const config = this.pendingWrite
this.pendingWrite = undefined
if (config === undefined) return this.writeQueue
const run = this.writeQueue.then(
() => this._writeFile(config),
() => this._writeFile(config),
)
this.writeQueue = run
void run.catch((error) => {
this.ctx.root.logger?.('loader').warn('failed to write config file %C', this.filename)
this.ctx.root.logger?.('loader').warn(error)
})
return run
}
/** Schedule a write of the current root entry data. */
write() {
this.context.emit('loader/config-update')
return this.writeFile(this.root.data)
}
}
export default Include
@@ -0,0 +1 @@
../../../@deepseek-ai+cordis-plugin-loader@1.0.2_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/cordis-plugin-loader
@@ -0,0 +1 @@
../../../@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit
@@ -0,0 +1 @@
../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,48 @@
# @cordisjs/plugin-loader
Runtime plugin loader for Cordis. The loader owns an `EntryTree`, imports plugin
modules by name, applies their config, and keeps the running plugin graph in
sync with entry updates.
## Usage
```ts
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
const root = new Context()
await root.plugin(Loader, { baseUrl: import.meta.url })
const id = await root.loader.create({
name: './plugins/example',
config: { enabled: true },
})
await root.loader.await()
root.loader.update(id, { config: { enabled: false } })
```
## Entry Options
| Field | Description |
| --- | --- |
| `id` | Stable id for resolving, updating, and removing the entry. |
| `name` | Module specifier imported by the loader. |
| `config` | Config passed to the plugin. |
| `group` | Marks the entry as a group whose `config` is a child entry list. |
| `disabled` | Stops the entry and prevents it from starting. |
| `inject` | Adds required services or intercept config for this entry. |
## API
| API | Description |
| --- | --- |
| `loader.create(options, parent?, position?)` | Add and start an entry. |
| `loader.update(id, options, parent?, position?)` | Update, move, and restart an entry. |
| `loader.remove(id)` | Stop and delete an entry. |
| `loader.resolve(id)` | Resolve an entry by id, including nested `a:b` ids. |
| `loader.resolveGroup(id)` | Resolve the root group or a nested group. |
| `loader.await()` | Wait for pending entry imports and fiber reloads. |
| `loader.locate(fiber?)` | Return the loader entry id that owns a fiber. |
For file-backed trees, use `@cordisjs/plugin-include`.
@@ -0,0 +1,744 @@
import { createRequire } from "node:module";
import { Context, Inject, Service, composeError } from "@deepseek-ai/cordis";
import { deepEqual, defineProperty, isNonNullable, isNullable, valueMap } from "@deepseek-ai/cosmokit";
//#region lib/types/internal.js
/** Helpers for locating the current Node internal module loader. */
var ModuleLoader;
(function(ModuleLoader) {
let _cachedLoader;
function requireInternal(id) {
const require = createRequire(import.meta.url);
if (process.execArgv.includes("--expose-internals")) try {
return require(id);
} catch {}
try {
return require("node-addon-require-builtin").requireBuiltin(id);
} catch {}
}
function fromInternal() {
if (_cachedLoader) return _cachedLoader;
const [major] = process.versions.node.split(".").map(Number);
if (major >= 24) {
const raw = requireInternal("internal/modules/esm/loader")?.getOrInitializeCascadedLoader();
if (raw) return _cachedLoader = Object.assign(raw, { version: "v2" });
} else if (major >= 22) {
const raw = requireInternal("internal/modules/esm/loader")?.getOrInitializeCascadedLoader();
if (raw) return _cachedLoader = Object.assign(raw, { version: "v1" });
}
}
ModuleLoader.fromInternal = fromInternal;
})(ModuleLoader || (ModuleLoader = {}));
//#endregion
//#region lib/types/config/group.js
/** Runtime owner for a list of child loader entries. */
var EntryGroup = class {
ctx;
tree;
static key = Symbol.for("cordis.group");
data = [];
constructor(ctx, tree) {
this.ctx = ctx;
this.tree = tree;
const entry = ctx.fiber.entry;
if (entry) entry.subgroup = this;
}
get context() {
return this.ctx;
}
async create(options) {
const id = this.tree.ensureId(options);
const existing = this.tree.store[id];
const entry = existing ?? (this.tree.store[id] = new Entry(this.ctx.loader));
const previousParent = entry.parent;
entry.parent = this;
try {
await entry.update(options, true, true);
} catch (error) {
if (existing) entry.parent = previousParent;
else delete this.tree.store[id];
throw error;
}
return entry.id;
}
unlink(options) {
const config = this.data;
const index = config.indexOf(options);
if (index >= 0) config.splice(index, 1);
}
async remove(id, isDispose = false) {
const entry = this.tree.store[id];
if (!entry) return;
await entry._dispose();
if (!isDispose) this.unlink(entry.options);
delete this.tree.store[id];
this.context.emit("loader/partial-dispose", entry, entry.options, false);
}
async update(config) {
const oldConfig = this.data;
const seen = /* @__PURE__ */ new Set();
for (const options of config) {
const id = this.tree.ensureId(options);
if (seen.has(id)) throw new TypeError(`duplicate loader entry id: ${id}`);
seen.add(id);
}
const oldMap = Object.fromEntries(oldConfig.map((options) => [options.id, options]));
const newMap = Object.fromEntries(config.map((options) => [options.id, options]));
try {
const outcomes = await Promise.allSettled(config.map((options) => this.create(options)));
if (this.ctx.fiber.uid === null) return;
const failures = outcomes.filter((outcome) => outcome.status === "rejected").map((outcome) => outcome.reason);
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, "loader entries failed to apply");
for (const id of Object.keys(oldMap)) if (!newMap[id]) await this.remove(id, true);
this.data = config;
} catch (error) {
const rollbackErrors = [];
for (const id of Object.keys(newMap).reverse()) {
if (oldMap[id]) continue;
try {
await this.remove(id, true);
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
}
for (const options of oldConfig) try {
await this.create(options);
} catch (rollbackError) {
rollbackErrors.push(rollbackError);
}
this.data = oldConfig;
if (rollbackErrors.length) throw new AggregateError([error, ...rollbackErrors], "loader entry rollback failed");
throw error;
}
}
async stop() {
for (const options of this.data) await this.remove(options.id, true);
}
};
/** Plugin that mounts a nested loader entry group. */
var Group = class extends EntryGroup {
ctx;
config;
static initial = [];
static [EntryGroup.key] = true;
constructor(ctx, config) {
super(ctx, ctx.fiber.entry.parent.tree);
this.ctx = ctx;
this.config = config;
ctx.on("internal/update", (config) => this.update(config));
}
async *[Service.init]() {
yield () => this.stop();
await this.update(this.config);
}
};
//#endregion
//#region lib/types/config/tree.js
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
});
return path;
};
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
var EntryTree = class EntryTree {
static sep = ":";
ctx;
enableLogs;
root;
store = Object.create(null);
constructor(ctx) {
this.ctx = ctx.extend({ baseUrl: ctx.baseUrl });
this.root = new EntryGroup(this.ctx, this);
const entry = this.ctx.fiber.entry;
if (entry) entry.subtree = this;
}
get context() {
return this.ctx;
}
/** Iterate entries in this tree and any nested subtrees. */
*entries() {
for (const entry of Object.values(this.store)) {
yield entry;
if (!entry.subtree) continue;
yield* entry.subtree.entries();
}
}
/** Return pending import and lifecycle tasks owned by this tree. */
getTasks() {
return [...this.entries()].map((entry) => entry._initTask || entry.fiber?.inertia).filter(isNonNullable);
}
/**
* Wait until this tree has no active import or lifecycle tasks.
* @throws a settled fiber failure, or an aggregate when several fibers failed.
*/
async await() {
while (true) {
const tasks = this.getTasks();
if (tasks.length) {
await Promise.allSettled(tasks);
continue;
}
const failures = (await Promise.allSettled([...this.entries()].map((entry) => entry._await()))).filter((outcome) => outcome.status === "rejected").map((outcome) => outcome.reason);
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, "loader fibers failed");
this.ctx.reflect.notify(["loader"]);
if (!this.getTasks().length) return;
}
}
ensureId(options) {
if (!options.id) do
options.id = Math.random().toString(16).slice(2, 10);
while (this.store[options.id]);
return options.id;
}
/** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
resolve(id) {
const parts = id.split(EntryTree.sep);
let tree = this;
const final = parts.pop();
for (const part of parts) {
tree = tree.store[part]?.subtree;
if (!tree) throw new Error(`cannot resolve entry ${id}`);
}
const entry = tree.store[final];
if (!entry) throw new Error(`cannot resolve entry ${id}`);
return entry;
}
resolveGroup(id) {
if (!id) return this.root;
const entry = this.resolve(id);
if (!entry.subgroup) throw new Error(`entry ${id} is not a group`);
return entry.subgroup;
}
/** Create an entry in the root group or a nested group. */
async create(options, parent = null, position = Infinity) {
const group = this.resolveGroup(parent);
const id = await group.create(options);
const entry = this.resolve(id);
group.data.splice(position, 0, entry.options);
group.tree.write();
return id;
}
/** Stop and remove an entry from its parent group. */
async remove(id) {
const entry = this.resolve(id);
await entry.parent.remove(id);
entry.parent.tree.write();
}
/** Update an entry and optionally move it to another group. */
async update(id, options, parent, position) {
const entry = this.resolve(id);
const source = entry.parent;
const sourceIndex = source.data.indexOf(entry.options);
let target = source;
if (parent !== void 0) {
target = this.resolveGroup(parent);
source.unlink(entry.options);
target.data.splice(position ?? Infinity, 0, entry.options);
entry.parent = target;
}
try {
await entry.update(options, false, true);
} catch (error) {
if (parent !== void 0) {
target.unlink(entry.options);
source.data.splice(sourceIndex < 0 ? source.data.length : sourceIndex, 0, entry.options);
entry.parent = source;
try {
await entry.update({}, false, true);
} catch (rollbackError) {
throw new AggregateError([error, rollbackError], `failed to roll back loader entry move ${id}`);
}
}
throw error;
}
source.tree.write();
if (target !== source) target.tree.write();
}
/** Import a plugin module from a specifier or `cordis:` builtin. */
import(name, getOuterStack) {
if (name.startsWith("cordis:")) return this.ctx.loader.builtins[name.slice(7)];
return composeError(async (info) => {
info.offset += 3;
if (this.ctx.loader.internal) return await this.ctx.loader.internal.import(name, this.ctx.baseUrl, {});
else if (name.startsWith(".")) return await import(__rewriteRelativeImportExtension(
/* @vite-ignore */
new URL(name, this.ctx.baseUrl).href
));
else return await import(__rewriteRelativeImportExtension(
/* @vite-ignore */
name
));
}, getOuterStack);
}
};
//#endregion
//#region lib/types/config/utils.js
/** Evaluate a JavaScript expression against a loader context scope. */
const evaluate = new Function("ctx", "expr", `
with (ctx) {
return eval(expr)
}
`);
/** Recursively replace YAML `!js` expression nodes with evaluated values. */
function interpolate(ctx, value) {
if (isJsExpr(value)) return evaluate(ctx, value.__jsExpr);
else if (!value || typeof value !== "object") return value;
else if (Array.isArray(value)) return value.map((item) => interpolate(ctx, item));
else return valueMap(value, (item) => interpolate(ctx, item));
}
/** Return true when a value is a serialized loader JavaScript expression. */
function isJsExpr(value) {
return value instanceof Object && "__jsExpr" in value;
}
//#endregion
//#region lib/types/config/entry.js
function updateError(stage, options, cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
return new Error(`failed to ${stage} loader entry ${options.id} (${options.name}): ${detail}`, { cause });
}
function takeEntries(object, keys) {
const result = [];
for (const key of keys) {
if (!(key in object)) continue;
result.push([key, object[key]]);
delete object[key];
}
return result;
}
function sortKeys(object, prepend = ["id", "name"], append = ["config"]) {
const part1 = takeEntries(object, prepend);
const part2 = takeEntries(object, append);
const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b));
return Object.assign(object, Object.fromEntries([
...part1,
...rest,
...part2
]));
}
function replaceKeys(target, source) {
for (const key of Object.keys(target)) Reflect.deleteProperty(target, key);
return Object.assign(target, source);
}
/** One configured plugin node inside an `EntryTree`. */
var Entry = class Entry {
loader;
static key = Symbol.for("cordis.entry");
ctx;
fiber;
parent;
options = {};
subgroup;
subtree;
_initTask;
_disposing = 0;
constructor(loader) {
this.loader = loader;
this.ctx = loader.ctx.extend({ [Entry.key]: this });
this.context.emit("loader/entry-init", this);
}
get context() {
return this.ctx;
}
get id() {
let id = this.options.id;
if (this.parent.tree.ctx.fiber.entry) id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id;
return id;
}
/** True when this entry or any owning parent entry is disabled. */
get disabled() {
return this._disabled(this.options);
}
_disabled(options) {
if (options.group) return false;
if (this.disabledOf(options)) return true;
let entry = this.parent.ctx.fiber.entry;
while (entry) {
if (this.disabledOf(entry.options)) return true;
entry = entry.parent.ctx.fiber.entry;
}
return false;
}
/**
* Effective disabled state: a `!!js` expression evaluates against the loader
* context. The raw node stays in the options, so write-back keeps the form.
*/
disabledOf(options) {
return isJsExpr(options.disabled) ? Boolean(this.evaluate(options.disabled.__jsExpr)) : Boolean(options.disabled);
}
evaluate(expr) {
return evaluate(this.ctx, expr);
}
async _patchContext(diff) {
await this.context.waterfall("loader/patch-context", this, async () => {
Object.setPrototypeOf(this.ctx, this.parent.ctx);
if (this.fiber?.uid && (diff.includes("config") || this.options.group)) await this.fiber.update(this.options.config, true);
});
}
async refresh() {
if (this.fiber) return;
if (this.disabled) return;
await this.init();
}
async _dispose(fiber = this.fiber) {
if (!fiber) return;
if (this.fiber === fiber) this.fiber = void 0;
this._disposing += 1;
try {
await fiber.dispose();
} finally {
this._disposing -= 1;
}
}
/** Merge new options, restart as needed, and persist through the parent tree. */
async update(options, create = false, force = false) {
const previousOptions = this.options;
const legacy = { ...previousOptions };
const candidate = create ? options : { ...previousOptions };
if (!create) for (const [key, value] of Object.entries(options)) if (isNullable(value)) delete candidate[key];
else candidate[key] = value;
sortKeys(candidate);
const diff = Object.keys({
...candidate,
...legacy
}).filter((key) => !deepEqual(candidate[key], legacy[key]));
if (!diff.length && !force) return;
const commit = () => {
if (create) return;
this.options = replaceKeys(previousOptions, candidate);
};
const previous = this.fiber;
if (!previous?.uid) {
this.fiber = void 0;
this.options = candidate;
try {
if (!this._disabled(candidate)) await this.init();
} catch (error) {
this.options = previousOptions;
throw error;
}
commit();
return;
}
if (this._disabled(candidate)) {
this.options = candidate;
try {
await this._dispose(previous);
} catch (error) {
this.options = previousOptions;
throw updateError("dispose", candidate, error);
}
commit();
this.context.emit("loader/partial-dispose", this, legacy, true);
return;
}
if (!diff.some((key) => key === "name" || key === "inject" || key === "group")) {
this.options = candidate;
try {
await this._patchContext(diff);
} catch (error) {
this.options = previousOptions;
try {
await this._patchContext(diff);
} catch (rollbackError) {
throw updateError("rollback", legacy, new AggregateError([error, rollbackError]));
}
this.context.emit("loader/partial-dispose", this, candidate, true);
throw updateError("apply", candidate, error);
}
commit();
this.context.emit("loader/partial-dispose", this, legacy, true);
return;
}
let plugin;
try {
plugin = diff.includes("name") ? this.loader.unwrapExports(await this.parent.tree.import(candidate.name, this.getOuterStack)) : previous.runtime.callback;
} catch (error) {
throw updateError("import", candidate, error);
}
const previousPlugin = previous.runtime.callback;
this.options = candidate;
try {
await this._dispose(previous);
} catch (error) {
this.options = previousOptions;
throw updateError("dispose", candidate, error);
}
try {
await this._start(plugin);
} catch (error) {
this.options = previousOptions;
try {
await this._start(previousPlugin);
} catch (rollbackError) {
throw updateError("rollback", legacy, new AggregateError([error, rollbackError]));
}
this.context.emit("loader/partial-dispose", this, candidate, true);
throw updateError("apply", candidate, error);
}
commit();
this.context.emit("loader/partial-dispose", this, legacy, true);
}
getOuterStack = () => {
let entry = this;
const result = [];
do {
result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`);
entry = entry.parent.ctx.fiber.entry;
} while (entry);
return result;
};
/** Import and start the configured plugin if it is not already running. */
async init() {
try {
await (this._initTask ??= this._init());
} finally {
this._initTask = void 0;
if (!this.loader.getTasks().length) this.ctx.reflect.notify(["loader"]);
}
await this._await();
}
async _await() {
try {
await this.fiber?.await();
} catch (error) {
throw updateError("apply", this.options, error);
}
}
async _init() {
let plugin;
try {
plugin = this.loader.unwrapExports(await this.parent.tree.import(this.options.name, this.getOuterStack));
} catch (error) {
throw updateError("import", this.options, error);
}
try {
await this._start(plugin);
} catch (error) {
throw updateError("apply", this.options, error);
}
}
async _start(plugin) {
let fiber;
try {
await this._patchContext([]);
this.loader.showLog(this, "apply");
fiber = this.fiber = this.ctx.registry.plugin(plugin, this.options.config, this.getOuterStack);
await fiber.await();
} catch (error) {
await this._dispose(fiber);
throw error;
}
}
};
//#endregion
//#region lib/types/config/isolate.js
function swap(target, source) {
for (const key of Reflect.ownKeys(target)) Reflect.deleteProperty(target, key);
for (const key of Reflect.ownKeys(source || {})) Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source, key));
}
/** Symbol realm used to isolate service implementations by entry or label. */
var Realm = class {
store = Object.create(null);
access(key, create = false) {
if (create) return this.store[key] ??= Symbol(`${key}${this.suffix}`);
else return this.store[key] ?? Symbol(`${key}${this.suffix}`);
}
delete(key) {
delete this.store[key];
}
get size() {
return Object.keys(this.store).length;
}
};
/** Entry-local isolation realm. */
var LocalRealm = class extends Realm {
entry;
constructor(entry) {
super();
this.entry = entry;
}
get suffix() {
return "#" + this.entry.options.id;
}
};
/** Named isolation realm shared by entries that use the same label. */
var GlobalRealm = class extends Realm {
label;
constructor(label) {
super();
this.label = label;
}
get suffix() {
return "@" + this.label;
}
};
/** Install loader hooks that apply `intercept` and `isolate` entry options. */
function isolate(ctx) {
const realms = Object.create(null);
const delims = Object.create(null);
function access(entry, name, create = false) {
let realm;
const label = entry.options.isolate?.[name];
if (!label) return;
if (label === true) realm = entry.realm ??= new LocalRealm(entry);
else if (create) realm = realms[label] ??= new GlobalRealm(label);
else realm = realms[label];
return realm?.access(name, create);
}
ctx.on("loader/entry-init", (entry) => {
entry.ctx[Context.intercept] = Object.create(entry.ctx[Context.intercept]);
entry.ctx[Context.isolate] = Object.create(entry.ctx[Context.isolate]);
});
ctx.on("loader/patch-context", async (entry, next) => {
const newMap = Object.create(entry.parent.ctx[Context.isolate]);
for (const name of Object.keys(entry.options.isolate ?? {})) newMap[name] = access(entry, name, true);
const diff = Object.create(null);
const oldMap = entry.ctx[Context.isolate];
for (const name in {
...newMap,
...delims
}) {
if (newMap[name] === oldMap[name]) continue;
const delim = delims[name] ??= Symbol(`delim:${name}`);
entry.ctx[delim] = Symbol(`${name}#${entry.id}`);
for (const symbol of [oldMap[name], newMap[name]]) {
const impl = symbol && entry.ctx.reflect.store[symbol];
if (!impl) continue;
if (!impl.fiber) {
entry.ctx.logger.warn(/* @__PURE__ */ new Error(`expected service ${name} to be implemented`));
continue;
}
diff[name] = [
oldMap[name],
newMap[name],
entry.ctx[delim],
impl.fiber.ctx[delim]
];
if (entry.ctx[delim] !== impl.fiber.ctx[delim]) break;
}
}
Object.setPrototypeOf(entry.ctx[Context.isolate], entry.parent.ctx[Context.isolate]);
Object.setPrototypeOf(entry.ctx[Context.intercept], entry.parent.ctx[Context.intercept]);
swap(entry.ctx[Context.isolate], newMap);
swap(entry.ctx[Context.intercept], entry.options.intercept);
await next();
for (const [symbol1, symbol2, flag1, flag2] of Object.values(diff)) if (flag1 === flag2 && entry.ctx.reflect.store[symbol1] && !entry.ctx.reflect.store[symbol2]) {
entry.ctx.reflect.store[symbol2] = entry.ctx.reflect.store[symbol1];
delete entry.ctx.reflect.store[symbol1];
}
ctx.reflect.notify(Object.keys(diff), (ctx, name) => {
const [symbol1, symbol2, flag1, flag2] = diff[name];
const symbol3 = ctx[Context.isolate][name];
const flag3 = ctx[delims[name]];
return (symbol1 === symbol3 || symbol2 === symbol3) && flag1 === flag3 !== (flag1 === flag2);
});
for (const name in delims) if (!Reflect.ownKeys(newMap).includes(name)) delete entry.ctx[delims[name]];
});
ctx.on("loader/partial-dispose", (entry, legacy, active) => {
for (const [name, label] of Object.entries(legacy.isolate ?? {})) {
if (label === true) continue;
if (active && entry.options.isolate?.[name] === label) continue;
const realm = realms[label];
if (!realm) continue;
for (const entry of ctx.loader.entries()) if (entry.options.isolate?.[name] === realm.label) return;
realm.delete(name);
if (!realm.size) delete realms[realm.label];
}
});
}
//#endregion
//#region lib/types/index.js
/**
* Service that owns a loader entry tree and imports configured plugins.
*
* Subclasses provide persistence by implementing `write()` on `EntryTree`.
*/
var Loader = class extends EntryTree {
config;
envData = process.env.CORDIS_SHARED ? JSON.parse(process.env.CORDIS_SHARED) : { startTime: Date.now() };
name = "loader";
internal = ModuleLoader.fromInternal();
builtins = Object.create(null);
constructor(ctx, config = {}) {
super(ctx);
this.config = config;
if (config.baseUrl) this.ctx.baseUrl = config.baseUrl;
const self = this;
defineProperty(this, Service.tracker, {
associate: "loader",
property: "ctx",
noShadow: true
});
ctx.reflect.provide("loader", this, this[Service.check]);
ctx.on("internal/config", function(_config, next) {
const config = next();
if (!this.entry || this.parent.fiber?.entry === this.entry) return config;
if ((this.runtime?.callback)?.[EntryGroup.key]) return config;
return interpolate(this.ctx, config);
}, { global: true });
ctx.on("internal/update", async function(config, noSave, next) {
if (!this.entry || noSave || this.parent.fiber?.entry === this.entry) return next();
await next();
const unparse = this.runtime?.Config?.["simplify"];
this.entry.options.config = unparse ? unparse(config) : config;
this.entry.parent.tree.write();
}, {
global: true,
prepend: true
});
ctx.on("internal/update", function(config, _, next) {
if (!this.entry || this.parent.fiber?.entry === this.entry) return next();
self.showLog(this.entry, "reload");
return next();
}, { global: true });
ctx.on("internal/plugin", (fiber) => {
if (fiber.parent[Entry.key] && !fiber.entry) {
fiber.entry = fiber.parent[Entry.key];
Inject.resolve(fiber.entry.options.inject, fiber.inject);
}
if (fiber.uid) return;
if (!fiber.entry) return;
if (fiber.parent.fiber?.entry === fiber.entry) return;
if (!ctx.registry.has(fiber.runtime.callback)) return;
const treeOwner = fiber.entry.parent.tree.ctx.fiber;
if (!treeOwner.uid || treeOwner.state === 5) return;
if (fiber.entry._disposing) return;
this.showLog(fiber.entry, "unload");
if (fiber.entry.disabled) return;
fiber.entry.options.disabled = true;
fiber.entry.parent.tree.write();
});
ctx.plugin(isolate);
}
write() {}
[Service.check]() {
if (Service.prototype[Service.resolveConfig].call(this).await && this.getTasks().length) return false;
return true;
}
showLog(entry, type) {
if (entry.options.group || !entry.parent.tree.enableLogs) return;
this.ctx.root.logger?.("loader").info("%s plugin %C", type, entry.options.name);
}
/** Return the loader entry id that owns `fiber`, if any. */
locate(fiber = this.ctx.fiber) {
while (1) {
if (fiber.entry) return fiber.entry.id;
const next = fiber.parent.fiber;
if (fiber === next) return;
fiber = next;
}
}
/** Hook for hosts that can restart the process on full-reload requests. */
exit() {}
/** Normalize ESM/CJS/default export shapes before applying a plugin. */
unwrapExports(exports) {
if (isNullable(exports)) return exports;
exports = exports.default ?? exports;
if (!exports.__esModule) return exports;
return exports.default ?? exports;
}
};
//#endregion
export { Entry, EntryGroup, EntryTree, GlobalRealm, Group, Loader, Loader as default, LocalRealm, ModuleLoader, Realm, evaluate, interpolate, isJsExpr };
@@ -0,0 +1,56 @@
import { Context, Fiber, Inject } from '@deepseek-ai/cordis';
import { Loader } from '../index.ts';
import { EntryGroup } from './group.ts';
import { EntryTree } from './tree.ts';
/** Serialized plugin entry options stored in loader config files. */
export interface EntryOptions {
/** Stable id inside the containing entry tree. */
id: string;
/** Module specifier imported by the entry tree. */
name: string;
/** Config passed to the plugin. */
config?: any;
/** Marks this entry as a nested group. */
group?: boolean | null;
/** Prevents this entry and descendants from running. */
disabled?: boolean | null;
/** Required services or service intercept config for this entry. */
inject?: Inject | null;
}
/** One configured plugin node inside an `EntryTree`. */
export declare class Entry {
loader: Loader;
static readonly key: unique symbol;
ctx: Context;
fiber?: Fiber;
parent: EntryGroup;
options: EntryOptions;
subgroup?: EntryGroup;
subtree?: EntryTree;
_initTask?: Promise<void>;
_disposing: number;
constructor(loader: Loader);
get context(): Context;
get id(): string;
/** True when this entry or any owning parent entry is disabled. */
get disabled(): boolean;
private _disabled;
/**
* Effective disabled state: a `!!js` expression evaluates against the loader
* context. The raw node stays in the options, so write-back keeps the form.
*/
private disabledOf;
evaluate(expr: string): any;
private _patchContext;
refresh(): Promise<void>;
_dispose(fiber?: Fiber | undefined): Promise<void>;
/** Merge new options, restart as needed, and persist through the parent tree. */
update(options: Partial<EntryOptions>, create?: boolean, force?: boolean): Promise<void>;
getOuterStack: () => string[];
/** Import and start the configured plugin if it is not already running. */
init(): Promise<void>;
_await(): Promise<void>;
private _init;
private _start;
}
//# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../../src/config/entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAE5D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAA;IACZ,mCAAmC;IACnC,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACtB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACzB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AA6BD,wDAAwD;AACxD,qBAAa,KAAK;IAcG,MAAM,EAAE,MAAM;IAbjC,MAAM,CAAC,QAAQ,CAAC,GAAG,gBAA6B;IAEzC,GAAG,EAAE,OAAO,CAAA;IACZ,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,MAAM,EAAG,UAAU,CAAA;IAEnB,OAAO,EAAS,YAAY,CAAA;IAC5B,QAAQ,CAAC,EAAE,UAAU,CAAA;IACrB,OAAO,CAAC,EAAE,SAAS,CAAA;IAE1B,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,UAAU,SAAI;gBAEK,MAAM,EAAE,MAAM;IAKjC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,EAAE,WAML;IAED,mEAAmE;IACnE,IAAI,QAAQ,YAEX;IAED,OAAO,CAAC,SAAS;IAYjB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAMlB,QAAQ,CAAC,IAAI,EAAE,MAAM;YAIP,aAAa;IAUrB,OAAO;IAMP,QAAQ,CAAC,KAAK,oBAAa;IAWjC,iFAAiF;IAC3E,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,MAAM,UAAQ,EAAE,KAAK,UAAQ;IA0G1E,aAAa,iBAQZ;IAED,2EAA2E;IACrE,IAAI;IAUJ,MAAM;YAQE,KAAK;YAcL,MAAM;CAYrB"}
@@ -0,0 +1,27 @@
import { Context, Service } from '@deepseek-ai/cordis';
import { type EntryOptions } from './entry.ts';
import { EntryTree } from './tree.ts';
/** Runtime owner for a list of child loader entries. */
export declare class EntryGroup {
ctx: Context;
tree: EntryTree;
static readonly key: unique symbol;
data: EntryOptions[];
constructor(ctx: Context, tree: EntryTree);
get context(): Context;
create(options: Omit<EntryOptions, 'id'>): Promise<string>;
unlink(options: EntryOptions): void;
remove(id: string, isDispose?: boolean): Promise<void>;
update(config: EntryOptions[]): Promise<void>;
stop(): Promise<void>;
}
/** Plugin that mounts a nested loader entry group. */
export declare class Group extends EntryGroup {
ctx: Context;
config: EntryOptions[];
static initial: Omit<EntryOptions, 'id'>[];
static readonly [EntryGroup.key] = true;
constructor(ctx: Context, config: EntryOptions[]);
[Service.init](): AsyncGenerator<() => Promise<void>, void, unknown>;
}
//# sourceMappingURL=group.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../../../src/config/group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,wDAAwD;AACxD,qBAAa,UAAU;IAKF,GAAG,EAAE,OAAO;IAAS,IAAI,EAAE,SAAS;IAJvD,MAAM,CAAC,QAAQ,CAAC,GAAG,gBAA6B;IAEzC,IAAI,EAAE,YAAY,EAAE,CAAK;gBAEb,GAAG,EAAE,OAAO,EAAS,IAAI,EAAE,SAAS;IAKvD,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC;IAsB9C,MAAM,CAAC,OAAO,EAAE,YAAY;IAMtB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,UAAQ;IAWpC,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE;IAiD7B,IAAI;CAKX;AAED,sDAAsD;AACtD,qBAAa,KAAM,SAAQ,UAAU;IAIhB,GAAG,EAAE,OAAO;IAAS,MAAM,EAAE,YAAY,EAAE;IAH9D,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAK;IAC/C,MAAM,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,QAAO;gBAEpB,GAAG,EAAE,OAAO,EAAS,MAAM,EAAE,YAAY,EAAE;IAKvD,CAAC,OAAO,CAAC,IAAI,CAAC;CAItB"}
@@ -0,0 +1,35 @@
import { Context } from '@deepseek-ai/cordis';
import type { Dict } from '@deepseek-ai/cosmokit';
import { Entry } from './entry.ts';
declare module './entry.ts' {
interface EntryOptions {
intercept?: Dict | null;
isolate?: Dict<true | string> | null;
}
interface Entry {
realm: LocalRealm;
}
}
/** Symbol realm used to isolate service implementations by entry or label. */
export declare abstract class Realm {
protected store: Dict<symbol>;
abstract get suffix(): string;
access(key: string, create?: boolean): symbol;
delete(key: string): void;
get size(): number;
}
/** Entry-local isolation realm. */
export declare class LocalRealm extends Realm {
private entry;
constructor(entry: Entry);
get suffix(): string;
}
/** Named isolation realm shared by entries that use the same label. */
export declare class GlobalRealm extends Realm {
label: string;
constructor(label: string);
get suffix(): string;
}
/** Install loader hooks that apply `intercept` and `isolate` entry options. */
export default function isolate(ctx: Context): void;
//# sourceMappingURL=isolate.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"isolate.d.ts","sourceRoot":"","sources":["../../../src/config/isolate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,OAAO,QAAQ,YAAY,CAAC;IAC1B,UAAU,YAAY;QACpB,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;QACvB,OAAO,CAAC,EAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,CAAA;KACrC;IAED,UAAU,KAAK;QACb,KAAK,EAAE,UAAU,CAAA;KAClB;CACF;AAWD,8EAA8E;AAC9E,8BAAsB,KAAK;IACzB,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAsB;IAEnD,QAAQ,KAAK,MAAM,IAAI,MAAM,CAAA;IAE7B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,UAAQ;IAQlC,MAAM,CAAC,GAAG,EAAE,MAAM;IAIlB,IAAI,IAAI,WAEP;CACF;AAED,mCAAmC;AACnC,qBAAa,UAAW,SAAQ,KAAK;IACvB,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,KAAK;IAIhC,IAAI,MAAM,WAET;CACF;AAED,uEAAuE;AACvE,qBAAa,WAAY,SAAQ,KAAK;IACjB,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;IAIhC,IAAI,MAAM,WAET;CACF;AAED,+EAA+E;AAC/E,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,GAAG,EAAE,OAAO,QAsG3C"}
@@ -0,0 +1,38 @@
import { Context } from '@deepseek-ai/cordis';
import { type Dict } from '@deepseek-ai/cosmokit';
import { Entry, type EntryOptions } from './entry.ts';
import { EntryGroup } from './group.ts';
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
export declare abstract class EntryTree {
static readonly sep = ":";
ctx: Context;
enableLogs?: boolean;
root: EntryGroup;
store: Dict<Entry>;
constructor(ctx: Context);
get context(): Context;
/** Iterate entries in this tree and any nested subtrees. */
entries(): Generator<Entry, void, void>;
/** Return pending import and lifecycle tasks owned by this tree. */
getTasks(): Promise<void>[];
/**
* Wait until this tree has no active import or lifecycle tasks.
* @throws a settled fiber failure, or an aggregate when several fibers failed.
*/
await(): Promise<void>;
ensureId(options: Partial<EntryOptions>): string;
/** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
resolve(id: string): Entry;
resolveGroup(id: string | null): EntryGroup;
/** Create an entry in the root group or a nested group. */
create(options: Omit<EntryOptions, 'id'>, parent?: string | null, position?: number): Promise<string>;
/** Stop and remove an entry from its parent group. */
remove(id: string): Promise<void>;
/** Update an entry and optionally move it to another group. */
update(id: string, options: Omit<EntryOptions, 'id' | 'name'>, parent?: string | null, position?: number): Promise<void>;
/** Import a plugin module from a specifier or `cordis:` builtin. */
import(name: string, getOuterStack?: () => string[]): any;
/** Persist current tree state. In-memory trees may implement this as a no-op. */
abstract write(): void;
}
//# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../src/config/tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAiB,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEvC,6EAA6E;AAC7E,8BAAsB,SAAS;IAC7B,MAAM,CAAC,QAAQ,CAAC,GAAG,OAAM;IAElB,GAAG,EAAE,OAAO,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAsB;gBAEnC,GAAG,EAAE,OAAO;IAOxB,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,4DAA4D;IAC1D,OAAO,IAAI,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC;IAQzC,oEAAoE;IACpE,QAAQ;IAMR;;;OAGG;IACG,KAAK;IAoBX,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC;IASvC,iFAAiF;IACjF,OAAO,CAAC,EAAE,EAAE,MAAM;IAalB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAO9B,2DAA2D;IACrD,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW,EAAE,QAAQ,SAAW;IASjG,sDAAsD;IAChD,MAAM,CAAC,EAAE,EAAE,MAAM;IAMvB,+DAA+D;IACzD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM;IA8B9G,oEAAoE;IACpE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,MAAM,EAAE;IAmBnD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,IAAI,IAAI;CACvB"}
@@ -0,0 +1,11 @@
/** Evaluate a JavaScript expression against a loader context scope. */
export declare const evaluate: ((ctx: object, expr: string) => any);
/** Recursively replace YAML `!js` expression nodes with evaluated values. */
export declare function interpolate(ctx: object, value: any): any;
/** Return true when a value is a serialized loader JavaScript expression. */
export declare function isJsExpr(value: any): value is JsExpr;
/** Serialized JavaScript expression produced by the include YAML tag. */
export interface JsExpr {
__jsExpr: string;
}
//# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/config/utils.ts"],"names":[],"mappings":"AAGA,uEAAuE;AACvE,eAAO,MAAM,QAAQ,EAIf,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,GAAG,CAAC,CAAA;AAE1C,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,OAUlD;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,MAAM,CAEpD;AAED,yEAAyE;AACzE,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAA;CACjB"}
@@ -0,0 +1,73 @@
import { Context, Service, type Fiber } from '@deepseek-ai/cordis';
import { type Dict } from '@deepseek-ai/cosmokit';
import { ModuleLoader } from './internal.ts';
import { Entry, type EntryOptions } from './config/entry.ts';
import { EntryTree } from './config/tree.ts';
/** Re-export entry node APIs. */
export * from './config/entry.ts';
/** Re-export nested entry group APIs. */
export * from './config/group.ts';
/** Re-export service isolation helpers. */
export * from './config/isolate.ts';
/** Re-export entry tree persistence APIs. */
export * from './config/tree.ts';
/** Re-export loader config expression helpers. */
export * from './config/utils.ts';
/** Re-export Node internal module loader compatibility types. */
export * from './internal.ts';
declare module '@deepseek-ai/cordis' {
interface Events {
'exit'(signal: NodeJS.Signals): Promise<void>;
'loader/config-update'(): void;
'loader/entry-init'(entry: Entry): void;
'loader/partial-dispose'(entry: Entry, legacy: Partial<EntryOptions>, active: boolean): void;
'loader/patch-context'(entry: Entry, next: () => void | Promise<void>): void | Promise<void>;
}
interface Context {
loader: Loader;
}
interface EnvData {
startTime?: number;
}
interface Fiber {
entry?: Entry;
}
}
/** Loader config and dependency intercept namespace. */
export declare namespace Loader {
/** Root loader configuration. */
interface Config {
/** Base URL used to resolve relative plugin specifiers and config paths. */
baseUrl?: string;
}
/** Intercept config used when other plugins depend on `loader`. */
interface Intercept {
/** Keep dependent plugins pending while loader entries are still loading. */
await?: boolean;
}
}
/**
* Service that owns a loader entry tree and imports configured plugins.
*
* Subclasses provide persistence by implementing `write()` on `EntryTree`.
*/
export declare class Loader extends EntryTree {
config: Loader.Config;
[Service.config]: Loader.Intercept;
envData: any;
name: string;
internal: ModuleLoader | undefined;
builtins: Dict<any>;
constructor(ctx: Context, config?: Loader.Config);
write(): void;
[Service.check](): boolean;
showLog(entry: Entry, type: string): void;
/** Return the loader entry id that owns `fiber`, if any. */
locate(fiber?: Fiber): string | undefined;
/** Hook for hosts that can restart the process on full-reload requests. */
exit(): void;
/** Normalize ESM/CJS/default export shapes before applying a plugin. */
unwrapExports(exports: any): any;
}
export default Loader;
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAsB,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAA;AACtF,OAAO,EAA8B,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAG5D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAG5C,iCAAiC;AACjC,cAAc,mBAAmB,CAAA;AACjC,yCAAyC;AACzC,cAAc,mBAAmB,CAAA;AACjC,2CAA2C;AAC3C,cAAc,qBAAqB,CAAA;AACnC,6CAA6C;AAC7C,cAAc,kBAAkB,CAAA;AAChC,kDAAkD;AAClD,cAAc,mBAAmB,CAAA;AACjC,iEAAiE;AACjE,cAAc,eAAe,CAAA;AAE7B,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;QAC7C,sBAAsB,IAAI,IAAI,CAAA;QAC9B,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;QACvC,wBAAwB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;QAC5F,sBAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAC7F;IAED,UAAU,OAAO;QACf,MAAM,EAAE,MAAM,CAAA;KACf;IAED,UAAU,OAAO;QACf,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB;IAED,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,KAAK,CAAA;KACd;CACF;AAED,wDAAwD;AACxD,yBAAiB,MAAM,CAAC;IACtB,iCAAiC;IACjC,UAAiB,MAAM;QACrB,4EAA4E;QAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB;IAED,mEAAmE;IACnE,UAAiB,SAAS;QACxB,6EAA6E;QAC7E,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB;CACF;AAED;;;;GAIG;AACH,qBAAa,MAAO,SAAQ,SAAS;IAYF,MAAM,EAAE,MAAM,CAAC,MAAM;IAX9C,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,SAAS,CAAA;IAEnC,OAAO,MAEe;IAEtB,IAAI,SAAW;IACf,QAAQ,2BAA8B;IAEtC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAsB;gBAEpC,GAAG,EAAE,OAAO,EAAS,MAAM,GAAE,MAAM,CAAC,MAAW;IAqF3D,KAAK;IAIL,CAAC,OAAO,CAAC,KAAK,CAAC;IAMf,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM;IAKlC,4DAA4D;IAC5D,MAAM,CAAC,KAAK,QAAiB;IAS7B,2EAA2E;IAC3E,IAAI;IAGJ,wEAAwE;IACxE,aAAa,CAAC,OAAO,EAAE,GAAG;CAQ3B;AAED,eAAe,MAAM,CAAA"}
@@ -0,0 +1,97 @@
import { type LoadHookContext } from 'node:module';
import type { Dict } from '@deepseek-ai/cosmokit';
/** Node internal module format names handled by loader hooks. */
export type ModuleFormat = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm';
/** Source payload accepted by Node internal module load hooks. */
export type ModuleSource = string | ArrayBuffer;
/** Result returned by a Node internal resolve hook. */
export interface ResolveResult {
format: ModuleFormat;
url: string;
}
/** Result returned by a Node internal load hook. */
export interface LoadResult {
format: ModuleFormat;
source?: ModuleSource;
}
type LoadCacheData = ModuleJob;
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_map.js */
interface LoadCache extends Omit<Map<string, Dict<LoadCacheData>>, 'get' | 'set' | 'has'> {
get(url: string, type?: string): LoadCacheData | undefined;
set(url: string, type?: string, job?: LoadCacheData): this;
has(url: string, type?: string): boolean;
}
/** Minimal Node internal ModuleWrap surface used by HMR helpers. */
export interface ModuleWrap {
url: string;
getNamespace(): any;
}
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_job.js */
export interface ModuleJob {
url: string;
loader: ModuleLoader;
module?: ModuleWrap;
importAttributes: ImportAttributes;
linked: Promise<ModuleJob[]>;
instantiate(): Promise<void>;
run(): Promise<{
module: ModuleWrap;
}>;
}
/**
* Node 22/23 ModuleLoader interface.
*
* Key methods:
* - getModuleJobForImport(specifier, parentURL, importAttributes)
* - resolve(specifier, parentURL, importAttributes) → Promise<ResolveResult>
* - resolveSync(specifier, parentURL, importAttributes) → ResolveResult
*/
export interface ModuleLoaderV1 {
version: 'v1';
loadCache: LoadCache;
import(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<any>;
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[]): void;
getModuleJobForImport(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ModuleJob>;
resolve(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ResolveResult>;
resolveSync(specifier: string, parentURL: string, importAttributes: ImportAttributes): ResolveResult;
load(specifier: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>;
}
/** Node 24+ module request object. */
export interface ModuleRequest {
specifier: string;
attributes?: ImportAttributes;
phase?: ModulePhase;
}
/** @see https://github.com/nodejs/node/blob/main/src/module_wrap.h */
export declare const enum ModulePhase {
Source = 1,
Evaluation = 2
}
/** Opaque Node internal module request type marker. */
export type ModuleRequestType = unknown;
/**
* Node 24+ ModuleLoader interface.
*
* Breaking changes from v1:
* - getModuleJobForImport removed → getOrCreateModuleJob(parentURL, request, requestType)
* - resolve removed (became private #resolve) → resolveSync(parentURL, request)
* - Parameter order reversed for resolveSync, request object { specifier, attributes }
* - LoadCache became typed Map<url, { [type]: ModuleJob }> with delete only setting undefined
*/
export interface ModuleLoaderV2 {
version: 'v2';
loadCache: LoadCache;
import(specifier: string, parentURL: string, importAttributes: ImportAttributes, phase?: ModulePhase, isEntryPoint?: boolean): Promise<any>;
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[], isInternal?: boolean): void;
getOrCreateModuleJob(parentURL: string, request: ModuleRequest, requestType?: ModuleRequestType): Promise<ModuleJob>;
resolveSync(parentURL: string, request: ModuleRequest): ResolveResult;
load(url: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>;
}
/** Supported Node internal ESM loader shapes. */
export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2;
/** Helpers for locating the current Node internal module loader. */
export declare namespace ModuleLoader {
function fromInternal(): ModuleLoader | undefined;
}
export {};
//# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../../src/internal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
#!/bin/sh
# Resolve $0 through symlinks so basedir is the shim's real directory.
# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim.
link="$0"
hops=0
while [ -L "$link" ] && [ "$hops" -lt 40 ]; do
hops=$((hops+1))
target=$(readlink "$link")
case "$target" in
/*) link="$target" ;;
*) link="$(dirname "$link")/$target" ;;
esac
done
basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')")
basedir_win="$basedir"
exe=""
msys=""
case `uname -a` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir_win=`cygpath -w "$basedir"`
fi
exe=".exe"
msys="true"
;;
*WSL2*)
if command -v wslpath > /dev/null 2>&1; then
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
basedir_win="$basedir"
else
exe=".exe"
fi
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
exec "$basedir/node.exe" "$basedir_win/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
elif [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
elif command -v node >/dev/null 2>&1; then
exec node "$basedir/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
exec node.exe "$basedir_win/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
else
exec node "$basedir/../../../../../../@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js" "$@"
fi
# cmd-shim-target=/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js
@@ -0,0 +1,44 @@
{
"name": "@deepseek-ai/cordis-plugin-loader",
"description": "Plugin loader for cordis",
"version": "1.0.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "vendor/loader"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"node-addon-require-builtin": "^0.1.4",
"@deepseek-ai/cordis": "^4.0.1"
},
"peerDependenciesMeta": {
"node-addon-require-builtin": {
"optional": true
}
},
"dependencies": {
"@deepseek-ai/cosmokit": "^1.8.2"
}
}
@@ -0,0 +1,303 @@
import { Context, Fiber, Inject } from '@deepseek-ai/cordis'
import { deepEqual, isNullable } from '@deepseek-ai/cosmokit'
import { Loader } from '../index.ts'
import { EntryGroup } from './group.ts'
import { EntryTree } from './tree.ts'
import { evaluate, isJsExpr } from './utils.ts'
/** Serialized plugin entry options stored in loader config files. */
export interface EntryOptions {
/** Stable id inside the containing entry tree. */
id: string
/** Module specifier imported by the entry tree. */
name: string
/** Config passed to the plugin. */
config?: any
/** Marks this entry as a nested group. */
group?: boolean | null
/** Prevents this entry and descendants from running. */
disabled?: boolean | null
/** Required services or service intercept config for this entry. */
inject?: Inject | null
}
function updateError(stage: 'import' | 'dispose' | 'apply' | 'rollback', options: EntryOptions, cause: unknown) {
const detail = cause instanceof Error ? cause.message : String(cause)
return new Error(`failed to ${stage} loader entry ${options.id} (${options.name}): ${detail}`, { cause })
}
function takeEntries(object: {}, keys: string[]) {
const result: [string, any][] = []
for (const key of keys) {
if (!(key in object)) continue
result.push([key, object[key]])
delete object[key]
}
return result
}
function sortKeys<T extends {}>(object: T, prepend = ['id', 'name'], append = ['config']): T {
const part1 = takeEntries(object, prepend)
const part2 = takeEntries(object, append)
const rest = takeEntries(object, Object.keys(object)).sort(([a], [b]) => a.localeCompare(b))
return Object.assign(object, Object.fromEntries([...part1, ...rest, ...part2]))
}
function replaceKeys<T extends {}>(target: T, source: T): T {
for (const key of Object.keys(target)) Reflect.deleteProperty(target, key)
return Object.assign(target, source)
}
/** One configured plugin node inside an `EntryTree`. */
export class Entry {
static readonly key = Symbol.for('cordis.entry')
public ctx: Context
public fiber?: Fiber
public parent!: EntryGroup
// safety: call `entry.update()` immediately after creating an entry
public options = {} as EntryOptions
public subgroup?: EntryGroup
public subtree?: EntryTree
_initTask?: Promise<void>
_disposing = 0
constructor(public loader: Loader) {
this.ctx = loader.ctx.extend({ [Entry.key]: this })
this.context.emit('loader/entry-init', this)
}
get context(): Context {
return this.ctx
}
get id() {
let id = this.options.id
if (this.parent.tree.ctx.fiber.entry) {
id = this.parent.tree.ctx.fiber.entry.id + EntryTree.sep + id
}
return id
}
/** True when this entry or any owning parent entry is disabled. */
get disabled() {
return this._disabled(this.options)
}
private _disabled(options: EntryOptions) {
// group is always enabled
if (options.group) return false
if (this.disabledOf(options)) return true
let entry = this.parent.ctx.fiber.entry
while (entry) {
if (this.disabledOf(entry.options)) return true
entry = entry.parent.ctx.fiber.entry
}
return false
}
/**
* Effective disabled state: a `!!js` expression evaluates against the loader
* context. The raw node stays in the options, so write-back keeps the form.
*/
private disabledOf(options: EntryOptions): boolean {
return isJsExpr(options.disabled)
? Boolean(this.evaluate(options.disabled.__jsExpr))
: Boolean(options.disabled)
}
evaluate(expr: string) {
return evaluate(this.ctx, expr)
}
private async _patchContext(diff: string[]) {
await this.context.waterfall('loader/patch-context', this, async () => {
Object.setPrototypeOf(this.ctx, this.parent.ctx)
if (this.fiber?.uid && (diff.includes('config') || this.options.group)) {
await this.fiber.update(this.options.config, true)
}
})
}
async refresh() {
if (this.fiber) return
if (this.disabled) return
await this.init()
}
async _dispose(fiber = this.fiber) {
if (!fiber) return
if (this.fiber === fiber) this.fiber = undefined
this._disposing += 1
try {
await fiber.dispose()
} finally {
this._disposing -= 1
}
}
/** Merge new options, restart as needed, and persist through the parent tree. */
async update(options: Partial<EntryOptions>, create = false, force = false) {
const previousOptions = this.options
const legacy = { ...previousOptions }
const candidate = create ? options as EntryOptions : { ...previousOptions }
if (!create) {
for (const [key, value] of Object.entries(options)) {
if (isNullable(value)) {
delete candidate[key as keyof EntryOptions]
} else {
candidate[key as keyof EntryOptions] = value as never
}
}
}
sortKeys(candidate)
const diff = Object
.keys({ ...candidate, ...legacy })
.filter(key => !deepEqual(candidate[key as keyof EntryOptions], legacy[key as keyof EntryOptions]))
if (!diff.length && !force) return
const commit = () => {
if (create) return
this.options = replaceKeys(previousOptions, candidate)
}
const previous = this.fiber
if (!previous?.uid) {
this.fiber = undefined
this.options = candidate
try {
if (!this._disabled(candidate)) await this.init()
} catch (error) {
this.options = previousOptions
throw error
}
commit()
return
}
if (this._disabled(candidate)) {
this.options = candidate
try {
await this._dispose(previous)
} catch (error) {
this.options = previousOptions
throw updateError('dispose', candidate, error)
}
commit()
this.context.emit('loader/partial-dispose', this, legacy, true)
return
}
const replace = diff.some(key => key === 'name' || key === 'inject' || key === 'group')
if (!replace) {
this.options = candidate
try {
await this._patchContext(diff)
} catch (error) {
this.options = previousOptions
try {
await this._patchContext(diff)
} catch (rollbackError) {
throw updateError('rollback', legacy, new AggregateError([error, rollbackError]))
}
this.context.emit('loader/partial-dispose', this, candidate, true)
throw updateError('apply', candidate, error)
}
commit()
this.context.emit('loader/partial-dispose', this, legacy, true)
return
}
let plugin: any
try {
plugin = diff.includes('name')
? this.loader.unwrapExports(await this.parent.tree.import(candidate.name, this.getOuterStack))
: previous.runtime!.callback
} catch (error) {
throw updateError('import', candidate, error)
}
const previousPlugin = previous.runtime!.callback
this.options = candidate
try {
await this._dispose(previous)
} catch (error) {
this.options = previousOptions
throw updateError('dispose', candidate, error)
}
try {
await this._start(plugin)
} catch (error) {
this.options = previousOptions
try {
await this._start(previousPlugin)
} catch (rollbackError) {
throw updateError('rollback', legacy, new AggregateError([error, rollbackError]))
}
this.context.emit('loader/partial-dispose', this, candidate, true)
throw updateError('apply', candidate, error)
}
commit()
this.context.emit('loader/partial-dispose', this, legacy, true)
}
getOuterStack = () => {
let entry: Entry | undefined = this
const result: string[] = []
do {
result.push(` at ${entry.parent.tree.ctx.baseUrl}#${entry.options.id}`)
entry = entry.parent.ctx.fiber.entry
} while (entry)
return result
}
/** Import and start the configured plugin if it is not already running. */
async init() {
try {
await (this._initTask ??= this._init())
} finally {
this._initTask = undefined
if (!this.loader.getTasks().length) this.ctx.reflect.notify(['loader'])
}
await this._await()
}
async _await() {
try {
await this.fiber?.await()
} catch (error) {
throw updateError('apply', this.options, error)
}
}
private async _init() {
let plugin: any
try {
plugin = this.loader.unwrapExports(await this.parent.tree.import(this.options.name, this.getOuterStack))
} catch (error) {
throw updateError('import', this.options, error)
}
try {
await this._start(plugin)
} catch (error) {
throw updateError('apply', this.options, error)
}
}
private async _start(plugin: any) {
let fiber: Fiber | undefined
try {
await this._patchContext([])
this.loader.showLog(this, 'apply')
fiber = this.fiber = this.ctx.registry.plugin(plugin, this.options.config, this.getOuterStack)
await fiber.await()
} catch (error) {
await this._dispose(fiber)
throw error
}
}
}
@@ -0,0 +1,129 @@
import { Context, Service } from '@deepseek-ai/cordis'
import { Entry, type EntryOptions } from './entry.ts'
import { EntryTree } from './tree.ts'
/** Runtime owner for a list of child loader entries. */
export class EntryGroup {
static readonly key = Symbol.for('cordis.group')
public data: EntryOptions[] = []
constructor(public ctx: Context, public tree: EntryTree) {
const entry = ctx.fiber.entry
if (entry) entry.subgroup = this
}
get context(): Context {
return this.ctx
}
async create(options: Omit<EntryOptions, 'id'>) {
const id = this.tree.ensureId(options)
const existing = this.tree.store[id]
const entry: Entry = existing ?? (this.tree.store[id] = new Entry(this.ctx.loader))
const previousParent = entry.parent
// Entry may be moved from another group,
// so we need to update the parent reference.
entry.parent = this
// Use `create: true` to replace existing entry.options.
try {
await entry.update(options, true, true)
} catch (error) {
if (existing) {
entry.parent = previousParent
} else {
delete this.tree.store[id]
}
throw error
}
return entry.id
}
unlink(options: EntryOptions) {
const config = this.data
const index = config.indexOf(options)
if (index >= 0) config.splice(index, 1)
}
async remove(id: string, isDispose = false) {
const entry = this.tree.store[id]
if (!entry) return
await entry._dispose()
if (!isDispose) {
this.unlink(entry.options)
}
delete this.tree.store[id]
this.context.emit('loader/partial-dispose', entry, entry.options, false)
}
async update(config: EntryOptions[]) {
const oldConfig = this.data as EntryOptions[]
const seen = new Set<string>()
for (const options of config) {
const id = this.tree.ensureId(options)
if (seen.has(id)) throw new TypeError(`duplicate loader entry id: ${id}`)
seen.add(id)
}
const oldMap = Object.fromEntries(oldConfig.map(options => [options.id, options]))
const newMap = Object.fromEntries(config.map(options => [options.id, options]))
try {
const outcomes = await Promise.allSettled(config.map(options => this.create(options)))
// Disposal owns termination: sibling starts can still be settling after
// the containing tree has gone away, but their failures no longer
// describe a live update to roll back.
if (this.ctx.fiber.uid === null) return
const failures = outcomes
.filter((outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected')
.map(outcome => outcome.reason)
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, 'loader entries failed to apply')
for (const id of Object.keys(oldMap)) {
if (!newMap[id]) await this.remove(id, true)
}
this.data = config
} catch (error) {
const rollbackErrors: unknown[] = []
for (const id of Object.keys(newMap).reverse()) {
if (oldMap[id]) continue
try {
await this.remove(id, true)
} catch (rollbackError) {
rollbackErrors.push(rollbackError)
}
}
for (const options of oldConfig) {
try {
await this.create(options)
} catch (rollbackError) {
rollbackErrors.push(rollbackError)
}
}
this.data = oldConfig
if (rollbackErrors.length) throw new AggregateError([error, ...rollbackErrors], 'loader entry rollback failed')
throw error
}
}
async stop() {
for (const options of this.data) {
await this.remove(options.id, true)
}
}
}
/** Plugin that mounts a nested loader entry group. */
export class Group extends EntryGroup {
static initial: Omit<EntryOptions, 'id'>[] = []
static readonly [EntryGroup.key] = true
constructor(public ctx: Context, public config: EntryOptions[]) {
super(ctx, ctx.fiber.entry!.parent.tree)
ctx.on('internal/update', config => this.update(config))
}
async* [Service.init]() {
yield () => this.stop()
await this.update(this.config)
}
}
@@ -0,0 +1,173 @@
import { Context } from '@deepseek-ai/cordis'
import type { Dict } from '@deepseek-ai/cosmokit'
import { Entry } from './entry.ts'
declare module './entry.ts' {
interface EntryOptions {
intercept?: Dict | null
isolate?: Dict<true | string> | null
}
interface Entry {
realm: LocalRealm
}
}
function swap<T extends {}>(target: T, source?: T | null) {
for (const key of Reflect.ownKeys(target)) {
Reflect.deleteProperty(target, key)
}
for (const key of Reflect.ownKeys(source || {})) {
Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source!, key)!)
}
}
/** Symbol realm used to isolate service implementations by entry or label. */
export abstract class Realm {
protected store: Dict<symbol> = Object.create(null)
abstract get suffix(): string
access(key: string, create = false) {
if (create) {
return this.store[key] ??= Symbol(`${key}${this.suffix}`)
} else {
return this.store[key] ?? Symbol(`${key}${this.suffix}`)
}
}
delete(key: string) {
delete this.store[key]
}
get size() {
return Object.keys(this.store).length
}
}
/** Entry-local isolation realm. */
export class LocalRealm extends Realm {
constructor(private entry: Entry) {
super()
}
get suffix() {
return '#' + this.entry.options.id
}
}
/** Named isolation realm shared by entries that use the same label. */
export class GlobalRealm extends Realm {
constructor(public label: string) {
super()
}
get suffix() {
return '@' + this.label
}
}
/** Install loader hooks that apply `intercept` and `isolate` entry options. */
export default function isolate(ctx: Context) {
const realms: Dict<GlobalRealm> = Object.create(null)
const delims: Dict<symbol> = Object.create(null)
function access(entry: Entry, name: string, create: true): symbol
function access(entry: Entry, name: string, create?: boolean): symbol | undefined
function access(entry: Entry, name: string, create = false) {
let realm: Realm | undefined
const label = entry.options.isolate?.[name]
if (!label) return
if (label === true) {
realm = entry.realm ??= new LocalRealm(entry)
} else if (create) {
realm = realms[label] ??= new GlobalRealm(label)
} else {
realm = realms[label]
}
return realm?.access(name, create)
}
ctx.on('loader/entry-init', (entry) => {
entry.ctx[Context.intercept] = Object.create(entry.ctx[Context.intercept])
entry.ctx[Context.isolate] = Object.create(entry.ctx[Context.isolate])
})
ctx.on('loader/patch-context', async (entry, next) => {
// step 1: generate new isolate map
const newMap: Dict<symbol> = Object.create(entry.parent.ctx[Context.isolate])
for (const name of Object.keys(entry.options.isolate ?? {})) {
newMap[name] = access(entry, name, true)
}
// step 2: generate service diff
const diff: Dict<[symbol, symbol, symbol, symbol]> = Object.create(null)
const oldMap = entry.ctx[Context.isolate]
for (const name in { ...newMap, ...delims }) {
if (newMap[name] === oldMap[name]) continue
const delim = delims[name] ??= Symbol(`delim:${name}`)
entry.ctx[delim] = Symbol(`${name}#${entry.id}`)
for (const symbol of [oldMap[name], newMap[name]]) {
const impl = symbol && entry.ctx.reflect.store[symbol]
if (!impl) continue
if (!impl.fiber) {
entry.ctx.logger.warn(new Error(`expected service ${name} to be implemented`))
continue
}
diff[name] = [oldMap[name], newMap[name], entry.ctx[delim], impl.fiber.ctx[delim]]
if (entry.ctx[delim] !== impl.fiber.ctx[delim]) break
}
}
// step 3: set prototype for transferred context
Object.setPrototypeOf(entry.ctx[Context.isolate], entry.parent.ctx[Context.isolate])
Object.setPrototypeOf(entry.ctx[Context.intercept], entry.parent.ctx[Context.intercept])
swap(entry.ctx[Context.isolate], newMap)
swap(entry.ctx[Context.intercept], entry.options.intercept)
// step 4: reload fiber
await next()
// step 5: replace service impl
for (const [symbol1, symbol2, flag1, flag2] of Object.values(diff)) {
if (flag1 === flag2 && entry.ctx.reflect.store[symbol1] && !entry.ctx.reflect.store[symbol2]) {
entry.ctx.reflect.store[symbol2] = entry.ctx.reflect.store[symbol1]
delete entry.ctx.reflect.store[symbol1]
}
}
// step 6: reflect notify
ctx.reflect.notify(Object.keys(diff), (ctx, name) => {
const [symbol1, symbol2, flag1, flag2] = diff[name]
const symbol3 = ctx[Context.isolate][name]
const flag3 = ctx[delims[name]]
return (symbol1 === symbol3 || symbol2 === symbol3) && (flag1 === flag3) !== (flag1 === flag2)
})
// step 7: clean up delimiters
for (const name in delims) {
if (!Reflect.ownKeys(newMap).includes(name)) {
delete entry.ctx[delims[name]]
}
}
})
ctx.on('loader/partial-dispose', (entry, legacy, active) => {
for (const [name, label] of Object.entries(legacy.isolate ?? {})) {
if (label === true) continue
if (active && entry.options.isolate?.[name] === label) continue
const realm = realms[label]
if (!realm) continue
// realm garbage collection
for (const entry of ctx.loader.entries()) {
// has reference to this realm
if (entry.options.isolate?.[name] === realm.label) return
}
realm.delete(name)
if (!realm.size) {
delete realms[realm.label]
}
}
})
}
@@ -0,0 +1,166 @@
import { composeError, Context } from '@deepseek-ai/cordis'
import { isNonNullable, type Dict } from '@deepseek-ai/cosmokit'
import { Entry, type EntryOptions } from './entry.ts'
import { EntryGroup } from './group.ts'
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
export abstract class EntryTree {
static readonly sep = ':'
public ctx: Context
public enableLogs?: boolean
public root: EntryGroup
public store: Dict<Entry> = Object.create(null)
constructor(ctx: Context) {
this.ctx = ctx.extend({ baseUrl: ctx.baseUrl })
this.root = new EntryGroup(this.ctx, this)
const entry = this.ctx.fiber.entry
if (entry) entry.subtree = this
}
get context(): Context {
return this.ctx
}
/** Iterate entries in this tree and any nested subtrees. */
* entries(): Generator<Entry, void, void> {
for (const entry of Object.values(this.store)) {
yield entry
if (!entry.subtree) continue
yield* entry.subtree.entries()
}
}
/** Return pending import and lifecycle tasks owned by this tree. */
getTasks() {
return [...this.entries()]
.map(entry => entry._initTask || entry.fiber?.inertia)
.filter(isNonNullable)
}
/**
* Wait until this tree has no active import or lifecycle tasks.
* @throws a settled fiber failure, or an aggregate when several fibers failed.
*/
async await() {
while (true) {
const tasks = this.getTasks()
if (tasks.length) {
await Promise.allSettled(tasks)
continue
}
const outcomes = await Promise.allSettled(
[...this.entries()].map(entry => entry._await()),
)
const failures = outcomes
.filter((outcome): outcome is PromiseRejectedResult => outcome.status === 'rejected')
.map(outcome => outcome.reason)
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, 'loader fibers failed')
this.ctx.reflect.notify(['loader'])
if (!this.getTasks().length) return
}
}
ensureId(options: Partial<EntryOptions>) {
if (!options.id) {
do {
options.id = Math.random().toString(16).slice(2, 10)
} while (this.store[options.id])
}
return options.id!
}
/** Resolve an entry by id, including nested ids separated by `EntryTree.sep`. */
resolve(id: string) {
const parts = id.split(EntryTree.sep)
let tree: EntryTree | undefined = this
const final = parts.pop()!
for (const part of parts) {
tree = tree.store[part]?.subtree
if (!tree) throw new Error(`cannot resolve entry ${id}`)
}
const entry = tree.store[final]
if (!entry) throw new Error(`cannot resolve entry ${id}`)
return entry
}
resolveGroup(id: string | null) {
if (!id) return this.root
const entry = this.resolve(id)
if (!entry.subgroup) throw new Error(`entry ${id} is not a group`)
return entry.subgroup
}
/** Create an entry in the root group or a nested group. */
async create(options: Omit<EntryOptions, 'id'>, parent: string | null = null, position = Infinity) {
const group = this.resolveGroup(parent)
const id = await group.create(options)
const entry = this.resolve(id)
group.data.splice(position, 0, entry.options)
group.tree.write()
return id
}
/** Stop and remove an entry from its parent group. */
async remove(id: string) {
const entry = this.resolve(id)
await entry.parent.remove(id)
entry.parent.tree.write()
}
/** Update an entry and optionally move it to another group. */
async update(id: string, options: Omit<EntryOptions, 'id' | 'name'>, parent?: string | null, position?: number) {
const entry = this.resolve(id)
const source = entry.parent
const sourceIndex = source.data.indexOf(entry.options)
let target = source
if (parent !== undefined) {
target = this.resolveGroup(parent)
source.unlink(entry.options)
target.data.splice(position ?? Infinity, 0, entry.options)
entry.parent = target
}
try {
await entry.update(options, false, true)
} catch (error) {
if (parent !== undefined) {
target.unlink(entry.options)
source.data.splice(sourceIndex < 0 ? source.data.length : sourceIndex, 0, entry.options)
entry.parent = source
try {
await entry.update({}, false, true)
} catch (rollbackError) {
throw new AggregateError([error, rollbackError], `failed to roll back loader entry move ${id}`)
}
}
throw error
}
source.tree.write()
if (target !== source) target.tree.write()
}
/** Import a plugin module from a specifier or `cordis:` builtin. */
import(name: string, getOuterStack?: () => string[]) {
if (name.startsWith('cordis:')) {
return this.ctx.loader.builtins[name.slice(7)]
}
return composeError(async (info) => {
// ModuleJob.run
// onImport.tracePromise.__proto__
// internal.import
info.offset += 3
if (this.ctx.loader.internal) {
return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {})
} else if (name.startsWith('.')) {
return await import(/* @vite-ignore */new URL(name, this.ctx.baseUrl).href)
} else {
return await import(/* @vite-ignore */name)
}
}, getOuterStack)
}
/** Persist current tree state. In-memory trees may implement this as a no-op. */
abstract write(): void
}
@@ -0,0 +1,32 @@
import { valueMap } from '@deepseek-ai/cosmokit'
// eslint-disable-next-line no-new-func
/** Evaluate a JavaScript expression against a loader context scope. */
export const evaluate = new Function('ctx', 'expr', `
with (ctx) {
return eval(expr)
}
`) as ((ctx: object, expr: string) => any)
/** Recursively replace YAML `!js` expression nodes with evaluated values. */
export function interpolate(ctx: object, value: any) {
if (isJsExpr(value)) {
return evaluate(ctx, value.__jsExpr)
} else if (!value || typeof value !== 'object') {
return value
} else if (Array.isArray(value)) {
return value.map(item => interpolate(ctx, item))
} else {
return valueMap(value, item => interpolate(ctx, item))
}
}
/** Return true when a value is a serialized loader JavaScript expression. */
export function isJsExpr(value: any): value is JsExpr {
return value instanceof Object && '__jsExpr' in value
}
/** Serialized JavaScript expression produced by the include YAML tag. */
export interface JsExpr {
__jsExpr: string
}
@@ -0,0 +1,202 @@
import { Context, FiberState, Inject, Service, type Fiber } from '@deepseek-ai/cordis'
import { defineProperty, isNullable, type Dict } from '@deepseek-ai/cosmokit'
import { ModuleLoader } from './internal.ts'
import { Entry, type EntryOptions } from './config/entry.ts'
import { EntryGroup } from './config/group.ts'
import isolate from './config/isolate.ts'
import { EntryTree } from './config/tree.ts'
import { interpolate } from './config/utils.ts'
/** Re-export entry node APIs. */
export * from './config/entry.ts'
/** Re-export nested entry group APIs. */
export * from './config/group.ts'
/** Re-export service isolation helpers. */
export * from './config/isolate.ts'
/** Re-export entry tree persistence APIs. */
export * from './config/tree.ts'
/** Re-export loader config expression helpers. */
export * from './config/utils.ts'
/** Re-export Node internal module loader compatibility types. */
export * from './internal.ts'
declare module '@deepseek-ai/cordis' {
interface Events {
'exit'(signal: NodeJS.Signals): Promise<void>
'loader/config-update'(): void
'loader/entry-init'(entry: Entry): void
'loader/partial-dispose'(entry: Entry, legacy: Partial<EntryOptions>, active: boolean): void
'loader/patch-context'(entry: Entry, next: () => void | Promise<void>): void | Promise<void>
}
interface Context {
loader: Loader
}
interface EnvData {
startTime?: number
}
interface Fiber {
entry?: Entry
}
}
/** Loader config and dependency intercept namespace. */
export namespace Loader {
/** Root loader configuration. */
export interface Config {
/** Base URL used to resolve relative plugin specifiers and config paths. */
baseUrl?: string
}
/** Intercept config used when other plugins depend on `loader`. */
export interface Intercept {
/** Keep dependent plugins pending while loader entries are still loading. */
await?: boolean
}
}
/**
* Service that owns a loader entry tree and imports configured plugins.
*
* Subclasses provide persistence by implementing `write()` on `EntryTree`.
*/
export class Loader extends EntryTree {
declare [Service.config]: Loader.Intercept
public envData = process.env.CORDIS_SHARED
? JSON.parse(process.env.CORDIS_SHARED)
: { startTime: Date.now() }
public name = 'loader'
public internal = ModuleLoader.fromInternal()
public builtins: Dict<any> = Object.create(null)
constructor(ctx: Context, public config: Loader.Config = {}) {
super(ctx)
if (config.baseUrl) {
this.ctx.baseUrl = config.baseUrl
}
const self = this
defineProperty(this, Service.tracker, {
associate: 'loader',
property: 'ctx',
noShadow: true,
})
ctx.reflect.provide('loader', this, this[Service.check])
ctx.on('internal/config', function (this: Fiber, _config, next) {
const config = next()
if (!this.entry || this.parent.fiber?.entry === this.entry) return config
// Tree carriers (Group, Include) keep their configs literal: their
// entry and patch lists hold other rows' configs, whose `!!js`
// expressions belong to those rows' own fibers.
const plugin = this.runtime?.callback as Record<PropertyKey, unknown> | undefined
if (plugin?.[EntryGroup.key]) return config
return interpolate(this.ctx, config)
}, { global: true })
ctx.on('internal/update', async function (config, noSave, next) {
if (!this.entry || noSave || this.parent.fiber?.entry === this.entry) return next()
await next()
const unparse = this.runtime?.Config?.['simplify']
this.entry.options.config = unparse ? unparse(config) : config
this.entry.parent.tree.write()
}, { global: true, prepend: true })
ctx.on('internal/update', function (config, _, next) {
if (!this.entry || this.parent.fiber?.entry === this.entry) return next()
self.showLog(this.entry, 'reload')
return next()
}, { global: true })
ctx.on('internal/plugin', (fiber) => {
// 1. set `fiber.entry`
if (fiber.parent[Entry.key] && !fiber.entry) {
fiber.entry = fiber.parent[Entry.key]
// FIXME merge config
Inject.resolve(fiber.entry!.options.inject, fiber.inject)
}
// 2. handle self-dispose
// We only care about `ctx.fiber.dispose()`, so we need to filter out other cases.
// case 1: fiber is created
if (fiber.uid) return
// case 2: fiber is not tracked by loader
if (!fiber.entry) return
// case 3: fiber is a child plugin under the entry (not the entry's root fiber)
if (fiber.parent.fiber?.entry === fiber.entry) return
// case 4: fiber is disposed on behalf of plugin deletion (such as plugin hmr)
// self-dispose: ctx.fiber.dispose() -> fiber / runtime dispose -> delete(plugin)
// plugin hmr: delete(plugin) -> runtime dispose -> fiber dispose
if (!ctx.registry.has(fiber.runtime!.callback)) return
// case 5: the entry's tree is being disposed
const treeOwner = fiber.entry.parent.tree.ctx.fiber
if (!treeOwner.uid || treeOwner.state === FiberState.UNLOADING) return
// case 6: Loader is replacing or removing this exact fiber
if (fiber.entry._disposing) return
this.showLog(fiber.entry, 'unload')
// case 7: fiber is disposed by loader behavior
// such as inject checker, config file update, ancestor group disable
if (fiber.entry.disabled) return
fiber.entry.options.disabled = true
fiber.entry.parent.tree.write()
})
ctx.plugin(isolate)
}
write() {
// Loader's root tree is in-memory; writes are no-ops.
}
[Service.check]() {
const config: Loader.Intercept = Service.prototype[Service.resolveConfig].call(this)
if (config.await && this.getTasks().length) return false
return true
}
showLog(entry: Entry, type: string) {
if (entry.options.group || !entry.parent.tree.enableLogs) return
this.ctx.root.logger?.('loader').info('%s plugin %C', type, entry.options.name)
}
/** Return the loader entry id that owns `fiber`, if any. */
locate(fiber = this.ctx.fiber) {
while (1) {
if (fiber.entry) return fiber.entry.id
const next = fiber.parent.fiber
if (fiber === next) return
fiber = next
}
}
/** Hook for hosts that can restart the process on full-reload requests. */
exit() {
}
/** Normalize ESM/CJS/default export shapes before applying a plugin. */
unwrapExports(exports: any) {
if (isNullable(exports)) return exports
exports = exports.default ?? exports
// https://github.com/evanw/esbuild/issues/2623
// https://esbuild.github.io/content-types/#default-interop
if (!exports.__esModule) return exports
return exports.default ?? exports
}
}
export default Loader
@@ -0,0 +1,132 @@
import { createRequire, type LoadHookContext } from 'node:module'
import type { Dict } from '@deepseek-ai/cosmokit'
/** Node internal module format names handled by loader hooks. */
export type ModuleFormat = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
/** Source payload accepted by Node internal module load hooks. */
export type ModuleSource = string | ArrayBuffer
/** Result returned by a Node internal resolve hook. */
export interface ResolveResult {
format: ModuleFormat
url: string
}
/** Result returned by a Node internal load hook. */
export interface LoadResult {
format: ModuleFormat
source?: ModuleSource
}
type LoadCacheData = ModuleJob // | Function
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_map.js */
interface LoadCache extends Omit<Map<string, Dict<LoadCacheData>>, 'get' | 'set' | 'has'> {
get(url: string, type?: string): LoadCacheData | undefined
set(url: string, type?: string, job?: LoadCacheData): this
has(url: string, type?: string): boolean
}
/** Minimal Node internal ModuleWrap surface used by HMR helpers. */
export interface ModuleWrap {
url: string
getNamespace(): any
}
/** @see https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/module_job.js */
export interface ModuleJob {
url: string
loader: ModuleLoader
module?: ModuleWrap
importAttributes: ImportAttributes
linked: Promise<ModuleJob[]>
instantiate(): Promise<void>
run(): Promise<{ module: ModuleWrap }>
}
/**
* Node 22/23 ModuleLoader interface.
*
* Key methods:
* - getModuleJobForImport(specifier, parentURL, importAttributes)
* - resolve(specifier, parentURL, importAttributes) → Promise<ResolveResult>
* - resolveSync(specifier, parentURL, importAttributes) → ResolveResult
*/
export interface ModuleLoaderV1 {
version: 'v1'
loadCache: LoadCache
import(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<any>
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[]): void
getModuleJobForImport(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ModuleJob>
resolve(specifier: string, parentURL: string, importAttributes: ImportAttributes): Promise<ResolveResult>
resolveSync(specifier: string, parentURL: string, importAttributes: ImportAttributes): ResolveResult
load(specifier: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>
}
/** Node 24+ module request object. */
export interface ModuleRequest {
specifier: string
attributes?: ImportAttributes
phase?: ModulePhase
}
/** @see https://github.com/nodejs/node/blob/main/src/module_wrap.h */
export const enum ModulePhase {
Source = 1,
Evaluation = 2,
}
/** Opaque Node internal module request type marker. */
export type ModuleRequestType = unknown // internal symbols
/**
* Node 24+ ModuleLoader interface.
*
* Breaking changes from v1:
* - getModuleJobForImport removed → getOrCreateModuleJob(parentURL, request, requestType)
* - resolve removed (became private #resolve) → resolveSync(parentURL, request)
* - Parameter order reversed for resolveSync, request object { specifier, attributes }
* - LoadCache became typed Map<url, { [type]: ModuleJob }> with delete only setting undefined
*/
export interface ModuleLoaderV2 {
version: 'v2'
loadCache: LoadCache
import(specifier: string, parentURL: string, importAttributes: ImportAttributes, phase?: ModulePhase, isEntryPoint?: boolean): Promise<any>
register(specifier: string | URL, parentURL?: string | URL, data?: any, transferList?: any[], isInternal?: boolean): void
getOrCreateModuleJob(parentURL: string, request: ModuleRequest, requestType?: ModuleRequestType): Promise<ModuleJob>
resolveSync(parentURL: string, request: ModuleRequest): ResolveResult
load(url: string, context: Pick<LoadHookContext, 'format' | 'importAttributes'>): Promise<LoadResult>
}
/** Supported Node internal ESM loader shapes. */
export type ModuleLoader = ModuleLoaderV1 | ModuleLoaderV2
/** Helpers for locating the current Node internal module loader. */
export namespace ModuleLoader {
let _cachedLoader: ModuleLoader | undefined
function requireInternal(id: string): any {
const require = createRequire(import.meta.url)
if (process.execArgv.includes('--expose-internals')) {
try {
return require(id)
} catch {}
}
try {
return require('node-addon-require-builtin').requireBuiltin(id)
} catch {}
}
export function fromInternal(): ModuleLoader | undefined {
if (_cachedLoader) return _cachedLoader
const [major] = process.versions.node.split('.').map(Number)
if (major >= 24) {
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
if (raw) return _cachedLoader = Object.assign(raw, { version: 'v2' })
} else if (major >= 22) {
const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader()
if (raw) return _cachedLoader = Object.assign(raw, { version: 'v1' })
}
}
}
@@ -0,0 +1 @@
../../../@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit
@@ -0,0 +1 @@
../../../@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/cordis-plugin-include
@@ -0,0 +1 @@
../../../@deepseek-ai+cordis-plugin-loader@1.0.2_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/cordis-plugin-loader
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,101 @@
# Cordis
Cordis is a TypeScript plugin framework for applications that need explicit
dependency injection, scoped services, lifecycle-managed cleanup, and optional
configuration-driven loading. The core package is published as `cordis`; the
official packages in this repository add a loader, config-file includes, HMR,
console logging, timers, and project scaffolding.
## Install
```sh
yarn add cordis
```
Cordis is ESM-first. The repository is tested on current Node releases, and the
scaffolder requires Node 22 or newer.
## Quick Start
```ts
import { Context, Service } from 'cordis'
declare module 'cordis' {
interface Context {
counter: Counter
}
interface Events {
'app/ready'(message: string): void
}
}
class Counter extends Service {
value = 0
constructor(ctx: Context) {
super(ctx, 'counter')
}
next() {
return ++this.value
}
}
const greeter = Object.assign((ctx: Context) => {
ctx.on('app/ready', (message) => {
ctx.logger.info('%s #%d', message, ctx.counter.next())
})
}, {
inject: ['counter'],
})
const root = new Context()
await root.plugin(Counter)
await root.plugin(greeter)
root.emit('app/ready', 'started')
await root.fiber.dispose()
```
The important pieces are:
- `new Context()` creates the root dependency container.
- `ctx.plugin()` starts a plugin and returns a `Fiber`.
- `inject` tells Cordis which services must exist before the plugin runs.
- Effects, event listeners, and services are removed when their owning fiber is
disposed.
## Documentation
- [Tutorial: build a plugin](../../docs/tutorials/build-a-plugin.md)
- [Guide: plugin lifecycle](../../docs/guides/plugin-lifecycle.md)
- [Guide: loader configuration](../../docs/guides/loader-config.md)
- [API reference](../../docs/api/core.md)
## Packages
| Package | Purpose |
| --- | --- |
| `cordis` | Core context, plugin registry, fiber lifecycle, events, services, and logger. |
| `create-cordis` | Interactive project scaffolder. |
| `@cordisjs/plugin-loader` | Runtime plugin tree and loader service. |
| `@cordisjs/plugin-include` | YAML/JSON config-file include support for the loader. |
| `@cordisjs/plugin-group` | Nested plugin groups for loader configs. |
| `@cordisjs/plugin-hmr` | Hot module replacement for loader-managed plugins. |
| `@cordisjs/plugin-logger-console` | Console exporter for the built-in logger. |
| `@cordisjs/plugin-timer` | Disposal-aware timeout, interval, throttle, and debounce helpers. |
| `@cordisjs/utils` | Shared utilities used by Cordis packages. |
## Development
```sh
yarn install
yarn build
yarn test
yarn lint
```
The monorepo uses Yakumo to build and test all packages. Most examples in the
docs use public APIs from `cordis`; loader examples additionally use
`@cordisjs/plugin-loader` and `@cordisjs/plugin-include`.
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { Context } from '@deepseek-ai/cordis'
import { pathToFileURL } from 'node:url'
import Loader from '@deepseek-ai/cordis-plugin-loader'
const ctx = new Context()
ctx.baseUrl = pathToFileURL(process.cwd()).href + '/'
await ctx.plugin(Loader)
await ctx.loader.create({
name: '@deepseek-ai/cordis-plugin-include',
config: {
path: './cordis.yml',
},
})
@@ -0,0 +1,101 @@
import type { Dict } from '@deepseek-ai/cosmokit';
import { EventsService } from './events.ts';
import { LoggerService } from './logger.ts';
import { ReflectService } from './reflect.ts';
import { RegistryService, type InjectKey } from './registry.ts';
import { symbols } from './utils.ts';
import './fiber.ts';
/**
* Public shape of a Cordis context.
*
* The concrete `Context` class is proxied at runtime, so this interface is
* augmented by core services and plugins to describe the properties that may
* be read from `ctx`.
*/
export interface Context {
/** Isolation map: service name → scope label. Lookups for a name resolve within its label. */
[symbols.isolate]: Dict<symbol>;
/** Intercept map: service name → config merged into that service's per-plugin config. */
[symbols.intercept]: Dict;
/** The root context of the application (every child context shares it). @experimental */
root: this;
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
baseUrl?: string;
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
events: EventsService;
/** The logging service. Call `ctx.logger(name)` for a named logger. */
logger: LoggerService;
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
reflect: ReflectService;
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
registry: RegistryService;
}
/**
* Root and child dependency containers for Cordis plugins.
*
* A context is a proxy: normal property reads go through the service resolver,
* while `extend()`, `isolate()`, and `intercept()` create scoped child
* contexts without mutating their parent.
*/
export declare class Context {
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
static readonly effect: unique symbol;
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
static readonly filter: unique symbol;
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
static readonly isolate: unique symbol;
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
static readonly intercept: unique symbol;
/**
* 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: any): value is Context;
/** Create the root context and install the built-in services. */
constructor();
/**
* 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?: {}): this;
/**
* 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: string, label?: symbol): this;
/**
* Add service-specific intercept config for plugins started below this
* context.
*
* Plugins loaded under the returned context see `config` merged into the
* service's resolved config (ancestor entries first; see
* `Service[symbols.resolveConfig]`). The parent context is not affected.
*
* @param name — the service name whose config to intercept.
* @param config — the intercept config to merge for that service.
* @returns a child context carrying the additional intercept entry.
*/
intercept<K extends InjectKey>(name: K, config: Context[K] extends {
[symbols.config]: infer T;
} ? T : never): this;
intercept(name: string, config: any): this;
}
//# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAA;AAC/D,OAAO,EAAgB,OAAO,EAAE,MAAM,YAAY,CAAA;AAClD,OAAsB,YAAY,CAAA;AAElC;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,8FAA8F;IAC9F,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAC/B,yFAAyF;IACzF,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;IACzB,yFAAyF;IACzF,IAAI,EAAE,IAAI,CAAA;IACV,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wFAAwF;IACxF,MAAM,EAAE,aAAa,CAAA;IACrB,uEAAuE;IACvE,MAAM,EAAE,aAAa,CAAA;IACrB,sFAAsF;IACtF,OAAO,EAAE,cAAc,CAAA;IACvB,0FAA0F;IAC1F,QAAQ,EAAE,eAAe,CAAA;CAC1B;AAED;;;;;;GAMG;AACH,qBAAa,OAAO;IAClB,yFAAyF;IACzF,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,MAAM,CAAiB;IACtD,qFAAqF;IACrF,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,MAAM,CAAiB;IACtD,qFAAqF;IACrF,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,CAAkB;IACxD,uFAAuF;IACvF,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,MAAM,CAAoB;IAE5D;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,OAAO;IASvC,iEAAiE;;IAoBjE;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,KAAK,GAAG,IAAI;IAUvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;IAMpC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,CAAC,SAAS,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;QAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI;IACnH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI;CAM3C"}
@@ -0,0 +1,240 @@
import type { Promisify } from '@deepseek-ai/cosmokit';
import { Context } from './context.ts';
import { Fiber, FiberState } from './fiber.ts';
/**
* Return whether an event result should stop a bail-style dispatch.
*
* @param value — a listener's return value.
* @returns `true` unless `value` is `null`, `false`, or `undefined`.
*/
export declare function isBailed(value: any): boolean;
/** Extract the parameter tuple from a function type. */
export type Parameters<F> = F extends (...args: infer P) => any ? P : never;
/** Extract the return type from a function type. */
export type ReturnType<F> = F extends (...args: any) => infer R ? R : never;
/** Extract the explicit `this` type from a function type. */
export type ThisType<F> = F extends (this: infer T, ...args: any) => any ? T : never;
/**
* Event dispatch strategy used by the event service.
*
* `emit` runs synchronous listeners without awaiting them, `parallel` awaits
* all listeners together, `serial` awaits them in order until one bails,
* `bail` stops on the first synchronous bail value, and `waterfall` composes
* listeners around a final `next` callback.
*/
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall';
declare module './context.ts' {
interface Context {
/**
* Dispatch an event, running all listeners concurrently.
*
* @param name — the event name.
* @param args — arguments passed to every listener.
* @returns a promise resolving once every listener has settled.
*/
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>;
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>;
/**
* Dispatch an event synchronously, ignoring listener return values.
*
* @param name — the event name.
* @param args — arguments passed to every listener.
*/
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void;
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void;
/**
* Dispatch an event, awaiting listeners in order until one bails.
*
* @param name — the event name.
* @param args — arguments passed to each listener.
* @returns the first bail value (non-null, non-false, non-undefined), if any.
*/
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>;
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>;
/**
* Dispatch an event, calling listeners in order until one bails.
*
* @param name — the event name.
* @param args — arguments passed to each listener.
* @returns the first bail value (non-null, non-false, non-undefined), if any.
*/
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>;
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>;
/**
* Dispatch an event whose last argument is a `next` continuation.
*
* Each listener wraps the rest of the chain: calling `next()` invokes the
* next listener (finally the built-in behavior); not calling it vetoes.
*
* @param name — the event name.
* @param args — listener arguments; the final one is the innermost `next`.
* @returns the outermost listener's return value.
*/
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>;
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>;
/**
* Register an event listener owned by the current fiber.
*
* @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<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean;
/**
* Same as `on()`, but the listener disposes itself after its 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<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean;
}
}
/** Options accepted by `ctx.on()` and `ctx.once()`. */
export interface EventOptions {
/** Add the listener before existing listeners for the same event. */
prepend?: boolean;
/** Receive the event regardless of context filter checks. */
global?: boolean;
}
/** Registered listener record stored by the event service. */
export interface Hook extends EventOptions {
ctx: Context;
callback: (...args: any[]) => any;
}
/**
* Event bus installed as `ctx.events` and mixed into every context.
*
* The service supports concurrent, synchronous, serial, bail, and waterfall
* dispatch and automatically disposes listeners with their owning fiber.
*/
export declare class EventsService {
private ctx;
_hooks: Record<keyof any, Hook[]>;
constructor(ctx: Context);
/**
* 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: string, args: any[]): ((...args: any[]) => any)[];
/**
* 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.
*/
parallel(...args: any[]): Promise<void>;
/**
* Run listeners synchronously without waiting for returned promises.
*
* @param args — optional `this`, the event name, then listener arguments.
*/
emit(...args: any[]): void;
/**
* 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.
*/
serial(...args: any[]): Promise<any>;
/**
* 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: any[]): any;
/**
* 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: any[]): any;
/**
* 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: string, hooks: Hook[], callback: any, options: EventOptions): () => void;
/**
* 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: Hook[], callback: any): true | undefined;
/**
* 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: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions): any;
/**
* 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: string, listener: (...args: any) => any, options?: boolean | EventOptions): any;
}
/**
* Built-in framework events used by core services and extension points.
*
* Plugin and status events track fiber lifecycle, service events observe
* dependency registration, update/get/set/listener events allow core services
* to intercept runtime operations, and `internal/dispatch` exposes event-bus
* diagnostics before public events are delivered.
*/
export interface Events {
/** A plugin fiber was created or its uid was cleared on disposal. */
'internal/plugin'(fiber: Fiber): void;
/** A fiber changed lifecycle state; receives the fiber and its previous state. */
'internal/status'(fiber: Fiber, oldValue: FiberState): void;
/**
* Resolve raw plugin config after the fiber's injections become active.
* @param config - the raw config for this activation.
* @mode waterfall
*/
'internal/config'(this: Fiber, config: any, next: () => any): any;
/** Interception hook for a service binding (no core producer). */
'internal/service'(this: Context, name: string, value: any): void;
/** Waterfall: a fiber config update is being applied; skip `next()` to veto. */
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void | Promise<void>): void | Promise<void>;
/** Waterfall: a service is being read through the context proxy. */
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any;
/** Waterfall: a service is being written through the context proxy. */
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean;
/** Bail: a listener is being registered; a non-null result replaces registration. */
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void;
/** An event is being dispatched to listeners (fired for non-internal events only). */
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void;
}
//# sourceMappingURL=events.d.ts.map
@@ -0,0 +1,202 @@
import type { Awaitable, Dict } from '@deepseek-ai/cosmokit';
import { Context } from './context.ts';
import type { Plugin } from './registry.ts';
import { DisposableList } from './utils.ts';
import type { Impl } from './reflect.ts';
import type { StandardSchemaV1 } from '@standard-schema/spec';
declare module './context.ts' {
interface Context extends Pick<Fiber, 'effect'> {
/** The fiber (plugin runtime instance) that owns this context. */
fiber: Fiber;
}
}
/** Error raised when plugin configuration fails standard-schema validation. */
export declare class ValidationError extends TypeError {
name: string;
/**
* Build the aggregated message from schema issues.
*
* @param issues — the standard-schema issues, one message line each.
*/
constructor(issues: readonly StandardSchemaV1.Issue[]);
}
/**
* Validate and normalize config for a plugin runtime before it starts.
*
* @param runtime — the plugin runtime whose `Config` schema to apply.
* @param config — the raw user config.
* @returns the validated config, or `config` unchanged if the runtime has no schema.
* @throws {ValidationError} when validation reports issues.
*/
export declare function resolveConfig(runtime: Plugin.Runtime, config: any): any;
interface AsyncDisposable<T extends Awaitable<void> = Awaitable<void>> extends PromiseLike<() => T> {
(): T;
}
/**
* Function returned by an effect to release resources during disposal.
*
* Disposers run in reverse registration order when the owning fiber unloads;
* they may be async, in which case unloading awaits them.
*/
export type Disposable<T = any> = () => T;
/**
* Effect body result accepted by `ctx.effect()` and plugin startup.
*
* Either a single disposer, a promise of one, or a (possibly async) iterable
* yielding several — generator effects register each yielded disposer as it
* is produced.
*/
export type Effect<T = any> = SyncEffect<T> | AsyncEffect<T>;
type SyncEffect<T = any> = Disposable<T> | Iterable<Disposable<T>, void, void>;
type AsyncEffect<T = any> = Promise<Disposable<T>> | AsyncIterable<Disposable<T>, void, void>;
/** Tree node used to expose nested effect labels for diagnostics. */
export interface EffectMeta {
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
label: string;
/** Metadata of nested effects registered while this effect ran. */
children: EffectMeta[];
}
/**
* Lifecycle state for one plugin fiber.
*
* `PENDING` — waiting for required services; `LOADING` — the plugin callback
* is running; `ACTIVE` — loaded and providing; `FAILED` — the callback or its
* config threw; `UNLOADING` — disposers are running; `DISPOSED` — the fiber
* was removed and cannot restart.
*/
export declare const enum FiberState {
PENDING = 0,
LOADING = 1,
ACTIVE = 2,
FAILED = 3,
DISPOSED = 4,
UNLOADING = 5
}
/** Framework error with a stable machine-readable code. */
export declare class CordisError extends Error {
code: CordisError.Code;
/**
* @param code — the stable error code; also the default message.
* @param message — optional human-readable override.
*/
constructor(code: CordisError.Code, message?: string);
}
/** Cordis error code definitions. */
export declare namespace CordisError {
type Code = keyof typeof Code;
const Code: {
readonly INACTIVE_EFFECT: "cannot create effect on inactive context";
};
}
/**
* Runtime instance of one plugin application.
*
* A fiber tracks dependency state, validated config, lifecycle effects, and
* cleanup for the plugin context returned by `ctx.plugin()`.
*/
export declare class Fiber {
parent: Context;
inject: Dict<any>;
runtime: Plugin.Runtime | null;
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
uid: number | null;
/** The context this fiber's plugin runs in (extends the parent context). */
readonly ctx: Context;
/** The validated plugin config (updated by `update()`). */
config: any;
/** The raw plugin config, re-resolved before each activation. */
_config: any;
/** Current lifecycle state; transitions emit `internal/status`. */
state: FiberState;
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
readonly dispose: () => Promise<void>;
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
store: Dict<Impl> | undefined;
/** The in-flight load/unload transition, if one is currently running. */
inertia: Promise<void> | undefined;
readonly _hooks: Dict<DisposableList<Function>>;
readonly _disposables: DisposableList<Disposable<any>>;
protected context: Context;
private _error;
private _runner;
private _store;
/**
* 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: Context, config: any, inject: Dict<any>, runtime: Plugin.Runtime | null, getOuterStack: () => string[]);
/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
get name(): string;
/**
* 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(): void;
private _execute;
/**
* Register a cleanup-aware effect on this fiber.
*
* `execute` runs immediately; the disposers it produces are collected and
* run (in reverse order) either when the returned disposer is called or
* when the fiber unloads, whichever comes first. Calling the disposer twice
* is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
* already disposed, and `TypeError` if `execute` returns an invalid shape.
*
* @param execute — the effect body; see {@link Effect} for accepted shapes.
* @param label — effect label shown in `getEffects()` diagnostics.
* @returns a disposer that tears the effect down and settles once done.
*/
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>;
/** Same as above for async effects; the disposer is also awaitable. */
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>;
/**
* Return metadata for currently registered effects.
*
* @returns one {@link EffectMeta} tree per labeled live effect.
*/
getEffects(): EffectMeta[];
private _getState;
private _updateState;
_checkImpl(name: string): boolean | undefined;
_refresh(): void;
private _setEpoch;
private _resolveConfig;
private _reload;
private _unload;
/**
* 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.
*/
await(): Promise<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.
*/
restart(): Promise<void>;
/**
* 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 the update waterfall result; the default restart returns a promise.
* @throws when validation, an update listener, or the restarted plugin fails.
*/
update(config: any, noSave?: boolean): void | Promise<void>;
}
export {};
//# sourceMappingURL=fiber.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"fiber.d.ts","sourceRoot":"","sources":["../../src/fiber.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAiC,cAAc,EAAkD,MAAM,YAAY,CAAA;AAC1H,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAiB,OAAQ,SAAQ,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC;QACpD,kEAAkE;QAClE,KAAK,EAAE,KAAK,CAAA;KACb;CACF;AAID,+EAA+E;AAC/E,qBAAa,eAAgB,SAAQ,SAAS;IAC5C,IAAI,SAAoB;IAExB;;;;OAIG;gBACS,MAAM,EAAE,SAAS,gBAAgB,CAAC,KAAK,EAAE;CAStD;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAYjE;AAED,UAAU,eAAe,CAAC,CAAC,SAAS,SAAS,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAE,SAAQ,WAAW,CAAC,MAAM,CAAC,CAAC;IACjG,IAAI,CAAC,CAAA;CACN;AAED;;;;;GAKG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,CAAA;AAEzC;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,GAAG,IACtB,UAAU,CAAC,CAAC,CAAC,GACb,WAAW,CAAC,CAAC,CAAC,CAAA;AAElB,KAAK,UAAU,CAAC,CAAC,GAAG,GAAG,IACnB,UAAU,CAAC,CAAC,CAAC,GACb,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AAEvC,KAAK,WAAW,CAAC,CAAC,GAAG,GAAG,IACpB,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GACtB,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AAE5C,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,QAAQ,EAAE,UAAU,EAAE,CAAA;CACvB;AAsCD;;;;;;;GAOG;AACH,0BAAkB,UAAU;IAC1B,OAAO,IAAA;IACP,OAAO,IAAA;IACP,MAAM,IAAA;IACN,MAAM,IAAA;IACN,QAAQ,IAAA;IACR,SAAS,IAAA;CACV;AAED,2DAA2D;AAC3D,qBAAa,WAAY,SAAQ,KAAK;IAKjB,IAAI,EAAE,WAAW,CAAC,IAAI;IAJzC;;;OAGG;gBACgB,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM;CAG5D;AAED,qCAAqC;AACrC,yBAAiB,WAAW,CAAC;IAC3B,KAAY,IAAI,GAAG,MAAM,OAAO,IAAI,CAAA;IAE7B,MAAM,IAAI;;KAEP,CAAA;CACX;AAID;;;;;GAKG;AACH,qBAAa,KAAK;IAuCP,MAAM,EAAE,OAAO;IAEf,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI;IAzCvC,iFAAiF;IAC1E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,4EAA4E;IAC5E,SAAgB,GAAG,EAAE,OAAO,CAAA;IAC5B,2DAA2D;IACpD,MAAM,EAAE,GAAG,CAAA;IAClB,iEAAiE;IAC1D,OAAO,EAAE,GAAG,CAAA;IACnB,mEAAmE;IAC5D,KAAK,aAAqB;IACjC,gFAAgF;IAChF,SAAgB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,wFAAwF;IACjF,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;IACpC,yEAAyE;IAClE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;IAEzC,SAAgB,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAsB;IAC5E,SAAgB,YAAY,kCAAmC;IAG/D,SAAS,CAAC,OAAO,EAAE,OAAO,CAAA;IAE1B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,MAAM,CAAkC;IAEhD;;;;;;;;;OASG;gBAEM,MAAM,EAAE,OAAO,EACtB,MAAM,EAAE,GAAG,EACJ,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,EACrC,aAAa,EAAE,MAAM,MAAM,EAAE;IA4G/B,2FAA2F;IAC3F,IAAI,IAAI,WAOP;IAED;;;;;OAKG;IACH,YAAY;IAKZ,OAAO,CAAC,QAAQ;IA8ChB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,uEAAuE;IACvE,MAAM,CAAC,OAAO,EAAE,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAkJ7E;;;;OAIG;IACH,UAAU;IAMV,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,YAAY;IAgBpB,UAAU,CAAC,IAAI,EAAE,MAAM;IAcvB,QAAQ;IAcR,OAAO,CAAC,SAAS;IAgBjB,OAAO,CAAC,cAAc;YAKR,OAAO;YA6BP,OAAO;IAuBrB;;;;;OAKG;IACG,KAAK;IAQX;;;;;OAKG;IACG,OAAO;IAOb;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,UAAQ;CAkBnC"}
@@ -0,0 +1,15 @@
/** Core context type and root context implementation. */
export * from './context.ts';
/** Event bus, dispatch modes, and event augmentation types. */
export * from './events.ts';
/** Plugin fiber lifecycle, effects, and config validation helpers. */
export * from './fiber.ts';
/** Logger facade, logger service, message, exporter, and formatting types. */
export * from './logger.ts';
/** Plugin registry, dependency injection, and plugin entrypoint types. */
export * from './registry.ts';
/** Base service class and service lifecycle symbols. */
export * from './service.ts';
/** Shared internal helpers used by context, services, and plugin fibers. */
export * from './utils.ts';
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,cAAc,cAAc,CAAA;AAC5B,+DAA+D;AAC/D,cAAc,aAAa,CAAA;AAC3B,sEAAsE;AACtE,cAAc,YAAY,CAAA;AAC1B,8EAA8E;AAC9E,cAAc,aAAa,CAAA;AAC3B,0EAA0E;AAC1E,cAAc,eAAe,CAAA;AAC7B,wDAAwD;AACxD,cAAc,cAAc,CAAA;AAC5B,4EAA4E;AAC5E,cAAc,YAAY,CAAA"}
@@ -0,0 +1,105 @@
import { Context } from './context.ts';
import { Fiber } from './fiber.ts';
import { symbols } from './utils.ts';
declare module './context.ts' {
interface Intercept {
logger: LoggerService.Intercept;
}
}
/** Logger method name and severity category. */
export type LoggerType = 'error' | 'info' | 'warn' | 'debug';
/** Callable shape for one logger severity method. */
export type LoggerMethod = (format: any, ...param: any[]) => void;
/** Formatter used to resolve a printf-style placeholder. */
export type Formatter = (value: any, exporter: Exporter, message: Message) => any;
/** Numeric severity used when exporters decide whether to emit a message. */
export declare const enum LoggerLevel {
ERROR = 0,
INFO = 1,
WARN = 2,
DEBUG = 3
}
/** Structured log record delivered to exporters. */
export interface Message {
sn: number;
ts: number;
name: string;
type: LoggerType;
level: number;
args: any[];
fiber?: WeakRef<Fiber>;
}
/** Sink that receives structured log messages. */
export interface Exporter {
colors?: number | false;
maxLength?: number;
levels?: Record<string, number>;
formatters?: Record<string, Formatter>;
export(message: Message): void;
}
/** Built-in placeholder formatters used by `Logger.format()`. */
export declare const defaultFormatters: Record<string, Formatter>;
/** Options used when creating a named logger facade. */
export interface LoggerOptions {
/** The logger name shown with each message. */
name: string;
/** Message fields merged into every record from this logger. */
meta?: Partial<Message>;
/** Default maximum level exported when an exporter has no own threshold. */
level?: number;
}
/** Logger facade identity, inherited message metadata, and optional minimum level. */
export interface Logger extends LoggerOptions {
}
/** Logger facade severity methods. */
export interface Logger extends Record<LoggerType, LoggerMethod> {
}
/** Logger facade for one named subsystem. */
export declare class Logger {
private service;
static color(exporter: Exporter, code: number, value: any, decoration?: string): string;
static code(name: string, level?: false | number): number;
static format(exporter: Exporter, message: Message): string;
constructor(options: LoggerOptions, service: LoggerService);
private _method;
}
/** ANSI 16-color palette indexes used for logger name coloring. */
export declare const c16: number[];
/** ANSI 256-color palette indexes used for logger name coloring. */
export declare const c256: number[];
/** Logger service configuration merged from context intercepts. */
export declare namespace LoggerService {
interface Intercept {
name?: string;
level?: number;
}
}
/** Callable `ctx.logger` service shape. */
export interface LoggerService extends Record<LoggerType, LoggerMethod> {
(name?: string): Logger;
}
/**
* Built-in logging service.
*
* Call `ctx.logger()` to create a named logger, or call `ctx.logger.info()`
* directly to log with the current fiber-derived name.
*/
export declare class LoggerService {
bufferSize: number;
buffer: Message[];
ctx: Context;
_snMessage: number;
_snExporter: number;
exporters: Map<number, Exporter>;
constructor(ctx: Context);
/**
* 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: Exporter): import("./fiber.ts").Disposable<Promise<void>>;
private _resolveConfig;
[symbols.invoke](name?: string): Logger;
}
//# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,186 @@
import type { Dict } from '@deepseek-ai/cosmokit';
import { Context } from './context.ts';
import { Fiber } from './fiber.ts';
declare module './context.ts' {
interface Context {
/**
* Read a service from the store without the inject requirement.
*
* @param name — the service name.
* @param strict — when `true` (default), only return implementations
* whose providing fiber is currently active.
* @returns the service value, or `undefined` when not (yet) provided.
*/
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K];
/** Same as above for service names outside the typed `Context` surface. */
get(name: string, strict?: boolean): any;
/**
* Overwrite a provided service's value.
*
* Only the fiber that provided the service may set it; setting an
* unprovided name throws.
*
* @param name — the service name.
* @param value — the new service value.
*/
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void;
/** Same as above for service names outside the typed `Context` surface. */
set(name: string, value: any): void;
/**
* Register a service implementation owned by the current fiber.
*
* The service becomes visible to dependents in the same isolation scope
* once the fiber is active; it is unregistered (waking dependents) when
* the returned disposer runs or the fiber unloads. Throws if the name is
* already provided in this scope or declared as an accessor.
*
* @param name — the service name.
* @param value — the service value.
* @returns a disposer that unregisters the service.
*/
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void;
/** Same as above for service names outside the typed `Context` surface. */
provide(name: string, value?: any): () => void;
/**
* Define a computed context property backed by get/set hooks.
*
* The accessor is removed when the current fiber unloads. Throws if the
* name is already declared.
*
* @param name — the context property name.
* @param options — the `get` hook and optional `set` hook.
*/
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void;
/**
* Expose selected members of a service directly on `ctx`.
*
* Each mixed-in key becomes an accessor that forwards to the service
* (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`.
* Mixins are removed when the current fiber unloads.
*
* @param name — the context property holding the source service.
* @param mixins — keys to forward, or a source-key → ctx-key map.
*/
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void;
/** Same as above with a source object instead of a context property name. */
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void;
}
}
/** Context property definition known by the reflection service. */
export type Property = Property.Service | Property.Accessor;
/** Property definition variants understood by `ReflectService`. */
export declare namespace Property {
/** Service property backed by a provided implementation. */
interface Service {
/** Discriminator. */
type: 'service';
}
/** Computed context property backed by custom get/set hooks. */
interface Accessor {
/** Discriminator. */
type: 'accessor';
/** Compute the property value; `error` carries the caller stack for diagnostics. */
get: (this: Context, receiver: any, error: Error) => any;
/** Optional setter; return `false` to reject the write. */
set?: (this: Context, value: any, receiver: any, error: Error) => boolean;
}
}
/** Concrete service implementation record stored in the root reflect service. */
export interface Impl {
/** The service name. */
name: string;
/** The fiber that provided the service (owns its lifetime). */
fiber: Fiber;
/** The current service value. */
value?: any;
/** Optional availability predicate consulted before dependents may load. */
check?: () => boolean;
}
/**
* Reflection and service-resolution layer installed as `ctx.reflect`.
*
* This service powers the context proxy, service registration, accessors, and
* the mixins that expose core service methods directly on `ctx`.
*/
export declare class ReflectService {
ctx: Context;
/** Proxy traps implementing service resolution for every context object. */
static handler: ProxyHandler<Context>;
/** Service implementations, keyed by isolation label. */
store: Dict<Impl, symbol>;
/** Declared context properties (services and accessors), by name. */
props: Dict<Property>;
constructor(ctx: Context);
/**
* 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: string, strict?: boolean): any;
_getImpl(name: string, strict?: boolean): Impl | undefined;
/**
* 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: string, value: any, error?: Error): boolean;
/**
* 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: string, value?: any, check?: () => boolean): import("./fiber.ts").Disposable<Promise<void>>;
/**
* 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: string[], filter?: (ctx: Context, name: string) => boolean): Fiber[];
/**
* 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: string, options: Omit<Property.Accessor, 'type'>): import("./fiber.ts").Disposable<Promise<void>>;
/**
* 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: any, mixins: string[] | Dict<string>): import("./fiber.ts").Disposable<Promise<void>>;
/**
* 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<T>(value: T): T;
/**
* 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<T extends Function>(callback: T): T;
}
//# sourceMappingURL=reflect.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"reflect.d.ts","sourceRoot":"","sources":["../../src/reflect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,201 @@
import type { Dict } from '@deepseek-ai/cosmokit';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { Context } from './context.ts';
import { Fiber } from './fiber.ts';
import { DisposableList, symbols } from './utils.ts';
/**
* Service dependency declaration accepted by plugins and the `@Inject`
* decorator.
*
* Array form requests services without intercept config. Object form maps each
* service name to optional intercept config for the plugin context.
*/
export type Inject<M = Dict> = (keyof M)[] | {
[K in keyof M]?: M[K];
};
/** Context keys that correspond to services with typed intercept config. */
export type InjectKey = keyof {
[K in keyof Context & string as Context[K] extends {
[symbols.config]: any;
} ? K : never]: any;
};
/**
* Decorator for declaring service dependencies on classes or class methods.
*
* On classes it contributes to the plugin's static `inject` map. On methods it
* delays the method call until the declared services are available.
*/
/**
* @param name — the required service name.
* @param config — optional intercept config applied for that service.
* @returns the class or method decorator.
*/
export declare function Inject<K extends InjectKey>(name: K, config?: Context[K] extends {
[symbols.config]: infer T;
} ? T : never): (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) => void;
/** Utilities for normalizing plugin dependency declarations. */
export declare namespace Inject {
/**
* Convert array/object/class-inherited inject metadata into a plain map.
*
* @param inject — the declaration to normalize; `null`/`undefined` add nothing.
* @param result — the map to fill (service name → intercept config or `null`).
* @returns `result`.
*/
function resolve(inject: Inject | null | undefined, result?: Dict): Dict;
}
/** Supported plugin entrypoint shapes. */
export type Plugin<T = any> = Plugin.Function<T> | Plugin.Constructor<T> | Plugin.Object<T>;
/** Types associated with plugin entrypoints and runtime records. */
export declare namespace Plugin {
/** Shared metadata understood by the plugin registry and related tooling. */
interface Base<T = any> {
/** Display name used for fiber diagnostics and logger names. */
name?: string;
/** Standard-schema validator applied to config before the plugin starts. */
Config?: StandardSchemaV1<any, T>;
/** Services the plugin requires; it only loads while all are available. */
inject?: Inject;
/** Service name(s) the plugin provides (read by `Service` and by loaders). */
provide?: string | string[];
/** Service names whose intercept config the plugin declares it consumes. */
intercept?: Dict<boolean>;
}
interface Transform<S, T> {
/** Marks the transform object as a schema/config transform. */
schema?: true;
/** Convert user-facing config to runtime config. */
Config: (config: S) => T;
}
/** Function plugin called with `(ctx, config)`. */
interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any;
}
/** Class plugin constructed with `(ctx, config)`. */
interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any;
}
/** Object plugin with an `apply(ctx, config)` method. */
interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any;
}
/** Mutable registry record shared by all fibers of one plugin callback. */
interface Runtime {
/** Display name copied from the first registered plugin shape. */
name?: string;
/** Every live fiber of this plugin (one per `ctx.plugin()` call). */
fibers: DisposableList<Fiber>;
/** The executable entrypoint all fibers share (registry identity key). */
callback: globalThis.Function;
/** Standard-schema validator applied to each fiber's config. */
Config?: StandardSchemaV1;
}
}
type Spread<T> = undefined extends T ? [config?: T] : [config: T];
type GetPluginParameters<P> = P extends (ctx: Context, ...args: infer R) => any ? R : P extends new (ctx: Context, ...args: infer R) => any ? R : P extends {
apply(ctx: Context, ...args: infer R): any;
} ? R : never;
type GetPluginConfig<P> = P extends Plugin.Transform<infer S, any> ? S : GetPluginParameters<P>[0];
declare module './context.ts' {
interface Context {
/**
* Run a callback once the requested services are available.
*
* Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback
* is unloaded and re-run whenever a required service changes.
*
* @param deps — 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(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>;
/**
* Load a plugin in the current context.
*
* @param plugin — a function, class, or `{ apply }` object plugin.
* @param args — the plugin config, validated against its `Config` schema.
* @returns the fiber; awaiting it settles once loading finished
* (rejecting on config or startup errors).
*/
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>;
}
}
/**
* Plugin registry installed as `ctx.registry` and mixed into every context.
*
* It normalizes plugin shapes, tracks plugin runtimes, starts fibers, and
* exposes map-like inspection over active plugin callbacks.
*/
export declare class RegistryService {
ctx: Context;
private _counter;
private _internal;
constructor(ctx: Context);
/** Allocate the next fiber uid (increments on every read). */
get counter(): number;
/** Number of registered plugin runtimes. */
get size(): number;
/**
* 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: Plugin): Function | undefined;
/**
* 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: Plugin): Plugin.Runtime | undefined;
/**
* 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: Plugin): boolean;
/**
* 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: Plugin): Plugin.Runtime | undefined;
/** Iterate the registered plugin callbacks. */
keys(): MapIterator<Function>;
/** Iterate the registered plugin runtimes. */
values(): MapIterator<Plugin.Runtime>;
/** Iterate `[callback, runtime]` pairs. */
entries(): MapIterator<[Function, Plugin.Runtime]>;
/**
* Visit every registered runtime.
*
* @param callback — receives each runtime and its identifying callback.
*/
forEach(callback: (value: Plugin.Runtime, key: Function) => void): void;
/**
* 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: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>;
/**
* 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: Plugin, config?: any, getOuterStack?: () => string[]): Fiber & PromiseLike<Fiber>;
}
export {};
//# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAmB,cAAc,EAAE,OAAO,EAAa,MAAM,YAAY,CAAA;AAMhF;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;CAAE,CAAA;AAEtE,4EAA4E;AAC5E,MAAM,MAAM,SAAS,GAAG,MAAM;KAC3B,CAAC,IAAI,MAAM,OAAO,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS;QAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK,GAAG,GAAG;CAC/F,CAAA;AAED;;;;;GAKG;AACH;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;IAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,IACvG,OAAO,GAAG,EAAE,WAAW,qBAAqB,CAAC,GAAG,CAAC,GAAG,2BAA2B,CAAC,GAAG,CAAC,UAsBtG;AAED,gEAAgE;AAChE,yBAAiB,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,SAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,GAAE,IAA0B,QAiB5F;CACF;AAED,0CAA0C;AAC1C,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,GAAG,IACtB,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAClB,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,GACrB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;AAEpB,oEAAoE;AACpE,yBAAiB,MAAM,CAAC;IACtB,6EAA6E;IAC7E,UAAiB,IAAI,CAAC,CAAC,GAAG,GAAG;QAC3B,gEAAgE;QAChE,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,4EAA4E;QAC5E,MAAM,CAAC,EAAE,gBAAgB,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACjC,2EAA2E;QAC3E,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,8EAA8E;QAC9E,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;QAC3B,4EAA4E;QAC5E,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;KAC1B;IAED,UAAiB,SAAS,CAAC,CAAC,EAAE,CAAC;QAC7B,+DAA+D;QAC/D,MAAM,CAAC,EAAE,IAAI,CAAA;QACb,oDAAoD;QACpD,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAA;KACzB;IAED,mDAAmD;IACnD,UAAiB,QAAQ,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,IAAI,CAAC,CAAC,CAAC;QAChD,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,GAAG,CAAA;KAC/B;IAED,qDAAqD;IACrD,UAAiB,WAAW,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,IAAI,CAAC,CAAC,CAAC;QACnD,KAAK,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,GAAG,CAAA;KACnC;IAED,yDAAyD;IACzD,UAAiB,MAAM,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,IAAI,CAAC,CAAC,CAAC;QAC9C,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,GAAG,CAAA;KACpC;IAED,2EAA2E;IAC3E,UAAiB,OAAO;QACtB,kEAAkE;QAClE,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,qEAAqE;QACrE,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;QAC7B,0EAA0E;QAC1E,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAA;QAC7B,gEAAgE;QAChE,MAAM,CAAC,EAAE,gBAAgB,CAAA;KAC1B;CACF;AAED,KAAK,MAAM,CAAC,CAAC,IAAI,SAAS,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;AAEjE,KAAK,mBAAmB,CAAC,CAAC,IACtB,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,GAAG,GACjD,CAAC,GACD,CAAC,SAAS,KAAK,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,GAAG,GACrD,CAAC,GACD,CAAC,SAAS;IAAE,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,GAAG,GAAG,CAAA;CAAE,GACxD,CAAC,GACD,KAAK,CAAA;AAET,KAAK,eAAe,CAAC,CAAC,IAClB,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,GACxC,CAAC,GACD,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAE7B,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAiB,OAAO;QACtB;;;;;;;;;WASG;QACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;QACjF;;;;;;;WAOG;QACH,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;KACrG;CACF;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAIP,GAAG,EAAE,OAAO;IAH/B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,SAAS,CAAsC;gBAEpC,GAAG,EAAE,OAAO;IAO/B,8DAA8D;IAC9D,IAAI,OAAO,WAEV;IAED,4CAA4C;IAC5C,IAAI,IAAI,WAEP;IAED;;;;;OAKG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAQ7C;;;;;OAKG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM;IAKlB;;;;;OAKG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM;IAKlB;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM;IAWrB,+CAA+C;IAC/C,IAAI;IAIJ,8CAA8C;IAC9C,MAAM;IAIN,2CAA2C;IAC3C,OAAO;IAIP;;;;OAIG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,IAAI;IAIhE;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;IAItD;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,aAAa,iBAAoB;CAqBvE"}
@@ -0,0 +1,55 @@
import { Context } from './context.ts';
import { symbols } from './utils.ts';
/**
* Base class for services that expose a named API on `ctx`.
*
* Subclasses call `super(ctx, name)` from their constructor. The service is
* registered immediately and is automatically removed with the owning fiber.
*/
export declare abstract class Service<out T = never> {
protected ctx: Context;
/** Symbol key of an instance method run after construction (class plugins). */
static readonly init: unique symbol;
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
static readonly check: unique symbol;
/** Symbol key of the phantom intercept-config type parameter. */
static readonly config: unique symbol;
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
static readonly invoke: unique symbol;
/** Symbol key of the helper deriving an extended service instance. */
static readonly extend: unique symbol;
/** Symbol key of the tracker metadata used for context tracing. */
static readonly tracker: unique symbol;
/** Symbol key of the intercept-config resolution helper below. */
static readonly resolveConfig: unique symbol;
[symbols.config]: T;
/** The service name this instance is registered under. */
name: string;
/**
* 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: Context, name: string);
protected [symbols.filter](ctx: Context): boolean;
protected [symbols.extend](props?: any): any;
/**
* 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?: T, head?: T): T;
static [Symbol.hasInstance](instance: any): boolean;
}
//# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAiC,OAAO,EAAgB,MAAM,YAAY,CAAA;AAEjF;;;;;GAKG;AACH,8BAAsB,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK;IA+B7B,SAAS,CAAC,GAAG,EAAE,OAAO;IA9BlC,+EAA+E;IAC/E,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,MAAM,CAAe;IAClD,0EAA0E;IAC1E,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,MAAM,CAAgB;IACpD,iEAAiE;IACjE,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,MAAM,CAAiB;IACtD,mFAAmF;IACnF,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,MAAM,CAAiB;IACtD,sEAAsE;IACtE,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,MAAM,CAAiB;IACtD,mEAAmE;IACnE,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,CAAkB;IACxD,kEAAkE;IAClE,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,OAAO,MAAM,CAAwB;IAE5D,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAE3B,0DAA0D;IACnD,IAAI,EAAG,MAAM,CAAA;IAEpB;;;;;;;;;OASG;gBACmB,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM;IAmBhD,SAAS,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO;IAIvC,SAAS,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG;IAUtC;;;;;;;;;;OAUG;IACH,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC;IAkB9C,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,GAAG;CAW1C"}
@@ -0,0 +1,62 @@
import type { Context, Service } from './index.ts';
/** Ordered collection of disposable values with O(1) deletion by value. */
export declare class DisposableList<T extends WeakKey> {
private sn;
private map;
private weak;
get length(): number;
push(value: T): () => boolean;
delete(value: T): boolean;
clear(): T[];
[Symbol.iterator](): MapIterator<T>;
}
/** Metadata used by traceable proxies to rebind `ctx` and associated services. */
export interface Tracker {
associate?: string;
property?: string;
noShadow?: boolean;
}
/** Shared symbols used to avoid public property-name collisions. */
export declare const symbols: {
shadow: symbol;
receiver: symbol;
original: symbol;
metadata: symbol;
initHooks: symbol;
checkProto: symbol;
effect: typeof Context.effect;
filter: typeof Context.filter;
isolate: typeof Context.isolate;
intercept: typeof Context.intercept;
init: typeof Service.init;
check: typeof Service.check;
config: typeof Service.config;
invoke: typeof Service.invoke;
extend: typeof Service.extend;
tracker: typeof Service.tracker;
resolveConfig: typeof Service.resolveConfig;
};
/** Return true when a plugin callback should be constructed with `new`. */
export declare function isConstructor(func: any): func is new (...args: any) => any;
/** Merge two prototype chains while preserving descriptors from `proto1`. */
export declare function joinPrototype(proto1: {}, proto2: {}): any;
/** Return true for non-null objects and functions. */
export declare function isObject(value: any): value is {};
/** Find a property descriptor by walking an object's prototype chain. */
export declare function getPropertyDescriptor(target: any, prop: string | symbol): TypedPropertyDescriptor<any> | undefined;
/** Wrap services/functions so method calls see the caller's active context. */
export declare function getTraceable<T>(ctx: Context, value: T): T;
/** Return a proxy that overlays readonly or writable properties onto a target. */
export declare function withProps(target: any, props?: {}): any;
/** Create a callable service object that dispatches through `symbols.invoke`. */
export declare function createCallable(name: string, proto: {}, tracker: Tracker): any;
interface StackInfo {
offset: number;
error: Error;
}
/** Run a callback and splice outer call-site frames into thrown async errors. */
export declare function composeError<T>(callback: (info: StackInfo) => T, getOuterStack?: () => string[]): T;
/** Capture a lazy stack-frame supplier for later error composition. */
export declare function buildOuterStack(offset?: number): () => string[];
export {};
//# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAElD,2EAA2E;AAC3E,qBAAa,cAAc,CAAC,CAAC,SAAS,OAAO;IAC3C,OAAO,CAAC,EAAE,CAAI;IACd,OAAO,CAAC,GAAG,CAAuB;IAClC,OAAO,CAAC,IAAI,CAA2B;IAEvC,IAAI,MAAM,WAET;IAED,IAAI,CAAC,KAAK,EAAE,CAAC;IAOb,MAAM,CAAC,KAAK,EAAE,CAAC;IAMf,KAAK;IAML,CAAC,MAAM,CAAC,QAAQ,CAAC;CAOlB;AAED,kFAAkF;AAClF,MAAM,WAAW,OAAO;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,oEAAoE;AACpE,eAAO,MAAM,OAAO;;;;;;;YAUqB,OAAO,OAAO,CAAC,MAAM;YACrB,OAAO,OAAO,CAAC,MAAM;aACnB,OAAO,OAAO,CAAC,OAAO;eAClB,OAAO,OAAO,CAAC,SAAS;UAGlC,OAAO,OAAO,CAAC,IAAI;WACjB,OAAO,OAAO,CAAC,KAAK;YAClB,OAAO,OAAO,CAAC,MAAM;YACrB,OAAO,OAAO,CAAC,MAAM;YACrB,OAAO,OAAO,CAAC,MAAM;aACnB,OAAO,OAAO,CAAC,OAAO;mBACV,OAAO,OAAO,CAAC,aAAa;CAClF,CAAA;AAKD,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,KAAK,GAAG,CAU1E;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,OAOnD;AAED,sDAAsD;AACtD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,EAAE,CAEhD;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,4CAOvE;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAQzD;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,OAYhD;AAqFD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,OAOvE;AAED,UAAU,SAAS;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,KAAK,CAAA;CACb;AA6BD,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,CAAC,EAAE,aAAa,iBAAoB,GAAG,CAAC,CAatG;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,MAAM,SAAI,kBAGzC"}
@@ -0,0 +1,55 @@
#!/bin/sh
# Resolve $0 through symlinks so basedir is the shim's real directory.
# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim.
link="$0"
hops=0
while [ -L "$link" ] && [ "$hops" -lt 40 ]; do
hops=$((hops+1))
target=$(readlink "$link")
case "$target" in
/*) link="$target" ;;
*) link="$(dirname "$link")/$target" ;;
esac
done
basedir=$(dirname "$(echo "$link" | sed -e 's,\\,/,g')")
basedir_win="$basedir"
exe=""
msys=""
case `uname -a` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir_win=`cygpath -w "$basedir"`
fi
exe=".exe"
msys="true"
;;
*WSL2*)
if command -v wslpath > /dev/null 2>&1; then
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
basedir_win="$basedir"
else
exe=".exe"
fi
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules:/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
exec "$basedir/node.exe" "$basedir_win/../../bin.js" "$@"
elif [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../bin.js" "$@"
elif command -v node >/dev/null 2>&1; then
exec node "$basedir/../../bin.js" "$@"
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
exec node.exe "$basedir_win/../../bin.js" "$@"
else
exec node "$basedir/../../bin.js" "$@"
fi
# cmd-shim-target=/home/salmonstill/projects/server/dsh-local-403-fix/node_modules/.pnpm/@deepseek-ai+cordis@4.0.1_@deepseek-ai+cordis-plugin-include@1.0.6_@deepseek-ai+cordis-plugin-loader@1.0.2/node_modules/@deepseek-ai/cordis/bin.js
@@ -0,0 +1,53 @@
{
"name": "@deepseek-ai/cordis",
"description": "Meta-Framework for Modern JavaScript Applications",
"version": "4.0.1",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "vendor/cordis"
},
"sideEffects": false,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"bin.js",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
"@deepseek-ai/cordis-plugin-include": "^1.0.6"
},
"peerDependenciesMeta": {
"@deepseek-ai/cordis-plugin-include": {
"optional": true
},
"@deepseek-ai/cordis-plugin-loader": {
"optional": true
}
},
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@deepseek-ai/cosmokit": "^1.8.2"
},
"bin": {
"cordis": "bin.js"
}
}
@@ -0,0 +1,146 @@
import type { Dict } from '@deepseek-ai/cosmokit'
import { EventsService } from './events.ts'
import { LoggerService } from './logger.ts'
import { ReflectService } from './reflect.ts'
import { RegistryService, type InjectKey } from './registry.ts'
import { getTraceable, symbols } from './utils.ts'
import { Fiber } from './fiber.ts'
/**
* Public shape of a Cordis context.
*
* The concrete `Context` class is proxied at runtime, so this interface is
* augmented by core services and plugins to describe the properties that may
* be read from `ctx`.
*/
export interface Context {
/** Isolation map: service name → scope label. Lookups for a name resolve within its label. */
[symbols.isolate]: Dict<symbol>
/** Intercept map: service name → config merged into that service's per-plugin config. */
[symbols.intercept]: Dict
/** The root context of the application (every child context shares it). @experimental */
root: this
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
baseUrl?: string
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
events: EventsService
/** The logging service. Call `ctx.logger(name)` for a named logger. */
logger: LoggerService
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
reflect: ReflectService
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
registry: RegistryService
}
/**
* Root and child dependency containers for Cordis plugins.
*
* A context is a proxy: normal property reads go through the service resolver,
* while `extend()`, `isolate()`, and `intercept()` create scoped child
* contexts without mutating their parent.
*/
export class Context {
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
static readonly effect: unique symbol = symbols.effect
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
static readonly filter: unique symbol = symbols.filter
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
static readonly isolate: unique symbol = symbols.isolate
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
static readonly intercept: unique symbol = 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: any): value is Context {
return !!value?.[Context.is as any]
}
static {
Context.is[Symbol.toPrimitive] = () => Symbol.for('cordis.is')
Context.prototype[Context.is as any] = true
}
/** Create the root context and install the built-in services. */
constructor() {
this[symbols.isolate] = Object.create(null)
this[symbols.intercept] = Object.create(null)
const self = new Proxy<this>(this, ReflectService.handler)
this.root = self
this.baseUrl = undefined
this.fiber = new Fiber(self, {}, 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
}
[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 = {}): this {
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: string, label?: symbol) {
const shadow = Object.create(this[symbols.isolate])
shadow[name] = label ?? Symbol(name)
return this.extend({ [symbols.isolate]: shadow })
}
/**
* Add service-specific intercept config for plugins started below this
* context.
*
* Plugins loaded under the returned context see `config` merged into the
* service's resolved config (ancestor entries first; see
* `Service[symbols.resolveConfig]`). The parent context is not affected.
*
* @param name — the service name whose config to intercept.
* @param config — the intercept config to merge for that service.
* @returns a child context carrying the additional intercept entry.
*/
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
intercept(name: string, config: any): this
intercept(name: string, config: any) {
const intercept = Object.create(this[symbols.intercept])
intercept[name] = config
return this.extend({ [symbols.intercept]: intercept })
}
}
@@ -0,0 +1,352 @@
import { defineProperty } from '@deepseek-ai/cosmokit'
import type { Promisify } from '@deepseek-ai/cosmokit'
import { Context } from './context.ts'
import { Fiber, FiberState } from './fiber.ts'
import { DisposableList, symbols } from './utils.ts'
/**
* Return whether an event result should stop a bail-style dispatch.
*
* @param value — a listener's return value.
* @returns `true` unless `value` is `null`, `false`, or `undefined`.
*/
export function isBailed(value: any) {
return value !== null && value !== false && value !== undefined
}
/** Extract the parameter tuple from a function type. */
export type Parameters<F> = F extends (...args: infer P) => any ? P : never
/** Extract the return type from a function type. */
export type ReturnType<F> = F extends (...args: any) => infer R ? R : never
/** Extract the explicit `this` type from a function type. */
export type ThisType<F> = F extends (this: infer T, ...args: any) => any ? T : never
/**
* Event dispatch strategy used by the event service.
*
* `emit` runs synchronous listeners without awaiting them, `parallel` awaits
* all listeners together, `serial` awaits them in order until one bails,
* `bail` stops on the first synchronous bail value, and `waterfall` composes
* listeners around a final `next` callback.
*/
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
declare module './context.ts' {
export interface Context {
/* eslint-disable max-len */
/**
* Dispatch an event, running all listeners concurrently.
*
* @param name — the event name.
* @param args — arguments passed to every listener.
* @returns a promise resolving once every listener has settled.
*/
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
/**
* Dispatch an event synchronously, ignoring listener return values.
*
* @param name — the event name.
* @param args — arguments passed to every listener.
*/
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
/**
* Dispatch an event, awaiting listeners in order until one bails.
*
* @param name — the event name.
* @param args — arguments passed to each listener.
* @returns the first bail value (non-null, non-false, non-undefined), if any.
*/
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
/**
* Dispatch an event, calling listeners in order until one bails.
*
* @param name — the event name.
* @param args — arguments passed to each listener.
* @returns the first bail value (non-null, non-false, non-undefined), if any.
*/
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
/**
* Dispatch an event whose last argument is a `next` continuation.
*
* Each listener wraps the rest of the chain: calling `next()` invokes the
* next listener (finally the built-in behavior); not calling it vetoes.
*
* @param name — the event name.
* @param args — listener arguments; the final one is the innermost `next`.
* @returns the outermost listener's return value.
*/
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
/** Same as above, with an explicit `this` for listeners (also used for filtering). */
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
/**
* Register an event listener owned by the current fiber.
*
* @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<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
/**
* Same as `on()`, but the listener disposes itself after its 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<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
/* eslint-enable max-len */
}
}
/** Options accepted by `ctx.on()` and `ctx.once()`. */
export interface EventOptions {
/** Add the listener before existing listeners for the same event. */
prepend?: boolean
/** Receive the event regardless of context filter checks. */
global?: boolean
}
/** Registered listener record stored by the event service. */
export interface Hook extends EventOptions {
ctx: Context
callback: (...args: any[]) => any
}
/**
* Event bus installed as `ctx.events` and mixed into every context.
*
* The service supports concurrent, synchronous, serial, bail, and waterfall
* dispatch and automatically disposes listeners with their owning fiber.
*/
export class EventsService {
_hooks: Record<keyof any, Hook[]> = {}
constructor(private ctx: Context) {
defineProperty(this, symbols.tracker, {
property: 'ctx',
noShadow: true,
})
this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
if (name === 'internal/update' && !options.global) {
const hooks = this.fiber._hooks['internal/update'] ??= new DisposableList()
const method = options.prepend ? 'unshift' : 'push'
return hooks[method](listener)
}
})
this.on('internal/update', function (config, noSave, next) {
const cbs = [...this._hooks['internal/update'] || []]
const _next = () => {
const cb = cbs.shift() ?? next
return cb.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: string, args: any[]) {
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
const name: string = 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: any[]) {
const results = await Promise.allSettled(this.dispatch('emit', args).map(async cb => cb(...args)))
const errors = results.filter((result): result is PromiseRejectedResult => 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: any[]) {
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: any[]) {
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: any[]) {
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: any[]) {
const cbs = this.dispatch('waterfall', args)
const inner = args.pop()
const next = () => {
const cb = cbs.shift() ?? inner
return cb(...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: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
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: Hook[], callback: any) {
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: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
if (typeof options !== 'object') {
options = { prepend: options }
}
// handle special events
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: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
const dispose = this.on(name, function (...args: any[]) {
dispose()
return listener.apply(this, args)
}, options)
return dispose
}
}
/**
* Built-in framework events used by core services and extension points.
*
* Plugin and status events track fiber lifecycle, service events observe
* dependency registration, update/get/set/listener events allow core services
* to intercept runtime operations, and `internal/dispatch` exposes event-bus
* diagnostics before public events are delivered.
*/
export interface Events {
/** A plugin fiber was created or its uid was cleared on disposal. */
'internal/plugin'(fiber: Fiber): void
/** A fiber changed lifecycle state; receives the fiber and its previous state. */
'internal/status'(fiber: Fiber, oldValue: FiberState): void
/**
* Resolve raw plugin config after the fiber's injections become active.
* @param config - the raw config for this activation.
* @mode waterfall
*/
'internal/config'(this: Fiber, config: any, next: () => any): any
/** Interception hook for a service binding (no core producer). */
'internal/service'(this: Context, name: string, value: any): void
/** Waterfall: a fiber config update is being applied; skip `next()` to veto. */
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void | Promise<void>): void | Promise<void>
/** Waterfall: a service is being read through the context proxy. */
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any
/** Waterfall: a service is being written through the context proxy. */
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean
/** Bail: a listener is being registered; a non-null result replaces registration. */
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void
/** An event is being dispatched to listeners (fired for non-internal events only). */
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void
}
@@ -0,0 +1,754 @@
import { defineProperty, isNullable } from '@deepseek-ai/cosmokit'
import type { Awaitable, Dict } from '@deepseek-ai/cosmokit'
import { Context } from './context.ts'
import type { Plugin } from './registry.ts'
import { buildOuterStack, composeError, DisposableList, getTraceable, isConstructor, isObject, symbols } from './utils.ts'
import type { Impl } from './reflect.ts'
import type { StandardSchemaV1 } from '@standard-schema/spec'
declare module './context.ts' {
export interface Context extends Pick<Fiber, 'effect'> {
/** The fiber (plugin runtime instance) that owns this context. */
fiber: Fiber
}
}
const kValidationError = Symbol.for('ValidationError')
/** Error raised when plugin configuration fails standard-schema validation. */
export class ValidationError extends TypeError {
name = 'ValidationError'
/**
* Build the aggregated message from schema issues.
*
* @param issues — the standard-schema issues, one message line each.
*/
constructor(issues: readonly StandardSchemaV1.Issue[]) {
super(`invalid config:\n` + 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,
})
/**
* Validate and normalize config for a plugin runtime before it starts.
*
* @param runtime — the plugin runtime whose `Config` schema to apply.
* @param config — the raw user config.
* @returns the validated config, or `config` unchanged if the runtime has no schema.
* @throws {ValidationError} when validation reports issues.
*/
export function resolveConfig(runtime: Plugin.Runtime, config: any) {
if (!runtime.Config) return config
// TODO: async validation
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
}
}
interface AsyncDisposable<T extends Awaitable<void> = Awaitable<void>> extends PromiseLike<() => T> {
(): T
}
/**
* Function returned by an effect to release resources during disposal.
*
* Disposers run in reverse registration order when the owning fiber unloads;
* they may be async, in which case unloading awaits them.
*/
export type Disposable<T = any> = () => T
/**
* Effect body result accepted by `ctx.effect()` and plugin startup.
*
* Either a single disposer, a promise of one, or a (possibly async) iterable
* yielding several — generator effects register each yielded disposer as it
* is produced.
*/
export type Effect<T = any> =
| SyncEffect<T>
| AsyncEffect<T>
type SyncEffect<T = any> =
| Disposable<T>
| Iterable<Disposable<T>, void, void>
type AsyncEffect<T = any> =
| Promise<Disposable<T>>
| AsyncIterable<Disposable<T>, void, void>
/** Tree node used to expose nested effect labels for diagnostics. */
export interface EffectMeta {
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
label: string
/** Metadata of nested effects registered while this effect ran. */
children: EffectMeta[]
}
interface EffectRunner<T> {
epoch: T
execute: () => any
collect: (dispose: Disposable) => void
getOuterStack: () => string[]
}
// Public effect disposers remain single-shot, but structural owners and outer
// effects must still be able to join a cleanup that another caller started.
const effectInertia = new WeakMap<Disposable, () => void | Promise<void>>()
function runDisposable(dispose: Disposable) {
const result = dispose()
return effectInertia.get(dispose)?.() ?? result
}
/** Notify plugin teardown without allowing one observer to break ownership cleanup. */
function emitPluginDisposed(context: Context, fiber: Fiber) {
const args: any[] = ['internal/plugin', fiber]
let callbacks: Function[]
try {
callbacks = context.events.dispatch('emit', args)
} catch (error) {
context.logger.error(error)
return
}
for (const callback of callbacks) {
try {
const returned = callback(...args)
void Promise.resolve(returned).catch(error => context.logger.error(error))
} catch (error) {
context.logger.error(error)
}
}
}
/**
* Lifecycle state for one plugin fiber.
*
* `PENDING` — waiting for required services; `LOADING` — the plugin callback
* is running; `ACTIVE` — loaded and providing; `FAILED` — the callback or its
* config threw; `UNLOADING` — disposers are running; `DISPOSED` — the fiber
* was removed and cannot restart.
*/
export const enum FiberState {
PENDING,
LOADING,
ACTIVE,
FAILED,
DISPOSED,
UNLOADING,
}
/** Framework error with a stable machine-readable code. */
export class CordisError extends Error {
/**
* @param code — the stable error code; also the default message.
* @param message — optional human-readable override.
*/
constructor(public code: CordisError.Code, message?: string) {
super(message ?? CordisError.Code[code])
}
}
/** Cordis error code definitions. */
export namespace CordisError {
export type Code = keyof typeof Code
export const Code = {
INACTIVE_EFFECT: 'cannot create effect on inactive context',
} as const
}
const INACTIVE = '__INACTIVE__'
/**
* Runtime instance of one plugin application.
*
* A fiber tracks dependency state, validated config, lifecycle effects, and
* cleanup for the plugin context returned by `ctx.plugin()`.
*/
export class Fiber {
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
public uid: number | null
/** The context this fiber's plugin runs in (extends the parent context). */
public readonly ctx: Context
/** The validated plugin config (updated by `update()`). */
public config: any
/** The raw plugin config, re-resolved before each activation. */
public _config: any
/** Current lifecycle state; transitions emit `internal/status`. */
public state = FiberState.PENDING
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
public readonly dispose: () => Promise<void>
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
public store: Dict<Impl> | undefined
/** The in-flight load/unload transition, if one is currently running. */
public inertia: Promise<void> | undefined
public readonly _hooks: Dict<DisposableList<Function>> = Object.create(null)
public readonly _disposables = new DisposableList<Disposable>()
// Same as `this.ctx`, but with a more specific type.
protected context: Context
private _error: any
private _runner: EffectRunner<string>
private _store: Dict<Impl> = 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(
public parent: Context,
config: any,
public inject: Dict<any>,
public runtime: Plugin.Runtime | null,
getOuterStack: () => string[],
) {
this._config = config
const collect = (dispose: Disposable) => {
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, config] of injectEntries) {
if (isNullable(config)) continue
this.ctx[Context.intercept][name] = config
}
}
this._runner = {
epoch: INACTIVE,
getOuterStack,
execute: function () {
if (isConstructor(runtime.callback)) {
// eslint-disable-next-line new-cap
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)
// A PENDING fiber can already own effects registered by an
// internal/plugin observer. Its epoch is still INACTIVE, so
// _setEpoch() has no transition to drive; explicitly unload that
// pre-activation work before reporting disposal complete.
if (!this.inertia) {
this._updateState(() => {
this.inertia = this._unload()
return FiberState.UNLOADING
})
}
// `this.inertia` itself should never reject — both `_reload` and
// `_unload` swallow their own work errors via `ctx.logger.error`.
// If it *does* reject, the only remaining cause is the logger
// itself failing, which we can't recover from in this exact spot
// (calling the logger again is what just failed). Let the
// rejection propagate; process-level crash is the honest outcome.
while (this.inertia) {
await this.inertia
}
}
}, 'ctx.plugin()')
try {
// Publish only after the parent owns a fully assigned disposer. A
// synchronous observer may dispose either this fiber or its parent.
this.context.emit('internal/plugin', this)
} catch (error) {
// Publication failed synchronously. The disposer removes the child
// from both the parent and runtime before control escapes.
void Promise.resolve(this.dispose()).catch(reason => this.ctx.logger.error(reason))
throw error
}
// Keep the initial notification's historical PENDING view. The loader
// may also extend `inject` in that notification, so resolve dependencies
// only after publication. A reentrant parent unload makes the child
// disposer responsible for draining any PENDING effects instead.
if (this.uid !== null && parent.fiber.state !== FiberState.UNLOADING) {
for (const name of Object.keys(this.inject)) {
this._checkImpl(name)
}
this._refresh()
}
} else {
this.uid = 0
this.ctx = this.context = parent
this.state = FiberState.ACTIVE
this.store = 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: 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')
}
private _execute<T>(runner: EffectRunner<T>) {
const oldEpoch = runner.epoch
return composeError((info) => {
const safeCollect = (dispose: void | Disposable) => {
if (typeof dispose === 'function') {
runner.collect(dispose)
} else if (!isNullable(dispose)) {
throw new TypeError('Invalid effect')
}
}
const effect: Effect = runner.execute.call(this)
if (typeof effect === 'function') {
return runner.collect(effect)
} else if (isNullable(effect)) {
// return
} 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 = 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 () => {
// force async stack trace
await Promise.resolve()
info.error = 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)
}
/**
* Register a cleanup-aware effect on this fiber.
*
* `execute` runs immediately; the disposers it produces are collected and
* run (in reverse order) either when the returned disposer is called or
* when the fiber unloads, whichever comes first. Calling the disposer twice
* is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
* already disposed, and `TypeError` if `execute` returns an invalid shape.
*
* @param execute — the effect body; see {@link Effect} for accepted shapes.
* @param label — effect label shown in `getEffects()` diagnostics.
* @returns a disposer that tears the effect down and settles once done.
*/
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
/** Same as above for async effects; the disposer is also awaitable. */
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
effect(execute: () => Effect, label = 'anonymous'): any {
this.assertActive()
if (this.state === FiberState.UNLOADING) {
throw new CordisError('INACTIVE_EFFECT')
}
const disposables: Disposable[] = []
let disposing = false
let disposalTask: void | Promise<void>
const dispose = () => {
if (disposing) return disposalTask
disposing = true
let task!: void | Promise<void>
for (const disposable of disposables.splice(0).reverse()) {
if (task) {
task = task.then(() => runDisposable(disposable))
} else {
const result = runDisposable(disposable)
if (isObject(result) && 'then' in result) {
task = result as any
}
}
}
return disposalTask = task
}
const meta: EffectMeta = { label, children: [] }
const runner: EffectRunner<boolean> = {
execute,
epoch: true,
collect: (dispose) => {
disposables.push(dispose)
this._disposables.delete(dispose)
if (dispose[symbols.effect]) {
meta.children.push(dispose[symbols.effect])
}
},
getOuterStack: buildOuterStack(),
}
let task: void | Promise<void>
let executing = true
let resolveSetup: (() => void) | undefined
let rejectSetup: ((reason: unknown) => void) | undefined
let setupBarrier: Promise<void> | undefined
let setupFailed = false
let inFlight: void | Promise<void>
let removeWrapper = () => false
const waitForSetup = () => {
setupBarrier ??= new Promise<void>((resolve, reject) => {
resolveSetup = resolve
rejectSetup = reject
})
return setupBarrier
}
const disposeAfter = (setup: PromiseLike<void>) => {
return Promise.resolve(setup).then(
() => dispose(),
async (reason) => {
await dispose()
throw reason
},
)
}
const finalizeDisposal = (callback: () => void | Promise<void>) => {
let result: void | Promise<void>
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 = undefined
})
return inFlight = pending
}
removeWrapper()
return result
}
const wrapper = defineProperty(() => {
// A synchronous setup failure can race an owner unload that already
// captured this wrapper but has not invoked it yet. The failed effect is
// never returned publicly, so let that internal caller await rollback.
if (!runner.epoch) return setupFailed ? inFlight : undefined
runner.epoch = false
return finalizeDisposal(() => {
if (executing) return disposeAfter(waitForSetup())
return task ? disposeAfter(task) : dispose()
})
}, symbols.effect, meta) as AsyncDisposable
effectInertia.set(wrapper, () => inFlight)
// Make the effect visible to a reentrant owner unload before execute()
// runs any plugin code. Async teardown stays owner-visible until it
// settles, allowing an outer effect to join cleanup another caller began.
removeWrapper = this._disposables.push(wrapper)
try {
task = this._execute(runner)
} catch (reason) {
executing = false
setupFailed = true
runner.epoch = false
let cleanup: void | Promise<void>
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)
}
// prevent unhandled rejection — both from `task` itself and from the
// disposer chain if it fails to settle cleanly.
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<EffectMeta>(dispose => dispose[symbols.effect])
.filter(Boolean)
}
private _getState() {
if (this.uid === null) return FiberState.DISPOSED
if (this._error) return FiberState.FAILED
if (this._runner.epoch !== INACTIVE) return FiberState.ACTIVE
return FiberState.PENDING
}
private _updateState(callback: () => void | FiberState) {
const oldState = this.state
this.state = callback() ?? this._getState()
if (oldState === this.state) return
// FIXME internal/fiber-info
this.context.emit('internal/status', this, oldState)
// only notify changes between ACTIVE and NON-ACTIVE states
if (oldState !== FiberState.ACTIVE && this.state !== FiberState.ACTIVE) return
for (const key of Reflect.ownKeys(this.ctx.reflect.store)) {
const impl = this.ctx.reflect.store[key as symbol]
if (impl.fiber !== this) continue
this.ctx.reflect.notify([impl.name])
}
}
_checkImpl(name: string) {
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: string | boolean = 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)
}
private _setEpoch(epoch: string) {
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 FiberState.LOADING
} else {
this.inertia = this._unload()
return FiberState.UNLOADING
}
})
}
private _resolveConfig(config: any) {
config = this.context.waterfall(this, 'internal/config', config, () => config)
return this.runtime ? resolveConfig(this.runtime, config) : config
}
private async _reload() {
this.store = { ...this._store }
const oldEpoch = this._runner.epoch
try {
await Promise.resolve()
// A disposer queued before this checkpoint may already have invalidated
// the load. Do not run plugin code for a stale epoch; the state update
// below will drain any effects collected while the fiber was PENDING.
if (this._runner.epoch === oldEpoch) {
this.config = this._resolveConfig(this._config)
await this._execute(this._runner)
this._error = undefined
}
} catch (reason) {
// impl guarantees that the error is non-null (?)
this.ctx.logger.error(reason)
this._error = reason
this._runner.epoch = INACTIVE
}
this._updateState(() => {
if (this._runner.epoch === oldEpoch) {
this.inertia = undefined
} else {
this.inertia = this._unload()
return FiberState.UNLOADING
}
})
}
private async _unload() {
await Promise.all(this._disposables.clear().map(async (dispose) => {
try {
await composeError(async (info) => {
await Promise.resolve()
info.error = new Error()
await runDisposable(dispose)
}, this._runner.getOuterStack)
} catch (reason) {
this.ctx.logger.error(reason)
}
}))
this.store = undefined
this._updateState(() => {
if (this._runner.epoch === INACTIVE) {
this.inertia = undefined
} else {
this.inertia = this._reload()
return FiberState.LOADING
}
})
}
/**
* 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 the update waterfall result; the default restart returns a promise.
* @throws when validation, an update listener, or the restarted plugin fails.
*/
update(config: any, noSave = false) {
this.assertActive()
this._config = config
if (this.state !== FiberState.ACTIVE) {
// Config resolution may access injected services, so defer it until the
// fiber can activate.
this._error = undefined
this._setEpoch(INACTIVE)
this._refresh()
return
}
config = this._resolveConfig(config)
return this.context.waterfall(this, 'internal/update', config, noSave, () => {
this.config = config
this._error = undefined
return this.restart()
})
}
}
@@ -0,0 +1,14 @@
/** Core context type and root context implementation. */
export * from './context.ts'
/** Event bus, dispatch modes, and event augmentation types. */
export * from './events.ts'
/** Plugin fiber lifecycle, effects, and config validation helpers. */
export * from './fiber.ts'
/** Logger facade, logger service, message, exporter, and formatting types. */
export * from './logger.ts'
/** Plugin registry, dependency injection, and plugin entrypoint types. */
export * from './registry.ts'
/** Base service class and service lifecycle symbols. */
export * from './service.ts'
/** Shared internal helpers used by context, services, and plugin fibers. */
export * from './utils.ts'
@@ -0,0 +1,270 @@
import { defineProperty, hyphenate } from '@deepseek-ai/cosmokit'
import { Context } from './context.ts'
import { Fiber } from './fiber.ts'
import { createCallable, joinPrototype, symbols, type Tracker } from './utils.ts'
declare module './context.ts' {
interface Intercept {
logger: LoggerService.Intercept
}
}
/** Logger method name and severity category. */
export type LoggerType = 'error' | 'info' | 'warn' | 'debug'
/** Callable shape for one logger severity method. */
export type LoggerMethod = (format: any, ...param: any[]) => void
/** Formatter used to resolve a printf-style placeholder. */
export type Formatter = (value: any, exporter: Exporter, message: Message) => any
/** Numeric severity used when exporters decide whether to emit a message. */
export const enum LoggerLevel {
ERROR = 0,
INFO = 1,
WARN = 2,
DEBUG = 3,
}
/** Structured log record delivered to exporters. */
export interface Message {
sn: number
ts: number
name: string
type: LoggerType
level: number
args: any[]
fiber?: WeakRef<Fiber>
}
/** Sink that receives structured log messages. */
export interface Exporter {
colors?: number | false
maxLength?: number
levels?: Record<string, number>
formatters?: Record<string, Formatter>
export(message: Message): void
}
/** Built-in placeholder formatters used by `Logger.format()`. */
export const defaultFormatters: Record<string, Formatter> = {
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)
},
}
/** Options used when creating a named logger facade. */
export interface LoggerOptions {
/** The logger name shown with each message. */
name: string
/** Message fields merged into every record from this logger. */
meta?: Partial<Message>
/** Default maximum level exported when an exporter has no own threshold. */
level?: number
}
/** Logger facade identity, inherited message metadata, and optional minimum level. */
export interface Logger extends LoggerOptions {}
/** Logger facade severity methods. */
export interface Logger extends Record<LoggerType, LoggerMethod> {}
function isAggregateError(error: any): error is Error & { errors: Error[] } {
return error instanceof Error && Array.isArray(error['errors'])
}
/** Logger facade for one named subsystem. */
export class Logger {
static color(exporter: Exporter, code: number, value: any, decoration = '') {
if (!exporter.colors) return '' + value
return `\u001b[3${code < 8 ? code : '8;5;' + code}${exporter.colors >= 2 ? decoration : ''}m${value}\u001b[0m`
}
static code(name: string, level?: false | number) {
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: Exporter, message: Message): string {
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: string = 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') {
const value = args.shift()
return formatter(value, 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: LoggerOptions, private service: LoggerService) {
Object.assign(this, options)
this.error = this._method('error', LoggerLevel.ERROR)
this.info = this._method('info', LoggerLevel.INFO)
this.warn = this._method('warn', LoggerLevel.WARN)
this.debug = this._method('debug', LoggerLevel.DEBUG)
}
private _method(type: LoggerType, level: number): LoggerMethod {
return (...args: any[]) => {
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()) {
const targetLevel = exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? LoggerLevel.INFO
if (targetLevel < level) continue
const message: Message = { sn, ts, type, level, name: this.name, ...this.meta, args }
exporter.export(message)
}
}
}
}
/** ANSI 16-color palette indexes used for logger name coloring. */
export const c16 = [6, 2, 3, 4, 5, 1]
/** ANSI 256-color palette indexes used for logger name coloring. */
export const 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,
]
/** Logger service configuration merged from context intercepts. */
export namespace LoggerService {
export interface Intercept {
name?: string
level?: number
}
}
/** Callable `ctx.logger` service shape. */
export interface LoggerService extends Record<LoggerType, LoggerMethod> {
(name?: string): Logger
}
/**
* Built-in logging service.
*
* Call `ctx.logger()` to create a named logger, or call `ctx.logger.info()`
* directly to log with the current fiber-derived name.
*/
export class LoggerService {
bufferSize = 1000
buffer: Message[] = []
ctx!: Context
_snMessage = 0
_snExporter = 0
exporters = new Map<number, Exporter>()
constructor(ctx: Context) {
const tracker: Tracker = {
property: 'ctx',
noShadow: true,
}
const self = createCallable('logger', joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker) as unknown as LoggerService
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: Exporter) {
return this.ctx.effect(() => {
this.exporters.set(++this._snExporter, exporter)
return () => this.exporters.delete(this._snExporter)
}, 'ctx.logger.exporter()')
}
private _resolveConfig(): LoggerService.Intercept {
let intercept = this.ctx[symbols.intercept]
const configs: LoggerService.Intercept[] = []
while ('logger' in intercept) {
if (Object.hasOwn(intercept, 'logger')) {
configs.unshift(intercept['logger'])
}
intercept = Object.getPrototypeOf(intercept)
}
return Object.assign({}, ...configs)
}
[symbols.invoke](name?: string): Logger {
const config = this._resolveConfig()
const fiber = ((this.ctx as any)[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'] as const) {
;(LoggerService.prototype as any)[type] = function (this: LoggerService, ...args: any[]) {
return (this as any)()[type](...args)
}
}
}
}
@@ -0,0 +1,418 @@
import { defineProperty, isNullable } from '@deepseek-ai/cosmokit'
import type { Dict } from '@deepseek-ai/cosmokit'
import { Context } from './context.ts'
import { getTraceable, symbols, withProps } from './utils.ts'
import { Fiber, FiberState } from './fiber.ts'
declare module './context.ts' {
interface Context {
/**
* Read a service from the store without the inject requirement.
*
* @param name — the service name.
* @param strict — when `true` (default), only return implementations
* whose providing fiber is currently active.
* @returns the service value, or `undefined` when not (yet) provided.
*/
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
/** Same as above for service names outside the typed `Context` surface. */
get(name: string, strict?: boolean): any
/**
* Overwrite a provided service's value.
*
* Only the fiber that provided the service may set it; setting an
* unprovided name throws.
*
* @param name — the service name.
* @param value — the new service value.
*/
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
/** Same as above for service names outside the typed `Context` surface. */
set(name: string, value: any): void
/**
* Register a service implementation owned by the current fiber.
*
* The service becomes visible to dependents in the same isolation scope
* once the fiber is active; it is unregistered (waking dependents) when
* the returned disposer runs or the fiber unloads. Throws if the name is
* already provided in this scope or declared as an accessor.
*
* @param name — the service name.
* @param value — the service value.
* @returns a disposer that unregisters the service.
*/
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
/** Same as above for service names outside the typed `Context` surface. */
provide(name: string, value?: any): () => void
/**
* Define a computed context property backed by get/set hooks.
*
* The accessor is removed when the current fiber unloads. Throws if the
* name is already declared.
*
* @param name — the context property name.
* @param options — the `get` hook and optional `set` hook.
*/
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
/**
* Expose selected members of a service directly on `ctx`.
*
* Each mixed-in key becomes an accessor that forwards to the service
* (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`.
* Mixins are removed when the current fiber unloads.
*
* @param name — the context property holding the source service.
* @param mixins — keys to forward, or a source-key → ctx-key map.
*/
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
/** Same as above with a source object instead of a context property name. */
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
}
}
function enhanceError(error: Error) {
const lines = error.stack!.split('\n')
lines.splice(0, 2, `Error: ${error.message}`)
error.stack = lines.join('\n')
return error
}
const RESERVED_WORDS = ['prototype', 'then']
// - is a symbol
// - is a reserved word (prototype, then)
// - is a number string (0, 1, 2, ...)
// - starts with `_`
function isSpecialProperty(prop: string | symbol): prop is symbol {
return typeof prop === 'symbol'
|| RESERVED_WORDS.includes(prop)
|| parseInt(prop).toString() === prop
|| prop.startsWith('_')
}
/** Context property definition known by the reflection service. */
export type Property = Property.Service | Property.Accessor
/** Property definition variants understood by `ReflectService`. */
export namespace Property {
/** Service property backed by a provided implementation. */
export interface Service {
/** Discriminator. */
type: 'service'
}
/** Computed context property backed by custom get/set hooks. */
export interface Accessor {
/** Discriminator. */
type: 'accessor'
/** Compute the property value; `error` carries the caller stack for diagnostics. */
get: (this: Context, receiver: any, error: Error) => any
/** Optional setter; return `false` to reject the write. */
set?: (this: Context, value: any, receiver: any, error: Error) => boolean
}
}
/** Concrete service implementation record stored in the root reflect service. */
export interface Impl {
/** The service name. */
name: string
/** The fiber that provided the service (owns its lifetime). */
fiber: Fiber
/** The current service value. */
value?: any
/** Optional availability predicate consulted before dependents may load. */
check?: () => boolean
}
/**
* Reflection and service-resolution layer installed as `ctx.reflect`.
*
* This service powers the context proxy, service registration, accessors, and
* the mixins that expose core service methods directly on `ctx`.
*/
export class ReflectService {
/** Proxy traps implementing service resolution for every context object. */
static handler: ProxyHandler<Context> = {
get: (target, prop, ctx: Context) => {
if (isSpecialProperty(prop)) {
return Reflect.get(target, prop, ctx)
}
if (Reflect.has(target, prop)) {
return getTraceable(ctx, Reflect.get(target, prop, ctx))
}
const error = 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] as Context ?? 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: any) {
throw e === error ? enhanceError(e) : e
}
},
set: (target, prop, value, ctx: Context) => {
if (isSpecialProperty(prop)) {
return Reflect.set(target, prop, value, ctx)
}
const error = 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: any) {
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. */
public store: Dict<Impl, symbol> = Object.create(null)
/** Declared context properties (services and accessors), by name. */
public props: Dict<Property> = Object.create(null)
constructor(public ctx: Context) {
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: string, strict = true) {
return getTraceable(this.ctx, this._getImpl(name, strict)?.value)
}
_getImpl(name: string, strict = true) {
const key = this.ctx[symbols.isolate][name]
const impl = key && this.store[key]
if (!impl) return
if (strict && impl.fiber.state !== FiberState.ACTIVE) 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: string, value: any, error?: 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: string, value?: any, check?: () => boolean) {
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: 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 === FiberState.ACTIVE) {
this.notify([name])
}
return async () => {
delete this.store[key]
const fibers = this.notify([name])
await Promise.allSettled(fibers.map(fiber => fiber.await()))
// ensure self access before dependencies cleanup
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: string[], filter = (ctx: Context, name: string) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
const fibers: Fiber[] = []
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: Context = Object.create(this.ctx)
self[symbols.filter] = (target: Context) => 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: string, options: Omit<Property.Accessor, 'type'>) {
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: any, mixins: string[] | Dict<string>) {
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: Context, error: Error) => {
// TODO enhance error message
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 value = Reflect.get(service, key, mixin)
if (typeof value !== 'function') return value
return value.bind(mixin ?? service)
},
set(value, receiver, error) {
const service = getTarget(this, error)
const mixin = receiver ? withProps(receiver, service) : service
return Reflect.set(service, key, value, 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<T>(value: T) {
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<T extends Function>(callback: T) {
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)
},
})
}
}
@@ -0,0 +1,337 @@
import { defineProperty } from '@deepseek-ai/cosmokit'
import type { Dict } from '@deepseek-ai/cosmokit'
import type { StandardSchemaV1 } from '@standard-schema/spec'
import { Context } from './context.ts'
import { Fiber } from './fiber.ts'
import { buildOuterStack, DisposableList, symbols, withProps } from './utils.ts'
function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
}
/**
* Service dependency declaration accepted by plugins and the `@Inject`
* decorator.
*
* Array form requests services without intercept config. Object form maps each
* service name to optional intercept config for the plugin context.
*/
export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
/** Context keys that correspond to services with typed intercept config. */
export type InjectKey = keyof {
[K in keyof Context & string as Context[K] extends { [symbols.config]: any } ? K : never]: any
}
/**
* Decorator for declaring service dependencies on classes or class methods.
*
* On classes it contributes to the plugin's static `inject` map. On methods it
* delays the method call until the declared services are available.
*/
/**
* @param name — the required service name.
* @param config — optional intercept config applied for that service.
* @returns the class or method decorator.
*/
export function Inject<K extends InjectKey>(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) {
return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
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 ??= Object.create(null)
inject[name] = config
decorator.addInitializer(function () {
const property = this[symbols.tracker]?.property
;(this[symbols.initHooks] ??= []).push(() => {
(this.ctx as Context).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')
}
}
}
/** Utilities for normalizing plugin dependency declarations. */
export namespace Inject {
/**
* Convert array/object/class-inherited inject metadata into a plain map.
*
* @param inject — the declaration to normalize; `null`/`undefined` add nothing.
* @param result — the map to fill (service name → intercept config or `null`).
* @returns `result`.
*/
export function resolve(inject: Inject | null | undefined, result: Dict = 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
}
}
/** Supported plugin entrypoint shapes. */
export type Plugin<T = any> =
| Plugin.Function<T>
| Plugin.Constructor<T>
| Plugin.Object<T>
/** Types associated with plugin entrypoints and runtime records. */
export namespace Plugin {
/** Shared metadata understood by the plugin registry and related tooling. */
export interface Base<T = any> {
/** Display name used for fiber diagnostics and logger names. */
name?: string
/** Standard-schema validator applied to config before the plugin starts. */
Config?: StandardSchemaV1<any, T>
/** Services the plugin requires; it only loads while all are available. */
inject?: Inject
/** Service name(s) the plugin provides (read by `Service` and by loaders). */
provide?: string | string[]
/** Service names whose intercept config the plugin declares it consumes. */
intercept?: Dict<boolean>
}
export interface Transform<S, T> {
/** Marks the transform object as a schema/config transform. */
schema?: true
/** Convert user-facing config to runtime config. */
Config: (config: S) => T
}
/** Function plugin called with `(ctx, config)`. */
export interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any
}
/** Class plugin constructed with `(ctx, config)`. */
export interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any
}
/** Object plugin with an `apply(ctx, config)` method. */
export interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any
}
/** Mutable registry record shared by all fibers of one plugin callback. */
export interface Runtime {
/** Display name copied from the first registered plugin shape. */
name?: string
/** Every live fiber of this plugin (one per `ctx.plugin()` call). */
fibers: DisposableList<Fiber>
/** The executable entrypoint all fibers share (registry identity key). */
callback: globalThis.Function
/** Standard-schema validator applied to each fiber's config. */
Config?: StandardSchemaV1
}
}
type Spread<T> = undefined extends T ? [config?: T] : [config: T]
type GetPluginParameters<P> =
| P extends (ctx: Context, ...args: infer R) => any
? R
: P extends new (ctx: Context, ...args: infer R) => any
? R
: P extends { apply(ctx: Context, ...args: infer R): any }
? R
: never
type GetPluginConfig<P> =
| P extends Plugin.Transform<infer S, any>
? S
: GetPluginParameters<P>[0]
declare module './context.ts' {
export interface Context {
/**
* Run a callback once the requested services are available.
*
* Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback
* is unloaded and re-run whenever a required service changes.
*
* @param deps — 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(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
/**
* Load a plugin in the current context.
*
* @param plugin — a function, class, or `{ apply }` object plugin.
* @param args — the plugin config, validated against its `Config` schema.
* @returns the fiber; awaiting it settles once loading finished
* (rejecting on config or startup errors).
*/
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
}
}
/**
* Plugin registry installed as `ctx.registry` and mixed into every context.
*
* It normalizes plugin shapes, tracks plugin runtimes, starts fibers, and
* exposes map-like inspection over active plugin callbacks.
*/
export class RegistryService {
private _counter = 0
private _internal = new Map<Function, Plugin.Runtime>()
constructor(public ctx: Context) {
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: Plugin): Function | undefined {
// plugin.apply may throw
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: 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: 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: 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: (value: Plugin.Runtime, key: Function) => void) {
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: Inject, callback: Plugin.Function<void>) {
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: Plugin, config?: any, getOuterStack = buildOuterStack()) {
// check if it's a valid plugin
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 = undefined
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) as Fiber & PromiseLike<Fiber>
wrapped.then = (onFulfilled, onRejected) => {
return fiber.await().then(onFulfilled, onRejected)
}
return wrapped
}
}
@@ -0,0 +1,115 @@
import { defineProperty } from '@deepseek-ai/cosmokit'
import { Context } from './context.ts'
import { createCallable, joinPrototype, symbols, type Tracker } from './utils.ts'
/**
* Base class for services that expose a named API on `ctx`.
*
* Subclasses call `super(ctx, name)` from their constructor. The service is
* registered immediately and is automatically removed with the owning fiber.
*/
export abstract class Service<out T = never> {
/** Symbol key of an instance method run after construction (class plugins). */
static readonly init: unique symbol = symbols.init
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
static readonly check: unique symbol = symbols.check
/** Symbol key of the phantom intercept-config type parameter. */
static readonly config: unique symbol = symbols.config
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
static readonly invoke: unique symbol = symbols.invoke
/** Symbol key of the helper deriving an extended service instance. */
static readonly extend: unique symbol = symbols.extend
/** Symbol key of the tracker metadata used for context tracing. */
static readonly tracker: unique symbol = symbols.tracker
/** Symbol key of the intercept-config resolution helper below. */
static readonly resolveConfig: unique symbol = symbols.resolveConfig
declare [symbols.config]: T
/** The service name this instance is registered under. */
public name!: string
/**
* 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(protected ctx: Context, name: string) {
name ??= this.constructor['provide'] as string
let self = this
const tracker: 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
}
protected [symbols.filter](ctx: Context) {
return ctx[symbols.isolate][this.name] === this.ctx[symbols.isolate][this.name]
}
protected [symbols.extend](props?: any) {
let self: any
if (this[Service.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?: T, head?: T): T {
let intercept = this.ctx[Context.intercept]
const configs: any[] = []
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: any) {
if (!instance) return false
let constructor = instance.constructor
while (constructor) {
// constructor may be a proxy
constructor = constructor.prototype?.constructor
if (constructor === this) return true
constructor &&= Object.getPrototypeOf(constructor)
}
return false
}
}
@@ -0,0 +1,287 @@
import { defineProperty } from '@deepseek-ai/cosmokit'
import type { Context, Service } from './index.ts'
/** Ordered collection of disposable values with O(1) deletion by value. */
export class DisposableList<T extends WeakKey> {
private sn = 0
private map = new Map<number, T>()
private weak = new WeakMap<T, number>()
get length() {
return this.map.size
}
push(value: T) {
const sn = ++this.sn
this.map.set(sn, value)
this.weak.set(value, sn)
return () => this.map.delete(sn)
}
delete(value: T) {
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()
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return [...this]
}
}
/** Metadata used by traceable proxies to rebind `ctx` and associated services. */
export interface Tracker {
associate?: string
property?: string
noShadow?: boolean
}
/** Shared symbols used to avoid public property-name collisions. */
export const symbols = {
// internal symbols
shadow: Symbol.for('cordis.shadow'),
receiver: Symbol.for('cordis.receiver'),
original: Symbol.for('cordis.original'),
metadata: Symbol.for('cordis.metadata'),
initHooks: Symbol.for('cordis.initHooks'),
checkProto: Symbol.for('cordis.checkProto'),
// context symbols
effect: Symbol.for('cordis.effect') as typeof Context.effect,
filter: Symbol.for('cordis.filter') as typeof Context.filter,
isolate: Symbol.for('cordis.isolate') as typeof Context.isolate,
intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,
// service symbols
init: Symbol.for('cordis.init') as typeof Service.init,
check: Symbol.for('cordis.check') as typeof Service.check,
config: Symbol.for('cordis.config') as typeof Service.config,
invoke: Symbol.for('cordis.invoke') as typeof Service.invoke,
extend: Symbol.for('cordis.extend') as typeof Service.extend,
tracker: Symbol.for('cordis.tracker') as typeof Service.tracker,
resolveConfig: Symbol.for('cordis.resolveConfig') as typeof Service.resolveConfig,
}
const GeneratorFunction = function* () {}.constructor
const AsyncGeneratorFunction = async function* () {}.constructor
/** Return true when a plugin callback should be constructed with `new`. */
export function isConstructor(func: any): func is new (...args: any) => any {
// async function or arrow function
if (!func.prototype) return false
// generator function or malformed definition
// we cannot use below check because `mock.fn()` is proxied
// if (func.prototype.constructor !== func) return false
if (func instanceof GeneratorFunction) return false
// polyfilled AsyncGeneratorFunction === Function
if (AsyncGeneratorFunction !== Function && func instanceof AsyncGeneratorFunction) return false
return true
}
/** Merge two prototype chains while preserving descriptors from `proto1`. */
export 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
}
/** Return true for non-null objects and functions. */
export function isObject(value: any): value is {} {
return value && (typeof value === 'object' || typeof value === 'function')
}
/** Find a property descriptor by walking an object's prototype chain. */
export function getPropertyDescriptor(target: any, prop: string | symbol) {
let proto = target
while (proto) {
const desc = Reflect.getOwnPropertyDescriptor(proto, prop)
if (desc) return desc
proto = Object.getPrototypeOf(proto)
}
}
/** Wrap services/functions so method calls see the caller's active context. */
export function getTraceable<T>(ctx: Context, value: T): T {
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)
}
/** Return a proxy that overlays readonly or writable properties onto a target. */
export function withProps(target: any, props?: {}) {
if (!props) return target
return new Proxy(target, {
get: (target, prop, receiver) => {
if (prop in props && prop !== 'constructor') return Reflect.get(props, prop, receiver)
return Reflect.get(target, prop, receiver)
},
set: (target, prop, value, receiver) => {
if (prop in props && prop !== 'constructor') return Reflect.set(props, prop, value, receiver)
return Reflect.set(target, prop, value, receiver)
},
})
}
function withProp(target: any, prop: string | symbol, value: any) {
return withProps(target, Object.defineProperty(Object.create(null), prop, {
value,
writable: false,
}))
}
function createShadow(ctx: Context, target: any, property: string | undefined, receiver: any) {
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: Context, value: any, outer: any, 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: Context, value: any, tracker: Tracker) {
// noShadow services are identity-aware (e.g. logger uses the origin fiber to
// derive its name): keep the shadow ctx so they can read [symbols.shadow]
// and resolve the origin. Non-noShadow services strip — their side effects
// bind to caller, not origin.
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: any, innerValue: any
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, value, receiver) => {
if (prop === symbols.original) return false
if (prop === tracker.property) return false
if (typeof prop === 'symbol') {
return Reflect.set(target, prop, value, receiver)
}
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
return Reflect.set(ctx, `${tracker.associate}.${prop}`, value, withProp(ctx, symbols.receiver, receiver))
}
const shadow = createShadow(ctx, target, tracker.property, receiver)
return Reflect.set(target, prop, value, shadow)
},
apply: (target, thisArg, args) => {
return applyTraceable(proxy, target, thisArg, args)
},
})
return proxy
}
function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args)
return value[symbols.invoke].apply(proxy, args)
}
/** Create a callable service object that dispatches through `symbols.invoke`. */
export function createCallable(name: string, proto: {}, tracker: Tracker) {
const self = function (...args: any[]) {
const proxy = createTraceable(self['ctx'], self, tracker)
return applyTraceable(proxy, self, this, args)
}
defineProperty(self, 'name', name)
return Object.setPrototypeOf(self, proto)
}
interface StackInfo {
offset: number
error: Error
}
function handleError(info: StackInfo, reason: any, getOuterStack: () => string[]): never {
const innerLines = info.error.stack!.split('\n')
// malformed error
if (typeof reason?.stack !== 'string') {
const outerError = new Error(reason)
const lines = outerError.stack!.split('\n')
lines.splice(1, Infinity, ...getOuterStack())
outerError.stack = lines.join('\n')
throw outerError
}
// long stack trace
const lines: string[] = 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
}
/** Run a callback and splice outer call-site frames into thrown async errors. */
export function composeError<T>(callback: (info: StackInfo) => T, getOuterStack = buildOuterStack()): T {
const info: StackInfo = { offset: 1, error: new Error() }
try {
const result: any = callback(info)
if (isObject(result) && 'then' in result) {
return (result as any).then(undefined, (reason) => handleError(info, reason, getOuterStack)) as T
} else {
return result
}
} catch (reason: any) {
handleError(info, reason, getOuterStack)
}
}
/** Capture a lazy stack-frame supplier for later error composition. */
export function buildOuterStack(offset = 0) {
const outerError = new Error()
return () => outerError.stack!.split('\n').slice(3 + offset)
}
@@ -0,0 +1 @@
../../../@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit
@@ -0,0 +1 @@
../../../@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,24 @@
# cosmokit
[![Codecov](https://img.shields.io/codecov/c/github/shigma/cosmokit?style=flat-square)](https://codecov.io/gh/shigma/cosmokit)
[![npm](https://img.shields.io/npm/v/cosmokit?style=flat-square)](https://www.npmjs.com/package/cosmokit)
A collection of common utilities.
## Usage
### Node.js
```sh
npm install cosmokit
```
```ts
import cosmokit from 'cosmokit'
```
### Deno
```ts
import cosmokit from 'npm:cosmokit@latest'
```
@@ -0,0 +1,325 @@
//#region lib/types/misc.js
/** No-op callback returning `undefined` at runtime and `any` at type level. */
function noop() {}
/** Return true when a value is `null` or `undefined`. */
function isNullable(value) {
return value === null || value === void 0;
}
/** Return true when a value is neither `null` nor `undefined`. */
function isNonNullable(value) {
return !isNullable(value);
}
/** Return true for non-array object values. */
function isPlainObject(data) {
return data && typeof data === "object" && !Array.isArray(data);
}
/** Filter object entries and return a new object. */
function filterKeys(object, filter) {
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
}
/** Map object values while preserving the original key set. */
function mapValues(object, transform) {
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
}
/** Pick selected keys from an object, optionally including `undefined` values. */
function pick(source, keys, forced) {
if (!keys) return { ...source };
const result = {};
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
return result;
}
/** Omit selected keys from a shallow object copy. */
function omit(source, keys) {
if (!keys) return { ...source };
const result = { ...source };
for (const key of keys) Reflect.deleteProperty(result, key);
return result;
}
/** Define a non-enumerable writable property and return the object. */
function defineProperty(object, key, value) {
return Object.defineProperty(object, key, {
writable: true,
value,
enumerable: false
});
}
//#endregion
//#region lib/types/array.js
/** Return true when every item in `array2` is present in `array1`. */
function contain(array1, array2) {
return array2.every((item) => array1.includes(item));
}
/** Return items that appear in both arrays. */
function intersection(array1, array2) {
return array1.filter((item) => array2.includes(item));
}
/** Return items from `array1` that do not appear in `array2`. */
function difference(array1, array2) {
return array1.filter((item) => !array2.includes(item));
}
/** Return the set-union of two arrays while preserving first occurrence order. */
function union(array1, array2) {
return Array.from(new Set([...array1, ...array2]));
}
/** Remove duplicate values while preserving first occurrence order. */
function deduplicate(array) {
return [...new Set(array)];
}
/** Remove one item from an array and report whether it was found. */
function remove(list, item) {
const index = list?.indexOf(item);
if (index >= 0) {
list.splice(index, 1);
return true;
} else return false;
}
/** Normalize nullish, scalar, or array input to an array. */
function makeArray(source) {
return Array.isArray(source) ? source : isNullable(source) ? [] : [source];
}
//#endregion
//#region lib/types/types.js
/** Test values using `instanceof` with a `toStringTag` fallback. */
function is(type, value) {
if (arguments.length === 1) return (value) => is(type, value);
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);
}
/** Binary source detection and base64/hex conversion helpers. */
var Binary;
(function(Binary) {
Binary.is = isArrayBufferLike;
Binary.isSource = isArrayBufferSource;
function fromSource(source) {
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
else return source;
}
Binary.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);
}
Binary.toBase64 = toBase64;
function fromBase64(source) {
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
}
Binary.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("");
}
Binary.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;
}
Binary.fromHex = fromHex;
})(Binary || (Binary = {}));
/** Decode a base64 string into binary data. */
const base64ToArrayBuffer = Binary.fromBase64;
/** Encode binary data as base64. */
const arrayBufferToBase64 = Binary.toBase64;
/** Decode a hex string into binary data. */
const hexToArrayBuffer = Binary.fromHex;
/** Encode binary data as hex. */
const arrayBufferToHex = Binary.toHex;
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
function clone(source, refs = /* @__PURE__ */ new Map()) {
if (!source || typeof source !== "object") return source;
if (is("Date", source)) return new Date(source.valueOf());
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
if (isArrayBufferLike(source)) return source.slice(0);
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
const cached = refs.get(source);
if (cached) return cached;
if (Array.isArray(source)) {
const result = [];
refs.set(source, result);
source.forEach((value, index) => {
result[index] = Reflect.apply(clone, null, [value, refs]);
});
return result;
}
const result = Object.create(Object.getPrototypeOf(source));
refs.set(source, result);
for (const key of Reflect.ownKeys(source)) {
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
Reflect.defineProperty(result, key, descriptor);
}
return result;
}
/** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
function deepEqual(a, b, strict) {
if (a === b) return true;
if (!strict && isNullable(a) && isNullable(b)) return true;
if (typeof a !== typeof b) return false;
if (typeof a !== "object") return false;
if (!a || !b) return false;
function check(test, then) {
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
}
return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
if (a.byteLength !== b.byteLength) return false;
const viewA = new Uint8Array(a);
const viewB = new Uint8Array(b);
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
return true;
}) ?? Object.keys({
...a,
...b
}).every((key) => deepEqual(a[key], b[key], strict));
}
//#endregion
//#region lib/types/string.js
/** Uppercase the first character of a string. */
function capitalize(source) {
return source.charAt(0).toUpperCase() + source.slice(1);
}
/** Lowercase the first character of a string. */
function uncapitalize(source) {
return source.charAt(0).toLowerCase() + source.slice(1);
}
/** Convert dash or underscore delimited text to camelCase. */
function camelCase(source) {
return source.replace(/[_-][a-z]/g, (str) => str.slice(1).toUpperCase());
}
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);
}
/** Convert text to dash-delimited parameter case. */
function paramCase(source) {
return tokenize(source, [45, 95], 45);
}
/** Convert text to underscore-delimited snake case. */
function snakeCase(source) {
return tokenize(source, [45, 95], 95);
}
/** Runtime alias for `camelCase`. */
const camelize = camelCase;
/** Runtime alias for `paramCase`. */
const hyphenate = paramCase;
/** Format a property key as a JavaScript member access suffix. */
function formatProperty(key) {
if (typeof key !== "string") return `[${key.toString()}]`;
return /^[a-z_$][\w$]*$/i.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
}
/** Remove one trailing slash from a path string. */
function trimSlash(source) {
return source.replace(/\/$/, "");
}
/** Ensure a path starts with `/` and has no trailing slash. */
function sanitize(source) {
if (!source.startsWith("/")) source = "/" + source;
return trimSlash(source);
}
//#endregion
//#region lib/types/time.js
/** Time constants plus parsing and formatting helpers. */
var Time;
(function(Time) {
Time.millisecond = 1;
Time.second = 1e3;
Time.minute = Time.second * 60;
Time.hour = Time.minute * 60;
Time.day = Time.hour * 24;
Time.week = Time.day * 7;
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
function setTimezoneOffset(offset) {
timezoneOffset = offset;
}
Time.setTimezoneOffset = setTimezoneOffset;
function getTimezoneOffset() {
return timezoneOffset;
}
Time.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() / Time.minute - offset) / 1440);
}
Time.getDateNumber = getDateNumber;
function fromDateNumber(value, offset) {
const date = new Date(value * Time.day);
if (offset === void 0) offset = timezoneOffset;
return new Date(+date + offset * Time.minute);
}
Time.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]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
}
Time.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();
}
Time.parseDate = parseDate;
function format(ms) {
const abs = Math.abs(ms);
if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
return ms + "ms";
}
Time.format = format;
function toDigits(source, length = 2) {
return source.toString().padStart(length, "0");
}
Time.toDigits = toDigits;
function template(template, time = /* @__PURE__ */ new Date()) {
return template.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));
}
Time.template = template;
})(Time || (Time = {}));
//#endregion
export { Binary, Time, arrayBufferToBase64, arrayBufferToHex, base64ToArrayBuffer, camelCase, camelize, capitalize, clone, contain, deduplicate, deepEqual, defineProperty, difference, filterKeys, formatProperty, hexToArrayBuffer, hyphenate, intersection, is, isNonNullable, isNullable, isPlainObject, makeArray, mapValues, mapValues as valueMap, noop, omit, paramCase, pick, remove, sanitize, snakeCase, trimSlash, uncapitalize, union };
@@ -0,0 +1,15 @@
/** Return true when every item in `array2` is present in `array1`. */
export declare function contain(array1: readonly any[], array2: readonly any[]): boolean;
/** Return items that appear in both arrays. */
export declare function intersection<T>(array1: readonly T[], array2: readonly T[]): T[];
/** Return items from `array1` that do not appear in `array2`. */
export declare function difference<S>(array1: readonly S[], array2: readonly any[]): S[];
/** Return the set-union of two arrays while preserving first occurrence order. */
export declare function union<T>(array1: readonly T[], array2: readonly T[]): T[];
/** Remove duplicate values while preserving first occurrence order. */
export declare function deduplicate<T>(array: readonly T[]): T[];
/** Remove one item from an array and report whether it was found. */
export declare function remove<T>(list: T[], item: T): boolean;
/** Normalize nullish, scalar, or array input to an array. */
export declare function makeArray<T>(source: null | undefined | T | T[]): T[];
//# sourceMappingURL=array.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"array.d.ts","sourceRoot":"","sources":["../../src/array.ts"],"names":[],"mappings":"AAEA,sEAAsE;AACtE,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,GAAG,EAAE,WAErE;AAED,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAEzE;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,GAAG,EAAE,OAEzE;AAED,kFAAkF;AAClF,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAElE;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,OAEjD;AAED,qEAAqE;AACrE,wBAAgB,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,WAQ3C;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,GAAG,SAAS,GAAG,CAAC,GAAG,CAAC,EAAE,OAE9D"}
@@ -0,0 +1,11 @@
/** Array set and normalization helpers. */
export * from './array.ts';
/** Runtime type, binary, clone, and equality helpers. */
export * from './types.ts';
/** Shared utility types and object helpers. */
export * from './misc.ts';
/** String case, path, and property formatting helpers. */
export * from './string.ts';
/** Time constants, parsing, and formatting helpers. */
export * from './time.ts';
//# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,cAAc,YAAY,CAAA;AAC1B,yDAAyD;AACzD,cAAc,YAAY,CAAA;AAC1B,+CAA+C;AAC/C,cAAc,WAAW,CAAA;AACzB,0DAA0D;AAC1D,cAAc,aAAa,CAAA;AAC3B,uDAAuD;AACvD,cAAc,WAAW,CAAA"}
@@ -0,0 +1,41 @@
/** String/symbol keyed dictionary type. */
export type Dict<T = any, K extends string | symbol = string> = {
[key in K]: T;
};
/** Safely read `T[K]`, returning `never` when `K` is not a key of `T`. */
export type Get<T extends {}, K> = K extends keyof T ? T[K] : never;
/** Conditional extraction helper with a configurable return type. */
export type Extract<S, T, U = S> = S extends T ? U : never;
/** Accept a value or an array, unless the value is already an array type. */
export type MaybeArray<T> = [T] extends [unknown[]] ? T : T | T[];
/** Wrap a value in `Promise`, preserving the resolved type of existing promises. */
export type Promisify<T> = Promise<T extends Promise<infer S> ? S : T>;
/** Accept a value or promise unless the value type is already promise-like. */
export type Awaitable<T> = [T] extends [Promise<unknown>] ? T : T | Promise<T>;
/** Convert a union type to an intersection type. */
export type Intersect<U> = (U extends any ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never;
/** No-op callback returning `undefined` at runtime and `any` at type level. */
export declare function noop(): any;
/** Return true when a value is `null` or `undefined`. */
export declare function isNullable(value: any): value is null | undefined | void;
/** Return true when a value is neither `null` nor `undefined`. */
export declare function isNonNullable<T>(value: T): value is NonNullable<T>;
/** Return true for non-array object values. */
export declare function isPlainObject(data: any): any;
/** Filter object entries with a key type guard. */
export declare function filterKeys<T, K extends string, U extends K>(object: Dict<T, K>, filter: (key: K, value: T) => key is U): Dict<T, U>;
/** Filter object entries with a boolean predicate. */
export declare function filterKeys<T, K extends string>(object: Dict<T, K>, filter: (key: K, value: T) => boolean): Dict<T, K>;
/** Map object values while preserving the original key set. */
export declare function mapValues<U, T, K extends string>(object: Dict<T, K>, transform: (value: T, key: K) => U): Dict<U, K>;
/** Alias for `mapValues`. */
export { mapValues as valueMap };
/** Pick selected keys from an object, optionally including `undefined` values. */
export declare function pick<T extends object, K extends keyof T>(source: T, keys?: Iterable<K>, forced?: boolean): Pick<T, K>;
/** Omit selected keys from a shallow object copy. */
export declare function omit<T, K extends keyof T>(source: T, keys?: Iterable<K>): Omit<T, K>;
/** Define a non-enumerable writable property with a typed key. */
export declare function defineProperty<T, K extends keyof T>(object: T, key: K, value: T[K]): T;
/** Define a non-enumerable writable property with an arbitrary key. */
export declare function defineProperty<T, K extends keyof any>(object: T, key: K, value: any): T;
//# sourceMappingURL=misc.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"misc.d.ts","sourceRoot":"","sources":["../../src/misc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
/** Uppercase the first character of a string. */
export declare function capitalize(source: string): string;
/** Lowercase the first character of a string. */
export declare function uncapitalize(source: string): string;
/** Convert dash or underscore delimited text to camelCase. */
export declare function camelCase(source: string): string;
/** Convert text to dash-delimited parameter case. */
export declare function paramCase(source: string): string;
/** Convert text to underscore-delimited snake case. */
export declare function snakeCase(source: string): string;
/** Runtime alias for `camelCase`. */
export declare const camelize: typeof camelCase;
/** Runtime alias for `paramCase`. */
export declare const hyphenate: typeof paramCase;
declare namespace Letter {
interface LowerToUpper {
a: 'A';
b: 'B';
c: 'C';
d: 'D';
e: 'E';
f: 'F';
g: 'G';
h: 'H';
i: 'I';
j: 'J';
k: 'K';
l: 'L';
m: 'M';
n: 'N';
o: 'O';
p: 'P';
q: 'Q';
r: 'R';
s: 'S';
t: 'T';
u: 'U';
v: 'V';
w: 'W';
x: 'X';
y: 'Y';
z: 'Z';
}
interface UpperToLower {
A: 'a';
B: 'b';
C: 'c';
D: 'd';
E: 'e';
F: 'f';
G: 'g';
H: 'h';
I: 'i';
J: 'j';
K: 'k';
L: 'l';
M: 'm';
N: 'n';
O: 'o';
P: 'p';
Q: 'q';
R: 'r';
S: 's';
T: 't';
U: 'u';
V: 'v';
W: 'w';
X: 'x';
Y: 'y';
Z: 'z';
}
export type Upper = keyof UpperToLower;
export type Lower = keyof LowerToUpper;
export type ToUpper<S extends string> = S extends Lower ? LowerToUpper[S] : S;
export type ToLower<S extends string, P extends string = ''> = S extends Upper ? `${P}${UpperToLower[S]}` : S;
export {};
}
/** Type-level conversion from dash-delimited text to camelCase. */
export type camelize<S extends string> = S extends `${infer L}-${infer M}${infer R}` ? `${L}${Letter.ToUpper<M>}${camelize<R>}` : S;
/** Type-level conversion from camelCase text to dash-delimited text. */
export type hyphenate<S extends string> = S extends `${infer L}${infer R}` ? `${Letter.ToLower<L, '-'>}${hyphenate<R>}` : S;
/** Format a property key as a JavaScript member access suffix. */
export declare function formatProperty(key: keyof any): string;
/** Remove one trailing slash from a path string. */
export declare function trimSlash(source: string): string;
/** Ensure a path starts with `/` and has no trailing slash. */
export declare function sanitize(source: string): string;
export {};
//# sourceMappingURL=string.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"string.d.ts","sourceRoot":"","sources":["../../src/string.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
/** Time constants plus parsing and formatting helpers. */
export declare namespace Time {
const millisecond = 1;
const second = 1000;
const minute: number;
const hour: number;
const day: number;
const week: number;
function setTimezoneOffset(offset: number): void;
function getTimezoneOffset(): number;
function getDateNumber(date?: number | Date, offset?: number): number;
function fromDateNumber(value: number, offset?: number): Date;
function parseTime(source: string): number;
function parseDate(date: string): Date;
function format(ms: number): string;
function toDigits(source: number, length?: number): string;
function template(template: string, time?: Date): string;
}
//# sourceMappingURL=time.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/time.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,yBAAiB,IAAI,CAAC;IACb,MAAM,WAAW,IAAI,CAAA;IACrB,MAAM,MAAM,OAAO,CAAA;IACnB,MAAM,MAAM,QAAc,CAAA;IAC1B,MAAM,IAAI,QAAc,CAAA;IACxB,MAAM,GAAG,QAAY,CAAA;IACrB,MAAM,IAAI,QAAU,CAAA;IAI3B,SAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,QAE/C;IAED,SAAgB,iBAAiB,WAEhC;IAED,SAAgB,aAAa,CAAC,IAAI,GAAE,MAAM,GAAG,IAAiB,EAAE,MAAM,CAAC,EAAE,MAAM,UAI9E;IAED,SAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,QAI5D;IAWD,SAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,UAQvC;IAED,SAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,QAUrC;IAED,SAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,UAYhC;IAED,SAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,UAElD;IAED,SAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,OAAa,UAU3D;CACF"}
@@ -0,0 +1,34 @@
type GlobalConstructorNames = keyof {
[K in keyof typeof globalThis as typeof globalThis[K] extends abstract new (...args: any) => any ? K : never]: K;
};
/** Create a predicate for a global constructor name. */
export declare function is<K extends GlobalConstructorNames>(type: K): (value: any) => value is InstanceType<typeof globalThis[K]>;
/** Test whether a value matches a global constructor name. */
export declare function is<K extends GlobalConstructorNames>(type: K, value: any): value is InstanceType<typeof globalThis[K]>;
declare function isArrayBufferLike(value: any): value is ArrayBufferLike;
declare function isArrayBufferSource(value: any): value is Binary.Source;
/** Binary source detection and base64/hex conversion helpers. */
export declare namespace Binary {
type Source<T extends ArrayBufferLike = ArrayBufferLike> = T | ArrayBufferView<T>;
const is: typeof isArrayBufferLike;
const isSource: typeof isArrayBufferSource;
function fromSource<T extends ArrayBufferLike>(source: Source<T>): T;
function toBase64(source: Source): string;
function fromBase64(source: string): ArrayBuffer | Uint8Array<ArrayBuffer>;
function toHex(source: Source): string;
function fromHex(source: string): ArrayBuffer;
}
/** Decode a base64 string into binary data. */
export declare const base64ToArrayBuffer: typeof Binary.fromBase64;
/** Encode binary data as base64. */
export declare const arrayBufferToBase64: typeof Binary.toBase64;
/** Decode a hex string into binary data. */
export declare const hexToArrayBuffer: typeof Binary.fromHex;
/** Encode binary data as hex. */
export declare const arrayBufferToHex: typeof Binary.toHex;
/** Deep-clone common JavaScript values while preserving prototypes. */
export declare function clone<T>(source: T): T;
/** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
export declare function deepEqual(a: any, b: any, strict?: boolean): boolean;
export {};
//# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAEA,KAAK,sBAAsB,GAAG,MAAM;KACjC,CAAC,IAAI,MAAM,OAAO,UAAU,IAAI,OAAO,UAAU,CAAC,CAAC,CAAC,SAAS,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC;CACjH,CAAA;AAED,wDAAwD;AACxD,wBAAgB,EAAE,CAAC,CAAC,SAAS,sBAAsB,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,KAAK,IAAI,YAAY,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;AAC1H,8DAA8D;AAC9D,wBAAgB,EAAE,CAAC,CAAC,SAAS,sBAAsB,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,YAAY,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;AAQtH,iBAAS,iBAAiB,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,eAAe,CAE/D;AAED,iBAAS,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,CAE/D;AAED,iEAAiE;AACjE,yBAAiB,MAAM,CAAC;IACtB,KAAY,MAAM,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAA;IAEjF,MAAM,EAAE,0BAAoB,CAAA;IAC5B,MAAM,QAAQ,4BAAsB,CAAA;IAE3C,SAAgB,UAAU,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAO1E;IAED,SAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,UAWtC;IAED,SAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,yCAGxC;IAED,SAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,UAInC;IAED,SAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,eAQrC;CACF;AAED,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB,0BAAoB,CAAA;AACpD,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,wBAAkB,CAAA;AAClD,4CAA4C;AAC5C,eAAO,MAAM,gBAAgB,uBAAiB,CAAA;AAC9C,iCAAiC;AACjC,eAAO,MAAM,gBAAgB,qBAAe,CAAA;AAE5C,uEAAuE;AACvE,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAA;AA8BtC,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAwBnE"}
@@ -0,0 +1,32 @@
{
"name": "@deepseek-ai/cosmokit",
"description": "A collection of common utilities",
"version": "1.8.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "vendor/cosmokit"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT"
}
@@ -0,0 +1,42 @@
import { isNullable } from './misc.ts'
/** Return true when every item in `array2` is present in `array1`. */
export function contain(array1: readonly any[], array2: readonly any[]) {
return array2.every(item => array1.includes(item))
}
/** Return items that appear in both arrays. */
export function intersection<T>(array1: readonly T[], array2: readonly T[]) {
return array1.filter(item => array2.includes(item))
}
/** Return items from `array1` that do not appear in `array2`. */
export function difference<S>(array1: readonly S[], array2: readonly any[]) {
return array1.filter(item => !array2.includes(item))
}
/** Return the set-union of two arrays while preserving first occurrence order. */
export function union<T>(array1: readonly T[], array2: readonly T[]) {
return Array.from(new Set([...array1, ...array2]))
}
/** Remove duplicate values while preserving first occurrence order. */
export function deduplicate<T>(array: readonly T[]) {
return [...new Set(array)]
}
/** Remove one item from an array and report whether it was found. */
export function remove<T>(list: T[], item: T) {
const index = list?.indexOf(item)
if (index >= 0) {
list.splice(index, 1)
return true
} else {
return false
}
}
/** Normalize nullish, scalar, or array input to an array. */
export function makeArray<T>(source: null | undefined | T | T[]) {
return Array.isArray(source) ? source : isNullable(source) ? [] : [source]
}
@@ -0,0 +1,10 @@
/** Array set and normalization helpers. */
export * from './array.ts'
/** Runtime type, binary, clone, and equality helpers. */
export * from './types.ts'
/** Shared utility types and object helpers. */
export * from './misc.ts'
/** String case, path, and property formatting helpers. */
export * from './string.ts'
/** Time constants, parsing, and formatting helpers. */
export * from './time.ts'
@@ -0,0 +1,78 @@
/** String/symbol keyed dictionary type. */
export type Dict<T = any, K extends string | symbol = string> = { [key in K]: T }
/** Safely read `T[K]`, returning `never` when `K` is not a key of `T`. */
export type Get<T extends {}, K> = K extends keyof T ? T[K] : never
/** Conditional extraction helper with a configurable return type. */
export type Extract<S, T, U = S> = S extends T ? U : never
/** Accept a value or an array, unless the value is already an array type. */
export type MaybeArray<T> = [T] extends [unknown[]] ? T : T | T[]
/** Wrap a value in `Promise`, preserving the resolved type of existing promises. */
export type Promisify<T> = Promise<T extends Promise<infer S> ? S : T>
/** Accept a value or promise unless the value type is already promise-like. */
export type Awaitable<T> = [T] extends [Promise<unknown>] ? T : T | Promise<T>
/** Convert a union type to an intersection type. */
export type Intersect<U> = (U extends any ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never
/** No-op callback returning `undefined` at runtime and `any` at type level. */
export function noop(): any {}
/** Return true when a value is `null` or `undefined`. */
export function isNullable(value: any): value is null | undefined | void {
return value === null || value === undefined
}
/** Return true when a value is neither `null` nor `undefined`. */
export function isNonNullable<T>(value: T): value is NonNullable<T> {
return !isNullable(value)
}
/** Return true for non-array object values. */
export function isPlainObject(data: any) {
return data && typeof data === 'object' && !Array.isArray(data)
}
/** Filter object entries with a key type guard. */
export function filterKeys<T, K extends string, U extends K>(object: Dict<T, K>, filter: (key: K, value: T) => key is U): Dict<T, U>
/** Filter object entries with a boolean predicate. */
export function filterKeys<T, K extends string>(object: Dict<T, K>, filter: (key: K, value: T) => boolean): Dict<T, K>
/** Filter object entries and return a new object. */
export function filterKeys(object: {}, filter: (key: string, value: any) => boolean) {
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)))
}
/** Map object values while preserving the original key set. */
export function mapValues<U, T, K extends string>(object: Dict<T, K>, transform: (value: T, key: K) => U) {
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, (transform as any)(value, key)])) as Dict<U, K>
}
/** Alias for `mapValues`. */
export { mapValues as valueMap }
/** Pick selected keys from an object, optionally including `undefined` values. */
export function pick<T extends object, K extends keyof T>(source: T, keys?: Iterable<K>, forced?: boolean) {
if (!keys) return { ...source }
const result = {} as Pick<T, K>
for (const key of keys) {
if (forced || source[key] !== undefined) result[key] = source[key]
}
return result
}
/** Omit selected keys from a shallow object copy. */
export function omit<T, K extends keyof T>(source: T, keys?: Iterable<K>) {
if (!keys) return { ...source }
const result = { ...source } as Omit<T, K>
for (const key of keys) {
Reflect.deleteProperty(result, key)
}
return result
}
/** Define a non-enumerable writable property with a typed key. */
export function defineProperty<T, K extends keyof T>(object: T, key: K, value: T[K]): T
/** Define a non-enumerable writable property with an arbitrary key. */
export function defineProperty<T, K extends keyof any>(object: T, key: K, value: any): T
/** Define a non-enumerable writable property and return the object. */
export function defineProperty<T, K extends keyof any>(object: T, key: K, value: any) {
return Object.defineProperty(object, key, { writable: true, value, enumerable: false })
}
@@ -0,0 +1,113 @@
/** Uppercase the first character of a string. */
export function capitalize(source: string) {
return source.charAt(0).toUpperCase() + source.slice(1)
}
/** Lowercase the first character of a string. */
export function uncapitalize(source: string) {
return source.charAt(0).toLowerCase() + source.slice(1)
}
/** Convert dash or underscore delimited text to camelCase. */
export function camelCase(source: string) {
return source.replace(/[_-][a-z]/g, str => str.slice(1).toUpperCase())
}
const enum State {
DELIM,
UPPER,
LOWER,
}
function tokenize(source: string, delimiters: number[], delimiter: number) {
const output: number[] = []
let state = State.DELIM
for (let i = 0; i < source.length; i++) {
const code = source.charCodeAt(i)
if (code >= 65 && code <= 90) {
if (state === State.UPPER) {
const next = source.charCodeAt(i + 1)
if (next >= 97 && next <= 122) {
output.push(delimiter)
}
output.push(code + 32)
} else {
if (state !== State.DELIM) {
output.push(delimiter)
}
output.push(code + 32)
}
state = State.UPPER
} else if (code >= 97 && code <= 122) {
output.push(code)
state = State.LOWER
} else if (delimiters.includes(code)) {
if (state !== State.DELIM) {
output.push(delimiter)
}
state = State.DELIM
} else {
output.push(code)
}
}
return String.fromCharCode(...output)
}
/** Convert text to dash-delimited parameter case. */
export function paramCase(source: string) {
return tokenize(source, [45, 95], 45)
}
/** Convert text to underscore-delimited snake case. */
export function snakeCase(source: string) {
return tokenize(source, [45, 95], 95)
}
/** Runtime alias for `camelCase`. */
export const camelize = camelCase
/** Runtime alias for `paramCase`. */
export const hyphenate = paramCase
namespace Letter {
/* eslint-disable @typescript-eslint/member-delimiter-style */
interface LowerToUpper {
a: 'A', b: 'B', c: 'C', d: 'D', e: 'E', f: 'F', g: 'G', h: 'H', i: 'I', j: 'J', k: 'K', l: 'L', m: 'M',
n: 'N', o: 'O', p: 'P', q: 'Q', r: 'R', s: 'S', t: 'T', u: 'U', v: 'V', w: 'W', x: 'X', y: 'Y', z: 'Z',
}
interface UpperToLower {
A: 'a', B: 'b', C: 'c', D: 'd', E: 'e', F: 'f', G: 'g', H: 'h', I: 'i', J: 'j', K: 'k', L: 'l', M: 'm',
N: 'n', O: 'o', P: 'p', Q: 'q', R: 'r', S: 's', T: 't', U: 'u', V: 'v', W: 'w', X: 'x', Y: 'y', Z: 'z',
}
/* eslint-enable @typescript-eslint/member-delimiter-style */
export type Upper = keyof UpperToLower
export type Lower = keyof LowerToUpper
export type ToUpper<S extends string> = S extends Lower ? LowerToUpper[S] : S
export type ToLower<S extends string, P extends string = ''> = S extends Upper ? `${P}${UpperToLower[S]}` : S
}
/* eslint-disable @typescript-eslint/naming-convention */
/** Type-level conversion from dash-delimited text to camelCase. */
export type camelize<S extends string> = S extends `${infer L}-${infer M}${infer R}` ? `${L}${Letter.ToUpper<M>}${camelize<R>}` : S
/** Type-level conversion from camelCase text to dash-delimited text. */
export type hyphenate<S extends string> = S extends `${infer L}${infer R}` ? `${Letter.ToLower<L, '-'>}${hyphenate<R>}` : S
/* eslint-enable @typescript-eslint/naming-convention */
/** Format a property key as a JavaScript member access suffix. */
export function formatProperty(key: keyof any) {
if (typeof key !== 'string') return `[${key.toString()}]`
return /^[a-z_$][\w$]*$/i.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
}
/** Remove one trailing slash from a path string. */
export function trimSlash(source: string) {
return source.replace(/\/$/, '')
}
/** Ensure a path starts with `/` and has no trailing slash. */
export function sanitize(source: string) {
if (!source.startsWith('/')) source = '/' + source
return trimSlash(source)
}
@@ -0,0 +1,92 @@
/** Time constants plus parsing and formatting helpers. */
export namespace Time {
export const millisecond = 1
export const second = 1000
export const minute = second * 60
export const hour = minute * 60
export const day = hour * 24
export const week = day * 7
let timezoneOffset = new Date().getTimezoneOffset()
export function setTimezoneOffset(offset: number) {
timezoneOffset = offset
}
export function getTimezoneOffset() {
return timezoneOffset
}
export function getDateNumber(date: number | Date = new Date(), offset?: number) {
if (typeof date === 'number') date = new Date(date)
if (offset === undefined) offset = timezoneOffset
return Math.floor((date.valueOf() / minute - offset) / 1440)
}
export function fromDateNumber(value: number, offset?: number) {
const date = new Date(value * day)
if (offset === undefined) offset = timezoneOffset
return new Date(+date + offset * minute)
}
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('')}$`)
export function parseTime(source: string) {
const capture = timeRegExp.exec(source)
if (!capture) return 0
return (parseFloat(capture[1]) * week || 0)
+ (parseFloat(capture[2]) * day || 0)
+ (parseFloat(capture[3]) * hour || 0)
+ (parseFloat(capture[4]) * minute || 0)
+ (parseFloat(capture[5]) * second || 0)
}
export function parseDate(date: string) {
const parsed = parseTime(date)
if (parsed) {
date = Date.now() + parsed as any
} else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
date = `${new Date().toLocaleDateString()}-${date}`
} else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) {
date = `${new Date().getFullYear()}-${date}`
}
return date ? new Date(date) : new Date()
}
export function format(ms: number) {
const abs = Math.abs(ms)
if (abs >= day - hour / 2) {
return Math.round(ms / day) + 'd'
} else if (abs >= hour - minute / 2) {
return Math.round(ms / hour) + 'h'
} else if (abs >= minute - second / 2) {
return Math.round(ms / minute) + 'm'
} else if (abs >= second) {
return Math.round(ms / second) + 's'
}
return ms + 'ms'
}
export function toDigits(source: number, length = 2) {
return source.toString().padStart(length, '0')
}
export function template(template: string, time = new Date()) {
return template
.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))
}
}
@@ -0,0 +1,142 @@
import { isNullable } from './misc.ts'
type GlobalConstructorNames = keyof {
[K in keyof typeof globalThis as typeof globalThis[K] extends abstract new (...args: any) => any ? K : never]: K
}
/** Create a predicate for a global constructor name. */
export function is<K extends GlobalConstructorNames>(type: K): (value: any) => value is InstanceType<typeof globalThis[K]>
/** Test whether a value matches a global constructor name. */
export function is<K extends GlobalConstructorNames>(type: K, value: any): value is InstanceType<typeof globalThis[K]>
/** Test values using `instanceof` with a `toStringTag` fallback. */
export function is<K extends GlobalConstructorNames>(type: K, value?: any): any {
if (arguments.length === 1) return (value: any) => is(type, value)
return type in globalThis && value instanceof (globalThis[type] as any)
|| Object.prototype.toString.call(value).slice(8, -1) === type
}
function isArrayBufferLike(value: any): value is ArrayBufferLike {
return is('ArrayBuffer', value) || is('SharedArrayBuffer', value)
}
function isArrayBufferSource(value: any): value is Binary.Source {
return isArrayBufferLike(value) || ArrayBuffer.isView(value)
}
/** Binary source detection and base64/hex conversion helpers. */
export namespace Binary {
export type Source<T extends ArrayBufferLike = ArrayBufferLike> = T | ArrayBufferView<T>
export const is = isArrayBufferLike
export const isSource = isArrayBufferSource
export function fromSource<T extends ArrayBufferLike>(source: Source<T>): T {
if (ArrayBuffer.isView(source)) {
// https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer#answer-31394257
return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength) as T
} else {
return source
}
}
export function toBase64(source: 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)
}
export function fromBase64(source: string) {
if (typeof Buffer !== 'undefined') return fromSource(Buffer.from(source, 'base64'))
return Uint8Array.from(atob(source), c => c.charCodeAt(0))
}
export function toHex(source: 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('')
}
export function fromHex(source: string) {
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: number[] = []
for (let i = 0; i < hex.length; i += 2) {
buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16))
}
return Uint8Array.from(buffer).buffer
}
}
/** Decode a base64 string into binary data. */
export const base64ToArrayBuffer = Binary.fromBase64
/** Encode binary data as base64. */
export const arrayBufferToBase64 = Binary.toBase64
/** Decode a hex string into binary data. */
export const hexToArrayBuffer = Binary.fromHex
/** Encode binary data as hex. */
export const arrayBufferToHex = Binary.toHex
/** Deep-clone common JavaScript values while preserving prototypes. */
export function clone<T>(source: T): T
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
export function clone(source: any, refs = new Map<any, any>()) {
if (!source || typeof source !== 'object') return source
if (is('Date', source)) return new Date(source.valueOf())
if (is('RegExp', source)) return new RegExp(source.source, source.flags)
if (isArrayBufferLike(source)) return source.slice(0)
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength)
const cached = refs.get(source)
if (cached) return cached
if (Array.isArray(source)) {
const result: any[] = []
refs.set(source, result)
source.forEach((value, index) => {
result[index] = Reflect.apply(clone, null, [value, refs])
})
return result
}
const result = Object.create(Object.getPrototypeOf(source))
refs.set(source, result)
for (const key of Reflect.ownKeys(source)) {
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) }
if ('value' in descriptor) {
descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs])
}
Reflect.defineProperty(result, key, descriptor)
}
return result
}
/** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
export function deepEqual(a: any, b: any, strict?: boolean): boolean {
if (a === b) return true
if (!strict && isNullable(a) && isNullable(b)) return true
if (typeof a !== typeof b) return false
if (typeof a !== 'object') return false
if (!a || !b) return false
function check<T>(test: (x: any) => x is T, then: (a: T, b: T) => boolean) {
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : undefined
}
return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index])))
?? check(is('Date'), (a, b) => a.valueOf() === b.valueOf())
?? check(is('RegExp'), (a, b) => a.source === b.source && a.flags === b.flags)
?? check(isArrayBufferLike, (a, b) => {
if (a.byteLength !== b.byteLength) return false
const viewA = new Uint8Array(a)
const viewB = new Uint8Array(b)
for (let i = 0; i < viewA.length; i++) {
if (viewA[i] !== viewB[i]) return false
}
return true
})
?? Object.keys({ ...a, ...b }).every(key => deepEqual(a[key], b[key], strict))
}

Some files were not shown because too many files have changed in this diff Show More