@tiangong-lca/cli 0.0.23 → 0.0.24
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 +14 -6
- package/dist/src/cli.js +20 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-apply.js +269 -4
- package/dist/src/lib/dataset-maintenance-apply.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-contract.js +118 -16
- package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-derivatives.js +265 -0
- package/dist/src/lib/dataset-maintenance-derivatives.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-plan.js +38 -6
- package/dist/src/lib/dataset-maintenance-plan.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-remote.js +25 -0
- package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-verify.js +309 -3
- package/dist/src/lib/dataset-maintenance-verify.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,9 +4,10 @@ import { writeJsonArtifact } from './artifacts.js';
|
|
|
4
4
|
import { CliError } from './errors.js';
|
|
5
5
|
import { withStateFileLock } from './state-lock.js';
|
|
6
6
|
import { MAINTENANCE_SCAN_TABLES, appendStableJsonLine, isJsonObject, maintenanceRowKey, parseMaintenancePlan, readJsonFile, readJsonLinesIfPresent, resolveMaintenancePlanArtifactPath, sha256Json, snapshotRemoteRow, writeImmutableJson, } from './dataset-maintenance-contract.js';
|
|
7
|
+
import { buildDerivativePlanRequest, derivativePlanAction, parseDerivativeSnapshotResponse, parseDerivativeSubmitResponse, } from './dataset-maintenance-derivatives.js';
|
|
7
8
|
import { maintenanceProjectedReferenceFingerprint } from './dataset-maintenance-plan.js';
|
|
8
9
|
import { isSnapshotCompletenessCompatible } from './dataset-maintenance-pagination.js';
|
|
9
|
-
import { applyMaintenanceAliasPlan, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
|
|
10
|
+
import { applyMaintenanceAliasPlan, applyMaintenanceDerivativeRebuild, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceDerivativeSnapshot, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
|
|
10
11
|
const POSITIVE_INTEGER_TEXT = /^[1-9]\d*$/u;
|
|
11
12
|
function clock(options) {
|
|
12
13
|
return (options.now ?? new Date()).toISOString();
|
|
@@ -101,6 +102,98 @@ function parseProgress(plan, progressPath) {
|
|
|
101
102
|
}
|
|
102
103
|
return { entries, successes, latestFailures };
|
|
103
104
|
}
|
|
105
|
+
function derivativeProofIdentity(proof) {
|
|
106
|
+
return sha256Json({
|
|
107
|
+
schema_version: proof.schema_version,
|
|
108
|
+
plan_sha256: proof.plan_sha256,
|
|
109
|
+
operation_id: proof.operation_id,
|
|
110
|
+
target_visibility: proof.target_visibility,
|
|
111
|
+
plan_request_sha256: proof.plan_request_sha256,
|
|
112
|
+
action_count: proof.action_count,
|
|
113
|
+
accepted_count: proof.accepted_count,
|
|
114
|
+
summary_audit_id: proof.summary_audit_id,
|
|
115
|
+
request_id: proof.request_id,
|
|
116
|
+
action_request_sha256: proof.action_request_sha256,
|
|
117
|
+
database_audit_id: proof.database_audit_id,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function parseDerivativeSubmitProgress(plan, progressPath) {
|
|
121
|
+
const action = derivativePlanAction(plan);
|
|
122
|
+
const entries = readJsonLinesIfPresent(progressPath).map((value) => {
|
|
123
|
+
const rawProof = isJsonObject(value) && isJsonObject(value.proof) ? value.proof : null;
|
|
124
|
+
let proof;
|
|
125
|
+
try {
|
|
126
|
+
proof = parseDerivativeSubmitResponse(rawProof
|
|
127
|
+
? {
|
|
128
|
+
ok: true,
|
|
129
|
+
command: 'cmd_dataset_derivative_rebuild_plan_guarded',
|
|
130
|
+
...rawProof,
|
|
131
|
+
}
|
|
132
|
+
: null, plan);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw new CliError('Derivative submit progress contains an invalid RPC proof.', {
|
|
136
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_PROGRESS_INVALID',
|
|
137
|
+
exitCode: 1,
|
|
138
|
+
details: errorMessage(error),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!isJsonObject(value) ||
|
|
142
|
+
value.schema_version !== 1 ||
|
|
143
|
+
value.plan_sha256 !== plan.plan_sha256 ||
|
|
144
|
+
value.operation_id !== plan.operation_id ||
|
|
145
|
+
value.action_id !== action.action_id ||
|
|
146
|
+
value.target_mode !== 'owner_draft' ||
|
|
147
|
+
!isJsonObject(value.actor) ||
|
|
148
|
+
value.actor.user_id !== plan.account.user_id ||
|
|
149
|
+
value.actor.email !== plan.account.email ||
|
|
150
|
+
typeof value.started_at_utc !== 'string' ||
|
|
151
|
+
typeof value.ended_at_utc !== 'string' ||
|
|
152
|
+
value.result !== 'accepted') {
|
|
153
|
+
throw new CliError('Derivative submit progress contains an invalid or foreign entry.', {
|
|
154
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_PROGRESS_INVALID',
|
|
155
|
+
exitCode: 1,
|
|
156
|
+
details: value,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return { ...value, proof };
|
|
160
|
+
});
|
|
161
|
+
const identities = new Set(entries.map((entry) => derivativeProofIdentity(entry.proof)));
|
|
162
|
+
if (identities.size > 1) {
|
|
163
|
+
throw new CliError('Derivative submit replays do not identify one durable request.', {
|
|
164
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_PROGRESS_INVALID',
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { entries, latest: entries.at(-1) ?? null };
|
|
169
|
+
}
|
|
170
|
+
function validateDerivativeAdmissionAttempt(options) {
|
|
171
|
+
if (!existsSync(options.path))
|
|
172
|
+
return null;
|
|
173
|
+
const value = readJsonFile(options.path, 'Derivative admission attempt');
|
|
174
|
+
const action = derivativePlanAction(options.plan);
|
|
175
|
+
if (!isJsonObject(value) ||
|
|
176
|
+
value.schema_version !== 1 ||
|
|
177
|
+
value.plan_sha256 !== options.plan.plan_sha256 ||
|
|
178
|
+
value.operation_id !== options.plan.operation_id ||
|
|
179
|
+
value.action_id !== action.action_id ||
|
|
180
|
+
value.table !== 'processes' ||
|
|
181
|
+
value.id !== action.id ||
|
|
182
|
+
value.version !== action.version ||
|
|
183
|
+
value.expected_snapshot_sha256 !== action.derivative_before?.snapshot_sha256 ||
|
|
184
|
+
!isJsonObject(value.actor) ||
|
|
185
|
+
value.actor.user_id !== options.context.account.user_id ||
|
|
186
|
+
value.actor.email !== options.context.account.email ||
|
|
187
|
+
typeof value.prepared_at_utc !== 'string' ||
|
|
188
|
+
!Number.isFinite(Date.parse(value.prepared_at_utc))) {
|
|
189
|
+
throw new CliError('Derivative admission attempt is invalid or belongs to another plan.', {
|
|
190
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_ATTEMPT_INVALID',
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
details: value,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
104
197
|
function parseAliasBatchProgress(plan, progressPath) {
|
|
105
198
|
const batches = new Map(plan.alias_batches.map((batch) => [batch.batch_id, batch]));
|
|
106
199
|
const entries = readJsonLinesIfPresent(progressPath).map((value) => {
|
|
@@ -1039,6 +1132,12 @@ function appendAliasPlanFailure(options) {
|
|
|
1039
1132
|
return entry;
|
|
1040
1133
|
}
|
|
1041
1134
|
async function executeAction(options) {
|
|
1135
|
+
if (options.action.action === 'rebuild_derivatives') {
|
|
1136
|
+
throw new CliError('Derivative rebuild actions may only execute through the guarded whole-plan RPC.', {
|
|
1137
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_SEQUENTIAL_WRITE_FORBIDDEN',
|
|
1138
|
+
exitCode: 1,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1042
1141
|
if (!options.action.before) {
|
|
1043
1142
|
throw new CliError(`Action lacks a before snapshot: ${options.action.action_id}`, {
|
|
1044
1143
|
code: 'DATASET_MAINTENANCE_PLAN_INVALID',
|
|
@@ -1114,6 +1213,12 @@ async function executeAction(options) {
|
|
|
1114
1213
|
exitCode: 1,
|
|
1115
1214
|
});
|
|
1116
1215
|
}
|
|
1216
|
+
if (options.action.action !== 'delete') {
|
|
1217
|
+
throw new CliError(`Unsupported maintenance action: ${options.action.action}`, {
|
|
1218
|
+
code: 'DATASET_MAINTENANCE_ACTION_UNSUPPORTED',
|
|
1219
|
+
exitCode: 2,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1117
1222
|
const remoteResult = await deleteMaintenanceRow({
|
|
1118
1223
|
context: options.context,
|
|
1119
1224
|
table: options.action.table,
|
|
@@ -1135,6 +1240,60 @@ async function executeAction(options) {
|
|
|
1135
1240
|
}
|
|
1136
1241
|
return { afterSha256: null, remoteResultSha256: sha256Json(remoteResult) };
|
|
1137
1242
|
}
|
|
1243
|
+
async function executeDerivativeAdmission(options) {
|
|
1244
|
+
const action = derivativePlanAction(options.plan);
|
|
1245
|
+
const plannedSnapshot = action.derivative_before;
|
|
1246
|
+
const preflight = parseDerivativeSnapshotResponse(await fetchMaintenanceDerivativeSnapshot({
|
|
1247
|
+
context: options.context,
|
|
1248
|
+
id: action.id,
|
|
1249
|
+
version: action.version,
|
|
1250
|
+
}), { id: action.id, version: action.version, userId: action.expected_user_id });
|
|
1251
|
+
if (preflight.modified_at !== plannedSnapshot.modified_at ||
|
|
1252
|
+
preflight.json_sha256 !== plannedSnapshot.json_sha256 ||
|
|
1253
|
+
preflight.json_ordered_sha256 !== plannedSnapshot.json_ordered_sha256 ||
|
|
1254
|
+
preflight.extracted_text_sha256 !== plannedSnapshot.extracted_text_sha256) {
|
|
1255
|
+
throw new CliError('Derivative action primary preconditions drifted after planning.', {
|
|
1256
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_PRIMARY_DRIFT',
|
|
1257
|
+
exitCode: 1,
|
|
1258
|
+
details: {
|
|
1259
|
+
expected_snapshot_sha256: plannedSnapshot.snapshot_sha256,
|
|
1260
|
+
actual_snapshot_sha256: preflight.snapshot_sha256,
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
if (!options.replayPossible && preflight.snapshot_sha256 !== plannedSnapshot.snapshot_sha256) {
|
|
1265
|
+
throw new CliError('Derivative action-scoped snapshot drifted before first admission.', {
|
|
1266
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_SNAPSHOT_DRIFT',
|
|
1267
|
+
exitCode: 1,
|
|
1268
|
+
details: {
|
|
1269
|
+
expected: plannedSnapshot.snapshot_sha256,
|
|
1270
|
+
actual: preflight.snapshot_sha256,
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (!options.replayPossible) {
|
|
1275
|
+
writeImmutableJson(options.attemptPath, {
|
|
1276
|
+
schema_version: 1,
|
|
1277
|
+
plan_sha256: options.plan.plan_sha256,
|
|
1278
|
+
operation_id: options.plan.operation_id,
|
|
1279
|
+
action_id: action.action_id,
|
|
1280
|
+
table: 'processes',
|
|
1281
|
+
id: action.id,
|
|
1282
|
+
version: action.version,
|
|
1283
|
+
expected_snapshot_sha256: plannedSnapshot.snapshot_sha256,
|
|
1284
|
+
actor: {
|
|
1285
|
+
user_id: options.context.account.user_id,
|
|
1286
|
+
email: options.context.account.email,
|
|
1287
|
+
},
|
|
1288
|
+
prepared_at_utc: options.preparedAtUtc,
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
const result = await applyMaintenanceDerivativeRebuild({
|
|
1292
|
+
context: options.context,
|
|
1293
|
+
plan: buildDerivativePlanRequest(options.plan),
|
|
1294
|
+
});
|
|
1295
|
+
return parseDerivativeSubmitResponse(result, options.plan);
|
|
1296
|
+
}
|
|
1138
1297
|
function nextAttemptPath(planDir) {
|
|
1139
1298
|
let attempt = 1;
|
|
1140
1299
|
while (existsSync(path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`))) {
|
|
@@ -1173,7 +1332,9 @@ export async function runDatasetMaintenanceApply(options) {
|
|
|
1173
1332
|
exitCode: 1,
|
|
1174
1333
|
});
|
|
1175
1334
|
}
|
|
1176
|
-
const progressPath = path.join(planDir, '
|
|
1335
|
+
const progressPath = path.join(planDir, plan.operation === 'rebuild-derivatives'
|
|
1336
|
+
? 'derivative-submit-progress.jsonl'
|
|
1337
|
+
: 'apply-progress.jsonl');
|
|
1177
1338
|
const aliasPlanProgressPath = path.join(planDir, 'alias-plan-progress.jsonl');
|
|
1178
1339
|
const aliasBatchProgressPath = path.join(planDir, 'alias-batch-progress.jsonl');
|
|
1179
1340
|
const aliasExchangeProgressPath = path.join(planDir, 'alias-exchange-progress.jsonl');
|
|
@@ -1197,7 +1358,12 @@ export async function runDatasetMaintenanceApply(options) {
|
|
|
1197
1358
|
exitCode: 2,
|
|
1198
1359
|
});
|
|
1199
1360
|
}
|
|
1200
|
-
const
|
|
1361
|
+
const derivativeProgress = plan.operation === 'rebuild-derivatives'
|
|
1362
|
+
? parseDerivativeSubmitProgress(plan, progressPath)
|
|
1363
|
+
: { entries: [], latest: null };
|
|
1364
|
+
const progress = plan.operation === 'rebuild-derivatives'
|
|
1365
|
+
? { entries: [], successes: new Map(), latestFailures: new Map() }
|
|
1366
|
+
: parseProgress(plan, progressPath);
|
|
1201
1367
|
let resumedSuccesses = progress.successes.size;
|
|
1202
1368
|
const aliasPlanProgress = plan.operation === 'merge-support-aliases'
|
|
1203
1369
|
? parseAliasPlanProgress(plan, aliasPlanProgressPath)
|
|
@@ -1220,8 +1386,9 @@ export async function runDatasetMaintenanceApply(options) {
|
|
|
1220
1386
|
await assertAliasSupportSnapshots({ plan, context });
|
|
1221
1387
|
}
|
|
1222
1388
|
const approvalPath = path.join(planDir, 'approval-record.json');
|
|
1389
|
+
const approvalAlreadyExisted = existsSync(approvalPath);
|
|
1223
1390
|
validateApprovalRecord({ path: approvalPath, plan, context });
|
|
1224
|
-
if (!
|
|
1391
|
+
if (!approvalAlreadyExisted) {
|
|
1225
1392
|
writeImmutableJson(approvalPath, {
|
|
1226
1393
|
schema_version: 1,
|
|
1227
1394
|
approved_at_utc: clock(options),
|
|
@@ -1243,6 +1410,100 @@ export async function runDatasetMaintenanceApply(options) {
|
|
|
1243
1410
|
: null,
|
|
1244
1411
|
});
|
|
1245
1412
|
}
|
|
1413
|
+
if (plan.operation === 'rebuild-derivatives') {
|
|
1414
|
+
const startedAt = clock(options);
|
|
1415
|
+
const derivativeAttemptPath = path.join(planDir, 'derivative-admission-attempt.json');
|
|
1416
|
+
const derivativeAttempt = validateDerivativeAdmissionAttempt({
|
|
1417
|
+
path: derivativeAttemptPath,
|
|
1418
|
+
plan,
|
|
1419
|
+
context,
|
|
1420
|
+
});
|
|
1421
|
+
const proof = await executeDerivativeAdmission({
|
|
1422
|
+
plan,
|
|
1423
|
+
context,
|
|
1424
|
+
replayPossible: derivativeAttempt !== null,
|
|
1425
|
+
attemptPath: derivativeAttemptPath,
|
|
1426
|
+
preparedAtUtc: startedAt,
|
|
1427
|
+
});
|
|
1428
|
+
if (derivativeProgress.latest &&
|
|
1429
|
+
derivativeProofIdentity(derivativeProgress.latest.proof) !==
|
|
1430
|
+
derivativeProofIdentity(proof)) {
|
|
1431
|
+
throw new CliError('Derivative guarded-RPC replay returned a different request proof.', {
|
|
1432
|
+
code: 'DATASET_MAINTENANCE_DERIVATIVE_REPLAY_MISMATCH',
|
|
1433
|
+
exitCode: 1,
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
const action = derivativePlanAction(plan);
|
|
1437
|
+
const entry = {
|
|
1438
|
+
schema_version: 1,
|
|
1439
|
+
plan_sha256: plan.plan_sha256,
|
|
1440
|
+
operation_id: plan.operation_id,
|
|
1441
|
+
action_id: action.action_id,
|
|
1442
|
+
target_mode: 'owner_draft',
|
|
1443
|
+
actor: { user_id: context.account.user_id, email: context.account.email },
|
|
1444
|
+
started_at_utc: startedAt,
|
|
1445
|
+
ended_at_utc: clock(options),
|
|
1446
|
+
result: 'accepted',
|
|
1447
|
+
proof,
|
|
1448
|
+
};
|
|
1449
|
+
appendStableJsonLine(progressPath, entry);
|
|
1450
|
+
const attemptPath = nextAttemptPath(planDir);
|
|
1451
|
+
const report = {
|
|
1452
|
+
schema_version: 1,
|
|
1453
|
+
generated_at_utc: clock(options),
|
|
1454
|
+
status: 'accepted',
|
|
1455
|
+
task_id: plan.task_id,
|
|
1456
|
+
operation: plan.operation,
|
|
1457
|
+
operation_id: plan.operation_id,
|
|
1458
|
+
target_mode: plan.target_mode,
|
|
1459
|
+
plan_sha256: plan.plan_sha256,
|
|
1460
|
+
actor: { user_id: context.account.user_id, email: context.account.email },
|
|
1461
|
+
summary: {
|
|
1462
|
+
actions: 1,
|
|
1463
|
+
success: 0,
|
|
1464
|
+
failed: 0,
|
|
1465
|
+
pending: 1,
|
|
1466
|
+
resumed_successes: 0,
|
|
1467
|
+
accepted: 1,
|
|
1468
|
+
},
|
|
1469
|
+
actions: [
|
|
1470
|
+
{
|
|
1471
|
+
action_id: action.action_id,
|
|
1472
|
+
action: action.action,
|
|
1473
|
+
table: action.table,
|
|
1474
|
+
id: action.id,
|
|
1475
|
+
version: action.version,
|
|
1476
|
+
status: 'accepted',
|
|
1477
|
+
error: null,
|
|
1478
|
+
},
|
|
1479
|
+
],
|
|
1480
|
+
artifacts: {
|
|
1481
|
+
approval_record: approvalPath,
|
|
1482
|
+
apply_progress: progressPath,
|
|
1483
|
+
derivative_submit_progress: progressPath,
|
|
1484
|
+
derivative_admission_attempt: derivativeAttemptPath,
|
|
1485
|
+
commit_report: path.join(planDir, 'commit-report.json'),
|
|
1486
|
+
attempt_report: attemptPath,
|
|
1487
|
+
},
|
|
1488
|
+
database_audit: {
|
|
1489
|
+
rpc_transaction_log: 'public.command_audit_log',
|
|
1490
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
1491
|
+
correlation_fields: [
|
|
1492
|
+
'plan_sha256',
|
|
1493
|
+
'operation_id',
|
|
1494
|
+
'action_id',
|
|
1495
|
+
'target_visibility',
|
|
1496
|
+
'plan_request_sha256',
|
|
1497
|
+
'action_request_sha256',
|
|
1498
|
+
'request_id',
|
|
1499
|
+
],
|
|
1500
|
+
},
|
|
1501
|
+
derivative_admission: { ...proof, admission: 'accepted' },
|
|
1502
|
+
};
|
|
1503
|
+
writeImmutableJson(attemptPath, report);
|
|
1504
|
+
writeJsonArtifact(report.artifacts.commit_report, report);
|
|
1505
|
+
return report;
|
|
1506
|
+
}
|
|
1246
1507
|
if (plan.operation === 'merge-support-aliases') {
|
|
1247
1508
|
const alreadyComplete = Boolean(aliasPlanProgress.success &&
|
|
1248
1509
|
aliasPlanDerivedLogsComplete({
|
|
@@ -1381,6 +1642,7 @@ export async function runDatasetMaintenanceApply(options) {
|
|
|
1381
1642
|
const rank = {
|
|
1382
1643
|
save_draft: 0,
|
|
1383
1644
|
update_json_ordered: 0,
|
|
1645
|
+
rebuild_derivatives: 0,
|
|
1384
1646
|
delete: 1,
|
|
1385
1647
|
};
|
|
1386
1648
|
const actionOrder = rank[left.action] - rank[right.action];
|
|
@@ -1525,11 +1787,14 @@ export const __testInternals = {
|
|
|
1525
1787
|
buildAliasPlanRequest,
|
|
1526
1788
|
clock,
|
|
1527
1789
|
errorMessage,
|
|
1790
|
+
executeDerivativeAdmission,
|
|
1528
1791
|
executeAliasPlan,
|
|
1529
1792
|
executeAction,
|
|
1530
1793
|
finalProjectedRows,
|
|
1531
1794
|
loadDesiredPayload,
|
|
1532
1795
|
nextAttemptPath,
|
|
1796
|
+
parseDerivativeSubmitProgress,
|
|
1797
|
+
validateDerivativeAdmissionAttempt,
|
|
1533
1798
|
parseAliasBatchProgress,
|
|
1534
1799
|
parseAliasPlanProgress,
|
|
1535
1800
|
parseProgress,
|