Files
skills/docs/api-stack/api/api-plugin/src/service-registry/service-registry.md
2026-05-29 08:20:56 +01:00

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 via declare module.
  • ServiceKey = keyof ServiceRegistry — never until augmented.
  • Zero runtime — types-only export.
  • Subpath export @suaveplan/api-plugin/service-registry for 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.