@ontos-ai/knowhere-sdk 2.6.1-beta.20261027152313 → 2.7.0

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/dist/index.js CHANGED
@@ -258,9 +258,7 @@ var init_parsed_document_archive_error = __esm({
258
258
  code = "ARCHIVE_SPOOL_FAILED";
259
259
  cause;
260
260
  constructor(cause) {
261
- super(
262
- "Parsed result ZIP temporary storage is unavailable. Range-enabled object storage avoids temporary disk usage."
263
- );
261
+ super("Parsed result ZIP temporary storage is unavailable.");
264
262
  this.name = "ParsedDocumentArchiveError";
265
263
  this.cause = cause;
266
264
  }
@@ -856,17 +854,15 @@ function readObjectVersion(object) {
856
854
  return { entityTag: object.entityTag, contentLength: object.contentLength };
857
855
  }
858
856
  function validateCachedResponse(response, marker, signal) {
859
- if (response.status !== 206 || !response.body)
857
+ if (response.headers.get("etag") !== marker.storageEntityTag)
860
858
  rejectResponse(
861
859
  response,
862
- new ArchiveCacheTransportError(
863
- `Parsed result archive cache requires a ranged response (HTTP ${response.status}).`
864
- )
860
+ new ArchiveCacheTransportError("Parsed result archive cache response version changed.")
865
861
  );
866
- if (response.headers.get("etag") !== marker.storageEntityTag)
862
+ if (response.status !== 206 || !response.body)
867
863
  rejectResponse(
868
864
  response,
869
- new ArchiveCacheTransportError("Parsed result archive cache response version changed.")
865
+ new ArchiveCacheTransportError("Parsed result archive cache requires a ranged response.")
870
866
  );
871
867
  const range = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(
872
868
  response.headers.get("content-range") ?? ""
@@ -949,7 +945,10 @@ __export(sequential_parsed_document_archive_exports, {
949
945
  openSequentialArchive: () => openSequentialArchive
950
946
  });
951
947
  async function openSequentialArchive(params, identity, validateResponse) {
952
- let response = await fetch(params.resultUrl, { signal: params.signal });
948
+ let response = await fetch(params.resultUrl, {
949
+ signal: params.signal,
950
+ headers: validateResponse ? { Range: "bytes=0-" } : void 0
951
+ });
953
952
  if (!response.ok || !response.body)
954
953
  throw new Error(`Parsed result download failed (HTTP ${response.status}).`);
955
954
  if (validateResponse) response = validateResponse(response);
@@ -978,9 +977,11 @@ async function openSequentialArchive(params, identity, validateResponse) {
978
977
  const parser = (0, import_parse.default)({ forceStream: true });
979
978
  const checksums = /* @__PURE__ */ new Map();
980
979
  let archiveSize = 0;
980
+ const archiveHash = (0, import_crypto2.createHash)("sha256");
981
981
  const spool = new import_stream.Transform({
982
982
  transform(chunk, _encoding, callback) {
983
983
  archiveSize += chunk.length;
984
+ archiveHash.update(chunk);
984
985
  void file.writeFile(chunk).then(
985
986
  () => {
986
987
  callback(null, chunk);
@@ -997,6 +998,23 @@ async function openSequentialArchive(params, identity, validateResponse) {
997
998
  identity,
998
999
  downloadIdentity: identity,
999
1000
  canSkipEntries: false,
1001
+ readArchive: () => {
1002
+ let position = 0;
1003
+ return new ReadableStream({
1004
+ async pull(controller) {
1005
+ params.signal?.throwIfAborted();
1006
+ if (position >= archiveSize) {
1007
+ controller.close();
1008
+ return;
1009
+ }
1010
+ const body2 = new Uint8Array(Math.min(65536, archiveSize - position));
1011
+ const { bytesRead } = await file.read(body2, 0, body2.length, position);
1012
+ if (bytesRead !== body2.length) throw new Error("Truncated verified archive spool.");
1013
+ position += bytesRead;
1014
+ controller.enqueue(body2);
1015
+ }
1016
+ });
1017
+ },
1000
1018
  entries: (async function* () {
1001
1019
  for await (const value of parser) {
1002
1020
  const entry = value;
@@ -1027,6 +1045,8 @@ async function openSequentialArchive(params, identity, validateResponse) {
1027
1045
  };
1028
1046
  }
1029
1047
  await completed;
1048
+ if (params.snapshot && archiveHash.digest("hex") !== params.snapshot.checksum)
1049
+ throw new Error("Parsed snapshot archive checksum mismatch for extracted bytes.");
1030
1050
  const reader = new import_zip.ZipReader(new DiskArchiveReader(file, archiveSize), {
1031
1051
  useWebWorkers: false
1032
1052
  });
@@ -1058,10 +1078,11 @@ async function openSequentialArchive(params, identity, validateResponse) {
1058
1078
  }
1059
1079
  };
1060
1080
  }
1061
- var import_stream, import_promises, import_promises2, import_os, import_path2, import_zlib, import_zip, import_parse, DiskArchiveReader;
1081
+ var import_crypto2, import_stream, import_promises, import_promises2, import_os, import_path2, import_zlib, import_zip, import_parse, DiskArchiveReader;
1062
1082
  var init_sequential_parsed_document_archive = __esm({
1063
1083
  "src/storage/sequential-parsed-document-archive.ts"() {
1064
1084
  "use strict";
1085
+ import_crypto2 = require("crypto");
1065
1086
  import_stream = require("stream");
1066
1087
  import_promises = require("stream/promises");
1067
1088
  import_promises2 = require("fs/promises");
@@ -1101,14 +1122,10 @@ async function syncRemoteResultArchive(params) {
1101
1122
  const checkpointIntervalMs = params.limits?.checkpointIntervalMs ?? 2e3;
1102
1123
  const controller = new AbortController();
1103
1124
  const signal = params.signal ? AbortSignal.any([params.signal, controller.signal]) : controller.signal;
1104
- const openingParams = { ...params, signal };
1125
+ const transferParams = { ...params, signal };
1105
1126
  const previous = await params.storage.readSyncProgress(params);
1106
- const openedSource = await openArchive(openingParams, previous);
1127
+ const openedSource = await openArchive(transferParams, previous);
1107
1128
  const source = !openedSource.canSkipEntries && previous?.archiveIdentity && previous.archiveDownloadIdentity === openedSource.downloadIdentity ? { ...openedSource, identity: previous.archiveIdentity } : openedSource;
1108
- const transferParams = {
1109
- ...openingParams,
1110
- signal: source.signal ?? signal
1111
- };
1112
1129
  const archiveDownloadIdentity = source.isCached ? source.downloadIdentity : previous?.archiveDownloadIdentity;
1113
1130
  const concurrency = source.canSkipEntries ? params.limits?.maxConcurrentFiles ?? 1 : 1;
1114
1131
  const nextEntry = previous?.status === "completed" ? 0 : previous?.nextArchiveEntry ?? 0;
@@ -1135,6 +1152,8 @@ async function syncRemoteResultArchive(params) {
1135
1152
  documentId: params.documentId,
1136
1153
  revisionKey: params.revisionKey,
1137
1154
  pinnedRevisionKey: params.revisionKey,
1155
+ snapshotChecksum: params.snapshot?.checksum,
1156
+ upstreamJobResultId: params.snapshot?.jobResultId,
1138
1157
  archiveIdentity: source.identity,
1139
1158
  archiveDownloadIdentity,
1140
1159
  archiveCacheBypassed,
@@ -1173,10 +1192,38 @@ async function syncRemoteResultArchive(params) {
1173
1192
  signal.throwIfAborted();
1174
1193
  }
1175
1194
  try {
1176
- if (previous?.archiveIdentity && previous.archiveIdentity !== source.identity)
1195
+ if (previous?.snapshotChecksum && previous.snapshotChecksum !== params.snapshot?.checksum)
1196
+ throw new Error("Parsed snapshot checksum changed within one generation.");
1197
+ if (previous?.upstreamJobResultId && previous.upstreamJobResultId !== params.snapshot?.jobResultId)
1198
+ throw new Error("Parsed snapshot upstream revision changed within one generation.");
1199
+ if (previous?.archiveIdentity && previous.archiveIdentity !== source.identity) {
1177
1200
  throw new Error("Parsed result archive identity changed; start a new revision.");
1178
- if (previous?.pinnedRevisionKey && previous.pinnedRevisionKey !== params.revisionKey)
1201
+ }
1202
+ if (previous?.pinnedRevisionKey && previous.pinnedRevisionKey !== params.revisionKey) {
1179
1203
  throw new Error("Parsed result revision changed while resuming archive sync.");
1204
+ }
1205
+ if (params.snapshot && previous?.status === "completed") {
1206
+ const committed = await readParsedDocumentCommit(params);
1207
+ if (committed) {
1208
+ if (source.verifyArchiveChecksum) {
1209
+ await source.verifyArchiveChecksum(previous.archiveEntryHashes ?? []);
1210
+ } else {
1211
+ await verifyArchiveChecksum(
1212
+ params,
1213
+ source.downloadIdentity,
1214
+ previous.archiveEntryHashes ?? [],
1215
+ params.retainResultArchive
1216
+ );
1217
+ }
1218
+ return {
1219
+ documentId: params.documentId,
1220
+ revisionKey: params.revisionKey,
1221
+ snapshot: params.snapshot,
1222
+ completed: true,
1223
+ commit: committed
1224
+ };
1225
+ }
1226
+ }
1180
1227
  for await (const entry of source.entries) {
1181
1228
  if (failure !== void 0) throw failure;
1182
1229
  signal.throwIfAborted();
@@ -1194,7 +1241,12 @@ async function syncRemoteResultArchive(params) {
1194
1241
  if (startedEntries > 0 && (startedEntries >= maxEntries || Date.now() - startedAt >= deadlineMs)) {
1195
1242
  await drainEntries();
1196
1243
  await flushProgress(true);
1197
- return { documentId: params.documentId, revisionKey: params.revisionKey, completed: false };
1244
+ return {
1245
+ documentId: params.documentId,
1246
+ revisionKey: params.revisionKey,
1247
+ snapshot: params.snapshot,
1248
+ completed: false
1249
+ };
1198
1250
  }
1199
1251
  if (successfulEntries >= checkpointFileInterval || Date.now() - checkpointAt >= checkpointIntervalMs) {
1200
1252
  await drainEntries();
@@ -1208,9 +1260,16 @@ async function syncRemoteResultArchive(params) {
1208
1260
  await drainEntries();
1209
1261
  await flushProgress(true);
1210
1262
  await validateRequiredObjects(params, paths);
1263
+ if (params.snapshot && source.canSkipEntries)
1264
+ if (source.verifyArchiveChecksum) await source.verifyArchiveChecksum(entryHashes);
1265
+ else await verifyArchiveChecksum(params, source.downloadIdentity, entryHashes);
1211
1266
  await params.verifyRevision?.();
1212
1267
  signal.throwIfAborted();
1213
1268
  const commit = {
1269
+ snapshotId: params.snapshot?.snapshotId,
1270
+ jobResultId: params.snapshot?.jobResultId,
1271
+ checksum: params.snapshot?.checksum,
1272
+ stage: params.snapshot?.stage,
1214
1273
  version: 1,
1215
1274
  documentId: params.documentId,
1216
1275
  revisionKey: params.revisionKey,
@@ -1229,6 +1288,8 @@ async function syncRemoteResultArchive(params) {
1229
1288
  documentId: params.documentId,
1230
1289
  revisionKey: params.revisionKey,
1231
1290
  pinnedRevisionKey: params.revisionKey,
1291
+ snapshotChecksum: params.snapshot?.checksum,
1292
+ upstreamJobResultId: params.snapshot?.jobResultId,
1232
1293
  archiveIdentity: source.identity,
1233
1294
  archiveDownloadIdentity,
1234
1295
  archiveCacheBypassed,
@@ -1249,6 +1310,7 @@ async function syncRemoteResultArchive(params) {
1249
1310
  documentId: params.documentId,
1250
1311
  revisionKey: params.revisionKey,
1251
1312
  completed: true,
1313
+ snapshot: params.snapshot,
1252
1314
  commit
1253
1315
  };
1254
1316
  } catch (error) {
@@ -1264,6 +1326,8 @@ async function syncRemoteResultArchive(params) {
1264
1326
  documentId: params.documentId,
1265
1327
  revisionKey: params.revisionKey,
1266
1328
  pinnedRevisionKey: params.revisionKey,
1329
+ snapshotChecksum: params.snapshot?.checksum,
1330
+ upstreamJobResultId: params.snapshot?.jobResultId,
1267
1331
  archiveIdentity: source.identity,
1268
1332
  archiveDownloadIdentity,
1269
1333
  archiveEntryHashes: entryHashes.slice(0, checkpointEntry),
@@ -1305,7 +1369,7 @@ function validateArchiveSyncLimits(limits) {
1305
1369
  }
1306
1370
  }
1307
1371
  async function copyEntry(params, entry, isVerification = false) {
1308
- const hash = (0, import_crypto2.createHash)("sha256");
1372
+ const hash = (0, import_crypto3.createHash)("sha256");
1309
1373
  const body = entry.read().pipeThrough(
1310
1374
  new TransformStream({
1311
1375
  transform(chunk, controller) {
@@ -1328,7 +1392,7 @@ async function copyEntry(params, entry, isVerification = false) {
1328
1392
  signal: params.signal
1329
1393
  });
1330
1394
  await entry.waitForRead?.();
1331
- return (0, import_crypto2.createHash)("sha256").update(entry.path).update(hash.digest()).digest("hex");
1395
+ return (0, import_crypto3.createHash)("sha256").update(entry.path).update(hash.digest()).digest("hex");
1332
1396
  }
1333
1397
  async function openArchive(params, previous) {
1334
1398
  const prepared = params.cacheResultArchive ? await prepareCachedArchive({
@@ -1353,6 +1417,8 @@ async function persistArchiveCacheBypass(params, previous) {
1353
1417
  documentId: params.documentId,
1354
1418
  revisionKey: params.revisionKey,
1355
1419
  pinnedRevisionKey: params.revisionKey,
1420
+ snapshotChecksum: params.snapshot?.checksum,
1421
+ upstreamJobResultId: params.snapshot?.jobResultId,
1356
1422
  archiveDownloadIdentity: previous?.archiveDownloadIdentity,
1357
1423
  archiveCacheBypassed: true,
1358
1424
  status: "running",
@@ -1385,43 +1451,32 @@ async function openArchiveTransport(params, previous, prepared) {
1385
1451
  );
1386
1452
  return { ...source, archiveCacheBypassed };
1387
1453
  }
1388
- const rangeController = new AbortController();
1389
- const rangeSignal = params.signal ? AbortSignal.any([params.signal, rangeController.signal]) : rangeController.signal;
1390
1454
  const options = {
1391
1455
  forceRangeRequests: true,
1392
1456
  preventHeadRequest: true,
1393
1457
  // Cached reads already pin a strong ETag and exact lengths; CDN timestamps can drift.
1394
1458
  checkResourceChanges: !prepared?.isCached,
1395
1459
  fetch: async (input, init) => {
1460
+ let response;
1396
1461
  try {
1397
- let response;
1398
- try {
1399
- response = await fetch(input, { ...init, signal: rangeSignal });
1400
- } catch (error) {
1401
- rangeSignal.throwIfAborted();
1402
- if (prepared?.isCached)
1403
- throw new ArchiveCacheTransportError(
1404
- "Parsed result archive cache transport unavailable."
1405
- );
1406
- throw error;
1407
- }
1408
- if (prepared?.isCached) return prepared.validateResponse(response);
1409
- if (response.status !== 206) {
1410
- const error = new Error(
1411
- `Parsed result Range request failed (HTTP ${response.status}).`
1412
- );
1413
- rangeController.abort(error);
1414
- await response.body?.cancel(error).catch(() => void 0);
1415
- throw error;
1416
- }
1417
- return prepared ? prepared.validateResponse(response) : response;
1462
+ response = await fetch(input, { ...init, signal: params.signal });
1418
1463
  } catch (error) {
1419
- rangeController.abort(error);
1464
+ params.signal?.throwIfAborted();
1465
+ if (prepared?.isCached)
1466
+ throw new ArchiveCacheTransportError(
1467
+ "Parsed result archive cache transport unavailable."
1468
+ );
1420
1469
  throw error;
1421
1470
  }
1471
+ if (prepared?.isCached) return prepared.validateResponse(response);
1472
+ if (response.status !== 206) {
1473
+ await response.body?.cancel();
1474
+ throw new Error(`Parsed result Range request failed (HTTP ${response.status}).`);
1475
+ }
1476
+ return prepared ? prepared.validateResponse(response) : response;
1422
1477
  }
1423
1478
  };
1424
- const reader = new import_zip2.ZipReader(new AbortableHttpRangeReader(resultUrl, options, rangeController), {
1479
+ const reader = new import_zip2.ZipReader(new import_zip2.HttpRangeReader(resultUrl, options), {
1425
1480
  useWebWorkers: false,
1426
1481
  checkCrc32: true
1427
1482
  });
@@ -1431,17 +1486,23 @@ async function openArchiveTransport(params, previous, prepared) {
1431
1486
  const files = entries.filter(
1432
1487
  (entry) => !entry.directory && isSnapshotPath(entry.filename)
1433
1488
  );
1434
- const archiveIdentity = (0, import_crypto2.createHash)("sha256").update(identity).update(
1489
+ const archiveIdentity = (0, import_crypto3.createHash)("sha256").update(identity).update(
1435
1490
  JSON.stringify(
1436
1491
  files.map((entry) => [entry.filename, entry.uncompressedSize, entry.signature])
1437
1492
  )
1438
1493
  ).digest("hex");
1439
1494
  return {
1440
- signal: rangeSignal,
1441
1495
  identity: previous?.archiveIdentity === identity ? identity : archiveIdentity,
1442
1496
  downloadIdentity: identity,
1443
1497
  archiveCacheBypassed,
1444
1498
  isCached: prepared?.isCached,
1499
+ verifyArchiveChecksum: prepared?.isCached ? (entryHashes) => verifyArchiveChecksum(
1500
+ { ...params, resultUrl },
1501
+ identity,
1502
+ entryHashes,
1503
+ false,
1504
+ prepared.validateResponse
1505
+ ) : void 0,
1445
1506
  canSkipEntries: previous?.archiveIdentity !== identity,
1446
1507
  entries: (function* () {
1447
1508
  for (const entry of files) {
@@ -1450,7 +1511,7 @@ async function openArchiveTransport(params, previous, prepared) {
1450
1511
  path: normalizeResultPath(entry.filename),
1451
1512
  contentLength: entry.uncompressedSize,
1452
1513
  read: () => {
1453
- entryRead = readRangeEntry(entry, rangeSignal, producers);
1514
+ entryRead = readRangeEntry(entry, params.signal, producers);
1454
1515
  return entryRead.body;
1455
1516
  },
1456
1517
  waitForRead: () => entryRead?.completed ?? Promise.resolve()
@@ -1463,7 +1524,6 @@ async function openArchiveTransport(params, previous, prepared) {
1463
1524
  }
1464
1525
  };
1465
1526
  } catch (error) {
1466
- rangeController.abort(error);
1467
1527
  await reader.close();
1468
1528
  throw error;
1469
1529
  }
@@ -1475,16 +1535,10 @@ function readRangeEntry(entry, signal, producers) {
1475
1535
  streamController = controller;
1476
1536
  }
1477
1537
  });
1478
- const cancelOutput = () => {
1479
- streamController?.error(signal?.reason);
1480
- };
1481
- signal?.addEventListener("abort", cancelOutput, { once: true });
1482
- if (signal?.aborted) cancelOutput();
1483
1538
  const completed = entry.getData(stream.writable, { signal, checkCrc32: true, useWebWorkers: false }).then(() => void 0);
1484
1539
  const producer = completed.catch((error) => {
1485
1540
  streamController?.error(error);
1486
1541
  }).finally(() => {
1487
- signal?.removeEventListener("abort", cancelOutput);
1488
1542
  producers.delete(producer);
1489
1543
  });
1490
1544
  producers.add(producer);
@@ -1521,6 +1575,9 @@ async function validateRequiredObjects(params, paths) {
1521
1575
  const manifestJobId = manifest.job_id ?? manifest.jobId;
1522
1576
  if (manifestJobId !== params.jobId)
1523
1577
  throw new Error("Parsed result manifest job identity differs from the pinned job.");
1578
+ const manifestRevision = manifest.job_result_id ?? manifest.jobResultId;
1579
+ if (params.snapshot && manifestRevision !== void 0 && manifestRevision !== params.snapshot.jobResultId)
1580
+ throw new Error("Parsed result manifest revision differs from the snapshot upstream revision.");
1524
1581
  const chunks = Array.isArray(payload) ? payload : isRecord2(payload) ? payload.chunks : void 0;
1525
1582
  if (!Array.isArray(chunks)) throw new Error("Invalid parsed result chunks.json.");
1526
1583
  if (isRecord2(manifest.files)) {
@@ -1545,7 +1602,49 @@ async function validateRequiredObjects(params, paths) {
1545
1602
  for (const path4 of required)
1546
1603
  if (!paths.has(path4)) throw new Error(`Required parsed result file missing: ${path4}`);
1547
1604
  }
1605
+ async function verifyArchiveChecksum(params, identity, expectedEntryHashes, retainResultArchive = false, validateResponse) {
1606
+ const { openSequentialArchive: openSequentialArchive2 } = await Promise.resolve().then(() => (init_sequential_parsed_document_archive(), sequential_parsed_document_archive_exports));
1607
+ const source = await openSequentialArchive2(params, identity, validateResponse);
1608
+ let entryIndex = 0;
1609
+ try {
1610
+ for await (const entry of source.entries) {
1611
+ const hash = (0, import_crypto3.createHash)("sha256");
1612
+ for await (const part of entry.read()) {
1613
+ params.signal?.throwIfAborted();
1614
+ hash.update(part);
1615
+ }
1616
+ const entryHash = (0, import_crypto3.createHash)("sha256").update(entry.path).update(hash.digest()).digest("hex");
1617
+ if (expectedEntryHashes[entryIndex] !== entryHash)
1618
+ throw new Error(
1619
+ "Parsed snapshot archive entry changed between extraction and checksum verification."
1620
+ );
1621
+ entryIndex += 1;
1622
+ }
1623
+ if (entryIndex !== expectedEntryHashes.length)
1624
+ throw new Error("Parsed snapshot archive entry count changed during checksum verification.");
1625
+ if (retainResultArchive && source.readArchive) {
1626
+ try {
1627
+ await writeStream(params.storage, {
1628
+ documentId: params.documentId,
1629
+ revisionKey: params.revisionKey,
1630
+ path: "result.zip",
1631
+ body: source.readArchive(),
1632
+ contentType: "application/zip",
1633
+ signal: params.signal
1634
+ });
1635
+ } catch {
1636
+ }
1637
+ }
1638
+ } finally {
1639
+ await source.close();
1640
+ }
1641
+ }
1548
1642
  async function retainArchive(params, expectedIdentity) {
1643
+ if (params.snapshot) {
1644
+ const progress = await params.storage.readSyncProgress(params);
1645
+ await verifyArchiveChecksum(params, expectedIdentity, progress?.archiveEntryHashes ?? [], true);
1646
+ return;
1647
+ }
1549
1648
  const response = await fetch(params.resultUrl, { signal: params.signal });
1550
1649
  if (!response.ok || !response.body)
1551
1650
  throw new Error(`Optional archive download failed (HTTP ${response.status}).`);
@@ -1570,7 +1669,7 @@ function isRecord2(value) {
1570
1669
  return typeof value === "object" && value !== null && !Array.isArray(value);
1571
1670
  }
1572
1671
  function createDownloadIdentity(jobId, response) {
1573
- return (0, import_crypto2.createHash)("sha256").update(
1672
+ return (0, import_crypto3.createHash)("sha256").update(
1574
1673
  JSON.stringify([
1575
1674
  jobId,
1576
1675
  response.headers.get("etag"),
@@ -1579,60 +1678,16 @@ function createDownloadIdentity(jobId, response) {
1579
1678
  ])
1580
1679
  ).digest("hex");
1581
1680
  }
1582
- var import_crypto2, import_zip2, AbortableHttpRangeReader;
1681
+ var import_crypto3, import_zip2;
1583
1682
  var init_streamed_parsed_document_sync = __esm({
1584
1683
  "src/storage/streamed-parsed-document-sync.ts"() {
1585
1684
  "use strict";
1586
- import_crypto2 = require("crypto");
1685
+ import_crypto3 = require("crypto");
1587
1686
  init_errors();
1588
1687
  init_archive_cache_transport_error();
1589
1688
  init_parsed_document_archive_cache();
1590
1689
  import_zip2 = require("@zip.js/zip.js");
1591
1690
  init_parsed_document_storage();
1592
- AbortableHttpRangeReader = class extends import_zip2.HttpRangeReader {
1593
- constructor(url, options, controller) {
1594
- super(url, options);
1595
- this.controller = controller;
1596
- }
1597
- controller;
1598
- createReadable(options) {
1599
- const reader = super.createReadable(options).getReader();
1600
- const cancellation = this.controller;
1601
- return new ReadableStream(
1602
- {
1603
- async pull(output) {
1604
- try {
1605
- const chunk = await reader.read();
1606
- if (chunk.done) {
1607
- reader.releaseLock();
1608
- output.close();
1609
- } else output.enqueue(chunk.value);
1610
- } catch (error) {
1611
- cancellation.abort(error);
1612
- reader.releaseLock();
1613
- output.error(error);
1614
- }
1615
- },
1616
- async cancel(reason) {
1617
- try {
1618
- await reader.cancel(reason);
1619
- } finally {
1620
- reader.releaseLock();
1621
- }
1622
- }
1623
- },
1624
- { highWaterMark: 0 }
1625
- );
1626
- }
1627
- async readUint8Array(index, length) {
1628
- try {
1629
- return await super.readUint8Array(index, length);
1630
- } catch (error) {
1631
- this.controller.abort(error);
1632
- throw error;
1633
- }
1634
- }
1635
- };
1636
1691
  }
1637
1692
  });
1638
1693
 
@@ -2707,6 +2762,12 @@ var Jobs = class extends BaseResource {
2707
2762
  enrichJobResult(jobResult);
2708
2763
  return jobResult;
2709
2764
  }
2765
+ /** Renew unfinished enhancement without creating another ingestion. */
2766
+ async retryEnhancement(jobId) {
2767
+ return this.httpClient.post(
2768
+ this.endpoint(`/jobs/${jobId}/enrichment/retry`)
2769
+ );
2770
+ }
2710
2771
  /**
2711
2772
  * Upload file for a job
2712
2773
  */
@@ -2917,7 +2978,7 @@ var Documents = class extends BaseResource {
2917
2978
  };
2918
2979
 
2919
2980
  // src/knowledge/knowledge.ts
2920
- var import_crypto4 = require("crypto");
2981
+ var import_crypto5 = require("crypto");
2921
2982
  init_errors();
2922
2983
 
2923
2984
  // src/storage/asset-storage.ts
@@ -3188,7 +3249,7 @@ init_utils();
3188
3249
  init_streamed_parsed_document_sync();
3189
3250
 
3190
3251
  // src/knowledge/local-store.ts
3191
- var import_crypto3 = require("crypto");
3252
+ var import_crypto4 = require("crypto");
3192
3253
  var import_os2 = __toESM(require("os"));
3193
3254
  var import_fs3 = require("fs");
3194
3255
  var import_path3 = __toESM(require("path"));
@@ -3372,7 +3433,7 @@ function isPathInsideDirectory(targetPath, parentDirectory) {
3372
3433
  return relativePath.length === 0 || !relativePath.startsWith("..") && !import_path3.default.isAbsolute(relativePath);
3373
3434
  }
3374
3435
  function createLocalDocumentId(result) {
3375
- const hash = (0, import_crypto3.createHash)("sha256").update(result.jobId).update("\0").update(result.manifest.sourceFileName).digest("hex").slice(0, 16);
3436
+ const hash = (0, import_crypto4.createHash)("sha256").update(result.jobId).update("\0").update(result.manifest.sourceFileName).digest("hex").slice(0, 16);
3376
3437
  return `local_${hash}`;
3377
3438
  }
3378
3439
  function countChunkTypes(result) {
@@ -3409,6 +3470,21 @@ function isMissingFileError2(error) {
3409
3470
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
3410
3471
  }
3411
3472
 
3473
+ // src/knowledge/serialize-snapshot-publication.ts
3474
+ var pendingPublications = /* @__PURE__ */ new WeakMap();
3475
+ async function serializeSnapshotPublication(storage, documentId, publish) {
3476
+ const publications = pendingPublications.get(storage) ?? /* @__PURE__ */ new Map();
3477
+ pendingPublications.set(storage, publications);
3478
+ const previous = publications.get(documentId) ?? Promise.resolve();
3479
+ const current = previous.catch(() => void 0).then(publish);
3480
+ publications.set(documentId, current);
3481
+ try {
3482
+ return await current;
3483
+ } finally {
3484
+ if (publications.get(documentId) === current) publications.delete(documentId);
3485
+ }
3486
+ }
3487
+
3412
3488
  // src/knowledge/knowledge.ts
3413
3489
  var DEFAULT_READ_LIMIT = 12;
3414
3490
  var MAX_READ_LIMIT = 40;
@@ -3526,11 +3602,37 @@ var Knowledge = class _Knowledge {
3526
3602
  return this.importJobResult(params);
3527
3603
  }
3528
3604
  async syncParsedDocument(params) {
3605
+ const response = await this.performParsedDocumentSync(params);
3606
+ if (response.completed && !response.snapshot) {
3607
+ const storage = this.requireParsedStorageConfig().storage;
3608
+ await serializeSnapshotPublication(storage, response.documentId, async () => {
3609
+ if (params.documentId || params.jobId) {
3610
+ const current = await this.client.documents.listChunks(
3611
+ response.documentId,
3612
+ { page: 1, pageSize: 1 }
3613
+ );
3614
+ if (params.jobId && current.jobId !== params.jobId) return;
3615
+ if (!params.jobId && current.jobResultId !== response.revisionKey) return;
3616
+ }
3617
+ await storage.writeObject({
3618
+ documentId: response.documentId,
3619
+ revisionKey: "current",
3620
+ path: ".knowhere-sdk/current-snapshot.json",
3621
+ body: Buffer.from(
3622
+ JSON.stringify({ snapshotId: response.revisionKey, jobResultId: response.revisionKey })
3623
+ ),
3624
+ contentType: "application/json; charset=utf-8"
3625
+ });
3626
+ });
3627
+ }
3628
+ return response;
3629
+ }
3630
+ async performParsedDocumentSync(params) {
3529
3631
  params.signal?.throwIfAborted();
3530
3632
  const normalized = normalizeDocumentReference(params);
3531
3633
  const parsedStorageConfig = this.requireParsedStorageConfig();
3532
3634
  validateArchiveSyncLimits(parsedStorageConfig.limits);
3533
- if (normalized.documentId) {
3635
+ if (normalized.documentId && !normalized.jobId) {
3534
3636
  return this.syncRemoteDocumentToParsedStorage({
3535
3637
  documentId: normalized.documentId,
3536
3638
  revisionKey: params.revisionKey,
@@ -3542,6 +3644,25 @@ var Knowledge = class _Knowledge {
3542
3644
  }
3543
3645
  if (normalized.jobId) {
3544
3646
  const job = await this.client.jobs.get(normalized.jobId);
3647
+ if (job.snapshots?.length) {
3648
+ return this.syncJobSnapshot({
3649
+ job,
3650
+ documentId: normalized.documentId ?? job.documentId ?? normalized.localDocumentId ?? normalized.jobId,
3651
+ revisionKey: params.revisionKey,
3652
+ parsedStorageConfig,
3653
+ signal: params.signal,
3654
+ retainResultArchive: params.retainResultArchive
3655
+ });
3656
+ }
3657
+ if (normalized.documentId) {
3658
+ return this.syncRemoteDocumentToParsedStorage({
3659
+ documentId: normalized.documentId,
3660
+ revisionKey: params.revisionKey,
3661
+ parsedStorageConfig,
3662
+ signal: params.signal,
3663
+ retainResultArchive: params.retainResultArchive
3664
+ });
3665
+ }
3545
3666
  if (!job.isDone || !job.resultUrl)
3546
3667
  throw new ValidationError("Completed job result archive is unavailable");
3547
3668
  return syncRemoteResultArchive({
@@ -3573,6 +3694,89 @@ var Knowledge = class _Knowledge {
3573
3694
  commit: synced.commit
3574
3695
  };
3575
3696
  }
3697
+ async syncJobSnapshot(params) {
3698
+ const snapshotId = params.revisionKey ?? params.job.currentSnapshotId;
3699
+ const snapshot = params.job.snapshots?.find(
3700
+ (candidate) => candidate.snapshotId === snapshotId
3701
+ );
3702
+ if (!params.job.isDone || !snapshot)
3703
+ throw new ValidationError("Requested immutable parsed snapshot is unavailable");
3704
+ if (snapshot.jobId !== params.job.jobId || snapshot.documentId !== params.documentId)
3705
+ throw new ValidationError(
3706
+ "Parsed snapshot document or job identity differs from the request"
3707
+ );
3708
+ const response = await syncRemoteResultArchive({
3709
+ documentId: params.documentId,
3710
+ revisionKey: snapshot.snapshotId,
3711
+ jobId: snapshot.jobId,
3712
+ snapshot,
3713
+ resultUrl: snapshot.resultUrl,
3714
+ storage: params.parsedStorageConfig.storage,
3715
+ limits: params.parsedStorageConfig.limits,
3716
+ signal: params.signal,
3717
+ retainResultArchive: params.retainResultArchive
3718
+ });
3719
+ if (response.completed && snapshot.snapshotId === params.job.currentSnapshotId) {
3720
+ await serializeSnapshotPublication(
3721
+ params.parsedStorageConfig.storage,
3722
+ params.documentId,
3723
+ async () => {
3724
+ const current = await this.client.documents.listChunks(
3725
+ params.documentId,
3726
+ { page: 1, pageSize: 1 }
3727
+ );
3728
+ if (current.jobResultId !== snapshot.jobResultId) return;
3729
+ const pointer = Buffer.from(
3730
+ JSON.stringify({
3731
+ snapshotId: snapshot.snapshotId,
3732
+ jobResultId: snapshot.jobResultId
3733
+ })
3734
+ );
3735
+ if (snapshot.stage === "final") {
3736
+ await params.parsedStorageConfig.storage.writeObject({
3737
+ documentId: params.documentId,
3738
+ revisionKey: "current",
3739
+ path: ".knowhere-sdk/final-snapshot.json",
3740
+ body: pointer,
3741
+ contentType: "application/json; charset=utf-8"
3742
+ });
3743
+ }
3744
+ await params.parsedStorageConfig.storage.writeObject({
3745
+ documentId: params.documentId,
3746
+ revisionKey: "current",
3747
+ path: ".knowhere-sdk/current-snapshot.json",
3748
+ body: pointer,
3749
+ contentType: "application/json; charset=utf-8"
3750
+ });
3751
+ }
3752
+ );
3753
+ }
3754
+ return response;
3755
+ }
3756
+ async resolveStoredSnapshotKey(documentId) {
3757
+ const object = await this.parsedStorageConfig?.storage.readObject({
3758
+ documentId,
3759
+ revisionKey: "current",
3760
+ path: ".knowhere-sdk/current-snapshot.json"
3761
+ });
3762
+ if (!object) return void 0;
3763
+ const payload = JSON.parse(Buffer.from(object.body).toString("utf8"));
3764
+ if (typeof payload !== "object" || payload === null || !("snapshotId" in payload))
3765
+ throw new ValidationError("Invalid current parsed snapshot pointer");
3766
+ if (typeof payload.snapshotId !== "string")
3767
+ throw new ValidationError("Invalid current parsed snapshot identity");
3768
+ const finalObject = await this.parsedStorageConfig?.storage.readObject({
3769
+ documentId,
3770
+ revisionKey: "current",
3771
+ path: ".knowhere-sdk/final-snapshot.json"
3772
+ });
3773
+ if (finalObject && "jobResultId" in payload) {
3774
+ const finalPayload = JSON.parse(Buffer.from(finalObject.body).toString("utf8"));
3775
+ if (typeof finalPayload === "object" && finalPayload !== null && "snapshotId" in finalPayload && typeof finalPayload.snapshotId === "string" && "jobResultId" in finalPayload && finalPayload.jobResultId === payload.jobResultId)
3776
+ return finalPayload.snapshotId;
3777
+ }
3778
+ return payload.snapshotId;
3779
+ }
3576
3780
  async syncParseResultToConfiguredStorage(result) {
3577
3781
  const parsedStorageConfig = this.parsedStorageConfig;
3578
3782
  if (!parsedStorageConfig) {
@@ -3808,7 +4012,7 @@ var Knowledge = class _Knowledge {
3808
4012
  throw new Error(`Local Knowhere document not found: ${normalized.localDocumentId}`);
3809
4013
  }
3810
4014
  async readRemoteDocumentChunks(documentId, params) {
3811
- const explicitRevisionKey = params.revisionKey ?? params.jobId;
4015
+ const explicitRevisionKey = params.revisionKey ?? params.jobId ?? await this.resolveStoredSnapshotKey(documentId);
3812
4016
  if (explicitRevisionKey) {
3813
4017
  const stored2 = await this.tryReadChunksFromParsedStorage({
3814
4018
  documentId,
@@ -3818,6 +4022,8 @@ var Knowledge = class _Knowledge {
3818
4022
  if (stored2) {
3819
4023
  return stored2;
3820
4024
  }
4025
+ if (params.revisionKey && await this.resolveStoredSnapshotKey(documentId))
4026
+ throw new ValidationError("Requested parsed snapshot is not committed locally");
3821
4027
  }
3822
4028
  if (hasScanReadFilters(params)) {
3823
4029
  return this.scanRemoteDocumentChunks(documentId, params);
@@ -3830,6 +4036,8 @@ var Knowledge = class _Knowledge {
3830
4036
  includeAssetUrls: true
3831
4037
  });
3832
4038
  const revisionKey = getRemoteRevisionKey(remotePage);
4039
+ if (params.revisionKey && params.revisionKey !== revisionKey)
4040
+ throw new ValidationError("Requested parsed snapshot revision is not committed locally");
3833
4041
  const stored = await this.tryReadChunksFromParsedStorage({
3834
4042
  documentId,
3835
4043
  revisionKey,
@@ -3854,7 +4062,7 @@ var Knowledge = class _Knowledge {
3854
4062
  };
3855
4063
  }
3856
4064
  async scanRemoteDocumentChunks(documentId, params) {
3857
- const explicitRevisionKey = params.revisionKey ?? params.jobId;
4065
+ const explicitRevisionKey = params.revisionKey ?? params.jobId ?? await this.resolveStoredSnapshotKey(documentId);
3858
4066
  if (explicitRevisionKey) {
3859
4067
  const stored2 = await this.tryReadChunksFromParsedStorage({
3860
4068
  documentId,
@@ -3864,6 +4072,8 @@ var Knowledge = class _Knowledge {
3864
4072
  if (stored2) {
3865
4073
  return stored2;
3866
4074
  }
4075
+ if (params.revisionKey && await this.resolveStoredSnapshotKey(documentId))
4076
+ throw new ValidationError("Requested parsed snapshot is not committed locally");
3867
4077
  }
3868
4078
  const limit = clampLimit(params.limit, DEFAULT_READ_LIMIT, MAX_READ_LIMIT);
3869
4079
  const limits = getParsedStorageLimits(this.parsedStorageConfig?.limits);
@@ -3874,6 +4084,8 @@ var Knowledge = class _Knowledge {
3874
4084
  includeAssetUrls: true
3875
4085
  });
3876
4086
  const revisionKey = getRemoteRevisionKey(firstPage);
4087
+ if (params.revisionKey && params.revisionKey !== revisionKey)
4088
+ throw new ValidationError("Requested parsed snapshot revision is not committed locally");
3877
4089
  const stored = await this.tryReadChunksFromParsedStorage({
3878
4090
  documentId,
3879
4091
  revisionKey,
@@ -3916,7 +4128,7 @@ var Knowledge = class _Knowledge {
3916
4128
  };
3917
4129
  }
3918
4130
  async grepRemoteDocumentChunks(documentId, params) {
3919
- const explicitRevisionKey = params.revisionKey ?? params.jobId;
4131
+ const explicitRevisionKey = params.revisionKey ?? params.jobId ?? await this.resolveStoredSnapshotKey(documentId);
3920
4132
  if (explicitRevisionKey && !params.continuationCursor) {
3921
4133
  const stored2 = await this.tryGrepChunksFromParsedStorage({
3922
4134
  documentId,
@@ -3926,6 +4138,8 @@ var Knowledge = class _Knowledge {
3926
4138
  if (stored2) {
3927
4139
  return stored2;
3928
4140
  }
4141
+ if (params.revisionKey && await this.resolveStoredSnapshotKey(documentId))
4142
+ throw new ValidationError("Requested parsed snapshot is not committed locally");
3929
4143
  }
3930
4144
  const maxResults = clampLimit(params.maxResults, DEFAULT_GREP_LIMIT, MAX_GREP_LIMIT);
3931
4145
  const contextChars = params.contextChars ?? DEFAULT_CONTEXT_CHARS;
@@ -3940,6 +4154,8 @@ var Knowledge = class _Knowledge {
3940
4154
  includeAssetUrls: true
3941
4155
  });
3942
4156
  const revisionKey = getRemoteRevisionKey(firstPage);
4157
+ if (params.revisionKey && params.revisionKey !== revisionKey)
4158
+ throw new ValidationError("Requested parsed snapshot revision is not committed locally");
3943
4159
  const stored = startPage === 1 ? await this.tryGrepChunksFromParsedStorage({
3944
4160
  documentId,
3945
4161
  revisionKey,
@@ -4025,11 +4241,14 @@ var Knowledge = class _Knowledge {
4025
4241
  }
4026
4242
  }
4027
4243
  async getRemoteDocumentOutline(documentId, revisionKeyOverride) {
4028
- if (revisionKeyOverride) {
4029
- const stored2 = await this.tryReadOutlineFromParsedStorage(documentId, revisionKeyOverride);
4244
+ const storedRevisionKey = revisionKeyOverride ?? await this.resolveStoredSnapshotKey(documentId);
4245
+ if (storedRevisionKey) {
4246
+ const stored2 = await this.tryReadOutlineFromParsedStorage(documentId, storedRevisionKey);
4030
4247
  if (stored2) {
4031
4248
  return stored2;
4032
4249
  }
4250
+ if (revisionKeyOverride && await this.resolveStoredSnapshotKey(documentId))
4251
+ throw new ValidationError("Requested parsed snapshot is not committed locally");
4033
4252
  }
4034
4253
  const limits = getParsedStorageLimits(this.parsedStorageConfig?.limits);
4035
4254
  const firstPage = await this.client.documents.listChunks(documentId, {
@@ -4038,6 +4257,8 @@ var Knowledge = class _Knowledge {
4038
4257
  includeAssetUrls: true
4039
4258
  });
4040
4259
  const revisionKey = getRemoteRevisionKey(firstPage);
4260
+ if (revisionKeyOverride && revisionKeyOverride !== revisionKey)
4261
+ throw new ValidationError("Requested parsed snapshot revision is not committed locally");
4041
4262
  const stored = await this.tryReadOutlineFromParsedStorage(documentId, revisionKey);
4042
4263
  if (stored) {
4043
4264
  return stored;
@@ -4246,11 +4467,7 @@ var Knowledge = class _Knowledge {
4246
4467
  const scheduler = parsedStorageConfig.scheduler ?? defaultParsedStorageScheduler;
4247
4468
  const task = async () => {
4248
4469
  try {
4249
- await this.syncRemoteDocumentToParsedStorage({
4250
- documentId,
4251
- revisionKey,
4252
- parsedStorageConfig
4253
- });
4470
+ await this.syncParsedDocument({ documentId, revisionKey });
4254
4471
  } catch (error) {
4255
4472
  const previous = await parsedStorageConfig.storage.readSyncProgress({
4256
4473
  documentId,
@@ -4276,14 +4493,25 @@ var Knowledge = class _Knowledge {
4276
4493
  includeAssetUrls: true
4277
4494
  });
4278
4495
  const revisionKey = params.revisionKey ?? getRemoteRevisionKey(firstPage);
4279
- if (params.revisionKey && getRemoteRevisionKey(firstPage) !== params.revisionKey) {
4280
- throw new ValidationError("Published document revision changed before parsed snapshot sync");
4281
- }
4282
4496
  if (firstPage.jobId) {
4283
4497
  const job = await this.client.jobs.get(firstPage.jobId).catch((error) => {
4284
4498
  if (error instanceof NotFoundError) return null;
4285
4499
  throw error;
4286
4500
  });
4501
+ if (job?.snapshots?.length) {
4502
+ const snapshot = job.snapshots.find(
4503
+ (candidate) => candidate.snapshotId === (params.revisionKey ?? job.currentSnapshotId)
4504
+ );
4505
+ if (snapshot && snapshot.jobResultId !== getRemoteRevisionKey(firstPage))
4506
+ throw new ValidationError(
4507
+ "Parsed snapshot upstream revision differs from the published document"
4508
+ );
4509
+ return this.syncJobSnapshot({ ...params, job });
4510
+ }
4511
+ if (params.revisionKey && getRemoteRevisionKey(firstPage) !== params.revisionKey)
4512
+ throw new ValidationError(
4513
+ "Published document revision changed before parsed snapshot sync"
4514
+ );
4287
4515
  if (job?.isDone && job.resultUrl) {
4288
4516
  if (job.documentId && job.documentId !== params.documentId)
4289
4517
  throw new ValidationError("Parsed job document identity differs from requested document");
@@ -4492,7 +4720,7 @@ function createLocalDocumentIdForRemote(documentId) {
4492
4720
  if (SAFE_LOCAL_DOCUMENT_ID_PATTERN.test(documentId) && !documentId.includes("..") && !documentId.includes("/") && !documentId.includes("\\")) {
4493
4721
  return documentId;
4494
4722
  }
4495
- const hash = (0, import_crypto4.createHash)("sha256").update(documentId).digest("hex").slice(0, 16);
4723
+ const hash = (0, import_crypto5.createHash)("sha256").update(documentId).digest("hex").slice(0, 16);
4496
4724
  return `remote_${hash}`;
4497
4725
  }
4498
4726
  var defaultParsedStorageScheduler = {
@@ -5450,7 +5678,7 @@ var Knowhere = class {
5450
5678
  };
5451
5679
 
5452
5680
  // src/storage/disk-parsed-document-storage.ts
5453
- var import_crypto5 = require("crypto");
5681
+ var import_crypto6 = require("crypto");
5454
5682
  var import_fs4 = require("fs");
5455
5683
  var import_path5 = __toESM(require("path"));
5456
5684
  var syncProgressPath = ".knowhere-sdk/sync-progress.json";
@@ -5583,7 +5811,7 @@ function hashPathPart(value) {
5583
5811
  if (normalized.length === 0) {
5584
5812
  throw new Error("Parsed document storage path part must be a non-empty value.");
5585
5813
  }
5586
- return (0, import_crypto5.createHash)("sha256").update(normalized).digest("hex").slice(0, 24);
5814
+ return (0, import_crypto6.createHash)("sha256").update(normalized).digest("hex").slice(0, 24);
5587
5815
  }
5588
5816
  function normalizeRelativeStoragePath(value) {
5589
5817
  const normalized = value.replaceAll("\\", "/").replace(/^\.\/+/, "");