@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 +94 -8
- package/dist/index.js.map +1 -1
- package/dist/url-migration.js +94 -8
- package/dist/url-migration.js.map +1 -1
- package/package.json +1 -1
package/dist/url-migration.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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))
|
|
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
|
|
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.
|
|
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 =
|
|
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",
|