@feng0531/dsh-execution-bus 0.12.0 → 1.0.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 +47 -264
- package/cordis.patch.yml +1 -1
- package/dist/error-codes.d.ts +14 -0
- package/dist/error-codes.js +15 -0
- package/dist/mode.d.ts +2 -2
- package/dist/mode.js +7 -7
- package/dist/plugin.d.ts +2 -6
- package/dist/plugin.js +28 -232
- package/dist/preset.d.ts +1 -1
- package/dist/preset.js +4 -6
- package/dist/product.d.ts +91 -195
- package/dist/product.js +66 -146
- package/dist/runtime/executor.d.ts +18 -0
- package/dist/runtime/executor.js +239 -0
- package/dist/runtime/plan.d.ts +30 -0
- package/dist/runtime/plan.js +185 -0
- package/dist/runtime/service.d.ts +66 -0
- package/dist/runtime/service.js +185 -0
- package/dist/runtime/store.d.ts +45 -0
- package/dist/runtime/store.js +102 -0
- package/docs/DECLARATIVE-RUNTIME.md +44 -0
- package/docs/FLOW-TEST.md +7 -54
- package/docs/INTEGRATION.md +4 -2
- package/docs/OPTIMIZATION.md +2 -0
- package/docs/PRODUCT.md +15 -40
- package/docs/RETROSPECTIVE.md +51 -0
- package/docs/host-compatibility.md +11 -74
- package/docs/protocol.md +27 -120
- package/docs/releases/0.11.1.md +2 -0
- package/docs/releases/0.12.1.md +30 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/validation.md +23 -61
- package/package.json +4 -6
- package/dist/bus.d.ts +0 -148
- package/dist/bus.js +0 -773
- package/dist/diagnostics.d.ts +0 -41
- package/dist/diagnostics.js +0 -32
- package/dist/execution.d.ts +0 -70
- package/dist/execution.js +0 -193
- package/dist/handlers.d.ts +0 -25
- package/dist/handlers.js +0 -194
- package/dist/json.d.ts +0 -4
- package/dist/json.js +0 -46
- package/dist/limits.d.ts +0 -20
- package/dist/limits.js +0 -23
- package/dist/protocol.d.ts +0 -62
- package/dist/protocol.js +0 -54
package/dist/bus.js
DELETED
|
@@ -1,773 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
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";
|
|
7
|
-
import { CORE_DEFAULTS, SCHEDULER_POLICY } from "./product.js";
|
|
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");
|
|
14
|
-
}
|
|
15
|
-
/** Volatile single-process bus; no acceptance durability or automatic execution retry. */
|
|
16
|
-
export class ExecutionBus {
|
|
17
|
-
options;
|
|
18
|
-
admission;
|
|
19
|
-
scheduler;
|
|
20
|
-
entries = new Map();
|
|
21
|
-
handlers = new Map();
|
|
22
|
-
queue = [];
|
|
23
|
-
readyOwners = [];
|
|
24
|
-
running = 0;
|
|
25
|
-
runningByHandler = new Map();
|
|
26
|
-
/** 资源键 → 占用它的运行中 Entry;有副作用 Handler 据此互斥。 */
|
|
27
|
-
resourceLocks = new Map();
|
|
28
|
-
/** payload 指纹 → 已成功或运行中的 Entry,用于唯一任务复用。 */
|
|
29
|
-
dedupeIndex = new Map();
|
|
30
|
-
retainedResultBytes = 0;
|
|
31
|
-
closed = false;
|
|
32
|
-
telemetry = new Diagnostics();
|
|
33
|
-
diagnostics() {
|
|
34
|
-
return {
|
|
35
|
-
...this.telemetry.snapshot(),
|
|
36
|
-
phase: this.shutdownPhase,
|
|
37
|
-
queued: this.queue.length,
|
|
38
|
-
running: this.running,
|
|
39
|
-
records: this.entries.size,
|
|
40
|
-
retainedResultBytes: this.retainedResultBytes,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
closing;
|
|
44
|
-
shutdownPhase = "open";
|
|
45
|
-
constructor(options = CORE_DEFAULTS, admission = {}, scheduler = SCHEDULER_POLICY) {
|
|
46
|
-
this.options = options;
|
|
47
|
-
this.admission = admission;
|
|
48
|
-
this.scheduler = scheduler;
|
|
49
|
-
const schedulerKeys = ["queueClassOrder", "queueAgingMs", "longRunningMs"];
|
|
50
|
-
if (!scheduler ||
|
|
51
|
-
typeof scheduler !== "object" ||
|
|
52
|
-
Array.isArray(scheduler) ||
|
|
53
|
-
Reflect.ownKeys(scheduler).some((key) => typeof key !== "string" || !schedulerKeys.includes(key)))
|
|
54
|
-
throw Error(ERRORS.INVALID_OPTIONS);
|
|
55
|
-
if (!Number.isSafeInteger(scheduler.queueAgingMs) ||
|
|
56
|
-
scheduler.queueAgingMs < 1 ||
|
|
57
|
-
!Number.isSafeInteger(scheduler.longRunningMs) ||
|
|
58
|
-
scheduler.longRunningMs < 1)
|
|
59
|
-
throw Error(ERRORS.INVALID_OPTIONS);
|
|
60
|
-
const order = scheduler.queueClassOrder;
|
|
61
|
-
if (!order || typeof order !== "object" || Array.isArray(order))
|
|
62
|
-
throw Error(ERRORS.INVALID_OPTIONS);
|
|
63
|
-
for (const [className, rank] of Object.entries(order))
|
|
64
|
-
if (typeof className !== "string" ||
|
|
65
|
-
className.length === 0 ||
|
|
66
|
-
className.length > 64 ||
|
|
67
|
-
!Number.isSafeInteger(rank) ||
|
|
68
|
-
rank < 0)
|
|
69
|
-
throw Error(ERRORS.INVALID_OPTIONS);
|
|
70
|
-
this.scheduler = Object.freeze({
|
|
71
|
-
queueClassOrder: Object.freeze({ ...order }),
|
|
72
|
-
queueAgingMs: scheduler.queueAgingMs,
|
|
73
|
-
longRunningMs: scheduler.longRunningMs,
|
|
74
|
-
});
|
|
75
|
-
const allowed = ["sessionQueueLimit", "maxQueueWaitMs"];
|
|
76
|
-
if (!admission ||
|
|
77
|
-
typeof admission !== "object" ||
|
|
78
|
-
Array.isArray(admission) ||
|
|
79
|
-
Reflect.ownKeys(admission).some((key) => typeof key !== "string" || !allowed.includes(key)))
|
|
80
|
-
throw Error(ERRORS.INVALID_ADMISSION_POLICY);
|
|
81
|
-
for (const key of Reflect.ownKeys(admission)) {
|
|
82
|
-
const descriptor = Object.getOwnPropertyDescriptor(admission, key);
|
|
83
|
-
if (!("value" in descriptor) ||
|
|
84
|
-
!descriptor.enumerable ||
|
|
85
|
-
!Number.isSafeInteger(descriptor.value) ||
|
|
86
|
-
descriptor.value < 1 ||
|
|
87
|
-
descriptor.value > 2147483647)
|
|
88
|
-
throw Error(ERRORS.INVALID_ADMISSION_POLICY);
|
|
89
|
-
}
|
|
90
|
-
this.admission = Object.freeze({ ...admission });
|
|
91
|
-
const keys = [
|
|
92
|
-
"concurrency",
|
|
93
|
-
"queueLimit",
|
|
94
|
-
"recordLimit",
|
|
95
|
-
"resultBytes",
|
|
96
|
-
"retentionMs",
|
|
97
|
-
];
|
|
98
|
-
if (!options ||
|
|
99
|
-
typeof options !== "object" ||
|
|
100
|
-
Array.isArray(options) ||
|
|
101
|
-
Reflect.ownKeys(options).length !== keys.length ||
|
|
102
|
-
keys.some((key) => {
|
|
103
|
-
const descriptor = Object.getOwnPropertyDescriptor(options, key);
|
|
104
|
-
return (!descriptor || !("value" in descriptor) || !descriptor.enumerable);
|
|
105
|
-
}))
|
|
106
|
-
throw Error(ERRORS.INVALID_OPTIONS);
|
|
107
|
-
this.options = Object.freeze({ ...options });
|
|
108
|
-
for (const v of Object.values(this.options))
|
|
109
|
-
if (!Number.isSafeInteger(v) || v < 1)
|
|
110
|
-
throw Error(ERRORS.INVALID_OPTIONS);
|
|
111
|
-
}
|
|
112
|
-
register(name, handler) {
|
|
113
|
-
if (this.handlers.has(name))
|
|
114
|
-
throw Error(ERRORS.DUPLICATE_HANDLER);
|
|
115
|
-
if (!handler ||
|
|
116
|
-
typeof handler !== "object" ||
|
|
117
|
-
(handler.queue !== undefined &&
|
|
118
|
-
(typeof handler.queue !== "string" ||
|
|
119
|
-
handler.queue.length === 0 ||
|
|
120
|
-
handler.queue.length > 64)) ||
|
|
121
|
-
(handler.maxConcurrency !== undefined &&
|
|
122
|
-
(!Number.isSafeInteger(handler.maxConcurrency) ||
|
|
123
|
-
handler.maxConcurrency < 1)) ||
|
|
124
|
-
(handler.dedupe !== undefined && handler.dedupe !== "payload") ||
|
|
125
|
-
(handler.dedupe === "payload" && handler.idempotent !== true) ||
|
|
126
|
-
(handler.resources !== undefined && typeof handler.resources !== "function"))
|
|
127
|
-
throw Error(ERRORS.INVALID_HANDLER_OPTIONS);
|
|
128
|
-
const registration = { handler, token: Symbol(name) };
|
|
129
|
-
this.handlers.set(name, registration);
|
|
130
|
-
let disposed = false;
|
|
131
|
-
return () => {
|
|
132
|
-
if (disposed)
|
|
133
|
-
return;
|
|
134
|
-
disposed = true;
|
|
135
|
-
if (this.handlers.get(name)?.token === registration.token)
|
|
136
|
-
this.handlers.delete(name);
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
key(o, id) {
|
|
140
|
-
if (!o.sessionId || !o.callId)
|
|
141
|
-
throw Error(ERRORS.UNTRUSTED_CONTEXT);
|
|
142
|
-
return JSON.stringify([o.sessionId, id]);
|
|
143
|
-
}
|
|
144
|
-
emit(e, type, payload, correlationId = "") {
|
|
145
|
-
e.view.sequence++;
|
|
146
|
-
const event = freezeJson({
|
|
147
|
-
protocolVersion: 1,
|
|
148
|
-
messageId: randomUUID(),
|
|
149
|
-
requestId: e.view.requestId,
|
|
150
|
-
correlationId: correlationId || e.correlationId,
|
|
151
|
-
sequence: e.view.sequence,
|
|
152
|
-
type,
|
|
153
|
-
payload,
|
|
154
|
-
});
|
|
155
|
-
// 在调用用户代码前快照并排队:嵌套取消无法抢到当前事件。
|
|
156
|
-
e.notifications.push({ event, listeners: [...e.listeners] });
|
|
157
|
-
if (e.dispatching)
|
|
158
|
-
return;
|
|
159
|
-
e.dispatching = true;
|
|
160
|
-
try {
|
|
161
|
-
while (e.notifications.length) {
|
|
162
|
-
const batch = e.notifications.shift();
|
|
163
|
-
for (const listener of batch.listeners) {
|
|
164
|
-
if (!e.listeners.has(listener))
|
|
165
|
-
continue;
|
|
166
|
-
try {
|
|
167
|
-
void Promise.resolve(listener(batch.event)).catch(() => { });
|
|
168
|
-
}
|
|
169
|
-
catch { }
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
e.dispatching = false;
|
|
175
|
-
if (isTerminal(e.view.state))
|
|
176
|
-
e.listeners.clear();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
view(e) {
|
|
180
|
-
return structuredClone(e.view);
|
|
181
|
-
}
|
|
182
|
-
get(o, id) {
|
|
183
|
-
const e = this.entries.get(this.key(o, id));
|
|
184
|
-
if (!e)
|
|
185
|
-
throw Error(ERRORS.NOT_FOUND);
|
|
186
|
-
return e;
|
|
187
|
-
}
|
|
188
|
-
sweep(now = Date.now()) {
|
|
189
|
-
for (const [k, e] of this.entries)
|
|
190
|
-
if (isTerminal(e.view.state) && now - e.created > this.options.retentionMs) {
|
|
191
|
-
this.retainedResultBytes -= e.resultBytes;
|
|
192
|
-
this.entries.delete(k);
|
|
193
|
-
}
|
|
194
|
-
const live = new Set(this.entries.values());
|
|
195
|
-
for (const [key, e] of this.dedupeIndex)
|
|
196
|
-
if (!live.has(e))
|
|
197
|
-
this.dedupeIndex.delete(key);
|
|
198
|
-
}
|
|
199
|
-
submit(owner, raw, onEvent, invoke) {
|
|
200
|
-
if (this.closed)
|
|
201
|
-
throw Error(ERRORS.CLOSED);
|
|
202
|
-
const m = parse(raw);
|
|
203
|
-
if (m.type !== "request")
|
|
204
|
-
throw Error(ERRORS.EXPECTED_REQUEST);
|
|
205
|
-
const parts = this.requestParts(m);
|
|
206
|
-
const key = this.key(owner, m.requestId);
|
|
207
|
-
const fingerprint = hashPayload(parts.payload);
|
|
208
|
-
this.sweep();
|
|
209
|
-
const existing = this.entries.get(key);
|
|
210
|
-
return existing
|
|
211
|
-
? this.reuseInvocation(existing, m, fingerprint, onEvent)
|
|
212
|
-
: this.admitInvocation(owner, m, parts, key, fingerprint, invoke, onEvent);
|
|
213
|
-
}
|
|
214
|
-
/** 校验并拆解 request 的 payload,返回 handler 名、入参与原文。 */
|
|
215
|
-
requestParts(m) {
|
|
216
|
-
const p = m.payload;
|
|
217
|
-
if (Object.keys(p).some((k) => !["handler", "input"].includes(k)) ||
|
|
218
|
-
typeof p.handler !== "string")
|
|
219
|
-
throw Error(ERRORS.INVALID_REQUEST);
|
|
220
|
-
return {
|
|
221
|
-
handler: p.handler,
|
|
222
|
-
input: object(p.input),
|
|
223
|
-
payload: p,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
/** 同一 requestId 的重复提交:校验一致性后复用执行或结果。 */
|
|
227
|
-
reuseInvocation(e, m, fingerprint, onEvent) {
|
|
228
|
-
if (e.fingerprint !== fingerprint)
|
|
229
|
-
throw Error(ERRORS.REQUEST_CONFLICT);
|
|
230
|
-
// 仅显式声明非幂等(idempotent:false)的 handler,终态结果才不可复用:
|
|
231
|
-
// 返回"已执行"会掩盖副作用状态。运行中的重复 attach 仍放行,不会二次执行。
|
|
232
|
-
if (e.handler.idempotent === false && isTerminal(e.view.state))
|
|
233
|
-
throw Error(ERRORS.MUTATING_REUSE_DENIED);
|
|
234
|
-
const previous = e.messages.get(m.messageId);
|
|
235
|
-
if (previous && previous !== canonical(m))
|
|
236
|
-
throw Error(ERRORS.MESSAGE_CONFLICT);
|
|
237
|
-
if (!previous && e.messages.size >= LIMITS.messageAccountLimit)
|
|
238
|
-
throw Error(ERRORS.MESSAGE_LIMIT);
|
|
239
|
-
// 先取得附着,再提交消息身份:附着被拒(WAITER_LIMIT)时无副作用。
|
|
240
|
-
const attached = this.attachment(e, onEvent, m.messageId, true);
|
|
241
|
-
if (!previous)
|
|
242
|
-
e.messages.set(m.messageId, canonical(m));
|
|
243
|
-
this.telemetry.record(e.trace, "reused");
|
|
244
|
-
return attached;
|
|
245
|
-
}
|
|
246
|
-
/** 受理全新请求:校验 Handler、检查容量、构建记录并入队。 */
|
|
247
|
-
admitInvocation(owner, m, parts, key, fingerprint, invoke, onEvent) {
|
|
248
|
-
const handler = this.handlers.get(parts.handler)?.handler;
|
|
249
|
-
if (!handler)
|
|
250
|
-
throw Error(ERRORS.UNKNOWN_HANDLER);
|
|
251
|
-
handler.validate(parts.input);
|
|
252
|
-
const dedupeKey = handler.dedupe === "payload"
|
|
253
|
-
? JSON.stringify([owner.sessionId, parts.handler, fingerprint])
|
|
254
|
-
: undefined;
|
|
255
|
-
if (dedupeKey !== undefined) {
|
|
256
|
-
const target = this.dedupeIndex.get(dedupeKey);
|
|
257
|
-
if (target !== undefined &&
|
|
258
|
-
target.view.state === "succeeded" &&
|
|
259
|
-
isTerminal(target.view.state)) {
|
|
260
|
-
try {
|
|
261
|
-
const unique = this.syntheticUnique(owner, m, parts, key, fingerprint, handler, target, dedupeKey);
|
|
262
|
-
return this.attachment(unique, onEvent, m.messageId, true);
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
if (error instanceof Error && error.message === ERRORS.BACKPRESSURE)
|
|
266
|
-
throw error;
|
|
267
|
-
// 结果不可克隆时退回正常执行,避免把唯一缓存错误变成请求失败。
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const resources = this.resourcesFor(handler, parts.input);
|
|
272
|
-
if (this.entries.size >= this.options.recordLimit ||
|
|
273
|
-
this.queue.length >= this.options.queueLimit)
|
|
274
|
-
throw Error(ERRORS.BACKPRESSURE);
|
|
275
|
-
if (this.admission.sessionQueueLimit !== undefined &&
|
|
276
|
-
this.queue.filter((entry) => entry.owner === owner.sessionId).length >=
|
|
277
|
-
this.admission.sessionQueueLimit)
|
|
278
|
-
throw Error(ERRORS.SESSION_BACKPRESSURE);
|
|
279
|
-
let resolve;
|
|
280
|
-
const done = new Promise((r) => (resolve = r));
|
|
281
|
-
const e = {
|
|
282
|
-
trace: this.telemetry.begin(),
|
|
283
|
-
resultBytes: 0,
|
|
284
|
-
invoke,
|
|
285
|
-
correlationId: m.messageId,
|
|
286
|
-
owner: owner.sessionId,
|
|
287
|
-
fingerprint,
|
|
288
|
-
handler,
|
|
289
|
-
handlerName: parts.handler,
|
|
290
|
-
queueClass: handler.queue ?? "default",
|
|
291
|
-
resources,
|
|
292
|
-
dedupeKey,
|
|
293
|
-
input: structuredClone(parts.input),
|
|
294
|
-
view: {
|
|
295
|
-
requestId: m.requestId,
|
|
296
|
-
state: "queued",
|
|
297
|
-
sequence: 0,
|
|
298
|
-
cancelRequested: false,
|
|
299
|
-
delivery: "pending",
|
|
300
|
-
},
|
|
301
|
-
controller: new AbortController(),
|
|
302
|
-
done,
|
|
303
|
-
resolve,
|
|
304
|
-
listeners: new Set(),
|
|
305
|
-
notifications: [],
|
|
306
|
-
dispatching: false,
|
|
307
|
-
sourceSequence: 0,
|
|
308
|
-
created: Date.now(),
|
|
309
|
-
messages: new Map([[m.messageId, canonical(m)]]),
|
|
310
|
-
};
|
|
311
|
-
this.entries.set(key, e);
|
|
312
|
-
if (dedupeKey !== undefined)
|
|
313
|
-
this.dedupeIndex.set(dedupeKey, e);
|
|
314
|
-
this.telemetry.record(e.trace, "accepted");
|
|
315
|
-
const attached = this.attachment(e, onEvent, m.messageId);
|
|
316
|
-
this.queue.push(e);
|
|
317
|
-
this.scheduleQueueDeadline(e);
|
|
318
|
-
if (!this.readyOwners.includes(e.owner))
|
|
319
|
-
this.readyOwners.push(e.owner);
|
|
320
|
-
queueMicrotask(() => this.pump());
|
|
321
|
-
return attached;
|
|
322
|
-
}
|
|
323
|
-
/** 若启用了排队期限,登记到期结算为 QUEUE_WAIT_TIMEOUT 的计时器。 */
|
|
324
|
-
scheduleQueueDeadline(e) {
|
|
325
|
-
if (this.admission.maxQueueWaitMs === undefined)
|
|
326
|
-
return;
|
|
327
|
-
e.deadline = performance.now() + this.admission.maxQueueWaitMs;
|
|
328
|
-
const expire = () => {
|
|
329
|
-
if (e.view.state !== "queued")
|
|
330
|
-
return;
|
|
331
|
-
const remaining = e.deadline - performance.now();
|
|
332
|
-
if (remaining > 0) {
|
|
333
|
-
e.queueTimer = setTimeout(expire, Math.ceil(remaining));
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
this.removeQueued(e);
|
|
337
|
-
this.finish(e, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
|
|
338
|
-
};
|
|
339
|
-
e.queueTimer = setTimeout(expire, this.admission.maxQueueWaitMs);
|
|
340
|
-
}
|
|
341
|
-
/** 计算并校验 Handler 的资源键;失败则请求不进入队列。 */
|
|
342
|
-
resourcesFor(handler, input) {
|
|
343
|
-
if (handler.resources === undefined)
|
|
344
|
-
return [];
|
|
345
|
-
let raw;
|
|
346
|
-
try {
|
|
347
|
-
raw = handler.resources(input);
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
throw Error(ERRORS.INVALID_RESOURCES);
|
|
351
|
-
}
|
|
352
|
-
if (!Array.isArray(raw) || raw.length > 8)
|
|
353
|
-
throw Error(ERRORS.INVALID_RESOURCES);
|
|
354
|
-
const resources = [];
|
|
355
|
-
for (const resource of raw) {
|
|
356
|
-
if (typeof resource !== "string" ||
|
|
357
|
-
resource.length === 0 ||
|
|
358
|
-
resource.length > 256)
|
|
359
|
-
throw Error(ERRORS.INVALID_RESOURCES);
|
|
360
|
-
if (!resources.includes(resource))
|
|
361
|
-
resources.push(resource);
|
|
362
|
-
}
|
|
363
|
-
resources.sort();
|
|
364
|
-
return resources;
|
|
365
|
-
}
|
|
366
|
-
/** 唯一任务命中已成功终态时,构造一个不再执行 Handler 的只读结果记录。 */
|
|
367
|
-
syntheticUnique(owner, m, parts, key, fingerprint, handler, target, dedupeKey) {
|
|
368
|
-
if (this.entries.size >= this.options.recordLimit)
|
|
369
|
-
throw Error(ERRORS.BACKPRESSURE);
|
|
370
|
-
const snapshot = freezeJson(structuredClone({
|
|
371
|
-
requestId: m.requestId,
|
|
372
|
-
state: "succeeded",
|
|
373
|
-
sequence: 1,
|
|
374
|
-
cancelRequested: false,
|
|
375
|
-
delivery: "pending",
|
|
376
|
-
result: target.view.result,
|
|
377
|
-
}));
|
|
378
|
-
let resolve;
|
|
379
|
-
const done = new Promise((r) => (resolve = r));
|
|
380
|
-
const e = {
|
|
381
|
-
trace: this.telemetry.begin(),
|
|
382
|
-
resultBytes: 0,
|
|
383
|
-
invoke: undefined,
|
|
384
|
-
correlationId: m.messageId,
|
|
385
|
-
owner: owner.sessionId,
|
|
386
|
-
fingerprint,
|
|
387
|
-
handler,
|
|
388
|
-
handlerName: parts.handler,
|
|
389
|
-
queueClass: handler.queue ?? "default",
|
|
390
|
-
resources: [],
|
|
391
|
-
dedupeKey,
|
|
392
|
-
input: structuredClone(parts.input),
|
|
393
|
-
view: { ...snapshot, sequence: 1 },
|
|
394
|
-
controller: new AbortController(),
|
|
395
|
-
done,
|
|
396
|
-
resolve,
|
|
397
|
-
listeners: new Set(),
|
|
398
|
-
notifications: [],
|
|
399
|
-
dispatching: false,
|
|
400
|
-
sourceSequence: 0,
|
|
401
|
-
created: Date.now(),
|
|
402
|
-
messages: new Map([[m.messageId, canonical(m)]]),
|
|
403
|
-
};
|
|
404
|
-
this.entries.set(key, e);
|
|
405
|
-
this.telemetry.record(e.trace, "accepted");
|
|
406
|
-
this.telemetry.record(e.trace, "unique_reused");
|
|
407
|
-
e.resolve(snapshot);
|
|
408
|
-
return e;
|
|
409
|
-
}
|
|
410
|
-
releaseResources(e) {
|
|
411
|
-
for (const resource of e.resources)
|
|
412
|
-
if (this.resourceLocks.get(resource) === e)
|
|
413
|
-
this.resourceLocks.delete(resource);
|
|
414
|
-
}
|
|
415
|
-
/** 是否有空闲槽位供此 Entry 开始执行(Handler 舱壁 + 资源互斥)。 */
|
|
416
|
-
eligible(e, now) {
|
|
417
|
-
void now;
|
|
418
|
-
const limit = e.handler.maxConcurrency;
|
|
419
|
-
if (limit !== undefined &&
|
|
420
|
-
(this.runningByHandler.get(e.handler) ?? 0) >= limit)
|
|
421
|
-
return false;
|
|
422
|
-
for (const resource of e.resources)
|
|
423
|
-
if (this.resourceLocks.has(resource))
|
|
424
|
-
return false;
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
/** 类别优先级 + 等待老化:分数越低越先出队,长时间等待会逐步压过类别差异。 */
|
|
428
|
-
effectivePriority(e, now) {
|
|
429
|
-
const rank = this.scheduler.queueClassOrder[e.queueClass] ?? 3;
|
|
430
|
-
const boost = Math.min(2, Math.floor(Math.max(0, now - e.trace.at) / this.scheduler.queueAgingMs));
|
|
431
|
-
// 老化只削平类别差距,不把优先级推到负值;同分时按队列先后(FIFO)决出。
|
|
432
|
-
return Math.max(0, rank - boost);
|
|
433
|
-
}
|
|
434
|
-
/** 对已到期的排队任务结算 QUEUE_WAIT_TIMEOUT,再从队列移除。 */
|
|
435
|
-
expireQueued(now) {
|
|
436
|
-
for (const e of [...this.queue]) {
|
|
437
|
-
if (e.view.state !== "queued") {
|
|
438
|
-
this.removeQueued(e);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
if (e.deadline !== undefined && now >= e.deadline) {
|
|
442
|
-
this.removeQueued(e);
|
|
443
|
-
this.finish(e, "failed", undefined, "QUEUE_WAIT_TIMEOUT");
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
attachment(e, onEvent, id, reused = false) {
|
|
448
|
-
// 每次附着独立占配额,即使调用方复用同一个回调。
|
|
449
|
-
const listener = onEvent ? (event) => onEvent(event) : undefined;
|
|
450
|
-
if (listener && !isTerminal(e.view.state)) {
|
|
451
|
-
if (e.listeners.size >= LIMITS.waiterLimit)
|
|
452
|
-
throw Error(ERRORS.WAITER_LIMIT);
|
|
453
|
-
e.listeners.add(listener);
|
|
454
|
-
}
|
|
455
|
-
const accepted = {
|
|
456
|
-
protocolVersion: 1,
|
|
457
|
-
messageId: randomUUID(),
|
|
458
|
-
requestId: e.view.requestId,
|
|
459
|
-
correlationId: id,
|
|
460
|
-
sequence: e.view.sequence,
|
|
461
|
-
type: "accepted",
|
|
462
|
-
payload: { state: e.view.state, durable: false },
|
|
463
|
-
};
|
|
464
|
-
return {
|
|
465
|
-
accepted,
|
|
466
|
-
done: e.done,
|
|
467
|
-
/** 本次附着是否复用已有逻辑执行(结果标记可见性用)。 */
|
|
468
|
-
reused,
|
|
469
|
-
/** 当前快照:供仅在停止等待时使用,不改变交付标记。 */
|
|
470
|
-
snapshot: () => this.view(e),
|
|
471
|
-
cancel: () => this.cancel(e),
|
|
472
|
-
detach: () => {
|
|
473
|
-
if (listener)
|
|
474
|
-
e.listeners.delete(listener);
|
|
475
|
-
},
|
|
476
|
-
returned: () => {
|
|
477
|
-
if (e.view.delivery !== "returned")
|
|
478
|
-
this.telemetry.record(e.trace, "returned");
|
|
479
|
-
e.view.delivery = "returned";
|
|
480
|
-
},
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
command(owner, raw) {
|
|
484
|
-
const m = parse(raw);
|
|
485
|
-
if (m.type === "request")
|
|
486
|
-
throw Error(ERRORS.EXPECTED_CONTROL);
|
|
487
|
-
if (Object.keys(m.payload).length)
|
|
488
|
-
throw Error(ERRORS.INVALID_CONTROL);
|
|
489
|
-
this.sweep();
|
|
490
|
-
const e = this.get(owner, m.requestId);
|
|
491
|
-
const previous = e.messages.get(m.messageId), fingerprint = canonical(m);
|
|
492
|
-
if (previous && previous !== fingerprint)
|
|
493
|
-
throw Error(ERRORS.MESSAGE_CONFLICT);
|
|
494
|
-
if (!previous) {
|
|
495
|
-
// 保留一个应急控制槽:读流量绝不能阻塞取消。
|
|
496
|
-
if (e.messages.size >= LIMITS.messageAccountLimit) {
|
|
497
|
-
if (m.type !== "cancel")
|
|
498
|
-
throw Error(ERRORS.MESSAGE_LIMIT);
|
|
499
|
-
if (e.view.cancelRequested || isTerminal(e.view.state))
|
|
500
|
-
return this.view(e);
|
|
501
|
-
}
|
|
502
|
-
e.messages.set(m.messageId, fingerprint);
|
|
503
|
-
if (m.type === "cancel")
|
|
504
|
-
this.cancel(e);
|
|
505
|
-
}
|
|
506
|
-
return this.view(e);
|
|
507
|
-
}
|
|
508
|
-
removeQueued(e) {
|
|
509
|
-
this.queue = this.queue.filter((entry) => entry !== e);
|
|
510
|
-
if (!this.queue.some((entry) => entry.owner === e.owner))
|
|
511
|
-
this.readyOwners = this.readyOwners.filter((owner) => owner !== e.owner);
|
|
512
|
-
}
|
|
513
|
-
/** Trusted host owner required. Never exposes other sessions or business payloads. */
|
|
514
|
-
inspect(owner) {
|
|
515
|
-
if (!owner || typeof owner.sessionId !== "string" || !owner.sessionId)
|
|
516
|
-
throw Error(ERRORS.OWNING_SESSION_REQUIRED);
|
|
517
|
-
this.sweep();
|
|
518
|
-
const now = performance.now();
|
|
519
|
-
const records = [...this.entries.values()].filter((e) => e.owner === owner.sessionId);
|
|
520
|
-
// 活跃任务不得被更新的终态历史挤到列表之外。
|
|
521
|
-
const active = records.filter((e) => !isTerminal(e.view.state));
|
|
522
|
-
const history = records.filter((e) => isTerminal(e.view.state)).reverse();
|
|
523
|
-
const selected = [...active, ...history].slice(0, LIMITS.inspectTaskLimit);
|
|
524
|
-
const tasks = selected.map((e) => ({
|
|
525
|
-
requestId: e.view.requestId,
|
|
526
|
-
state: e.view.state,
|
|
527
|
-
delivery: e.view.delivery,
|
|
528
|
-
cancelRequested: e.view.cancelRequested,
|
|
529
|
-
error: e.view.error ?? null,
|
|
530
|
-
progress: e.view.progress ?? null,
|
|
531
|
-
waitReason: e.view.state === "queued"
|
|
532
|
-
? this.running >= this.options.concurrency
|
|
533
|
-
? "concurrency"
|
|
534
|
-
: this.eligible(e, now)
|
|
535
|
-
? "dispatch"
|
|
536
|
-
: "resource"
|
|
537
|
-
: null,
|
|
538
|
-
longRunning: e.view.state === "running" &&
|
|
539
|
-
e.startedAt !== undefined &&
|
|
540
|
-
(e.finishedAt ?? now) - e.startedAt >=
|
|
541
|
-
this.scheduler.longRunningMs,
|
|
542
|
-
phase: isTerminal(e.view.state)
|
|
543
|
-
? e.view.delivery === "returned"
|
|
544
|
-
? "returned"
|
|
545
|
-
: "terminal"
|
|
546
|
-
: e.view.cancelRequested
|
|
547
|
-
? "cancelling"
|
|
548
|
-
: e.view.state,
|
|
549
|
-
queueMs: Math.max(0, (e.startedAt ?? e.finishedAt ?? now) - e.trace.at),
|
|
550
|
-
runMs: e.startedAt === undefined
|
|
551
|
-
? 0
|
|
552
|
-
: Math.max(0, (e.finishedAt ?? now) - e.startedAt),
|
|
553
|
-
remainingQueueMs: e.view.state === "queued" && e.deadline !== undefined
|
|
554
|
-
? Math.max(0, e.deadline - now)
|
|
555
|
-
: null,
|
|
556
|
-
}));
|
|
557
|
-
return freezeJson({
|
|
558
|
-
phase: this.shutdownPhase,
|
|
559
|
-
policy: { ...this.admission },
|
|
560
|
-
counts: {
|
|
561
|
-
queued: records.filter((e) => e.view.state === "queued").length,
|
|
562
|
-
running: records.filter((e) => e.view.state === "running").length,
|
|
563
|
-
terminal: records.filter((e) => isTerminal(e.view.state)).length,
|
|
564
|
-
},
|
|
565
|
-
tasks,
|
|
566
|
-
truncated: records.length > LIMITS.inspectTaskLimit,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
cancel(e) {
|
|
570
|
-
if (isTerminal(e.view.state) || e.view.cancelRequested)
|
|
571
|
-
return;
|
|
572
|
-
e.view.cancelRequested = true;
|
|
573
|
-
this.telemetry.record(e.trace, "cancel_requested");
|
|
574
|
-
e.controller.abort();
|
|
575
|
-
this.emit(e, "cancel_requested", {});
|
|
576
|
-
if (e.view.state === "queued") {
|
|
577
|
-
this.removeQueued(e);
|
|
578
|
-
this.finish(e, "cancelled");
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
finish(e, state, result, error, resultBytes = 0) {
|
|
582
|
-
if (isTerminal(e.view.state))
|
|
583
|
-
return;
|
|
584
|
-
this.releaseResources(e);
|
|
585
|
-
if (e.dedupeKey !== undefined &&
|
|
586
|
-
this.dedupeIndex.get(e.dedupeKey) === e &&
|
|
587
|
-
state !== "succeeded")
|
|
588
|
-
this.dedupeIndex.delete(e.dedupeKey);
|
|
589
|
-
if (e.queueTimer !== undefined) {
|
|
590
|
-
clearTimeout(e.queueTimer);
|
|
591
|
-
e.queueTimer = undefined;
|
|
592
|
-
}
|
|
593
|
-
e.finishedAt = performance.now();
|
|
594
|
-
// 终态事件是下一个序号;先用它构建快照,避免把不可逆终态提交到视图之外。
|
|
595
|
-
const terminalSequence = e.view.sequence + 1;
|
|
596
|
-
let next = { ...e.view, state, sequence: terminalSequence };
|
|
597
|
-
if (result !== undefined)
|
|
598
|
-
next.result = result;
|
|
599
|
-
if (error)
|
|
600
|
-
next.error = error;
|
|
601
|
-
let snapshot;
|
|
602
|
-
try {
|
|
603
|
-
snapshot = freezeJson(structuredClone(next));
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
606
|
-
state = "unknown";
|
|
607
|
-
resultBytes = 0;
|
|
608
|
-
next = {
|
|
609
|
-
requestId: e.view.requestId,
|
|
610
|
-
state,
|
|
611
|
-
sequence: terminalSequence,
|
|
612
|
-
cancelRequested: e.view.cancelRequested,
|
|
613
|
-
delivery: e.view.delivery,
|
|
614
|
-
error: "RESULT_NOT_RETAINED",
|
|
615
|
-
};
|
|
616
|
-
snapshot = freezeJson({ ...next });
|
|
617
|
-
}
|
|
618
|
-
e.resultBytes = resultBytes;
|
|
619
|
-
this.retainedResultBytes += resultBytes;
|
|
620
|
-
e.created = Date.now();
|
|
621
|
-
// emit() 会再自增序号;先整体回退一档,使终态事件恰好落在 terminalSequence,
|
|
622
|
-
// 与上面交付给等待者的快照序号完全一致。
|
|
623
|
-
e.view = { ...next, sequence: terminalSequence - 1 };
|
|
624
|
-
this.telemetry.record(e.trace, "terminal");
|
|
625
|
-
e.resolve(snapshot);
|
|
626
|
-
this.emit(e, state, { state });
|
|
627
|
-
// 重入的终态通知可能仍排在本批之后。
|
|
628
|
-
if (!e.dispatching)
|
|
629
|
-
e.listeners.clear();
|
|
630
|
-
}
|
|
631
|
-
/** 按会话轮转选择下一个可执行项:会话内按类别优先级 + 老化,跨会话公平。 */
|
|
632
|
-
nextQueuedIndex(now) {
|
|
633
|
-
const owners = this.readyOwners.length;
|
|
634
|
-
for (let n = 0; n < owners; n++) {
|
|
635
|
-
const owner = this.readyOwners.shift();
|
|
636
|
-
let chosen = -1;
|
|
637
|
-
let chosenScore = Infinity;
|
|
638
|
-
for (let i = 0; i < this.queue.length; i++) {
|
|
639
|
-
const e = this.queue[i];
|
|
640
|
-
if (e.owner !== owner || !this.eligible(e, now))
|
|
641
|
-
continue;
|
|
642
|
-
const score = this.effectivePriority(e, now);
|
|
643
|
-
if (score < chosenScore) {
|
|
644
|
-
chosen = i;
|
|
645
|
-
chosenScore = score;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (chosen < 0) {
|
|
649
|
-
// 该会话暂时全部被舱壁/资源阻塞;保留轮次,等释放后重新争取。
|
|
650
|
-
if (this.queue.some((entry) => entry.owner === owner))
|
|
651
|
-
this.readyOwners.push(owner);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
if (this.queue.some((entry, i) => i !== chosen && entry.owner === owner))
|
|
655
|
-
this.readyOwners.push(owner);
|
|
656
|
-
return chosen;
|
|
657
|
-
}
|
|
658
|
-
return -1;
|
|
659
|
-
}
|
|
660
|
-
pump() {
|
|
661
|
-
while (!this.closed) {
|
|
662
|
-
const now = performance.now();
|
|
663
|
-
this.expireQueued(now);
|
|
664
|
-
if (this.running >= this.options.concurrency || this.queue.length === 0)
|
|
665
|
-
return;
|
|
666
|
-
const index = this.nextQueuedIndex(now);
|
|
667
|
-
if (index < 0)
|
|
668
|
-
return;
|
|
669
|
-
const e = this.queue.splice(index, 1)[0];
|
|
670
|
-
if (e.view.state !== "queued")
|
|
671
|
-
continue;
|
|
672
|
-
if (e.queueTimer !== undefined) {
|
|
673
|
-
clearTimeout(e.queueTimer);
|
|
674
|
-
e.queueTimer = undefined;
|
|
675
|
-
}
|
|
676
|
-
e.startedAt = performance.now();
|
|
677
|
-
this.running++;
|
|
678
|
-
this.runningByHandler.set(e.handler, (this.runningByHandler.get(e.handler) ?? 0) + 1);
|
|
679
|
-
for (const resource of e.resources)
|
|
680
|
-
this.resourceLocks.set(resource, e);
|
|
681
|
-
e.view.state = "running";
|
|
682
|
-
this.telemetry.record(e.trace, "started");
|
|
683
|
-
this.emit(e, "started", {});
|
|
684
|
-
void this.run(e).finally(() => {
|
|
685
|
-
this.running--;
|
|
686
|
-
const remaining = (this.runningByHandler.get(e.handler) ?? 1) - 1;
|
|
687
|
-
if (remaining > 0)
|
|
688
|
-
this.runningByHandler.set(e.handler, remaining);
|
|
689
|
-
else
|
|
690
|
-
this.runningByHandler.delete(e.handler);
|
|
691
|
-
this.pump();
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
async run(e) {
|
|
696
|
-
try {
|
|
697
|
-
const result = await e.handler.run(e.input, {
|
|
698
|
-
invoke: e.invoke,
|
|
699
|
-
signal: e.controller.signal,
|
|
700
|
-
requestId: e.view.requestId,
|
|
701
|
-
progress: (seq, pct) => {
|
|
702
|
-
if (e.view.state !== "running" ||
|
|
703
|
-
!Number.isSafeInteger(seq) ||
|
|
704
|
-
seq <= e.sourceSequence ||
|
|
705
|
-
!Number.isFinite(pct) ||
|
|
706
|
-
pct < 0 ||
|
|
707
|
-
pct > 100)
|
|
708
|
-
return;
|
|
709
|
-
e.sourceSequence = seq;
|
|
710
|
-
e.view.progress = pct;
|
|
711
|
-
this.emit(e, "progress", { percent: pct });
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
let json;
|
|
715
|
-
try {
|
|
716
|
-
assertJson(result);
|
|
717
|
-
json = JSON.stringify(result);
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
this.finish(e, "unknown", undefined, "RESULT_NOT_RETAINED");
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
if (json === undefined ||
|
|
724
|
-
Buffer.byteLength(json) > this.options.resultBytes) {
|
|
725
|
-
this.finish(e, "unknown", undefined, "RESULT_NOT_RETAINED");
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
this.finish(e, "succeeded", JSON.parse(json), undefined, Buffer.byteLength(json));
|
|
729
|
-
}
|
|
730
|
-
catch (err) {
|
|
731
|
-
const cancelled = e.controller.signal.aborted &&
|
|
732
|
-
err instanceof Error &&
|
|
733
|
-
err.name === "AbortError";
|
|
734
|
-
this.finish(e, cancelled
|
|
735
|
-
? "cancelled"
|
|
736
|
-
: e.controller.signal.aborted
|
|
737
|
-
? "unknown"
|
|
738
|
-
: "failed", undefined, cancelled
|
|
739
|
-
? "CANCELLED"
|
|
740
|
-
: e.controller.signal.aborted
|
|
741
|
-
? "CANCELLATION_UNCONFIRMED"
|
|
742
|
-
: "HANDLER_FAILED");
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
close() {
|
|
746
|
-
if (this.closing)
|
|
747
|
-
return this.closing;
|
|
748
|
-
this.closed = true;
|
|
749
|
-
this.shutdownPhase = "cancelling";
|
|
750
|
-
let resolve, reject;
|
|
751
|
-
this.closing = new Promise((r, j) => {
|
|
752
|
-
resolve = r;
|
|
753
|
-
reject = j;
|
|
754
|
-
});
|
|
755
|
-
void (async () => {
|
|
756
|
-
const entries = [...this.entries.values()];
|
|
757
|
-
for (const e of entries)
|
|
758
|
-
this.cancel(e);
|
|
759
|
-
this.shutdownPhase = "draining";
|
|
760
|
-
await Promise.all(entries.map((e) => e.done));
|
|
761
|
-
this.handlers.clear();
|
|
762
|
-
this.entries.clear();
|
|
763
|
-
this.dedupeIndex.clear();
|
|
764
|
-
this.resourceLocks.clear();
|
|
765
|
-
this.runningByHandler.clear();
|
|
766
|
-
this.retainedResultBytes = 0;
|
|
767
|
-
this.queue = [];
|
|
768
|
-
this.readyOwners = [];
|
|
769
|
-
this.shutdownPhase = "closed";
|
|
770
|
-
})().then(resolve, reject);
|
|
771
|
-
return this.closing;
|
|
772
|
-
}
|
|
773
|
-
}
|