@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.
@@ -463,6 +463,73 @@ function isPassiveSvg(content) {
463
463
  return isPassive && sawRoot && depth === 0;
464
464
  }
465
465
 
466
+ // src/url-asset-cache.ts
467
+ function createMigrationAssetCache() {
468
+ const settled = /* @__PURE__ */ new Map();
469
+ const pending = /* @__PURE__ */ new Map();
470
+ const remember = (url, asset) => {
471
+ settled.delete(url);
472
+ settled.set(url, asset);
473
+ if (settled.size > 480) settled.delete(settled.keys().next().value);
474
+ return asset;
475
+ };
476
+ const get = (url) => {
477
+ const asset = settled.get(url);
478
+ if (asset) return Promise.resolve(remember(url, asset));
479
+ return pending.get(url);
480
+ };
481
+ return {
482
+ remember,
483
+ get,
484
+ has(url) {
485
+ return settled.has(url) || pending.has(url);
486
+ },
487
+ get size() {
488
+ return settled.size + pending.size;
489
+ },
490
+ load(url, acquire) {
491
+ const existing = get(url);
492
+ if (existing) return existing;
493
+ const request = Promise.resolve().then(acquire).then((asset) => {
494
+ pending.delete(url);
495
+ return remember(url, asset);
496
+ }, (error) => {
497
+ pending.delete(url);
498
+ throw error;
499
+ });
500
+ pending.set(url, request);
501
+ return request;
502
+ },
503
+ delete(url) {
504
+ settled.delete(url);
505
+ }
506
+ };
507
+ }
508
+
509
+ // src/url-journal-clone.ts
510
+ function cloneUrlMigrationCheckpoint(checkpoint) {
511
+ const ancestors = /* @__PURE__ */ new Set();
512
+ const clone = (value) => {
513
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
514
+ if (typeof value === "number") return Number.isFinite(value) ? value === 0 ? 0 : value : null;
515
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
516
+ if (typeof value !== "object") throw new TypeError("Checkpoint must contain JSON-safe values.");
517
+ if (ancestors.has(value)) throw new TypeError("Checkpoint must not contain circular references.");
518
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("Checkpoint must contain plain JSON containers.");
519
+ ancestors.add(value);
520
+ try {
521
+ if (Array.isArray(value)) return Array.from(value, (entry) => clone(entry) ?? null);
522
+ return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
523
+ const copied = clone(entry);
524
+ return copied === void 0 ? [] : [[key, copied]];
525
+ }));
526
+ } finally {
527
+ ancestors.delete(value);
528
+ }
529
+ };
530
+ return clone(checkpoint);
531
+ }
532
+
466
533
  // src/url-images.ts
467
534
  function migrationImageUrl(value, base) {
468
535
  const url = new URL(value, base);
@@ -3191,7 +3258,8 @@ async function runUrlMigration(options) {
3191
3258
  const batchSize = Math.max(1, Math.min(500, Math.floor(options.batchSize ?? 50)));
3192
3259
  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.");
3193
3260
  const captureVersion = options.captureVersion ?? "fetch-v1";
3194
- const checkpoint = options.checkpoint ? JSON.parse(JSON.stringify(options.checkpoint)) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
3261
+ if (options.checkpoint && options.checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
3262
+ const checkpoint = options.checkpoint ? cloneUrlMigrationCheckpoint(options.checkpoint) : { schemaVersion: 1, converterVersion: CONVERTER_VERSION, sourceUrl, scopePath, captureVersion, records: [], navigation: [] };
3195
3263
  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.");
3196
3264
  if (checkpoint.records.length > MAX_RECORDS) throw new Error("Checkpoint inventory exceeds its bound.");
3197
3265
  const records = /* @__PURE__ */ new Map();
@@ -3239,9 +3307,21 @@ async function runUrlMigration(options) {
3239
3307
  const finalUrls = new Map([...records.values()].filter((record) => record.evidence && record.page).map((record) => [canonical(record.evidence.finalUrl, sourceUrl), record.url]));
3240
3308
  let imported = usedIds.size;
3241
3309
  const assetPayloads = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.sha256, asset])));
