Size / Priority
- Size: Trivial (~30+ call-sites consolidated)
- Category: C.2 Simplifications & DRY.
- Risk: low.
Affected files
- ~30 sites across the codebase that lazy-import optional peer-deps (memjs, kafkajs, mqtt, nats, amqplib, cassandra-driver, ioredis, etc.).
Background
Optional peer-deps are imported via dynamic import() with the ESM default-export-vs-named-export dance:
const moduleName = 'memjs';
const mod = await import(moduleName);
const client = (mod.default ?? mod).Client.create(...);
The default ?? mod fallback handles modules that ship as either module.exports = { Client } (CommonJS, no default) or module.exports.default = ... (ESM with default export). This appears ~30 times across the broker, journal, cache, and HTTP backend files.
Bugs:
- Inconsistent fallback shapes (some sites use
mod.default ?? mod, others use (mod as any).default || mod, others spread).
- Error messages on missing peer-dep are bespoke (some good, some terse).
- TypeScript inference is awkward.
Target code
// src/util/LazyImport.ts (new)
/**
* Lazy-import an optional peer-dep with a uniform error message.
*
* const memjs = await lazyImportModule<typeof MemjsType>('memjs');
* const client = memjs.Client.create(servers);
*/
export async function lazyImportModule<T>(moduleName: string, hint?: string): Promise<T> {
try {
const mod = await import(moduleName);
// ESM default-export-vs-named normalisation
return ('default' in mod && typeof mod.default !== 'undefined' ? mod.default : mod) as T;
} catch (e) {
const installHint = hint ?? `Install the package: bun add ${moduleName}`;
throw new Error(
`[actor-ts] optional peer-dep '${moduleName}' is not installed. ${installHint}\n` +
`Original import error: ${(e as Error).message}`,
);
}
}
Per-site usage:
// Before:
const mod = await import('memjs');
const Client = (mod.default ?? mod).Client;
// After:
const memjs = await lazyImportModule<{ Client: typeof MemjsClient }>('memjs');
const Client = memjs.Client;
Integration / risk
- Behaviour preserved — same import semantics.
- Error message uniformity — users get the same helpful text everywhere.
- TypeScript types: caller supplies
T; if the user types it correctly, ergonomics improve.
Test plan
- Per-site regression — each migrated site loads its peer-dep successfully (verified by existing peer-dep integration tests).
- Missing peer-dep: uninstall a peer-dep; the error message includes the install command + module name.
- Type-test: TypeScript type returned matches caller expectation.
Acceptance criteria
Pre-implementation note
Some sites do non-trivial transformation after import (e.g., wrap the imported class in a TS-friendly façade). Those sites should keep the transformation but use lazyImportModule for the import + default-resolution step.
Size / Priority
Affected files
Background
Optional peer-deps are imported via dynamic
import()with the ESM default-export-vs-named-export dance:The
default ?? modfallback handles modules that ship as eithermodule.exports = { Client }(CommonJS, no default) ormodule.exports.default = ...(ESM with default export). This appears ~30 times across the broker, journal, cache, and HTTP backend files.Bugs:
mod.default ?? mod, others use(mod as any).default || mod, others spread).Target code
Per-site usage:
Integration / risk
T; if the user types it correctly, ergonomics improve.Test plan
Acceptance criteria
lazyImportModule<T>(name, hint?)exported.Pre-implementation note
Some sites do non-trivial transformation after import (e.g., wrap the imported class in a TS-friendly façade). Those sites should keep the transformation but use
lazyImportModulefor the import + default-resolution step.