Small typed dependency injection built on thenables and async hooks.
Copy src/di.ts into your project. Optionally, you may also copy src/di.test.ts.
- Define the thing your code needs.
import { Dependency, Runtime } from "@/lib/di"
type Random = {
readonly next: () => number
}
const Random = new Dependency<Random>("Random")- Use it in your code by
awaiting it.
async function program() {
const random = await Random
console.log(`random number: ${random.next()}`)
}- Create a real implementation, then run your code with it.
const RandomLive = Random.make(() => ({
next: () => Math.random(),
}))
const runtimeLive = new Runtime(RandomLive)
await runtimeLive.run(program) // stdout: random number: 0.8241872233134417- In tests, swap in a test implementation.
const RandomTest = Random.make(() => ({
next: () => 0.25,
}))
const runtimeTest = new Runtime(RandomTest)
await runtimeTest.run(program) // stdout: random number: 0.25Use a default when most runtimes share one implementation. It also lets TypeScript infer the dependency type.
import { Dependency } from "@/lib/di"
export const Random = new Dependency("Random", () => ({
next: () => Math.random(),
}))
export const RandomTest = Random.make(() => ({
next: () => 0.25,
}))Pass an async function to build one dependency from another.
import { createClient } from "@libsql/client"
import { drizzle } from "drizzle-orm/libsql"
import { Dependency } from "@/lib/di"
export const Database = new Dependency("Database", () =>
createClient({
url: "file:sqlite.db",
}),
)
export const Drizzle = new Dependency("Drizzle", async () => {
const database = await Database
return drizzle(database)
})Use finalizers for resources like database clients, sockets, or file handles. Finalizers execute when the enclosing Runtime.run() exits, even if it throws.
import { createClient } from "@libsql/client"
import { Dependency, Runtime } from "@/lib/di"
export const Database = new Dependency(
"Database",
() =>
createClient({
url: "file:sqlite.db",
}),
(client) => {
client.close()
console.log("SQLite client closed")
},
)
const runtime = new Runtime(Database.Default)
await runtime.run(async () => {
console.log("Using database")
throw Error("Oops")
})
// stdout:
// Using database
// SQLite client closedUse scoped resources for one-off runtime-owned things like temp workspaces, file handles, or log streams that should be cleaned up automatically without becoming full Dependency values.
import { mkdtemp, open, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { Runtime, acquireRelease, acquireUseRelease } from "@/lib/di"
async function program() {
const workspace = await acquireRelease(
() => mkdtemp(join(tmpdir(), "di-export-")),
(path) => rm(path, { recursive: true, force: true }),
)
await writeFile(join(workspace, "users.csv"), "id,name\n1,Jai Dixit\n")
await acquireUseRelease(
() => open(join(workspace, "summary.txt"), "w"),
(file) => file.writeFile("export complete\n"),
(file) => file.close(),
)
}
await new Runtime().run(program)Use cancellation when dependencies do I/O and should stop with the request or job. Runtime.run() provides one scoped AbortSignal, and signal() reads it anywhere in that runtime.
import { Dependency, Runtime, signal } from "@/lib/di"
type FeatureFlags = {
readonly newCheckout: boolean
readonly referralBanner: boolean
}
export const FeatureFlags = new Dependency<FeatureFlags>("FeatureFlags")
export const FeatureFlagsMock = FeatureFlags.make(() => ({
newCheckout: true,
referralBanner: false,
}))
export const FeatureFlagsLive = FeatureFlags.make(async () => {
const response = await fetch("https://flags.internal.example/api/flags", {
signal: signal(),
})
return (await response.json()) as FeatureFlags
})
async function program() {
const flags = await FeatureFlags
console.log(flags.newCheckout)
}
const controller = new AbortController()
await new Runtime(FeatureFlagsMock).run(program)
await new Runtime(FeatureFlagsLive).run(program, {
signal: controller.signal,
})Nested runtimes inherit the parent signal. If a child runtime also receives a signal, both signals are composed.