7.1 KiB
Add @suaveplan/api-plugin — plugin system foundation (1 of 5)
Package: @suaveplan/api-plugin (NEW, universal-tier-server)
Status: PROPOSED
Type: new-package (split from upstream @suaveplan/api)
- Spec delta: ./specs/api-plugin/spec.md
- Companions:
add-api-tenant,add-api-openapi,add-api-migrations,enterprise-api - Position in chain: foundation — gates all 4 companions
Why
The upstream @suaveplan/api package (8.7k LOC) bundles 7 architectural layers into one. The plugin system (plugin/ + hooks/ + topo-sort util — 2,034 LOC) is the foundation every other layer depends on. Splitting it out lets the tenant / openapi / migrations / app layers compose against a stable, fully-tested plug-and-play primitive without dragging the whole framework along.
The upstream plugin system has known design issues that genesis fixes: dual-runtime hook execution (registry calls lifecycle hooks directly AND HookExecutor registers them — duplication hazard), unsafe getService<T>(name) casts, unenforced initTimeout option, and asymmetric error policy between init (abort-on-throw) and shutdown (swallow-and-log). Forty-plus in-flight enterprise-api-plugin-* change folders are all blocked on this landing.
What changes
Net-new package at 0.1.0. ADDED:
definePlugin<TConfig>(descriptor): PluginDescriptor<TConfig>— typed plugin factory. Descriptor includesname,version,dependencies,optionalDependencies,priority?,enabledByDefault?,configSchema?: ZodType, and lifecycle hooks (onRegister/onInit/onReady/onClose).PluginRegistryclass — single canonical registry. Methods:register,registerAll,get,has,getAll,getSortedPlugins,initialize(context),shutdown(),disable(name). Internally owns the topo-sort + cycle detection.PluginContext— runtime injection:{ logger: LoggerLike, eventBus: EventBusLike, getOwnConfig<T>(): T, registerService<K extends ServiceKey>(name: K, service: ServiceRegistry[K]): void, getService<K extends ServiceKey>(name: K): ServiceRegistry[K] | undefined, hasService(name: string): boolean, signal: AbortSignal }.ServiceRegistryinterface (empty by default) — designed for module augmentation so plugins declare their typed slots:declare module "@suaveplan/api-plugin/service-registry" { interface ServiceRegistry { db: DbAdapter } }.topologicalSort(nodes): { sorted, unresolved }— Kahn's algorithm + DFS cycle-detection diagnostic. Equal-in-degree nodes sorted alphabetically by id (deterministic).validateDependencies(nodes)— single canonical validator (upstream has two; we ship one).HookExecutorfor non-lifecycle hooks only (request/db/custom plugin-contributed hooks). Lifecycle hooks (onRegister/onInit/onReady/onClose) are owned byPluginRegistryand called once per plugin — never via the executor. Eliminates upstream's dual-runtime hazard.PluginError extends SuaveplanError<PluginErrorCode>withdefineCodes("PLUGIN", [...])registry. Codes:PLUGIN_NOT_FOUND,PLUGIN_ALREADY_REGISTERED,PLUGIN_INIT_FAILED,PLUGIN_DEP_MISSING,PLUGIN_CIRCULAR_DEPENDENCY,PLUGIN_INIT_TIMEOUT,PLUGIN_SHUTDOWN_FAILED,PLUGIN_INVALID_CONFIG,PLUGIN_HOOK_FAILED,PLUGIN_ABORTED.PluginRegistryOptions—{ initTimeout?: number (default 30000), shutdownTimeout?: number (default 10000), strictOptionalDeps?: boolean, clock?: Clock }.initTimeoutis enforced — exceeded init rejects withPluginError(PLUGIN_INIT_TIMEOUT). Same for shutdown.AbortSignalpropagation — every lifecycle hook receivesctx.signal; aborted ops reject withPluginError(PLUGIN_ABORTED).- Subpath exports per feature module:
./define,./registry,./context,./service-registry,./hook-executor,./topo-sort,./error-codes,./types.
Out of scope
- Tenant lifecycle hooks (
onTenantCreate/Update/Delete/Suspend/Activate,onPluginEnable/Disable). Belong in@suaveplan/api-tenant(sibling changeadd-api-tenant) so non-tenant apps don't pay the surface cost. - Request lifecycle hooks (
onRequest/onResponse/onRequestError). Belong in@suaveplan/apiorchestrator + middleware layer (enterprise-api) because they need Hono request context to fire. - Database migration auto-application. The registry's upstream
applyMigrations()between register/init is dropped. Migrations are owned by@suaveplan/api-migrationsand run explicitly by the app orchestrator at the appropriate boot phase. - OpenAPI registry integration.
@suaveplan/api-openapiconsumes the plugin context but does not live inside the plugin package. - Filesystem plugin discovery (
PluginLoader.autoDiscover()). Has two known bugs upstream (prefix mismatch between filter and rewrite) and adds a Node-only filesystem dep to what is otherwise a universal-tier package. Belongs in a follow-up@suaveplan/api-plugin-loaderonly if a real consumer asks. Explicitplugins: [foo, bar]arrays are the v1 contract.
Source mapping
| Capability | Upstream source | Notes |
|---|---|---|
| Plugin interface + descriptor | ../suaveplan/packages/server/api/src/plugin/plugin.interface.ts L1-L201 |
port shape; switch hook surface to 4 lifecycle hooks only (others move to sibling packages) |
definePlugin factory |
../suaveplan/packages/server/api/src/plugin/plugin-metadata.ts L77-L123 |
port shape; drop decorator helpers (require reflect-metadata) |
PluginRegistry class |
../suaveplan/packages/server/api/src/plugin/plugin-registry.ts L44-L433 |
port + enforce initTimeout, unify lifecycle runtime, switch errors to defineCodes registry, drop applyMigrations |
PluginContext |
../suaveplan/packages/server/api/src/plugin/plugin-context.ts L43-L227 |
port shape; registerService/getService become typed via ServiceRegistry module augmentation; drop getPluginConfig(name) cross-plugin coupling; ALS-based getCurrentXxx() helpers dropped (callers thread ctx explicitly) |
topologicalSort + detectCycle + validateDependencies |
../suaveplan/packages/server/api/src/utils/topological-sort.ts L1-L157 |
verbatim port + alphabetical tie-break for stable order; drop duplicate validateDependencies in registry |
HookExecutor (priority-ordered dispatch) |
../suaveplan/packages/server/api/src/hooks/hook-executor.ts L1-L221 |
port shape; excludes the 4 lifecycle hook slots (those are registry-owned); switch to defineCodes errors |
| Plugin error subclasses | ../suaveplan/packages/server/api/src/errors/plugin-errors.ts L1-L97 |
replace with defineCodes("PLUGIN", [...]) registry + PluginError discriminant |
Impact
Affected dependents
None today — net-new on genesis. Forward-declared consumers (all sibling changes in this set + the 40+ in-flight enterprise-api-plugin-* packages):
@suaveplan/api-tenant(sibling) — adds tenant lifecycle hooks on top@suaveplan/api-openapi(sibling) — registers routes via pluginonInit@suaveplan/api(sibling) — composes the registry intoSuaveApp- Every
@suaveplan/api-plugin-*package (40+ inopenspec/changes/) — implementsPluginDescriptor