UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

280 lines 11.4 kB
/** * Provider system for Beignet * * Providers are modular extensions that can add new ports or replace existing ones * during application initialization. They support configuration via Standard Schema * and optional lifecycle hooks. */ import type { StandardSchemaV1 } from "@standard-schema/spec"; type ProviderPorts = Record<string, unknown>; declare const noProvidedPorts: unique symbol; type NoProvidedPorts = { [noProvidedPorts]?: never; }; /** * Extract the output type from a Standard Schema. * This is the validated/parsed type that results from schema validation. */ export type InferOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>; /** * Configuration definition for a service provider. * Specifies the schema for validating config and optional environment variable prefix. */ export interface ProviderConfigDef<CfgSchema extends StandardSchemaV1> { /** * Standard Schema for validating provider configuration. * Can be Zod, Valibot, ArkType, or any Standard Schema compatible library. */ schema: CfgSchema; /** * Optional prefix to read env vars, e.g. "REDIS_". * When provided, the implementation will read process.env keys starting with this prefix * and pass them to the schema for validation. */ envPrefix?: string; /** * Field-level config overrides, keyed by schema field name. Defined values * are merged over the env-derived (or server-supplied) input before * validation, so factory options win over environment variables and still * satisfy required fields when the env var is absent. `undefined` values * are ignored. */ overrides?: Record<string, unknown>; } /** * Value or promise of that value. */ export type MaybePromise<T> = T | Promise<T>; /** * Late-bound service context factory exposed to providers. * * Calling it before all providers have started throws, so providers should * only invoke it from runtime entrypoints such as job dispatch, listeners, or * scheduled work. * * App-local providers can type the factory by declaring `Context` and * `ServiceInput` through the curried `createProvider<Requires, Context, * ServiceInput>()` form. Untyped providers see `(input: void) => * Promise<unknown>`. */ export type ProviderServiceContextFactory<Context = unknown, ServiceInput = void> = (input: ServiceInput) => Promise<Context>; /** * Context passed to provider lifecycle hooks. */ export type ProviderLifecycleContext<Ports = ProviderPorts, Context = unknown, ServiceInput = void> = { /** * Final app ports after provider setup. */ ports: Readonly<Ports>; /** * Build an app service context through the server context blueprint. */ createServiceContext: ProviderServiceContextFactory<Context, ServiceInput>; }; /** * Result returned from provider setup. */ export type ProviderSetupResult<ProvidedPorts extends ProviderPorts, Ports = ProviderPorts, Context = unknown, ServiceInput = void> = { /** * Ports contributed by this provider. * Keys overwrite earlier ports with the same name at runtime. Prefer unique * keys unless the replacement implements the same port contract. */ ports?: ProvidedPorts; /** * Optional hook called after all providers have contributed their ports. * * Declared as a method so typed providers stay assignable to loosely typed * provider lists. Hooks that take `ctx` with an unannotated parameter keep * TypeScript from inferring `ProvidedPorts` from the returned `ports`. * Prefer closing over setup locals, or annotate `ctx` with * `ProviderLifecycleContext<...>`. */ start?(ctx: ProviderLifecycleContext<Ports & ProvidedPorts, Context, ServiceInput>): MaybePromise<void>; /** * Optional hook called when the server is stopped. */ stop?(ctx: ProviderLifecycleContext<Ports & ProvidedPorts, Context, ServiceInput>): MaybePromise<void>; }; /** * Static provider metadata used by docs and app-local tooling. * * Metadata is descriptive. It does not change provider setup, ordering, or * runtime port merging behavior. * * Reusable provider packages should also declare package-owned * `beignet.provider` metadata in package.json so external tooling can inspect * provider facts without importing runtime code. */ export interface ServiceProviderMetadata { /** * Package that exports this provider, when it comes from a reusable package. */ packageName?: string; /** * App port keys this provider contributes or replaces. */ ports?: readonly string[]; /** * App port keys this provider expects previous providers or base app ports to * have installed before setup runs. */ requires?: readonly string[]; /** * Environment variables this provider reads directly or via config loading. */ env?: readonly string[]; /** * Devtools watcher names this provider can emit through provider * instrumentation. */ watchers?: readonly string[]; } /** * A service provider that can extend or replace ports during app initialization. * * Providers support: * - Configuration via Standard Schema (any compatible library: Zod, Valibot, etc.) * - Returning ports with new capabilities (e.g., cache, mailer) * - Replacing existing ports by returning the same key * - Optional start/stop hooks * * @example * ```ts * const cacheProvider = createProvider({ * name: "cache-redis", * config: { * schema: z.object({ URL: z.string().url() }), * envPrefix: "REDIS_", * }, * async setup({ config }) { * const client = new Redis(config.URL); * return { * ports: { * cache: { * get: (key) => client.get(key), * set: (key, value) => client.set(key, value), * }, * }, * stop: () => client.quit(), * }; * }, * }); * ``` */ export interface ServiceProvider<Ports, CfgSchema extends StandardSchemaV1 = StandardSchemaV1<void, void>, ProvidedPorts extends ProviderPorts = NoProvidedPorts, Context = unknown, ServiceInput = void> { /** * Unique name for this provider (used for logging/debugging) */ name: string; /** * Optional static metadata for docs and diagnostics. */ metadata?: ServiceProviderMetadata; /** * Optional configuration definition. * If provided, the config will be loaded and validated before calling setup. */ config?: ProviderConfigDef<CfgSchema>; /** * Setup phase: create the ports this provider contributes. * Called during server initialization before provider `start` hooks and * before the server handles requests. * * @param ctx.ports - Ports contributed by previous providers * @param ctx.config - Validated config (if config was defined), or undefined * @param ctx.createServiceContext - Late-bound service context factory. * Throws until all providers have started, so call it lazily from runtime * entrypoints such as job dispatchers and event listeners. */ setup(ctx: { ports: Readonly<Ports>; config: InferOutput<CfgSchema> | undefined; createServiceContext: ProviderServiceContextFactory<Context, ServiceInput>; }): MaybePromise<ProviderSetupResult<ProvidedPorts, Ports, Context, ServiceInput>>; /** * Type-only marker for ports this provider contributes. * Runtime provider objects do not need to set this property. */ readonly __providedPorts?: ProvidedPorts; } /** * A provider configuration schema whose concrete validation library and input * shape are intentionally erased while its validated output may stay typed. * * Reusable provider packages use this in their named provider return types so * internal Zod schemas do not become part of the package's public API. */ export type AnyProviderConfigSchema<Output = any> = StandardSchemaV1<any, Output>; /** * Loosely typed service provider. * * Required-port, config, app-context, and service-input generics are erased * here so any provider created with `createProvider(...)` — including the * typed curried form — stays assignable. Use this for code that works across * arbitrary providers, such as provider lists and test helpers. */ export type AnyServiceProvider = ServiceProvider<unknown, AnyProviderConfigSchema, ProviderPorts, any, any>; /** * Extract the ports a provider contributes. */ export type ProvidedPortsOf<TProvider> = TProvider extends ServiceProvider<infer _Ports, infer _CfgSchema, infer ProvidedPorts, infer _Context, infer _ServiceInput> ? ProvidedPorts : NoProvidedPorts; type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer I) => void ? I : never; /** * Extract and merge the ports contributed by a provider list. * * Use this with `typeof providers` to type provider-contributed ports in app * code without hand-written casts: * * @example * ```ts * import type { InferProviderPorts } from "@beignet/core/providers"; * import type { providers } from "@/server/providers"; * import type { AppPorts } from "@/ports"; * * export type AppRuntimePorts = AppPorts & InferProviderPorts<typeof providers>; * ``` */ export type InferProviderPorts<TProviders> = TProviders extends readonly unknown[] ? [TProviders[number]] extends [never] ? NoProvidedPorts : UnionToIntersection<ProvidedPortsOf<TProviders[number]>> : NoProvidedPorts; /** * Helper function to create a provider with proper type inference. * * This is a simple identity function that helps TypeScript infer the correct types * for the provider definition. * * App-local providers can use the curried zero-argument form to declare the * ports they require from earlier providers plus their app context and * service-context input. The required ports, `ctx.ports`, and * `ctx.createServiceContext` are then fully typed with no casts. * * @example * ```ts * export const myProvider = createProvider({ * name: "my-provider", * config: { * schema: z.object({ apiKey: z.string() }), * envPrefix: "MY_SERVICE_", * }, * async setup({ config }) { * return { ports: { myService: createMyService(config) } }; * }, * }); * * // Typed app-local provider: * export const appDatabaseProvider = createProvider< * { db: DbPort<typeof schema>; devtools?: DevtoolsPort }, * AppContext, * AppServiceContextInput * >()({ * name: "app-database", * async setup({ ports, createServiceContext }) { * const repositories = createRepositories(ports.db.drizzle); * return { ports: repositories }; * }, * }); * ``` */ export declare function createProvider<Requires = unknown, Context = unknown, ServiceInput = void>(): <CfgSchema extends StandardSchemaV1 = StandardSchemaV1<void, void>, Provided extends ProviderPorts = NoProvidedPorts>(def: ServiceProvider<Requires, CfgSchema, Provided, Context, ServiceInput>) => ServiceProvider<Requires, CfgSchema, Provided, Context, ServiceInput>; export declare function createProvider<Ports = unknown, CfgSchema extends StandardSchemaV1 = StandardSchemaV1<void, void>, ProvidedPorts extends ProviderPorts = NoProvidedPorts>(def: ServiceProvider<Ports, CfgSchema, ProvidedPorts>): ServiceProvider<Ports, CfgSchema, ProvidedPorts>; export {}; //# sourceMappingURL=provider.d.ts.map