惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
The Cloudflare Blog
G
Google Developers Blog
博客园_首页
Martin Fowler
Martin Fowler
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
D
Docker
C
Check Point Blog
T
Tailwind CSS Blog
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
Microsoft Security Blog
Microsoft Security Blog
V
V2EX
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
酷 壳 – CoolShell
酷 壳 – CoolShell
IT之家
IT之家
M
MIT News - Artificial intelligence
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 【当耐特】
GbyAI
GbyAI

Show HN

GitHub - astefanutti/shaderbang: Shebang for Shaders Show HN: AI agents for UK GDAD PCF roles and their skills The Two Pillars: Mixer Mode and Meta-Software in the Reorganization of Software Work After AI GitHub - JaiCode08/teleport-env What 1,000+ Harness Experiments Taught Me About Self-Improving Agents Show HN: Liiists, a Markdown-first, iOS and CLI list app SwiperTab – Get this Extension for 🦊 Firefox (en-US) GitHub - kouhxp/fftext: Summarize, explain, fact-check, or translate any text, URL, or file. No GPU. No cloud. One command GitHub - sweetpad-dev/sweetpad: Develop Swift/iOS projects using VSCode GitHub - dogmaticdev/IRON: IRON a.k.a. Intermediate Representation Object Notation is a Interpreter/Database that is used to create Programming Languages. GitHub - sjhalani7/vaen: Package your AI coding harness into a portable .agent file, and share it across repos, teams, & the community without ever having to copy-paste instructions, skills, MCP config, or secrets. Show HN: Gandalf the Grader Show HN: Citadeld – replay any CI failure locally from a single file GitHub - tdortman/cuSBF: High-Performance GPU Super Bloom Filter coral-ai/claude-code-token-xray at main · Coral-Bricks-AI/coral-ai GitHub - ulyssestenn/funes: Funes is a Git-based framework for LLM-managed knowledge work: an AI Librarian ingests raw sources, builds an interlinked Markdown knowledge base, and uses it to produce cited reports, analyses, and other outputs. GitHub - ThatXliner/gah: Git Add Hunk, built for agents to use GitHub - harmont-dev/harmont-cli: Command-line client for the Harmont CI platform GitHub - brooksmcmillin/mcp-authflow: OAuth 2.0 Authorization Server framework for MCP servers GitHub - javaid-codes/audit-supply-chain-agents GitHub - amorey/gochan: A small library of common channel architectures for Go, inspired by Rust GitHub - arifozgun/OpenGem: Free, Open-Source AI API Gateway with Gemini, OpenAI & Anthropic Compatibility in 1 file GitHub - Pranesh950/BioPetals: 🌸 Run BIOxAI models at home, BitTorrent-style. Fine-tuning and inference up to 10x faster than offloading GitHub - cnguyen14/bounty-doctor: Diagnose a GitHub bounty issue before you waste hours: detects honeypot scam repos, AI-bot attempt swarms, and stale contests. Show HN: CoreMCP – MCP Server for On-Prem DBs Show HN: KittyHTML – Render HTML/CSS as an inline image in your terminal GitHub - bingud/filemat: Web-based file manager Show HN: TruthLens – Free multi-signal deepfake image detector GitHub - apexlocal-jz/claude-usage-tray: Windows system-tray app showing your Claude Code rate-limit usage at a glance. Zero deps, ~300 lines of PowerShell. Cross-IDE (works regardless of VS Code, Cursor, plain terminal). Release v0.1.2.1 · kouhxp/yapsnap
GitHub - ukstv/ampulla: Modern TypeScript DI with NestJS ...
murmansk · 2026-06-28 · via Show HN

Type-safe dependency injection with the ergonomics of NestJS. None of the framework.

  • End-to-end type safety. container.get(MyService) returns MyService, not unknown. Mismatched tokens are compile errors, not runtime crashes.
  • No reflect-metadata. Uses TC39 Stage 3 decorators — no experimentalDecorators, no polyfills, no surprises when TypeScript changes its metadata output.
  • Zero dependencies. Built-in Hono and H3 adapters are type-only — the runtime is whatever you already have.
  • Tree-shakeable. HTTP adapters live in separate entry points (ampulla/hono, ampulla/h3). If you don't import them, they don't exist in your bundle.
  • Module-scoped visibility. No global singleton registry. Providers are only visible where explicitly exported — same mental model as NestJS, without the rest of the framework.
  • Testing built-in. TestingContainer spins up a full container in one line, with any provider overrideable.
