@longzai-intelligence-git/fetch-scheduler 0.1.7
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 +15 -0
- package/dist/index.d.ts +427 -0
- package/dist/index.js +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @longzai-intelligence-git/fetch-scheduler
|
|
2
|
+
|
|
3
|
+
Longzai Intelligence Git 的多仓拉取调度引擎。
|
|
4
|
+
|
|
5
|
+
## 简介
|
|
6
|
+
|
|
7
|
+
治理巨量仓库远端更新的调度关注点:以分档 TTL 与每仓抖动错峰决定何时对哪些仓库发起 fetch,受控并发池与单仓超时保障执行不击穿磁盘与远端,连续失败按阶梯退避隔离不健康仓库。仓库候选装载、拉取执行、状态持久化与事件上报全部经依赖注入回调交由宿主承载,引擎自身零落盘。
|
|
8
|
+
|
|
9
|
+
## 能力
|
|
10
|
+
|
|
11
|
+
- 分档调度:活跃、近期、休眠三档 TTL 与每仓抖动错峰
|
|
12
|
+
- 并发限流:受控并发池与单仓拉取超时保护
|
|
13
|
+
- 失败退避:连续失败阶梯退避与成功复位
|
|
14
|
+
- 纯规划层:选仓判定为注入时钟与随机源的纯函数,零定时器可测
|
|
15
|
+
- 宿主无关:候选、执行器、状态存储与事件全部经注入回调
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* fetch 调度引擎的类型契约
|
|
4
|
+
*
|
|
5
|
+
* 承载分档拉取调度的全部类型定义:候选、退避状态、选项与事件
|
|
6
|
+
* 宿主经这些类型注入仓库候选并接收调度事件,引擎不定义持久化与执行细节
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 时钟源函数,返回毫秒时间戳
|
|
10
|
+
*/
|
|
11
|
+
type ClockSource = () => number;
|
|
12
|
+
/**
|
|
13
|
+
* 随机源函数,返回零到一区间的浮点数
|
|
14
|
+
*/
|
|
15
|
+
type RandomSource = () => number;
|
|
16
|
+
/**
|
|
17
|
+
* 拉取分档标识
|
|
18
|
+
*
|
|
19
|
+
* `active` 活跃档,`recent` 近期档,`dormant` 休眠档
|
|
20
|
+
*/
|
|
21
|
+
type FetchTier = 'active' | 'recent' | 'dormant';
|
|
22
|
+
/**
|
|
23
|
+
* 单仓失败退避状态
|
|
24
|
+
*
|
|
25
|
+
* 由引擎在拉取失败时构造并经 `onFetchStateChange` 交宿主持久化
|
|
26
|
+
* 拉取成功时引擎回传 `null` 表示清除退避状态
|
|
27
|
+
*/
|
|
28
|
+
type FetchFailureState = {
|
|
29
|
+
/**
|
|
30
|
+
* 连续失败次数,成功拉取后复位为清除状态
|
|
31
|
+
*/
|
|
32
|
+
readonly consecutiveFailures: number;
|
|
33
|
+
/**
|
|
34
|
+
* 退避截止时刻,毫秒时间戳,到期前该仓被跳过
|
|
35
|
+
*/
|
|
36
|
+
readonly backoffUntil: number | null;
|
|
37
|
+
/**
|
|
38
|
+
* 最近一次失败的错误描述
|
|
39
|
+
*/
|
|
40
|
+
readonly lastError: string | null;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* 单仓拉取候选
|
|
44
|
+
*
|
|
45
|
+
* 由宿主装载并整批提供给引擎,引擎只读不改
|
|
46
|
+
*/
|
|
47
|
+
type FetchCandidate = {
|
|
48
|
+
/**
|
|
49
|
+
* 宿主侧仓库标识,引擎不解释语义仅原样透传
|
|
50
|
+
*/
|
|
51
|
+
readonly repoId: string;
|
|
52
|
+
/**
|
|
53
|
+
* 仓库绝对路径,供宿主拉取执行器定位仓库
|
|
54
|
+
*/
|
|
55
|
+
readonly repoPath: string;
|
|
56
|
+
/**
|
|
57
|
+
* 当前分支名,`null` 表示游离头指针或未诞生的仓库,规划层直接跳过
|
|
58
|
+
*/
|
|
59
|
+
readonly branch: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* 最近一次成功拉取时刻,毫秒时间戳,`null` 表示从未拉取视为立即到期
|
|
62
|
+
*/
|
|
63
|
+
readonly lastFetchAt: number | null;
|
|
64
|
+
/**
|
|
65
|
+
* 最近一次活动时刻,毫秒时间戳,由宿主定义活动语义,`null` 视为休眠档
|
|
66
|
+
*/
|
|
67
|
+
readonly lastActivityAt: number | null;
|
|
68
|
+
/**
|
|
69
|
+
* 是否存在未推送提交,存在时无条件归入活跃档
|
|
70
|
+
*/
|
|
71
|
+
readonly hasUnpushed: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* 既有失败退避状态,`null` 表示健康
|
|
74
|
+
*/
|
|
75
|
+
readonly fetchState: FetchFailureState | null;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* 三档 TTL 配置,单位毫秒
|
|
79
|
+
*/
|
|
80
|
+
type FetchTierTtlMs = {
|
|
81
|
+
/**
|
|
82
|
+
* 活跃档拉取间隔
|
|
83
|
+
*/
|
|
84
|
+
readonly active: number;
|
|
85
|
+
/**
|
|
86
|
+
* 近期档拉取间隔
|
|
87
|
+
*/
|
|
88
|
+
readonly recent: number;
|
|
89
|
+
/**
|
|
90
|
+
* 休眠档拉取间隔
|
|
91
|
+
*/
|
|
92
|
+
readonly dormant: number;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* 调度器选项
|
|
96
|
+
*
|
|
97
|
+
* 宿主可部分覆盖,未覆盖字段取引擎内置默认值
|
|
98
|
+
*/
|
|
99
|
+
type FetchSchedulerOptions = {
|
|
100
|
+
/**
|
|
101
|
+
* 三档拉取间隔
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* 默认活跃档 15 分钟,近期档 1 小时,休眠档 12 小时
|
|
105
|
+
*/
|
|
106
|
+
readonly tierTtlMs: FetchTierTtlMs;
|
|
107
|
+
/**
|
|
108
|
+
* 活跃档判定窗口,最近活动落在窗口内或存在未推送提交即归活跃档
|
|
109
|
+
*
|
|
110
|
+
* @default 21600000
|
|
111
|
+
*/
|
|
112
|
+
readonly activeWindowMs: number;
|
|
113
|
+
/**
|
|
114
|
+
* 近期档判定窗口,最近活动落在窗口外且晚于休眠即归近期档
|
|
115
|
+
*
|
|
116
|
+
* @default 604800000
|
|
117
|
+
*/
|
|
118
|
+
readonly recentWindowMs: number;
|
|
119
|
+
/**
|
|
120
|
+
* 每仓到期抖动比例,实际间隔在档位 TTL 上下浮动该比例,用于错峰
|
|
121
|
+
*
|
|
122
|
+
* @default 0.2
|
|
123
|
+
*/
|
|
124
|
+
readonly jitterRatio: number;
|
|
125
|
+
/**
|
|
126
|
+
* 常驻轮询间隔
|
|
127
|
+
*
|
|
128
|
+
* @default 30000
|
|
129
|
+
*/
|
|
130
|
+
readonly tickIntervalMs: number;
|
|
131
|
+
/**
|
|
132
|
+
* 单轮拉取并发上限
|
|
133
|
+
*
|
|
134
|
+
* @default 3
|
|
135
|
+
*/
|
|
136
|
+
readonly concurrency: number;
|
|
137
|
+
/**
|
|
138
|
+
* 单仓拉取超时,超时按失败计入退避
|
|
139
|
+
*
|
|
140
|
+
* @default 120000
|
|
141
|
+
*/
|
|
142
|
+
readonly fetchTimeoutMs: number;
|
|
143
|
+
/**
|
|
144
|
+
* 连续失败退避阶梯,第 n 次连续失败取下标 n 减一的位置,越界取末位
|
|
145
|
+
*
|
|
146
|
+
* @remarks
|
|
147
|
+
* 默认阶梯为 15 分钟,1 小时,4 小时
|
|
148
|
+
*/
|
|
149
|
+
readonly backoffStepsMs: readonly number[];
|
|
150
|
+
/**
|
|
151
|
+
* 单轮最大派发数,到期的多余仓顺延到下一轮
|
|
152
|
+
*
|
|
153
|
+
* @default 64
|
|
154
|
+
*/
|
|
155
|
+
readonly maxBatchSize: number;
|
|
156
|
+
/**
|
|
157
|
+
* 参与调度的分档集合,不在集合内的分档本轮不派发
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* 默认三档全部参与
|
|
161
|
+
*/
|
|
162
|
+
readonly tiers: readonly FetchTier[];
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* 调度器选项的用户覆盖形态
|
|
166
|
+
*
|
|
167
|
+
* 顶层字段与三档间隔嵌套字段均可部分覆盖,未覆盖字段取引擎内置默认值
|
|
168
|
+
*/
|
|
169
|
+
type FetchSchedulerOptionsOverrides = { readonly [K in Exclude<keyof FetchSchedulerOptions, 'tierTtlMs'>]?: FetchSchedulerOptions[K]; } & {
|
|
170
|
+
/**
|
|
171
|
+
* 三档拉取间隔的部分覆盖
|
|
172
|
+
*/
|
|
173
|
+
readonly tierTtlMs?: Partial<FetchTierTtlMs>;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* 单仓计划派发条目
|
|
177
|
+
*/
|
|
178
|
+
type PlannedFetch = {
|
|
179
|
+
/**
|
|
180
|
+
* 被选中派发的候选
|
|
181
|
+
*/
|
|
182
|
+
readonly candidate: FetchCandidate;
|
|
183
|
+
/**
|
|
184
|
+
* 规划层判定的分档
|
|
185
|
+
*/
|
|
186
|
+
readonly tier: FetchTier;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* 规划统计计数
|
|
190
|
+
*/
|
|
191
|
+
type FetchPlanCounts = {
|
|
192
|
+
/**
|
|
193
|
+
* 因退避未到期被跳过的仓数
|
|
194
|
+
*/
|
|
195
|
+
readonly skippedByBackoff: number;
|
|
196
|
+
/**
|
|
197
|
+
* 因无有效分支被跳过的仓数
|
|
198
|
+
*/
|
|
199
|
+
readonly skippedNoBranch: number;
|
|
200
|
+
/**
|
|
201
|
+
* 分档 TTL 未到期未入选的仓数
|
|
202
|
+
*/
|
|
203
|
+
readonly notDue: number;
|
|
204
|
+
/**
|
|
205
|
+
* 分档不在参与集合被跳过的仓数
|
|
206
|
+
*/
|
|
207
|
+
readonly skippedOutOfTier: number;
|
|
208
|
+
/**
|
|
209
|
+
* 到期但因单轮上限顺延的仓数
|
|
210
|
+
*/
|
|
211
|
+
readonly deferredByBatchCap: number;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* 单轮规划结果
|
|
215
|
+
*/
|
|
216
|
+
type FetchPlan = {
|
|
217
|
+
/**
|
|
218
|
+
* 本轮派发目标列表,保持候选输入顺序
|
|
219
|
+
*/
|
|
220
|
+
readonly targets: readonly PlannedFetch[];
|
|
221
|
+
/**
|
|
222
|
+
* 各类跳过与顺延的计数
|
|
223
|
+
*/
|
|
224
|
+
readonly counts: FetchPlanCounts;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* 调度器事件
|
|
228
|
+
*/
|
|
229
|
+
type FetchSchedulerEvent = {
|
|
230
|
+
/**
|
|
231
|
+
* 本轮开始派发
|
|
232
|
+
*/
|
|
233
|
+
readonly kind: 'round-started';
|
|
234
|
+
/**
|
|
235
|
+
* 本轮派发目标数
|
|
236
|
+
*/
|
|
237
|
+
readonly total: number;
|
|
238
|
+
} | {
|
|
239
|
+
/**
|
|
240
|
+
* 单仓拉取成功
|
|
241
|
+
*/
|
|
242
|
+
readonly kind: 'repo-fetched';
|
|
243
|
+
/**
|
|
244
|
+
* 宿主侧仓库标识
|
|
245
|
+
*/
|
|
246
|
+
readonly repoId: string;
|
|
247
|
+
/**
|
|
248
|
+
* 本次拉取耗时
|
|
249
|
+
*/
|
|
250
|
+
readonly durationMs: number;
|
|
251
|
+
} | {
|
|
252
|
+
/**
|
|
253
|
+
* 单仓拉取失败并进入退避
|
|
254
|
+
*/
|
|
255
|
+
readonly kind: 'repo-fetch-failed';
|
|
256
|
+
/**
|
|
257
|
+
* 宿主侧仓库标识
|
|
258
|
+
*/
|
|
259
|
+
readonly repoId: string;
|
|
260
|
+
/**
|
|
261
|
+
* 失败错误描述
|
|
262
|
+
*/
|
|
263
|
+
readonly error: string;
|
|
264
|
+
/**
|
|
265
|
+
* 失败后的连续失败次数
|
|
266
|
+
*/
|
|
267
|
+
readonly consecutiveFailures: number;
|
|
268
|
+
/**
|
|
269
|
+
* 本次失败对应的退避截止时刻,毫秒时间戳
|
|
270
|
+
*/
|
|
271
|
+
readonly backoffUntil: number | null;
|
|
272
|
+
} | {
|
|
273
|
+
/**
|
|
274
|
+
* 本轮执行完毕
|
|
275
|
+
*/
|
|
276
|
+
readonly kind: 'round-finished';
|
|
277
|
+
/**
|
|
278
|
+
* 成功拉取数
|
|
279
|
+
*/
|
|
280
|
+
readonly done: number;
|
|
281
|
+
/**
|
|
282
|
+
* 失败数
|
|
283
|
+
*/
|
|
284
|
+
readonly failed: number;
|
|
285
|
+
/**
|
|
286
|
+
* 规划层各类跳过与顺延的总数
|
|
287
|
+
*/
|
|
288
|
+
readonly skipped: number;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* 调度器依赖注入集合
|
|
292
|
+
*/
|
|
293
|
+
type FetchSchedulerDeps = {
|
|
294
|
+
/**
|
|
295
|
+
* 装载全部候选,每轮开始时调用一次
|
|
296
|
+
*/
|
|
297
|
+
readonly loadCandidates: () => Promise<readonly FetchCandidate[]>;
|
|
298
|
+
/**
|
|
299
|
+
* 执行单仓拉取,抛错视为失败并计入退避
|
|
300
|
+
*
|
|
301
|
+
* @param candidate - 待拉取的候选
|
|
302
|
+
*/
|
|
303
|
+
readonly fetchRepo: (candidate: FetchCandidate) => Promise<void>;
|
|
304
|
+
/**
|
|
305
|
+
* 拉取状态变更通知,成功传 `null` 清除退避,失败传新的退避状态
|
|
306
|
+
*
|
|
307
|
+
* @remarks
|
|
308
|
+
* 宿主据此持久化,引擎自身不落盘
|
|
309
|
+
* @param candidate - 状态变更的候选
|
|
310
|
+
* @param state - 新的退避状态或 `null`
|
|
311
|
+
*/
|
|
312
|
+
readonly onFetchStateChange: (candidate: FetchCandidate, state: FetchFailureState | null) => Promise<void> | void;
|
|
313
|
+
/**
|
|
314
|
+
* 调度事件回调,可选
|
|
315
|
+
*
|
|
316
|
+
* @param event - 单条调度事件
|
|
317
|
+
*/
|
|
318
|
+
readonly onEvent?: (event: FetchSchedulerEvent) => void;
|
|
319
|
+
/**
|
|
320
|
+
* 时钟注入,默认取 `Date.now`,测试注入伪时钟获得确定性
|
|
321
|
+
*/
|
|
322
|
+
readonly now?: ClockSource;
|
|
323
|
+
/**
|
|
324
|
+
* 随机源注入,默认取 `Math.random`,测试注入固定值获得确定性抖动
|
|
325
|
+
*/
|
|
326
|
+
readonly random?: RandomSource;
|
|
327
|
+
/**
|
|
328
|
+
* 选项部分覆盖,未覆盖字段取引擎内置默认值
|
|
329
|
+
*/
|
|
330
|
+
readonly options?: FetchSchedulerOptionsOverrides;
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* 常驻调度器句柄
|
|
334
|
+
*/
|
|
335
|
+
type FetchScheduler = {
|
|
336
|
+
/**
|
|
337
|
+
* 启动常驻轮询,立即执行一轮后按 `tickIntervalMs` 周期执行,幂等
|
|
338
|
+
*/
|
|
339
|
+
readonly start: () => void;
|
|
340
|
+
/**
|
|
341
|
+
* 停止常驻轮询,在途轮次自然收束不再派发新轮,幂等
|
|
342
|
+
*/
|
|
343
|
+
readonly stop: () => void;
|
|
344
|
+
/**
|
|
345
|
+
* 手动执行一轮,与常驻轮询共享单飞闸门,在途时静默跳过
|
|
346
|
+
*/
|
|
347
|
+
readonly tickNow: () => Promise<void>;
|
|
348
|
+
/**
|
|
349
|
+
* 常驻轮询是否在运行
|
|
350
|
+
*/
|
|
351
|
+
readonly isRunning: () => boolean;
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/create-fetch-scheduler.factory.d.ts
|
|
355
|
+
/**
|
|
356
|
+
* 引擎内置完整默认选项
|
|
357
|
+
*/
|
|
358
|
+
declare const DEFAULT_FETCH_SCHEDULER_OPTIONS: FetchSchedulerOptions;
|
|
359
|
+
/**
|
|
360
|
+
* 部分选项到完整选项的解析
|
|
361
|
+
*
|
|
362
|
+
* 顶层字段浅合并,三档间隔嵌套合并,数组字段未提供时整体取默认
|
|
363
|
+
*
|
|
364
|
+
* @param partial - 宿主提供的部分选项,可为空
|
|
365
|
+
* @returns 合并默认值后的完整选项
|
|
366
|
+
*/
|
|
367
|
+
declare const resolveFetchSchedulerOptions: (partial?: FetchSchedulerOptionsOverrides) => FetchSchedulerOptions;
|
|
368
|
+
/**
|
|
369
|
+
* 常驻 fetch 调度器创建
|
|
370
|
+
*
|
|
371
|
+
* 规划判定委托纯规划层,执行经并发池限流并带单仓超时,状态变更与事件经注入回调外抛
|
|
372
|
+
*
|
|
373
|
+
* @param deps - 宿主注入的依赖集合
|
|
374
|
+
* @returns 调度器句柄
|
|
375
|
+
*/
|
|
376
|
+
declare const createFetchScheduler: (deps: FetchSchedulerDeps) => FetchScheduler;
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/planner.utils.d.ts
|
|
379
|
+
/**
|
|
380
|
+
* 候选分档判定
|
|
381
|
+
*
|
|
382
|
+
* 存在未推送提交无条件归活跃档,随后按最近活动时刻对照窗口判定,无活动记录归休眠档
|
|
383
|
+
*
|
|
384
|
+
* @param candidate - 待判定的候选
|
|
385
|
+
* @param options - 完整调度选项
|
|
386
|
+
* @param now - 当前时刻,毫秒时间戳
|
|
387
|
+
* @returns 分档标识
|
|
388
|
+
*/
|
|
389
|
+
declare const resolveFetchTier: (candidate: FetchCandidate, options: FetchSchedulerOptions, now: number) => FetchTier;
|
|
390
|
+
/**
|
|
391
|
+
* 带抖动的有效到期间隔计算
|
|
392
|
+
*
|
|
393
|
+
* 实际间隔等于档位 TTL 乘以一到加减抖动比例区间内的系数,随机源居中时系数恰为一
|
|
394
|
+
*
|
|
395
|
+
* @param tier - 分档标识
|
|
396
|
+
* @param options - 完整调度选项
|
|
397
|
+
* @param random - 随机源,取值区间零到一
|
|
398
|
+
* @returns 该仓本轮适用的有效到期间隔
|
|
399
|
+
*/
|
|
400
|
+
declare const effectiveTtlMs: (tier: FetchTier, options: FetchSchedulerOptions, random: RandomSource) => number;
|
|
401
|
+
/**
|
|
402
|
+
* 候选到期判定
|
|
403
|
+
*
|
|
404
|
+
* 从未拉取视为立即到期,否则按距上次拉取时长对照带抖动的有效间隔判定
|
|
405
|
+
*
|
|
406
|
+
* @param candidate - 待判定的候选
|
|
407
|
+
* @param tier - 候选分档
|
|
408
|
+
* @param options - 完整调度选项
|
|
409
|
+
* @param now - 当前时刻,毫秒时间戳
|
|
410
|
+
* @param random - 随机源
|
|
411
|
+
* @returns 是否到期
|
|
412
|
+
*/
|
|
413
|
+
declare const isDue: (candidate: FetchCandidate, tier: FetchTier, options: FetchSchedulerOptions, now: number, random: RandomSource) => boolean;
|
|
414
|
+
/**
|
|
415
|
+
* 单轮派发目标规划
|
|
416
|
+
*
|
|
417
|
+
* 逐候选完成分支过滤、退避过滤、分档判定、参与档过滤与到期筛选,到期目标按输入顺序截断到单轮上限
|
|
418
|
+
*
|
|
419
|
+
* @param candidates - 宿主装载的全部候选
|
|
420
|
+
* @param options - 完整调度选项
|
|
421
|
+
* @param now - 当前时刻,毫秒时间戳
|
|
422
|
+
* @param random - 随机源
|
|
423
|
+
* @returns 单轮规划结果
|
|
424
|
+
*/
|
|
425
|
+
declare const planFetchBatch: (candidates: readonly FetchCandidate[], options: FetchSchedulerOptions, now: number, random: RandomSource) => FetchPlan;
|
|
426
|
+
//#endregion
|
|
427
|
+
export { DEFAULT_FETCH_SCHEDULER_OPTIONS, type FetchCandidate, type FetchFailureState, type FetchPlan, type FetchPlanCounts, type FetchScheduler, type FetchSchedulerDeps, type FetchSchedulerEvent, type FetchSchedulerOptions, type FetchSchedulerOptionsOverrides, type FetchTier, type FetchTierTtlMs, type PlannedFetch, createFetchScheduler, effectiveTtlMs, isDue, planFetchBatch, resolveFetchSchedulerOptions, resolveFetchTier };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createSemaphoreScheduler as e}from"@longzai-intelligence-git/core";const t=(e,t,n)=>{if(e.hasUnpushed)return`active`;let r=e.lastActivityAt;if(r===null)return`dormant`;let i=n-r;return i<=t.activeWindowMs?`active`:i<=t.recentWindowMs?`recent`:`dormant`},n=(e,t,n)=>t.tierTtlMs[e]*(1+(n()*2-1)*t.jitterRatio),r=(e,t,r,i,a)=>{let o=e.lastFetchAt;return o===null||i-o>=n(t,r,a)},i=(e,n,i,a)=>{let o=[],s=0,c=0,l=0,u=0,d=new Set(n.tiers);for(let f of e){if(f.branch===null){c+=1;continue}let e=f.fetchState?.backoffUntil??null;if(e!==null&&e>i){s+=1;continue}let p=t(f,n,i);if(!d.has(p)){u+=1;continue}if(!r(f,p,n,i,a)){l+=1;continue}o.push({candidate:f,tier:p})}let f=o.slice(0,n.maxBatchSize),p=o.length-f.length;return{targets:f,counts:{skippedByBackoff:s,skippedNoBranch:c,notDue:l,skippedOutOfTier:u,deferredByBatchCap:p}}},a={active:9e5,recent:36e5,dormant:432e5},o={tierTtlMs:a,activeWindowMs:216e5,recentWindowMs:6048e5,jitterRatio:.2,tickIntervalMs:3e4,concurrency:3,fetchTimeoutMs:12e4,backoffStepsMs:[9e5,36e5,144e5],maxBatchSize:64,tiers:[`active`,`recent`,`dormant`]},s=e=>({...o,...e,tierTtlMs:{...a,...e?.tierTtlMs},backoffStepsMs:e?.backoffStepsMs??o.backoffStepsMs,tiers:e?.tiers??o.tiers}),c=(e,t,n,r)=>{let i=e===null?1:e.consecutiveFailures+1;return r.length===0?{consecutiveFailures:i,backoffUntil:null,lastError:t}:{consecutiveFailures:i,backoffUntil:n+(r[Math.min(i-1,r.length-1)]??0),lastError:t}},l=t=>{let n=s(t.options),r=e(),a=null,o=!1,l=t.now??Date.now,u=t.random??Math.random,d=e=>{try{t.onEvent?.(e)}catch{return}},f=async()=>{let e=await t.loadCandidates(),a=i(e,n,l(),u);a.targets.length>0&&d({kind:`round-started`,total:a.targets.length});let o=a.targets.map(e=>({input:e,run:e=>t.fetchRepo(e.candidate)})),s=await r.schedule(o,{concurrency:n.concurrency,timeoutMs:n.fetchTimeoutMs,retries:0}),f=0,p=0;for(let e=0;e<s.length;e+=1){let r=a.targets[e];if(r===void 0)continue;let i=s[e];if(i===void 0)continue;let o=l();if(i.ok){f+=1,await t.onFetchStateChange(r.candidate,null),d({kind:`repo-fetched`,repoId:r.candidate.repoId,durationMs:i.durationMs});continue}p+=1;let u=i.error?.message??`未知错误`,m=c(r.candidate.fetchState,u,o,n.backoffStepsMs);await t.onFetchStateChange(r.candidate,m),d({kind:`repo-fetch-failed`,repoId:r.candidate.repoId,error:u,consecutiveFailures:m.consecutiveFailures,backoffUntil:m.backoffUntil})}let m=a.counts.skippedByBackoff+a.counts.skippedNoBranch+a.counts.notDue+a.counts.skippedOutOfTier+a.counts.deferredByBatchCap;d({kind:`round-finished`,done:f,failed:p,skipped:m})};return{start:()=>{a===null&&(f(),a=setInterval(()=>{f()},n.tickIntervalMs))},stop:()=>{a!==null&&(clearInterval(a),a=null)},tickNow:async()=>{if(!o){o=!0;try{await f()}finally{o=!1}}},isRunning:()=>a!==null}};export{o as DEFAULT_FETCH_SCHEDULER_OPTIONS,l as createFetchScheduler,n as effectiveTtlMs,r as isDue,i as planFetchBatch,s as resolveFetchSchedulerOptions,t as resolveFetchTier};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@longzai-intelligence-git/fetch-scheduler",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Longzai Intelligence Git 多仓拉取调度引擎",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fetch",
|
|
7
|
+
"git",
|
|
8
|
+
"longzai-intelligence",
|
|
9
|
+
"scheduler",
|
|
10
|
+
"tiering"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "lzi-builder",
|
|
26
|
+
"build:prod": "NODE_ENV=production bun run build",
|
|
27
|
+
"prepublishOnly": "bun run build:prod",
|
|
28
|
+
"typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
|
|
29
|
+
"typecheck:app": "lzi-tsgo typecheck tsconfig/app.json",
|
|
30
|
+
"typecheck:node": "lzi-tsgo typecheck tsconfig/node.json",
|
|
31
|
+
"typecheck:test": "lzi-tsgo typecheck tsconfig/test.json",
|
|
32
|
+
"lint": "oxlint && oxfmt --check",
|
|
33
|
+
"lint:fix": "oxlint --fix && oxfmt",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"test:watch": "bun test --watch",
|
|
36
|
+
"test:coverage": "bun test --coverage",
|
|
37
|
+
"clean": "lzi-dev-cli clean"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@longzai-intelligence-git/core": "0.1.7"
|
|
41
|
+
}
|
|
42
|
+
}
|