Bundles
Bundles group related services that load together lazily — on first access, not at container creation. They are the key to code splitting and progressive loading with asimo.
Why Bundles?
Without bundles, all services in a container are registered eagerly. With bundles, you defer loading until the service is actually needed:
// Without bundles: analytics loads at startup even if never usedfactory({ provide: ANALYTICS, load: () => createAnalytics() })
// With bundles: analytics loads only when first accessedbundle({ provide: [ANALYTICS], load: () => import("./analytics") })Creating a Bundle
import { bundle } from "@asimojs/asimo";
const analyticsBundle = bundle({ provide: [ANALYTICS, TELEMETRY], load: async () => { const mod = await import("./analytics-bundle"); return mod; // returns Container<any> },});Bundle Rules
providemust beAsyncIID[]—SyncIIDtokens are rejected at compile time.loadis async — returns aPromise<Container<any>>containing the sub-services.- A bundle loads once — concurrent requests for the same bundle wait for the first load to complete.
Bundle with Dependencies
Bundles can declare dependencies on the parent container, typing the load context:
bundle({ provide: [DASHBOARD_SERVICE], dependencies: [DB, CONFIG], load: async (c) => { const db = c.get(DB); // from parent container const config = c.get(CONFIG); // from parent container const mod = await import("./dashboard"); return createContainer({ name: "dashboard", services: mod.createServices(db, config), }); },});Nested Bundles
A bundle’s load() can return a container that itself contains bundles:
bundle({ provide: [HEAVY_FEATURE], load: async () => { const mod = await import("./feature"); return createContainer({ name: "feature", services: [ // Sub-bundles load lazily within the parent bundle's scope bundle({ provide: [SUB_SERVICE], load: () => import("./sub-service"), }), factory({ provide: CORE, load: () => mod.core }), ], }); },});When HEAVY_FEATURE is fetched, the top-level bundle loads. SUB_SERVICE loads later, on its own first access.
How Bundles Work Internally
- On
container.fetch(SOME_IID), asimo looks up the service map. - If the entry is marked as a bundle (status:
"pending"),loadBundleAndResolve()is called. - Concurrent access guard: if the bundle is already loading, the second caller waits for the existing load promise.
- Circular detection: if a bundle’s own
loadcall re-enters the same bundle, anAsimoErroris thrown. - Merge: the sub-container’s service entries are merged into the parent’s service map.
- The requested service is resolved from the now-merged entries.
Scope Filtering
Only IIDs listed in the bundle’s provide array are exposed to the parent container. Services in the sub-container that are not listed in provide are silently dropped:
bundle({ provide: [ANALYTICS], // only ANALYTICS is accessible load: async () => { const mod = await import("./analytics"); return createContainer({ name: "analytics", services: [ factory({ provide: ANALYTICS, load: () => mod.analytics }), factory({ provide: TELEMETRY, load: () => mod.telemetry }), // ↑ TELEMETRY is created but NOT exposed to the parent ], }); },});
// ✅ ANALYTICS is accessibleconst a = await container.fetch(ANALYTICS);
// ❌ TELEMETRY is not in provide — throws Service not foundconst t = await container.fetch(TELEMETRY);This scoping is intentional — the bundle’s provide array is the authoritative list of what the bundle exports. The sub-container may contain additional internal services (helpers, shared dependencies, etc.) that should not leak into the parent. Use provide to control exactly which IIDs are made available.
Bundle Retry
Bundles support the same retry mechanism as async factories:
bundle({ provide: [UNRELIABLE_SERVICE], load: async () => import("./unreliable"), maxRetries: 3, retryDelayMs: 2000,});Code Splitting Pattern
The canonical use case for bundles is code splitting with dynamic import():
src/├── services/│ ├── core-bundle.ts # Always loaded (config, logger, DB)│ ├── auth-bundle.ts # Loaded on first auth action│ ├── analytics-bundle.ts # Loaded on first track() call│ └── dashboard-bundle.ts # Loaded when user opens dashboard// app.ts — minimal startup, everything else lazyconst app = createContainer({ name: "app", services: [ // Core: loaded eagerly factory({ provide: CONFIG, load: () => loadConfig() }), factory({ provide: LOGGER, load: () => createLogger() }),
// Feature bundles: loaded on demand bundle({ provide: [AUTH_SVC], load: () => import("./auth-bundle") }), bundle({ provide: [ANALYTICS], load: () => import("./analytics-bundle") }), bundle({ provide: [DASHBOARD], load: () => import("./dashboard-bundle") }), ],});The browser downloads app.js (~3 KB + core services) and loads feature code as users interact with the application.
Decoupling Declaration from Strategy
Bundle creation is separate from service code — service authors declare their dependency as an asyncIID, and bundle authors define the loading strategy independently:
// auth-service.ts — service author declares the IIDexport const AUTH_SVC = asyncIID<AuthService>()("app.services.Auth");
// auth-bundle.ts — bundle author defines loading strategyexport const authBundle = bundle({ provide: [AUTH_SVC], load: () => import("./auth-service-impl"),});This separation means you can change how services are grouped, split, and loaded without touching the service implementations or their consumers. A service that starts as a standalone bundle can be merged into a larger bundle, split across multiple bundles, or have its retry policy adjusted — all by editing the bundle definition, not the application code.