@withtruing/companion 0.13.3 → 0.14.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/README.md +24 -25
- package/dist/anthropic-output-schema.js +41 -0
- package/dist/api-key-caller.js +9 -2
- package/dist/claude-cli-caller.js +107 -8
- package/dist/cli.js +833 -206
- package/dist/codex-cli-caller.js +85 -14
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +107 -10
- package/dist/investigation-reader.js +12 -6
- package/dist/managed-repo.js +132 -0
- package/dist/observed-sources.js +132 -6
- package/dist/reader-result-contract.js +296 -2
- package/dist/repository-provider.js +608 -0
- package/dist/schema-entities.js +18 -18
- package/dist/service.js +7 -2
- package/dist/surface-mapper.js +53 -11
- package/dist/text-model.js +91 -8
- package/dist/whole-read/archive.js +196 -0
- package/dist/whole-read/checks.js +297 -0
- package/dist/whole-read/claims.js +196 -0
- package/dist/whole-read/coverage-ledger.js +51 -0
- package/dist/whole-read/decode.js +234 -0
- package/dist/whole-read/drafting.js +502 -0
- package/dist/whole-read/generated-rules.js +68 -0
- package/dist/whole-read/go-proof.js +255 -0
- package/dist/whole-read/grouping-prompt.js +19 -0
- package/dist/whole-read/grouping.js +247 -0
- package/dist/whole-read/identity.js +103 -0
- package/dist/whole-read/import-graph.js +293 -0
- package/dist/whole-read/inventory-collector.js +55 -0
- package/dist/whole-read/inventory-rules.js +141 -0
- package/dist/whole-read/lockfiles.js +947 -0
- package/dist/whole-read/notes.js +216 -0
- package/dist/whole-read/npm-proof.js +272 -0
- package/dist/whole-read/owner-decision-detectors.js +270 -0
- package/dist/whole-read/packing.js +612 -0
- package/dist/whole-read/pages.js +429 -0
- package/dist/whole-read/phase-policy.js +54 -0
- package/dist/whole-read/prd-build.js +279 -0
- package/dist/whole-read/prd-fidelity.js +14 -0
- package/dist/whole-read/python-proof.js +489 -0
- package/dist/whole-read/read-prompt.js +67 -0
- package/dist/whole-read/redaction.js +411 -0
- package/dist/whole-read/relationship-mapping.js +98 -0
- package/dist/whole-read/sanitize.js +266 -0
- package/dist/whole-read/secret-rules.js +26 -0
- package/dist/whole-read/structure.js +300 -0
- package/dist/whole-read/types.js +2 -0
- package/dist/whole-read-claims.js +12 -0
- package/dist/whole-read-git.js +340 -0
- package/dist/whole-read-pool.js +80 -0
- package/dist/whole-read-runner.js +945 -0
- package/dist/whole-read-shared-source.js +194 -0
- package/dist/whole-read-temp.js +479 -0
- package/package.json +8 -2
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import crossSpawn from 'cross-spawn';
|
|
4
|
+
const METADATA_LIMIT = 1 * 1024 * 1024;
|
|
5
|
+
const TREE_LIMIT = 256 * 1024 * 1024;
|
|
6
|
+
const BLOB_LIMIT = 256 * 1024 * 1024;
|
|
7
|
+
const COMMAND_TIMEOUT_MS = 120_000;
|
|
8
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
9
|
+
export class WholeReadGitError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code) {
|
|
12
|
+
super(`Whole-read Git operation failed: ${code}.`);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = 'WholeReadGitError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class ProcessFailure extends Error {
|
|
18
|
+
kind;
|
|
19
|
+
constructor(kind) {
|
|
20
|
+
super(kind);
|
|
21
|
+
this.kind = kind;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function runGit(repoPath, args, signal, outputLimit, input) {
|
|
25
|
+
if (signal.aborted)
|
|
26
|
+
return Promise.reject(new ProcessFailure('cancelled'));
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let child;
|
|
29
|
+
try {
|
|
30
|
+
child = crossSpawn('git', ['--no-replace-objects', ...args], {
|
|
31
|
+
cwd: repoPath,
|
|
32
|
+
env: {
|
|
33
|
+
...process.env,
|
|
34
|
+
GIT_NO_REPLACE_OBJECTS: '1',
|
|
35
|
+
GIT_NO_LAZY_FETCH: '1',
|
|
36
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
37
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
38
|
+
},
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
reject(new ProcessFailure('unavailable'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const chunks = [];
|
|
48
|
+
let byteCount = 0;
|
|
49
|
+
let settled = false;
|
|
50
|
+
let pendingFailure = null;
|
|
51
|
+
const finish = (error) => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return;
|
|
54
|
+
settled = true;
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
signal.removeEventListener('abort', abort);
|
|
57
|
+
if (error)
|
|
58
|
+
reject(error);
|
|
59
|
+
else
|
|
60
|
+
resolve({ bytes: Buffer.concat(chunks, byteCount) });
|
|
61
|
+
};
|
|
62
|
+
const stop = (kind) => {
|
|
63
|
+
if (pendingFailure || settled)
|
|
64
|
+
return;
|
|
65
|
+
pendingFailure = new ProcessFailure(kind);
|
|
66
|
+
child.kill('SIGKILL');
|
|
67
|
+
};
|
|
68
|
+
const abort = () => stop('cancelled');
|
|
69
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
70
|
+
child.stdout?.on('data', (chunk) => {
|
|
71
|
+
byteCount += chunk.length;
|
|
72
|
+
if (byteCount > outputLimit)
|
|
73
|
+
stop('resource');
|
|
74
|
+
else
|
|
75
|
+
chunks.push(chunk);
|
|
76
|
+
});
|
|
77
|
+
// Diagnostics are intentionally drained but never retained or exposed.
|
|
78
|
+
child.stderr?.on('data', () => undefined);
|
|
79
|
+
child.stdin?.on('error', () => undefined);
|
|
80
|
+
child.on('error', (error) => {
|
|
81
|
+
finish(new ProcessFailure(error.code === 'ENOENT' ? 'unavailable' : 'resource'));
|
|
82
|
+
});
|
|
83
|
+
child.on('close', (code, closeSignal) => {
|
|
84
|
+
if (pendingFailure)
|
|
85
|
+
finish(pendingFailure);
|
|
86
|
+
else if (closeSignal !== null || code !== 0)
|
|
87
|
+
finish(new ProcessFailure('exit'));
|
|
88
|
+
else
|
|
89
|
+
finish();
|
|
90
|
+
});
|
|
91
|
+
const timer = setTimeout(() => stop('resource'), COMMAND_TIMEOUT_MS);
|
|
92
|
+
if (input === undefined)
|
|
93
|
+
child.stdin?.end();
|
|
94
|
+
else
|
|
95
|
+
child.stdin?.end(input);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function mapProcessFailure(error, fallback) {
|
|
99
|
+
if (error instanceof WholeReadGitError)
|
|
100
|
+
throw error;
|
|
101
|
+
if (error instanceof ProcessFailure) {
|
|
102
|
+
if (error.kind === 'cancelled')
|
|
103
|
+
throw new WholeReadGitError('cancelled');
|
|
104
|
+
if (error.kind === 'unavailable')
|
|
105
|
+
throw new WholeReadGitError('git_unavailable');
|
|
106
|
+
if (error.kind === 'resource')
|
|
107
|
+
throw new WholeReadGitError('resource_limit');
|
|
108
|
+
}
|
|
109
|
+
throw new WholeReadGitError(fallback);
|
|
110
|
+
}
|
|
111
|
+
function exactAsciiLine(bytes) {
|
|
112
|
+
if (bytes.length < 2 || bytes[bytes.length - 1] !== 0x0a || bytes.includes(0x0d))
|
|
113
|
+
return null;
|
|
114
|
+
const line = bytes.subarray(0, -1);
|
|
115
|
+
if (line.includes(0x0a) || line.some((byte) => byte > 0x7f))
|
|
116
|
+
return null;
|
|
117
|
+
return line.toString('ascii');
|
|
118
|
+
}
|
|
119
|
+
async function repositoryObjectFormat(repoPath, signal) {
|
|
120
|
+
if (signal.aborted)
|
|
121
|
+
throw new WholeReadGitError('cancelled');
|
|
122
|
+
try {
|
|
123
|
+
if (!(await stat(repoPath)).isDirectory())
|
|
124
|
+
throw new Error('not a directory');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new WholeReadGitError('repository_unavailable');
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const output = await runGit(repoPath, ['rev-parse', '--show-object-format'], signal, METADATA_LIMIT);
|
|
131
|
+
const format = exactAsciiLine(output.bytes);
|
|
132
|
+
if (format === 'sha1' || format === 'sha256')
|
|
133
|
+
return format;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
mapProcessFailure(error, 'repository_unavailable');
|
|
137
|
+
}
|
|
138
|
+
throw new WholeReadGitError('repository_unavailable');
|
|
139
|
+
}
|
|
140
|
+
function validObjectId(value, format) {
|
|
141
|
+
return (format === 'sha1' ? /^[0-9a-f]{40}$/ : /^[0-9a-f]{64}$/).test(value);
|
|
142
|
+
}
|
|
143
|
+
function validLocalBranch(branch) {
|
|
144
|
+
if (!branch || Buffer.byteLength(branch, 'utf8') > 1_024 || branch.startsWith('-')
|
|
145
|
+
|| branch.startsWith('refs/') || branch === 'HEAD' || branch === '@'
|
|
146
|
+
|| branch.startsWith('/') || branch.endsWith('/') || branch.endsWith('.')
|
|
147
|
+
|| branch.includes('..') || branch.includes('//') || branch.includes('@{')
|
|
148
|
+
|| /[\x00-\x20\x7f~^:?*\\[]/.test(branch))
|
|
149
|
+
return false;
|
|
150
|
+
return branch.split('/').every((part) => part && !part.startsWith('.') && !part.endsWith('.lock'));
|
|
151
|
+
}
|
|
152
|
+
/** Resolve one local branch without consulting remotes or the current checkout. */
|
|
153
|
+
export async function resolveLocalBuildCommit(repoPath, branch, signal) {
|
|
154
|
+
if (signal.aborted)
|
|
155
|
+
throw new WholeReadGitError('cancelled');
|
|
156
|
+
if (!validLocalBranch(branch))
|
|
157
|
+
throw new WholeReadGitError('branch_unavailable');
|
|
158
|
+
const format = await repositoryObjectFormat(repoPath, signal);
|
|
159
|
+
try {
|
|
160
|
+
await runGit(repoPath, ['check-ref-format', '--branch', branch], signal, METADATA_LIMIT);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
mapProcessFailure(error, 'branch_unavailable');
|
|
164
|
+
}
|
|
165
|
+
let commit;
|
|
166
|
+
try {
|
|
167
|
+
const output = await runGit(repoPath, ['rev-parse', '--verify', `refs/heads/${branch}^{commit}`], signal, METADATA_LIMIT);
|
|
168
|
+
commit = exactAsciiLine(output.bytes) ?? '';
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
mapProcessFailure(error, 'branch_unavailable');
|
|
172
|
+
}
|
|
173
|
+
if (!validObjectId(commit, format))
|
|
174
|
+
throw new WholeReadGitError('branch_unavailable');
|
|
175
|
+
try {
|
|
176
|
+
const output = await runGit(repoPath, ['cat-file', '-t', commit], signal, METADATA_LIMIT);
|
|
177
|
+
if (exactAsciiLine(output.bytes) !== 'commit')
|
|
178
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
mapProcessFailure(error, 'snapshot_unavailable');
|
|
182
|
+
}
|
|
183
|
+
return commit;
|
|
184
|
+
}
|
|
185
|
+
function escapedPath(bytes) {
|
|
186
|
+
let validText = null;
|
|
187
|
+
try {
|
|
188
|
+
validText = utf8Decoder.decode(bytes);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Invalid bytes are shown individually below; base64 remains the identity.
|
|
192
|
+
}
|
|
193
|
+
if (validText !== null) {
|
|
194
|
+
let display = '';
|
|
195
|
+
for (const character of validText) {
|
|
196
|
+
const code = character.codePointAt(0);
|
|
197
|
+
if (character === '\\')
|
|
198
|
+
display += '\\\\';
|
|
199
|
+
else if (character === '\t')
|
|
200
|
+
display += '\\t';
|
|
201
|
+
else if (character === '\n')
|
|
202
|
+
display += '\\n';
|
|
203
|
+
else if (character === '\r')
|
|
204
|
+
display += '\\r';
|
|
205
|
+
else if (code < 0x20 || code === 0x7f)
|
|
206
|
+
display += `\\x${code.toString(16).padStart(2, '0')}`;
|
|
207
|
+
else
|
|
208
|
+
display += character;
|
|
209
|
+
}
|
|
210
|
+
return display;
|
|
211
|
+
}
|
|
212
|
+
let display = '';
|
|
213
|
+
for (const byte of bytes) {
|
|
214
|
+
if (byte === 0x5c)
|
|
215
|
+
display += '\\\\';
|
|
216
|
+
else if (byte === 0x09)
|
|
217
|
+
display += '\\t';
|
|
218
|
+
else if (byte === 0x0a)
|
|
219
|
+
display += '\\n';
|
|
220
|
+
else if (byte === 0x0d)
|
|
221
|
+
display += '\\r';
|
|
222
|
+
else if (byte >= 0x20 && byte <= 0x7e)
|
|
223
|
+
display += String.fromCharCode(byte);
|
|
224
|
+
else
|
|
225
|
+
display += `\\x${byte.toString(16).padStart(2, '0')}`;
|
|
226
|
+
}
|
|
227
|
+
return display;
|
|
228
|
+
}
|
|
229
|
+
function parseTree(bytes, format) {
|
|
230
|
+
if (bytes.length === 0)
|
|
231
|
+
return [];
|
|
232
|
+
if (bytes[bytes.length - 1] !== 0)
|
|
233
|
+
throw new WholeReadGitError('tree_invalid');
|
|
234
|
+
const entries = [];
|
|
235
|
+
const identities = new Set();
|
|
236
|
+
let offset = 0;
|
|
237
|
+
while (offset < bytes.length) {
|
|
238
|
+
const end = bytes.indexOf(0, offset);
|
|
239
|
+
if (end < 0)
|
|
240
|
+
throw new WholeReadGitError('tree_invalid');
|
|
241
|
+
const record = bytes.subarray(offset, end);
|
|
242
|
+
const tab = record.indexOf(0x09);
|
|
243
|
+
if (tab <= 0 || tab === record.length - 1)
|
|
244
|
+
throw new WholeReadGitError('tree_invalid');
|
|
245
|
+
const header = record.subarray(0, tab);
|
|
246
|
+
if (header.some((byte) => byte > 0x7f))
|
|
247
|
+
throw new WholeReadGitError('tree_invalid');
|
|
248
|
+
const match = /^(100644|100755|120000|160000) (blob|commit) ([0-9a-f]+)$/
|
|
249
|
+
.exec(header.toString('ascii'));
|
|
250
|
+
if (!match || !validObjectId(match[3], format))
|
|
251
|
+
throw new WholeReadGitError('tree_invalid');
|
|
252
|
+
const mode = match[1];
|
|
253
|
+
const objectType = match[2];
|
|
254
|
+
if ((mode === '160000') !== (objectType === 'commit'))
|
|
255
|
+
throw new WholeReadGitError('tree_invalid');
|
|
256
|
+
const path = record.subarray(tab + 1);
|
|
257
|
+
const pathBase64 = path.toString('base64');
|
|
258
|
+
if (identities.has(pathBase64))
|
|
259
|
+
throw new WholeReadGitError('tree_invalid');
|
|
260
|
+
identities.add(pathBase64);
|
|
261
|
+
entries.push({
|
|
262
|
+
pathBase64,
|
|
263
|
+
pathDisplay: escapedPath(path),
|
|
264
|
+
mode,
|
|
265
|
+
objectType,
|
|
266
|
+
blobId: match[3],
|
|
267
|
+
});
|
|
268
|
+
offset = end + 1;
|
|
269
|
+
}
|
|
270
|
+
return entries;
|
|
271
|
+
}
|
|
272
|
+
/** Enumerate only after the complete NUL-delimited tree output has been validated. */
|
|
273
|
+
export async function* enumerateBuildTree(repoPath, commit, signal) {
|
|
274
|
+
const format = await repositoryObjectFormat(repoPath, signal);
|
|
275
|
+
if (!validObjectId(commit, format))
|
|
276
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
277
|
+
try {
|
|
278
|
+
const type = await runGit(repoPath, ['cat-file', '-t', commit], signal, METADATA_LIMIT);
|
|
279
|
+
if (exactAsciiLine(type.bytes) !== 'commit')
|
|
280
|
+
throw new WholeReadGitError('snapshot_unavailable');
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
mapProcessFailure(error, 'snapshot_unavailable');
|
|
284
|
+
}
|
|
285
|
+
let output;
|
|
286
|
+
try {
|
|
287
|
+
output = await runGit(repoPath, ['ls-tree', '-r', '-z', '--full-tree', commit], signal, TREE_LIMIT);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
mapProcessFailure(error, 'tree_invalid');
|
|
291
|
+
}
|
|
292
|
+
const entries = parseTree(output.bytes, format);
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
if (signal.aborted)
|
|
295
|
+
throw new WholeReadGitError('cancelled');
|
|
296
|
+
yield entry;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function parseBlobResponse(response, expectedId, format) {
|
|
300
|
+
const headerEnd = response.indexOf(0x0a);
|
|
301
|
+
if (headerEnd < 0)
|
|
302
|
+
throw new WholeReadGitError('blob_invalid');
|
|
303
|
+
const header = response.subarray(0, headerEnd);
|
|
304
|
+
if (header.includes(0x0d) || header.some((byte) => byte > 0x7f)) {
|
|
305
|
+
throw new WholeReadGitError('blob_invalid');
|
|
306
|
+
}
|
|
307
|
+
const match = /^([0-9a-f]+) ([^ ]+) (0|[1-9][0-9]*)$/.exec(header.toString('ascii'));
|
|
308
|
+
if (!match || match[1] !== expectedId || match[2] !== 'blob'
|
|
309
|
+
|| !validObjectId(match[1], format))
|
|
310
|
+
throw new WholeReadGitError('blob_invalid');
|
|
311
|
+
const size = Number(match[3]);
|
|
312
|
+
if (!Number.isSafeInteger(size) || size > BLOB_LIMIT)
|
|
313
|
+
throw new WholeReadGitError('resource_limit');
|
|
314
|
+
const bodyStart = headerEnd + 1;
|
|
315
|
+
const bodyEnd = bodyStart + size;
|
|
316
|
+
if (bodyEnd + 1 !== response.length || response[bodyEnd] !== 0x0a) {
|
|
317
|
+
throw new WholeReadGitError('blob_invalid');
|
|
318
|
+
}
|
|
319
|
+
const body = response.subarray(bodyStart, bodyEnd);
|
|
320
|
+
const actualId = createHash(format)
|
|
321
|
+
.update(Buffer.from(`blob ${size}\0`, 'ascii'))
|
|
322
|
+
.update(body)
|
|
323
|
+
.digest('hex');
|
|
324
|
+
if (actualId !== expectedId)
|
|
325
|
+
throw new WholeReadGitError('blob_hash_mismatch');
|
|
326
|
+
return Buffer.from(body);
|
|
327
|
+
}
|
|
328
|
+
/** Read and verify one canonical blob through Git's framed batch protocol. */
|
|
329
|
+
export async function readBuildBlob(repoPath, blobId, signal) {
|
|
330
|
+
const format = await repositoryObjectFormat(repoPath, signal);
|
|
331
|
+
if (!validObjectId(blobId, format))
|
|
332
|
+
throw new WholeReadGitError('blob_invalid');
|
|
333
|
+
try {
|
|
334
|
+
const output = await runGit(repoPath, ['cat-file', '--batch'], signal, BLOB_LIMIT + METADATA_LIMIT, Buffer.from(`${blobId}\n`, 'ascii'));
|
|
335
|
+
return parseBlobResponse(output.bytes, blobId, format);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
mapProcessFailure(error, 'blob_invalid');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const DEFAULT_PARALLEL_STEPS = 3;
|
|
2
|
+
export function wholeReadParallelSteps(value = process.env.TRUING_PARALLEL_STEPS) {
|
|
3
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
4
|
+
return DEFAULT_PARALLEL_STEPS;
|
|
5
|
+
const parsed = typeof value === 'number' ? value : Number(value);
|
|
6
|
+
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 8 ? parsed : DEFAULT_PARALLEL_STEPS;
|
|
7
|
+
}
|
|
8
|
+
export function createWholeReadAssignmentPool(dependencies) {
|
|
9
|
+
const concurrency = wholeReadParallelSteps(dependencies.concurrency);
|
|
10
|
+
const states = new Map();
|
|
11
|
+
let stopping = false;
|
|
12
|
+
const retire = (state) => {
|
|
13
|
+
if (state.revoked)
|
|
14
|
+
return;
|
|
15
|
+
state.revoked = true;
|
|
16
|
+
void Promise.resolve(dependencies.onRevoke?.(state.assignment)).catch((error) => {
|
|
17
|
+
dependencies.onError?.(error, state.assignment);
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const start = (state) => {
|
|
21
|
+
dependencies.onStart?.(state.assignment);
|
|
22
|
+
let task;
|
|
23
|
+
task = dependencies.run(state.assignment)
|
|
24
|
+
.then((result) => {
|
|
25
|
+
if (result.status === 'build_concurrency_limit')
|
|
26
|
+
state.limitedPolls = 1;
|
|
27
|
+
else if (result.status === 'claimed')
|
|
28
|
+
state.limitedPolls = 0;
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => { dependencies.onError?.(error, state.assignment); })
|
|
31
|
+
.finally(() => {
|
|
32
|
+
state.active.delete(task);
|
|
33
|
+
if (state.revoked && state.active.size === 0)
|
|
34
|
+
states.delete(dependencies.key(state.assignment));
|
|
35
|
+
});
|
|
36
|
+
state.active.add(task);
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
poll(assignments) {
|
|
40
|
+
if (stopping || dependencies.shouldStop())
|
|
41
|
+
return 0;
|
|
42
|
+
const visible = new Set(assignments.map(dependencies.key));
|
|
43
|
+
for (const [key, state] of states)
|
|
44
|
+
if (!visible.has(key))
|
|
45
|
+
retire(state);
|
|
46
|
+
if (!dependencies.mayClaim())
|
|
47
|
+
return 0;
|
|
48
|
+
let started = 0;
|
|
49
|
+
for (const assignment of assignments) {
|
|
50
|
+
if (stopping || dependencies.shouldStop() || !dependencies.mayClaim())
|
|
51
|
+
break;
|
|
52
|
+
const key = dependencies.key(assignment);
|
|
53
|
+
let state = states.get(key);
|
|
54
|
+
if (!state || state.revoked) {
|
|
55
|
+
state = { assignment, active: new Set(), revoked: false, limitedPolls: 0 };
|
|
56
|
+
states.set(key, state);
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
state.assignment = assignment;
|
|
60
|
+
if (state.limitedPolls > 0) {
|
|
61
|
+
state.limitedPolls--;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
while (state.active.size < concurrency && !dependencies.shouldStop() && dependencies.mayClaim()) {
|
|
65
|
+
start(state);
|
|
66
|
+
started++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return started;
|
|
70
|
+
},
|
|
71
|
+
inFlight(key) { return states.get(key)?.active.size ?? 0; },
|
|
72
|
+
async shutdown() {
|
|
73
|
+
stopping = true;
|
|
74
|
+
for (const state of states.values())
|
|
75
|
+
retire(state);
|
|
76
|
+
await Promise.allSettled([...states.values()].flatMap((state) => [...state.active]));
|
|
77
|
+
states.clear();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|