@stage5/lumine 0.2.65 → 0.2.67
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 +22 -3
- package/lib/admin-runtime-logs.js +786 -0
- package/lib/admin-workflows.js +350 -59
- package/lib/admin.js +586 -47
- package/lib/commands.js +46 -5
- package/lib/sponsor-duty.js +479 -28
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +11 -7
- package/sdk/LUMINE_ADMIN.md +358 -88
package/lib/admin-workflows.js
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
chmodSync,
|
|
4
4
|
closeSync,
|
|
5
5
|
createReadStream,
|
|
6
|
+
fchmodSync,
|
|
6
7
|
fsyncSync,
|
|
8
|
+
linkSync,
|
|
7
9
|
lstatSync,
|
|
8
10
|
mkdirSync,
|
|
9
11
|
openSync,
|
|
@@ -74,9 +76,198 @@ function pathWithCursor(requestPath, cursor) {
|
|
|
74
76
|
return `${url.pathname}${url.search}`;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function defaultCheckpointPath({
|
|
79
|
+
function defaultCheckpointPath({
|
|
80
|
+
runId,
|
|
81
|
+
operationName,
|
|
82
|
+
operationFingerprint = "",
|
|
83
|
+
}) {
|
|
78
84
|
const safeName = String(operationName).replace(/[^a-z0-9_.-]+/gi, "-");
|
|
79
|
-
|
|
85
|
+
const fingerprintSuffix = operationFingerprint
|
|
86
|
+
? `-${operationFingerprint}`
|
|
87
|
+
: "";
|
|
88
|
+
return path.join(
|
|
89
|
+
os.tmpdir(),
|
|
90
|
+
`lumine-admin-run-${runId}-${safeName}${fingerprintSuffix}.json`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveAutomaticCheckpointPath({
|
|
95
|
+
options,
|
|
96
|
+
operation,
|
|
97
|
+
runId,
|
|
98
|
+
operationFingerprint,
|
|
99
|
+
}) {
|
|
100
|
+
if (String(options.adminCheckpoint || "").trim()) {
|
|
101
|
+
return path.resolve(options.adminCheckpoint);
|
|
102
|
+
}
|
|
103
|
+
const keyedPath = defaultCheckpointPath({
|
|
104
|
+
runId,
|
|
105
|
+
operationName: operation.name,
|
|
106
|
+
operationFingerprint,
|
|
107
|
+
});
|
|
108
|
+
if (
|
|
109
|
+
options.adminResume &&
|
|
110
|
+
!pathEntryExists(keyedPath) &&
|
|
111
|
+
!pathEntryExists(`${keyedPath}.lock`) &&
|
|
112
|
+
!pathEntryExists(`${keyedPath}.lock.reclaim`)
|
|
113
|
+
) {
|
|
114
|
+
const legacyPath = defaultCheckpointPath({
|
|
115
|
+
runId,
|
|
116
|
+
operationName: operation.name,
|
|
117
|
+
});
|
|
118
|
+
if (pathEntryExists(legacyPath)) return legacyPath;
|
|
119
|
+
}
|
|
120
|
+
return keyedPath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pathEntryExists(filePath) {
|
|
124
|
+
try {
|
|
125
|
+
lstatSync(filePath);
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code === "ENOENT") return false;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function acquireCheckpointLock(checkpointPath, operationFingerprint) {
|
|
134
|
+
const lockPath = `${checkpointPath}.lock`;
|
|
135
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
136
|
+
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
137
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
138
|
+
if (pathEntryExists(reclaimPath)) {
|
|
139
|
+
throw validationError(
|
|
140
|
+
`Another Lumine admin scan is recovering checkpoint ${checkpointPath}. Wait for it to finish or choose a different --checkpoint.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const token = randomUUID();
|
|
144
|
+
let descriptor;
|
|
145
|
+
let ownsLock = false;
|
|
146
|
+
try {
|
|
147
|
+
descriptor = openSync(lockPath, "wx", 0o600);
|
|
148
|
+
ownsLock = true;
|
|
149
|
+
fchmodSync(descriptor, 0o600);
|
|
150
|
+
writeFileSync(
|
|
151
|
+
descriptor,
|
|
152
|
+
`${JSON.stringify({
|
|
153
|
+
kind: "admin-pagination-lock",
|
|
154
|
+
pid: process.pid,
|
|
155
|
+
token,
|
|
156
|
+
operationFingerprint,
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
})}\n`,
|
|
159
|
+
);
|
|
160
|
+
fsyncSync(descriptor);
|
|
161
|
+
closeSync(descriptor);
|
|
162
|
+
descriptor = undefined;
|
|
163
|
+
// A stale-lock reclaimer can begin after the check above but before this
|
|
164
|
+
// O_EXCL create. It hard-links the exact old inode before removal. If
|
|
165
|
+
// that claim still exists, relinquish our new lock rather than allowing
|
|
166
|
+
// two owners to cross during the handoff.
|
|
167
|
+
if (pathEntryExists(reclaimPath)) {
|
|
168
|
+
releaseCheckpointLock({ lockPath, token });
|
|
169
|
+
ownsLock = false;
|
|
170
|
+
throw validationError(
|
|
171
|
+
`Another Lumine admin scan is recovering checkpoint ${checkpointPath}. Retry after it finishes.`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return { lockPath, token };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (descriptor !== undefined) {
|
|
177
|
+
try {
|
|
178
|
+
closeSync(descriptor);
|
|
179
|
+
} catch {
|
|
180
|
+
// The failed lock initialization may already have closed it.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (ownsLock) {
|
|
184
|
+
try {
|
|
185
|
+
unlinkSync(lockPath);
|
|
186
|
+
} catch {
|
|
187
|
+
// A failed initialization may leave no owned lock to remove.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (error?.code !== "EEXIST") throw error;
|
|
191
|
+
if (attempt > 0 || !removeDeadCheckpointLock(lockPath)) {
|
|
192
|
+
throw validationError(
|
|
193
|
+
`Another Lumine admin scan owns checkpoint ${checkpointPath}. Wait for it to finish or choose a different --checkpoint.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
throw validationError(
|
|
199
|
+
`Could not acquire the pagination checkpoint lock for ${checkpointPath}.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function removeDeadCheckpointLock(lockPath) {
|
|
204
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
205
|
+
let ownsReclaim = false;
|
|
206
|
+
try {
|
|
207
|
+
// Creating a fixed-name hard link is an atomic claim on the exact lock
|
|
208
|
+
// inode. It serializes reclaimers and lets us prove we are not unlinking a
|
|
209
|
+
// replacement lock created after another process removed the stale one.
|
|
210
|
+
linkSync(lockPath, reclaimPath);
|
|
211
|
+
ownsReclaim = true;
|
|
212
|
+
const fileState = lstatSync(reclaimPath);
|
|
213
|
+
if (!fileState.isFile() || fileState.isSymbolicLink()) return false;
|
|
214
|
+
if (fileState.size > 16 * 1024) return false;
|
|
215
|
+
const state = JSON.parse(readFileSync(reclaimPath, "utf8"));
|
|
216
|
+
if (
|
|
217
|
+
state?.kind !== "admin-pagination-lock" ||
|
|
218
|
+
!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(
|
|
219
|
+
String(state?.token || ""),
|
|
220
|
+
) ||
|
|
221
|
+
!/^[a-f0-9]{64}$/i.test(String(state?.operationFingerprint || ""))
|
|
222
|
+
) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const pid = Number(state?.pid);
|
|
226
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
227
|
+
try {
|
|
228
|
+
process.kill(pid, 0);
|
|
229
|
+
return false;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error?.code !== "ESRCH") return false;
|
|
232
|
+
}
|
|
233
|
+
const claimedState = lstatSync(reclaimPath);
|
|
234
|
+
const currentState = lstatSync(lockPath);
|
|
235
|
+
if (
|
|
236
|
+
claimedState.dev !== currentState.dev ||
|
|
237
|
+
claimedState.ino !== currentState.ino
|
|
238
|
+
) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
unlinkSync(lockPath);
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
} finally {
|
|
246
|
+
if (ownsReclaim) {
|
|
247
|
+
try {
|
|
248
|
+
unlinkSync(reclaimPath);
|
|
249
|
+
} catch {
|
|
250
|
+
// A failed or completed reclaim leaves the current lock untouched.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function releaseCheckpointLock(lock) {
|
|
257
|
+
try {
|
|
258
|
+
const fileState = lstatSync(lock.lockPath);
|
|
259
|
+
if (!fileState.isFile() || fileState.isSymbolicLink()) return;
|
|
260
|
+
const saved = JSON.parse(readFileSync(lock.lockPath, "utf8"));
|
|
261
|
+
if (saved?.token === lock.token && Number(saved?.pid) === process.pid) {
|
|
262
|
+
unlinkSync(lock.lockPath);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error?.code !== "ENOENT") {
|
|
266
|
+
process.stderr.write(
|
|
267
|
+
`Lumine admin: could not release pagination lock ${lock.lockPath} (${error?.message || error}).\n`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
80
271
|
}
|
|
81
272
|
|
|
82
273
|
function operationFingerprintValue({
|
|
@@ -91,6 +282,13 @@ function operationFingerprintValue({
|
|
|
91
282
|
apiUrl: String(options.apiUrl || "").replace(/\/$/, ""),
|
|
92
283
|
name: operation.name,
|
|
93
284
|
path: pathWithCursor(operation.path, ""),
|
|
285
|
+
...(operation.name === "builds.candidates"
|
|
286
|
+
? {
|
|
287
|
+
resultTransform: {
|
|
288
|
+
siteUrl: String(options.siteUrl || "").replace(/\/$/, ""),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
: {}),
|
|
94
292
|
pagination: {
|
|
95
293
|
collectionKey: operation.pagination.collectionKey,
|
|
96
294
|
coverageQueue: operation.pagination.coverageQueue || null,
|
|
@@ -141,9 +339,13 @@ function createCandidateSpool(checkpointPath) {
|
|
|
141
339
|
function loadSupersededCandidateSpool(checkpointPath) {
|
|
142
340
|
let saved;
|
|
143
341
|
try {
|
|
144
|
-
saved = readAdminJsonFile(
|
|
145
|
-
|
|
146
|
-
|
|
342
|
+
saved = readAdminJsonFile(
|
|
343
|
+
checkpointPath,
|
|
344
|
+
"the previous pagination checkpoint",
|
|
345
|
+
{
|
|
346
|
+
maxBytes: MAX_LEGACY_ADMIN_CHECKPOINT_BYTES,
|
|
347
|
+
},
|
|
348
|
+
);
|
|
147
349
|
} catch {
|
|
148
350
|
return null;
|
|
149
351
|
}
|
|
@@ -215,8 +417,7 @@ function normalizeClientFilterSummary(value) {
|
|
|
215
417
|
if (
|
|
216
418
|
!Array.isArray(value.contentTypes) ||
|
|
217
419
|
value.contentTypes.some(
|
|
218
|
-
(contentType) =>
|
|
219
|
-
typeof contentType !== "string" || !contentType.trim(),
|
|
420
|
+
(contentType) => typeof contentType !== "string" || !contentType.trim(),
|
|
220
421
|
)
|
|
221
422
|
) {
|
|
222
423
|
throw validationError(
|
|
@@ -324,7 +525,9 @@ function verifyCandidateSpool({
|
|
|
324
525
|
try {
|
|
325
526
|
fileState = lstatSync(spoolPath);
|
|
326
527
|
} catch {
|
|
327
|
-
throw validationError(
|
|
528
|
+
throw validationError(
|
|
529
|
+
`Could not read pagination candidates at ${spoolPath}.`,
|
|
530
|
+
);
|
|
328
531
|
}
|
|
329
532
|
if (!fileState.isFile() || fileState.isSymbolicLink()) {
|
|
330
533
|
throw validationError(
|
|
@@ -430,7 +633,9 @@ function serializedObjectEntries(value, excludedKeys = new Set()) {
|
|
|
430
633
|
export async function forEachPaginatedResultItem(result, visit) {
|
|
431
634
|
const storage = paginationStorageFromResult(result);
|
|
432
635
|
if (!storage) {
|
|
433
|
-
throw validationError(
|
|
636
|
+
throw validationError(
|
|
637
|
+
"The result does not contain spooled pagination data.",
|
|
638
|
+
);
|
|
434
639
|
}
|
|
435
640
|
verifyCandidateSpool({ ...storage, discardUnconfirmedTail: false });
|
|
436
641
|
const input = createReadStream(storage.spoolPath, { encoding: "utf8" });
|
|
@@ -604,22 +809,87 @@ function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
|
604
809
|
});
|
|
605
810
|
}
|
|
606
811
|
|
|
607
|
-
export async function runAutomaticPagination({
|
|
812
|
+
export async function runAutomaticPagination(args) {
|
|
813
|
+
const { options, operation, runId } = args;
|
|
814
|
+
if (!operation.pagination) {
|
|
815
|
+
throw validationError(
|
|
816
|
+
"--all is supported only by paginated admin listings.",
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const operationFingerprint = operationFingerprintValue({
|
|
820
|
+
schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
821
|
+
options,
|
|
822
|
+
operation,
|
|
823
|
+
runId,
|
|
824
|
+
});
|
|
825
|
+
const checkpointPath = resolveAutomaticCheckpointPath({
|
|
826
|
+
options,
|
|
827
|
+
operation,
|
|
828
|
+
runId,
|
|
829
|
+
operationFingerprint,
|
|
830
|
+
});
|
|
831
|
+
const lock = acquireCheckpointLock(checkpointPath, operationFingerprint);
|
|
832
|
+
const signalSource = args.signalSource || process;
|
|
833
|
+
const controller = new AbortController();
|
|
834
|
+
let interruptedSignal = "";
|
|
835
|
+
const interrupt = (signalName) => {
|
|
836
|
+
interruptedSignal ||= signalName;
|
|
837
|
+
controller.abort(
|
|
838
|
+
new Error(`Lumine admin scan interrupted by ${signalName}.`),
|
|
839
|
+
);
|
|
840
|
+
};
|
|
841
|
+
const onSigint = () => interrupt("SIGINT");
|
|
842
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
843
|
+
signalSource.once("SIGINT", onSigint);
|
|
844
|
+
signalSource.once("SIGTERM", onSigterm);
|
|
845
|
+
try {
|
|
846
|
+
return await runAutomaticPaginationWithLock({
|
|
847
|
+
...args,
|
|
848
|
+
checkpointPath,
|
|
849
|
+
operationFingerprint,
|
|
850
|
+
signal: controller.signal,
|
|
851
|
+
});
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (interruptedSignal) {
|
|
854
|
+
const interrupted = validationError(
|
|
855
|
+
`Canonical scan cancelled by ${interruptedSignal}. Confirmed progress remains at ${checkpointPath}; rerun the same command with --resume to continue it.`,
|
|
856
|
+
);
|
|
857
|
+
interrupted.code = "LUMINE_ADMIN_SCAN_CANCELLED";
|
|
858
|
+
interrupted.data = {
|
|
859
|
+
ok: false,
|
|
860
|
+
status: "validation_error",
|
|
861
|
+
error: {
|
|
862
|
+
code: interrupted.code,
|
|
863
|
+
message: interrupted.message,
|
|
864
|
+
details: {
|
|
865
|
+
checkpointPath,
|
|
866
|
+
signal: interruptedSignal,
|
|
867
|
+
resumable: true,
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
throw interrupted;
|
|
872
|
+
}
|
|
873
|
+
throw error;
|
|
874
|
+
} finally {
|
|
875
|
+
signalSource.removeListener("SIGINT", onSigint);
|
|
876
|
+
signalSource.removeListener("SIGTERM", onSigterm);
|
|
877
|
+
releaseCheckpointLock(lock);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function runAutomaticPaginationWithLock({
|
|
608
882
|
options,
|
|
609
883
|
operation,
|
|
610
884
|
runId,
|
|
885
|
+
checkpointPath,
|
|
886
|
+
operationFingerprint,
|
|
611
887
|
fetchPage,
|
|
612
888
|
transformPage,
|
|
613
889
|
recordCoverage,
|
|
614
890
|
reportProgress = (message) => process.stderr.write(`${message}\n`),
|
|
891
|
+
signal,
|
|
615
892
|
}) {
|
|
616
|
-
if (!operation.pagination) {
|
|
617
|
-
throw validationError("--all is supported only by paginated admin listings.");
|
|
618
|
-
}
|
|
619
|
-
const checkpointPath = path.resolve(
|
|
620
|
-
options.adminCheckpoint ||
|
|
621
|
-
defaultCheckpointPath({ runId, operationName: operation.name }),
|
|
622
|
-
);
|
|
623
893
|
const requestedOutputPath = String(options.adminOutput || "").trim();
|
|
624
894
|
if (options.adminOutput && !requestedOutputPath) {
|
|
625
895
|
throw validationError("An output file path is required.");
|
|
@@ -632,12 +902,6 @@ export async function runAutomaticPagination({
|
|
|
632
902
|
"The pagination checkpoint and result output must use different files.",
|
|
633
903
|
);
|
|
634
904
|
}
|
|
635
|
-
const operationFingerprint = operationFingerprintValue({
|
|
636
|
-
schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
637
|
-
options,
|
|
638
|
-
operation,
|
|
639
|
-
runId,
|
|
640
|
-
});
|
|
641
905
|
const legacyOperationFingerprint = operationFingerprintValue({
|
|
642
906
|
schemaVersion: LEGACY_ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
643
907
|
options,
|
|
@@ -774,15 +1038,12 @@ export async function runAutomaticPagination({
|
|
|
774
1038
|
: null,
|
|
775
1039
|
snapshotTimeStamp:
|
|
776
1040
|
savedPages > 0
|
|
777
|
-
? normalizeBoundary(
|
|
778
|
-
saved?.snapshotTimeStamp,
|
|
779
|
-
"snapshotTimeStamp",
|
|
780
|
-
)
|
|
1041
|
+
? normalizeBoundary(saved?.snapshotTimeStamp, "snapshotTimeStamp")
|
|
781
1042
|
: null,
|
|
782
1043
|
after:
|
|
783
1044
|
savedPages > 0
|
|
784
1045
|
? normalizeBoundary(saved?.after, "after")
|
|
785
|
-
: operation.pagination.after ?? null,
|
|
1046
|
+
: (operation.pagination.after ?? null),
|
|
786
1047
|
boundariesConfirmed: saved?.boundariesConfirmed,
|
|
787
1048
|
candidateCount,
|
|
788
1049
|
spoolPath: path.resolve(String(saved?.spoolPath || "")),
|
|
@@ -849,19 +1110,27 @@ export async function runAutomaticPagination({
|
|
|
849
1110
|
}
|
|
850
1111
|
let lastPage = null;
|
|
851
1112
|
while (!state.exhausted) {
|
|
1113
|
+
signal?.throwIfAborted();
|
|
852
1114
|
if (state.pages >= MAX_AUTOMATIC_PAGES) {
|
|
853
1115
|
throw validationError(
|
|
854
1116
|
`Automatic pagination stopped at the ${MAX_AUTOMATIC_PAGES}-page safety ceiling. Start a narrower request with a new checkpoint.`,
|
|
855
1117
|
);
|
|
856
1118
|
}
|
|
857
1119
|
const requestPath = pathWithCursor(operation.path, state.nextCursor);
|
|
858
|
-
const raw = await fetchPage(requestPath);
|
|
1120
|
+
const raw = await fetchPage(requestPath, signal);
|
|
1121
|
+
signal?.throwIfAborted();
|
|
859
1122
|
const page = transformPage(raw);
|
|
860
1123
|
lastPage = page;
|
|
861
1124
|
const data = page?.data || {};
|
|
862
1125
|
const pagination = data.pagination;
|
|
863
|
-
if (
|
|
864
|
-
|
|
1126
|
+
if (
|
|
1127
|
+
!pagination ||
|
|
1128
|
+
typeof pagination !== "object" ||
|
|
1129
|
+
Array.isArray(pagination)
|
|
1130
|
+
) {
|
|
1131
|
+
throw validationError(
|
|
1132
|
+
"The API response is missing canonical pagination metadata.",
|
|
1133
|
+
);
|
|
865
1134
|
}
|
|
866
1135
|
const items = Array.isArray(data[operation.pagination.collectionKey])
|
|
867
1136
|
? data[operation.pagination.collectionKey]
|
|
@@ -873,7 +1142,9 @@ export async function runAutomaticPagination({
|
|
|
873
1142
|
}
|
|
874
1143
|
const nextCursor = String(pagination.nextCursor || "");
|
|
875
1144
|
if (nextCursor && nextCursor === state.nextCursor) {
|
|
876
|
-
throw validationError(
|
|
1145
|
+
throw validationError(
|
|
1146
|
+
"The API returned the same pagination cursor twice.",
|
|
1147
|
+
);
|
|
877
1148
|
}
|
|
878
1149
|
if (!pagination.exhausted && !nextCursor) {
|
|
879
1150
|
throw validationError(
|
|
@@ -895,7 +1166,11 @@ export async function runAutomaticPagination({
|
|
|
895
1166
|
);
|
|
896
1167
|
const pageAfter = normalizeBoundary(pagination.after, "after");
|
|
897
1168
|
if (state.boundariesConfirmed) {
|
|
898
|
-
assertStableBoundary(
|
|
1169
|
+
assertStableBoundary(
|
|
1170
|
+
"snapshotMaxId",
|
|
1171
|
+
state.snapshotMaxId,
|
|
1172
|
+
pageSnapshotMaxId,
|
|
1173
|
+
);
|
|
899
1174
|
assertStableBoundary(
|
|
900
1175
|
"snapshotTimeStamp",
|
|
901
1176
|
state.snapshotTimeStamp,
|
|
@@ -903,7 +1178,10 @@ export async function runAutomaticPagination({
|
|
|
903
1178
|
);
|
|
904
1179
|
assertStableBoundary("after", state.after, pageAfter);
|
|
905
1180
|
} else {
|
|
906
|
-
const requestedAfter = normalizeBoundary(
|
|
1181
|
+
const requestedAfter = normalizeBoundary(
|
|
1182
|
+
operation.pagination.after,
|
|
1183
|
+
"after",
|
|
1184
|
+
);
|
|
907
1185
|
if (requestedAfter !== null) {
|
|
908
1186
|
assertStableBoundary("after", requestedAfter, pageAfter);
|
|
909
1187
|
}
|
|
@@ -931,10 +1209,7 @@ export async function runAutomaticPagination({
|
|
|
931
1209
|
pages: state.pages + 1,
|
|
932
1210
|
scannedCount:
|
|
933
1211
|
state.scannedCount +
|
|
934
|
-
normalizeScannedCount(
|
|
935
|
-
pagination.scannedCount,
|
|
936
|
-
items.length,
|
|
937
|
-
),
|
|
1212
|
+
normalizeScannedCount(pagination.scannedCount, items.length),
|
|
938
1213
|
candidateCount: state.candidateCount + items.length,
|
|
939
1214
|
spoolBytes: state.spoolBytes + appended.addedBytes,
|
|
940
1215
|
spoolSha256: appended.spoolSha256,
|
|
@@ -961,19 +1236,24 @@ export async function runAutomaticPagination({
|
|
|
961
1236
|
checkpointPath,
|
|
962
1237
|
state,
|
|
963
1238
|
});
|
|
1239
|
+
signal?.throwIfAborted();
|
|
964
1240
|
if (recordCoverage && operation.pagination.coverageQueue) {
|
|
965
|
-
await recordCoverage(
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1241
|
+
await recordCoverage(
|
|
1242
|
+
{
|
|
1243
|
+
queue: operation.pagination.coverageQueue,
|
|
1244
|
+
mode: operation.pagination.coverageMode || "all",
|
|
1245
|
+
after: state.after,
|
|
1246
|
+
pages: state.pages,
|
|
1247
|
+
scannedCount: state.scannedCount,
|
|
1248
|
+
candidateCount: state.candidateCount,
|
|
1249
|
+
snapshotMaxId: state.snapshotMaxId,
|
|
1250
|
+
snapshotTimeStamp: state.snapshotTimeStamp,
|
|
1251
|
+
exhausted: state.exhausted,
|
|
1252
|
+
filters: operation.pagination.filters || {},
|
|
1253
|
+
},
|
|
1254
|
+
signal,
|
|
1255
|
+
);
|
|
1256
|
+
signal?.throwIfAborted();
|
|
977
1257
|
}
|
|
978
1258
|
if (outputPath) {
|
|
979
1259
|
result.data.scan.outputPath = outputPath;
|
|
@@ -1010,12 +1290,16 @@ export function readBatchSkipTargets({ filePath, parseTarget, defaultReason }) {
|
|
|
1010
1290
|
throw validationError("The batch target file contains no targets.");
|
|
1011
1291
|
}
|
|
1012
1292
|
if (values.length > MAX_BATCH_TARGETS) {
|
|
1013
|
-
throw validationError(
|
|
1293
|
+
throw validationError(
|
|
1294
|
+
`A batch can contain at most ${MAX_BATCH_TARGETS} targets.`,
|
|
1295
|
+
);
|
|
1014
1296
|
}
|
|
1015
1297
|
const deduped = new Map();
|
|
1016
1298
|
for (const value of values) {
|
|
1017
1299
|
const rawTarget =
|
|
1018
|
-
typeof value === "string"
|
|
1300
|
+
typeof value === "string"
|
|
1301
|
+
? value
|
|
1302
|
+
: value?.target || value?.url || value?.id;
|
|
1019
1303
|
const parsed = parseTarget({
|
|
1020
1304
|
target: rawTarget,
|
|
1021
1305
|
explicitType: typeof value === "object" ? value?.type || "" : "",
|
|
@@ -1082,7 +1366,9 @@ export async function runBatchSkips({
|
|
|
1082
1366
|
!saved.completed ||
|
|
1083
1367
|
typeof saved.completed !== "object"
|
|
1084
1368
|
) {
|
|
1085
|
-
throw validationError(
|
|
1369
|
+
throw validationError(
|
|
1370
|
+
"The checkpoint does not match this exact skip batch.",
|
|
1371
|
+
);
|
|
1086
1372
|
}
|
|
1087
1373
|
state = saved;
|
|
1088
1374
|
} else {
|
|
@@ -1123,8 +1409,9 @@ export async function runBatchSkips({
|
|
|
1123
1409
|
batch: {
|
|
1124
1410
|
targetCount: targets.length,
|
|
1125
1411
|
completedCount: Object.keys(state.completed).length,
|
|
1126
|
-
changedCount: Object.values(state.completed).filter(
|
|
1127
|
-
.
|
|
1412
|
+
changedCount: Object.values(state.completed).filter(
|
|
1413
|
+
(entry) => entry.changed,
|
|
1414
|
+
).length,
|
|
1128
1415
|
checkpointPath,
|
|
1129
1416
|
resumed: options.adminResume === true,
|
|
1130
1417
|
targets: state.completed,
|
|
@@ -1132,10 +1419,14 @@ export async function runBatchSkips({
|
|
|
1132
1419
|
},
|
|
1133
1420
|
};
|
|
1134
1421
|
if (options.adminOutput) {
|
|
1135
|
-
result.data.batch.outputPath = writeAdminJsonFile(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1422
|
+
result.data.batch.outputPath = writeAdminJsonFile(
|
|
1423
|
+
options.adminOutput,
|
|
1424
|
+
result,
|
|
1425
|
+
{
|
|
1426
|
+
privateFile: true,
|
|
1427
|
+
maxBytes: MAX_BATCH_CHECKPOINT_BYTES,
|
|
1428
|
+
},
|
|
1429
|
+
);
|
|
1139
1430
|
}
|
|
1140
1431
|
return result;
|
|
1141
1432
|
}
|