@thallylabs/migrate 0.2.28 → 0.2.29

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
@@ -13876,6 +13876,73 @@ function isPassiveSvg(content) {
13876
13876
  return isPassive && sawRoot && depth === 0;
13877
13877
  }
13878
13878
 
13879
+ // src/url-asset-cache.ts
13880
+ function createMigrationAssetCache() {
13881
+ const settled = /* @__PURE__ */ new Map();
13882
+ const pending = /* @__PURE__ */ new Map();
13883
+ const remember = (url, asset) => {
13884
+ settled.delete(url);
13885
+ settled.set(url, asset);
13886
+ if (settled.size > 480) settled.delete(settled.keys().next().value);
13887
+ return asset;
13888
+ };
13889
+ const get = (url) => {
13890
+ const asset = settled.get(url);
13891
+ if (asset) return Promise.resolve(remember(url, asset));
13892
+ return pending.get(url);
13893
+ };
13894
+ return {
13895
+ remember,
13896
+ get,
13897
+ has(url) {
13898
+ return settled.has(url) || pending.has(url);
13899
+ },
13900
+ get size() {
13901
+ return settled.size + pending.size;
13902
+ },
13903
+ load(url, acquire) {
13904
+ const existing = get(url);
13905
+ if (existing) return existing;
13906
+ const request2 = Promise.resolve().then(acquire).then((asset) => {
13907
+ pending.delete(url);
13908
+ return remember(url, asset);
13909
+ }, (error) => {
13910
+ pending.delete(url);
13911
+ throw error;
13912
+ });
13913
+ pending.set(url, request2);
13914
+ return request2;
13915
+ },
13916
+ delete(url) {
13917
+ settled.delete(url);
13918
+ }
13919
+ };
13920
+ }
13921
+
13922
+ // src/url-journal-clone.ts
13923
+ function cloneUrlMigrationCheckpoint(checkpoint) {
13924
+ const ancestors = /* @__PURE__ */ new Set();
13925
+ const clone = (value) => {
13926
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
13927
+ if (typeof value === "number") return Number.isFinite(value) ? value === 0 ? 0 : value : null;
13928
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
13929
+ if (typeof value !== "object") throw new TypeError("Checkpoint must contain JSON-safe values.");
13930
+ if (ancestors.has(value)) throw new TypeError("Checkpoint must not contain circular references.");
13931
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("Checkpoint must contain plain JSON containers.");
13932
+ ancestors.add(value);
13933
+ try {
13934
+ if (Array.isArray(value)) return Array.from(value, (entry) => clone(entry) ?? null);
13935
+ return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
13936
+ const copied = clone(entry);
13937
+ return copied === void 0 ? [] : [[key, copied]];
13938
+ }));
13939
+ } finally {
13940
+ ancestors.delete(value);
13941
+ }
13942
+ };
13943
+ return clone(checkpoint);
13944
+ }
13945
+
13879
13946
  // src/url-run.ts
13880
13947
  var CONVERTER_VERSION = "url-evidence-v2";
13881
13948
  var parser2 = unified4().use(remarkParse4).use(remarkMdx4);
@@ -14250,7 +14317,8 @@ async function runUrlMigration(options) {
14250
14317
  const batchSize = Math.max(1, Math.min(500, Math.floor(options.batchSize ?? 50)));
14251
14318
  if (!Number.isFinite(concurrency) || !Number.isFinite(batchSize) || options.maxCaptures !== void 0 && (!Number.isInteger(options.maxCaptures) || options.maxCaptures < 1) || options.maxTotalBytes !== void 0 && (!Number.isFinite(options.maxTotalBytes) || options.maxTotalBytes < 1)) throw new Error("Migration batch, concurrency and byte limits must be positive finite numbers.");
14252
14319
  const captureVersion = options.captureVersion ?? "fetch-v1";
14253
- const checkpoint = options.checkpoint ? JSON.parse(JSON.stringify(options.checkpoint)) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
14320
+ if (options.checkpoint && options.checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
14321
+ const checkpoint = options.checkpoint ? cloneUrlMigrationCheckpoint(options.checkpoint) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
14254
14322
  if (checkpoint.schemaVersion !== 1 || checkpoint.converterVersion !== CONVERTER_VERSION || checkpoint.sourceUrl !== sourceUrl || checkpoint.scopePath !== scopePath || checkpoint.captureVersion !== captureVersion) throw new Error("Checkpoint does not match this source, scope or capture/converter version.");
14255
14323
  if (checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
14256
14324
  const records = /* @__PURE__ */ new Map();
@@ -14298,9 +14366,21 @@ async function runUrlMigration(options) {
14298
14366
  const finalUrls = new Map([...records.values()].filter((record) => record.evidence && record.page).map((record) => [canonical(record.evidence.finalUrl, sourceUrl), record.url]));
14299
14367
  let imported = usedIds.size;
14300
14368
  const assetPayloads = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.sha256, asset])));
