First of all, what is dependency injection?
From https://en.wikipedia.org/wiki/Dependency_injection:
dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs. The pattern ensures that an object or function that wants to use a given service should not have to know how to construct those services. Instead, the receiving "client" (object or function) is provided with its dependencies by external code (an "injector"), which it is not aware of. Dependency injection makes implicit dependencies explicit and helps solve the following problems:
- How can a class be independent from the creation of the objects it depends on?
- How can an application, and the objects it uses support different configurations?
Matt's pattern is a specialised kind of Service Locator, because the object
is aware of it's "injector"(actually service locator), the object does partially support different configurations, however it's limited to continuously awaiting functions.
It does not generally support different configurations if multiple configurations need to exist in one execution context.
To see why, imagine a multi-tenant EmailClient, each tenant has its own SMTP creds:
// Good old DI
async function sendWelcome(user, emailClient) {
await emailClient.send(user.email, "Welcome", `Hi ${user.name}!`);
}
await Promise.all([
sendWelcome(alice, tenantAClient), // uses tenant-A SMTP
sendWelcome(bob, tenantBClient), // uses tenant-B SMTP
]);
Because the dependency is passed in, both configs coexist safely.
// Matt’s AsyncLocalStorage "DI"
const store = new AsyncLocalStorage<EmailClient>();
async function sendWelcome(user) {
await store.getStore()!.send(user.email, "Welcome", `Hi ${user.name}!`);
}
store.enterWith(tenantAClient);
const t1 = sendWelcome(alice); // expects tenant-A SMTP
store.enterWith(tenantBClient);
const t2 = sendWelcome(bob); // overwrites the store
await Promise.all([t1, t2]); // both now send via tenant-B SMTP
enterWith mutates a single ambient slot, so the "later" configuration wins. That's classic Service-Locator behaviour: fine when only one config is ever active, brittle the moment two need to run side-by-side.