@beignet/core
Version:
Core framework primitives for Beignet
280 lines • 11.4 kB
TypeScript
/**
* 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