Compare commits
4 Commits
d5d02f65c4
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 08df19bde6 | |||
| 5368da3be7 | |||
| ba25b13d33 | |||
| 58f6a2a67f |
@@ -16,22 +16,25 @@
|
|||||||
| **qBittorrent** | `192.168.1.200` (macvlan) | PT 下载,独立 IP |
|
| **qBittorrent** | `192.168.1.200` (macvlan) | PT 下载,独立 IP |
|
||||||
| **Windows** | `192.168.1.177` | 内网办公,SSH |
|
| **Windows** | `192.168.1.177` | 内网办公,SSH |
|
||||||
| **Spark** | `192.168.1.166` | Ubuntu 主机,本地 TUN 代理 |
|
| **Spark** | `192.168.1.166` | Ubuntu 主机,本地 TUN 代理 |
|
||||||
|
| **iPhone** | WG `10.0.0.3` | WireGuard 客户端,远程访问内网 |
|
||||||
|
| **Mac** | WG `10.0.0.4` | WireGuard 客户端,远程访问内网 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nginx SNI 分流 (`beijing-vps-stream.conf`)
|
## Nginx SNI 分流 (`北京vps-stream.conf`)
|
||||||
|
|
||||||
监听 `:443`,按 SNI 将流量分发到不同后端:
|
**端口 443 — 公网入口**,按 SNI 分发流量:
|
||||||
|
|
||||||
| SNI | 后端 | 端口 | 用途 |
|
| SNI | 后端 | 端口 | 用途 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `www.apple.com` | Xray interconn | :9443 | 反向代理隧道 |
|
| `www.apple.com` / `www.samsung.com` / `www.amd.com` / `www.asus.com` | Xray interconn | :9443 | 反向代理隧道(4 SNI leastPing 轮换) |
|
||||||
| `www.microsoft.com` | Xray mihomo_in | :9444 | Mihomo 客户端 → 搬瓦工出口 |
|
| `www.bilibili.com` | Xray proxy_from_lan | :9445 | 北京直连代理(qBittorrent 出站) |
|
||||||
| `news.apple.com` | Xray proxy_from_lan | :9445 | 北京直连代理 |
|
|
||||||
| `drive.salmonstill.cn` | NAS | :38653 | 绿联云直接转发 |
|
| `drive.salmonstill.cn` | NAS | :38653 | 绿联云直接转发 |
|
||||||
| 默认 | NPM | :8443 | Nginx Proxy Manager |
|
| 默认 | NPM | :8443 | Nginx Proxy Manager |
|
||||||
|
|
||||||
搬瓦工 VPS(部署 `东京-vps-stream.conf` + `tokyo-vps-config.json`):`www.microsoft.com` → Xray :9443,默认 → NPM。
|
**端口 9447 — 数据中心回落**:Reality 探针/探测流量回落到此,按 SNI 代理到真实网站。
|
||||||
|
|
||||||
|
搬瓦工 VPS(部署 `代理vps-stream.conf` + `xray-代理服务器-config.json`):`www.cloudflare.com` → Xray :9443,默认 → NPM。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,11 +44,12 @@
|
|||||||
|
|
||||||
**Reality 入站** — 共享 privateKey `GGT9LfN_...`,由 Nginx SNI 转发:
|
**Reality 入站** — 共享 privateKey `GGT9LfN_...`,由 Nginx SNI 转发:
|
||||||
|
|
||||||
| Tag | 端口 | SNI | Reality target |
|
| Tag | 端口 | serverNames | Reality target | 说明 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `interconn` | :9443 | `www.apple.com` | `www.apple.com:443` |
|
| `interconn` | :9443 | apple/samsung/amd/asus 4 SNI leastPing 轮换 | `127.0.0.1:9447` | 反向隧道,多 SNI 防封 + 数据中心回落 |
|
||||||
| `mihomo_in` | :9444 | `www.microsoft.com` | `www.microsoft.com:443` |
|
| `proxy_from_lan` | :9445 | `www.bilibili.com` | `www.bilibili.com:443` | 北京直连代理(qBittorrent) |
|
||||||
| `proxy_from_lan` | :9445 | `news.apple.com` | `www.apple.com:443` |
|
|
||||||
|
> `mihomo_in`(旧 Mihomo 中转)已删除,客户端直接连接搬瓦工。
|
||||||
|
|
||||||
**dokodemo-door 入站** — 端口转发到旁路由反向隧道,共 11 个:
|
**dokodemo-door 入站** — 端口转发到旁路由反向隧道,共 11 个:
|
||||||
|
|
||||||
@@ -71,12 +75,13 @@
|
|||||||
|
|
||||||
| Tag | 端口 | 类型 | 用途 |
|
| Tag | 端口 | 类型 | 用途 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `proxy_in` | 10809 | mixed | SOCKS5/HTTP 代理 → 搬瓦工出口 |
|
|
||||||
| `socks-dynamic` | 38658 | SOCKS5 (password) | 动态 LAN 端口访问 |
|
| `socks-dynamic` | 38658 | SOCKS5 (password) | 动态 LAN 端口访问 |
|
||||||
|
|
||||||
**出站**:`direct`(freedom 直连)、`to_tokyo`(VLESS+Reality → `67.230.189.235:443`)
|
**出站**:`direct`(freedom 直连)
|
||||||
|
|
||||||
**路由**:所有 `external_*` + `interconn` + `socks-dynamic` → `portal`(反向隧道);`mihomo_in` + `proxy_in` → `to_tokyo`;`proxy_from_lan` → `direct`
|
**路由**:所有 `external_*` + `interconn` + `socks-dynamic` → `portal`(反向隧道);`interconn` 探测流量(baidu.com)→ `direct`;`proxy_from_lan` → `direct`
|
||||||
|
|
||||||
|
> `mihomo_in` + `proxy_in` + `to_tokyo` 旧中转链路(北京→搬瓦工),已删除。
|
||||||
|
|
||||||
### 旁路由 (`xray-旁路由-config.json`)
|
### 旁路由 (`xray-旁路由-config.json`)
|
||||||
|
|
||||||
@@ -100,8 +105,8 @@
|
|||||||
| `to_spark_ssh` | `192.168.1.166:22` | Spark SSH 远程 |
|
| `to_spark_ssh` | `192.168.1.166:22` | Spark SSH 远程 |
|
||||||
| `to_spark_rdp` | `192.168.1.166:3389` | Spark xRDP 桌面远程 |
|
| `to_spark_rdp` | `192.168.1.166:3389` | Spark xRDP 桌面远程 |
|
||||||
| `to_spark_38662` | `192.168.1.166:38662` | Spark 自定义服务 |
|
| `to_spark_38662` | `192.168.1.166:38662` | Spark 自定义服务 |
|
||||||
| `interconn` | VLESS+Reality → `salmonstill.cn:443` (SNI=www.apple.com) | 隧道链接 |
|
| `interconn-apple/samsung/mozilla` | VLESS+Reality → `salmonstill.cn:443` (多 SNI 轮换) | 隧道链接(balancer + leastPing + fallback) |
|
||||||
| `to_beijing_direct` | VLESS+Reality → `salmonstill.cn:443` (SNI=news.apple.com) | 北京直连 |
|
| `to_beijing_direct` | VLESS+Reality → `salmonstill.cn:443` (SNI=www.bilibili.com) | 北京直连 |
|
||||||
| `direct` | freedom | 直连 |
|
| `direct` | freedom | 直连 |
|
||||||
|
|
||||||
**路由规则**(bridge 入站按端口匹配):
|
**路由规则**(bridge 入站按端口匹配):
|
||||||
@@ -127,9 +132,9 @@
|
|||||||
|
|
||||||
> 所有显式端口规则优先于 catch-all,现有端口转发不受影响。
|
> 所有显式端口规则优先于 catch-all,现有端口转发不受影响。
|
||||||
|
|
||||||
### 搬瓦工 VPS / 洛杉矶 (`tokyo-vps-config.json`)
|
### 搬瓦工 VPS (`xray-代理服务器-config.json`)
|
||||||
|
|
||||||
极简:VLESS+Reality 入站 `:9443`(SNI=www.microsoft.com),freedom 出站。
|
极简:VLESS+Reality 入站 `:9443`(SNI=www.cloudflare.com, dest=www.cloudflare.com),freedom 出站。
|
||||||
所有客户端(Spark、旁路由、macOS)直连此服务器,不再经北京 VPS 中转。
|
所有客户端(Spark、旁路由、macOS)直连此服务器,不再经北京 VPS 中转。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -149,7 +154,9 @@
|
|||||||
配合 **fake-ip DNS**(`enhanced-mode: fake-ip`),DNS 请求返回 `198.18.x.x` 假 IP,强制流量进入代理路由。
|
配合 **fake-ip DNS**(`enhanced-mode: fake-ip`),DNS 请求返回 `198.18.x.x` 假 IP,强制流量进入代理路由。
|
||||||
|
|
||||||
**代理节点**:
|
**代理节点**:
|
||||||
- `US-Direct` — VLESS+Reality → `67.230.189.235:443`(默认国外出口,客户端直连搬瓦工)
|
- `US-Direct` — VLESS+Reality → `67.230.189.235:443`(默认国外出口,客户端直连搬瓦工,SNI=www.cloudflare.com)
|
||||||
|
|
||||||
|
> `Xray-Real` 代理节点(经北京 VPS 中转)已移除,Mihomo 全部直连搬瓦工。
|
||||||
|
|
||||||
**关键策略组**:
|
**关键策略组**:
|
||||||
- 国外 → `[US-Direct, 直连]`
|
- 国外 → `[US-Direct, 直连]`
|
||||||
@@ -183,7 +190,7 @@ launchctl load ~/Library/LaunchAgents/com.mihomo.proxy.plist
|
|||||||
qBittorrent(192.168.1.200) 互联网
|
qBittorrent(192.168.1.200) 互联网
|
||||||
↓ SOCKS5 旁路由:1080 (socks-lan) ↑
|
↓ SOCKS5 旁路由:1080 (socks-lan) ↑
|
||||||
↓ Xray routing → to_beijing_direct │
|
↓ Xray routing → to_beijing_direct │
|
||||||
↓ Reality(VLESS, SNI=news.apple.com) │
|
↓ Reality(VLESS, SNI=www.bilibili.com) │
|
||||||
↓ 北京VPS:443 → Nginx分流 │
|
↓ 北京VPS:443 → Nginx分流 │
|
||||||
↓ proxy_from_lan:9445 → direct(freedom) ─────────────────┘
|
↓ proxy_from_lan:9445 → direct(freedom) ─────────────────┘
|
||||||
|
|
||||||
@@ -209,14 +216,14 @@ Peer → 北京VPS:51413 → external_qbit → portal
|
|||||||
|
|
||||||
#### 1.1 Nginx Stream SNI 分流 `beijing-vps-stream.conf`
|
#### 1.1 Nginx Stream SNI 分流 `beijing-vps-stream.conf`
|
||||||
|
|
||||||
新增 `news.apple.com` SNI 映射(用于旁路由 Mihomo 直连代理):
|
新增 `www.bilibili.com` SNI 映射(用于旁路由 Mihomo 直连代理):
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
stream {
|
stream {
|
||||||
map $ssl_preread_server_name $backend {
|
map $ssl_preread_server_name $backend {
|
||||||
www.apple.com xray; # 旁路由反向代理隧道
|
www.apple.com xray; # 旁路由反向代理隧道
|
||||||
www.microsoft.com mihomo; # 外部客户端代理 → 东京出口
|
www.microsoft.com mihomo; # 外部客户端代理 → 东京出口
|
||||||
news.apple.com xray_lan; # 旁路由 Mihomo 北京直连代理
|
www.bilibili.com xray_lan; # 旁路由 Mihomo 北京直连代理
|
||||||
drive.salmonstill.cn nas; # 绿联云服务
|
drive.salmonstill.cn nas; # 绿联云服务
|
||||||
default npm; # Nginx Proxy Manager
|
default npm; # Nginx Proxy Manager
|
||||||
}
|
}
|
||||||
@@ -260,7 +267,7 @@ stream {
|
|||||||
"realitySettings": {
|
"realitySettings": {
|
||||||
"show": false,
|
"show": false,
|
||||||
"target": "www.apple.com:443",
|
"target": "www.apple.com:443",
|
||||||
"serverNames": ["news.apple.com"],
|
"serverNames": ["www.bilibili.com"],
|
||||||
"privateKey": "GGT9LfN_2JdQG68cwrULgUK-adfT6wIokLzWjaB0fXs",
|
"privateKey": "GGT9LfN_2JdQG68cwrULgUK-adfT6wIokLzWjaB0fXs",
|
||||||
"shortIds": ["7c947a71b94f369e"]
|
"shortIds": ["7c947a71b94f369e"]
|
||||||
}
|
}
|
||||||
@@ -268,7 +275,7 @@ stream {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> Reality 公私钥复用已有的 `interconn` 入站 keypair,`serverNames` 用新的 `news.apple.com` 与 Nginx 对应。
|
> Reality 公私钥复用已有的 `interconn` 入站 keypair,`serverNames` 用新的 `www.bilibili.com` 与 Nginx 对应。
|
||||||
> `target` 设为 `www.apple.com:443`,Reality 从此地址偷取真实 TLS 证书用于伪装。
|
> `target` 设为 `www.apple.com:443`,Reality 从此地址偷取真实 TLS 证书用于伪装。
|
||||||
|
|
||||||
##### 新增入站 `external_qbit`(BT 入站端口)
|
##### 新增入站 `external_qbit`(BT 入站端口)
|
||||||
@@ -353,7 +360,7 @@ qBittorrent 出站不走 Mihomo 透明代理,而是通过旁路由上 Xray 的
|
|||||||
"security": "reality",
|
"security": "reality",
|
||||||
"realitySettings": {
|
"realitySettings": {
|
||||||
"fingerprint": "chrome",
|
"fingerprint": "chrome",
|
||||||
"serverName": "news.apple.com",
|
"serverName": "www.bilibili.com",
|
||||||
"publicKey": "62y5gDjPrdeuePGl-D2IW4Cw9Kb8_bSBBTmArvL7Nhs",
|
"publicKey": "62y5gDjPrdeuePGl-D2IW4Cw9Kb8_bSBBTmArvL7Nhs",
|
||||||
"shortId": "7c947a71b94f369e"
|
"shortId": "7c947a71b94f369e"
|
||||||
}
|
}
|
||||||
@@ -367,7 +374,7 @@ qBittorrent 出站不走 Mihomo 透明代理,而是通过旁路由上 Xray 的
|
|||||||
{ "type": "field", "inboundTag": ["socks-lan"], "outboundTag": "to_beijing_direct" }
|
{ "type": "field", "inboundTag": ["socks-lan"], "outboundTag": "to_beijing_direct" }
|
||||||
```
|
```
|
||||||
|
|
||||||
> 路径:`qBittorrent → SOCKS5 旁路由:1080 → to_beijing_direct → 北京VPS:443(SNI=news.apple.com) → proxy_from_lan:9445 → direct → 互联网`
|
> 路径:`qBittorrent → SOCKS5 旁路由:1080 → to_beijing_direct → 北京VPS:443(SNI=www.bilibili.com) → proxy_from_lan:9445 → direct → 互联网`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -535,10 +542,160 @@ nc -zv salmonstill.cn 51413
|
|||||||
|
|
||||||
| 文件 | 位置 | 作用 |
|
| 文件 | 位置 | 作用 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `beijing-vps-stream.conf` | 北京VPS `/etc/nginx/stream.conf.d/` | Nginx SNI分流(含 news.apple.com → 9445) |
|
| `beijing-vps-stream.conf` | 北京VPS `/etc/nginx/stream.conf.d/` | Nginx SNI分流(含 www.bilibili.com → 9445) |
|
||||||
| `xray-北京vps-config.json` | 北京VPS `/usr/local/etc/xray/config.json` | Xray 入站+路由(含 proxy_from_lan + external_qbit) |
|
| `xray-北京vps-config.json` | 北京VPS `/usr/local/etc/xray/config.json` | Xray 入站+路由(含 proxy_from_lan + external_qbit) |
|
||||||
| `xray-旁路由-config.json` | 旁路由 `/etc/xray/config.json` | Xray bridge + to_qbit(51413) + socks-lan(1080) → to_beijing_direct |
|
| `xray-旁路由-config.json` | 旁路由 `/etc/xray/config.json` | Xray bridge + to_qbit(51413) + socks-lan(1080) → to_beijing_direct |
|
||||||
|
|
||||||
|
## WireGuard 内网穿透(iPhone / Mac 远程访问内网)
|
||||||
|
|
||||||
|
复用已有的 WireGuard 隧道,让外部 iPhone / Mac 通过北京 VPS 中继,访问家里整个 `192.168.1.0/24` 内网的**任意端口**(无需逐个配置端口规则)。
|
||||||
|
|
||||||
|
### 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
iPhone (WireGuard App, 10.0.0.3)
|
||||||
|
Mac (WireGuard, 10.0.0.4)
|
||||||
|
↓ WG 隧道 (UDP 51820)
|
||||||
|
北京VPS 49.232.242.90 (wg0, 10.0.0.1/24)
|
||||||
|
↓ 转发 (iptables FORWARD ACCEPT)
|
||||||
|
旁路由 192.168.1.199 (10.0.0.2, penetration zone)
|
||||||
|
↓ 转发 + SNAT (→ 192.168.1.199)
|
||||||
|
整个 192.168.1.0/24 任意端口可达 (如 Spark 192.168.1.166:8081 Expo dev server)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 虚拟网段 IP 分配
|
||||||
|
|
||||||
|
| 设备 | WG IP | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 北京 VPS | `10.0.0.1/24` | WG 服务端,监听 `:51820` |
|
||||||
|
| 旁路由 | `10.0.0.2/24` | 子网代理节点,进 LAN |
|
||||||
|
| iPhone | `10.0.0.3/32` | WireGuard App |
|
||||||
|
| Mac | `10.0.0.4/32` | WireGuard |
|
||||||
|
|
||||||
|
### 北京 VPS 配置 (`/etc/wireguard/wg0.conf`)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
Address = 10.0.0.1/24
|
||||||
|
ListenPort = 51820
|
||||||
|
PrivateKey = <北京VPS私钥>
|
||||||
|
MTU = 1420
|
||||||
|
|
||||||
|
PostUp = iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
|
||||||
|
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
|
||||||
|
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
|
||||||
|
PostDown = iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
|
||||||
|
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
|
||||||
|
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
# 旁路由(子网代理,带 192.168.1.0/24)
|
||||||
|
PublicKey = <旁路由公钥>
|
||||||
|
AllowedIPs = 10.0.0.2/32, 192.168.1.0/24
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
# iPhone
|
||||||
|
PublicKey = <iPhone公钥>
|
||||||
|
AllowedIPs = 10.0.0.3/32
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
# Mac
|
||||||
|
PublicKey = <Mac公钥>
|
||||||
|
AllowedIPs = 10.0.0.4/32
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **关键坑:iptables FORWARD policy DROP**。VPS 装过 Docker 后,`iptables` 的 FORWARD 链默认策略会变成 `DROP`,导致客户端流量穿透 VPS 转发时被丢弃(症状:`ping 10.0.0.1` 通、`ping 10.0.0.2` 不通)。必须在 PostUp 加 `-i wg0` / `-o wg0` 两条 ACCEPT。
|
||||||
|
> 查看规则用 `iptables -L FORWARD -n -v`(**必须带 `-v`**,否则不显示 In/Out 接口列,`grep wg0` 会以为规则不存在)。
|
||||||
|
|
||||||
|
### 旁路由配置(ImmortalWrt LuCI)
|
||||||
|
|
||||||
|
#### 1. 新建 `penetration` zone
|
||||||
|
|
||||||
|
网络 → 防火墙 → General Settings → 添加:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| Name | `penetration` |
|
||||||
|
| Input | `accept` |
|
||||||
|
| Output | `accept` |
|
||||||
|
| Forward | `accept` |
|
||||||
|
| Covered networks | `WireGuard` |
|
||||||
|
| Allow forward to destination zones | `lan` |
|
||||||
|
|
||||||
|
同时把 WG 接口从 `wan` zone 的 **Covered networks 取消勾选**(一个接口只能属于一个 zone)。
|
||||||
|
|
||||||
|
#### 2. 新增通用 SNAT 规则(关键)
|
||||||
|
|
||||||
|
网络 → 防火墙 → NAT 规则 → 添加:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| Name | `penetration-snat` |
|
||||||
|
| 地址族限制 | 仅 IPv4 |
|
||||||
|
| 协议 | Any |
|
||||||
|
| 出站区域 | `lan` |
|
||||||
|
| 源地址 | `10.0.0.0/24` |
|
||||||
|
| 目标地址 | any |
|
||||||
|
| 操作 | `SNAT` → 重写 IP `192.168.1.199` |
|
||||||
|
|
||||||
|
生成规则:`ip saddr 10.0.0.0/24 ... snat ip to 192.168.1.199`
|
||||||
|
|
||||||
|
> ⚠️ **关键坑:penetration zone 勾 masq 无效**。ImmortalWrt (fw4) 的 masquerade 按"出接口"触发,隧道流量转发进 LAN 时出接口是 `br-lan`(属于 lan zone),不会进 `srcnat_penetration`。必须显式加 SNAT 规则,否则 166 等设备的回包发给主路由(192.168.1.1)而丢失(症状:`ping 10.0.0.2` 通、`ping 192.168.1.166` 不通)。
|
||||||
|
|
||||||
|
#### 3. Minecraft 端口转发联动
|
||||||
|
|
||||||
|
因为 WG 接口从 `wan` 移到了 `penetration`,Minecraft 那条 `Minecraft-udp` 端口转发的 **Source zone 要从 `wan` 改成 `penetration`**(玩家流量实际也走 WG 隧道进来),否则 Minecraft 会断。
|
||||||
|
|
||||||
|
### 客户端配置
|
||||||
|
|
||||||
|
#### iPhone(海外区 App Store 官方 WireGuard)
|
||||||
|
|
||||||
|
```
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = <iPhone私钥>
|
||||||
|
Address = 10.0.0.3/32
|
||||||
|
DNS = 223.5.5.5
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <北京VPS服务端公钥>
|
||||||
|
Endpoint = 49.232.242.90:51820
|
||||||
|
AllowedIPs = 10.0.0.0/24, 192.168.1.0/24
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mac
|
||||||
|
|
||||||
|
同上,`Address = 10.0.0.4/32`,PrivateKey 换成 Mac 的。
|
||||||
|
|
||||||
|
> AllowedIPs 只含虚拟网段和内网网段 → split tunnel,仅内网流量走隧道,不影响正常上网。
|
||||||
|
|
||||||
|
### 验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 北京 VPS 上:确认 3 个 peer 都有握手
|
||||||
|
wg show
|
||||||
|
|
||||||
|
# 客户端设备上:三层诊断(定位故障层级)
|
||||||
|
ping -c 3 10.0.0.1 # ① 隧道到 VPS —— 通说明客户端配置对
|
||||||
|
ping -c 3 10.0.0.2 # ② 隧道到旁路由 —— 通说明 VPS 转发对
|
||||||
|
ping -c 3 192.168.1.166 # ③ 进内网 —— 通说明旁路由转发+SNAT 对
|
||||||
|
|
||||||
|
# 旁路由上确认 SNAT 规则
|
||||||
|
nft list ruleset | grep -iE "penetration-snat|10.0.0.0/24"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 排障对照表
|
||||||
|
|
||||||
|
| 现象 | 原因 | 修复 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ping 10.0.0.1` 通、`ping 10.0.0.2` 不通 | VPS iptables FORWARD policy DROP | wg0.conf PostUp 加 `-i wg0` / `-o wg0` ACCEPT |
|
||||||
|
| `ping 10.0.0.2` 通、`ping 192.168.1.166` 不通 | 旁路由缺通用 SNAT | 新增 `penetration-snat`(SNAT → 192.168.1.199) |
|
||||||
|
| 旁路由自己 ping 166 通,但客户端不通 | 同上 | 同上 |
|
||||||
|
| `iptables -L FORWARD -n \| grep wg0` 为空 | 没带 `-v`,接口列不显示 | 用 `iptables -L FORWARD -n -v` |
|
||||||
|
|
||||||
## Minecraft UDP 转发
|
## Minecraft UDP 转发
|
||||||
|
|
||||||
### 架构
|
### 架构
|
||||||
@@ -805,7 +962,7 @@ sudo tcpdump -i bridge0 udp port 19132 -n
|
|||||||
| **北京 VPS**(3 个入站共用) | `GGT9LfN_2JdQG68cwrULgUK-adfT6wIokLzWjaB0fXs` | `62y5gDjPrdeuePGl-D2IW4Cw9Kb8_bSBBTmArvL7Nhs` |
|
| **北京 VPS**(3 个入站共用) | `GGT9LfN_2JdQG68cwrULgUK-adfT6wIokLzWjaB0fXs` | `62y5gDjPrdeuePGl-D2IW4Cw9Kb8_bSBBTmArvL7Nhs` |
|
||||||
| **搬瓦工 VPS**(客户端直连) | `iBlu3eH1VLf1S5Qw87m_1w0TGYUktDwHAzgpQ2aKuGI` | `jr_zQjC4mvlQITuG5Ap5Mxqe5EBbGyyvwbVLDEi8OCA` |
|
| **搬瓦工 VPS**(客户端直连) | `iBlu3eH1VLf1S5Qw87m_1w0TGYUktDwHAzgpQ2aKuGI` | `jr_zQjC4mvlQITuG5Ap5Mxqe5EBbGyyvwbVLDEi8OCA` |
|
||||||
|
|
||||||
`Beijing-Direct` 节点必须 `skip-cert-verify: true`,因 Reality 返回 target(`www.apple.com`)的证书,而 SNI 是 `news.apple.com`,TLS SAN 校验会失败。
|
`Beijing-Direct` 节点必须 `skip-cert-verify: true`,因 Reality 返回 target(`www.bilibili.com`)的证书,而 SNI 是 `www.bilibili.com`,TLS SAN 校验可能失败。
|
||||||
|
|
||||||
### Reality 故障排查
|
### Reality 故障排查
|
||||||
|
|
||||||
@@ -840,7 +997,7 @@ Mihomo 客户端 → 67.230.189.235:443 (SNI=www.cloudflare.com)
|
|||||||
### 路径 3:qBittorrent 代理出站
|
### 路径 3:qBittorrent 代理出站
|
||||||
```
|
```
|
||||||
qBittorrent → SOCKS5 旁路由:1080 → Xray to_beijing_direct
|
qBittorrent → SOCKS5 旁路由:1080 → Xray to_beijing_direct
|
||||||
→ salmonstill.cn:443 (SNI=news.apple.com) → proxy_from_lan → direct → 互联网
|
→ salmonstill.cn:443 (SNI=www.bilibili.com) → proxy_from_lan → direct → 互联网
|
||||||
```
|
```
|
||||||
|
|
||||||
### 路径 4:BT 入站
|
### 路径 4:BT 入站
|
||||||
@@ -862,15 +1019,15 @@ BT Peer → salmonstill.cn:51413 → portal → 隧道 → bridge → to_qbit
|
|||||||
|
|
||||||
|
|
||||||
| 文件 | 目标设备 | 部署路径 |
|
| 文件 | 目标设备 | 部署路径 |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `beijing-vps-stream.conf` | 北京 VPS | `/etc/nginx/stream.conf.d/` |
|
| `北京vps-stream.conf` | 北京 VPS | `/etc/nginx/nginx.conf` 的 `stream {}` 块 |
|
||||||
| `xray-北京vps-config.json` | 北京 VPS | `/usr/local/etc/xray/config.json` |
|
| `xray-北京vps-config.json` | 北京 VPS | `/usr/local/etc/xray/config.json` |
|
||||||
| `东京-vps-stream.conf` | 搬瓦工 VPS | `/etc/nginx/stream.conf.d/` |
|
| `代理vps-stream.conf` | 搬瓦工 VPS | `/etc/nginx/nginx.conf` 的 `stream {}` 块 |
|
||||||
| `tokyo-vps-config.json` | 搬瓦工 VPS | `/usr/local/etc/xray/config.json` |
|
| `xray-代理服务器-config.json` | 搬瓦工 VPS | `/usr/local/etc/xray/config.json` |
|
||||||
| `xray-旁路由-config.json` | 旁路由 | `/etc/xray/config.json` |
|
| `xray-旁路由-config.json` | 旁路由 | `/etc/xray/config.json` |
|
||||||
| `旁路由的mihomo config.yaml` | 旁路由 | `/opt/mihomo/config.yaml` |
|
| `旁路由的mihomo config.yaml` | 旁路由 | `/opt/mihomo/config.yaml` |
|
||||||
| `非tun模式的主机mihomo config.yaml` | macOS | `~/Library/LaunchAgents/` (launchctl 管理) |
|
| `非tun模式的主机mihomo config.yaml` | macOS | `~/Library/Application Support/mihomo/config.yaml` (launchctl 管理) |
|
||||||
| `tun模式的主机mihomo config.yaml` | Spark (Ubuntu) | `/opt/mihomo/config.yaml` (systemd) |
|
| `tun模式的主机mihomo config.yaml` | Spark (Ubuntu) | `/opt/mihomo/config.yaml`
|
||||||
| `subscribe-7891-only.yaml` | 通用 | 精简版模板(单独 7891 端口) |
|
| `subscribe-7891-only.yaml` | 通用 | 精简版模板(单独 7891 端口) |
|
||||||
| `qbittorrent流量转发.md` | — | 方案文档(已合并到 README,可删除) |
|
| `qbittorrent流量转发.md` | — | 方案文档(已合并到 README,可删除) |
|
||||||
| `我的世界udp转发.md` | — | 方案文档(已合并到 README,可删除) |
|
| `我的世界udp转发.md` | — | 方案文档(已合并到 README,可删除) |
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# dsh-local-403-fix
|
||||||
|
|
||||||
|
修复 `dsh web` 反向代理访问时特权 `/api` 方法返回 **403** 的问题。
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
官方 `@deepseek-ai/dsh-client-connection` 把 15 个特权方法(`settings.*`、`credentials.*`、
|
||||||
|
`agentPreset.*`、`host.pickDirectory/openPath`、`llm.discoverModels`)钉死在 **loopback**:
|
||||||
|
前缀路由先用配置的 `trustedHosts` 校验,但进入 fetch handler 后,这些方法会**用空列表
|
||||||
|
再校验一次**:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// dsh-client-connection@0.1.0-rc.7 lib/index.js 第 538 行
|
||||||
|
if (method !== void 0 && PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, []))
|
||||||
|
return new Response("forbidden", { status: 403 });
|
||||||
|
```
|
||||||
|
|
||||||
|
因此即使 `--trusted-host` 配置了公网域名/IP,任何经过反向代理(本环境为
|
||||||
|
Xray 隧道 + socat)到达的请求,只要 `Host` 头不是 loopback,特权方法一律 403。
|
||||||
|
这是官方刻意设计(配置/凭据平面在"真正的认证层出现之前"保持 loopback 同源),
|
||||||
|
上游 master 至今未改(`packages/client/connection/src/index.ts` 第 147 行)。
|
||||||
|
|
||||||
|
## 方案(本目录采用一行补丁,替代旧的 260 行插件)
|
||||||
|
|
||||||
|
对运行时实际加载的全局包打一行补丁,把特权检查的空列表换成配置的 `trustedHosts`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- if (method !== void 0 && PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, [])) return new Response("forbidden", { status: 403 });
|
||||||
|
+ if (method !== void 0 && PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, trustedHosts)) return new Response("forbidden", { status: 403 });
|
||||||
|
```
|
||||||
|
|
||||||
|
语义与上游一致:`trustedHosts` 仍是 DNS-rebinding 围栏,不是认证层。
|
||||||
|
`trustedHosts` 变量在 `apply()` 作用域内(第 531 行),补丁无需额外改动。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
| 文件 | 作用 |
|
||||||
|
|---|---|
|
||||||
|
| `patches/@deepseek-ai+dsh-client-connection@0.1.0-rc.7.patch` | 标准 pnpm 补丁(进 git,可复现) |
|
||||||
|
| `apply-403-fix.sh` | 幂等脚本:定位全局包 → 备份 → `patch -p1` 应用/回滚/检查 |
|
||||||
|
| `package.json` | `pnpm.patchedDependencies` 声明(工程内 pnpm install 时自动应用) |
|
||||||
|
| `cordis.patch.yml` + `src/index.js` | ⚠️ 旧方案(260 行插件 bundle),已由本补丁替代,保留仅供回退参考 |
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用补丁(幂等;my_deepseek.sh 启动前会自动调用)
|
||||||
|
./apply-403-fix.sh
|
||||||
|
|
||||||
|
# 检查状态
|
||||||
|
./apply-403-fix.sh --check
|
||||||
|
|
||||||
|
# 回滚
|
||||||
|
./apply-403-fix.sh --revert
|
||||||
|
```
|
||||||
|
|
||||||
|
应用后重启 dsh 生效:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
my_deepseek.sh stop && my_deepseek.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 为什么不用 pnpm patchedDependencies 直接生效?
|
||||||
|
|
||||||
|
`pnpm.patchedDependencies` 只对**当前 pnpm 工程的依赖树**生效;而本环境的 dsh 是
|
||||||
|
**全局 npm 安装**(`/home/salmonstill/.nvm/.../lib/node_modules/@deepseek-ai/dsh`),
|
||||||
|
运行时加载的是全局那份 `dsh-client-connection`。所以工程内保留标准补丁文件 +
|
||||||
|
`apply-403-fix.sh` 幂等打到全局包,两者兼顾可复现与运行时生效。
|
||||||
|
将来若把 dsh 改为工程内安装,`patchedDependencies` 声明可直接复用。
|
||||||
|
|
||||||
|
## 升级 dsh 后
|
||||||
|
|
||||||
|
- `apply-403-fix.sh --check` 会显示 `unknown`(特权检查行已变),此时需对照新版
|
||||||
|
源码更新补丁文件;
|
||||||
|
- 补丁文件内的 blob hash 需用 `git hash-object` 重新计算(见 git 历史或脚本注释)。
|
||||||
Executable
+124
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# apply-403-fix.sh — 幂等应用/回滚 dsh 403 修复补丁(方案 2)
|
||||||
|
#
|
||||||
|
# 背景:官方 dsh 把特权 /api 方法(settings.*、credentials.* 等)钉死在
|
||||||
|
# loopback,反向代理访问时即使配置了 --trusted-host 也返回 403。本脚本把
|
||||||
|
# 一行补丁打到运行时实际加载的全局 @deepseek-ai/dsh-client-connection 上,
|
||||||
|
# 让特权方法也使用配置的 trustedHosts 列表校验(trustedHosts 仍是
|
||||||
|
# DNS-rebinding 围栏,不是认证,语义与上游一致)。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# apply-403-fix.sh # 应用补丁(幂等,已应用则跳过)
|
||||||
|
# apply-403-fix.sh --check # 仅检查补丁状态,不修改
|
||||||
|
# apply-403-fix.sh --revert # 从备份回滚补丁
|
||||||
|
# apply-403-fix.sh --help # 帮助
|
||||||
|
#
|
||||||
|
# 说明:
|
||||||
|
# - 通过 `readlink -f $(which dsh)` 定位全局 dsh 包根,不依赖硬编码路径
|
||||||
|
# - 应用前自动备份为 lib/index.js.orig
|
||||||
|
# - 补丁文件在工程内维护:patches/@deepseek-ai+dsh-client-connection@0.1.0-rc.7.patch
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PATCH_FILE="$SCRIPT_DIR/patches/@deepseek-ai+dsh-client-connection@0.1.0-rc.7.patch"
|
||||||
|
|
||||||
|
# 期望补丁后特权检查行包含 trustedHosts(而非空列表)
|
||||||
|
PATCHED_MARKER='PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, trustedHosts)'
|
||||||
|
ORIGINAL_MARKER='PRIVILEGED_METHODS.has(method) && !isTrustedApiRequest(request, []))'
|
||||||
|
|
||||||
|
die() { echo "[apply-403-fix] 错误: $*" >&2; exit 1; }
|
||||||
|
info() { echo "[apply-403-fix] $*"; }
|
||||||
|
|
||||||
|
# ── 定位运行时实际加载的全局 dsh 包 ──────────────────────────────────────
|
||||||
|
DSH_BIN="$(command -v dsh || true)"
|
||||||
|
if [ -z "$DSH_BIN" ]; then
|
||||||
|
die "未找到 dsh 命令。请确认已全局安装 @deepseek-ai/dsh。"
|
||||||
|
fi
|
||||||
|
DSH_REAL="$(readlink -f "$DSH_BIN")" # .../lib/node_modules/@deepseek-ai/dsh/lib/bin.js
|
||||||
|
DSH_ROOT="$(dirname "$(dirname "$DSH_REAL")")" # .../lib/node_modules/@deepseek-ai/dsh
|
||||||
|
CC_DIR="$DSH_ROOT/node_modules/@deepseek-ai/dsh-client-connection"
|
||||||
|
TARGET="$CC_DIR/lib/index.js"
|
||||||
|
|
||||||
|
if [ ! -f "$TARGET" ]; then
|
||||||
|
die "未找到 $TARGET(dsh 依赖布局与预期不符,请检查 dsh 安装)。"
|
||||||
|
fi
|
||||||
|
if [ ! -f "$PATCH_FILE" ]; then
|
||||||
|
die "未找到补丁文件 $PATCH_FILE。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 检查状态 ──────────────────────────────────────────────────────────────
|
||||||
|
check_state() {
|
||||||
|
if grep -qF "$PATCHED_MARKER" "$TARGET"; then
|
||||||
|
echo "patched"
|
||||||
|
elif grep -qF "$ORIGINAL_MARKER" "$TARGET"; then
|
||||||
|
echo "unpatched"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── --check ───────────────────────────────────────────────────────────────
|
||||||
|
if [ "${1:-}" = "--check" ]; then
|
||||||
|
case "$(check_state)" in
|
||||||
|
patched) info "已打补丁: $TARGET"; exit 0 ;;
|
||||||
|
unpatched) info "未打补丁: $TARGET"; exit 0 ;;
|
||||||
|
*) die "无法识别 $TARGET 的状态(可能 dsh 版本已升级,补丁已过期)。" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── --revert ──────────────────────────────────────────────────────────────
|
||||||
|
if [ "${1:-}" = "--revert" ]; then
|
||||||
|
if [ "$(check_state)" = "unpatched" ]; then
|
||||||
|
info "当前未打补丁,无需回滚。"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ ! -f "$TARGET.orig" ]; then
|
||||||
|
die "没有备份文件 $TARGET.orig,无法回滚(可能不是本脚本打的补丁)。"
|
||||||
|
fi
|
||||||
|
cp "$TARGET.orig" "$TARGET"
|
||||||
|
info "已从备份回滚: $TARGET"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── --help / 未知参数 ────────────────────────────────────────────────────
|
||||||
|
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
||||||
|
grep '^#' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
die "未知参数: $*(用 --help 查看用法)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 应用补丁(幂等) ─────────────────────────────────────────────────────
|
||||||
|
case "$(check_state)" in
|
||||||
|
patched)
|
||||||
|
info "已打补丁,跳过: $TARGET"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
unknown)
|
||||||
|
die "无法识别 $TARGET 的状态,拒绝修改。请人工检查(可能 dsh 已升级)。"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 备份原始文件(仅在应用时创建,--revert 依赖它)
|
||||||
|
if [ ! -f "$TARGET.orig" ]; then
|
||||||
|
cp "$TARGET" "$TARGET.orig"
|
||||||
|
info "已备份原始文件: $TARGET.orig"
|
||||||
|
else
|
||||||
|
info "备份已存在,保留: $TARGET.orig"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 在包根目录执行 patch -p1(补丁内路径为 lib/index.js)
|
||||||
|
if ! (cd "$CC_DIR" && patch -p1 --forward < "$PATCH_FILE"); then
|
||||||
|
info "patch 应用失败,正在回滚本次修改…"
|
||||||
|
if [ -f "$TARGET.orig" ]; then cp "$TARGET.orig" "$TARGET"; fi
|
||||||
|
die "补丁应用失败,已恢复原状。请人工检查。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 应用后校验
|
||||||
|
if [ "$(check_state)" = "patched" ]; then
|
||||||
|
info "补丁已应用: $TARGET"
|
||||||
|
else
|
||||||
|
die "补丁应用后校验失败,请人工检查 $TARGET。"
|
||||||
|
fi
|
||||||
|
info "提示: 需要重启 dsh(my_deepseek.sh stop && my_deepseek.sh)后生效。"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Local 403 fix for the web profile.
|
||||||
|
# Official dsh pins privileged /api methods (settings, credentials, presets,
|
||||||
|
# host dialogs, llm.discoverModels) to loopback by re-checking them with an
|
||||||
|
# empty trusted-host list. That makes a reverse-proxied /api always return 403
|
||||||
|
# even when --trusted-host lists the public host.
|
||||||
|
#
|
||||||
|
# This bundle inserts one row that registers exact /api/<method> routes for
|
||||||
|
# those methods and validates them against the same webRuntime.trustedHosts
|
||||||
|
# list the rest of /api uses, so a host listed in --trusted-host can call them.
|
||||||
|
#
|
||||||
|
# trustedHosts keeps the official webRuntime expression, so
|
||||||
|
# `dsh web --trusted-host <authority>` still feeds this fence.
|
||||||
|
- insert:
|
||||||
|
- id: local-403-fix
|
||||||
|
name: dsh-local-403-fix
|
||||||
|
inject: [webRuntime]
|
||||||
|
config:
|
||||||
|
trustedHosts: !!js ctx.webRuntime.trustedHosts
|
||||||
+55
@@ -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/../.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" "$@"
|
||||||
|
elif [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../.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" "$@"
|
||||||
|
elif command -v node >/dev/null 2>&1; then
|
||||||
|
exec node "$basedir/../.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" "$@"
|
||||||
|
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
|
||||||
|
exec node.exe "$basedir_win/../.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" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../.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" "$@"
|
||||||
|
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
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
"hoistedDependencies": {
|
||||||
|
"@standard-schema/spec@1.1.0": {
|
||||||
|
"@standard-schema/spec": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/cosmokit@1.8.2": {
|
||||||
|
"@deepseek-ai/cosmokit": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/cordis-plugin-loader@1.0.2(@deepseek-ai/cordis@4.0.1)": {
|
||||||
|
"@deepseek-ai/cordis-plugin-loader": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/cordis-plugin-include@1.0.6(@deepseek-ai/cordis-plugin-loader@1.0.2)(@deepseek-ai/cordis@4.0.1)": {
|
||||||
|
"@deepseek-ai/cordis-plugin-include": "private"
|
||||||
|
},
|
||||||
|
"ws@8.21.3": {
|
||||||
|
"ws": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/schemastery@3.18.1": {
|
||||||
|
"@deepseek-ai/schemastery": "private"
|
||||||
|
},
|
||||||
|
"zod@4.4.3": {
|
||||||
|
"zod": "private"
|
||||||
|
},
|
||||||
|
"fflate@0.8.3": {
|
||||||
|
"fflate": "private"
|
||||||
|
},
|
||||||
|
"js-yaml@4.3.1": {
|
||||||
|
"js-yaml": "private"
|
||||||
|
},
|
||||||
|
"argparse@2.0.1": {
|
||||||
|
"argparse": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-agent@0.1.0-rc.7(1e28fca6adb316c000fd361f8e5f3a08)": {
|
||||||
|
"@deepseek-ai/dsh-agent": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-agent-default-model@0.1.0-rc.7(1eb7ceb2998733be9eba21a37a3dc291)": {
|
||||||
|
"@deepseek-ai/dsh-agent-default-model": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-api-remotes@0.1.0-rc.7(022498c733cc9b813e36f108b907ce4c)": {
|
||||||
|
"@deepseek-ai/dsh-api-remotes": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-brand": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-commands@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-agent@0.1.0-rc.7(1e28fca6adb316c000fd361f8e5f3a08))(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-scope@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-session@0.1.0-rc.7(1dcde9bfc9fa0140c66648fbcb67b346))(@deepseek-ai/dsh-typert-protocol@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-commands": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-attachment@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-attachment": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-credentials@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-credentials": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-goal@0.1.0-rc.7(9616cea51a4bea931c5522310292a042)": {
|
||||||
|
"@deepseek-ai/dsh-goal": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-host-directory-picker@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-host-directory-picker": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-llm@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-llm": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-native-command@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-native-command": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-session@0.1.0-rc.7(1dcde9bfc9fa0140c66648fbcb67b346)": {
|
||||||
|
"@deepseek-ai/dsh-session": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-session-persistence@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.0-rc.7(1dcde9bfc9fa0140c66648fbcb67b346))(@deepseek-ai/dsh-timeout@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-session-persistence": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-session-projection@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.0-rc.7(1dcde9bfc9fa0140c66648fbcb67b346))": {
|
||||||
|
"@deepseek-ai/dsh-session-projection": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-session-projection-cache@0.1.0-rc.7(5ae699d01eec9f3ccaa1e8c13f88dd36)": {
|
||||||
|
"@deepseek-ai/dsh-session-projection-cache": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-session-query@0.1.0-rc.7(fa24861d08a9d735fc78888af6ddf326)": {
|
||||||
|
"@deepseek-ai/dsh-session-query": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-settings@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/schemastery@3.18.1)": {
|
||||||
|
"@deepseek-ai/dsh-settings": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-skill@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-skill": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-subagent@0.1.0-rc.7(17b22c2aa5d99096c13835bb226cad51)": {
|
||||||
|
"@deepseek-ai/dsh-subagent": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-jobs@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-agent@0.1.0-rc.7(1e28fca6adb316c000fd361f8e5f3a08))(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-session@0.1.0-rc.7(1dcde9bfc9fa0140c66648fbcb67b346))": {
|
||||||
|
"@deepseek-ai/dsh-jobs": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-tools@0.1.0-rc.7(38c91eb2d47c5b4ba2c8ead86dfcb4aa)": {
|
||||||
|
"@deepseek-ai/dsh-tools": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-user-questions@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-agent@0.1.0-rc.7(1e28fca6adb316c000fd361f8e5f3a08))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))))": {
|
||||||
|
"@deepseek-ai/dsh-user-questions": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-user-approval@0.1.0-rc.7(6b8cefd624fd84694a941b103cc9b3fb)": {
|
||||||
|
"@deepseek-ai/dsh-user-approval": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-workspace@0.1.0-rc.7(52d55750df813bb508c3c8815dfcc2bd)": {
|
||||||
|
"@deepseek-ai/dsh-workspace": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-session-title@0.1.0-rc.7(54a387d0d580a07b8dbef208df0b04c2)": {
|
||||||
|
"@deepseek-ai/dsh-session-title": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/cordis@4.0.1(@deepseek-ai/cordis-plugin-include@1.0.6)(@deepseek-ai/cordis-plugin-loader@1.0.2)": {
|
||||||
|
"@deepseek-ai/cordis": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-agent-presets@0.1.0-rc.7(c0f9f44edab94cddae92cd4b4f319bab)": {
|
||||||
|
"@deepseek-ai/dsh-agent-presets": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-cordis-host-runner@0.1.0-rc.7(2a80d56ba6af58ca8075e00e0fa2157b)": {
|
||||||
|
"@deepseek-ai/dsh-cordis-host-runner": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)": {
|
||||||
|
"@deepseek-ai/dsh-invariants": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-atomic-write@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-atomic-write": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-home-paths@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-home-paths": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-scope@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-scope": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-system-prompt@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-system-prompt": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-typert-protocol@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-typert-protocol": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-api-gateway@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-client-connection@0.1.0-rc.7)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-typert-registry@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-api-gateway": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-host-plugin-inventory@0.1.0-rc.7(@deepseek-ai/cordis-plugin-loader@1.0.2)(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-typert-protocol@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-host-plugin-inventory": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-message-feedback@0.1.0-rc.7(1fcfefbbf5e365eda1a59fadd94056b9)": {
|
||||||
|
"@deepseek-ai/dsh-message-feedback": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-typert-registry@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-typert-registry": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-timeout@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-timeout": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-storage-domain@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-storage@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)))": {
|
||||||
|
"@deepseek-ai/dsh-storage-domain": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-code-runtime@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-code-runtime": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-storage@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-storage": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-client-connection@0.1.0-rc.7(18ff0c05ab41670b8daa2e2e166479db)": {
|
||||||
|
"@deepseek-ai/dsh-client-connection": "private"
|
||||||
|
},
|
||||||
|
"@deepseek-ai/dsh-host-webserver@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.7(@deepseek-ai/cordis@4.0.1))": {
|
||||||
|
"@deepseek-ai/dsh-host-webserver": "private"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hoistPattern": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"included": {
|
||||||
|
"dependencies": true,
|
||||||
|
"devDependencies": true,
|
||||||
|
"optionalDependencies": true
|
||||||
|
},
|
||||||
|
"injectedDeps": {},
|
||||||
|
"layoutVersion": 5,
|
||||||
|
"nodeLinker": "isolated",
|
||||||
|
"packageManager": "pnpm@11.22.0",
|
||||||
|
"pendingBuilds": [],
|
||||||
|
"publicHoistPattern": [],
|
||||||
|
"prunedAt": "Tue, 18 Aug 2026 01:51:00 GMT",
|
||||||
|
"registries": {
|
||||||
|
"default": "https://registry.npmjs.org/",
|
||||||
|
"@jsr": "https://npm.jsr.io/"
|
||||||
|
},
|
||||||
|
"skipped": [],
|
||||||
|
"storeDir": "/home/salmonstill/.local/share/pnpm/store/v11",
|
||||||
|
"virtualStoreDir": ".pnpm",
|
||||||
|
"virtualStoreDirMaxLength": 120
|
||||||
|
}
|
||||||
+1
File diff suppressed because one or more lines are too long
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"lastValidatedTimestamp": 1787018031089,
|
||||||
|
"projects": {},
|
||||||
|
"pnpmfiles": [],
|
||||||
|
"settings": {
|
||||||
|
"autoInstallPeers": true,
|
||||||
|
"dedupeDirectDeps": false,
|
||||||
|
"dedupeInjectedDeps": true,
|
||||||
|
"dedupePeerDependents": true,
|
||||||
|
"dedupePeers": false,
|
||||||
|
"dev": true,
|
||||||
|
"excludeLinksFromLockfile": false,
|
||||||
|
"hoistPattern": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"hoistWorkspacePackages": true,
|
||||||
|
"injectWorkspacePackages": false,
|
||||||
|
"linkWorkspacePackages": false,
|
||||||
|
"minimumReleaseAge": 1440,
|
||||||
|
"minimumReleaseAgeIgnoreMissingTime": true,
|
||||||
|
"nodeLinker": "isolated",
|
||||||
|
"optional": true,
|
||||||
|
"peersSuffixMaxLength": 1000,
|
||||||
|
"preferWorkspacePackages": false,
|
||||||
|
"production": true,
|
||||||
|
"publicHoistPattern": []
|
||||||
|
},
|
||||||
|
"filteredInstall": false
|
||||||
|
}
|
||||||
Generated
Vendored
Symlink
+1
@@ -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
|
||||||
+21
@@ -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.
|
||||||
+43
@@ -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`.
|
||||||
+284
@@ -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 };
|
||||||
+99
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
Generated
Vendored
Executable
+55
@@ -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
|
||||||
Generated
Vendored
Executable
+55
@@ -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
|
||||||
+40
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+377
@@ -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
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../@deepseek-ai+cordis-plugin-loader@1.0.2_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/cordis-plugin-loader
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../js-yaml@4.3.1/node_modules/js-yaml
|
||||||
Generated
Vendored
Symlink
+1
@@ -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
|
||||||
Generated
Vendored
+21
@@ -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.
|
||||||
Generated
Vendored
+48
@@ -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`.
|
||||||
Generated
Vendored
+744
@@ -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 };
|
||||||
+56
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+27
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+35
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+38
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+11
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
Generated
Vendored
+73
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+97
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../../src/internal.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
Executable
+55
@@ -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
|
||||||
Generated
Vendored
+44
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
Vendored
+303
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
Vendored
+129
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
Vendored
+173
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Generated
Vendored
+166
@@ -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
|
||||||
|
}
|
||||||
Generated
Vendored
+32
@@ -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
|
||||||
|
}
|
||||||
Generated
Vendored
+202
@@ -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
|
||||||
Generated
Vendored
+132
@@ -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' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit
|
||||||
Generated
Vendored
Symlink
+1
@@ -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
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../@deepseek-ai+cordis-plugin-loader@1.0.2_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/cordis-plugin-loader
|
||||||
+21
@@ -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.
|
||||||
+101
@@ -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`.
|
||||||
Generated
Vendored
Executable
+16
@@ -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',
|
||||||
|
},
|
||||||
|
})
|
||||||
+1828
File diff suppressed because it is too large
Load Diff
+101
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+240
@@ -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
|
||||||
+1
File diff suppressed because one or more lines are too long
+202
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"fiber.d.ts","sourceRoot":"","sources":["../../src/fiber.ts"],"names":[],"mappings":"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"}
|
||||||
+15
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+105
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"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"}
|
||||||
+186
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"reflect.d.ts","sourceRoot":"","sources":["../../src/reflect.ts"],"names":[],"mappings":"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"}
|
||||||
+201
@@ -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
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"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"}
|
||||||
+55
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
+62
@@ -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
|
||||||
+1
@@ -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"}
|
||||||
Generated
Vendored
Executable
+55
@@ -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
|
||||||
+53
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
+352
@@ -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
|
||||||
|
}
|
||||||
+754
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -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'
|
||||||
+270
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+418
@@ -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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+337
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+287
@@ -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)
|
||||||
|
}
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit
|
||||||
Generated
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec
|
||||||
Generated
Vendored
+21
@@ -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.
|
||||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# cosmokit
|
||||||
|
|
||||||
|
[](https://codecov.io/gh/shigma/cosmokit)
|
||||||
|
[](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'
|
||||||
|
```
|
||||||
Generated
Vendored
+325
@@ -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 };
|
||||||
Generated
Vendored
+15
@@ -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
|
||||||
Generated
Vendored
+1
@@ -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"}
|
||||||
Generated
Vendored
+11
@@ -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
|
||||||
Generated
Vendored
+1
@@ -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"}
|
||||||
Generated
Vendored
+41
@@ -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
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"misc.d.ts","sourceRoot":"","sources":["../../src/misc.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+89
@@ -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
|
||||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"string.d.ts","sourceRoot":"","sources":["../../src/string.ts"],"names":[],"mappings":"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"}
|
||||||
Generated
Vendored
+19
@@ -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
|
||||||
Generated
Vendored
+1
@@ -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"}
|
||||||
Generated
Vendored
+34
@@ -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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user