@deepseek-ai/dsh-experimental-forge 0.0.0-stage → 0.2.2-alpha.3
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/LICENSE +21 -0
- package/README.i18n.yaml +37 -0
- package/README.md +103 -2
- package/README.zh.md +104 -0
- package/lib/index.js +537 -0
- package/lib/types/index.d.ts +185 -0
- package/lib/types/keys.d.ts +10 -0
- package/lib/types/schedule.d.ts +22 -0
- package/lib/types/types.d.ts +226 -0
- package/locale/de.json +6 -0
- package/locale/en.json +6 -0
- package/locale/es.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/pt.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +42 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { MAX_TIMER_DELAY_MS, deadline } from "@deepseek-ai/dsh-timeout";
|
|
4
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
5
|
+
//#region lib/types/keys.js
|
|
6
|
+
/** Canonical identities for change requests. */
|
|
7
|
+
/**
|
|
8
|
+
* Build the canonical identity of a change request. Host, owner, and name
|
|
9
|
+
* compare case-insensitively, as they do on the hosts this vocabulary serves.
|
|
10
|
+
* @param ref - change request to identify.
|
|
11
|
+
* @returns `<forge>:<host>/<owner>/<name>#<number>` in lowercase.
|
|
12
|
+
*/
|
|
13
|
+
function changeRequestKey(ref) {
|
|
14
|
+
const { forge, host, owner, name } = ref.repository;
|
|
15
|
+
return brandString(`${forge}:${host}/${owner}/${name}#${ref.number}`.toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region lib/types/schedule.js
|
|
19
|
+
/** Poll cadence for watched change requests. */
|
|
20
|
+
/**
|
|
21
|
+
* Choose the delay before the next poll of one change request. Failures back
|
|
22
|
+
* off exponentially from the active interval; merged change requests stop
|
|
23
|
+
* polling, while closed ones use the idle cadence to detect reopening.
|
|
24
|
+
* @param result - latest snapshot or failure.
|
|
25
|
+
* @param failures - consecutive failures, counting `result` when it is one.
|
|
26
|
+
* @param intervals - configured cadence.
|
|
27
|
+
* @returns the delay in milliseconds, or undefined after a terminal state.
|
|
28
|
+
*/
|
|
29
|
+
function nextPollDelay(result, failures, intervals) {
|
|
30
|
+
if ("code" in result) return Math.min(intervals.maxBackoffMs, intervals.activeIntervalMs * 2 ** failures);
|
|
31
|
+
if (result.state === "merged") return void 0;
|
|
32
|
+
if (result.state === "closed") return intervals.idleIntervalMs;
|
|
33
|
+
return result.checksRollup === "pending" || result.mergeability === "unknown" ? intervals.activeIntervalMs : intervals.idleIntervalMs;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/types/index.js
|
|
37
|
+
/** Registry of code-host providers with provider-neutral change requests and shared status polling. */
|
|
38
|
+
var __addDisposableResource = function(env, value, async) {
|
|
39
|
+
if (value !== null && value !== void 0) {
|
|
40
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
41
|
+
var dispose, inner;
|
|
42
|
+
if (async) {
|
|
43
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
44
|
+
dispose = value[Symbol.asyncDispose];
|
|
45
|
+
}
|
|
46
|
+
if (dispose === void 0) {
|
|
47
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
48
|
+
dispose = value[Symbol.dispose];
|
|
49
|
+
if (async) inner = dispose;
|
|
50
|
+
}
|
|
51
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
52
|
+
if (inner) dispose = function() {
|
|
53
|
+
try {
|
|
54
|
+
inner.call(this);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return Promise.reject(e);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
env.stack.push({
|
|
60
|
+
value,
|
|
61
|
+
dispose,
|
|
62
|
+
async
|
|
63
|
+
});
|
|
64
|
+
} else if (async) env.stack.push({ async: true });
|
|
65
|
+
return value;
|
|
66
|
+
};
|
|
67
|
+
var __disposeResources = (function(SuppressedError) {
|
|
68
|
+
return function(env) {
|
|
69
|
+
function fail(e) {
|
|
70
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
71
|
+
env.hasError = true;
|
|
72
|
+
}
|
|
73
|
+
var r, s = 0;
|
|
74
|
+
function next() {
|
|
75
|
+
while (r = env.stack.pop()) try {
|
|
76
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
77
|
+
if (r.dispose) {
|
|
78
|
+
var result = r.dispose.call(r.value);
|
|
79
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
|
|
80
|
+
fail(e);
|
|
81
|
+
return next();
|
|
82
|
+
});
|
|
83
|
+
} else s |= 1;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
fail(e);
|
|
86
|
+
}
|
|
87
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
88
|
+
if (env.hasError) throw env.error;
|
|
89
|
+
}
|
|
90
|
+
return next();
|
|
91
|
+
};
|
|
92
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
|
93
|
+
var e = new Error(message);
|
|
94
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Registry of forge providers. Watchers of one change request share a single
|
|
98
|
+
* polling loop; each forge has at most one fetch in flight, which batches
|
|
99
|
+
* every due change request of that forge.
|
|
100
|
+
*/
|
|
101
|
+
var ForgeService = class extends Service {
|
|
102
|
+
static Config = z.object({
|
|
103
|
+
activeIntervalMs: z.number().default(3e4),
|
|
104
|
+
idleIntervalMs: z.number().default(12e4),
|
|
105
|
+
maxBackoffMs: z.number().default(6e5),
|
|
106
|
+
fetchTimeoutMs: z.number().default(3e4)
|
|
107
|
+
});
|
|
108
|
+
config;
|
|
109
|
+
providers = /* @__PURE__ */ new Map();
|
|
110
|
+
watches = /* @__PURE__ */ new Map();
|
|
111
|
+
fetching = /* @__PURE__ */ new Set();
|
|
112
|
+
pending = /* @__PURE__ */ new Set();
|
|
113
|
+
lifetime = new AbortController();
|
|
114
|
+
timer;
|
|
115
|
+
/**
|
|
116
|
+
* Poll what is due. A field rather than a method, so the timer runs it on
|
|
117
|
+
* the instance itself: a polling round started through a caller's `ctx.forge`
|
|
118
|
+
* would otherwise run on that caller's shadow receiver, and every later round
|
|
119
|
+
* would nest one more shadow until a property write overflows the stack.
|
|
120
|
+
*/
|
|
121
|
+
tick = () => {
|
|
122
|
+
this.timer = void 0;
|
|
123
|
+
this.poll();
|
|
124
|
+
};
|
|
125
|
+
constructor(ctx, config) {
|
|
126
|
+
super(ctx, "forge");
|
|
127
|
+
this.config = config;
|
|
128
|
+
for (const key of [
|
|
129
|
+
"activeIntervalMs",
|
|
130
|
+
"idleIntervalMs",
|
|
131
|
+
"maxBackoffMs",
|
|
132
|
+
"fetchTimeoutMs"
|
|
133
|
+
]) {
|
|
134
|
+
const value = this.config[key];
|
|
135
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TIMER_DELAY_MS) throw new Error(`forge: ${key} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
136
|
+
}
|
|
137
|
+
if (this.config.maxBackoffMs < this.config.activeIntervalMs) throw new Error("forge: maxBackoffMs must be greater than or equal to activeIntervalMs");
|
|
138
|
+
ctx.effect(() => async () => {
|
|
139
|
+
this.lifetime.abort(/* @__PURE__ */ new Error("forge service disposed"));
|
|
140
|
+
clearTimeout(this.timer);
|
|
141
|
+
this.timer = void 0;
|
|
142
|
+
await Promise.allSettled(this.pending);
|
|
143
|
+
}, "forge: stop polling");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Register the provider of one forge family as an effect on the calling
|
|
147
|
+
* fiber. Watches of that forge receive an `unavailable` failure when the
|
|
148
|
+
* registration ends. Unregistering cancels and awaits its in-flight calls;
|
|
149
|
+
* results from the withdrawn registration are discarded.
|
|
150
|
+
* @param provider - provider to serve `provider.forge`.
|
|
151
|
+
* @returns the disposer that unregisters the provider and waits for its calls to finish.
|
|
152
|
+
* @throws when another provider already serves the same forge.
|
|
153
|
+
*/
|
|
154
|
+
register(provider) {
|
|
155
|
+
const dispose = this.ctx.effect(() => {
|
|
156
|
+
if (this.providers.has(provider.forge)) throw new Error(`forge: a provider for "${provider.forge}" is already registered`);
|
|
157
|
+
const registration = {
|
|
158
|
+
provider,
|
|
159
|
+
lifetime: new AbortController(),
|
|
160
|
+
pending: /* @__PURE__ */ new Set()
|
|
161
|
+
};
|
|
162
|
+
this.providers.set(provider.forge, registration);
|
|
163
|
+
for (const watch of this.watches.values()) if (watch.ref.repository.forge === provider.forge) watch.dueAt = Date.now();
|
|
164
|
+
this.providersChanged(provider.forge);
|
|
165
|
+
this.schedule();
|
|
166
|
+
return async () => {
|
|
167
|
+
this.providers.delete(provider.forge);
|
|
168
|
+
registration.lifetime.abort(/* @__PURE__ */ new Error(`forge: provider "${provider.forge}" was unregistered`));
|
|
169
|
+
for (const watch of [...this.watches.values()]) if (watch.ref.repository.forge === provider.forge) this.settle(watch, this.missing(provider.forge), void 0);
|
|
170
|
+
this.providersChanged(provider.forge);
|
|
171
|
+
this.schedule();
|
|
172
|
+
await Promise.allSettled(registration.pending);
|
|
173
|
+
};
|
|
174
|
+
}, `forge.register(${provider.forge})`);
|
|
175
|
+
return async () => {
|
|
176
|
+
await dispose();
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Map a Git remote URL to a repository through the first provider that serves it.
|
|
181
|
+
* @param url - fetch or push URL as Git reports it.
|
|
182
|
+
* @returns the repository, or undefined when no provider serves the URL.
|
|
183
|
+
*/
|
|
184
|
+
repositoryFromRemote(url) {
|
|
185
|
+
for (const { provider } of this.providers.values()) {
|
|
186
|
+
const repository = provider.repositoryFromRemote(url);
|
|
187
|
+
if (repository !== void 0) return repository;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Map a change request web URL through the first provider that serves it.
|
|
192
|
+
* @param url - web URL of a pull or merge request.
|
|
193
|
+
* @returns the change request, or undefined when no provider serves the URL.
|
|
194
|
+
*/
|
|
195
|
+
changeRequestFromUrl(url) {
|
|
196
|
+
for (const { provider } of this.providers.values()) {
|
|
197
|
+
const ref = provider.changeRequestFromUrl(url);
|
|
198
|
+
if (ref !== void 0) return ref;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Build the canonical web URL of a change request.
|
|
203
|
+
* @param ref - change request served by a registered provider.
|
|
204
|
+
* @returns the provider's canonical URL.
|
|
205
|
+
* @throws when no provider serves the change request's forge.
|
|
206
|
+
*/
|
|
207
|
+
changeRequestUrl(ref) {
|
|
208
|
+
return this.provider(ref.repository.forge).changeRequestUrl(ref);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Find open change requests whose head is `branch`, excluding the default branch and forks.
|
|
212
|
+
* @param repository - repository served by a registered provider.
|
|
213
|
+
* @param branch - head branch name on the host.
|
|
214
|
+
* @param signal - cancellation for the lookup.
|
|
215
|
+
* @returns matching change requests, newest first as the provider reports them.
|
|
216
|
+
* @throws when no provider serves the repository's forge or the lookup fails.
|
|
217
|
+
*/
|
|
218
|
+
async findByHead(repository, branch, signal) {
|
|
219
|
+
return await this.invoke(this.registration(repository.forge), signal, (provider, operation) => provider.findByHead(repository, branch, operation));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Report whether the provider can reach the repository's host with current credentials.
|
|
223
|
+
* @param repository - repository served by a registered provider.
|
|
224
|
+
* @param signal - cancellation for the probe.
|
|
225
|
+
* @returns the provider's readiness for the host.
|
|
226
|
+
* @throws when no provider serves the repository's forge.
|
|
227
|
+
*/
|
|
228
|
+
async readiness(repository, signal) {
|
|
229
|
+
return await this.invoke(this.registration(repository.forge), signal, (provider, operation) => provider.readiness(repository.host, operation));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Mark a draft change request ready for review, then poll it at once when it is watched.
|
|
233
|
+
* @param ref - change request served by a registered provider.
|
|
234
|
+
* @param signal - cancellation of the request.
|
|
235
|
+
* @returns undefined once the host accepted the change, or why it did not.
|
|
236
|
+
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
|
237
|
+
*/
|
|
238
|
+
async markReady(ref, signal) {
|
|
239
|
+
const failure = await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.markReady(ref, operation));
|
|
240
|
+
this.refresh(ref);
|
|
241
|
+
return failure;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Merge a change request while its head is still `request.headSha`, then
|
|
245
|
+
* poll it at once when it is watched.
|
|
246
|
+
* @param ref - change request served by a registered provider.
|
|
247
|
+
* @param request - merge method and the head commit the user confirmed.
|
|
248
|
+
* @param signal - cancellation of the request.
|
|
249
|
+
* @returns undefined once the host merged it, or why it did not.
|
|
250
|
+
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
|
251
|
+
*/
|
|
252
|
+
async merge(ref, request, signal) {
|
|
253
|
+
const failure = await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.merge(ref, request, operation));
|
|
254
|
+
this.refresh(ref);
|
|
255
|
+
return failure;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Read complete check details independently of periodic polling.
|
|
259
|
+
* @param ref - change request served by a registered provider.
|
|
260
|
+
* @param signal - cancellation of the detail view.
|
|
261
|
+
* @returns a complete snapshot or the provider's failure; unsupported providers return unavailable.
|
|
262
|
+
* @throws on cancellation, provider disposal, or expiry of this read's own `fetchTimeoutMs` deadline.
|
|
263
|
+
*/
|
|
264
|
+
async fetchChecks(ref, signal) {
|
|
265
|
+
const env_1 = {
|
|
266
|
+
stack: [],
|
|
267
|
+
error: void 0,
|
|
268
|
+
hasError: false
|
|
269
|
+
};
|
|
270
|
+
try {
|
|
271
|
+
const timeout = __addDisposableResource(env_1, deadline(signal, this.config.fetchTimeoutMs, "FORGE_FETCH_TIMEOUT"), false);
|
|
272
|
+
return await this.invoke(this.registration(ref.repository.forge), timeout.signal, (provider, operation) => provider.fetchChecks?.(ref, operation) ?? Promise.resolve({
|
|
273
|
+
code: "unavailable",
|
|
274
|
+
message: "This provider does not support complete check reads"
|
|
275
|
+
}));
|
|
276
|
+
} catch (e_1) {
|
|
277
|
+
env_1.error = e_1;
|
|
278
|
+
env_1.hasError = true;
|
|
279
|
+
} finally {
|
|
280
|
+
__disposeResources(env_1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Read the changes of a change request as its host computes them.
|
|
285
|
+
* @param ref - change request served by a registered provider.
|
|
286
|
+
* @param signal - cancellation of the read.
|
|
287
|
+
* @returns one unified patch with `a/` and `b/` path prefixes, or why the host did not provide it.
|
|
288
|
+
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
|
289
|
+
*/
|
|
290
|
+
async diff(ref, signal) {
|
|
291
|
+
return await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.diff(ref, operation));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Publish one comment or a completed review, then refresh the watched status.
|
|
295
|
+
* @param ref - change request served by a registered provider.
|
|
296
|
+
* @param request - explicitly selected commit, comments, and review decision.
|
|
297
|
+
* @param signal - request cancellation.
|
|
298
|
+
* @returns a publication URL or a failure, including an unknown write outcome.
|
|
299
|
+
*/
|
|
300
|
+
async submitReview(ref, request, signal) {
|
|
301
|
+
const result = await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.submitReview?.(ref, request, operation) ?? Promise.resolve({
|
|
302
|
+
ok: false,
|
|
303
|
+
code: "unsupported",
|
|
304
|
+
message: "This provider does not support review submission"
|
|
305
|
+
}));
|
|
306
|
+
this.refresh(ref);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Read the text of one file at a commit of a change request's repository.
|
|
311
|
+
* @param ref - change request served by a registered provider.
|
|
312
|
+
* @param commit - commit object name.
|
|
313
|
+
* @param path - path relative to the repository root.
|
|
314
|
+
* @param signal - cancellation of the read.
|
|
315
|
+
* @returns the text, or why the host did not provide it.
|
|
316
|
+
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
|
|
317
|
+
*/
|
|
318
|
+
async fileText(ref, commit, path, signal) {
|
|
319
|
+
return await this.invoke(this.registration(ref.repository.forge), signal, (provider, operation) => provider.fileText(ref, commit, path, operation));
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Build the web page of one file at a commit of a change request's repository.
|
|
323
|
+
* @param ref - change request served by a registered provider.
|
|
324
|
+
* @param commit - commit object name.
|
|
325
|
+
* @param path - path relative to the repository root.
|
|
326
|
+
* @returns the provider's URL.
|
|
327
|
+
* @throws when no provider serves the change request's forge.
|
|
328
|
+
*/
|
|
329
|
+
fileUrl(ref, commit, path) {
|
|
330
|
+
return this.provider(ref.repository.forge).fileUrl(ref, commit, path);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Watch one change request as an effect on the calling fiber. The first
|
|
334
|
+
* watcher starts polling at once; listeners receive every later result,
|
|
335
|
+
* and `latest` returns the current one. Polling stops when the last watcher
|
|
336
|
+
* leaves. Listeners must not throw; a thrown error is logged and skipped.
|
|
337
|
+
* @param ref - change request to poll.
|
|
338
|
+
* @param listener - receives each polling result.
|
|
339
|
+
* @returns the disposer that stops this watch.
|
|
340
|
+
*/
|
|
341
|
+
watch(ref, listener) {
|
|
342
|
+
const key = changeRequestKey(ref);
|
|
343
|
+
const dispose = this.ctx.effect(() => {
|
|
344
|
+
let watch = this.watches.get(key);
|
|
345
|
+
if (watch === void 0) {
|
|
346
|
+
watch = {
|
|
347
|
+
key,
|
|
348
|
+
ref,
|
|
349
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
350
|
+
latest: void 0,
|
|
351
|
+
dueAt: Date.now(),
|
|
352
|
+
failures: 0
|
|
353
|
+
};
|
|
354
|
+
this.watches.set(key, watch);
|
|
355
|
+
this.schedule();
|
|
356
|
+
}
|
|
357
|
+
const owned = watch;
|
|
358
|
+
owned.listeners.add(listener);
|
|
359
|
+
return () => {
|
|
360
|
+
owned.listeners.delete(listener);
|
|
361
|
+
if (owned.listeners.size === 0 && this.watches.get(key) === owned) {
|
|
362
|
+
this.watches.delete(key);
|
|
363
|
+
this.schedule();
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}, "forge.watch()");
|
|
367
|
+
return () => void dispose();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Poll a watched change request at once, including after a terminal state.
|
|
371
|
+
* A fetch already in flight is followed immediately by another fetch; an
|
|
372
|
+
* unwatched change request is unchanged.
|
|
373
|
+
* @param ref - change request to poll.
|
|
374
|
+
*/
|
|
375
|
+
refresh(ref) {
|
|
376
|
+
const watch = this.watches.get(changeRequestKey(ref));
|
|
377
|
+
if (watch === void 0) return;
|
|
378
|
+
watch.dueAt = Date.now();
|
|
379
|
+
this.schedule();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Read the latest polling result of a watched change request.
|
|
383
|
+
* @param ref - change request to read.
|
|
384
|
+
* @returns the latest result, or undefined before the first result or without a watch.
|
|
385
|
+
*/
|
|
386
|
+
latest(ref) {
|
|
387
|
+
return this.watches.get(changeRequestKey(ref))?.latest;
|
|
388
|
+
}
|
|
389
|
+
provider(forge) {
|
|
390
|
+
return this.registration(forge).provider;
|
|
391
|
+
}
|
|
392
|
+
providersChanged(forge) {
|
|
393
|
+
if (this.lifetime.signal.aborted) return;
|
|
394
|
+
const task = this.ctx.parallel("forge/providers-changed", forge).catch((error) => {
|
|
395
|
+
this.ctx.logger.warn(`forge: provider change listener failed: ${String(error)}`);
|
|
396
|
+
});
|
|
397
|
+
this.pending.add(task);
|
|
398
|
+
task.then(() => this.pending.delete(task));
|
|
399
|
+
}
|
|
400
|
+
registration(forge) {
|
|
401
|
+
const registration = this.providers.get(forge);
|
|
402
|
+
if (registration === void 0) throw new Error(`forge: ${this.missing(forge).message}`);
|
|
403
|
+
return registration;
|
|
404
|
+
}
|
|
405
|
+
async invoke(registration, signal, action) {
|
|
406
|
+
const operation = AbortSignal.any([
|
|
407
|
+
signal,
|
|
408
|
+
this.lifetime.signal,
|
|
409
|
+
registration.lifetime.signal
|
|
410
|
+
]);
|
|
411
|
+
operation.throwIfAborted();
|
|
412
|
+
const completion = Promise.withResolvers();
|
|
413
|
+
registration.pending.add(completion.promise);
|
|
414
|
+
this.pending.add(completion.promise);
|
|
415
|
+
try {
|
|
416
|
+
const result = await action(registration.provider, operation);
|
|
417
|
+
operation.throwIfAborted();
|
|
418
|
+
return result;
|
|
419
|
+
} finally {
|
|
420
|
+
registration.pending.delete(completion.promise);
|
|
421
|
+
this.pending.delete(completion.promise);
|
|
422
|
+
completion.resolve();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
missing(forge) {
|
|
426
|
+
return {
|
|
427
|
+
code: "unavailable",
|
|
428
|
+
message: `no provider is registered for "${forge}"`
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
schedule() {
|
|
432
|
+
clearTimeout(this.timer);
|
|
433
|
+
this.timer = void 0;
|
|
434
|
+
if (this.lifetime.signal.aborted) return;
|
|
435
|
+
let next;
|
|
436
|
+
for (const watch of this.watches.values()) {
|
|
437
|
+
if (watch.dueAt === void 0 || this.fetching.has(watch.ref.repository.forge)) continue;
|
|
438
|
+
next = next === void 0 ? watch.dueAt : Math.min(next, watch.dueAt);
|
|
439
|
+
}
|
|
440
|
+
if (next === void 0) return;
|
|
441
|
+
this.timer = setTimeout(this.tick, Math.max(0, next - Date.now()));
|
|
442
|
+
}
|
|
443
|
+
poll() {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const due = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const watch of this.watches.values()) {
|
|
447
|
+
const forge = watch.ref.repository.forge;
|
|
448
|
+
if (watch.dueAt === void 0 || watch.dueAt > now || this.fetching.has(forge)) continue;
|
|
449
|
+
const batch = due.get(forge);
|
|
450
|
+
if (batch === void 0) due.set(forge, [watch]);
|
|
451
|
+
else batch.push(watch);
|
|
452
|
+
}
|
|
453
|
+
for (const [forge, batch] of due) {
|
|
454
|
+
const task = this.fetch(forge, batch);
|
|
455
|
+
this.pending.add(task);
|
|
456
|
+
task.then(() => this.pending.delete(task));
|
|
457
|
+
}
|
|
458
|
+
this.schedule();
|
|
459
|
+
}
|
|
460
|
+
/** Fetch one forge's batch; never rejects. */
|
|
461
|
+
async fetch(forge, batch) {
|
|
462
|
+
this.fetching.add(forge);
|
|
463
|
+
for (const watch of batch) watch.dueAt = void 0;
|
|
464
|
+
let results;
|
|
465
|
+
let failure;
|
|
466
|
+
const registration = this.providers.get(forge);
|
|
467
|
+
try {
|
|
468
|
+
if (registration === void 0) failure = this.missing(forge);
|
|
469
|
+
else {
|
|
470
|
+
const env_2 = {
|
|
471
|
+
stack: [],
|
|
472
|
+
error: void 0,
|
|
473
|
+
hasError: false
|
|
474
|
+
};
|
|
475
|
+
try {
|
|
476
|
+
const timeout = __addDisposableResource(env_2, deadline(this.lifetime.signal, this.config.fetchTimeoutMs, "FORGE_FETCH_TIMEOUT"), false);
|
|
477
|
+
results = await this.invoke(registration, timeout.signal, (provider, operation) => provider.fetch(batch.map((watch) => watch.ref), operation));
|
|
478
|
+
} catch (e_2) {
|
|
479
|
+
env_2.error = e_2;
|
|
480
|
+
env_2.hasError = true;
|
|
481
|
+
} finally {
|
|
482
|
+
__disposeResources(env_2);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
failure = {
|
|
487
|
+
code: "unavailable",
|
|
488
|
+
message: error instanceof Error ? error.message : String(error)
|
|
489
|
+
};
|
|
490
|
+
} finally {
|
|
491
|
+
this.fetching.delete(forge);
|
|
492
|
+
}
|
|
493
|
+
if (this.lifetime.signal.aborted) return;
|
|
494
|
+
if (registration !== this.providers.get(forge)) {
|
|
495
|
+
this.schedule();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
for (const watch of batch) {
|
|
499
|
+
if (this.watches.get(watch.key) !== watch) continue;
|
|
500
|
+
const result = failure ?? results?.get(watch.key) ?? {
|
|
501
|
+
code: "unavailable",
|
|
502
|
+
message: `provider returned no result for ${watch.key}`
|
|
503
|
+
};
|
|
504
|
+
this.settle(watch, result, watch.dueAt);
|
|
505
|
+
}
|
|
506
|
+
this.schedule();
|
|
507
|
+
}
|
|
508
|
+
settle(watch, result, refreshAt) {
|
|
509
|
+
const fetchedAt = Date.now();
|
|
510
|
+
let update;
|
|
511
|
+
if ("code" in result) {
|
|
512
|
+
watch.failures++;
|
|
513
|
+
update = {
|
|
514
|
+
key: watch.key,
|
|
515
|
+
fetchedAt,
|
|
516
|
+
failure: result
|
|
517
|
+
};
|
|
518
|
+
} else {
|
|
519
|
+
watch.failures = 0;
|
|
520
|
+
update = {
|
|
521
|
+
key: watch.key,
|
|
522
|
+
fetchedAt,
|
|
523
|
+
snapshot: result
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
watch.latest = update;
|
|
527
|
+
const delay = nextPollDelay(result, watch.failures, this.config);
|
|
528
|
+
watch.dueAt = refreshAt ?? (delay === void 0 ? void 0 : fetchedAt + delay);
|
|
529
|
+
for (const listener of [...watch.listeners]) try {
|
|
530
|
+
listener(update);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.ctx.logger.warn(`forge: change request listener failed: ${String(error)}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
export { ForgeService, ForgeService as default, changeRequestKey };
|