fount
v1.2.0
Published
A source from which dependencies flow
Readme
Fount
Fount provides synchronous and asynchronous dependency injection with automatic deferred resolution.
Getting Started
npm i fountimport { fount } from 'fount';Key Features
- Deferred Resolution: When resolving dependencies that haven't been registered yet, fount will wait until they become available rather than throwing an error.
- Multiple Containers: Organize dependencies into namespaced containers.
- Scoped Resolution: Cache factory results per scope for memoization.
- Flexible Registration: Register values, factories, or npm modules.
Dependency Identification Styles
Fount supports two styles of identifying dependencies:
// By argument name - each argument is looked up by name in fount's store
fount().inject((one, two, three) => {
// your code here
});
// By string array - dependencies are looked up and passed in order
fount().inject(['one', 'two', 'three'], (one, two, three) => {
// your code here
});The string array approach is recommended for production code as minifiers will rename function arguments and break name-based resolution.
Multiple Containers
Fount supports multiple dependency containers identified by namespace. If you don't specify one, it uses the default namespace.
You can use a non-default namespace in two ways:
- Pass the namespace in parenthesis before making the call
- Use period-delimited namespaces in your key names
// Using the default namespace
const value = await fount().resolve('myKey');
// Using a named namespace
const value = await fount('myContainer').resolve('myKey');
// Using period-delimited keys to access other namespaces
const value = await fount().resolve('myContainer.myKey');Scopes
Scopes work with factory dependencies to provide memoization. The first time a scoped factory is resolved within a scope, the result is cached. Subsequent resolutions within the same scope return the cached value.
// Register a scoped factory
fount().registerFactory('timestamp', () => Date.now(), 'scoped');
// First call computes the value
const t1 = await fount().resolve('timestamp', 'myScope');
// Second call returns cached value
const t2 = await fount().resolve('timestamp', 'myScope');
console.log(t1 === t2); // truePurging Scopes
// Purge a specific scope from the default namespace
fount().purgeScope('myScope');
// Purge all scopes from the default namespace
fount().purgeScopes();
// Purge a specific scope from a named namespace
fount('example').purgeScope('myScope');
// Purge all scopes from a named namespace
fount('example').purgeScopes();Registering Dependencies
Values
Register static values that fount returns as-is:
fount().register('port', 8080);
fount().register('config', { debug: true, maxRetries: 3 });
fount('custom').register('apiKey', 'secret-key');Factories
Register functions that fount invokes during resolution:
// Simple factory
fount().registerFactory('timestamp', () => Date.now());
// Factory with dependencies
fount().registerFactory('connection', ['host', 'port'], (host, port) => {
return createConnection(host, port);
});
// Dependencies inferred from argument names
fount().registerFactory('connection', (host, port) => {
return createConnection(host, port);
});Factory Lifecycles
Factories support three lifecycles:
- factory (default): Invoked on every resolution
- static: Invoked once, result is cached permanently
- scoped: Invoked once per scope, result is cached per scope
// Factory lifecycle - new instance every time
fount().registerFactory('id', () => generateId(), 'factory');
// Static lifecycle - computed once, cached forever
fount().registerFactory('startTime', () => Date.now(), 'static');
// Scoped lifecycle - computed once per scope
fount().registerFactory('requestId', () => generateId(), 'scoped');NPM Modules
Register npm modules directly:
fount().registerModule('lodash');
// Later, resolve it
const _ = await fount().resolve('lodash');If the module exports a function that fount cannot resolve dependencies for, it provides the function as-is.
Asynchronous Methods
These methods return promises and support deferred resolution - if a dependency isn't registered yet, fount waits until it becomes available.
resolve
Resolve a single dependency:
const port = await fount().resolve('port');
// With scope
const value = await fount().resolve('config', 'myScope');resolveAll
Resolve multiple dependencies at once:
const { host, port } = await fount().resolveAll(['host', 'port']);
// Across namespaces
const results = await fount().resolveAll(['db.host', 'cache.host']);inject
Resolve dependencies and pass them to a function:
// With explicit dependency list
const result = await fount().inject(['host', 'port'], (host, port) => {
return `${host}:${port}`;
});
// With inferred dependencies
const result = await fount().inject((host, port) => {
return `${host}:${port}`;
});
// With scope
const result = await fount().inject(['config'], (config) => {
return config.value;
}, 'myScope');Deferred Resolution
A key feature of fount is that async methods wait for dependencies to be registered:
// Start resolving before the dependency exists
const valuePromise = fount().resolve('laterValue');
// Register the dependency later
setTimeout(() => {
fount().register('laterValue', 42);
}, 1000);
// The promise resolves once the dependency is registered
const value = await valuePromise; // 42This also works with dependency chains:
// Register factories that depend on unregistered values
fount().registerFactory('doubled', ['base'], (base) => base * 2);
fount().registerFactory('result', ['doubled'], (doubled) => doubled + 10);
// Start resolution
const resultPromise = fount().resolve('result');
// Register the base value later
setTimeout(() => {
fount().register('base', 5);
}, 100);
const result = await resultPromise; // 20 (5 * 2 + 10)Synchronous Methods
These methods return values immediately and assume no promises in the dependency chain.
Warning: If a promise is encountered in the dependency chain, synchronous methods will not resolve it properly.
get
Get a dependency value synchronously:
const port = fount().get('port');
// With scope
const value = fount().get('config', 'myScope');
// Multiple values
const { host, port } = fount().getAll(['host', 'port']);invoke
Invoke a function with resolved dependencies synchronously:
const result = fount().invoke(['host', 'port'], (host, port) => {
return `${host}:${port}`;
});
// With inferred dependencies
const result = fount().invoke((host, port) => {
return `${host}:${port}`;
});Checking Resolution
Use canResolve to check if dependencies can be resolved without waiting:
if (fount().canResolve('myKey')) {
const value = fount().get('myKey');
}
// Check multiple keys
if (fount().canResolve(['key1', 'key2'])) {
const values = fount().getAll(['key1', 'key2']);
}Configuration
Configure multiple containers and values at once:
fount({
default: {
host: 'localhost',
port: 8080
},
database: {
host: 'db.example.com',
port: 5432,
pool: {
factory: () => createPool()
}
},
cache: {
ttl: 3600,
maxSize: {
scoped: () => calculateMaxSize()
}
}
});Values can be:
- Plain values (registered as static)
- Objects with
factorykey (registered as factory lifecycle) - Objects with
scopedkey (registered as scoped lifecycle) - Objects with
statickey (registered as static lifecycle)
Utility Methods
Key Lists
// All keys across all namespaces
const allKeys = fount.allKeys();
// Keys in a specific namespace
const keys = fount('myNamespace').keys();Namespaces
const namespaces = fount.namespaces();Purging
// Remove everything
fount.purgeAll();
// Remove all keys from the default namespace
fount().purge();
// Remove all keys from a specific namespace
fount('myNamespace').purge();Diagnostics
// Log the current state of all containers
fount.log();TypeScript Support
Fount is written in TypeScript and exports its types:
import { fount, Fount, ContainerApi } from 'fount';
// The main fount export
const f: Fount = fount;
// Container API returned when calling fount()
const container: ContainerApi = fount();Testing
# Run tests once
npm test
# Run tests in watch mode
npm run test:watch