14301
- const assetRequests = /* @__PURE__ */ new Map();
14369
+ const assetBytes = /* @__PURE__ */ new Map();
14370
+ const retainAsset = (asset) => {
14371
+ if (!acceptedAsset(asset)) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
14372
+ const sha256 = hash2(asset.content);
14373
+ const content = assetBytes.get(sha256) ?? Buffer.from(asset.content);
14374
+ assetBytes.set(sha256, content);
14375
+ return { ...asset, content };
14376
+ };
14377
+ const assetRequests = createMigrationAssetCache();
14302
14378
  for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
14303
- if (!assetRequests.has(asset.url)) assetRequests.set(asset.url, Promise.resolve({ url: asset.url, contentType: asset.contentType, content: Buffer.from(asset.base64, "base64") }));
14379
+ if (!assetRequests.has(asset.url)) {
14380
+ const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
14381
+ assetBytes.set(asset.sha256, content);
14382
+ assetRequests.remember(asset.url, { url: asset.url, contentType: asset.contentType, content });
14383
+ }
14304
14384
  }
14305
14385
  let totalBytes = checkpoint.records.reduce((sum, record) => sum + (record.evidence?.byteLength ?? 0), 0) + [...assetPayloads.values()].reduce((sum, asset) => sum + Buffer.byteLength(asset.base64, "base64"), 0);
14306
14386
  let budgetExhausted = false;
@@ -14412,10 +14492,10 @@ ${difference}`;
14412
14492
  try {
14413
14493
  let assetRequest = assetRequests.get(url);
14414
14494
  if (!suppliedAssets.has(url) && !assetRequest && assetFetcher) {
14415
- assetRequest = assetFetcher(new URL(url), options.signal);
14416
- assetRequests.set(url, assetRequest);
14495
+ assetRequest = assetRequests.load(url, () => assetFetcher(new URL(url), options.signal).then(retainAsset));
14417
14496
  }
14418
- const asset = suppliedAssets.get(url) ?? (assetRequest ? await assetRequest : null);
14497
+ const supplied = suppliedAssets.get(url);
14498
+ const asset = supplied ? retainAsset(supplied) : assetRequest ? await assetRequest : null;
14419
14499
  if (!asset) throw new Error("Capture host did not supply asset bytes or a binary fetch boundary.");
14420
14500
  const extension = acceptedAsset(asset);
14421
14501
  if (!extension) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
@@ -14432,7 +14512,7 @@ ${difference}`;
14432
14512
  const receipt = { url, path, sha256, contentType: asset.contentType, base64: existing?.base64 ?? Buffer.from(asset.content).toString("base64") };
14433
14513
  record.assets.push(receipt);
14434
14514
  if (!existing) assetPayloads.set(sha256, receipt);
14435
- if (!assetRequests.has(url)) assetRequests.set(url, Promise.resolve(asset));
14515
+ if (!assetRequests.has(url)) assetRequests.remember(url, asset);
14436
14516
  page.body = rewriteMarkdownDestinations(page.body, (destination) => canonical(destination, final) === url ? `/${path}${new URL(destination, final).hash}` : destination);
14437
14517
  } catch (error) {
14438
14518
  assetRequests.delete(url);
@@ -14548,7 +14628,13 @@ ${difference}`;
14548
14628
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
14549
14629
  validation.issues.push(...navigationIssues);
14550
14630
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
14551
- const assets = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.path, { path: asset.path, content: Buffer.from(asset.base64, "base64") }])));
14631
+ const assets = /* @__PURE__ */ new Map();
14632
+ for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
14633
+ if (!assets.has(asset.path)) {
14634
+ const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
14635
+ assets.set(asset.path, { path: asset.path, content: Buffer.from(content.buffer, content.byteOffset, content.byteLength) });
14636
+ }
14637
+ }
14552
14638
  const bundle = {
14553
14639
  sourceUrl,
14554
14640
  sourceKind: "url",