3242
- const assetRequests = /* @__PURE__ */ new Map();
3310
+ const assetBytes = /* @__PURE__ */ new Map();
3311
+ const retainAsset = (asset) => {
3312
+ if (!acceptedAsset(asset)) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
3313
+ const sha256 = hash(asset.content);
3314
+ const content = assetBytes.get(sha256) ?? Buffer.from(asset.content);
3315
+ assetBytes.set(sha256, content);
3316
+ return { ...asset, content };
3317
+ };
3318
+ const assetRequests = createMigrationAssetCache();
3243
3319
  for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
3244
- if (!assetRequests.has(asset.url)) assetRequests.set(asset.url, Promise.resolve({ url: asset.url, contentType: asset.contentType, content: Buffer.from(asset.base64, "base64") }));
3320
+ if (!assetRequests.has(asset.url)) {
3321
+ const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
3322
+ assetBytes.set(asset.sha256, content);
3323
+ assetRequests.remember(asset.url, { url: asset.url, contentType: asset.contentType, content });
3324
+ }
3245
3325
  }
3246
3326
  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);
3247
3327
  let budgetExhausted = false;
@@ -3353,10 +3433,10 @@ ${difference}`;
3353
3433
  try {
3354
3434
  let assetRequest = assetRequests.get(url);
3355
3435
  if (!suppliedAssets.has(url) && !assetRequest && assetFetcher) {
3356
- assetRequest = assetFetcher(new URL(url), options.signal);
3357
- assetRequests.set(url, assetRequest);
3436
+ assetRequest = assetRequests.load(url, () => assetFetcher(new URL(url), options.signal).then(retainAsset));
3358
3437
  }
3359
- const asset = suppliedAssets.get(url) ?? (assetRequest ? await assetRequest : null);
3438
+ const supplied = suppliedAssets.get(url);
3439
+ const asset = supplied ? retainAsset(supplied) : assetRequest ? await assetRequest : null;
3360
3440
  if (!asset) throw new Error("Capture host did not supply asset bytes or a binary fetch boundary.");
3361
3441
  const extension = acceptedAsset(asset);
3362
3442
  if (!extension) throw new Error("Asset MIME/signature is unsupported or unsafe to serve.");
@@ -3373,7 +3453,7 @@ ${difference}`;
3373
3453
  const receipt = { url, path, sha256, contentType: asset.contentType, base64: existing?.base64 ?? Buffer.from(asset.content).toString("base64") };
3374
3454
  record.assets.push(receipt);
3375
3455
  if (!existing) assetPayloads.set(sha256, receipt);
3376
- if (!assetRequests.has(url)) assetRequests.set(url, Promise.resolve(asset));
3456
+ if (!assetRequests.has(url)) assetRequests.remember(url, asset);
3377
3457
  page.body = rewriteMarkdownDestinations(page.body, (destination) => canonical(destination, final) === url ? `/${path}${new URL(destination, final).hash}` : destination);
3378
3458
  } catch (error) {
3379
3459
  assetRequests.delete(url);
@@ -3489,7 +3569,13 @@ ${difference}`;
3489
3569
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
3490
3570
  validation.issues.push(...navigationIssues);
3491
3571
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
3492
- const assets = new Map(checkpoint.records.flatMap((record) => (record.assets ?? []).map((asset) => [asset.path, { path: asset.path, content: Buffer.from(asset.base64, "base64") }])));
3572
+ const assets = /* @__PURE__ */ new Map();
3573
+ for (const record of checkpoint.records) for (const asset of record.assets ?? []) {
3574
+ if (!assets.has(asset.path)) {
3575
+ const content = assetBytes.get(asset.sha256) ?? Buffer.from(asset.base64, "base64");
3576
+ assets.set(asset.path, { path: asset.path, content: Buffer.from(content.buffer, content.byteOffset, content.byteLength) });
3577
+ }
3578
+ }
3493
3579
  const bundle = {
3494
3580
  sourceUrl,
3495
3581
  sourceKind: "url",