3.4 KiB
service-registry
(no submodules)
Purpose
The service-registry module defines the single shared interface that
plugins augment to publish typed services to other plugins. Plugin A
registers a db adapter; plugin B retrieves it with ctx.getService("db")
and receives the augmented type — no cast, no unknown, no runtime
shape-check. The interface ships empty so a consumer who has not
augmented it cannot accidentally type-pun a service slot.
This is the canonical solution to upstream's getService<T>(name) cast
hazard: there, T was a free type parameter, so a typo in the name or a
mismatch between producer and consumer compiled cleanly and crashed at
runtime. With module augmentation, the interface itself is the contract;
both ends agree on every key.
Features
ServiceRegistry— empty by default, augmented viadeclare module.ServiceKey = keyof ServiceRegistry—neveruntil augmented.- Zero runtime — types-only export.
- Subpath export
@suaveplan/api-plugin/service-registryfor direct augmentation imports.
Usage: Basic
Declare your plugin's service slot once, anywhere in the consumer code that runs before the registry is created:
import type { DbAdapter } from "./db-adapter.js";
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry {
db: DbAdapter;
}
}
Then registration and lookup are both type-safe:
import { definePlugin } from "@suaveplan/api-plugin";
export const dbPlugin = definePlugin({
name: "db",
version: "0.1.0",
onInit(ctx) {
ctx.registerService("db", createDbAdapter());
},
});
export const consumerPlugin = definePlugin({
name: "consumer",
version: "0.1.0",
dependencies: ["db"],
onReady(ctx) {
const db = ctx.getService("db"); // DbAdapter | undefined
db?.query("SELECT 1");
},
});
Usage: Advanced
Multiple consumers may augment independently — TypeScript merges the interface across declarations:
// package-a/services.d.ts
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { cache: CacheAdapter; }
}
// package-b/services.d.ts
declare module "@suaveplan/api-plugin/service-registry" {
interface ServiceRegistry { auth: AuthAdapter; }
}
// Anywhere: both keys are now type-safe.
ctx.getService("cache"); // CacheAdapter | undefined
ctx.getService("auth"); // AuthAdapter | undefined
ctx.getService("xyz"); // type error — not augmented
API Reference
| Export | Kind | Notes |
|---|---|---|
ServiceRegistry |
interface | Empty extension surface — augment via declare module. |
ServiceKey |
type | keyof ServiceRegistry; resolves to never without augmentation. |
Implementation Notes
Biome's noEmptyInterface rule would flag a literally-empty interface
declaration. The module sidesteps the lint cleanly by extending a
Record<never, never> alias — semantically equivalent to {} for the
augmentation pattern but no longer "empty" in the syntactic sense Biome
inspects. The alias is private to the module; consumers only see the
public ServiceRegistry interface and the ServiceKey helper.
No runtime artefacts are emitted from this file — Vite/dts strip declaration-only modules during the library build. Subpath consumers benefit from a sub-1KB import that is entirely tree-shaken at consumer build time.