import { Container, Module, Injectable, injection, useValue } from "ampulla";

const DB_URL = injection<string>("DB_URL");

@Injectable(DB_URL)
class UserService {
  constructor(private readonly url: string) {}

  findAll() {
    return fetch(`${this.url}/users`).then((r) => r.json());
  }
}

@Module({
  providers: [useValue(DB_URL, "https://api.example.com"), UserService],
  exports: [UserService],
})
class AppModule {}

const container = await Container.create(AppModule);
const users = container.get(UserService); // typed as UserService

Table of Contents

  • Install
  • Guide
    • Declaring services
    • Injection tokens
    • Optional dependencies
    • Modules
    • Providers
    • Bootstrapping
    • Lifecycle hooks
  • HTTP Adapters
    • Hono
    • H3
  • Testing
  • Tags
  • Documentation

Install

Requires TypeScript 5.2+ with no additional compiler flags.


Guide

Declaring services

@Injectable declares what a class needs. Its arguments are the tokens the container resolves and passes to the constructor — in order.

import { Injectable } from "ampulla";

@Injectable()
class Logger {
  log(msg: string) { console.log(`[${new Date().toISOString()}] ${msg}`); }
}

@Injectable(Logger)
class UserService {
  constructor(private readonly logger: Logger) {}

  create(name: string) {
    this.logger.log(`Creating user: ${name}`);
  }
}

TypeScript verifies at compile time that @Injectable(Logger) matches the constructor signature. Forget a token, pass the wrong type, or misorder them — it's a type error.

Injection tokens

Classes double as their own tokens. For everything else — strings, numbers, config objects, interfaces — create an explicit typed token with injection<T>().

import { injection } from "ampulla";

export const DB_URL = injection<string>("DB_URL");
export const CONFIG  = injection<AppConfig>("CONFIG");

The string label is only used in error messages. Token identity is object reference — always export and import the same constant, never recreate it.

Optional dependencies

Wrap a token with optional() to inject undefined when the provider is absent instead of throwing.

import { Injectable, injection, optional } from "ampulla";

const CACHE = injection<Cache>("CACHE");

@Injectable(DB_URL, optional(CACHE))
class UserService {
  constructor(private url: string, private cache?: Cache) {}
}

Modules

A @Module declares which providers it owns (providers) and which it exposes to importers (exports).

import { Module, useValue } from "ampulla";

@Module({
  providers: [useValue(DB_URL, "postgres://localhost/app"), Logger, UserService],
  exports: [UserService],
})
class UserModule {}

Modules compose — importers can only see what is explicitly exported:

@Module({
  imports: [DatabaseModule, UserModule],
  providers: [AppService],
})
class AppModule {}

The container deduplicates shared modules automatically. If UserModule and PostModule both import DatabaseModule, a single DatabaseService instance is shared between them.

Providers

Three provider shapes beyond bare class constructors:

import { useValue, useClass, useFactory } from "ampulla";

useValue(PORT, 3000)                                      // pre-existing value
useClass(LoggerDep, ConsoleLogger)                        // concrete under abstract token
useFactory(DB, [DB_URL], async url => new Pool(url))      // async factory

Factory providers may return a Promise — the container awaits all factories concurrently before making any value available.

Bootstrapping — sync and async

Container.create awaits every async factory before resolving. By the time you have a container, every provider is fully initialized and ready — no deferred initialization, no "is it ready yet?" checks.

import { Container, Module, useFactory, injection } from "ampulla";

const DB_URL = injection<string>("DB_URL");
const DB = injection<Pool>("DB");

// Async factory: the container waits for the connection before proceeding.
// Independent async factories run concurrently.
const dbProvider = useFactory(DB, [DB_URL], async (url) => {
  const pool = new Pool(url);
  await pool.connect();   // fully connected by the time any consumer gets it
  return pool;
});

