diff --git a/src/matrix/client/config.ts b/src/matrix/client/config.ts index cb9cc38b3..1cc28cc2b 100644 --- a/src/matrix/client/config.ts +++ b/src/matrix/client/config.ts @@ -5,6 +5,7 @@ import type { CoreConfig, MatrixAccountConfig, MatrixConfig } from "../types.js" import { getMatrixRuntime } from "../../runtime.js"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; import type { MatrixAuth, MatrixResolvedConfig } from "./types.js"; +import { importCredentials } from "../import-mutex.js"; function clean(value?: string): string { return value?.trim() ?? ""; @@ -93,12 +94,13 @@ export async function resolveMatrixAuth(params?: { const isDefaultAccount = normalizedAccountId === DEFAULT_ACCOUNT_ID || normalizedAccountId === "default"; // Only use cached credentials for default account + // Use serialized import to prevent race conditions during parallel account startup const { loadMatrixCredentials, saveMatrixCredentials, credentialsMatchConfig, touchMatrixCredentials, - } = await import("../credentials.js"); + } = await importCredentials(); const cached = isDefaultAccount ? loadMatrixCredentials(env) : null; const cachedCredentials = diff --git a/src/matrix/client/create-client.ts b/src/matrix/client/create-client.ts index 874da7e92..3f8d11040 100644 --- a/src/matrix/client/create-client.ts +++ b/src/matrix/client/create-client.ts @@ -6,6 +6,8 @@ import { SimpleFsStorageProvider, RustSdkCryptoStorageProvider, } from "@vector-im/matrix-bot-sdk"; + +import { importCryptoNodejs } from "../import-mutex.js"; import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; @@ -65,7 +67,8 @@ export async function createMatrixClient(params: { fs.mkdirSync(storagePaths.cryptoPath, { recursive: true }); try { - const { StoreType } = await import("@matrix-org/matrix-sdk-crypto-nodejs"); + // Use serialized import to prevent race conditions with native Rust module + const { StoreType } = await importCryptoNodejs(); cryptoStorage = new RustSdkCryptoStorageProvider( storagePaths.cryptoPath, StoreType.Sqlite, diff --git a/src/matrix/import-mutex.ts b/src/matrix/import-mutex.ts new file mode 100644 index 000000000..caf145b8a --- /dev/null +++ b/src/matrix/import-mutex.ts @@ -0,0 +1,70 @@ +/** + * Import Mutex - Serializes dynamic imports to prevent race conditions + * + * Problem: When multiple Matrix accounts start in parallel, they all call + * dynamic imports simultaneously. Native modules (like @matrix-org/matrix-sdk-crypto-nodejs) + * can crash when loaded in parallel from multiple promises. + * + * Solution: Cache the import promise so that concurrent callers await the same promise + * instead of triggering parallel imports. + */ + +// Cache for import promises - key is module specifier +const importCache = new Map>(); + +/** + * Safely import a module with deduplication. + * If an import is already in progress, returns the existing promise. + * Once resolved, the result is cached for future calls. + */ +export async function serializedImport( + moduleSpecifier: string, + importFn: () => Promise +): Promise { + const existing = importCache.get(moduleSpecifier); + if (existing) { + return existing as Promise; + } + + const importPromise = importFn().catch((err) => { + // On failure, remove from cache to allow retry + importCache.delete(moduleSpecifier); + throw err; + }); + + importCache.set(moduleSpecifier, importPromise); + return importPromise; +} + +// Pre-cached imports for critical modules +let cryptoNodejsModule: typeof import("@matrix-org/matrix-sdk-crypto-nodejs") | null = null; +let credentialsModule: typeof import("./credentials.js") | null = null; + +/** + * Safely import the crypto-nodejs module (Rust native). + * This is the most critical one - parallel imports of native modules crash. + */ +export async function importCryptoNodejs(): Promise { + if (cryptoNodejsModule) return cryptoNodejsModule; + + const mod = await serializedImport( + "@matrix-org/matrix-sdk-crypto-nodejs", + () => import("@matrix-org/matrix-sdk-crypto-nodejs") + ); + cryptoNodejsModule = mod; + return mod; +} + +/** + * Safely import the credentials module. + */ +export async function importCredentials(): Promise { + if (credentialsModule) return credentialsModule; + + const mod = await serializedImport( + "../credentials.js", + () => import("./credentials.js") + ); + credentialsModule = mod; + return mod; +}