@feng0531/dsh-execution-bus 0.5.1 → 0.6.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 +3 -3
- 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 +54 -0
- package/dist/error-codes.js +60 -0
- package/dist/execution.d.ts +8 -0
- package/dist/execution.js +67 -48
- package/dist/handlers.js +14 -11
- 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 +60 -36
- package/dist/preset.js +19 -17
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.js +54 -0
- package/docs/PRODUCT.md +1 -1
- package/docs/releases/0.6.0.md +20 -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.6.0**
|
|
8
8
|
|
|
9
9
|
> 萧何模式从宿主已声明的 PTC 组合派生,保留日常工具,再叠加可选执行总线。已验证隔离的真实 Node PTC → 原 read + 总线调用,以及脚本模型内存会话;真实模型、桌面界面与实际沙箱仍未验收。**不支持重启后恢复任务。**
|
|
10
10
|
|
|
@@ -124,7 +124,7 @@ flowchart TD
|
|
|
124
124
|
2. 在你选定的隔离 DSH 配置依赖目录安装固定版本,不要在 DSH 程序安装目录执行:
|
|
125
125
|
|
|
126
126
|
```powershell
|
|
127
|
-
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.
|
|
127
|
+
npm install --ignore-scripts @feng0531/dsh-execution-bus@0.6.0
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
这是 npm 目录示例。若目标配置使用 pnpm,应沿用其包管理器,不混用锁文件。遇到宿主 peer 版本冲突就停止,不用强制安装绕过。
|
|
@@ -267,7 +267,7 @@ npm run test:install # 临时目录安装,不操作日常 DSH
|
|
|
267
267
|
|
|
268
268
|
- [协议和投递语义](<docs/protocol.md>):字段、状态、重复处理与取消规则。
|
|
269
269
|
- [宿主兼容性与源码调用链](<docs/host-compatibility.md>):为什么可以等待,为什么不能跨重启恢复。
|
|
270
|
-
- [验证记录](<docs/validation.md>)、[0.
|
|
270
|
+
- [验证记录](<docs/validation.md>)、[0.6.0 发布说明](<docs/releases/0.6.0.md>):已验证与未验证范围。
|
|
271
271
|
- [同类项目源码整合](<docs/INTEGRATION.md>):固定来源、采用范围、许可与验证边界。
|
|
272
272
|
- [维护者发布流程](<docs/RELEASING.md>):标签 CI、安装包校验与正式发布门禁。
|
|
273
273
|
|
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
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** 集中全部固定错误码。值即协议 v1 的稳定错误词汇,不得重命名或改动。
|
|
2
|
+
* 新增错误码前先更新 docs/protocol.md 的错误词汇表。 */
|
|
3
|
+
export declare const ERRORS: Readonly<{
|
|
4
|
+
INVALID_OBJECT: "INVALID_OBJECT";
|
|
5
|
+
UNKNOWN_FIELD: "UNKNOWN_FIELD";
|
|
6
|
+
UNSUPPORTED_VERSION: "UNSUPPORTED_VERSION";
|
|
7
|
+
INVALID_ID: "INVALID_ID";
|
|
8
|
+
UNSUPPORTED_TYPE: "UNSUPPORTED_TYPE";
|
|
9
|
+
MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE";
|
|
10
|
+
JSON_LIMIT: "JSON_LIMIT";
|
|
11
|
+
INVALID_JSON: "INVALID_JSON";
|
|
12
|
+
CYCLIC_JSON: "CYCLIC_JSON";
|
|
13
|
+
INVALID_JSON_ARRAY: "INVALID_JSON_ARRAY";
|
|
14
|
+
INVALID_JSON_OBJECT: "INVALID_JSON_OBJECT";
|
|
15
|
+
INVALID_JSON_KEY: "INVALID_JSON_KEY";
|
|
16
|
+
INVALID_JSON_PROPERTY: "INVALID_JSON_PROPERTY";
|
|
17
|
+
CLOSED: "CLOSED";
|
|
18
|
+
EXPECTED_REQUEST: "EXPECTED_REQUEST";
|
|
19
|
+
INVALID_REQUEST: "INVALID_REQUEST";
|
|
20
|
+
REQUEST_CONFLICT: "REQUEST_CONFLICT";
|
|
21
|
+
MESSAGE_CONFLICT: "MESSAGE_CONFLICT";
|
|
22
|
+
MESSAGE_LIMIT: "MESSAGE_LIMIT";
|
|
23
|
+
UNKNOWN_HANDLER: "UNKNOWN_HANDLER";
|
|
24
|
+
BACKPRESSURE: "BACKPRESSURE";
|
|
25
|
+
SESSION_BACKPRESSURE: "SESSION_BACKPRESSURE";
|
|
26
|
+
WAITER_LIMIT: "WAITER_LIMIT";
|
|
27
|
+
EXPECTED_CONTROL: "EXPECTED_CONTROL";
|
|
28
|
+
INVALID_CONTROL: "INVALID_CONTROL";
|
|
29
|
+
NOT_FOUND: "NOT_FOUND";
|
|
30
|
+
UNTRUSTED_CONTEXT: "UNTRUSTED_CONTEXT";
|
|
31
|
+
OWNING_SESSION_REQUIRED: "OWNING_SESSION_REQUIRED";
|
|
32
|
+
QUEUE_WAIT_TIMEOUT: "QUEUE_WAIT_TIMEOUT";
|
|
33
|
+
RESULT_NOT_RETAINED: "RESULT_NOT_RETAINED";
|
|
34
|
+
CANCELLED: "CANCELLED";
|
|
35
|
+
CANCELLATION_UNCONFIRMED: "CANCELLATION_UNCONFIRMED";
|
|
36
|
+
HANDLER_FAILED: "HANDLER_FAILED";
|
|
37
|
+
INVALID_ADMISSION_POLICY: "INVALID_ADMISSION_POLICY";
|
|
38
|
+
INVALID_OPTIONS: "INVALID_OPTIONS";
|
|
39
|
+
DUPLICATE_HANDLER: "DUPLICATE_HANDLER";
|
|
40
|
+
MUTATING_REUSE_DENIED: "MUTATING_REUSE_DENIED";
|
|
41
|
+
DUPLICATE_EXECUTION_HANDLE: "DUPLICATE_EXECUTION_HANDLE";
|
|
42
|
+
INVALID_STATE_QUERY: "INVALID_STATE_QUERY";
|
|
43
|
+
HOST_READ_FAILED: "HOST_READ_FAILED";
|
|
44
|
+
HOST_JOB_UNAVAILABLE: "HOST_JOB_UNAVAILABLE";
|
|
45
|
+
PTC_PRESET_TOO_LARGE: "PTC_PRESET_TOO_LARGE";
|
|
46
|
+
PTC_PRESET_ALREADY_CONTAINS_BUS: "PTC_PRESET_ALREADY_CONTAINS_BUS";
|
|
47
|
+
PTC_PRESET_RELATIVE_BASE_UNSUPPORTED: "PTC_PRESET_RELATIVE_BASE_UNSUPPORTED";
|
|
48
|
+
PTC_PRESET_UNAVAILABLE: "PTC_PRESET_UNAVAILABLE";
|
|
49
|
+
INVALID_TEST_INPUT: "INVALID_TEST_INPUT";
|
|
50
|
+
INVALID_READ_INPUT: "INVALID_READ_INPUT";
|
|
51
|
+
READ_LIMIT: "READ_LIMIT";
|
|
52
|
+
CONTROLLED_FAILURE: "CONTROLLED_FAILURE";
|
|
53
|
+
HOST_CAPABILITY_REQUIRED: "HOST_CAPABILITY_REQUIRED";
|
|
54
|
+
}>;
|