@feng0531/dsh-execution-bus 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/bus.d.ts +14 -43
- package/dist/bus.js +135 -148
- package/dist/diagnostics.js +6 -4
- package/dist/error-codes.d.ts +57 -0
- package/dist/error-codes.js +63 -0
- package/dist/execution.d.ts +8 -0
- package/dist/execution.js +67 -48
- package/dist/handlers.d.ts +12 -0
- package/dist/handlers.js +71 -24
- package/dist/json.d.ts +1 -1
- package/dist/json.js +13 -11
- package/dist/limits.d.ts +20 -0
- package/dist/limits.js +23 -0
- package/dist/plugin.js +83 -39
- package/dist/preset.js +19 -17
- package/dist/product.d.ts +2 -2
- package/dist/product.js +3 -3
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.js +54 -0
- package/docs/PRODUCT.md +2 -2
- package/docs/protocol.md +1 -1
- package/docs/releases/0.6.0.md +20 -0
- package/docs/releases/0.7.0.md +18 -0
- package/package.json +18 -18
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
“萧何”是产品与模式显示名,执行总线是其技术功能;不新增人物 Agent,不进行角色扮演。仓库 dsh-execution-bus、npm 包名、execution_bus 工具名及 execution-bus-test 预设 ID 保持不变。历史版本发布说明保留当时名称。
|
|
6
6
|
|
|
7
|
-
DSH 独立插件:`@feng0531/dsh-execution-bus` · 当前版本 **0.
|
|
7
|
+
DSH 独立插件:`@feng0531/dsh-execution-bus` · 当前版本 **0.7.0**
|
|
8
8
|
|
|
9
9
|
> 萧何模式从宿主已声明的 PTC 组合派生,保留日常工具,再叠加可选执行总线。已验证隔离的真实 Node PTC → 原 read + 总线调用,以及脚本模型内存会话;真实模型、桌面界面与实际沙箱仍未验收。**不支持重启后恢复任务。**
|
|
10
10
|
|
|
@@ -85,8 +85,10 @@ flowchart TD
|
|
|
85
85
|
|---|---|---|
|
|
86
86
|
| `test`(诊断,非业务) | 仅在明确诊断时检查延迟、进度、成功、失败和取消 | `delayMs`:0–30000;`fail`:布尔值 |
|
|
87
87
|
| `host.read` | 经由 DSH 现有 `read` 工具读取文本 | `file_path`;可选 `offset` 和 `limit`,最多 100 行 |
|
|
88
|
+
| `host.edit` | 经由 DSH 现有 `edit` 工具替换文本(有副作用) | `file_path`、`old_string`、`new_string`;可选 `replace_all` |
|
|
89
|
+
| `host.write` | 经由 DSH 现有 `write` 工具写入文本(有副作用) | `file_path`、`content` |
|
|
88
90
|
|
|
89
|
-
`host.read`
|
|
91
|
+
`host.read`、`host.edit` 与 `host.write` 在出队时重新进入宿主工具管线,沿用宿主当前权限、会话工作目录及输出限制。`host.edit`/`host.write` 有副作用,显式声明非幂等:同一 requestId 的终态结果不可复用,重复提交返回 `MUTATING_REUSE_DENIED`。插件不自行启动命令、不提供任意远程执行服务,也不会强制将现有工具改成总线调用。
|
|
90
92
|
|
|
91
93
|
## 选择“萧何”预设
|
|
92
94
|
|
|
@@ -103,7 +105,7 @@ flowchart TD
|
|
|
103
105
|
|
|
104
106
|
**0.3.0 起不再是只读验证模式。** 加载时读取宿主 `ptc` 预设的组合声明快照,保留原工具、平台表达式、禁用项和配置,只追加萧何总线与说明。不再强制 native 呈现或添加只读白名单;终端、写入、搜索、技能等能力是否可用,取决于原 PTC 配置和宿主服务。其他预设和默认模式不被修改,原 PTC 会话看不到新增总线。
|
|
105
107
|
|
|
106
|
-
普通任务仍通过 `run_code` 调用原工具;**总线目前只支持 test 和 host.
|
|
108
|
+
普通任务仍通过 `run_code` 调用原工具;**总线目前只支持 test、host.read、host.edit 和 host.write,不接管任意命令、安装或全部工具**。例如更新插件应使用原终端能力并遵守宿主授权;专用插件管理工具若在源 PTC 中禁用,萧何不会擅自启用。命令受理、返回 Job ID 不等于执行完成。
|
|
107
109
|
|
|
108
110
|
升级提示:预设 ID 保持兼容,但从旧版只读模式升级后能力范围扩大,使用者须审阅后加载;本次不自动升级日常配置。它不是进程沙箱,同一预设会话共享总线容量,结果按会话隔离。派生是加载时快照,不自动跟随源预设热编辑;源 PTC 更新后应按宿主正常流程重新加载派生行,旧会话遵循宿主版本保留规则。
|
|
109
111
|
|
|
@@ -124,7 +126,7 @@ flowchart TD
|
|
|
124
126
|
2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
|
|
125
127
|
|
|
126
128
|
```powershell
|
|
127
|
-
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.
|
|
129
|
+
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.7.0
|
|
128
130
|
```
|
|
129
131
|
|
|
130
132
|
这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
|
|
@@ -267,7 +269,7 @@ npm run test:install # 临时目录安装,不操作日常 DSH
|
|
|
267
269
|
|
|
268
270
|
- [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
|
|
269
271
|
- [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
|
|
270
|
-
- [验证记录](<docs/validation.md>)、[0.
|
|
272
|
+
- [验证记录](<docs/validation.md>)、[0.7.0 发布说明](<docs/releases/0.7.0.md>):已验证与未验证范围。
|
|
271
273
|
- [同类项目源码整合](<docs/INTEGRATION.md>):固定来源、采用范围、许可与验证边界。
|
|
272
274
|
- [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
|
|
273
275
|
|
package/dist/bus.d.ts
CHANGED
|
@@ -1,50 +1,11 @@
|
|
|
1
|
+
import { object, parse } from "./protocol.js";
|
|
2
|
+
import type { Capability, Event, Handler, Message, Owner, RecordView, State } from "./protocol.js";
|
|
3
|
+
export { object, parse };
|
|
4
|
+
export type { Capability, Event, Handler, Message, Owner, RecordView, State };
|
|
1
5
|
export interface AdmissionPolicy {
|
|
2
6
|
sessionQueueLimit?: number;
|
|
3
7
|
maxQueueWaitMs?: number;
|
|
4
8
|
}
|
|
5
|
-
export type State = "queued" | "running" | "succeeded" | "failed" | "cancelled" | "unknown";
|
|
6
|
-
export type Owner = {
|
|
7
|
-
sessionId: string;
|
|
8
|
-
callId: string;
|
|
9
|
-
};
|
|
10
|
-
export type Message = {
|
|
11
|
-
protocolVersion: 1;
|
|
12
|
-
messageId: string;
|
|
13
|
-
requestId: string;
|
|
14
|
-
type: "request" | "status" | "result" | "cancel";
|
|
15
|
-
payload: Record<string, unknown>;
|
|
16
|
-
};
|
|
17
|
-
export type Event = {
|
|
18
|
-
protocolVersion: 1;
|
|
19
|
-
messageId: string;
|
|
20
|
-
requestId: string;
|
|
21
|
-
correlationId: string;
|
|
22
|
-
sequence: number;
|
|
23
|
-
type: string;
|
|
24
|
-
payload: Record<string, unknown>;
|
|
25
|
-
};
|
|
26
|
-
export type Capability = (input: Record<string, unknown>, signal: AbortSignal) => Promise<unknown>;
|
|
27
|
-
export interface Handler {
|
|
28
|
-
validate(input: Record<string, unknown>): void;
|
|
29
|
-
run(input: Record<string, unknown>, ctx: {
|
|
30
|
-
signal: AbortSignal;
|
|
31
|
-
progress: (sequence: number, percent: number) => void;
|
|
32
|
-
requestId: string;
|
|
33
|
-
invoke?: Capability;
|
|
34
|
-
}): Promise<unknown>;
|
|
35
|
-
}
|
|
36
|
-
export interface RecordView {
|
|
37
|
-
requestId: string;
|
|
38
|
-
state: State;
|
|
39
|
-
sequence: number;
|
|
40
|
-
cancelRequested: boolean;
|
|
41
|
-
delivery: "pending" | "returned";
|
|
42
|
-
result?: unknown;
|
|
43
|
-
error?: string;
|
|
44
|
-
progress?: number;
|
|
45
|
-
}
|
|
46
|
-
export declare function object(v: unknown): Record<string, unknown>;
|
|
47
|
-
export declare function parse(raw: unknown): Message;
|
|
48
9
|
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
49
10
|
export declare class ExecutionBus {
|
|
50
11
|
readonly options: {
|
|
@@ -108,6 +69,14 @@ export declare class ExecutionBus {
|
|
|
108
69
|
detach: () => void;
|
|
109
70
|
returned: () => void;
|
|
110
71
|
};
|
|
72
|
+
/** 校验并拆解 request 的 payload,返回 handler 名、入参与原文。 */
|
|
73
|
+
private requestParts;
|
|
74
|
+
/** 同一 requestId 的重复提交:校验一致性后复用执行或结果。 */
|
|
75
|
+
private reuseInvocation;
|
|
76
|
+
/** 受理全新请求:校验 Handler、检查容量、构建记录并入队。 */
|
|
77
|
+
private admitInvocation;
|
|
78
|
+
/** 若启用了排队期限,登记到期结算为 QUEUE_WAIT_TIMEOUT 的计时器。 */
|
|
79
|
+
private scheduleQueueDeadline;
|
|
111
80
|
private attachment;
|
|
112
81
|
command(owner: Owner, raw: unknown): RecordView;
|
|
113
82
|
private removeQueued;
|
|
@@ -140,6 +109,8 @@ export declare class ExecutionBus {
|
|
|
140
109
|
};
|
|
141
110
|
private cancel;
|
|
142
111
|
private finish;
|
|
112
|
+
/** 按会话轮转挑选下一个出队项:各会话内部 FIFO,跨会话公平。 */
|
|
113
|
+
private nextQueuedIndex;
|
|
143
114
|
private pump;
|
|
144
115
|
private run;
|
|
145
116
|
close(): Promise<void>;
|
package/dist/bus.js
CHANGED
|
@@ -1,60 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { assertJson } from "./json.js";
|
|
3
|
-
import { Diagnostics } from "./diagnostics.js";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
2
|
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { Diagnostics } from "./diagnostics.js";
|
|
4
|
+
import { ERRORS } from "./error-codes.js";
|
|
5
|
+
import { assertJson } from "./json.js";
|
|
6
|
+
import { LIMITS } from "./limits.js";
|
|
5
7
|
import { CORE_DEFAULTS } from "./product.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (v === null || typeof v !== "object")
|
|
13
|
-
return JSON.stringify(v);
|
|
14
|
-
if (Array.isArray(v))
|
|
15
|
-
return "[" + v.map(canonical).join(",") + "]";
|
|
16
|
-
return ("{" +
|
|
17
|
-
Object.keys(v)
|
|
18
|
-
.sort()
|
|
19
|
-
.map((k) => JSON.stringify(k) +
|
|
20
|
-
":" +
|
|
21
|
-
canonical(v[k]))
|
|
22
|
-
.join(",") +
|
|
23
|
-
"}");
|
|
8
|
+
import { canonical, freezeJson, object, parse } from "./protocol.js";
|
|
9
|
+
export { object, parse };
|
|
10
|
+
const isTerminal = (s) => !["queued", "running"].includes(s);
|
|
11
|
+
/** 对 payload 做规范排序后的 SHA-256 摘要,仅用于同 requestId 的冲突比较。 */
|
|
12
|
+
function hashPayload(payload) {
|
|
13
|
+
return createHash("sha256").update(canonical(payload)).digest("hex");
|
|
24
14
|
}
|
|
25
|
-
export function parse(raw) {
|
|
26
|
-
assertJson(raw);
|
|
27
|
-
const m = object(raw);
|
|
28
|
-
if (Object.keys(m).some((k) => ![
|
|
29
|
-
"protocolVersion",
|
|
30
|
-
"messageId",
|
|
31
|
-
"requestId",
|
|
32
|
-
"type",
|
|
33
|
-
"payload",
|
|
34
|
-
].includes(k)))
|
|
35
|
-
throw Error("UNKNOWN_FIELD");
|
|
36
|
-
if (m.protocolVersion !== 1)
|
|
37
|
-
throw Error("UNSUPPORTED_VERSION");
|
|
38
|
-
for (const k of ["messageId", "requestId"])
|
|
39
|
-
if (typeof m[k] !== "string" ||
|
|
40
|
-
!/^[a-zA-Z0-9_.:-]{1,128}$/.test(m[k]))
|
|
41
|
-
throw Error("INVALID_ID");
|
|
42
|
-
if (!["request", "status", "result", "cancel"].includes(m.type))
|
|
43
|
-
throw Error("UNSUPPORTED_TYPE");
|
|
44
|
-
object(m.payload);
|
|
45
|
-
if (Buffer.byteLength(JSON.stringify(m)) > 16384)
|
|
46
|
-
throw Error("MESSAGE_TOO_LARGE");
|
|
47
|
-
return structuredClone(m);
|
|
48
|
-
}
|
|
49
|
-
function freezeJson(value) {
|
|
50
|
-
if (value && typeof value === "object") {
|
|
51
|
-
for (const child of Object.values(value))
|
|
52
|
-
freezeJson(child);
|
|
53
|
-
Object.freeze(value);
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
const terminal = (s) => !["queued", "running"].includes(s);
|
|
58
15
|
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
59
16
|
export class ExecutionBus {
|
|
60
17
|
options;
|
|
@@ -87,7 +44,7 @@ export class ExecutionBus {
|
|
|
87
44
|
typeof admission !== "object" ||
|
|
88
45
|
Array.isArray(admission) ||
|
|
89
46
|
Reflect.ownKeys(admission).some((key) => typeof key !== "string" || !allowed.includes(key)))
|
|
90
|
-
throw Error(
|
|
47
|
+
throw Error(ERRORS.INVALID_ADMISSION_POLICY);
|
|
91
48
|
for (const key of Reflect.ownKeys(admission)) {
|
|
92
49
|
const descriptor = Object.getOwnPropertyDescriptor(admission, key);
|
|
93
50
|
if (!("value" in descriptor) ||
|
|
@@ -95,7 +52,7 @@ export class ExecutionBus {
|
|
|
95
52
|
!Number.isSafeInteger(descriptor.value) ||
|
|
96
53
|
descriptor.value < 1 ||
|
|
97
54
|
descriptor.value > 2147483647)
|
|
98
|
-
throw Error(
|
|
55
|
+
throw Error(ERRORS.INVALID_ADMISSION_POLICY);
|
|
99
56
|
}
|
|
100
57
|
this.admission = Object.freeze({ ...admission });
|
|
101
58
|
const keys = [
|
|
@@ -113,15 +70,15 @@ export class ExecutionBus {
|
|
|
113
70
|
const descriptor = Object.getOwnPropertyDescriptor(options, key);
|
|
114
71
|
return (!descriptor || !("value" in descriptor) || !descriptor.enumerable);
|
|
115
72
|
}))
|
|
116
|
-
throw Error(
|
|
73
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
117
74
|
this.options = Object.freeze({ ...options });
|
|
118
75
|
for (const v of Object.values(this.options))
|
|
119
76
|
if (!Number.isSafeInteger(v) || v < 1)
|
|
120
|
-
throw Error(
|
|
77
|
+
throw Error(ERRORS.INVALID_OPTIONS);
|
|
121
78
|
}
|
|
122
79
|
register(name, handler) {
|
|
123
80
|
if (this.handlers.has(name))
|
|
124
|
-
throw Error(
|
|
81
|
+
throw Error(ERRORS.DUPLICATE_HANDLER);
|
|
125
82
|
const registration = { handler, token: Symbol(name) };
|
|
126
83
|
this.handlers.set(name, registration);
|
|
127
84
|
let disposed = false;
|
|
@@ -135,7 +92,7 @@ export class ExecutionBus {
|
|
|
135
92
|
}
|
|
136
93
|
key(o, id) {
|
|
137
94
|
if (!o.sessionId || !o.callId)
|
|
138
|
-
throw Error(
|
|
95
|
+
throw Error(ERRORS.UNTRUSTED_CONTEXT);
|
|
139
96
|
return JSON.stringify([o.sessionId, id]);
|
|
140
97
|
}
|
|
141
98
|
emit(e, type, payload, correlationId = "") {
|
|
@@ -149,8 +106,7 @@ export class ExecutionBus {
|
|
|
149
106
|
type,
|
|
150
107
|
payload,
|
|
151
108
|
});
|
|
152
|
-
//
|
|
153
|
-
// overtake the current event for later observers. Removed listeners are skipped.
|
|
109
|
+
// 在调用用户代码前快照并排队:嵌套取消无法抢到当前事件。
|
|
154
110
|
e.notifications.push({ event, listeners: [...e.listeners] });
|
|
155
111
|
if (e.dispatching)
|
|
156
112
|
return;
|
|
@@ -170,7 +126,7 @@ export class ExecutionBus {
|
|
|
170
126
|
}
|
|
171
127
|
finally {
|
|
172
128
|
e.dispatching = false;
|
|
173
|
-
if (
|
|
129
|
+
if (isTerminal(e.view.state))
|
|
174
130
|
e.listeners.clear();
|
|
175
131
|
}
|
|
176
132
|
}
|
|
@@ -180,68 +136,87 @@ export class ExecutionBus {
|
|
|
180
136
|
get(o, id) {
|
|
181
137
|
const e = this.entries.get(this.key(o, id));
|
|
182
138
|
if (!e)
|
|
183
|
-
throw Error(
|
|
139
|
+
throw Error(ERRORS.NOT_FOUND);
|
|
184
140
|
return e;
|
|
185
141
|
}
|
|
186
142
|
sweep(now = Date.now()) {
|
|
187
143
|
for (const [k, e] of this.entries)
|
|
188
|
-
if (
|
|
189
|
-
now - e.created > this.options.retentionMs) {
|
|
144
|
+
if (isTerminal(e.view.state) && now - e.created > this.options.retentionMs) {
|
|
190
145
|
this.retainedResultBytes -= e.resultBytes;
|
|
191
146
|
this.entries.delete(k);
|
|
192
147
|
}
|
|
193
148
|
}
|
|
194
149
|
submit(owner, raw, onEvent, invoke) {
|
|
195
150
|
if (this.closed)
|
|
196
|
-
throw Error(
|
|
151
|
+
throw Error(ERRORS.CLOSED);
|
|
197
152
|
const m = parse(raw);
|
|
198
153
|
if (m.type !== "request")
|
|
199
|
-
throw Error(
|
|
154
|
+
throw Error(ERRORS.EXPECTED_REQUEST);
|
|
155
|
+
const parts = this.requestParts(m);
|
|
156
|
+
const key = this.key(owner, m.requestId);
|
|
157
|
+
const fingerprint = hashPayload(parts.payload);
|
|
158
|
+
this.sweep();
|
|
159
|
+
const existing = this.entries.get(key);
|
|
160
|
+
return existing
|
|
161
|
+
? this.reuseInvocation(existing, m, fingerprint, onEvent)
|
|
162
|
+
: this.admitInvocation(owner, m, parts, key, fingerprint, invoke, onEvent);
|
|
163
|
+
}
|
|
164
|
+
/** 校验并拆解 request 的 payload,返回 handler 名、入参与原文。 */
|
|
165
|
+
requestParts(m) {
|
|
200
166
|
const p = m.payload;
|
|
201
167
|
if (Object.keys(p).some((k) => !["handler", "input"].includes(k)) ||
|
|
202
168
|
typeof p.handler !== "string")
|
|
203
|
-
throw Error(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
169
|
+
throw Error(ERRORS.INVALID_REQUEST);
|
|
170
|
+
return {
|
|
171
|
+
handler: p.handler,
|
|
172
|
+
input: object(p.input),
|
|
173
|
+
payload: p,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** 同一 requestId 的重复提交:校验一致性后复用执行或结果。 */
|
|
177
|
+
reuseInvocation(e, m, fingerprint, onEvent) {
|
|
178
|
+
if (e.fingerprint !== fingerprint)
|
|
179
|
+
throw Error(ERRORS.REQUEST_CONFLICT);
|
|
180
|
+
// 仅显式声明非幂等(idempotent:false)的 handler,终态结果才不可复用:
|
|
181
|
+
// 返回"已执行"会掩盖副作用状态。运行中的重复 attach 仍放行,不会二次执行。
|
|
182
|
+
if (e.handler.idempotent === false && isTerminal(e.view.state))
|
|
183
|
+
throw Error(ERRORS.MUTATING_REUSE_DENIED);
|
|
184
|
+
const previous = e.messages.get(m.messageId);
|
|
185
|
+
if (previous && previous !== canonical(m))
|
|
186
|
+
throw Error(ERRORS.MESSAGE_CONFLICT);
|
|
187
|
+
if (!previous && e.messages.size >= LIMITS.messageAccountLimit)
|
|
188
|
+
throw Error(ERRORS.MESSAGE_LIMIT);
|
|
189
|
+
// 先取得附着,再提交消息身份:附着被拒(WAITER_LIMIT)时无副作用。
|
|
190
|
+
const attached = this.attachment(e, onEvent, m.messageId);
|
|
191
|
+
if (!previous)
|
|
192
|
+
e.messages.set(m.messageId, canonical(m));
|
|
193
|
+
this.telemetry.record(e.trace, "reused");
|
|
194
|
+
return attached;
|
|
195
|
+
}
|
|
196
|
+
/** 受理全新请求:校验 Handler、检查容量、构建记录并入队。 */
|
|
197
|
+
admitInvocation(owner, m, parts, key, fingerprint, invoke, onEvent) {
|
|
198
|
+
const handler = this.handlers.get(parts.handler)?.handler;
|
|
224
199
|
if (!handler)
|
|
225
|
-
throw Error(
|
|
226
|
-
handler.validate(input);
|
|
200
|
+
throw Error(ERRORS.UNKNOWN_HANDLER);
|
|
201
|
+
handler.validate(parts.input);
|
|
227
202
|
if (this.entries.size >= this.options.recordLimit ||
|
|
228
203
|
this.queue.length >= this.options.queueLimit)
|
|
229
|
-
throw Error(
|
|
204
|
+
throw Error(ERRORS.BACKPRESSURE);
|
|
230
205
|
if (this.admission.sessionQueueLimit !== undefined &&
|
|
231
206
|
this.queue.filter((entry) => entry.owner === owner.sessionId).length >=
|
|
232
207
|
this.admission.sessionQueueLimit)
|
|
233
|
-
throw Error(
|
|
208
|
+
throw Error(ERRORS.SESSION_BACKPRESSURE);
|
|
234
209
|
let resolve;
|
|
235
210
|
const done = new Promise((r) => (resolve = r));
|
|
236
|
-
e = {
|
|
211
|
+
const e = {
|
|
237
212
|
trace: this.telemetry.begin(),
|
|
238
213
|
resultBytes: 0,
|
|
239
214
|
invoke,
|
|
240
215
|
correlationId: m.messageId,
|
|
241
216
|
owner: owner.sessionId,
|
|
242
|
-
fingerprint
|
|
217
|
+
fingerprint,
|
|
243
218
|
handler,
|
|
244
|
-
input: structuredClone(input),
|
|
219
|
+
input: structuredClone(parts.input),
|
|
245
220
|
view: {
|
|
246
221
|
requestId: m.requestId,
|
|
247
222
|
state: "queued",
|
|
@@ -263,33 +238,36 @@ export class ExecutionBus {
|
|
|
263
238
|
this.telemetry.record(e.trace, "accepted");
|
|
264
239
|
const attached = this.attachment(e, onEvent, m.messageId);
|
|
265
240
|
this.queue.push(e);
|
|
266
|
-
|
|
267
|
-
e.deadline = performance.now() + this.admission.maxQueueWaitMs;
|
|
268
|
-
const entry = e;
|
|
269
|
-
const expire = () => {
|
|
270
|
-
if (entry.view.state !== "queued")
|
|
271
|
-
return;
|
|
272
|
-
const remaining = entry.deadline - performance.now();
|
|
273
|
-
if (remaining > 0) {
|
|
274
|
-
entry.queueTimer = setTimeout(expire, Math.ceil(remaining));
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
this.removeQueued(entry);
|
|
278
|
-
this.finish(entry, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
|
|
279
|
-
};
|
|
280
|
-
e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
|
|
281
|
-
}
|
|
241
|
+
this.scheduleQueueDeadline(e);
|
|
282
242
|
if (!this.readyOwners.includes(e.owner))
|
|
283
243
|
this.readyOwners.push(e.owner);
|
|
284
244
|
queueMicrotask(() => this.pump());
|
|
285
245
|
return attached;
|
|
286
246
|
}
|
|
247
|
+
/** 若启用了排队期限,登记到期结算为 QUEUE_WAIT_TIMEOUT 的计时器。 */
|
|
248
|
+
scheduleQueueDeadline(e) {
|
|
249
|
+
if (this.admission.maxQueueWaitMs === undefined)
|
|
250
|
+
return;
|
|
251
|
+
e.deadline = performance.now() + this.admission.maxQueueWaitMs;
|
|
252
|
+
const expire = () => {
|
|
253
|
+
if (e.view.state !== "queued")
|
|
254
|
+
return;
|
|
255
|
+
const remaining = e.deadline - performance.now();
|
|
256
|
+
if (remaining > 0) {
|
|
257
|
+
e.queueTimer = setTimeout(expire, Math.ceil(remaining));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.removeQueued(e);
|
|
261
|
+
this.finish(e, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
|
|
262
|
+
};
|
|
263
|
+
e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
|
|
264
|
+
}
|
|
287
265
|
attachment(e, onEvent, id) {
|
|
288
|
-
//
|
|
266
|
+
// 每次附着独立占配额,即使调用方复用同一个回调。
|
|
289
267
|
const listener = onEvent ? (event) => onEvent(event) : undefined;
|
|
290
|
-
if (listener && !
|
|
291
|
-
if (e.listeners.size >=
|
|
292
|
-
throw Error(
|
|
268
|
+
if (listener && !isTerminal(e.view.state)) {
|
|
269
|
+
if (e.listeners.size >= LIMITS.waiterLimit)
|
|
270
|
+
throw Error(ERRORS.WAITER_LIMIT);
|
|
293
271
|
e.listeners.add(listener);
|
|
294
272
|
}
|
|
295
273
|
const accepted = {
|
|
@@ -319,20 +297,20 @@ export class ExecutionBus {
|
|
|
319
297
|
command(owner, raw) {
|
|
320
298
|
const m = parse(raw);
|
|
321
299
|
if (m.type === "request")
|
|
322
|
-
throw Error(
|
|
300
|
+
throw Error(ERRORS.EXPECTED_CONTROL);
|
|
323
301
|
if (Object.keys(m.payload).length)
|
|
324
|
-
throw Error(
|
|
302
|
+
throw Error(ERRORS.INVALID_CONTROL);
|
|
325
303
|
this.sweep();
|
|
326
304
|
const e = this.get(owner, m.requestId);
|
|
327
305
|
const previous = e.messages.get(m.messageId), fingerprint = canonical(m);
|
|
328
306
|
if (previous && previous !== fingerprint)
|
|
329
|
-
throw Error(
|
|
307
|
+
throw Error(ERRORS.MESSAGE_CONFLICT);
|
|
330
308
|
if (!previous) {
|
|
331
|
-
//
|
|
332
|
-
if (e.messages.size >=
|
|
309
|
+
// 保留一个应急控制槽:读流量绝不能阻塞取消。
|
|
310
|
+
if (e.messages.size >= LIMITS.messageAccountLimit) {
|
|
333
311
|
if (m.type !== "cancel")
|
|
334
|
-
throw Error(
|
|
335
|
-
if (e.view.cancelRequested ||
|
|
312
|
+
throw Error(ERRORS.MESSAGE_LIMIT);
|
|
313
|
+
if (e.view.cancelRequested || isTerminal(e.view.state))
|
|
336
314
|
return this.view(e);
|
|
337
315
|
}
|
|
338
316
|
e.messages.set(m.messageId, fingerprint);
|
|
@@ -349,14 +327,14 @@ export class ExecutionBus {
|
|
|
349
327
|
/** Trusted host owner required. Never exposes other sessions or business payloads. */
|
|
350
328
|
inspect(owner) {
|
|
351
329
|
if (!owner || typeof owner.sessionId !== "string" || !owner.sessionId)
|
|
352
|
-
throw Error(
|
|
330
|
+
throw Error(ERRORS.OWNING_SESSION_REQUIRED);
|
|
353
331
|
this.sweep();
|
|
354
332
|
const now = performance.now();
|
|
355
333
|
const records = [...this.entries.values()].filter((e) => e.owner === owner.sessionId);
|
|
356
|
-
//
|
|
357
|
-
const active = records.filter((e) => !
|
|
358
|
-
const history = records.filter((e) =>
|
|
359
|
-
const selected = [...active, ...history].slice(0,
|
|
334
|
+
// 活跃任务不得被更新的终态历史挤到列表之外。
|
|
335
|
+
const active = records.filter((e) => !isTerminal(e.view.state));
|
|
336
|
+
const history = records.filter((e) => isTerminal(e.view.state)).reverse();
|
|
337
|
+
const selected = [...active, ...history].slice(0, LIMITS.inspectTaskLimit);
|
|
360
338
|
const tasks = selected.map((e) => ({
|
|
361
339
|
requestId: e.view.requestId,
|
|
362
340
|
state: e.view.state,
|
|
@@ -369,7 +347,7 @@ export class ExecutionBus {
|
|
|
369
347
|
? "concurrency"
|
|
370
348
|
: "dispatch"
|
|
371
349
|
: null,
|
|
372
|
-
phase:
|
|
350
|
+
phase: isTerminal(e.view.state)
|
|
373
351
|
? e.view.delivery === "returned"
|
|
374
352
|
? "returned"
|
|
375
353
|
: "terminal"
|
|
@@ -390,14 +368,14 @@ export class ExecutionBus {
|
|
|
390
368
|
counts: {
|
|
391
369
|
queued: records.filter((e) => e.view.state === "queued").length,
|
|
392
370
|
running: records.filter((e) => e.view.state === "running").length,
|
|
393
|
-
terminal: records.filter((e) =>
|
|
371
|
+
terminal: records.filter((e) => isTerminal(e.view.state)).length,
|
|
394
372
|
},
|
|
395
373
|
tasks,
|
|
396
|
-
truncated: records.length >
|
|
374
|
+
truncated: records.length > LIMITS.inspectTaskLimit,
|
|
397
375
|
});
|
|
398
376
|
}
|
|
399
377
|
cancel(e) {
|
|
400
|
-
if (
|
|
378
|
+
if (isTerminal(e.view.state) || e.view.cancelRequested)
|
|
401
379
|
return;
|
|
402
380
|
e.view.cancelRequested = true;
|
|
403
381
|
this.telemetry.record(e.trace, "cancel_requested");
|
|
@@ -409,15 +387,16 @@ export class ExecutionBus {
|
|
|
409
387
|
}
|
|
410
388
|
}
|
|
411
389
|
finish(e, state, result, error, resultBytes = 0) {
|
|
412
|
-
if (
|
|
390
|
+
if (isTerminal(e.view.state))
|
|
413
391
|
return;
|
|
414
392
|
if (e.queueTimer !== undefined) {
|
|
415
393
|
clearTimeout(e.queueTimer);
|
|
416
394
|
e.queueTimer = undefined;
|
|
417
395
|
}
|
|
418
396
|
e.finishedAt = performance.now();
|
|
419
|
-
//
|
|
420
|
-
|
|
397
|
+
// 终态事件是下一个序号;先用它构建快照,避免把不可逆终态提交到视图之外。
|
|
398
|
+
const terminalSequence = e.view.sequence + 1;
|
|
399
|
+
let next = { ...e.view, state, sequence: terminalSequence };
|
|
421
400
|
if (result !== undefined)
|
|
422
401
|
next.result = result;
|
|
423
402
|
if (error)
|
|
@@ -432,7 +411,7 @@ export class ExecutionBus {
|
|
|
432
411
|
next = {
|
|
433
412
|
requestId: e.view.requestId,
|
|
434
413
|
state,
|
|
435
|
-
sequence:
|
|
414
|
+
sequence: terminalSequence,
|
|
436
415
|
cancelRequested: e.view.cancelRequested,
|
|
437
416
|
delivery: e.view.delivery,
|
|
438
417
|
error: "RESULT_NOT_RETAINED",
|
|
@@ -442,28 +421,36 @@ export class ExecutionBus {
|
|
|
442
421
|
e.resultBytes = resultBytes;
|
|
443
422
|
this.retainedResultBytes += resultBytes;
|
|
444
423
|
e.created = Date.now();
|
|
445
|
-
|
|
424
|
+
// emit() 会再自增序号;先整体回退一档,使终态事件恰好落在 terminalSequence,
|
|
425
|
+
// 与上面交付给等待者的快照序号完全一致。
|
|
426
|
+
e.view = { ...next, sequence: terminalSequence - 1 };
|
|
446
427
|
this.telemetry.record(e.trace, "terminal");
|
|
447
428
|
e.resolve(snapshot);
|
|
448
429
|
this.emit(e, state, { state });
|
|
449
|
-
//
|
|
430
|
+
// 重入的终态通知可能仍排在本批之后。
|
|
450
431
|
if (!e.dispatching)
|
|
451
432
|
e.listeners.clear();
|
|
452
433
|
}
|
|
434
|
+
/** 按会话轮转挑选下一个出队项:各会话内部 FIFO,跨会话公平。 */
|
|
435
|
+
nextQueuedIndex() {
|
|
436
|
+
let index = -1;
|
|
437
|
+
while (this.readyOwners.length && index < 0) {
|
|
438
|
+
const owner = this.readyOwners.shift();
|
|
439
|
+
const i = this.queue.findIndex((entry) => entry.owner === owner);
|
|
440
|
+
if (i >= 0) {
|
|
441
|
+
index = i;
|
|
442
|
+
// 找到后,若该会话仍有其它排队任务,保留其轮次避免单个会话饥饿。
|
|
443
|
+
if (this.queue.some((entry, j) => j !== i && entry.owner === owner))
|
|
444
|
+
this.readyOwners.push(owner);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return index < 0 ? 0 : index;
|
|
448
|
+
}
|
|
453
449
|
pump() {
|
|
454
450
|
while (!this.closed &&
|
|
455
451
|
this.running < this.options.concurrency &&
|
|
456
452
|
this.queue.length) {
|
|
457
|
-
|
|
458
|
-
while (this.readyOwners.length && index < 0) {
|
|
459
|
-
const owner = this.readyOwners.shift();
|
|
460
|
-
index = this.queue.findIndex((entry) => entry.owner === owner);
|
|
461
|
-
if (index >= 0 &&
|
|
462
|
-
this.queue.some((entry, i) => i !== index && entry.owner === owner))
|
|
463
|
-
this.readyOwners.push(owner);
|
|
464
|
-
}
|
|
465
|
-
if (index < 0)
|
|
466
|
-
index = 0;
|
|
453
|
+
const index = this.nextQueuedIndex();
|
|
467
454
|
const e = this.queue.splice(index, 1)[0];
|
|
468
455
|
if (e.view.state !== "queued")
|
|
469
456
|
continue;
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { LIMITS } from "./limits.js";
|
|
2
3
|
/** In-memory numeric metadata only: never accepts request/session strings, input, output or error text. */
|
|
3
4
|
export class Diagnostics {
|
|
4
5
|
next = 0;
|
|
5
|
-
events = new Array(
|
|
6
|
+
events = new Array(LIMITS.traceRingSize);
|
|
6
7
|
cursor = 0;
|
|
7
8
|
size = 0;
|
|
8
9
|
counts = {};
|
|
@@ -16,14 +17,15 @@ export class Diagnostics {
|
|
|
16
17
|
stage,
|
|
17
18
|
elapsedMs: Math.max(0, performance.now() - context.at),
|
|
18
19
|
};
|
|
19
|
-
this.cursor = (this.cursor + 1) %
|
|
20
|
-
this.size = Math.min(this.size + 1,
|
|
20
|
+
this.cursor = (this.cursor + 1) % LIMITS.traceRingSize;
|
|
21
|
+
this.size = Math.min(this.size + 1, LIMITS.traceRingSize);
|
|
21
22
|
}
|
|
22
23
|
snapshot() {
|
|
23
24
|
return {
|
|
24
25
|
counts: { ...this.counts },
|
|
25
26
|
traces: Array.from({ length: this.size }, (_, index) => ({
|
|
26
|
-
...this.events[(this.cursor - this.size +
|
|
27
|
+
...this.events[(this.cursor - this.size + LIMITS.traceRingSize + index) %
|
|
28
|
+
LIMITS.traceRingSize],
|
|
27
29
|
})),
|
|
28
30
|
};
|
|
29
31
|
}
|