fhir-package-installer
Version:
A utility module for downloading, indexing, caching, and managing FHIR packages from the FHIR Package Registry and Simplifier
1,370 lines (1,367 loc) • 116 kB
JavaScript
// src/index.ts
import https from "https";
import http from "http";
import fs from "fs-extra";
// 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
import path from "path";
import { finished, pipeline } from "stream/promises";
import * as tar from "tar-stream";
import * as zlib from "zlib";
import os from "os";
import semver from "semver";
import crypto from "crypto";
var tempDirs = /* @__PURE__ */ new Set();
var tempCleanupRegistered = false;
var registerTempCleanup = () => {
if (tempCleanupRegistered) return;
tempCleanupRegistered = true;
process.once("exit", () => {
for (const dir of tempDirs) {
try {
fs.removeSync(dir);
} catch {
}
}
tempDirs.clear();
});
};
var createTempDir = (baseDir) => {
registerTempCleanup();
const parentDir = baseDir ?? os.tmpdir();
fs.ensureDirSync(parentDir);
const dir = fs.mkdtempSync(path.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 = semver.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) => crypto.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, os.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 = os.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 = path.normalize(homeDir).toLowerCase();
const systemProfileSuffix = path.normalize(path.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 (fs.pathExistsSync(fallbackProgramData)) {
fs.accessSync(fallbackProgramData, fs.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 path.join(programData, ".fhir", "packages");
}
return path.join(homeDir, ".fhir", "packages");
} else {
return "/var/lib/.fhir/packages";
}
}
return path.join(homeDir, ".fhir", "packages");
}
async withDiskLock(lockKey, fn, options) {
const locksDir = await this.ensureDiskCacheSubdir("locks");
const lockPath = path.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 fs.ensureDir(path.dirname(lockPath));
await fs.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(() => {
fs.utimes(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
}, 1e3);
heartbeat.unref?.();
try {
return await fn();
} finally {
clearInterval(heartbeat);
await fs.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 fs.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 fs.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 = path.join(locksDir, `${sha256Hex(lockKey)}.lock`);
const debugLabel = options?.debugLabel?.trim();
const debugEnabled = Boolean(debugLabel) && typeof this.logger.debug === "function";
try {
await fs.ensureDir(path.dirname(lockPath));
await fs.writeFile(lockPath, `${process.pid}
${Date.now()}
`, { flag: "wx" });
if (debugEnabled) {
this.logger.debug?.(`Claimed ${debugLabel}.`);
}
const heartbeat = setInterval(() => {
fs.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 fs.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 = path.join(this.cachePath, ".fpi.cache", name);
await fs.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 path.join(participantsRoot, sha256Hex(`install-participants|${this.getPackageKey(packageObject)}`));
}
async withInstallParticipant(packageObject, fn) {
const participantsDir = await this.getInstallParticipantDir(packageObject);
const participantPath = path.join(
participantsDir,
`${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.participant`
);
await fs.ensureDir(participantsDir);
await fs.writeFile(participantPath, `${process.pid}
${Date.now()}
`, "utf8");
const heartbeat = setInterval(() => {
fs.utimes(participantPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
}, 1e3);
heartbeat.unref?.();
try {
return await fn();
} finally {
clearInterval(heartbeat);
await fs.remove(participantPath).catch(() => void 0);
}
}
async countActiveInstallParticipants(packageObject) {
const participantsDir = await this.getInstallParticipantDir(packageObject);
if (!await fs.exists(participantsDir)) {
return 0;
}
const staleMs = Math.max(2 * 60 * 1e3, this.requestTimeoutMs * 2);
const now = Date.now();
const entries = await fs.readdir(participantsDir);
let activeCount = 0;
for (const entry of entries) {
const entryPath = path.join(participantsDir, entry);
try {
const stat = await fs.stat(entryPath);
if (now - stat.mtimeMs <= staleMs) {
activeCount += 1;
} else {
await fs.remove(entryPath).catch(() => void 0);
}
} catch {
}
}
return activeCount;
}
async isPackageInstallLockHeld(packageObject) {
const locksDir = await this.ensureDiskCacheSubdir("locks");
const lockPath = path.join(locksDir, `${sha256Hex(this.getPackageInstallLockKey(packageObject))}.lock`);
return await fs.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 fs.readdir(stagingPath);
const now = Date.now();
for (const entry of entries) {
const entryPath = path.join(stagingPath, entry);
try {
const stats = await fs.stat(entryPath);
if (now - stats.mtimeMs > maxAgeMs) {
await fs.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()}.${crypto.randomBytes(4).toString("hex")}`;
const fullPath = path.join(stagingPath, dirName);
await fs.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 fs.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 fs.readFile(path.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 = path.join(packageDir, ".fpi.index.json");
await fs.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 = path.join(packageDir, ".index.json");
if (!await fs.exists(legacyIndexPath)) {
return null;
}
return await this.withDiskLock(this.getIndexDiskLockKey(packageObject), async () => {
const fpiIndexPath = path.join(packageDir, ".fpi.index.json");
if (await fs.exists(fpiIndexPath)) {
const current = await fs.readJSON(fpiIndexPath, { encoding: "utf8" });
return this.normalizePackageIndex(current);
}
const legacyRaw = await fs.readJSON(legacyIndexPath, { encoding: "utf8" });
const legacyIndex = this.normalizePackageIndex(legacyRaw);
if (!legacyIndex) {
return null;
}
for (const file of legacyIndex.files) {
if (!await fs.exists(path.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 fs.exists(packageRoot)) {
return { complete: false, reason: "package-root-missing", missingFiles: [] };
}
const packageDir = path.join(packageRoot, "package");
if (!await fs.exists(packageDir)) {
return { complete: false, reason: "package-dir-missing", missingFiles: [] };
}
const manifestPath = path.join(packageDir, "package.json");
if (!await fs.exists(manifestPath)) {
return { complete: false, reason: "manifest-missing", missingFiles: [] };
}
try {
await fs.readJSON(manifestPath, { encoding: "utf8" });
} catch {
return { complete: false, reason: "manifest-invalid", missingFiles: [] };
}
const indexPath = path.join(packageDir, ".fpi.index.json");
if (!await fs.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 fs.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 fs.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 fs.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 fs.exists(path.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 fs.exists(path.join(src, "package")) ? path.join(src, "package") : src;
const stagingPackageDir = path.join(stagingRoot, "package");
const packageLabel = this.formatPackageForDebug(packageObject);
try {
const action = move ? fs.move : fs.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 fs.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,
path.join(stagingPackageDir, "package.json"),
path.join(stagingPackageDir, ".fpi.index.json")
);
this.logger.debug?.(
`[publish] Wrote materialization marker for ${packageLabel} in ${this.formatElapsedMs(markerStartedAtNs)}ms.`
);
return stagingRoot;
} catch (error) {
await fs.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 fs.exists(filePath)) return null;
const raw = await fs.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 fs.remove(filePath).catch(() => void 0);
return null;
}
return raw.data;
}
return raw;
} catch {
return null;
}
}
async writeDiskCacheJson(filePath, data, ttlMs) {
try {
await fs.ensureDir(path.dirname(filePath));
const expiresAt = Date.now() + ttlMs;
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
await fs.writeJSON(tmp, { expiresAt, data });
await fs.move(tmp, filePath, { overwrite: true });
} catch {
}
}
async writeDiskCacheJsonNoTtl(filePath, data) {
try {
await fs.ensureDir(path.dirname(filePath));
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
await fs.writeJSON(tmp, data);
await fs.move(tmp, filePath, { overwrite: true });
} catch {
}
}
getDiskRegistryMetadataCachePath(packageName) {
const key = `registry-meta|${this.getDiskCacheKeyPrefix()}|${packageName}`;
return path.join(this.cachePath, ".fpi.cache", "metadata", `${sha256Hex(key)}.json`);
}
getDiskIndexCachePath(packageObject) {
const key = `index|${FPI_INDEX_CACHE_VERSION}|${packageObject.id}#${packageObject.version}`;
return path.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 = path.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 fs.exists(tgzPath)) return null;
if (!await fs.exists(donePath)) {
await fs.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 fs.writeFile(tmp, "ok");
await fs.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 path.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([
fs.stat(packageDir),
fs.stat(manifestPath),
fs.stat(indexPath)
]);
await fs.writeJSON(tmpPath, {
packageDirMtimeMs: packageDirStat.mtimeMs,
packageDirCtimeMs: packageDirStat.ctimeMs,
manifestMtimeMs: manifestStat.mtimeMs,
manifestCtimeMs: manifestStat.ctimeMs,
indexMtimeMs: indexStat.mtimeMs,
indexCtimeMs: indexStat.ctimeMs
});
await fs.move(tmpPath, markerPath, { overwrite: true });
} catch {
} finally {
await fs.remove(tmpPath).catch(() => void 0);
}
}
async hasFreshMaterializationMarker(packageRoot, packageDir, manifestPath, indexPath) {
const markerPath = this.getMaterializationMarkerPath(packageRoot);
try {
const [marker, packageDirStat, manifestStat, indexStat] = await Promise.all([
fs.readJSON(markerPath, { encoding: "utf8" }),
fs.stat(packageDir),
fs.stat(manifestPath),
fs.stat(indexPath)
]);
return marker.packageDirMtimeMs === packageDirStat.mtimeMs && marker.packageDirCtimeMs === packageDirStat.ctimeMs && marker.manifestMtimeMs === manifestStat.mtimeMs && marker.manifestCtimeMs === manifestStat.ctimeMs && marker.indexMtimeMs === indexStat.mtimeMs && marker.indexCtimeMs === indexStat.ctimeMs;
} catch {
return false;
}
}
isRegistryDisabled() {
return this.registryDisabled;
}
formatRegistryDisabledMessage(detail) {
return `FHIR package registry is disabled (registryUrl=n/a). ${detail}`;
}
async hasShallowInstalledPackage(packageObject) {
const packageRoot = await this.getPackageDirPath(packageObject);
if (!await fs.exists(packageRoot)) {
return false;
}
const packageDir = path.join(packageRoot, "package");
if (!await fs.exists(packageDir)) {
return false;
}
const manifestPath = path.join(packageDir, "package.json");
if (!await fs.exists(manifestPath)) {
return false;
}
const indexPath = path.join(packageDir, ".fpi.index.json");
if (await fs.exists(indexPath)) {
return await this.isStrictlyMaterialized(packageObject);
}
const legacyIndexPath = path.join(packageDir, ".index.json");
if (await fs.exists(legacyIndexPath)) {
return await this.tryMaterializeLegacyPackageIndex(packageObject, packageDir) !== null;
}
return true;
}
async isStrictlyMaterialized(packageObject) {
const materialization = await this.getPackageMaterializationStatus(packageObject);
return materialization.complete;
}
async hasReadableManifest(packageObject) {
try {
await this.getManifest(packageObject);
return true;
} catch {
return false;
}
}
async collectMissingPackages(root, options) {
const missing = [];
const visited = /* @__PURE__ */ new Set();
const { explicitImplicitVersions } = await this.collectExplicitDependencyClosure(root);
const requireStrictMaterialization = options?.requireStrictMaterialization === true;
const visit = async (pkg) => {
const key = `${pkg.id}#${pkg.version}`;
if (visited.has(key)) return;
visited.add(key);
const isInstalled = requireStrictMaterialization ? await this.isStrictlyMaterialized(pkg) : await this.isInstalled(pkg, { deep: false });
if (!isInstalled) {
missing.push(key);
return;
}
const deps = await this.getDependencies(pkg, { explicitImplicitVersions });
for (const [depId, depVersion] of Object.entries(deps || {})) {
if (this.skipExamples && depId.includes("examples")) continue;
await visit({ id: depId, version: depVersion });
}
};
await visit(root);
return missing;
}
async collectPlannedDependencyClosure(root, seedExplicitImplicitVersions) {
return await this.withDebugTiming(
`collect-planned-dependency-closure ${this.formatPackageForDebug(root)}`,
async () => {
const { closure, explicitImplicitVersions: localExplicitImplicitVersions } = await this.collectExplicitDependencyClosure(root);
const explicitImplicitVersions = new Map(seedExplicitImpl