Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/typeschema/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const mkPackageAwareResolver = async (
if (acc[pkgId]) return acc[pkgId];

const index = mkEmptyPkgIndex(pkg);
acc[pkgId] = index;
for (const resource of await manager.search({ package: pkg })) {
const rawUrl = resource.url;
if (!rawUrl) continue;
Expand All @@ -115,7 +116,6 @@ const mkPackageAwareResolver = async (
resolutionOptions.sort((a, b) => a.deep - b.deep);
}

acc[pkgId] = index;
return index;
};

Expand Down
36 changes: 35 additions & 1 deletion test/unit/typeschema/register.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { describe, expect, it } from "bun:test";
import type { FHIRSchema } from "@atomic-ehr/fhirschema";
import { type CanonicalUrl, enrichFHIRSchema, type Name } from "@root/typeschema/types";
import { mergeFsElementProps, registerFromPackageMetas, resolveFsElementGenealogy } from "@typeschema/register";
import {
mergeFsElementProps,
registerFromManager,
registerFromPackageMetas,
resolveFsElementGenealogy,
} from "@typeschema/register";

type PFS = Partial<FHIRSchema>;

Expand Down Expand Up @@ -150,3 +155,32 @@ describe("Register tests", async () => {
});
});
});

describe("cyclic package dependencies", () => {
it("registerFromManager terminates on a dependency cycle", async () => {
// a ↔ b mutually depend (like hl7.terminology.r5 ↔ hl7.fhir.uv.extensions.r5).
// Without memoizing each package before recursing into its deps, the walk recurses
// forever; each unique package should be visited exactly once. The search-call cap
// makes a regression fail fast (red) instead of slowly exhausting memory.
const deps: Record<string, string[]> = { a: ["b"], b: ["a"] };
let searchCalls = 0;
const manager = {
packages: async () => Object.keys(deps).map((name) => ({ name, version: "1.0.0" })),
packageJson: async (name: string) => ({
name,
version: "1.0.0",
dependencies: Object.fromEntries((deps[name] ?? []).map((d) => [d, "1.0.0"])),
}),
search: async () => {
if (++searchCalls > 10) throw new Error("dependency walk did not terminate on cycle");
return [];
},
} as unknown as Parameters<typeof registerFromManager>[0];

const reg = await registerFromManager(manager, { focusedPackages: [{ name: "a", version: "1.0.0" }] });

expect(searchCalls).toBe(2); // each package scanned exactly once
expect(reg.resolver["a#1.0.0"]).toBeDefined();
expect(reg.resolver["b#1.0.0"]).toBeDefined();
});
});
Loading