@Module({ providers: [useValue(DB_URL, "postgres://localhost/app"), dbProvider, UserService] })
class AppModule {}

const container = await Container.create(AppModule);

// At this point every provider — including the async DB pool — is ready.
const users = container.get(UserService); // synchronous, fully typed

Lifecycle hooks

Async factories initialize a provider in isolation. Lifecycle hooks fire after every provider in the graph is ready, which is the right place for cross-service coordination.

  • @OnModuleInit — runs after all providers are instantiated, in dependency order. Use it to warm caches from a ready DB, subscribe to another service's events, register with a central bus, or start scheduled jobs once all dependencies are live.
  • @OnModuleDestroy — runs on dispose(), in reverse order (dependents first, deps last). Use it to flush queues before closing connections, finish in-flight work, or deregister from service discovery.

Rule of thumb: factories own "am I ready", lifecycle hooks own "now that everyone else is ready, coordinate".

import { Injectable, OnModuleInit, OnModuleDestroy } from "ampulla";

@OnModuleInit()
@OnModuleDestroy()
@Injectable(DB, EventBus)
class UserRepository {
  constructor(private db: Pool, private bus: EventBus) {}

  async onModuleInit() {
    // DB and EventBus are both fully ready here — safe to query and subscribe
    const count = await this.db.query("SELECT COUNT(*) FROM users");
    this.bus.emit("users:ready", { count });
  }

  async onModuleDestroy() {
    // EventBus is still up — dependents are torn down first
    this.bus.emit("users:shutdown");
  }
}

Use await using for automatic cleanup (TypeScript 5.2+):

await using container = await Container.create(AppModule);
// container.dispose() is called automatically at end of scope

HTTP Adapters

HTTP is the most common entry point, so ampulla ships adapters for Hono and H3 out of the box. But the container is just a container — nothing stops you from wiring it to WebSockets, message queues, cron jobs, or any other input source. container.get(MyService) works the same regardless of what calls it.

Hono

import { Hono } from "hono";
import { Controller, Get, Extract, query, registerControllers } from "ampulla/hono";

@Controller("users")
@Injectable()
class UserController {
  @Extract({ name: query("name") })
  @Get("search")
  search(params: { name: string | undefined }) {
    return new Response(params.name ?? "");
  }
}

@Module({ providers: [UserController] })
class AppModule {}

const app = new Hono();
const container = await Container.create(AppModule);
registerControllers(app, container);
export default app;

See Hono adapter docs for the full extractor and middleware API.

H3

Controllers, extractors, and middleware — same decorator API, adapted for H3's H3Event:

import { H3 } from "h3";
import { Controller, Get, Extract, query, registerControllers } from "ampulla/h3";

See H3 adapter docs.


Testing

TestingContainer creates a one-off module inline — no class declarations needed:

import { TestingContainer } from "ampulla/testing";
import { useValue } from "ampulla";

const svc = await TestingContainer.use(UserService, {
  providers: [useValue(DB_URL, "postgres://localhost/test"), UserService],
});

expect(svc.findAll()).toEqual([]);

For tests that need to inspect multiple providers:

const container = await TestingContainer.fromModule({
  providers: [useValue(DB_URL, "postgres://test"), Logger, UserService],
});

const logger = container.get(Logger);
const users  = container.get(UserService);

Tags

container.get retrieves a single known provider by token. But some patterns need a collection — all event handlers, all controllers, all plugins — where the consumer shouldn't have to know what's registered. Tags solve this: mark providers with a shared role, then retrieve every instance that carries it in one call, without any direct dependency between them. This is exactly the mechanism registerControllers uses internally — it collects all tagged controller instances and mounts them onto the Hono or H3 app.

import { tag, Tagged, allTagged } from "ampulla/tag";

const HANDLER = tag<{ handle(): void }>("handler");

@Tagged(HANDLER)
@Injectable()
class FooHandler { handle() { /* ... */ } }

const handlers = allTagged(container, HANDLER); // FooHandler[]
handlers.forEach(h => h.handle());

Documentation