@actiondock/core 2.0.12 → 2.2.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 +61 -30
- package/dist/app/app.d.ts +73 -0
- package/dist/app/app.js +700 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +2 -0
- package/dist/app/types.d.ts +283 -0
- package/dist/app/types.js +1 -0
- package/dist/catalog/action-index.d.ts +1 -1
- package/dist/catalog/action-index.js +93 -0
- package/dist/catalog/action-resolver.d.ts +2 -2
- package/dist/catalog/action-resolver.js +95 -0
- package/dist/catalog/index.d.ts +5 -5
- package/dist/catalog/index.js +5 -0
- package/dist/catalog/location-registry.d.ts +5 -5
- package/dist/catalog/location-registry.js +138 -0
- package/dist/catalog/package-catalog.d.ts +2 -2
- package/dist/catalog/package-catalog.js +76 -0
- package/dist/catalog/types.d.ts +2 -2
- package/dist/catalog/types.js +1 -0
- package/dist/doctor/doctor.d.ts +2 -1
- package/dist/doctor/doctor.js +613 -0
- package/dist/doctor/index.d.ts +2 -2
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/types.js +1 -0
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +123 -0
- package/dist/execution/index.d.ts +2 -2
- package/dist/execution/index.js +2 -0
- package/dist/execution/service.d.ts +46 -41
- package/dist/execution/service.js +633 -0
- package/dist/execution/types.d.ts +59 -2
- package/dist/execution/types.js +1 -0
- package/dist/export/index.d.ts +1 -1
- package/dist/export/index.js +1 -0
- package/dist/export/templates.d.ts +45 -7
- package/dist/export/templates.js +621 -0
- package/dist/filter/index.d.ts +1 -1
- package/dist/filter/index.js +1 -0
- package/dist/filter/intent.js +123 -0
- package/dist/host/host.d.ts +55 -0
- package/dist/host/host.js +740 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.js +2 -0
- package/dist/host/types.d.ts +81 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +21 -16
- package/dist/index.js +20 -8458
- package/dist/ipc/diagnostic.d.ts +65 -0
- package/dist/ipc/diagnostic.js +129 -0
- package/dist/ipc/host.d.ts +7 -0
- package/dist/ipc/host.js +93 -0
- package/dist/ipc/index.d.ts +4 -0
- package/dist/ipc/index.js +4 -0
- package/dist/ipc/target.d.ts +56 -0
- package/dist/ipc/target.js +297 -0
- package/dist/ipc/types.d.ts +42 -0
- package/dist/ipc/types.js +1 -0
- package/dist/platform/default.d.ts +32 -0
- package/dist/platform/default.js +40 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/node-fs.d.ts +37 -0
- package/dist/platform/node-fs.js +81 -0
- package/dist/platform/types.d.ts +104 -0
- package/dist/platform/types.js +1 -0
- package/dist/profile/client.d.ts +58 -12
- package/dist/profile/client.js +467 -0
- package/dist/profile/index.d.ts +3 -3
- package/dist/profile/index.js +3 -0
- package/dist/profile/manager.d.ts +2 -2
- package/dist/profile/manager.js +294 -0
- package/dist/profile/types.d.ts +1 -1
- package/dist/profile/types.js +1 -0
- package/dist/project/closure.js +68 -0
- package/dist/project/digest.d.ts +29 -0
- package/dist/project/digest.js +324 -0
- package/dist/project/index.d.ts +10 -5
- package/dist/project/index.js +10 -0
- package/dist/project/init.d.ts +3 -4
- package/dist/project/init.js +175 -0
- package/dist/project/loader.d.ts +21 -27
- package/dist/project/loader.js +484 -0
- package/dist/project/lockfile.d.ts +55 -0
- package/dist/project/lockfile.js +175 -0
- package/dist/project/manifest.d.ts +6 -28
- package/dist/project/manifest.js +220 -0
- package/dist/project/resolver.d.ts +116 -0
- package/dist/project/resolver.js +376 -0
- package/dist/project/transactions.d.ts +103 -0
- package/dist/project/transactions.js +1203 -0
- package/dist/project/types-generator.d.ts +33 -0
- package/dist/project/types-generator.js +168 -0
- package/dist/project/types.d.ts +58 -82
- package/dist/project/types.js +1 -0
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +5 -0
- package/dist/registry/lock.d.ts +27 -0
- package/dist/registry/lock.js +93 -0
- package/dist/registry/registry.d.ts +32 -11
- package/dist/registry/registry.js +611 -0
- package/dist/registry/resolve.d.ts +62 -0
- package/dist/registry/resolve.js +195 -0
- package/dist/registry/scan.d.ts +38 -0
- package/dist/registry/scan.js +135 -0
- package/dist/registry/types.d.ts +1 -1
- package/dist/registry/types.js +1 -0
- package/dist/runtime/action-collection.d.ts +27 -0
- package/dist/runtime/action-collection.js +57 -0
- package/dist/runtime/clock.d.ts +0 -2
- package/dist/runtime/clock.js +17 -0
- package/dist/runtime/context.d.ts +16 -5
- package/dist/runtime/context.js +242 -0
- package/dist/runtime/env.d.ts +3 -1
- package/dist/runtime/env.js +159 -0
- package/dist/runtime/events.d.ts +46 -14
- package/dist/runtime/events.js +604 -0
- package/dist/runtime/execution-manager.d.ts +1 -1
- package/dist/runtime/execution-manager.js +66 -0
- package/dist/runtime/index.d.ts +10 -9
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/module-loader.d.ts +0 -8
- package/dist/runtime/module-loader.js +16 -0
- package/dist/runtime/process.d.ts +2 -4
- package/dist/runtime/process.js +146 -0
- package/dist/runtime/runner.d.ts +117 -10
- package/dist/runtime/runner.js +984 -0
- package/dist/runtime/standalone.d.ts +68 -19
- package/dist/runtime/standalone.js +536 -0
- package/dist/schema/validator.js +116 -0
- package/dist/server/body.js +93 -0
- package/dist/server/index.d.ts +5 -6
- package/dist/server/index.js +5 -0
- package/dist/server/routes/actions.d.ts +2 -2
- package/dist/server/routes/actions.js +298 -0
- package/dist/server/routes/common.d.ts +28 -17
- package/dist/server/routes/common.js +104 -0
- package/dist/server/routes/config.d.ts +2 -1
- package/dist/server/routes/config.js +173 -0
- package/dist/server/routes/doctor.d.ts +2 -2
- package/dist/server/routes/doctor.js +63 -0
- package/dist/server/routes/health.d.ts +2 -2
- package/dist/server/routes/health.js +35 -0
- package/dist/server/routes/index.d.ts +9 -9
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/info.d.ts +4 -2
- package/dist/server/routes/info.js +158 -0
- package/dist/server/routes/playbooks.d.ts +2 -2
- package/dist/server/routes/playbooks.js +134 -0
- package/dist/server/routes/runs.d.ts +2 -2
- package/dist/server/routes/runs.js +343 -0
- package/dist/server/routes/state.d.ts +2 -1
- package/dist/server/routes/state.js +169 -0
- package/dist/server/security.d.ts +11 -4
- package/dist/server/security.js +101 -0
- package/dist/server/server.d.ts +6 -6
- package/dist/server/server.js +355 -0
- package/dist/server/types.d.ts +27 -8
- package/dist/server/types.js +1 -0
- package/dist/storage/data-dir-lock.d.ts +119 -0
- package/dist/storage/data-dir-lock.js +1016 -0
- package/dist/storage/driver.d.ts +4 -10
- package/dist/storage/driver.js +59 -0
- package/dist/storage/index.d.ts +30 -10
- package/dist/storage/index.js +98 -0
- package/dist/storage/lazy.d.ts +8 -0
- package/dist/storage/lazy.js +44 -0
- package/dist/storage/mask.d.ts +12 -5
- package/dist/storage/mask.js +48 -0
- package/dist/storage/params.d.ts +13 -0
- package/dist/storage/params.js +32 -0
- package/dist/storage/sqlite.d.ts +19 -6
- package/dist/storage/sqlite.js +689 -0
- package/dist/storage/types.d.ts +57 -6
- package/dist/storage/types.js +14 -0
- package/dist/target/index.d.ts +4 -0
- package/dist/target/index.js +4 -0
- package/dist/target/local.d.ts +52 -0
- package/dist/target/local.js +343 -0
- package/dist/target/remote.d.ts +76 -0
- package/dist/target/remote.js +839 -0
- package/dist/target/target.d.ts +6 -0
- package/dist/target/target.js +35 -0
- package/dist/target/types.d.ts +246 -0
- package/dist/target/types.js +34 -0
- package/dist/utils/index.d.ts +22 -2
- package/dist/utils/index.js +180 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +4 -0
- package/package.json +6 -7
- package/dist/build/index.d.ts +0 -1
- package/dist/build/templates.d.ts +0 -19
- package/dist/server/runtime-registry.d.ts +0 -78
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* 订阅者通道:封装实时事件队列、监听器与终止状态。
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 接收实时推送事件并入队(超出背压上限时推送终止事件并切断通道)。
|
|
6
|
+
* - 维护 done 终止标记与唤醒回调,供生成器编排消费。
|
|
7
|
+
* - 暴露 inactive 注销钩子,供外部在通道切断时注销监听器。
|
|
8
|
+
*/
|
|
9
|
+
class SubscriberChannel {
|
|
10
|
+
/** 实时事件队列(含背压终止事件) */
|
|
11
|
+
liveQueue = [];
|
|
12
|
+
/** 是否已收到终止信号(finish、淘汰、中断或唤醒) */
|
|
13
|
+
done = false;
|
|
14
|
+
/** 实时监听器(入队与背压终止逻辑入口) */
|
|
15
|
+
listener;
|
|
16
|
+
/** 通道被切断或订阅退出时的注销回调(由订阅方注入) */
|
|
17
|
+
onInactive = null;
|
|
18
|
+
/** 等待挂起期间的唤醒回调(由等待方注入) */
|
|
19
|
+
notify = null;
|
|
20
|
+
runId;
|
|
21
|
+
maxQueueSize;
|
|
22
|
+
cursor;
|
|
23
|
+
allocateEventId;
|
|
24
|
+
backpressureTerminated = false;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.runId = options.runId;
|
|
27
|
+
this.maxQueueSize = options.maxQueueSize;
|
|
28
|
+
this.cursor = options.cursor;
|
|
29
|
+
this.allocateEventId = options.allocateEventId;
|
|
30
|
+
this.listener = (evt) => this.receive(evt);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 接收单条实时事件:队列满时推送背压终止事件并切断通道。
|
|
34
|
+
*/
|
|
35
|
+
receive(evt) {
|
|
36
|
+
if (this.backpressureTerminated || this.done) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (this.liveQueue.length >= this.maxQueueSize) {
|
|
40
|
+
// 慢订阅者导致队列溢出:向该慢订阅者推送终止事件并切断通道,主运行不受反向阻塞
|
|
41
|
+
this.backpressureTerminated = true;
|
|
42
|
+
this.onInactive?.();
|
|
43
|
+
const termEvt = {
|
|
44
|
+
runId: this.runId,
|
|
45
|
+
rootRunId: evt.rootRunId || this.runId,
|
|
46
|
+
sequence: evt.sequence + 1,
|
|
47
|
+
eventId: this.allocateEventId(),
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
type: "error",
|
|
50
|
+
error: {
|
|
51
|
+
code: EVENT_BACKPRESSURE_LIMIT,
|
|
52
|
+
message: `Event subscription queue exceeded limit of ${this.maxQueueSize} items due to slow subscriber`,
|
|
53
|
+
details: {
|
|
54
|
+
lastConfirmedCursor: this.cursor.lastConfirmedEventId ??
|
|
55
|
+
(this.cursor.lastConfirmedSequence >= 0 ? String(this.cursor.lastConfirmedSequence) : "0"),
|
|
56
|
+
lastConfirmedEventId: this.cursor.lastConfirmedEventId,
|
|
57
|
+
lastConfirmedSequence: this.cursor.lastConfirmedSequence,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
this.liveQueue.length = 0;
|
|
62
|
+
this.liveQueue.push(termEvt);
|
|
63
|
+
if (this.notify) {
|
|
64
|
+
this.notify();
|
|
65
|
+
this.notify = null;
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.liveQueue.push(evt);
|
|
70
|
+
if (this.notify) {
|
|
71
|
+
this.notify();
|
|
72
|
+
this.notify = null;
|
|
73
|
+
}
|
|
74
|
+
if (evt.type === "finish") {
|
|
75
|
+
this.done = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 标记通道终止并唤醒挂起的等待方。
|
|
80
|
+
*/
|
|
81
|
+
terminate() {
|
|
82
|
+
this.done = true;
|
|
83
|
+
if (this.notify) {
|
|
84
|
+
this.notify();
|
|
85
|
+
this.notify = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 计算单个事件的 UTF-8 JSON 字节长度。
|
|
91
|
+
*/
|
|
92
|
+
function estimateEventBytes(event) {
|
|
93
|
+
try {
|
|
94
|
+
return Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return 256;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 进程内有界事件缓冲区实现。
|
|
102
|
+
* 遵循设计规范:
|
|
103
|
+
* - 单运行双配额保护:上限 1024 条事件或 1MiB 字节数,优先淘汰日志与进度,保护终态。
|
|
104
|
+
* - 全局有界回收:限制最大运行数(默认 500),溢出时优先淘汰已完成运行。
|
|
105
|
+
* - 原子订阅衔接:先注册实时队列再回放历史快照,去重并杜绝并发丢事件窗口。
|
|
106
|
+
* - 严格生命周期清理:迭代退出或中断时彻底注销监听器与 AbortSignal 事件。
|
|
107
|
+
* - 慢订阅者背压保护:队列超出上限时推送终止事件并切断通道,主运行不受阻。
|
|
108
|
+
* - 游标断点续传:支持全局持久化 eventId 与序列号,过期游标返回 EVENT_CURSOR_EXPIRED。
|
|
109
|
+
*/
|
|
110
|
+
export class InMemoryEventSink {
|
|
111
|
+
runs = new Map();
|
|
112
|
+
listeners = new Map();
|
|
113
|
+
wakeups = new Map();
|
|
114
|
+
evictions = new Map();
|
|
115
|
+
evictedRuns = new Set();
|
|
116
|
+
globalEventSeq = 0;
|
|
117
|
+
maxEventsPerRun;
|
|
118
|
+
maxBytesPerRun;
|
|
119
|
+
maxRuns;
|
|
120
|
+
maxSubscriberQueueSize;
|
|
121
|
+
constructor(options = {}) {
|
|
122
|
+
this.maxEventsPerRun = options.maxEventsPerRun ?? 1024;
|
|
123
|
+
this.maxBytesPerRun = options.maxBytesPerRun ?? 1024 * 1024; // 1 MiB
|
|
124
|
+
this.maxRuns = options.maxRuns ?? 500;
|
|
125
|
+
this.maxSubscriberQueueSize = options.maxSubscriberQueueSize ?? 1024;
|
|
126
|
+
}
|
|
127
|
+
emit(event) {
|
|
128
|
+
const runId = event.runId;
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
let run = this.runs.get(runId);
|
|
131
|
+
let processedEvent = event;
|
|
132
|
+
if (!processedEvent.eventId) {
|
|
133
|
+
processedEvent = {
|
|
134
|
+
...processedEvent,
|
|
135
|
+
eventId: String(++this.globalEventSeq),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (!run) {
|
|
139
|
+
// 全局有界淘汰
|
|
140
|
+
if (this.runs.size >= this.maxRuns) {
|
|
141
|
+
this.evictOldestRun();
|
|
142
|
+
}
|
|
143
|
+
run = {
|
|
144
|
+
entries: [],
|
|
145
|
+
totalBytes: 0,
|
|
146
|
+
lastAccessedAt: now,
|
|
147
|
+
isTerminal: false,
|
|
148
|
+
earliestRetainedSequence: processedEvent.sequence,
|
|
149
|
+
earliestRetainedEventId: processedEvent.eventId,
|
|
150
|
+
};
|
|
151
|
+
this.runs.set(runId, run);
|
|
152
|
+
this.evictedRuns.delete(runId);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
run.lastAccessedAt = now;
|
|
156
|
+
if (run.earliestRetainedSequence === undefined) {
|
|
157
|
+
run.earliestRetainedSequence = processedEvent.sequence;
|
|
158
|
+
run.earliestRetainedEventId = processedEvent.eventId;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let eventBytes = estimateEventBytes(processedEvent);
|
|
162
|
+
// 如果单条事件本身超过单运行最大字节限制,尝试截断 log / finish 数据
|
|
163
|
+
if (eventBytes > this.maxBytesPerRun) {
|
|
164
|
+
if (processedEvent.type === "log" && typeof processedEvent.message === "string") {
|
|
165
|
+
const keepChars = Math.max(0, Math.floor(this.maxBytesPerRun / 2));
|
|
166
|
+
processedEvent = {
|
|
167
|
+
...processedEvent,
|
|
168
|
+
message: `${processedEvent.message.slice(0, keepChars)}... [TRUNCATED]`,
|
|
169
|
+
data: undefined,
|
|
170
|
+
};
|
|
171
|
+
eventBytes = estimateEventBytes(processedEvent);
|
|
172
|
+
}
|
|
173
|
+
else if (processedEvent.type === "finish" && processedEvent.result) {
|
|
174
|
+
if (processedEvent.result.ok) {
|
|
175
|
+
processedEvent = {
|
|
176
|
+
...processedEvent,
|
|
177
|
+
result: {
|
|
178
|
+
ok: true,
|
|
179
|
+
runId: processedEvent.result.runId,
|
|
180
|
+
data: { _truncated: true, message: "Output exceeded maximum quota" },
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const err = processedEvent.result.error;
|
|
186
|
+
const keepChars = Math.max(0, Math.min(200, Math.floor(this.maxBytesPerRun / 2)));
|
|
187
|
+
const truncatedMsg = typeof err?.message === "string"
|
|
188
|
+
? `${err.message.slice(0, keepChars)}... [TRUNCATED]`
|
|
189
|
+
: "Error exceeded maximum quota";
|
|
190
|
+
processedEvent = {
|
|
191
|
+
...processedEvent,
|
|
192
|
+
result: {
|
|
193
|
+
ok: false,
|
|
194
|
+
runId: processedEvent.result.runId,
|
|
195
|
+
error: {
|
|
196
|
+
code: err?.code || "EXECUTION_ERROR",
|
|
197
|
+
message: truncatedMsg,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
eventBytes = estimateEventBytes(processedEvent);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 严格限制单运行缓存字节配额,超标事件不可入队,杜绝突破 maxBytesPerRun
|
|
206
|
+
if (eventBytes <= this.maxBytesPerRun) {
|
|
207
|
+
// 检查是否超出单运行条数或字节配额,按优先级淘汰
|
|
208
|
+
while (run.entries.length > 0 &&
|
|
209
|
+
(run.entries.length >= this.maxEventsPerRun ||
|
|
210
|
+
run.totalBytes + eventBytes > this.maxBytesPerRun)) {
|
|
211
|
+
// 优先淘汰非核心事件:log 或 progress
|
|
212
|
+
const nonEssentialIndex = run.entries.findIndex((e) => e.event.type === "log" || e.event.type === "progress");
|
|
213
|
+
if (nonEssentialIndex >= 0) {
|
|
214
|
+
const removed = run.entries.splice(nonEssentialIndex, 1)[0];
|
|
215
|
+
run.totalBytes -= removed.bytes;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// 无非核心事件时,淘汰最早的非 finish 事件
|
|
219
|
+
const nonTerminalIndex = run.entries.findIndex((e) => e.event.type !== "finish");
|
|
220
|
+
if (nonTerminalIndex >= 0) {
|
|
221
|
+
const removed = run.entries.splice(nonTerminalIndex, 1)[0];
|
|
222
|
+
run.totalBytes -= removed.bytes;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const removed = run.entries.shift();
|
|
226
|
+
run.totalBytes -= removed.bytes;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (run.entries.length > 0) {
|
|
231
|
+
run.earliestRetainedSequence = run.entries[0].event.sequence;
|
|
232
|
+
run.earliestRetainedEventId = run.entries[0].event.eventId;
|
|
233
|
+
}
|
|
234
|
+
if (run.totalBytes + eventBytes <= this.maxBytesPerRun) {
|
|
235
|
+
run.entries.push({ event: processedEvent, bytes: eventBytes });
|
|
236
|
+
run.totalBytes += eventBytes;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// 仅在接收到 finish 事件时才标记已终态,防止在终态 status 与 finish 之间的窗口内订阅过早结束
|
|
240
|
+
if (processedEvent.type === "finish") {
|
|
241
|
+
run.isTerminal = true;
|
|
242
|
+
}
|
|
243
|
+
// 广播至当前活跃监听器
|
|
244
|
+
const subs = this.listeners.get(runId);
|
|
245
|
+
if (subs && subs.size > 0) {
|
|
246
|
+
for (const listener of subs) {
|
|
247
|
+
try {
|
|
248
|
+
listener(processedEvent);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// 忽略单个监听器内部异常
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async *subscribe(runId, options = {}) {
|
|
257
|
+
let afterSequence;
|
|
258
|
+
let afterEventId;
|
|
259
|
+
if (options.after !== undefined) {
|
|
260
|
+
if (typeof options.after === "number") {
|
|
261
|
+
afterSequence = options.after;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const trimmed = String(options.after).trim();
|
|
265
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
266
|
+
afterSequence = parseInt(trimmed, 10);
|
|
267
|
+
}
|
|
268
|
+
afterEventId = trimmed;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const effectiveAfter = afterSequence ?? -1;
|
|
272
|
+
const cursor = {
|
|
273
|
+
lastYieldedSequence: effectiveAfter,
|
|
274
|
+
lastConfirmedEventId: afterEventId,
|
|
275
|
+
lastConfirmedSequence: effectiveAfter,
|
|
276
|
+
};
|
|
277
|
+
if (options.signal?.aborted || this.evictedRuns.has(runId)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// 检查游标是否在请求前已过期被清理
|
|
281
|
+
const run = this.runs.get(runId);
|
|
282
|
+
if (run && options.after !== undefined) {
|
|
283
|
+
this.assertCursorRetained(run, options.after, afterSequence);
|
|
284
|
+
}
|
|
285
|
+
const channel = new SubscriberChannel({
|
|
286
|
+
runId,
|
|
287
|
+
maxQueueSize: options.maxQueueSize ?? this.maxSubscriberQueueSize,
|
|
288
|
+
cursor,
|
|
289
|
+
allocateEventId: () => String(++this.globalEventSeq),
|
|
290
|
+
});
|
|
291
|
+
let subs = this.listeners.get(runId);
|
|
292
|
+
if (!subs) {
|
|
293
|
+
subs = new Set();
|
|
294
|
+
this.listeners.set(runId, subs);
|
|
295
|
+
}
|
|
296
|
+
const cleanupListener = () => {
|
|
297
|
+
subs?.delete(channel.listener);
|
|
298
|
+
if (subs && subs.size === 0) {
|
|
299
|
+
this.listeners.delete(runId);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
channel.onInactive = cleanupListener;
|
|
303
|
+
let evictSet = this.evictions.get(runId);
|
|
304
|
+
if (!evictSet) {
|
|
305
|
+
evictSet = new Set();
|
|
306
|
+
this.evictions.set(runId, evictSet);
|
|
307
|
+
}
|
|
308
|
+
const onEvict = () => channel.terminate();
|
|
309
|
+
const onAbort = () => channel.terminate();
|
|
310
|
+
let currentWakeupCleanup = null;
|
|
311
|
+
// 先挂载实时监听器,防止历史回放与监听注册之间的竞态丢事件
|
|
312
|
+
subs.add(channel.listener);
|
|
313
|
+
evictSet.add(onEvict);
|
|
314
|
+
if (options.signal) {
|
|
315
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
316
|
+
}
|
|
317
|
+
const cleanup = () => {
|
|
318
|
+
if (currentWakeupCleanup) {
|
|
319
|
+
currentWakeupCleanup();
|
|
320
|
+
currentWakeupCleanup = null;
|
|
321
|
+
}
|
|
322
|
+
cleanupListener();
|
|
323
|
+
evictSet?.delete(onEvict);
|
|
324
|
+
if (evictSet && evictSet.size === 0) {
|
|
325
|
+
this.evictions.delete(runId);
|
|
326
|
+
}
|
|
327
|
+
if (options.signal) {
|
|
328
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
try {
|
|
332
|
+
// 回放历史快照
|
|
333
|
+
if (run) {
|
|
334
|
+
run.lastAccessedAt = Date.now();
|
|
335
|
+
const historySnapshot = run.entries.map((e) => e.event);
|
|
336
|
+
for (const evt of historySnapshot) {
|
|
337
|
+
if (options.signal?.aborted)
|
|
338
|
+
return;
|
|
339
|
+
if (evt.sequence > cursor.lastYieldedSequence) {
|
|
340
|
+
this.trackDeliveredEvent(cursor, evt);
|
|
341
|
+
yield evt;
|
|
342
|
+
if (this.isTerminalDelivery(evt)) {
|
|
343
|
+
channel.done = true;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// 若该运行已终态且历史中已无 finish 事件,先排空实时队列已到达的事件
|
|
349
|
+
if (run.isTerminal) {
|
|
350
|
+
while (channel.liveQueue.length > 0) {
|
|
351
|
+
const evt = this.takeNextDeliverable(channel, cursor);
|
|
352
|
+
if (evt) {
|
|
353
|
+
yield evt;
|
|
354
|
+
if (this.isTerminalDelivery(evt)) {
|
|
355
|
+
channel.done = true;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
channel.done = true;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// 无缝衔接消费实时队列中的事件
|
|
365
|
+
while ((channel.liveQueue.length > 0 || !channel.done) && !options.signal?.aborted) {
|
|
366
|
+
if (this.evictedRuns.has(runId)) {
|
|
367
|
+
channel.done = true;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (channel.liveQueue.length > 0) {
|
|
371
|
+
const evt = this.takeNextDeliverable(channel, cursor);
|
|
372
|
+
if (evt) {
|
|
373
|
+
yield evt;
|
|
374
|
+
if (this.isTerminalDelivery(evt)) {
|
|
375
|
+
channel.done = true;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
if (this.evictedRuns.has(runId)) {
|
|
382
|
+
channel.done = true;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
await this.waitForNextEvent(runId, channel, (cleanupFn) => {
|
|
386
|
+
currentWakeupCleanup = cleanupFn;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// 消费中止或完成瞬间积压在 liveQueue 中的剩余事件
|
|
391
|
+
while (channel.liveQueue.length > 0) {
|
|
392
|
+
const evt = this.takeNextDeliverable(channel, cursor);
|
|
393
|
+
if (evt) {
|
|
394
|
+
yield evt;
|
|
395
|
+
if (this.isTerminalDelivery(evt)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
cleanup();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 断言请求游标仍处于保留窗口内(过期则抛出 EVENT_CURSOR_EXPIRED 错误)。
|
|
407
|
+
*/
|
|
408
|
+
assertCursorRetained(run, after, afterSequence) {
|
|
409
|
+
const earliestSeq = run.earliestRetainedSequence ?? 0;
|
|
410
|
+
if (afterSequence !== undefined && afterSequence >= 0 && afterSequence < earliestSeq - 1) {
|
|
411
|
+
const earliestCursor = run.earliestRetainedEventId ?? String(earliestSeq);
|
|
412
|
+
const expiredErr = new Error(`Event cursor '${after}' has expired; earliest available cursor is '${earliestCursor}'`);
|
|
413
|
+
expiredErr.code = EVENT_CURSOR_EXPIRED;
|
|
414
|
+
expiredErr.details = {
|
|
415
|
+
cursor: after,
|
|
416
|
+
earliestCursor,
|
|
417
|
+
earliestRetainedCursor: earliestCursor,
|
|
418
|
+
earliestSequence: earliestSeq,
|
|
419
|
+
earliestEventId: run.earliestRetainedEventId,
|
|
420
|
+
};
|
|
421
|
+
throw expiredErr;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* 记录已交付事件并推进游标(已交付序号与最后确认游标)。
|
|
426
|
+
*/
|
|
427
|
+
trackDeliveredEvent(cursor, evt) {
|
|
428
|
+
cursor.lastYieldedSequence = evt.sequence;
|
|
429
|
+
cursor.lastConfirmedSequence = evt.sequence;
|
|
430
|
+
if (evt.eventId)
|
|
431
|
+
cursor.lastConfirmedEventId = evt.eventId;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* 终止谓词:finish 事件或背压终止事件交付后订阅应立即结束。
|
|
435
|
+
*/
|
|
436
|
+
isTerminalDelivery(evt) {
|
|
437
|
+
return (evt.type === "finish" ||
|
|
438
|
+
(evt.type === "error" && evt.error?.code === EVENT_BACKPRESSURE_LIMIT));
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* 从实时队列取出下一条应交付的事件(跳过已交付序号),并同步推进游标。
|
|
442
|
+
*/
|
|
443
|
+
takeNextDeliverable(channel, cursor) {
|
|
444
|
+
const evt = channel.liveQueue.shift();
|
|
445
|
+
if (!evt)
|
|
446
|
+
return undefined;
|
|
447
|
+
if (evt.sequence > cursor.lastYieldedSequence || evt.type === "error") {
|
|
448
|
+
this.trackDeliveredEvent(cursor, evt);
|
|
449
|
+
return evt;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* 挂起等待下一条实时事件、通道终止或运行淘汰唤醒(严格注销监听避免泄漏)。
|
|
455
|
+
*/
|
|
456
|
+
async waitForNextEvent(runId, channel, registerCleanup) {
|
|
457
|
+
await new Promise((resolve) => {
|
|
458
|
+
let runWakeups = this.wakeups.get(runId);
|
|
459
|
+
if (!runWakeups) {
|
|
460
|
+
runWakeups = new Set();
|
|
461
|
+
this.wakeups.set(runId, runWakeups);
|
|
462
|
+
}
|
|
463
|
+
const unregister = () => {
|
|
464
|
+
runWakeups?.delete(onWakeup);
|
|
465
|
+
if (runWakeups && runWakeups.size === 0) {
|
|
466
|
+
this.wakeups.delete(runId);
|
|
467
|
+
}
|
|
468
|
+
registerCleanup(null);
|
|
469
|
+
};
|
|
470
|
+
const onWakeup = () => {
|
|
471
|
+
channel.done = true;
|
|
472
|
+
unregister();
|
|
473
|
+
resolve();
|
|
474
|
+
};
|
|
475
|
+
runWakeups.add(onWakeup);
|
|
476
|
+
registerCleanup(unregister);
|
|
477
|
+
channel.notify = () => {
|
|
478
|
+
unregister();
|
|
479
|
+
resolve();
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* 手动修剪已终态运行中早于指定序列号的历史事件(用于模拟过期清理)。
|
|
485
|
+
*/
|
|
486
|
+
pruneEvents(runId, upToSequence) {
|
|
487
|
+
const run = this.runs.get(runId);
|
|
488
|
+
if (!run)
|
|
489
|
+
return;
|
|
490
|
+
const idx = run.entries.findIndex((e) => e.event.sequence > upToSequence);
|
|
491
|
+
if (idx > 0) {
|
|
492
|
+
const removed = run.entries.splice(0, idx);
|
|
493
|
+
for (const r of removed) {
|
|
494
|
+
run.totalBytes -= r.bytes;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (idx === -1 && run.entries.length > 0) {
|
|
498
|
+
run.entries = [];
|
|
499
|
+
run.totalBytes = 0;
|
|
500
|
+
}
|
|
501
|
+
if (run.entries.length > 0) {
|
|
502
|
+
run.earliestRetainedSequence = run.entries[0].event.sequence;
|
|
503
|
+
run.earliestRetainedEventId = run.entries[0].event.eventId;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
run.earliestRetainedSequence = upToSequence + 1;
|
|
507
|
+
run.earliestRetainedEventId = undefined;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
clear(runId) {
|
|
511
|
+
this.evictedRuns.add(runId);
|
|
512
|
+
if (this.evictedRuns.size > 2000) {
|
|
513
|
+
const first = this.evictedRuns.values().next().value;
|
|
514
|
+
if (first) {
|
|
515
|
+
this.evictedRuns.delete(first);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
this.runs.delete(runId);
|
|
519
|
+
this.listeners.delete(runId);
|
|
520
|
+
const evictCbs = this.evictions.get(runId);
|
|
521
|
+
if (evictCbs) {
|
|
522
|
+
this.evictions.delete(runId);
|
|
523
|
+
for (const cb of Array.from(evictCbs)) {
|
|
524
|
+
try {
|
|
525
|
+
cb();
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// ignore
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const waiting = this.wakeups.get(runId);
|
|
533
|
+
if (waiting) {
|
|
534
|
+
const cbs = Array.from(waiting);
|
|
535
|
+
this.wakeups.delete(runId);
|
|
536
|
+
for (const wake of cbs) {
|
|
537
|
+
try {
|
|
538
|
+
wake();
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// ignore
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* 获取指定运行的事件统计信息(调试与测试用)。
|
|
548
|
+
*/
|
|
549
|
+
getRunStats(runId) {
|
|
550
|
+
const run = this.runs.get(runId);
|
|
551
|
+
if (!run)
|
|
552
|
+
return undefined;
|
|
553
|
+
return {
|
|
554
|
+
count: run.entries.length,
|
|
555
|
+
bytes: run.totalBytes,
|
|
556
|
+
isTerminal: run.isTerminal,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 获取全局缓存的运行数。
|
|
561
|
+
*/
|
|
562
|
+
getRunCount() {
|
|
563
|
+
return this.runs.size;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* 淘汰最久未活跃的运行记录。
|
|
567
|
+
* 优先淘汰已终态运行;若无终态运行则淘汰最旧活动运行,严格保证 runs.size <= maxRuns。
|
|
568
|
+
*/
|
|
569
|
+
evictOldestRun() {
|
|
570
|
+
let candidateRunId = null;
|
|
571
|
+
let candidateAccess = Infinity;
|
|
572
|
+
for (const [id, buffer] of this.runs.entries()) {
|
|
573
|
+
if (buffer.isTerminal) {
|
|
574
|
+
if (buffer.lastAccessedAt < candidateAccess) {
|
|
575
|
+
candidateAccess = buffer.lastAccessedAt;
|
|
576
|
+
candidateRunId = id;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (!candidateRunId) {
|
|
581
|
+
for (const [id, buffer] of this.runs.entries()) {
|
|
582
|
+
if (buffer.lastAccessedAt < candidateAccess) {
|
|
583
|
+
candidateAccess = buffer.lastAccessedAt;
|
|
584
|
+
candidateRunId = id;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (candidateRunId) {
|
|
589
|
+
this.clear(candidateRunId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 关闭事件接收器并唤醒所有活跃订阅者退出。
|
|
594
|
+
*/
|
|
595
|
+
close() {
|
|
596
|
+
for (const wakeups of this.wakeups.values()) {
|
|
597
|
+
for (const onWakeup of wakeups) {
|
|
598
|
+
onWakeup();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
this.wakeups.clear();
|
|
602
|
+
this.listeners.clear();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内存中活跃执行句柄(In-flight Execution Handles)生命周期管理器。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 集中追踪正在执行的异步 Action 任务句柄。
|
|
6
|
+
* 2. 任务完成(无论成功或失败)后通过 Promise.finally 自动注销,防止内存泄漏。
|
|
7
|
+
* 3. 支持依据 runId 查询、遍历或主动发送取消信号(cancel)。
|
|
8
|
+
*/
|
|
9
|
+
export class ExecutionManager {
|
|
10
|
+
active = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* 注册一个活跃的执行句柄。
|
|
13
|
+
* 当底层 Promise 完成结算(resolve/reject)时,自动从活跃映射表中移除。
|
|
14
|
+
*
|
|
15
|
+
* @param handle 执行句柄对象
|
|
16
|
+
*/
|
|
17
|
+
register(handle) {
|
|
18
|
+
this.active.set(handle.runId, handle);
|
|
19
|
+
handle.result.finally(() => {
|
|
20
|
+
this.active.delete(handle.runId);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 根据 runId 检索正在执行的任务句柄。
|
|
25
|
+
*
|
|
26
|
+
* @param runId 运行 ID
|
|
27
|
+
* @returns 匹配的 ExecutionHandle 或 undefined
|
|
28
|
+
*/
|
|
29
|
+
get(runId) {
|
|
30
|
+
return this.active.get(runId);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 向指定 runId 的在途执行任务发送取消信号。
|
|
34
|
+
*
|
|
35
|
+
* @param runId 目标运行 ID
|
|
36
|
+
* @param reason 取消原因描述
|
|
37
|
+
* @returns 若句柄存在且成功触发中断返回 true;若任务不存在或已完成返回 false
|
|
38
|
+
*/
|
|
39
|
+
cancel(runId, reason) {
|
|
40
|
+
const handle = this.active.get(runId);
|
|
41
|
+
if (!handle) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return handle.cancel(reason);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 手动从活跃集合中移除执行句柄。
|
|
48
|
+
*
|
|
49
|
+
* @param runId 运行 ID
|
|
50
|
+
*/
|
|
51
|
+
remove(runId) {
|
|
52
|
+
this.active.delete(runId);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 获取当前系统中所有正在执行的活跃句柄列表。
|
|
56
|
+
*/
|
|
57
|
+
list() {
|
|
58
|
+
return Array.from(this.active.values());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 清空所有活跃执行句柄。
|
|
62
|
+
*/
|
|
63
|
+
clear() {
|
|
64
|
+
this.active.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export * from "./context";
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
1
|
+
export * from "./context.js";
|
|
2
|
+
export * from "./action-collection.js";
|
|
3
|
+
export * from "./runner.js";
|
|
4
|
+
export * from "./execution-manager.js";
|
|
5
|
+
export * from "./standalone.js";
|
|
6
|
+
export * from "./env.js";
|
|
7
|
+
export * from "./clock.js";
|
|
8
|
+
export * from "./process.js";
|
|
9
|
+
export * from "./events.js";
|
|
10
|
+
export * from "./module-loader.js";
|