@enableai-job-runtime/core 0.0.5
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/core.cjs.js +899 -0
- package/dist/core.cjs.prod.js +899 -0
- package/dist/core.d.ts +768 -0
- package/dist/core.esm-bundler.mjs +870 -0
- package/index.js +7 -0
- package/package.json +38 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @enableai-job-runtime/core v0.0.5
|
|
3
|
+
* (c) 2024-present Enableai
|
|
4
|
+
* @license Proprietary
|
|
5
|
+
**/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
9
|
+
|
|
10
|
+
var EventEmitter = require('eventemitter3');
|
|
11
|
+
var PQueue = require('p-queue');
|
|
12
|
+
|
|
13
|
+
const AsyncStatus = {
|
|
14
|
+
"Idle": 0,
|
|
15
|
+
"0": "Idle",
|
|
16
|
+
"Running": 1,
|
|
17
|
+
"1": "Running",
|
|
18
|
+
"Finished": 2,
|
|
19
|
+
"2": "Finished"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CONCURRENCY_POLICY = "allow";
|
|
23
|
+
|
|
24
|
+
const JobErrorCode = { "ShouldYield": "ShouldYield" };
|
|
25
|
+
class JobError extends Error {
|
|
26
|
+
constructor(code, message) {
|
|
27
|
+
super(message ?? code);
|
|
28
|
+
this.name = "JobError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isShouldYieldError(error) {
|
|
33
|
+
return error instanceof JobError && error.code === "ShouldYield";
|
|
34
|
+
}
|
|
35
|
+
function createShouldYieldError() {
|
|
36
|
+
return new JobError("ShouldYield");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class JobContext {
|
|
40
|
+
constructor() {
|
|
41
|
+
/**
|
|
42
|
+
* HookSlot 数组,顺序即为 hook 调用顺序
|
|
43
|
+
*/
|
|
44
|
+
this.slots = [];
|
|
45
|
+
/**
|
|
46
|
+
* 当前执行帧中的 slot 游标
|
|
47
|
+
*/
|
|
48
|
+
this.hookIndex = 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 获取当前帧的全部 slot
|
|
52
|
+
*/
|
|
53
|
+
getAllSlots() {
|
|
54
|
+
return this.slots;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Job 在每次运行步骤开始时调用
|
|
58
|
+
* 重置 hookIndex,确保从 slot[0] 开始
|
|
59
|
+
*/
|
|
60
|
+
beginFrame() {
|
|
61
|
+
this.hookIndex = 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 获取或初始化一个 slot,
|
|
65
|
+
* 根据调用顺序递增 hookIndex。
|
|
66
|
+
*/
|
|
67
|
+
useSlot(kind, init) {
|
|
68
|
+
const index = this.hookIndex++;
|
|
69
|
+
let slot = this.slots[index];
|
|
70
|
+
if (!slot) {
|
|
71
|
+
slot = init();
|
|
72
|
+
this.slots[index] = slot;
|
|
73
|
+
return slot;
|
|
74
|
+
}
|
|
75
|
+
if (slot.kind !== kind) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`[JobContext] \u7B2C ${index} \u4E2A Hook \u7684\u7C7B\u578B\u4E0D\u4E00\u81F4\uFF1A\u9884\u671F\u4E3A "${kind}"\uFF0C\u5B9E\u9645\u4E3A "${slot.kind}"\u3002\u8BF7\u786E\u4FDD\u6BCF\u6B21\u6267\u884C\u65F6 Hook \u7684\u8C03\u7528\u987A\u5E8F\u5B8C\u5168\u4E00\u81F4\u3002`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return slot;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 在 Job 结束(Completed / Cancelled / Error)时调用
|
|
84
|
+
*
|
|
85
|
+
* - 执行所有 effect cleanup
|
|
86
|
+
* - 清理 async promise(可选增强功能)
|
|
87
|
+
* - 清理 sleep timers(可选增强功能)
|
|
88
|
+
*/
|
|
89
|
+
destroy() {
|
|
90
|
+
for (const slot of this.slots) {
|
|
91
|
+
if (slot.kind === "effect") {
|
|
92
|
+
slot.cleanup?.();
|
|
93
|
+
}
|
|
94
|
+
if (slot.kind === "async") {
|
|
95
|
+
slot.controller?.abort();
|
|
96
|
+
slot.cancel?.();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.slots = [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const JobStatus = {
|
|
104
|
+
"Idle": "Idle",
|
|
105
|
+
"Running": "Running",
|
|
106
|
+
"Yield": "Yield",
|
|
107
|
+
"Paused": "Paused",
|
|
108
|
+
"Cancelled": "Cancelled",
|
|
109
|
+
"Completed": "Completed",
|
|
110
|
+
"Error": "Error"
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
class JobLifecycle {
|
|
114
|
+
constructor() {
|
|
115
|
+
this._status = "Idle";
|
|
116
|
+
this.emitter = new EventEmitter();
|
|
117
|
+
}
|
|
118
|
+
get status() {
|
|
119
|
+
return this._status;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 判断一个 Job 是否处于终态
|
|
123
|
+
*
|
|
124
|
+
* - 终态:Cancelled / Completed / Error
|
|
125
|
+
*/
|
|
126
|
+
static isTerminalStatus(status) {
|
|
127
|
+
return status === "Completed" || status === "Cancelled" || status === "Error";
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 判断一个 Job 是否仍然处于活动状态
|
|
131
|
+
*
|
|
132
|
+
* - 活动状态:Idle / Running / Yield / Paused
|
|
133
|
+
* - 终态:Cancelled / Completed / Error
|
|
134
|
+
*/
|
|
135
|
+
static isActiveStatus(status) {
|
|
136
|
+
return status === "Idle" || status === "Running" || status === "Yield" || status === "Paused";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 状态迁移 + 自动触发事件
|
|
140
|
+
*/
|
|
141
|
+
transitionTo(next, ctx) {
|
|
142
|
+
const prev = this._status;
|
|
143
|
+
if (prev === next) return;
|
|
144
|
+
this._status = next;
|
|
145
|
+
this.emitter.emit("statusChange", next);
|
|
146
|
+
if (prev === "Idle" && next === "Running") {
|
|
147
|
+
this.emitter.emit("start", ctx.props);
|
|
148
|
+
} else if (prev === "Paused" && next === "Running") {
|
|
149
|
+
this.emitter.emit("resume", ctx.props);
|
|
150
|
+
}
|
|
151
|
+
if (next === "Completed") {
|
|
152
|
+
this.emitter.emit("complete", ctx.props);
|
|
153
|
+
} else if (next === "Cancelled") {
|
|
154
|
+
this.emitter.emit("cancel", ctx.props);
|
|
155
|
+
} else if (next === "Error" && ctx.error !== void 0) {
|
|
156
|
+
this.emitter.emit("error", { error: ctx.error, props: ctx.props });
|
|
157
|
+
}
|
|
158
|
+
if (JobLifecycle.isTerminalStatus(next)) {
|
|
159
|
+
this.emitter.removeAllListeners();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
on(event, listener) {
|
|
163
|
+
this.emitter.on(event, listener);
|
|
164
|
+
return () => {
|
|
165
|
+
this.emitter.off(event, listener);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function invariant(condition, message) {
|
|
171
|
+
if (!condition) throw new Error(message);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const jobStack = [];
|
|
175
|
+
function pushCurrentJob(job) {
|
|
176
|
+
jobStack.push(job);
|
|
177
|
+
}
|
|
178
|
+
function popCurrentJob(job) {
|
|
179
|
+
const current = jobStack.pop();
|
|
180
|
+
if (current !== job) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"[JobRuntime] \u5F53\u524D Job \u51FA\u6808\u987A\u5E8F\u5F02\u5E38\uFF1A\u8BF7\u68C0\u67E5\u662F\u5426\u5728\u6B63\u786E\u7684 try/finally \u4E2D\u8C03\u7528 pushCurrentJob/popCurrentJob\u3002"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function getCurrentJob(allowNoJob = false) {
|
|
187
|
+
const current = jobStack[jobStack.length - 1];
|
|
188
|
+
if (!current && allowNoJob) {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
invariant(
|
|
192
|
+
!!current,
|
|
193
|
+
"[JobRuntime] \u5F53\u524D\u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684 Job\uFF0C\u8BF7\u786E\u8BA4 Job Hooks \u662F\u5426\u5728 Job \u51FD\u6570\u5185\u90E8\u88AB\u8C03\u7528\uFF08\u800C\u4E0D\u662F\u5728\u666E\u901A\u51FD\u6570\u6216\u5F02\u6B65\u56DE\u8C03\u4E2D\uFF09\u3002"
|
|
194
|
+
);
|
|
195
|
+
return current;
|
|
196
|
+
}
|
|
197
|
+
function getCurrentJobContext() {
|
|
198
|
+
return getCurrentJob().context;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const _Job = class _Job {
|
|
202
|
+
constructor(component, options, schedulerFallback) {
|
|
203
|
+
this.children = /* @__PURE__ */ new Set();
|
|
204
|
+
this._error = void 0;
|
|
205
|
+
/**
|
|
206
|
+
* Job 生命周期管理器
|
|
207
|
+
*/
|
|
208
|
+
this.lifecycle = new JobLifecycle();
|
|
209
|
+
/**
|
|
210
|
+
* Job 执行上下文(保存 hooks slot)
|
|
211
|
+
*/
|
|
212
|
+
this.context = new JobContext();
|
|
213
|
+
/**
|
|
214
|
+
* 当前调度任务句柄(用于取消)
|
|
215
|
+
*/
|
|
216
|
+
this.currentTask = null;
|
|
217
|
+
/**
|
|
218
|
+
* 是否已destroy,防止重复销毁
|
|
219
|
+
*/
|
|
220
|
+
this.destroyed = false;
|
|
221
|
+
invariant(typeof component === "function", "[Job] component \u5FC5\u987B\u662F\u51FD\u6570");
|
|
222
|
+
this.component = component;
|
|
223
|
+
const { id, key, props, scheduler, priority, parent } = options;
|
|
224
|
+
this.id = id ?? _Job.generateId();
|
|
225
|
+
this.key = key;
|
|
226
|
+
this.props = props;
|
|
227
|
+
this.scheduler = scheduler ?? schedulerFallback;
|
|
228
|
+
this.priority = priority ?? 3;
|
|
229
|
+
this.parent = parent;
|
|
230
|
+
if (parent) {
|
|
231
|
+
parent.children.add(this);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
get error() {
|
|
235
|
+
return this._error;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 监听 Job 生命周期事件
|
|
239
|
+
*/
|
|
240
|
+
on(event, listener) {
|
|
241
|
+
return this.lifecycle.on(event, listener);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Job 当前状态(只读)
|
|
245
|
+
*/
|
|
246
|
+
get status() {
|
|
247
|
+
return this.lifecycle.status;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 提供给 Hook 使用的 shouldYield 查询接口
|
|
251
|
+
*/
|
|
252
|
+
shouldYield() {
|
|
253
|
+
return this.scheduler.shouldYield();
|
|
254
|
+
}
|
|
255
|
+
static generateId() {
|
|
256
|
+
return `job_${this._idCounter++}`;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 启动 Job
|
|
260
|
+
*/
|
|
261
|
+
start() {
|
|
262
|
+
if (this.destroyed) return;
|
|
263
|
+
if (this.status !== "Idle") return;
|
|
264
|
+
this.lifecycle.transitionTo("Running", { props: this.props });
|
|
265
|
+
this.scheduleNext();
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 调度下一帧运行
|
|
269
|
+
*/
|
|
270
|
+
scheduleNext() {
|
|
271
|
+
if (this.destroyed) return;
|
|
272
|
+
if (this.currentTask) {
|
|
273
|
+
this.scheduler.cancel(this.currentTask);
|
|
274
|
+
this.currentTask = null;
|
|
275
|
+
}
|
|
276
|
+
this.currentTask = this.scheduler.schedule(this.priority, () => this.runStep());
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 执行一帧 Job
|
|
280
|
+
*/
|
|
281
|
+
runStep() {
|
|
282
|
+
if (this.status !== "Running" && this.status !== "Yield") return;
|
|
283
|
+
if (this.destroyed) return;
|
|
284
|
+
if (JobLifecycle.isTerminalStatus(this.status)) return;
|
|
285
|
+
this.lifecycle.transitionTo("Running", { props: this.props });
|
|
286
|
+
pushCurrentJob(this);
|
|
287
|
+
this.context.beginFrame();
|
|
288
|
+
try {
|
|
289
|
+
this.component(this.props);
|
|
290
|
+
this.finishSuccessfully();
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (isShouldYieldError(err)) {
|
|
293
|
+
this.lifecycle.transitionTo("Yield", { props: this.props });
|
|
294
|
+
this.scheduleNext();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.handleError(err);
|
|
298
|
+
} finally {
|
|
299
|
+
popCurrentJob(this);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 正常完成 Job
|
|
304
|
+
*/
|
|
305
|
+
finishSuccessfully() {
|
|
306
|
+
if (this.destroyed) return;
|
|
307
|
+
this.lifecycle.transitionTo("Completed", { props: this.props });
|
|
308
|
+
this.destroy();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 错误处理
|
|
312
|
+
*/
|
|
313
|
+
handleError(error) {
|
|
314
|
+
if (this.destroyed) return;
|
|
315
|
+
this.lifecycle.transitionTo("Error", { props: this.props, error });
|
|
316
|
+
this._error = error;
|
|
317
|
+
this.destroy();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 释放上下文资源
|
|
321
|
+
*/
|
|
322
|
+
destroy() {
|
|
323
|
+
if (this.destroyed) return;
|
|
324
|
+
this.destroyed = true;
|
|
325
|
+
if (this.currentTask) {
|
|
326
|
+
this.scheduler.cancel(this.currentTask);
|
|
327
|
+
this.currentTask = null;
|
|
328
|
+
}
|
|
329
|
+
this.context.destroy();
|
|
330
|
+
if (this.parent) {
|
|
331
|
+
this.parent.children.delete(this);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 暂停 Job:
|
|
336
|
+
* - 仅在 Running / Yield 状态下生效
|
|
337
|
+
* - 取消当前已调度但尚未执行的 task
|
|
338
|
+
* - 不销毁上下文,后续可以 resume() 继续执行
|
|
339
|
+
*/
|
|
340
|
+
pause() {
|
|
341
|
+
if (this.destroyed) return;
|
|
342
|
+
if (this.lifecycle.status !== "Running" && this.lifecycle.status !== "Yield") return;
|
|
343
|
+
this.lifecycle.transitionTo("Paused", { props: this.props });
|
|
344
|
+
if (this.currentTask) {
|
|
345
|
+
this.scheduler.cancel(this.currentTask);
|
|
346
|
+
this.currentTask = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* 恢复 Job:
|
|
351
|
+
* - 仅在 Paused 状态下生效
|
|
352
|
+
* - 重新调度下一帧执行
|
|
353
|
+
*/
|
|
354
|
+
resume() {
|
|
355
|
+
if (this.destroyed) return;
|
|
356
|
+
if (this.status !== "Paused") return;
|
|
357
|
+
this.lifecycle.transitionTo("Running", { props: this.props });
|
|
358
|
+
this.scheduleNext();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 取消 Job:
|
|
362
|
+
* - 将状态置为 Cancelled
|
|
363
|
+
* - 调用 destroy() 统一清理资源
|
|
364
|
+
*/
|
|
365
|
+
cancel() {
|
|
366
|
+
if (this.destroyed) return;
|
|
367
|
+
if (JobLifecycle.isTerminalStatus(this.status)) return;
|
|
368
|
+
this.lifecycle.transitionTo("Cancelled", { props: this.props });
|
|
369
|
+
this.destroy();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* 生成简单自增 ID
|
|
374
|
+
*/
|
|
375
|
+
_Job._idCounter = 1;
|
|
376
|
+
let Job = _Job;
|
|
377
|
+
|
|
378
|
+
const useJobAsync = (fn) => {
|
|
379
|
+
const context = getCurrentJobContext();
|
|
380
|
+
const slot = context.useSlot("async", () => ({
|
|
381
|
+
kind: "async",
|
|
382
|
+
status: 0,
|
|
383
|
+
started: false,
|
|
384
|
+
value: void 0,
|
|
385
|
+
error: void 0,
|
|
386
|
+
controller: void 0,
|
|
387
|
+
cancel: void 0
|
|
388
|
+
}));
|
|
389
|
+
if (!slot.started) {
|
|
390
|
+
slot.started = true;
|
|
391
|
+
const controller = new AbortController();
|
|
392
|
+
slot.controller = controller;
|
|
393
|
+
const signal = controller.signal;
|
|
394
|
+
const returned = fn(signal);
|
|
395
|
+
let promise;
|
|
396
|
+
let cancel;
|
|
397
|
+
if (typeof returned === "object" && returned !== null && "promise" in returned) {
|
|
398
|
+
promise = returned.promise;
|
|
399
|
+
cancel = returned.cancel;
|
|
400
|
+
} else {
|
|
401
|
+
promise = returned;
|
|
402
|
+
}
|
|
403
|
+
slot.cancel = cancel;
|
|
404
|
+
slot.status = 1;
|
|
405
|
+
promise.then((res) => {
|
|
406
|
+
slot.value = res;
|
|
407
|
+
}).catch((err) => {
|
|
408
|
+
slot.error = err;
|
|
409
|
+
}).finally(() => {
|
|
410
|
+
slot.status = 2;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
switch (slot.status) {
|
|
414
|
+
case 0:
|
|
415
|
+
case 1: {
|
|
416
|
+
throw createShouldYieldError();
|
|
417
|
+
}
|
|
418
|
+
case 2: {
|
|
419
|
+
return [slot.value, slot.error];
|
|
420
|
+
}
|
|
421
|
+
default: {
|
|
422
|
+
throw new Error("[useAsync] \u65E0\u6548\u7684\u5F02\u6B65\u72B6\u6001\uFF1A" + String(slot.status));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
function depsAreEqual(prev, next) {
|
|
428
|
+
if (!prev || !next) return false;
|
|
429
|
+
if (prev.length !== next.length) return false;
|
|
430
|
+
for (let i = 0; i < prev.length; i++) {
|
|
431
|
+
if (!Object.is(prev[i], next[i])) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
function useJobEffect(effect, deps) {
|
|
438
|
+
const context = getCurrentJobContext();
|
|
439
|
+
const slot = context.useSlot("effect", () => ({
|
|
440
|
+
kind: "effect",
|
|
441
|
+
cleanup: void 0,
|
|
442
|
+
deps: void 0
|
|
443
|
+
}));
|
|
444
|
+
if (deps === void 0) {
|
|
445
|
+
if (slot.cleanup) {
|
|
446
|
+
slot.cleanup();
|
|
447
|
+
}
|
|
448
|
+
const cleanup2 = effect();
|
|
449
|
+
slot.cleanup = typeof cleanup2 === "function" ? cleanup2 : void 0;
|
|
450
|
+
slot.deps = void 0;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const shouldRun = !slot.deps || !depsAreEqual(slot.deps, deps);
|
|
454
|
+
if (!shouldRun) return;
|
|
455
|
+
if (slot.cleanup) slot.cleanup();
|
|
456
|
+
const cleanup = effect();
|
|
457
|
+
slot.cleanup = typeof cleanup === "function" ? cleanup : void 0;
|
|
458
|
+
slot.deps = deps;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function useJobLoop(length, body) {
|
|
462
|
+
const job = getCurrentJob();
|
|
463
|
+
const context = getCurrentJobContext();
|
|
464
|
+
const slot = context.useSlot("loop", () => ({
|
|
465
|
+
kind: "loop",
|
|
466
|
+
index: 0
|
|
467
|
+
}));
|
|
468
|
+
for (let i = slot.index; i < length; i++) {
|
|
469
|
+
try {
|
|
470
|
+
body(i);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
if (isShouldYieldError(err)) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
"[useJobLoop] \u5FAA\u73AF\u4F53\u5185\u90E8\u4E0D\u5141\u8BB8\u76F4\u63A5\u89E6\u53D1\u8BA9\u6743\uFF08\u5982\u624B\u52A8 throw createShouldYieldError \u6216\u4F7F\u7528\u4F1A\u629B\u51FA ShouldYield \u7684 Hook\uFF09\u3002\u8BF7\u6539\u4E3A\u5728\u5FAA\u73AF\u5916\u90E8\u4F7F\u7528\u5176\u4ED6 Hook \u63A7\u5236\u8BA9\u6743\uFF0C\u6216\u62C6\u5206\u903B\u8F91\u3002"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
throw err;
|
|
478
|
+
}
|
|
479
|
+
slot.index = i + 1;
|
|
480
|
+
if (job.shouldYield()) {
|
|
481
|
+
throw createShouldYieldError();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
slot.index = length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getNow() {
|
|
488
|
+
if (typeof performance !== "undefined" && performance.now) {
|
|
489
|
+
return performance.now();
|
|
490
|
+
}
|
|
491
|
+
return Date.now();
|
|
492
|
+
}
|
|
493
|
+
function useJobSleep(ms) {
|
|
494
|
+
const context = getCurrentJobContext();
|
|
495
|
+
const slot = context.useSlot("sleep", () => ({
|
|
496
|
+
kind: "sleep",
|
|
497
|
+
until: null,
|
|
498
|
+
done: false,
|
|
499
|
+
promise: void 0,
|
|
500
|
+
resolve: void 0
|
|
501
|
+
}));
|
|
502
|
+
if (slot.done) return;
|
|
503
|
+
const now = getNow();
|
|
504
|
+
if (slot.until == null) {
|
|
505
|
+
slot.until = now + ms;
|
|
506
|
+
}
|
|
507
|
+
if (now < slot.until) {
|
|
508
|
+
throw createShouldYieldError();
|
|
509
|
+
}
|
|
510
|
+
slot.done = true;
|
|
511
|
+
slot.until = null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function useJobState(initial) {
|
|
515
|
+
const context = getCurrentJobContext();
|
|
516
|
+
const slot = context.useSlot("state", () => {
|
|
517
|
+
const value = typeof initial === "function" ? initial() : initial;
|
|
518
|
+
return {
|
|
519
|
+
kind: "state",
|
|
520
|
+
value
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
const setValue = (next) => {
|
|
524
|
+
slot.value = next;
|
|
525
|
+
};
|
|
526
|
+
return [slot.value, setValue];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
class JobHandleImpl {
|
|
530
|
+
constructor(job, key) {
|
|
531
|
+
this.job = job;
|
|
532
|
+
this.key = key;
|
|
533
|
+
}
|
|
534
|
+
get status() {
|
|
535
|
+
return this.job.status;
|
|
536
|
+
}
|
|
537
|
+
get error() {
|
|
538
|
+
return this.job.error;
|
|
539
|
+
}
|
|
540
|
+
cancel(cascade = false) {
|
|
541
|
+
this.job.cancel();
|
|
542
|
+
if (cascade) {
|
|
543
|
+
const cancelChildren = (job) => {
|
|
544
|
+
for (const child of job.children) {
|
|
545
|
+
child.cancel();
|
|
546
|
+
cancelChildren(child);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
cancelChildren(this.job);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
pause() {
|
|
553
|
+
this.job.pause();
|
|
554
|
+
}
|
|
555
|
+
resume() {
|
|
556
|
+
this.job.resume();
|
|
557
|
+
}
|
|
558
|
+
toPromise() {
|
|
559
|
+
if (this._promise) return this._promise;
|
|
560
|
+
this._promise = new Promise((resolve, reject) => {
|
|
561
|
+
const status = this.status;
|
|
562
|
+
if (status === "Completed") {
|
|
563
|
+
return resolve();
|
|
564
|
+
} else if (status === "Cancelled") {
|
|
565
|
+
return reject({ isCancelled: true });
|
|
566
|
+
} else if (status === "Error") {
|
|
567
|
+
return reject(this.error);
|
|
568
|
+
}
|
|
569
|
+
this.job.on("complete", () => resolve());
|
|
570
|
+
this.job.on("cancel", () => reject({ isCancelled: true }));
|
|
571
|
+
this.job.on("error", ({ error }) => reject(error));
|
|
572
|
+
});
|
|
573
|
+
return this._promise;
|
|
574
|
+
}
|
|
575
|
+
on(event, listener) {
|
|
576
|
+
return this.job.on(event, listener);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function createJobHandle(job, key) {
|
|
580
|
+
return new JobHandleImpl(job, key);
|
|
581
|
+
}
|
|
582
|
+
class NoopJobHandle {
|
|
583
|
+
constructor(key) {
|
|
584
|
+
this.key = key;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* 永远返回 Completed:
|
|
588
|
+
* - 表示“这个调用已经结束,没有正在运行的 Job”
|
|
589
|
+
* - skip 语义下非常合理
|
|
590
|
+
*/
|
|
591
|
+
get status() {
|
|
592
|
+
return "Completed";
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* 提供一个最小可用的 fake Job
|
|
596
|
+
*
|
|
597
|
+
* ⚠️ 只用于:
|
|
598
|
+
* - 测试
|
|
599
|
+
* - 调试 / 打 log
|
|
600
|
+
*
|
|
601
|
+
* 不建议业务侧真正依赖
|
|
602
|
+
*/
|
|
603
|
+
get job() {
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
toPromise() {
|
|
607
|
+
return Promise.resolve();
|
|
608
|
+
}
|
|
609
|
+
cancel() {
|
|
610
|
+
}
|
|
611
|
+
pause() {
|
|
612
|
+
}
|
|
613
|
+
resume() {
|
|
614
|
+
}
|
|
615
|
+
on(_event, _listener) {
|
|
616
|
+
return () => {
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function createNoopHandle(key) {
|
|
621
|
+
return new NoopJobHandle(key);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
class JobQueueManager {
|
|
625
|
+
constructor() {
|
|
626
|
+
this.queues = /* @__PURE__ */ new Map();
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* 获取 / 创建某个 key 对应的队列
|
|
630
|
+
*/
|
|
631
|
+
getOrCreateQueue(key, concurrency) {
|
|
632
|
+
if (!key) {
|
|
633
|
+
throw new Error("JobQueueManager \u9700\u8981 key");
|
|
634
|
+
}
|
|
635
|
+
let queue = this.queues.get(key);
|
|
636
|
+
if (!queue) {
|
|
637
|
+
queue = new PQueue({ concurrency });
|
|
638
|
+
this.queues.set(key, queue);
|
|
639
|
+
} else {
|
|
640
|
+
queue.concurrency = concurrency;
|
|
641
|
+
}
|
|
642
|
+
return queue;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* 将一个任务丢到指定 key 的队列里去执行
|
|
646
|
+
*/
|
|
647
|
+
enqueue(key, concurrency, task) {
|
|
648
|
+
const queue = this.getOrCreateQueue(key, concurrency);
|
|
649
|
+
queue.add(task);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
class ConcurrencyController {
|
|
654
|
+
// 用于创建 Job 的工厂:由 JobManager 注入
|
|
655
|
+
constructor(defaultScheduler, onCreateJob) {
|
|
656
|
+
this.defaultScheduler = defaultScheduler;
|
|
657
|
+
this.onCreateJob = onCreateJob;
|
|
658
|
+
this.queueManager = new JobQueueManager();
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* 统一入口,按 policy 决定如何处理
|
|
662
|
+
*/
|
|
663
|
+
run(component, options, getExistingJobs) {
|
|
664
|
+
const { policy, key } = options;
|
|
665
|
+
const getActiveJobs = () => getExistingJobs().filter((job) => !JobStatusIsTerminal(job.status));
|
|
666
|
+
switch (policy) {
|
|
667
|
+
case "allow":
|
|
668
|
+
return this.runAllow(component, options);
|
|
669
|
+
case "skip":
|
|
670
|
+
return this.runSkip(component, options, getActiveJobs());
|
|
671
|
+
case "replace":
|
|
672
|
+
return this.runReplace(component, options, getActiveJobs());
|
|
673
|
+
case "queue":
|
|
674
|
+
if (!key) {
|
|
675
|
+
throw new Error("\u5E76\u53D1\u7B56\u7565 queue \u9700\u8981\u6307\u5B9A key");
|
|
676
|
+
}
|
|
677
|
+
return this.runQueue(component, options, key);
|
|
678
|
+
default:
|
|
679
|
+
throw new Error(`\u672A\u77E5\u7684\u5E76\u53D1\u7B56\u7565\uFF1A${policy}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
createJob(component, options) {
|
|
683
|
+
const job = new Job(component, options, this.defaultScheduler);
|
|
684
|
+
this.onCreateJob(job);
|
|
685
|
+
return job;
|
|
686
|
+
}
|
|
687
|
+
enqueueJob(key, concurrency, job) {
|
|
688
|
+
return this.queueManager.enqueue(key, concurrency, async () => {
|
|
689
|
+
if (job.status === "Cancelled") return;
|
|
690
|
+
job.start();
|
|
691
|
+
await new Promise((resolve) => {
|
|
692
|
+
const offComplete = job.on("complete", () => {
|
|
693
|
+
offAll();
|
|
694
|
+
resolve();
|
|
695
|
+
});
|
|
696
|
+
const offCancel = job.on("cancel", () => {
|
|
697
|
+
offAll();
|
|
698
|
+
resolve();
|
|
699
|
+
});
|
|
700
|
+
const offError = job.on("error", () => {
|
|
701
|
+
offAll();
|
|
702
|
+
resolve();
|
|
703
|
+
});
|
|
704
|
+
const offAll = () => {
|
|
705
|
+
offComplete();
|
|
706
|
+
offCancel();
|
|
707
|
+
offError();
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// ------- 各策略具体实现 -------
|
|
713
|
+
runAllow(component, options) {
|
|
714
|
+
const { key, maxConcurrency } = options;
|
|
715
|
+
if (!maxConcurrency || maxConcurrency <= 0 || !key) {
|
|
716
|
+
const job2 = this.createJob(component, options);
|
|
717
|
+
job2.start();
|
|
718
|
+
return job2;
|
|
719
|
+
}
|
|
720
|
+
const job = this.createJob(component, options);
|
|
721
|
+
this.enqueueJob(key, maxConcurrency, job);
|
|
722
|
+
return job;
|
|
723
|
+
}
|
|
724
|
+
runSkip(component, options, activeJobs) {
|
|
725
|
+
if (activeJobs.length > 0) {
|
|
726
|
+
return activeJobs[activeJobs.length - 1];
|
|
727
|
+
}
|
|
728
|
+
return this.runAllow(component, options);
|
|
729
|
+
}
|
|
730
|
+
runReplace(component, options, activeJobs) {
|
|
731
|
+
for (const job of activeJobs) {
|
|
732
|
+
job.cancel();
|
|
733
|
+
}
|
|
734
|
+
return this.runAllow(component, options);
|
|
735
|
+
}
|
|
736
|
+
runQueue(component, options, key) {
|
|
737
|
+
const job = this.createJob(component, options);
|
|
738
|
+
const concurrency = options.maxConcurrency && options.maxConcurrency > 0 ? options.maxConcurrency : 1;
|
|
739
|
+
this.enqueueJob(key, concurrency, job);
|
|
740
|
+
return job;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function JobStatusIsTerminal(status) {
|
|
744
|
+
return status === "Completed" || status === "Cancelled" || status === "Error";
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
class JobKeyRegistry {
|
|
748
|
+
constructor() {
|
|
749
|
+
this.jobsByKey = /* @__PURE__ */ new Map();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* 注册一个 Job 到指定 key 下
|
|
753
|
+
*/
|
|
754
|
+
register(key, job) {
|
|
755
|
+
let set = this.jobsByKey.get(key);
|
|
756
|
+
if (!set) {
|
|
757
|
+
set = /* @__PURE__ */ new Set();
|
|
758
|
+
this.jobsByKey.set(key, set);
|
|
759
|
+
}
|
|
760
|
+
set.add(job);
|
|
761
|
+
const unsubscribe = job.on("statusChange", (status) => {
|
|
762
|
+
if (JobLifecycle.isTerminalStatus(status)) {
|
|
763
|
+
this.deleteJob(key, job);
|
|
764
|
+
unsubscribe();
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* 获取指定 key 下的所有 Job(可能为空数组)
|
|
770
|
+
*/
|
|
771
|
+
getJobs(key) {
|
|
772
|
+
const set = this.jobsByKey.get(key);
|
|
773
|
+
if (!set) return [];
|
|
774
|
+
return Array.from(set);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* 获取指定 key 下最近注册的一个 Job。
|
|
778
|
+
*
|
|
779
|
+
* - 用于 getJob 等场景,默认返回“最后一个创建”的 Job
|
|
780
|
+
*/
|
|
781
|
+
getLatest(key) {
|
|
782
|
+
const set = this.jobsByKey.get(key);
|
|
783
|
+
if (!set) return void 0;
|
|
784
|
+
let latest;
|
|
785
|
+
for (const job of set) {
|
|
786
|
+
latest = job;
|
|
787
|
+
}
|
|
788
|
+
return latest;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* 删除指定 key 下的某个 Job 引用
|
|
792
|
+
*/
|
|
793
|
+
deleteJob(key, job) {
|
|
794
|
+
const set = this.jobsByKey.get(key);
|
|
795
|
+
if (!set) return;
|
|
796
|
+
set.delete(job);
|
|
797
|
+
if (set.size === 0) {
|
|
798
|
+
this.jobsByKey.delete(key);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* 删除整个 key(通常在全量 reset 或不再需要管理该 key 时使用)
|
|
803
|
+
*/
|
|
804
|
+
deleteKey(key) {
|
|
805
|
+
this.jobsByKey.delete(key);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* 清空所有 key → Job 关联(通常在全量 reset 时使用)
|
|
809
|
+
*/
|
|
810
|
+
clear() {
|
|
811
|
+
this.jobsByKey.clear();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
class JobManager {
|
|
816
|
+
constructor(scheduler) {
|
|
817
|
+
this.registry = new JobKeyRegistry();
|
|
818
|
+
this.concurrency = new ConcurrencyController(scheduler, (job) => {
|
|
819
|
+
if (job.key) this.registry.register(job.key, job);
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* 创建并启动一个新的 Job,并根据并发策略处理同 key Job,并返回 JobHandle。
|
|
824
|
+
*/
|
|
825
|
+
run(component, options) {
|
|
826
|
+
const { key, policy = DEFAULT_CONCURRENCY_POLICY, maxConcurrency, parent = getCurrentJob(true) } = options;
|
|
827
|
+
const job = this.concurrency.run(component, { ...options, policy, maxConcurrency, parent }, () => {
|
|
828
|
+
return key ? this.registry.getJobs(key) : [];
|
|
829
|
+
});
|
|
830
|
+
return job ? createJobHandle(job, key) : createNoopHandle(key);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* 按 key 取消 Job:
|
|
834
|
+
* - 当前实现:取消该 key 下所有 Job(包含活动和终态,终态调用 cancel 为 no-op)
|
|
835
|
+
*/
|
|
836
|
+
cancel(key) {
|
|
837
|
+
const jobs = this.registry.getJobs(key);
|
|
838
|
+
for (const job of jobs) {
|
|
839
|
+
job.cancel();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* 按 key 暂停 Job:
|
|
844
|
+
* - 暂停该 key 下所有活跃 Job
|
|
845
|
+
*/
|
|
846
|
+
pause(key) {
|
|
847
|
+
const jobs = this.registry.getJobs(key);
|
|
848
|
+
for (const job of jobs) {
|
|
849
|
+
if (JobLifecycle.isActiveStatus(job.status)) {
|
|
850
|
+
job.pause();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* 按 key 恢复 Job:
|
|
856
|
+
* - 恢复该 key 下所有处于 Paused 状态的 Job
|
|
857
|
+
*/
|
|
858
|
+
resume(key) {
|
|
859
|
+
const jobs = this.registry.getJobs(key);
|
|
860
|
+
for (const job of jobs) {
|
|
861
|
+
if (job.status === "Paused") {
|
|
862
|
+
job.resume();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* 获取指定 key 对应的“最近的一个 Job”(主要用于测试/调试)
|
|
868
|
+
*/
|
|
869
|
+
getJob(key) {
|
|
870
|
+
return this.registry.getLatest(key);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
exports.AsyncStatus = AsyncStatus;
|
|
875
|
+
exports.ConcurrencyController = ConcurrencyController;
|
|
876
|
+
exports.DEFAULT_CONCURRENCY_POLICY = DEFAULT_CONCURRENCY_POLICY;
|
|
877
|
+
exports.Job = Job;
|
|
878
|
+
exports.JobContext = JobContext;
|
|
879
|
+
exports.JobError = JobError;
|
|
880
|
+
exports.JobErrorCode = JobErrorCode;
|
|
881
|
+
exports.JobHandleImpl = JobHandleImpl;
|
|
882
|
+
exports.JobKeyRegistry = JobKeyRegistry;
|
|
883
|
+
exports.JobLifecycle = JobLifecycle;
|
|
884
|
+
exports.JobManager = JobManager;
|
|
885
|
+
exports.JobStatus = JobStatus;
|
|
886
|
+
exports.createJobHandle = createJobHandle;
|
|
887
|
+
exports.createNoopHandle = createNoopHandle;
|
|
888
|
+
exports.createShouldYieldError = createShouldYieldError;
|
|
889
|
+
exports.getCurrentJob = getCurrentJob;
|
|
890
|
+
exports.getCurrentJobContext = getCurrentJobContext;
|
|
891
|
+
exports.invariant = invariant;
|
|
892
|
+
exports.isShouldYieldError = isShouldYieldError;
|
|
893
|
+
exports.popCurrentJob = popCurrentJob;
|
|
894
|
+
exports.pushCurrentJob = pushCurrentJob;
|
|
895
|
+
exports.useJobAsync = useJobAsync;
|
|
896
|
+
exports.useJobEffect = useJobEffect;
|
|
897
|
+
exports.useJobLoop = useJobLoop;
|
|
898
|
+
exports.useJobSleep = useJobSleep;
|
|
899
|
+
exports.useJobState = useJobState;
|