fhir-package-installer
Version:
A utility module for downloading, indexing, caching, and managing FHIR packages from the FHIR Package Registry and Simplifier
1,311 lines (1,307 loc) • 121 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
FhirPackageInstaller: () => FhirPackageInstaller,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
var import_https = __toESM(require("https"), 1);
var import_http = __toESM(require("http"), 1);
var import_fs_extra = __toESM(require("fs-extra"), 1);
// node_modules/yocto-queue/index.js
var Node = class {
value;
next;
constructor(value) {
this.value = value;
}
};
var Queue = class {
#head;
#tail;
#size;
constructor() {
this.clear();
}
enqueue(value) {
const node = new Node(value);
if (this.#head) {
this.#tail.next = node;
this.#tail = node;
} else {
this.#head = node;
this.#tail = node;
}
this.#size++;
}
dequeue() {
const current = this.#head;
if (!current) {
return;
}
this.#head = this.#head.next;
this.#size--;
if (!this.#head) {
this.#tail = void 0;
}
return current.value;
}
peek() {
if (!this.#head) {
return;
}
return this.#head.value;
}
clear() {
this.#head = void 0;
this.#tail = void 0;
this.#size = 0;
}
get size() {
return this.#size;
}
*[Symbol.iterator]() {
let current = this.#head;
while (current) {
yield current.value;
current = current.next;
}
}
*drain() {
while (this.#head) {
yield this.dequeue();
}
}
};
// node_modules/p-limit/index.js
function pLimit(concurrency) {
let rejectOnClear = false;
if (typeof concurrency === "object") {
({ concurrency, rejectOnClear = false } = concurrency);
}
validateConcurrency(concurrency);
if (typeof rejectOnClear !== "boolean") {
throw new TypeError("Expected `rejectOnClear` to be a boolean");
}
const queue = new Queue();
let activeCount = 0;
const resumeNext = () => {
if (activeCount < concurrency && queue.size > 0) {
activeCount++;
queue.dequeue().run();
}
};
const next = () => {
activeCount--;
resumeNext();
};
const run = async (function_, resolve, arguments_) => {
const result = (async () => function_(...arguments_))();
resolve(result);
try {
await result;
} catch {
}
next();
};
const enqueue = (function_, resolve, reject, arguments_) => {
const queueItem = { reject };
new Promise((internalResolve) => {
queueItem.run = internalResolve;
queue.enqueue(queueItem);
}).then(run.bind(void 0, function_, resolve, arguments_));
if (activeCount < concurrency) {
resumeNext();
}
};
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
enqueue(function_, resolve, reject, arguments_);
});
Object.defineProperties(generator, {
activeCount: {
get: () => activeCount
},
pendingCount: {
get: () => queue.size
},
clearQueue: {
value() {
if (!rejectOnClear) {
queue.clear();
return;
}
const abortError = AbortSignal.abort().reason;
while (queue.size > 0) {
queue.dequeue().reject(abortError);
}
}
},
concurrency: {
get: () => concurrency,
set(newConcurrency) {
validateConcurrency(newConcurrency);
concurrency = newConcurrency;
queueMicrotask(() => {
while (activeCount < concurrency && queue.size > 0) {
resumeNext();
}
});
}
},
map: {
async value(iterable, function_) {
const promises = Array.from(iterable, (value, index) => this(function_, value, index));
return Promise.all(promises);
}
}
});
return generator;
}
function validateConcurrency(concurrency) {
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
throw new TypeError("Expected `concurrency` to be a number from 1 and up");
}
}
// src/index.ts
var import_path = __toESM(require("path"), 1);
var import_promises = require("stream/promises");
var tar = __toESM(require("tar-stream"), 1);
var zlib = __toESM(require("zlib"), 1);
var import_os = __toESM(require("os"), 1);
var import_semver = __toESM(require("semver"), 1);
var import_crypto = __toESM(require("crypto"), 1);
var tempDirs = /* @__PURE__ */ new Set();
var tempCleanupRegistered = false;
var registerTempCleanup = () => {
if (tempCleanupRegistered) return;
tempCleanupRegistered = true;
process.once("exit", () => {
for (const dir of tempDirs) {
try {
import_fs_extra.default.removeSync(dir);
} catch {
}
}
tempDirs.clear();
});
};
var createTempDir = (baseDir) => {
registerTempCleanup();
const parentDir = baseDir ?? import_os.default.tmpdir();
import_fs_extra.default.ensureDirSync(parentDir);
const dir = import_fs_extra.default.mkdtempSync(import_path.default.join(parentDir, "fhir-package-installer-"));
tempDirs.add(dir);
return dir;
};
var FPI_VERSION = "1.13.0".trim().length > 0 ? "1.13.0" : "0.0.0";
var FPI_INDEX_CACHE_VERSION = (() => {
const v = import_semver.default.parse(FPI_VERSION);
if (!v) return "0.0";
return `${v.major}.${v.minor}`;
})();
var FPI_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
var FPI_MATERIALIZATION_MARKER = ".fpi.materialized";
var DEPENDENCY_CLAIM_WAIT_MS = 1e3;
var DEPENDENCY_POST_CLAIM_YIELD_MS = 500;
var DEPENDENCY_WAIT_LOG_INTERVAL_MS = 5e3;
var PACKAGE_INSTALL_WAIT_LOG_INTERVAL_MS = 5e3;
var DEPENDENCY_PEER_HANDOFF_WAIT_MS = 3e3;
var DEPENDENCY_PEER_DISCOVERY_GRACE_MS = 300;
var IMPLICIT_DEPENDENCIES_MAP = {
"hl7.fhir.r3.core": [
"hl7.terminology.r3",
"hl7.fhir.uv.extensions.r3"
],
"hl7.fhir.r4.core": [
"hl7.terminology.r4",
"hl7.fhir.uv.extensions.r4"
],
"hl7.fhir.r5.core": [
"hl7.terminology.r5",
"hl7.fhir.uv.extensions.r5"
]
};
var IMPLICIT_PACKAGE_IDS = (() => {
const s = /* @__PURE__ */ new Set();
for (const ids of Object.values(IMPLICIT_DEPENDENCIES_MAP)) {
for (const id of ids) s.add(id);
}
return s;
})();
var DEFAULT_REGISTRY_TTL_MS = 30 * 60 * 1e3;
var MEM_CACHE_MAX_ENTRIES = 500;
var BoundedTtlCache = class {
maxEntries;
map = /* @__PURE__ */ new Map();
constructor(maxEntries) {
this.maxEntries = maxEntries;
}
get(key) {
const entry = this.map.get(key);
if (!entry) return void 0;
if (Date.now() >= entry.expiresAt) {
this.map.delete(key);
return void 0;
}
this.map.delete(key);
this.map.set(key, entry);
return entry.value;
}
set(key, value, ttlMs) {
const expiresAt = Date.now() + ttlMs;
const entry = { value, expiresAt };
this.map.delete(key);
this.map.set(key, entry);
while (this.map.size > this.maxEntries) {
const firstKey = this.map.keys().next().value;
if (firstKey === void 0) break;
this.map.delete(firstKey);
}
}
delete(key) {
return this.map.delete(key);
}
};
var inFlightJson = /* @__PURE__ */ new Map();
var inFlightTarball = /* @__PURE__ */ new Map();
var inFlightIndex = /* @__PURE__ */ new Map();
var inFlightImplicitEffectiveVersion = /* @__PURE__ */ new Map();
var implicitEffectiveVersionCache = new BoundedTtlCache(MEM_CACHE_MAX_ENTRIES);
var implicitResolutionFailureCache = new BoundedTtlCache(MEM_CACHE_MAX_ENTRIES);
var ImplicitPackageResolutionError = class extends Error {
packageId;
attemptedVersions;
registryUrl;
cachePath;
causes;
constructor(args) {
const attempted = args.attemptedVersions.length > 0 ? args.attemptedVersions.join(", ") : "(none)";
const prefix = `Failed to resolve implicit package ${args.packageId}`;
const meta = `attemptedVersions=[${attempted}] registryUrl=${args.registryUrl} cachePath=${args.cachePath}`;
const causeText = args.causes && args.causes.length > 0 ? ` causes=[${args.causes.join(" | ")}]` : "";
super(`${prefix}. ${meta}.${causeText}`);
this.name = "ImplicitPackageResolutionError";
this.packageId = args.packageId;
this.attemptedVersions = args.attemptedVersions;
this.registryUrl = args.registryUrl;
this.cachePath = args.cachePath;
this.causes = args.causes ?? [];
}
};
var FhirPackageInstallError = class extends Error {
packageId;
version;
registryUrl;
cachePath;
step;
tarballUrl;
constructor(args) {
const safe = (v) => {
if (v == null) return "";
if (typeof v === "string") return v;
if (v instanceof Error) return v.message;
try {
return JSON.stringify(v);
} catch {
return String(v);
}
};
const pkg = `${args.packageId}@${args.version}`;
const meta = `step=${args.step} registryUrl=${args.registryUrl} cachePath=${args.cachePath}`;
const tarball = args.tarballUrl ? ` tarballUrl=${args.tarballUrl}` : "";
const causeText = args.cause ? ` Cause: ${safe(args.cause)}` : "";
super(`Failed to install ${pkg}. ${meta}.${tarball}${causeText}`, { cause: args.cause });
this.name = "FhirPackageInstallError";
this.packageId = args.packageId;
this.version = args.version;
this.registryUrl = args.registryUrl;
this.cachePath = args.cachePath;
this.step = args.step;
this.tarballUrl = args.tarballUrl;
}
};
var withSingleFlight = async (map, key, fn) => {
const existing = map.get(key);
if (existing) return existing;
const p = (async () => {
try {
return await fn();
} finally {
map.delete(key);
}
})();
map.set(key, p);
return p;
};
var sha256Hex = (value) => import_crypto.default.createHash("sha256").update(value).digest("hex");
var memCache = /* @__PURE__ */ new Map();
var memGet = (key) => {
const e = memCache.get(key);
if (!e) return null;
if (typeof e.expiresAt === "number") {
if (Date.now() >= e.expiresAt) {
memCache.delete(key);
return null;
}
}
return e.value;
};
var memSet = (key, value, ttlMs) => {
const expiresAt = Date.now() + ttlMs;
memCache.delete(key);
memCache.set(key, { expiresAt, value });
while (memCache.size > MEM_CACHE_MAX_ENTRIES) {
const firstKey = memCache.keys().next().value;
if (!firstKey) break;
memCache.delete(firstKey);
}
};
var memSetNoTtl = (key, value) => {
memCache.delete(key);
memCache.set(key, { value });
while (memCache.size > MEM_CACHE_MAX_ENTRIES) {
const firstKey = memCache.keys().next().value;
if (!firstKey) break;
memCache.delete(firstKey);
}
};
var defaultLogger = {
info: () => void 0,
warn: () => void 0,
error: () => void 0
};
var limit = pLimit(Math.max(4, Math.min(32, import_os.default.cpus().length)));
var extractResourceIndexEntry = (filename, content) => {
const evalAttribute = (att) => typeof att === "string" ? att : void 0;
const indexEntry = {
filename,
resourceType: content.resourceType,
id: content.id,
url: evalAttribute(content.url),
name: evalAttribute(content.name),
version: evalAttribute(content.version),
kind: evalAttribute(content.kind),
type: evalAttribute(content.type),
supplements: evalAttribute(content.supplements),
content: evalAttribute(content.content),
baseDefinition: evalAttribute(content.baseDefinition),
derivation: evalAttribute(content.derivation),
date: evalAttribute(content.date)
};
return indexEntry;
};
var FhirPackageInstaller = class {
logger = defaultLogger;
registryUrl = "https://packages.fhir.org";
registryDisabled = false;
registryToken;
// optional token for private registries
requestTimeoutMs = 9e4;
// 90 seconds
extractTimeoutMs = 6e4;
// 60 seconds
registryTtlMs = DEFAULT_REGISTRY_TTL_MS;
/**
* Path to the FHIR package cache directory.
* This directory is used to store downloaded and extracted FHIR packages.
* If the directory does not exist, it will be created.
* Default location follows FHIR spec:
* - User apps: ~/.fhir/packages (Windows: C:\Users\<user>\.fhir\packages)
* - System services: /var/lib/.fhir/packages (Windows: %ProgramData%\.fhir\packages)
*/
cachePath;
skipExamples = false;
// skip dependency installation of example packages
allowHttp = false;
// allow HTTP URLs for testing
resolvingImplicitDeps = /* @__PURE__ */ new Set();
installingPackages = /* @__PURE__ */ new Set();
formatPackageForDebug(packageObject) {
return `${packageObject.id}@${packageObject.version}`;
}
formatMaterializationStatusForDebug(status) {
const missingPreview = status.missingFiles.length > 0 ? ` missingFiles=${status.missingFiles.slice(0, 3).join(",")}${status.missingFiles.length > 3 ? ",..." : ""}` : "";
return `materialization=${status.complete ? "complete" : "incomplete"} reason=${status.reason}${missingPreview}`;
}
async describePackageInstallWaitState(packageObject) {
try {
const status = await this.getPackageMaterializationStatus(packageObject, { emitTiming: true });
return this.formatMaterializationStatusForDebug(status);
} catch (error) {
return `materialization-check-error=${error instanceof Error ? error.message : String(error)}`;
}
}
formatElapsedMs(startedAtNs) {
return (Number(process.hrtime.bigint() - startedAtNs) / 1e6).toFixed(1);
}
async withDebugTiming(label, action, describeResult) {
if (!this.logger.debug || typeof this.logger.debug !== "function") {
return await action();
}
const startedAtNs = process.hrtime.bigint();
try {
const result = await action();
const resultText = describeResult ? ` ${describeResult(result)}` : "";
this.logger.debug(`[timing] ${label} completed in ${this.formatElapsedMs(startedAtNs)}ms.${resultText}`);
return result;
} catch (error) {
this.logger.debug(
`[timing] ${label} failed in ${this.formatElapsedMs(startedAtNs)}ms: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
getPackageKey(packageObject) {
return `${packageObject.id}#${packageObject.version}`;
}
normalizeDependencies(dependencies) {
if (dependencies["hl7.fhir.r4.core"] === "4.0.0") {
return {
...dependencies,
"hl7.fhir.r4.core": "4.0.1"
};
}
return dependencies;
}
constructor(config) {
const {
logger,
registryUrl,
registryToken,
cachePath,
skipExamples,
allowHttp,
requestTimeoutMs,
extractTimeoutMs,
registryTtlMs
} = config || {};
if (logger) {
this.logger = logger;
}
const normalizedCachePath = (() => {
if (cachePath == null) {
return void 0;
}
if (typeof cachePath !== "string") {
this.logger.warn?.(
`Non-string cachePath provided (${typeof cachePath}); falling back to FHIR spec default cache path.`
);
return void 0;
}
const trimmed = cachePath.trim();
if (trimmed === "" || trimmed.toLowerCase() === "n/a") {
this.logger.warn?.(
'Non-usable cachePath provided (empty/whitespace or "n/a"); falling back to FHIR spec default cache path.'
);
return void 0;
}
return trimmed;
})();
this.cachePath = normalizedCachePath ?? this.getDefaultCachePath();
if (registryUrl) {
const normalized = registryUrl.trim();
this.registryUrl = registryUrl;
if (normalized.toLowerCase() === "n/a") {
this.registryUrl = "n/a";
this.registryDisabled = true;
}
}
if (registryToken) {
this.registryToken = registryToken;
}
if (allowHttp) {
this.allowHttp = allowHttp;
}
if (typeof requestTimeoutMs === "number" && Number.isFinite(requestTimeoutMs) && requestTimeoutMs > 0) {
this.requestTimeoutMs = requestTimeoutMs;
}
if (typeof extractTimeoutMs === "number" && Number.isFinite(extractTimeoutMs) && extractTimeoutMs > 0) {
this.extractTimeoutMs = extractTimeoutMs;
}
const effectiveRegistryTtlMs = typeof registryTtlMs === "number" && Number.isFinite(registryTtlMs) && registryTtlMs > 0 ? registryTtlMs : void 0;
if (typeof effectiveRegistryTtlMs === "number") {
this.registryTtlMs = effectiveRegistryTtlMs;
}
if (skipExamples) {
this.skipExamples = skipExamples;
}
}
/**
* Determines the default FHIR package cache path based on FHIR specifications:
* https://confluence.hl7.org/display/FHIR/FHIR+Package+Cache
*
* For user applications:
* - Windows: C:\Users\<username>\.fhir\packages
* - Unix/Linux: ~/.fhir/packages
*
* For system services (daemons):
* - Windows: %ProgramData%\.fhir\packages (typically C:\ProgramData\.fhir\packages)
* - Unix/Linux: /var/lib/.fhir/packages
*
* Behavior can be overridden via the FHIR_PACKAGE_CACHE_MODE environment variable:
* - FHIR_PACKAGE_CACHE_MODE=system -> always use system service paths
* - FHIR_PACKAGE_CACHE_MODE=user -> always use user paths
*/
getDefaultCachePath() {
const isWindows = process.platform === "win32";
const homeDir = import_os.default.homedir();
const cacheMode = process.env.FHIR_PACKAGE_CACHE_MODE?.toLowerCase();
let isSystemService;
if (cacheMode === "system") {
isSystemService = true;
} else if (cacheMode === "user") {
isSystemService = false;
} else {
if (isWindows) {
const normalizedHome = import_path.default.normalize(homeDir).toLowerCase();
const systemProfileSuffix = import_path.default.normalize(import_path.default.join("Windows", "System32", "config", "systemprofile")).toLowerCase();
isSystemService = normalizedHome.endsWith(systemProfileSuffix);
} else {
const isRoot = process.getuid?.() === 0;
const isSudo = !!process.env.SUDO_USER;
const hasDisplay = !!process.env.DISPLAY;
const hasSshConnection = !!process.env.SSH_CONNECTION;
const hasTerm = !!process.env.TERM;
isSystemService = Boolean(
isRoot && !isSudo && !hasDisplay && !hasSshConnection && !hasTerm
);
}
}
if (isSystemService) {
if (isWindows) {
let programData = process.env.ProgramData;
if (!programData || programData.trim() === "") {
const fallbackProgramData = "C:\\ProgramData";
try {
if (import_fs_extra.default.pathExistsSync(fallbackProgramData)) {
import_fs_extra.default.accessSync(fallbackProgramData, import_fs_extra.default.constants.W_OK);
this.logger.warn(
'ProgramData environment variable is not set; using fallback "C:\\ProgramData" for system service cache directory.'
);
programData = fallbackProgramData;
} else {
this.logger.warn(
'ProgramData environment variable is not set and fallback "C:\\ProgramData" does not exist. Falling back to user cache directory.'
);
}
} catch {
this.logger.warn(
'ProgramData environment variable is not set and fallback "C:\\ProgramData" is not writable. Falling back to user cache directory.'
);
}
}
if (programData) {
return import_path.default.join(programData, ".fhir", "packages");
}
return import_path.default.join(homeDir, ".fhir", "packages");
} else {
return "/var/lib/.fhir/packages";
}
}
return import_path.default.join(homeDir, ".fhir", "packages");
}
async withDiskLock(lockKey, fn, options) {
const locksDir = await this.ensureDiskCacheSubdir("locks");
const lockPath = import_path.default.join(locksDir, `${sha256Hex(lockKey)}.lock`);
const start = Date.now();
const maxWaitMs = Math.max(1e3, this.requestTimeoutMs);
const staleMs = Math.max(2 * 60 * 1e3, maxWaitMs * 2);
const debugLabel = options?.debugLabel?.trim();
const debugEnabled = Boolean(debugLabel) && typeof this.logger.debug === "function";
const waitLogIntervalMs = Math.max(250, options?.waitLogIntervalMs ?? PACKAGE_INSTALL_WAIT_LOG_INTERVAL_MS);
const proceedWithoutLockAfterTimeout = options?.proceedWithoutLockAfterTimeout ?? true;
let contentionObserved = false;
let lastWaitLogAt = 0;
const readWaitState = async () => {
if (!debugEnabled || !options?.describeWaitState) {
return null;
}
try {
return await options.describeWaitState();
} catch (error) {
return `wait-state-error=${error instanceof Error ? error.message : String(error)}`;
}
};
while (true) {
try {
await import_fs_extra.default.ensureDir(import_path.default.dirname(lockPath));
await import_fs_extra.default.writeFile(lockPath, `${process.pid}
${Date.now()}
`, { flag: "wx" });
if (debugEnabled) {
const waitedMs = Date.now() - start;
this.logger.debug?.(
contentionObserved ? `Claimed ${debugLabel} after waiting ${waitedMs}ms.` : `Claimed ${debugLabel}.`
);
}
const heartbeat = setInterval(() => {
import_fs_extra.default.utimes(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
}, 1e3);
heartbeat.unref?.();
try {
return await fn();
} finally {
clearInterval(heartbeat);
await import_fs_extra.default.remove(lockPath).catch(() => void 0);
if (debugEnabled) {
this.logger.debug?.(`Released ${debugLabel}.`);
}
}
} catch (e) {
if (e?.code !== "EEXIST") {
if (debugEnabled) {
this.logger.debug?.(
`Skipping ${debugLabel} because the lock could not be created (${e?.code || e?.message || String(e)}); proceeding without the lock.`
);
}
return await fn();
}
const elapsedMs = Date.now() - start;
let lockAgeMs = null;
let waitState = null;
if (!contentionObserved && debugEnabled) {
waitState = await readWaitState();
this.logger.debug?.(
`Another process holds ${debugLabel}; entering wait loop.${waitState ? ` Current materialization state: ${waitState}.` : ""}`
);
}
contentionObserved = true;
try {
const stat = await import_fs_extra.default.stat(lockPath);
lockAgeMs = Date.now() - stat.mtimeMs;
if (lockAgeMs > staleMs) {
if (debugEnabled) {
waitState ??= await readWaitState();
this.logger.debug?.(
`Breaking stale ${debugLabel} after waiting ${elapsedMs}ms (lockAgeMs=${Math.round(lockAgeMs)}).${waitState ? ` Current materialization state: ${waitState}.` : ""}`
);
}
await import_fs_extra.default.remove(lockPath).catch(() => void 0);
continue;
}
} catch {
}
if (Date.now() - start > maxWaitMs) {
if (proceedWithoutLockAfterTimeout) {
if (debugEnabled) {
waitState ??= await readWaitState();
this.logger.debug?.(
`Waited ${elapsedMs}ms for ${debugLabel}, exceeding maxWaitMs=${maxWaitMs}; proceeding without the lock.${waitState ? ` Current materialization state: ${waitState}.` : ""}`
);
}
return await fn();
}
if (debugEnabled && Date.now() - lastWaitLogAt >= waitLogIntervalMs) {
waitState ??= await readWaitState();
this.logger.debug?.(
`Waited ${elapsedMs}ms for ${debugLabel}, exceeding maxWaitMs=${maxWaitMs}; continuing to wait for the live lock holder.${waitState ? ` Current materialization state: ${waitState}.` : ""}`
);
lastWaitLogAt = Date.now();
}
}
if (debugEnabled && Date.now() - lastWaitLogAt >= waitLogIntervalMs) {
waitState ??= await readWaitState();
const lockAgeText = typeof lockAgeMs === "number" ? ` lockAgeMs=${Math.round(lockAgeMs)}.` : "";
this.logger.debug?.(
`Still waiting for ${debugLabel} after ${elapsedMs}ms.${lockAgeText}${waitState ? ` Current materialization state: ${waitState}.` : ""}`
);
lastWaitLogAt = Date.now();
}
await new Promise((r) => setTimeout(r, 50 + Math.floor(Math.random() * 100)));
}
}
}
async tryWithDiskLock(lockKey, fn, options) {
const locksDir = await this.ensureDiskCacheSubdir("locks");
const lockPath = import_path.default.join(locksDir, `${sha256Hex(lockKey)}.lock`);
const debugLabel = options?.debugLabel?.trim();
const debugEnabled = Boolean(debugLabel) && typeof this.logger.debug === "function";
try {
await import_fs_extra.default.ensureDir(import_path.default.dirname(lockPath));
await import_fs_extra.default.writeFile(lockPath, `${process.pid}
${Date.now()}
`, { flag: "wx" });
if (debugEnabled) {
this.logger.debug?.(`Claimed ${debugLabel}.`);
}
const heartbeat = setInterval(() => {
import_fs_extra.default.utimes(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
}, 1e3);
heartbeat.unref?.();
try {
return { acquired: true, result: await fn() };
} finally {
clearInterval(heartbeat);
await import_fs_extra.default.remove(lockPath).catch(() => void 0);
if (debugEnabled) {
this.logger.debug?.(`Released ${debugLabel}.`);
}
}
} catch (e) {
if (e?.code === "EEXIST") {
return { acquired: false };
}
if (debugEnabled) {
this.logger.debug?.(
`Skipping ${debugLabel} because the lock could not be created (${e?.code || e?.message || String(e)}); proceeding without the lock.`
);
}
return { acquired: true, result: await fn() };
}
}
// ---- Per-cachePath persistent cache helpers ----
async ensureDiskCacheSubdir(name) {
const dir = import_path.default.join(this.cachePath, ".fpi.cache", name);
await import_fs_extra.default.ensureDir(dir);
return dir;
}
async createWorkingTempDir(options) {
if (options?.preferCache !== false) {
try {
const cacheTempRoot = await this.ensureDiskCacheSubdir("tmp");
return createTempDir(cacheTempRoot);
} catch {
}
}
return createTempDir();
}
getPackageInstallLockKey(packageObject) {
return `package-install|${packageObject.id}#${packageObject.version}`;
}
async getInstallParticipantDir(packageObject) {
const participantsRoot = await this.ensureDiskCacheSubdir("installers");
return import_path.default.join(participantsRoot, sha256Hex(`install-participants|${this.getPackageKey(packageObject)}`));
}
async withInstallParticipant(packageObject, fn) {
const participantsDir = await this.getInstallParticipantDir(packageObject);
const participantPath = import_path.default.join(
participantsDir,
`${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.participant`
);
await import_fs_extra.default.ensureDir(participantsDir);
await import_fs_extra.default.writeFile(participantPath, `${process.pid}
${Date.now()}
`, "utf8");
const heartbeat = setInterval(() => {
import_fs_extra.default.utimes(participantPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
}, 1e3);
heartbeat.unref?.();
try {
return await fn();
} finally {
clearInterval(heartbeat);
await import_fs_extra.default.remove(participantPath).catch(() => void 0);
}
}
async countActiveInstallParticipants(packageObject) {
const participantsDir = await this.getInstallParticipantDir(packageObject);
if (!await import_fs_extra.default.exists(participantsDir)) {
return 0;
}
const staleMs = Math.max(2 * 60 * 1e3, this.requestTimeoutMs * 2);
const now = Date.now();
const entries = await import_fs_extra.default.readdir(participantsDir);
let activeCount = 0;
for (const entry of entries) {
const entryPath = import_path.default.join(participantsDir, entry);
try {
const stat = await import_fs_extra.default.stat(entryPath);
if (now - stat.mtimeMs <= staleMs) {
activeCount += 1;
} else {
await import_fs_extra.default.remove(entryPath).catch(() => void 0);
}
} catch {
}
}
return activeCount;
}
async isPackageInstallLockHeld(packageObject) {
const locksDir = await this.ensureDiskCacheSubdir("locks");
const lockPath = import_path.default.join(locksDir, `${sha256Hex(this.getPackageInstallLockKey(packageObject))}.lock`);
return await import_fs_extra.default.exists(lockPath);
}
async waitForPeerDependencyHandoff(rootPackage, pendingDependencies) {
if (pendingDependencies.length === 0) {
return;
}
const startedAt = Date.now();
let peerDiscoveryGraceApplied = false;
while (Date.now() - startedAt < DEPENDENCY_PEER_HANDOFF_WAIT_MS) {
for (const dependency of pendingDependencies) {
if (await this.isStrictlyMaterialized(dependency) || await this.isPackageInstallLockHeld(dependency)) {
return;
}
}
if (await this.countActiveInstallParticipants(rootPackage) <= 1) {
if (!peerDiscoveryGraceApplied) {
peerDiscoveryGraceApplied = true;
await new Promise((resolve) => setTimeout(resolve, DEPENDENCY_PEER_DISCOVERY_GRACE_MS));
continue;
}
return;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
async withPackageInstallLock(packageObject, fn) {
const lockKey = this.getPackageInstallLockKey(packageObject);
return await this.withDiskLock(lockKey, async () => {
return await fn();
}, {
debugLabel: `package install ${this.formatPackageForDebug(packageObject)}`,
describeWaitState: async () => await this.describePackageInstallWaitState(packageObject),
proceedWithoutLockAfterTimeout: false
});
}
async tryWithPackageInstallLock(packageObject, fn) {
return await this.tryWithDiskLock(this.getPackageInstallLockKey(packageObject), fn, {
debugLabel: `package install ${this.formatPackageForDebug(packageObject)}`
});
}
async getStagingPath() {
return await this.ensureDiskCacheSubdir("staging");
}
async cleanupStaleStagingDirectories(maxAgeMs = FPI_STAGING_MAX_AGE_MS) {
try {
const stagingPath = await this.getStagingPath();
const entries = await import_fs_extra.default.readdir(stagingPath);
const now = Date.now();
for (const entry of entries) {
const entryPath = import_path.default.join(stagingPath, entry);
try {
const stats = await import_fs_extra.default.stat(entryPath);
if (now - stats.mtimeMs > maxAgeMs) {
await import_fs_extra.default.remove(entryPath);
}
} catch {
}
}
} catch {
}
}
async createStagingDirectory(packageObject) {
await this.cleanupStaleStagingDirectories();
const stagingPath = await this.getStagingPath();
const dirName = `${await this.toDirName(packageObject)}.${process.pid}.${Date.now()}.${import_crypto.default.randomBytes(4).toString("hex")}`;
const fullPath = import_path.default.join(stagingPath, dirName);
await import_fs_extra.default.ensureDir(fullPath);
return fullPath;
}
async buildPackageIndexFromPackageDir(packageDir) {
return await this.withDebugTiming(
`build-package-index packageDir=${packageDir}`,
async () => {
const discoverStartedAtNs = process.hrtime.bigint();
const fileList = await import_fs_extra.default.readdir(packageDir);
const candidateFiles = fileList.filter(
(file) => file.endsWith(".json") && file !== "package.json" && !file.endsWith(".index.json")
);
this.logger.debug?.(
`[index] Discovered ${candidateFiles.length} candidate JSON resources in ${packageDir} in ${this.formatElapsedMs(discoverStartedAtNs)}ms.`
);
const parseStartedAtNs = process.hrtime.bigint();
const files = await Promise.all(
candidateFiles.map(
(file) => limit(
async () => {
const contentText = await import_fs_extra.default.readFile(import_path.default.join(packageDir, file), { encoding: "utf8" });
const content = JSON.parse(contentText);
return extractResourceIndexEntry(file, content);
}
)
)
);
this.logger.debug?.(
`[index] Parsed ${files.length} resource entries from ${packageDir} in ${this.formatElapsedMs(parseStartedAtNs)}ms.`
);
return {
"index-version": 2,
files
};
},
(indexJson) => `fileCount=${indexJson.files.length}`
);
}
normalizeIndexEntry(entry) {
const filename = typeof entry.filename === "string" ? entry.filename : null;
const resourceType = typeof entry.resourceType === "string" ? entry.resourceType : null;
const id = typeof entry.id === "string" ? entry.id : null;
if (!filename) {
return null;
}
if (!resourceType || !id) {
return null;
}
const readOptionalString = (key) => {
const value = entry[key];
return typeof value === "string" ? value : void 0;
};
return {
filename,
resourceType,
id,
url: readOptionalString("url"),
name: readOptionalString("name"),
version: readOptionalString("version"),
kind: readOptionalString("kind"),
type: readOptionalString("type"),
supplements: readOptionalString("supplements"),
content: readOptionalString("content"),
baseDefinition: readOptionalString("baseDefinition"),
derivation: readOptionalString("derivation"),
date: readOptionalString("date")
};
}
normalizePackageIndex(raw) {
if (!raw || typeof raw !== "object") {
return null;
}
const candidate = raw;
if (!Array.isArray(candidate.files)) {
return null;
}
const files = [];
for (const file of candidate.files) {
if (!file || typeof file !== "object") {
return null;
}
const normalized = this.normalizeIndexEntry(file);
if (!normalized) {
return null;
}
files.push(normalized);
}
return {
"index-version": 2,
files
};
}
async persistMaterializedPackageIndex(packageObject, packageDir, indexJson) {
const indexPath = import_path.default.join(packageDir, ".fpi.index.json");
await import_fs_extra.default.writeJSON(indexPath, indexJson);
const memKey = this.getIndexMemKey(packageObject);
memSetNoTtl(memKey, indexJson);
try {
const diskPath = this.getDiskIndexCachePath(packageObject);
await this.ensureDiskCacheSubdir("indexes");
await this.writeDiskCacheJsonNoTtl(diskPath, indexJson);
} catch {
}
return indexJson;
}
async tryMaterializeLegacyPackageIndex(packageObject, packageDir) {
const legacyIndexPath = import_path.default.join(packageDir, ".index.json");
if (!await import_fs_extra.default.exists(legacyIndexPath)) {
return null;
}
return await this.withDiskLock(this.getIndexDiskLockKey(packageObject), async () => {
const fpiIndexPath = import_path.default.join(packageDir, ".fpi.index.json");
if (await import_fs_extra.default.exists(fpiIndexPath)) {
const current = await import_fs_extra.default.readJSON(fpiIndexPath, { encoding: "utf8" });
return this.normalizePackageIndex(current);
}
const legacyRaw = await import_fs_extra.default.readJSON(legacyIndexPath, { encoding: "utf8" });
const legacyIndex = this.normalizePackageIndex(legacyRaw);
if (!legacyIndex) {
return null;
}
for (const file of legacyIndex.files) {
if (!await import_fs_extra.default.exists(import_path.default.join(packageDir, file.filename))) {
return null;
}
}
return await this.persistMaterializedPackageIndex(packageObject, packageDir, legacyIndex);
}, {
proceedWithoutLockAfterTimeout: false
});
}
async materializePackageIndex(packageObject, packageDir) {
return await this.withDebugTiming(
`materialize-package-index ${this.formatPackageForDebug(packageObject)}`,
async () => {
const memKey = this.getIndexMemKey(packageObject);
const memHit = memGet(memKey);
if (memHit) {
return await this.persistMaterializedPackageIndex(packageObject, packageDir, memHit);
}
return await this.withDiskLock(this.getIndexDiskLockKey(packageObject), async () => {
const memHit2 = memGet(memKey);
if (memHit2) {
return await this.persistMaterializedPackageIndex(packageObject, packageDir, memHit2);
}
const diskPath = this.getDiskIndexCachePath(packageObject);
const diskHit = await withSingleFlight(inFlightIndex, `disk-${memKey}`, async () => {
await this.ensureDiskCacheSubdir("indexes");
return await this.readDiskCacheJson(diskPath);
});
if (diskHit) {
return await this.persistMaterializedPackageIndex(packageObject, packageDir, diskHit);
}
const indexJson = await this.buildPackageIndexFromPackageDir(packageDir);
return await this.persistMaterializedPackageIndex(packageObject, packageDir, indexJson);
}, {
proceedWithoutLockAfterTimeout: false
});
},
(indexJson) => `fileCount=${indexJson.files.length}`
);
}
async getPackageMaterializationStatus(packageObject, options) {
const computeStatus = async () => {
const packageRoot = await this.getPackageDirPath(packageObject);
if (!await import_fs_extra.default.exists(packageRoot)) {
return { complete: false, reason: "package-root-missing", missingFiles: [] };
}
const packageDir = import_path.default.join(packageRoot, "package");
if (!await import_fs_extra.default.exists(packageDir)) {
return { complete: false, reason: "package-dir-missing", missingFiles: [] };
}
const manifestPath = import_path.default.join(packageDir, "package.json");
if (!await import_fs_extra.default.exists(manifestPath)) {
return { complete: false, reason: "manifest-missing", missingFiles: [] };
}
try {
await import_fs_extra.default.readJSON(manifestPath, { encoding: "utf8" });
} catch {
return { complete: false, reason: "manifest-invalid", missingFiles: [] };
}
const indexPath = import_path.default.join(packageDir, ".fpi.index.json");
if (!await import_fs_extra.default.exists(indexPath)) {
const legacyMaterializedIndex = await this.tryMaterializeLegacyPackageIndex(packageObject, packageDir);
if (!legacyMaterializedIndex) {
try {
await this.materializePackageIndex(packageObject, packageDir);
} catch {
return { complete: false, reason: "index-missing", missingFiles: [] };
}
if (!await import_fs_extra.default.exists(indexPath)) {
return { complete: false, reason: "index-missing", missingFiles: [] };
}
}
}
if (await this.hasFreshMaterializationMarker(packageRoot, packageDir, manifestPath, indexPath)) {
return { complete: true, reason: "complete", missingFiles: [] };
}
let indexJson;
try {
indexJson = await import_fs_extra.default.readJSON(indexPath, { encoding: "utf8" });
} catch {
return { complete: false, reason: "index-invalid", missingFiles: [] };
}
if (!Array.isArray(indexJson.files)) {
return { complete: false, reason: "index-invalid", missingFiles: [] };
}
const packageDirEntries = new Set(await import_fs_extra.default.readdir(packageDir));
const missingFiles = [];
for (const file of indexJson.files) {
const filename = typeof file?.filename === "string" ? file.filename : null;
if (!filename) {
return { complete: false, reason: "index-invalid", missingFiles: [] };
}
const canUseDirectoryListing = !filename.includes("/") && !filename.includes("\\");
if (canUseDirectoryListing ? !packageDirEntries.has(filename) : !await import_fs_extra.default.exists(import_path.default.join(packageDir, filename))) {
missingFiles.push(filename);
}
}
if (missingFiles.length > 0) {
return { complete: false, reason: "indexed-files-missing", missingFiles };
}
await this.writeMaterializationMarker(packageRoot, packageDir, manifestPath, indexPath);
return { complete: true, reason: "complete", missingFiles: [] };
};
if (!options?.emitTiming) {
return await computeStatus();
}
return await this.withDebugTiming(
`materialization-status ${this.formatPackageForDebug(packageObject)}`,
computeStatus,
(status) => this.formatMaterializationStatusForDebug(status)
);
}
async stagePackageForPublish(packageObject, src, move) {
return await this.withDebugTiming(
`stage-package-for-publish ${this.formatPackageForDebug(packageObject)}`,
async () => {
const stagingRoot = await this.createStagingDirectory(packageObject);
const sourcePackageDir = await import_fs_extra.default.exists(import_path.default.join(src, "package")) ? import_path.default.join(src, "package") : src;
const stagingPackageDir = import_path.default.join(stagingRoot, "package");
const packageLabel = this.formatPackageForDebug(packageObject);
try {
const action = move ? import_fs_extra.default.move : import_fs_extra.default.copy;
const copyOrMoveStartedAtNs = process.hrtime.bigint();
await action(sourcePackageDir, stagingPackageDir, { overwrite: false });
this.logger.debug?.(
`[publish] ${move ? "Moved" : "Copied"} staged contents for ${packageLabel} from ${sourcePackageDir} to ${stagingPackageDir} in ${this.formatElapsedMs(copyOrMoveStartedAtNs)}ms.`
);
if (move && sourcePackageDir !== src) {
const cleanupStartedAtNs = process.hrtime.bigint();
await import_fs_extra.default.remove(src).catch(() => void 0);
this.logger.debug?.(
`[publish] Removed extraction root ${src} after staging ${packageLabel} in ${this.formatElapsedMs(cleanupStartedAtNs)}ms.`
);
}
const materializeIndexStartedAtNs = process.hrtime.bigint();
const materializedIndex = await this.materializePackageIndex(packageObject, stagingPackageDir);
this.logger.debug?.(
`[publish] Materialized package index for ${packageLabel} in staging in ${this.formatElapsedMs(materializeIndexStartedAtNs)}ms (fileCount=${materializedIndex.files.length}).`
);
const markerStartedAtNs = process.hrtime.bigint();
await this.writeMaterializationMarker(
stagingRoot,
stagingPackageDir,
import_path.default.join(stagingPackageDir, "package.json"),
import_path.default.join(stagingPackageDir, ".fpi.index.json")
);
this.logger.debug?.(
`[publish] Wrote materialization marker for ${packageLabel} in ${this.formatElapsedMs(markerStartedAtNs)}ms.`
);
return stagingRoot;
} catch (error) {
await import_fs_extra.default.remove(stagingRoot).catch(() => void 0);
throw error;
}
}
);
}
isAlreadyExistsError(error) {
if (!error || typeof error !== "object") {
return false;
}
const candidate = error;
return candidate.code === "EEXIST" || candidate.code === "ENOTEMPTY" || /dest already exists/i.test(candidate.message ?? "");
}
isRetryableStagePublishError(error) {
if (!error || typeof error !== "object") {
return false;
}
const candidate = error;
return candidate.code === "EACCES" || candidate.code === "EPERM" || candidate.code === "EBUSY";
}
getDiskCacheKeyPrefix() {
return `${this.registryUrl}`;
}
async readDiskCacheJson(filePath) {
try {
if (!await import_fs_extra.default.exists(filePath)) return null;
const raw = await import_fs_extra.default.readJSON(filePath, { encoding: "utf8" });
if (!raw || typeof raw !== "object") return null;
if (typeof raw.expiresAt === "number" && "data" in raw) {
if (Date.now() >= raw.expiresAt) {
await import_fs_extra.default.remove(filePath).catch(() => void 0);
return null;
}
return raw.data;
}
return raw;
} catch {
return null;
}
}
async writeDiskCacheJson(filePath, data, ttlMs) {
try {
await import_fs_extra.default.ensureDir(import_path.default.dirname(filePath));
const expiresAt = Date.now() + ttlMs;
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
await import_fs_extra.default.writeJSON(tmp, { expiresAt, data });
await import_fs_extra.default.move(tmp, filePath, { overwrite: true });
} catch {
}
}
async writeDiskCacheJsonNoTtl(filePath, data) {
try {
await import_fs_extra.default.ensureDir(import_path.default.dirname(filePath));
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
await import_fs_extra.default.writeJSON(tmp, data);
await import_fs_extra.default.move(tmp, filePath, { overwrite: true });
} catch {
}
}
getDiskRegistryMetadataCachePath(packageName) {
const key = `registry-meta|${this.getDiskCacheKeyPrefix()}|${packageName}`;
return import_path.default.join(this.cachePath, ".fpi.cache", "metadata", `${sha256Hex(key)}.json`);
}
getDiskIndexCachePath(packageObject) {
const key = `index|${FPI_INDEX_CACHE_VERSION}|${packageObject.id}#${packageObject.version}`;
return import_path.default.join(this.cachePath, ".fpi.cache", "indexes", `${sha256Hex(key)}.json`);
}
getDiskTarballCacheKey(packageObject) {
return `tarball|${this.getDiskCacheKeyPrefix()}|${packageObject.id}#${packageObject.version}`;
}
async getDiskTarballCachePaths(packageObject) {
const tarDir = await this.ensureDiskCacheSubdir("tarballs");
const tgzPath = import_path.default.join(tarDir, `${sha256Hex(this.getDiskTarballCacheKey(packageObject))}.tgz`);
const donePath = `${tgzPath}.done`;
return { tgzPath, donePath };
}
async readDiskTarballCache(packageObject) {
try {
const { tgzPath, donePath } = await this.getDiskTarballCachePaths(packageObject);
if (!await import_fs_extra.default.exists(tgzPath)) return null;
if (!await import_fs_extra.default.exists(donePath)) {
await import_fs_extra.default.remove(tgzPath).catch(() => void 0);
return null;
}
return tgzPath;
} catch {
return null;
}
}
async writeDiskTarballDoneMarker(donePath) {
try {
const tmp = `${donePath}.${process.pid}.${Date.now()}.tmp`;
await import_fs_extra.default.writeFile(tmp, "ok");
await import_fs_extra.default.move(tmp, donePath, { overwrite: true });
} catch {
}
}
getIndexMemKey(packageObject) {
return `index|${FPI_INDEX_CACHE_VERSION}|${packageObject.id}#${packageObject.version}`;
}
getIndexDiskLockKey(packageObject) {
return `index-cache|${FPI_INDEX_CACHE_VERSION}|${packageObject.id}#${packageObject.version}`;
}
getMaterializationMarkerPath(packageRoot) {
return import_path.default.join(packageRoot, FPI_MATERIALIZATION_MARKER);
}
async writeMaterializationMarker(packageRoot, packageDir, manifestPath, indexPath) {
const markerPath = this.getMaterializationMarkerPath(packageRoot);
const tmpPath = `${markerPath}.${process.pid}.${Date.now()}.tmp`;
try {
const [packageDirStat, manifestStat, indexStat] = await Promise.all([
import_fs_extra.default.stat(packageDir),
import_fs_extra.default.stat(manifestPath),
import_fs_extra.default.stat(indexPath)
]);
await import_fs_extra.default.writeJSON(tmpPath, {
packageDirMtimeMs: packageDirStat.mtimeMs,
packageDirCtimeMs: packageDirStat.ctimeMs,
man