diff --git a/packages/chord/CHANGELOG.md b/packages/chord/CHANGELOG.md index 06157ff48..c36e087f6 100644 --- a/packages/chord/CHANGELOG.md +++ b/packages/chord/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- `nodeLoader` registers every facet's directory before importing any, so a facet that imports a file of a facet listed after it gets the same module copy as that facet; before, the load order could produce duplicates. - A link that is already closed when a host accepts or routes to it is no longer kept as a connection: before, a route that returned a dead link (a child process that exited before its link was wrapped) made every later call to that host name fail with `unavailable` until restart, and leaked the connection's ping timer. - Fixed `processLock()` failing with `listen EINVAL` on macOS and the BSDs when the temporary directory is too deep for a socket path (sun_path holds 104 bytes); the lock socket then lives in a private per-user directory, `/tmp/chord2-` diff --git a/packages/chord/src/loader.ts b/packages/chord/src/loader.ts index c46a70b68..85a0fd879 100644 --- a/packages/chord/src/loader.ts +++ b/packages/chord/src/loader.ts @@ -45,11 +45,15 @@ export const nodeLoader: FacetLoader = async (modules) => { hook(); const version = String(++generation); const facets: Facet[] = []; - for (const module of modules) { + const urls = modules.map((module) => { const url = toUrl(module); url.search = ""; + return url; + }); + // Every root first: a facet importing another facet's directory gets this load's copy, whatever the order. + for (const url of urls) roots.add(new URL(".", url).href); + for (const url of urls) { const name = url.href; - roots.add(new URL(".", url).href); url.searchParams.set(PARAM, version); // Owner-approved exception to the no-dynamic-import rule; see TODO(loader) above. const loaded = (await import(url.href)) as { default?: FacetDefinition }; diff --git a/packages/chord/test/fixtures/loader.ts b/packages/chord/test/fixtures/loader.ts index 034576231..5393d3cf2 100644 --- a/packages/chord/test/fixtures/loader.ts +++ b/packages/chord/test/fixtures/loader.ts @@ -8,6 +8,8 @@ import { nodeLoader } from "../../src/loader.ts"; // Plugin modules live under the package so bare imports (typebox) resolve; removed afterwards. const dir = fileURLToPath(new URL(`../.tmp-plugins-${process.pid}/`, import.meta.url)); +// Separate from `dir`, which becomes a reload root once its facet loads. +const plugins = fileURLToPath(new URL(`../.tmp-plugins-${process.pid}-order/`, import.meta.url)); const chord = new URL("../../src/index.ts", import.meta.url).href; // The test's own copy of the plugin's contract: binding is by description, not object identity. @@ -49,6 +51,31 @@ try { assert.deepEqual(report.changed, ["t.hello"]); assert.equal(await handle.greet("x"), "v2 x"); await host.close(); + + // A facet listed first that imports a later facet's directory shares that facet's copy of the module. + const Shared = local("t.shared", { + version: 1, + input: Type.Object({}), + members: { same: method(Type.Null(), Type.Boolean()) }, + }); + mkdirSync(`${plugins}a`, { recursive: true }); + mkdirSync(`${plugins}b`, { recursive: true }); + writeFileSync(`${plugins}b/state.ts`, "export const state = {};\n"); + writeFileSync( + `${plugins}b/facet.ts`, + `import { defineFacet } from ${JSON.stringify(chord)};\nimport { state } from "./state.ts";\nexport const own = state;\nexport default defineFacet(() => {});\n`, + ); + writeFileSync( + `${plugins}a/facet.ts`, + `import { Type } from "typebox";\nimport { defineFacet, local, method } from ${JSON.stringify(chord)};\nimport { state } from "../b/state.ts";\n` + + `const Shared = local("t.shared", { version: 1, input: Type.Object({}), members: { same: method(Type.Null(), Type.Boolean()) } });\n` + + `export default defineFacet((host) => host.serve(Shared, () => ({ same: async () => (await import(new URL("../b/facet.ts", import.meta.url).href)).own === state })));\n`, + ); + const ordered = createHost({ loader: nodeLoader, onError: () => {} }); + await ordered.load([`${plugins}a/facet.ts`, `${plugins}b/facet.ts`]); + assert.equal(await ordered.get(Shared, {}).same(null), true); + await ordered.close(); } finally { rmSync(dir, { recursive: true, force: true }); + rmSync(plugins, { recursive: true, force: true }); }