fix: serialize dynamic imports to prevent race condition on parallel account startup

Problem: When multiple Matrix accounts start in parallel, they all trigger
dynamic imports simultaneously. The Rust native crypto module
(@matrix-org/matrix-sdk-crypto-nodejs) crashes when loaded in parallel.

Solution: Add import-mutex.ts that caches import promises. Concurrent callers
now await the same promise instead of triggering parallel imports.

Files changed:
- NEW: src/matrix/import-mutex.ts - serializedImport() utility
- src/matrix/client/create-client.ts - use importCryptoNodejs()
- src/matrix/client/config.ts - use importCredentials()
This commit is contained in:
Claudia 2026-02-01 11:31:44 +01:00
parent 8790ba5dad
commit c0e38bb990
3 changed files with 77 additions and 2 deletions

View file

@ -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 =

View file

@ -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,

View file

@ -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<string, Promise<unknown>>();
/**
* 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<T>(
moduleSpecifier: string,
importFn: () => Promise<T>
): Promise<T> {
const existing = importCache.get(moduleSpecifier);
if (existing) {
return existing as Promise<T>;
}
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<typeof import("@matrix-org/matrix-sdk-crypto-nodejs")> {
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<typeof import("./credentials.js")> {
if (credentialsModule) return credentialsModule;
const mod = await serializedImport(
"../credentials.js",
() => import("./credentials.js")
);
credentialsModule = mod;
return mod;
}