@lokvis/runtime 0.2.2 → 0.4.1
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/dist/asset-store.d.ts +13 -0
- package/dist/asset-store.d.ts.map +1 -1
- package/dist/asset-store.js +15 -2
- package/dist/asset-store.js.map +1 -1
- package/dist/batch-processor.d.ts +32 -161
- package/dist/batch-processor.d.ts.map +1 -1
- package/dist/batch-processor.js +107 -333
- package/dist/batch-processor.js.map +1 -1
- package/dist/batch-progress.d.ts +94 -0
- package/dist/batch-progress.d.ts.map +1 -0
- package/dist/batch-progress.js +128 -0
- package/dist/batch-progress.js.map +1 -0
- package/dist/batch-scheduler.d.ts +70 -0
- package/dist/batch-scheduler.d.ts.map +1 -0
- package/dist/batch-scheduler.js +147 -0
- package/dist/batch-scheduler.js.map +1 -0
- package/dist/browser-detect.d.ts +115 -0
- package/dist/browser-detect.d.ts.map +1 -0
- package/dist/browser-detect.js +160 -0
- package/dist/browser-detect.js.map +1 -0
- package/dist/concurrency-controller.d.ts +31 -0
- package/dist/concurrency-controller.d.ts.map +1 -0
- package/dist/concurrency-controller.js +33 -0
- package/dist/concurrency-controller.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/executor.d.ts +9 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +87 -48
- package/dist/executor.js.map +1 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +3 -2
- package/dist/history.js.map +1 -1
- package/dist/idb-asset-store.d.ts.map +1 -1
- package/dist/idb-asset-store.js +6 -1
- package/dist/idb-asset-store.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -15
- package/dist/index.js.map +1 -1
- package/dist/managers/asset-manager.d.ts +114 -0
- package/dist/managers/asset-manager.d.ts.map +1 -0
- package/dist/managers/asset-manager.js +238 -0
- package/dist/managers/asset-manager.js.map +1 -0
- package/dist/managers/history-manager.d.ts +100 -0
- package/dist/managers/history-manager.d.ts.map +1 -0
- package/dist/managers/history-manager.js +296 -0
- package/dist/managers/history-manager.js.map +1 -0
- package/dist/managers/mcp-manifest-builder.d.ts +18 -0
- package/dist/managers/mcp-manifest-builder.d.ts.map +1 -0
- package/dist/managers/mcp-manifest-builder.js +99 -0
- package/dist/managers/mcp-manifest-builder.js.map +1 -0
- package/dist/managers/quota-manager.d.ts +42 -0
- package/dist/managers/quota-manager.d.ts.map +1 -0
- package/dist/managers/quota-manager.js +126 -0
- package/dist/managers/quota-manager.js.map +1 -0
- package/dist/managers/workflow-coordinator.d.ts +62 -0
- package/dist/managers/workflow-coordinator.d.ts.map +1 -0
- package/dist/managers/workflow-coordinator.js +125 -0
- package/dist/managers/workflow-coordinator.js.map +1 -0
- package/dist/opfs-asset-store.d.ts.map +1 -1
- package/dist/opfs-asset-store.js +12 -2
- package/dist/opfs-asset-store.js.map +1 -1
- package/dist/plugin-context.d.ts +35 -0
- package/dist/plugin-context.d.ts.map +1 -0
- package/dist/plugin-context.js +62 -0
- package/dist/plugin-context.js.map +1 -0
- package/dist/plugin-permissions.d.ts +99 -0
- package/dist/plugin-permissions.d.ts.map +1 -0
- package/dist/plugin-permissions.js +205 -0
- package/dist/plugin-permissions.js.map +1 -0
- package/dist/runtime-impl.d.ts +116 -0
- package/dist/runtime-impl.d.ts.map +1 -0
- package/dist/runtime-impl.js +270 -0
- package/dist/runtime-impl.js.map +1 -0
- package/dist/runtime.d.ts +10 -233
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +12 -978
- package/dist/runtime.js.map +1 -1
- package/dist/test-utils/fakes.d.ts +35 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fakes.js +60 -0
- package/dist/test-utils/fakes.js.map +1 -0
- package/dist/types.d.ts +113 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-host.d.ts.map +1 -1
- package/dist/worker-host.js +32 -9
- package/dist/worker-host.js.map +1 -1
- package/dist/worker-protocol.d.ts +30 -0
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-protocol.js +24 -0
- package/dist/worker-protocol.js.map +1 -1
- package/package.json +2 -2
- package/src/asset-store.ts +32 -2
- package/src/batch-processor.ts +133 -510
- package/src/batch-progress.ts +228 -0
- package/src/batch-scheduler.ts +209 -0
- package/src/browser-detect.ts +221 -0
- package/src/concurrency-controller.ts +52 -0
- package/src/errors.ts +82 -0
- package/src/executor.ts +106 -55
- package/src/history.ts +3 -2
- package/src/idb-asset-store.ts +7 -1
- package/src/index.ts +50 -15
- package/src/managers/asset-manager.ts +259 -0
- package/src/managers/history-manager.ts +327 -0
- package/src/managers/mcp-manifest-builder.ts +108 -0
- package/src/managers/quota-manager.ts +149 -0
- package/src/managers/workflow-coordinator.ts +158 -0
- package/src/opfs-asset-store.ts +16 -2
- package/src/plugin-context.ts +87 -0
- package/src/plugin-permissions.ts +228 -0
- package/src/runtime-impl.ts +298 -0
- package/src/runtime.ts +16 -1116
- package/src/test-utils/fakes.ts +72 -0
- package/src/types.ts +116 -0
- package/src/worker-host.ts +35 -7
- package/src/worker-protocol.ts +47 -0
- package/dist/workflow-builder.d.ts +0 -124
- package/dist/workflow-builder.d.ts.map +0 -1
- package/dist/workflow-builder.js +0 -240
- package/dist/workflow-builder.js.map +0 -1
- package/src/workflow-builder.ts +0 -303
package/src/batch-processor.ts
CHANGED
|
@@ -1,121 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BatchProcessor - 批量作业处理器(
|
|
2
|
+
* BatchProcessor - 批量作业处理器 Facade(W4.4 拆分后)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 核心能力:
|
|
8
|
-
* - 并发控制:基于槽位池,默认 4(Pro=16),按 MemoryGuard 压力动态收缩
|
|
9
|
-
* - 失败重试:maxRetries(默认 0,与 W5 行为兼容);达到上限才发 batch:item:failed
|
|
10
|
-
* - 批量上限:免费 10 文件(W6.2)、Pro 无限(超额抛 BatchLimitExceededError)
|
|
11
|
-
* - 进度事件:batch:started / batch:item:* / batch:progress / batch:completed / batch:cancelled
|
|
12
|
-
* - 取消:批量级 cancel,逐个取消 in-flight 项,已 pending 的标记 cancelled
|
|
13
|
-
* - 暂停/恢复:挂起 schedule 循环(不取消 in-flight,完成后不再补满)
|
|
14
|
-
*
|
|
15
|
-
* 设计取舍:
|
|
16
|
-
* - 不持久化作业状态(刷新即丢):W6 主要保证 Asset 持久化,批量队列是临时调度
|
|
17
|
-
* 结构,刷新后由 UI 重新发起即可。持久化批量进度属 Phase 2。
|
|
18
|
-
* - 不持有 output Blob:每项执行完即返回 AssetId,Blob 由 AssetStore(OPFS/IDB)托管,
|
|
19
|
-
* 避免 50+ 图批量把 output Blob 全部留在内存导致 OOM。
|
|
20
|
-
* - 单 Job 串行 schedule:同一 BatchProcessor 实例可并发 enqueue 多个 Job,但内部
|
|
21
|
-
* schedule 逻辑用 per-job lock 防重入(参考 W5 BatchQueue 的 scheduleLock 修复)。
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* const job = runtime.batch.enqueue({
|
|
26
|
-
* items: files.map((f) => ({ source: { kind: 'file', file: f }, workflow: wf })),
|
|
27
|
-
* maxRetries: 1,
|
|
28
|
-
* });
|
|
29
|
-
* runtime.batch.onProgress(job.id, (p) => console.log(p.completed, '/', p.total));
|
|
30
|
-
* await runtime.batch.waitForCompletion(job.id);
|
|
31
|
-
* ```
|
|
4
|
+
* 持有 jobs Map + 协作对象(BatchScheduler / BatchProgressEmitter / ConcurrencyController),
|
|
5
|
+
* 对外保持原公开 API 不变。调度/重试/进度/并发 委托给协作对象;
|
|
6
|
+
* Facade 保留 jobs Map 管理、批量上限校验、状态机控制、toJobView。
|
|
32
7
|
*/
|
|
33
|
-
import type {
|
|
34
|
-
AssetId,
|
|
35
|
-
AssetSource,
|
|
36
|
-
BatchItemStatus,
|
|
37
|
-
BatchJobStatus,
|
|
38
|
-
LokvisEvent,
|
|
39
|
-
Workflow,
|
|
40
|
-
WorkflowResult,
|
|
41
|
-
} from '@lokvis/schema';
|
|
42
|
-
import type { EventBus } from '@lokvis/schema';
|
|
8
|
+
import type { BatchItemStatus, EventBus } from '@lokvis/schema';
|
|
43
9
|
import type { LokvisRuntime } from './types.js';
|
|
44
|
-
import { MemoryGuard
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 批量项运行时状态(对外只读视图) */
|
|
60
|
-
export interface BatchItem {
|
|
61
|
-
/** 项 ID(自动生成,用于事件追踪与 UI 定位) */
|
|
62
|
-
id: string;
|
|
63
|
-
/** 在 job 中的序号(0-based,用于事件中的 index 字段) */
|
|
64
|
-
index: number;
|
|
65
|
-
status: BatchItemStatus;
|
|
66
|
-
/** 输入 source(便于失败时 UI 展示文件名) */
|
|
67
|
-
source: AssetSource;
|
|
68
|
-
/** 该项执行的工作流 */
|
|
69
|
-
workflow: Workflow;
|
|
70
|
-
/** 输出 AssetId(completed 时填) */
|
|
71
|
-
outputAssetId?: AssetId;
|
|
72
|
-
/** 失败原因(failed 时填) */
|
|
73
|
-
error?: Error;
|
|
74
|
-
/** 已重试次数 */
|
|
75
|
-
attempts: number;
|
|
76
|
-
/** 该项最大重试次数 */
|
|
77
|
-
maxRetries: number;
|
|
78
|
-
/** 单项执行耗时(ms,completed 时填) */
|
|
79
|
-
duration?: number;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** 批量作业运行时状态(对外只读视图) */
|
|
83
|
-
export interface BatchJob {
|
|
84
|
-
id: string;
|
|
85
|
-
status: BatchJobStatus;
|
|
86
|
-
items: BatchItem[];
|
|
87
|
-
/** 已完成数(completed) */
|
|
88
|
-
completed: number;
|
|
89
|
-
/** 失败数(failed) */
|
|
90
|
-
failed: number;
|
|
91
|
-
/** 总项数 */
|
|
92
|
-
total: number;
|
|
93
|
-
/** job 起始时间(ms) */
|
|
94
|
-
startedAt: number;
|
|
95
|
-
/** job 结束时间(ms,完成后填) */
|
|
96
|
-
endedAt?: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** enqueue 选项 */
|
|
100
|
-
export interface EnqueueOptions {
|
|
101
|
-
/** 作业项 */
|
|
102
|
-
items: BatchItemInput[];
|
|
103
|
-
/** job 级最大重试次数(默认 0,单项可 BatchItemInput.maxRetries 覆盖) */
|
|
104
|
-
maxRetries?: number;
|
|
105
|
-
/**
|
|
106
|
-
* 并发槽位上限(默认按 isPro:free=4,pro=16)。
|
|
107
|
-
* 实际并发会按 MemoryGuard 压力动态收缩。
|
|
108
|
-
*/
|
|
109
|
-
concurrency?: number;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** 进度回调载荷 */
|
|
113
|
-
export interface BatchProgress {
|
|
114
|
-
jobId: string;
|
|
115
|
-
completed: number;
|
|
116
|
-
failed: number;
|
|
117
|
-
total: number;
|
|
118
|
-
}
|
|
10
|
+
import { MemoryGuard } from './memory-guard.js';
|
|
11
|
+
import {
|
|
12
|
+
BatchProgressEmitter,
|
|
13
|
+
type BatchJob,
|
|
14
|
+
type EnqueueOptions,
|
|
15
|
+
type BatchProgress,
|
|
16
|
+
} from './batch-progress.js';
|
|
17
|
+
import {
|
|
18
|
+
FREE_CONCURRENCY,
|
|
19
|
+
PRO_CONCURRENCY,
|
|
20
|
+
ConcurrencyController,
|
|
21
|
+
} from './concurrency-controller.js';
|
|
22
|
+
import { BatchScheduler, type BatchJobInternal } from './batch-scheduler.js';
|
|
119
23
|
|
|
120
24
|
/** 批量上限超限错误(W6.2) */
|
|
121
25
|
export class BatchLimitExceededError extends Error {
|
|
@@ -134,29 +38,18 @@ export class BatchLimitExceededError extends Error {
|
|
|
134
38
|
|
|
135
39
|
/** 免费版批量上限(W6.2) */
|
|
136
40
|
export const FREE_BATCH_LIMIT = 10;
|
|
41
|
+
/** 终态 job 保留上限(超出按 FIFO 淘汰最旧) */
|
|
42
|
+
export const MAX_RETAINED_JOBS = 10;
|
|
43
|
+
|
|
44
|
+
export { FREE_CONCURRENCY, PRO_CONCURRENCY } from './concurrency-controller.js';
|
|
45
|
+
export type {
|
|
46
|
+
BatchProgress,
|
|
47
|
+
BatchItemInput,
|
|
48
|
+
BatchItem,
|
|
49
|
+
BatchJob,
|
|
50
|
+
EnqueueOptions,
|
|
51
|
+
} from './batch-progress.js';
|
|
137
52
|
|
|
138
|
-
/** 默认并发槽位 */
|
|
139
|
-
export const FREE_CONCURRENCY = 4;
|
|
140
|
-
export const PRO_CONCURRENCY = 16;
|
|
141
|
-
|
|
142
|
-
/** 按 MemoryPressure 收缩并发槽位 */
|
|
143
|
-
function shrinkConcurrencyByPressure(
|
|
144
|
-
base: number,
|
|
145
|
-
pressure: MemoryPressure
|
|
146
|
-
): number {
|
|
147
|
-
switch (pressure) {
|
|
148
|
-
case 'critical':
|
|
149
|
-
return 1;
|
|
150
|
-
case 'high':
|
|
151
|
-
return Math.max(1, Math.floor(base / 2));
|
|
152
|
-
case 'elevated':
|
|
153
|
-
return Math.max(2, Math.floor((base * 3) / 4));
|
|
154
|
-
default:
|
|
155
|
-
return base;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** 生成项 ID */
|
|
160
53
|
function generateItemId(): string {
|
|
161
54
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
162
55
|
return `bi_${crypto.randomUUID()}`;
|
|
@@ -164,7 +57,6 @@ function generateItemId(): string {
|
|
|
164
57
|
return `bi_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
165
58
|
}
|
|
166
59
|
|
|
167
|
-
/** 生成 job ID */
|
|
168
60
|
function generateJobId(): string {
|
|
169
61
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
170
62
|
return `batch_${crypto.randomUUID()}`;
|
|
@@ -172,21 +64,14 @@ function generateJobId(): string {
|
|
|
172
64
|
return `batch_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
173
65
|
}
|
|
174
66
|
|
|
175
|
-
/**
|
|
176
|
-
* BatchProcessor 实现。
|
|
177
|
-
*
|
|
178
|
-
* 不直接依赖 LokvisRuntimeImpl(避免循环引用),通过构造注入 LokvisRuntime 接口。
|
|
179
|
-
* 内部用纯 JS Map 管理作业,无 React state(可被 SDK / MCP / Node 复用)。
|
|
180
|
-
*/
|
|
67
|
+
/** BatchProcessor Facade(不依赖 LokvisRuntimeImpl,通过构造注入接口) */
|
|
181
68
|
export class BatchProcessor {
|
|
182
69
|
private readonly runtime: LokvisRuntime;
|
|
183
|
-
private readonly eventBus: EventBus;
|
|
184
70
|
private readonly isPro: boolean;
|
|
185
|
-
private readonly
|
|
186
|
-
/** jobId → BatchJob 内部可变状态(含 private 字段) */
|
|
71
|
+
private readonly eventBus: EventBus;
|
|
187
72
|
private readonly jobs = new Map<string, BatchJobInternal>();
|
|
188
|
-
|
|
189
|
-
private readonly
|
|
73
|
+
private readonly scheduler: BatchScheduler;
|
|
74
|
+
private readonly progress: BatchProgressEmitter;
|
|
190
75
|
|
|
191
76
|
constructor(opts: {
|
|
192
77
|
runtime: LokvisRuntime;
|
|
@@ -195,35 +80,29 @@ export class BatchProcessor {
|
|
|
195
80
|
memoryGuard?: MemoryGuard;
|
|
196
81
|
}) {
|
|
197
82
|
this.runtime = opts.runtime;
|
|
198
|
-
this.eventBus = opts.eventBus;
|
|
199
83
|
this.isPro = opts.isPro;
|
|
200
|
-
this.
|
|
84
|
+
this.eventBus = opts.eventBus;
|
|
85
|
+
this.progress = new BatchProgressEmitter(opts.eventBus);
|
|
86
|
+
const concurrency = new ConcurrencyController({ memoryGuard: opts.memoryGuard });
|
|
87
|
+
this.scheduler = new BatchScheduler({
|
|
88
|
+
runtime: opts.runtime,
|
|
89
|
+
progress: this.progress,
|
|
90
|
+
concurrency,
|
|
91
|
+
});
|
|
201
92
|
}
|
|
202
93
|
|
|
203
|
-
/**
|
|
204
|
-
* 入队批量作业。返回 job 视图(只读)。
|
|
205
|
-
* 超过免费上限时同步抛 BatchLimitExceededError,不创建 job。
|
|
206
|
-
* 空 items 抛错(避免静默产出 0 项 completed job,遮蔽调用方逻辑错误)。
|
|
207
|
-
*/
|
|
208
94
|
enqueue(options: EnqueueOptions): BatchJob {
|
|
209
95
|
const { items, maxRetries = 0, concurrency } = options;
|
|
210
|
-
|
|
211
|
-
// m3: 空数组视为调用方逻辑错误,显式抛错而非静默完成
|
|
212
96
|
if (!Array.isArray(items) || items.length === 0) {
|
|
213
97
|
throw new Error('BatchProcessor.enqueue: items cannot be empty');
|
|
214
98
|
}
|
|
215
|
-
|
|
216
|
-
// W6.2 批量上限:免费 10,Pro 无限
|
|
217
99
|
if (!this.isPro && items.length > FREE_BATCH_LIMIT) {
|
|
218
100
|
throw new BatchLimitExceededError(FREE_BATCH_LIMIT, items.length);
|
|
219
101
|
}
|
|
220
|
-
|
|
221
|
-
const jobId = generateJobId();
|
|
222
102
|
const baseConcurrency =
|
|
223
103
|
concurrency ?? (this.isPro ? PRO_CONCURRENCY : FREE_CONCURRENCY);
|
|
224
|
-
|
|
225
104
|
const internal: BatchJobInternal = {
|
|
226
|
-
id:
|
|
105
|
+
id: generateJobId(),
|
|
227
106
|
status: 'queued',
|
|
228
107
|
baseConcurrency,
|
|
229
108
|
maxRetries,
|
|
@@ -239,104 +118,104 @@ export class BatchProcessor {
|
|
|
239
118
|
completed: 0,
|
|
240
119
|
failed: 0,
|
|
241
120
|
startedAt: Date.now(),
|
|
242
|
-
/** schedule 重入锁(参考 W5 BatchQueue scheduleLock 修复) */
|
|
243
121
|
scheduleLock: false,
|
|
244
|
-
/** pause 时 schedule 不再补满新项 */
|
|
245
122
|
paused: false,
|
|
246
|
-
/** cancel 标记:已发起 cancel 后所有 pending → cancelled,schedule 终止 */
|
|
247
123
|
cancelled: false,
|
|
248
124
|
};
|
|
249
|
-
this.jobs.set(
|
|
250
|
-
|
|
251
|
-
// 异步启动(不阻塞 enqueue 调用方)
|
|
252
|
-
void this.runJob(internal);
|
|
253
|
-
|
|
125
|
+
this.jobs.set(internal.id, internal);
|
|
126
|
+
void this.scheduler.runJob(internal);
|
|
254
127
|
return this.toJobView(internal);
|
|
255
128
|
}
|
|
256
129
|
|
|
257
|
-
/** 列出所有作业(只读视图) */
|
|
258
130
|
list(): BatchJob[] {
|
|
259
131
|
return Array.from(this.jobs.values()).map((j) => this.toJobView(j));
|
|
260
132
|
}
|
|
261
133
|
|
|
262
|
-
/** 获取指定 job */
|
|
263
134
|
get(jobId: string): BatchJob | undefined {
|
|
264
135
|
const internal = this.jobs.get(jobId);
|
|
265
136
|
return internal ? this.toJobView(internal) : undefined;
|
|
266
137
|
}
|
|
267
138
|
|
|
268
|
-
/** 取消整个 job:逐个取消 in-flight 项,pending 项标记 cancelled */
|
|
269
139
|
async cancel(jobId: string): Promise<void> {
|
|
270
140
|
const job = this.jobs.get(jobId);
|
|
271
141
|
if (!job) return;
|
|
272
142
|
// m2: failed 也是终态,任何终态 job 不应被改写
|
|
273
|
-
if (this.isTerminal(job.status)) return;
|
|
143
|
+
if (this.scheduler.isTerminal(job.status)) return;
|
|
274
144
|
|
|
275
|
-
// B1
|
|
276
|
-
// cancelled=true 后短路,避免覆盖 cancelled 状态或把被取消的项误标 failed。
|
|
145
|
+
// B1:同步先标记 cancelled,使 in-flight processItem 续跑时短路
|
|
277
146
|
job.cancelled = true;
|
|
278
147
|
job.paused = false;
|
|
279
148
|
let cancelledCount = 0;
|
|
280
|
-
|
|
281
|
-
// 同步把所有 processing/pending 项标记 cancelled(processItem 续跑会读到此状态)
|
|
282
149
|
for (const item of job.items) {
|
|
283
150
|
if (item.status === 'processing' || item.status === 'pending') {
|
|
284
151
|
item.status = 'cancelled';
|
|
285
152
|
cancelledCount++;
|
|
286
153
|
}
|
|
287
154
|
}
|
|
288
|
-
|
|
289
|
-
// 异步取消所有 in-flight workflow(此时项状态已是 cancelled,
|
|
290
|
-
// processItem 续跑会因 job.cancelled 短路,不会回写其他状态)
|
|
291
155
|
for (const item of job.items) {
|
|
292
156
|
if (item.status === 'cancelled') {
|
|
293
|
-
const wfId = this.itemWorkflowId(job.id, item.id);
|
|
157
|
+
const wfId = this.scheduler.itemWorkflowId(job.id, item.id);
|
|
294
158
|
try {
|
|
295
159
|
await this.runtime.cancel(wfId);
|
|
296
|
-
} catch {
|
|
297
|
-
//
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// 取消失败不阻断后续项的 cancel,但需记录便于调试
|
|
162
|
+
console.warn(`[lokvis] BatchProcessor.cancel: cancel(${wfId}) failed:`, err);
|
|
298
163
|
}
|
|
299
164
|
}
|
|
300
165
|
}
|
|
301
|
-
|
|
302
166
|
job.status = 'cancelled';
|
|
303
167
|
job.endedAt = Date.now();
|
|
304
|
-
this.
|
|
305
|
-
this.cleanupJobSubs(jobId);
|
|
168
|
+
this.progress.emitCancelled(jobId, cancelledCount);
|
|
169
|
+
this.progress.cleanupJobSubs(jobId);
|
|
170
|
+
this.pruneJobs();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 取消所有非终态 job + 清理 progress 订阅(W21.6 runtime.dispose 用)。
|
|
175
|
+
*
|
|
176
|
+
* 与单 job cancel 不同:不等待 runtime.cancel(wfId) 完成 —— runtime
|
|
177
|
+
* 自身的 dispose 会通过 executor.cancelAll 统一取消所有 workflow,
|
|
178
|
+
* 这里只做 batch 层面的状态标记 + 订阅清理,避免双 await 死锁。
|
|
179
|
+
*/
|
|
180
|
+
async dispose(): Promise<void> {
|
|
181
|
+
for (const job of [...this.jobs.values()]) {
|
|
182
|
+
if (this.scheduler.isTerminal(job.status)) continue;
|
|
183
|
+
job.cancelled = true;
|
|
184
|
+
job.paused = false;
|
|
185
|
+
for (const item of job.items) {
|
|
186
|
+
if (item.status === 'processing' || item.status === 'pending') {
|
|
187
|
+
item.status = 'cancelled';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
job.status = 'cancelled';
|
|
191
|
+
job.endedAt = Date.now();
|
|
192
|
+
this.progress.cleanupJobSubs(job.id);
|
|
193
|
+
}
|
|
306
194
|
}
|
|
307
195
|
|
|
308
|
-
/** 暂停 job:schedule 不再补满,已 in-flight 项跑完即止 */
|
|
309
196
|
async pause(jobId: string): Promise<void> {
|
|
310
197
|
const job = this.jobs.get(jobId);
|
|
311
198
|
if (!job) return;
|
|
312
199
|
if (job.status !== 'running' && job.status !== 'queued') return;
|
|
313
200
|
job.paused = true;
|
|
314
201
|
job.status = 'paused';
|
|
315
|
-
|
|
316
|
-
this.emit({ type: 'batch:paused', jobId });
|
|
202
|
+
this.progress.emitPaused(jobId);
|
|
317
203
|
}
|
|
318
204
|
|
|
319
|
-
/** 恢复 job:解除 pause,schedule 继续补满 */
|
|
320
205
|
async resume(jobId: string): Promise<void> {
|
|
321
206
|
const job = this.jobs.get(jobId);
|
|
322
207
|
if (!job) return;
|
|
323
208
|
if (job.status !== 'paused') return;
|
|
324
209
|
job.paused = false;
|
|
325
210
|
job.status = 'running';
|
|
326
|
-
|
|
327
|
-
this.
|
|
328
|
-
void this.schedule(job);
|
|
211
|
+
this.progress.emitResumed(jobId);
|
|
212
|
+
void this.scheduler.schedule(job);
|
|
329
213
|
}
|
|
330
214
|
|
|
331
|
-
/**
|
|
332
|
-
* 重试 job 中所有 failed 项(仅当 job 已结束)。
|
|
333
|
-
* 重置 attempts/error/status,pending 重新等待 schedule。
|
|
334
|
-
*/
|
|
335
215
|
async retryFailed(jobId: string): Promise<void> {
|
|
336
216
|
const job = this.jobs.get(jobId);
|
|
337
217
|
if (!job) return;
|
|
338
218
|
if (job.status !== 'failed' && job.status !== 'completed') return;
|
|
339
|
-
|
|
340
219
|
for (const item of job.items) {
|
|
341
220
|
if (item.status === 'failed') {
|
|
342
221
|
item.status = 'pending';
|
|
@@ -347,319 +226,87 @@ export class BatchProcessor {
|
|
|
347
226
|
job.failed = 0;
|
|
348
227
|
job.cancelled = false;
|
|
349
228
|
job.status = 'running';
|
|
350
|
-
void this.schedule(job);
|
|
229
|
+
void this.scheduler.schedule(job);
|
|
351
230
|
}
|
|
352
231
|
|
|
353
|
-
/** 订阅 job 进度(返回取消订阅函数) */
|
|
354
232
|
onProgress(jobId: string, handler: (p: BatchProgress) => void): () => void {
|
|
355
|
-
|
|
356
|
-
if (!subs) {
|
|
357
|
-
subs = new Set();
|
|
358
|
-
this.progressSubs.set(jobId, subs);
|
|
359
|
-
}
|
|
360
|
-
subs.add(handler);
|
|
361
|
-
return () => {
|
|
362
|
-
subs!.delete(handler);
|
|
363
|
-
if (subs!.size === 0) this.progressSubs.delete(jobId);
|
|
364
|
-
};
|
|
233
|
+
return this.progress.onProgress(jobId, handler);
|
|
365
234
|
}
|
|
366
235
|
|
|
367
236
|
/**
|
|
368
|
-
* 等待 job
|
|
237
|
+
* 等待 job 中至少 count 项进入 processing 状态(TD-2.2 长期方案)。
|
|
369
238
|
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
239
|
+
* 替代测试中固定 setTimeout 赌注:基于"当前状态快照 + batch:item:started
|
|
240
|
+
* 事件订阅"双重判定,确定性等待而非时间赌注。
|
|
241
|
+
*
|
|
242
|
+
* - 先快照当前 processing 数,若已 >= count 立即 resolve(schedule 循环内
|
|
243
|
+
* 同步设置 item.status='processing' + 发 batch:item:started,enqueue
|
|
244
|
+
* 返回时首批项通常已进入 processing)
|
|
245
|
+
* - 否则订阅 batch:item:started,累计到 count 时 resolve(兜底异步场景)
|
|
246
|
+
* - 超时(默认 5s)reject,避免坏 job 永久挂起
|
|
376
247
|
*/
|
|
377
|
-
async
|
|
248
|
+
async waitForItemsStarted(
|
|
378
249
|
jobId: string,
|
|
379
|
-
|
|
380
|
-
|
|
250
|
+
count: number,
|
|
251
|
+
timeoutMs = 5000
|
|
252
|
+
): Promise<void> {
|
|
381
253
|
const job = this.jobs.get(jobId);
|
|
382
254
|
if (!job) throw new Error(`Batch job not found: ${jobId}`);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
255
|
+
const current = job.items.filter((i) => i.status === 'processing').length;
|
|
256
|
+
if (current >= count) return;
|
|
257
|
+
let seen = current;
|
|
258
|
+
return new Promise<void>((resolve, reject) => {
|
|
386
259
|
let settled = false;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
let offCancelled: () => void;
|
|
390
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
391
|
-
const finish = (fn: () => void) => {
|
|
260
|
+
let off: () => void;
|
|
261
|
+
const timer = setTimeout(() => {
|
|
392
262
|
if (settled) return;
|
|
393
263
|
settled = true;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
offCompleted = this.eventBus.on('batch:completed', (e) => {
|
|
400
|
-
if (e.jobId !== jobId) return;
|
|
401
|
-
finish(() => resolve(this.toJobView(this.jobs.get(jobId) ?? job)));
|
|
402
|
-
});
|
|
403
|
-
offCancelled = this.eventBus.on('batch:cancelled', (e) => {
|
|
404
|
-
if (e.jobId !== jobId) return;
|
|
405
|
-
finish(() => resolve(this.toJobView(this.jobs.get(jobId) ?? job)));
|
|
406
|
-
});
|
|
407
|
-
timer = setTimeout(() => {
|
|
408
|
-
finish(() =>
|
|
409
|
-
reject(
|
|
410
|
-
new Error(
|
|
411
|
-
`BatchProcessor.waitForCompletion timed out after ${timeoutMs}ms (jobId=${jobId})`
|
|
412
|
-
)
|
|
264
|
+
off();
|
|
265
|
+
reject(
|
|
266
|
+
new Error(
|
|
267
|
+
`waitForItemsStarted timed out after ${timeoutMs}ms ` +
|
|
268
|
+
`(jobId=${jobId}, expected=${count}, seen=${seen})`
|
|
413
269
|
)
|
|
414
270
|
);
|
|
415
271
|
}, timeoutMs);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
/** 启动 job:发 batch:started,进入 running,触发首次 schedule */
|
|
427
|
-
private async runJob(job: BatchJobInternal): Promise<void> {
|
|
428
|
-
if (job.cancelled) return;
|
|
429
|
-
job.status = 'running';
|
|
430
|
-
this.emit({ type: 'batch:started', jobId: job.id, total: job.items.length });
|
|
431
|
-
await this.schedule(job);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* 调度循环:把 pending 项填入空闲槽位,直到无可用槽位或无 pending 项。
|
|
436
|
-
*
|
|
437
|
-
* 重入保护:用 scheduleLock 防止多个 schedule 调用并发导致超限(参考 W5
|
|
438
|
-
* BatchQueue.tsx 的 scheduleLock 修复:并发完成时多个 processItem 同时触发
|
|
439
|
-
* schedule 会读到相同 running 快照,导致并发超限)。
|
|
440
|
-
*/
|
|
441
|
-
private async schedule(job: BatchJobInternal): Promise<void> {
|
|
442
|
-
if (job.scheduleLock) return;
|
|
443
|
-
if (job.cancelled || job.paused) return;
|
|
444
|
-
if (this.isTerminal(job.status)) return;
|
|
445
|
-
|
|
446
|
-
job.scheduleLock = true;
|
|
447
|
-
try {
|
|
448
|
-
while (!job.cancelled && !job.paused) {
|
|
449
|
-
const running = job.items.filter((i) => i.status === 'processing').length;
|
|
450
|
-
const cap = this.currentConcurrency(job);
|
|
451
|
-
if (running >= cap) break;
|
|
452
|
-
|
|
453
|
-
const next = job.items.find((i) => i.status === 'pending');
|
|
454
|
-
if (!next) break;
|
|
455
|
-
|
|
456
|
-
// 标记 processing 并启动(不 await,让循环继续补满)
|
|
457
|
-
next.status = 'processing';
|
|
458
|
-
void this.processItem(job, next);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// 循环结束:检查是否所有项都已终结
|
|
462
|
-
this.maybeComplete(job);
|
|
463
|
-
} finally {
|
|
464
|
-
job.scheduleLock = false;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/** 处理单个项:导入 → run workflow → 导出 output AssetId */
|
|
469
|
-
private async processItem(
|
|
470
|
-
job: BatchJobInternal,
|
|
471
|
-
item: BatchItemInternal
|
|
472
|
-
): Promise<void> {
|
|
473
|
-
const start = Date.now();
|
|
474
|
-
const wfId = this.itemWorkflowId(job.id, item.id);
|
|
475
|
-
|
|
476
|
-
this.emit({
|
|
477
|
-
type: 'batch:item:started',
|
|
478
|
-
jobId: job.id,
|
|
479
|
-
itemId: item.id,
|
|
480
|
-
index: item.index,
|
|
481
|
-
total: job.items.length,
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// B1 修复:cancel 已同步把 item 标为 cancelled,此处若发现 cancelled
|
|
485
|
-
// 直接短路,不发起 import/run,避免与 cancel 竞态
|
|
486
|
-
if (job.cancelled || item.status === 'cancelled') {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
let inputAssetId: AssetId | undefined;
|
|
491
|
-
let result: WorkflowResult | undefined;
|
|
492
|
-
try {
|
|
493
|
-
// 1. 导入输入资产
|
|
494
|
-
inputAssetId = await this.runtime.importAsset(item.source);
|
|
495
|
-
|
|
496
|
-
// B1 守卫:import 期间可能被 cancel
|
|
497
|
-
if (job.cancelled) {
|
|
498
|
-
item.status = 'cancelled';
|
|
499
|
-
// 清理刚导入的 input,避免孤儿资产(cancel 期间产出的 input 不应残留)
|
|
500
|
-
void this.runtime.removeAsset(inputAssetId).catch(() => {});
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// 2. 执行工作流(用独立 workflow id,便于 cancel)
|
|
505
|
-
const wf: Workflow = { ...item.workflow, id: wfId };
|
|
506
|
-
result = await this.runtime.run(wf, [inputAssetId]);
|
|
507
|
-
|
|
508
|
-
// B1 守卫:run 期间可能被 cancel(cancel 已同步标 item=cancelled)
|
|
509
|
-
if (job.cancelled) {
|
|
510
|
-
item.status = 'cancelled';
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (result.status !== 'completed' || result.outputs.length === 0) {
|
|
515
|
-
throw new Error(result.error || `Workflow ${result.status}`);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const outputAssetId = result.outputs[0]!;
|
|
519
|
-
const duration = Date.now() - start;
|
|
520
|
-
|
|
521
|
-
// 成功
|
|
522
|
-
item.status = 'completed';
|
|
523
|
-
item.outputAssetId = outputAssetId;
|
|
524
|
-
item.duration = duration;
|
|
525
|
-
job.completed++;
|
|
526
|
-
|
|
527
|
-
this.emit({
|
|
528
|
-
type: 'batch:item:finished',
|
|
529
|
-
jobId: job.id,
|
|
530
|
-
itemId: item.id,
|
|
531
|
-
index: item.index,
|
|
532
|
-
total: job.items.length,
|
|
533
|
-
outputAssetId,
|
|
534
|
-
duration,
|
|
535
|
-
});
|
|
536
|
-
this.emitProgress(job);
|
|
537
|
-
} catch (err) {
|
|
538
|
-
// B1 守卫:被 cancel 的 workflow 抛错不计入 failed(item 已是 cancelled)
|
|
539
|
-
if (job.cancelled) {
|
|
540
|
-
item.status = 'cancelled';
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
// 失败:判断是否还可重试
|
|
544
|
-
item.attempts++;
|
|
545
|
-
const maxRetries = item.maxRetries;
|
|
546
|
-
if (item.attempts <= maxRetries) {
|
|
547
|
-
// 重试:回 pending,schedule 会再次拉起
|
|
548
|
-
item.status = 'pending';
|
|
549
|
-
// 注意:不重置 attempts,保留累计重试次数
|
|
550
|
-
// 清理本次导入的 input asset,避免重试重新 import 时旧 input 成为孤儿
|
|
551
|
-
// (Blocker 修复:maxRetries=3 全失败原本会累积 3 个孤儿 input)
|
|
552
|
-
if (inputAssetId !== undefined) {
|
|
553
|
-
void this.runtime.removeAsset(inputAssetId).catch(() => {});
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
item.status = 'failed';
|
|
557
|
-
item.error = err instanceof Error ? err : new Error(String(err));
|
|
558
|
-
job.failed++;
|
|
559
|
-
// M4:最终失败时清理已导入的 input asset,避免批量失败累积孤儿资产
|
|
560
|
-
// 占用 OPFS/IDB 空间并污染 listAssets / StatusBar 配额
|
|
561
|
-
if (inputAssetId !== undefined) {
|
|
562
|
-
void this.runtime.removeAsset(inputAssetId).catch(() => {});
|
|
272
|
+
off = this.eventBus.on('batch:item:started', (e) => {
|
|
273
|
+
if (e.jobId !== jobId) return;
|
|
274
|
+
seen++;
|
|
275
|
+
if (seen >= count) {
|
|
276
|
+
if (settled) return;
|
|
277
|
+
settled = true;
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
off();
|
|
280
|
+
resolve();
|
|
563
281
|
}
|
|
564
|
-
|
|
565
|
-
type: 'batch:item:failed',
|
|
566
|
-
jobId: job.id,
|
|
567
|
-
itemId: item.id,
|
|
568
|
-
index: item.index,
|
|
569
|
-
total: job.items.length,
|
|
570
|
-
error: item.error,
|
|
571
|
-
attempts: item.attempts,
|
|
572
|
-
});
|
|
573
|
-
this.emitProgress(job);
|
|
574
|
-
}
|
|
575
|
-
} finally {
|
|
576
|
-
// 触发 schedule 补满(用 ref 打破循环依赖)
|
|
577
|
-
if (!job.cancelled && !job.paused && !this.isTerminal(job.status)) {
|
|
578
|
-
void this.schedule(job);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/** 当前并发槽位(按内存压力动态收缩) */
|
|
584
|
-
private currentConcurrency(job: BatchJobInternal): number {
|
|
585
|
-
const pressure = this.memoryGuard?.getPressure() ?? 'low';
|
|
586
|
-
return shrinkConcurrencyByPressure(job.baseConcurrency, pressure);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/** 发出 progress 事件 + 通知订阅者 */
|
|
590
|
-
private emitProgress(job: BatchJobInternal): void {
|
|
591
|
-
const payload: BatchProgress = {
|
|
592
|
-
jobId: job.id,
|
|
593
|
-
completed: job.completed,
|
|
594
|
-
failed: job.failed,
|
|
595
|
-
total: job.items.length,
|
|
596
|
-
};
|
|
597
|
-
this.emit({
|
|
598
|
-
type: 'batch:progress',
|
|
599
|
-
...payload,
|
|
282
|
+
});
|
|
600
283
|
});
|
|
601
|
-
const subs = this.progressSubs.get(job.id);
|
|
602
|
-
if (subs) {
|
|
603
|
-
// M1 修复:遍历副本,防止 handler 内 unsubscribe/subscribe mutate 正在迭代的 Set
|
|
604
|
-
for (const fn of [...subs]) {
|
|
605
|
-
try {
|
|
606
|
-
fn(payload);
|
|
607
|
-
} catch {
|
|
608
|
-
// 单个订阅者异常不阻断其他订阅者
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
284
|
}
|
|
613
285
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
job.endedAt = Date.now();
|
|
623
|
-
job.status = job.failed > 0 ? 'failed' : 'completed';
|
|
624
|
-
// 注意:无论 job.status 是 'completed' 还是 'failed',都发 batch:completed 事件。
|
|
625
|
-
// 事件名表示"所有项已终结(含部分失败)",而非"全部成功"。
|
|
626
|
-
// 消费方应通过 payload.failed 区分;waitForCompletion 依赖此行为(只监听
|
|
627
|
-
// batch:completed + batch:cancelled 两种终态事件)。
|
|
628
|
-
this.emit({
|
|
629
|
-
type: 'batch:completed',
|
|
630
|
-
jobId: job.id,
|
|
631
|
-
total: job.items.length,
|
|
632
|
-
completed: job.completed,
|
|
633
|
-
failed: job.failed,
|
|
634
|
-
duration: job.endedAt - job.startedAt,
|
|
286
|
+
async waitForCompletion(jobId: string, timeoutMs = 5 * 60 * 1000): Promise<BatchJob> {
|
|
287
|
+
const job = this.jobs.get(jobId);
|
|
288
|
+
if (!job) throw new Error(`Batch job not found: ${jobId}`);
|
|
289
|
+
return this.progress.waitForCompletion<BatchJob>(jobId, {
|
|
290
|
+
timeoutMs,
|
|
291
|
+
isTerminal: () => this.scheduler.isTerminal(job.status),
|
|
292
|
+
resolveJob: () => this.toJobView(this.jobs.get(jobId) ?? job),
|
|
293
|
+
onTimeout: () => this.cancel(jobId),
|
|
635
294
|
});
|
|
636
|
-
// M5:进入终态清理进度订阅,避免 waitForCompletion 调用方丢弃 promise 后泄漏
|
|
637
|
-
this.cleanupJobSubs(job.id);
|
|
638
295
|
}
|
|
639
296
|
|
|
640
|
-
private
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
status
|
|
644
|
-
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return `${jobId}__${itemId}`;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/** 安全 emit(无 EventBus 时降级为 no-op,便于纯单元测试) */
|
|
654
|
-
private emit(event: LokvisEvent): void {
|
|
655
|
-
try {
|
|
656
|
-
this.eventBus.emit(event);
|
|
657
|
-
} catch {
|
|
658
|
-
// EventBus 异常不阻断批量逻辑
|
|
297
|
+
private pruneJobs(): void {
|
|
298
|
+
const terminal: BatchJobInternal[] = [];
|
|
299
|
+
for (const job of this.jobs.values()) {
|
|
300
|
+
if (this.scheduler.isTerminal(job.status)) terminal.push(job);
|
|
301
|
+
}
|
|
302
|
+
if (terminal.length <= MAX_RETAINED_JOBS) return;
|
|
303
|
+
terminal.sort((a, b) => a.startedAt - b.startedAt);
|
|
304
|
+
const removeCount = terminal.length - MAX_RETAINED_JOBS;
|
|
305
|
+
for (let i = 0; i < removeCount; i++) {
|
|
306
|
+
this.jobs.delete(terminal[i]!.id);
|
|
659
307
|
}
|
|
660
308
|
}
|
|
661
309
|
|
|
662
|
-
/** 内部可变状态 → 对外只读视图(浅拷贝 items 数组与每项) */
|
|
663
310
|
private toJobView(job: BatchJobInternal): BatchJob {
|
|
664
311
|
return {
|
|
665
312
|
id: job.id,
|
|
@@ -673,27 +320,3 @@ export class BatchProcessor {
|
|
|
673
320
|
};
|
|
674
321
|
}
|
|
675
322
|
}
|
|
676
|
-
|
|
677
|
-
/** BatchJob 内部可变状态(BatchProcessor 私有) */
|
|
678
|
-
interface BatchJobInternal {
|
|
679
|
-
id: string;
|
|
680
|
-
status: BatchJobStatus;
|
|
681
|
-
baseConcurrency: number;
|
|
682
|
-
maxRetries: number;
|
|
683
|
-
items: BatchItemInternal[];
|
|
684
|
-
completed: number;
|
|
685
|
-
failed: number;
|
|
686
|
-
startedAt: number;
|
|
687
|
-
endedAt?: number;
|
|
688
|
-
/** schedule 重入锁 */
|
|
689
|
-
scheduleLock: boolean;
|
|
690
|
-
paused: boolean;
|
|
691
|
-
cancelled: boolean;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/** BatchItem 内部可变状态 */
|
|
695
|
-
interface BatchItemInternal extends Omit<BatchItem, 'outputAssetId' | 'error' | 'duration'> {
|
|
696
|
-
outputAssetId?: AssetId;
|
|
697
|
-
error?: Error;
|
|
698
|
-
duration?: number;
|
|
699
|
-
}
|