@beignet/core
Version:
Core framework primitives for Beignet
596 lines • 19.9 kB
TypeScript
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { ChannelDefinition } from "../broadcasting/index.js";
import type { BroadcastPort, BroadcastPublication } from "../broadcasting/server.js";
import { type JobDef, type JobDispatcher, type JobHook, type JobRetryOptions, type JobTimeoutDuration } from "../jobs/index.js";
import type { SendMailOptions } from "../mail/index.js";
import type { ProviderInstrumentationTarget } from "../providers/index.js";
/**
* Any Standard Schema compatible validator.
*/
export type StandardSchema = StandardSchemaV1<unknown, unknown>;
/**
* Value or promise of that value.
*/
export type MaybePromise<T> = T | Promise<T>;
/**
* Infer the parsed output type from a Standard Schema.
*/
export type InferSchemaOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
/**
* Minimal notification definition shape accepted by notification ports.
*/
export interface NotificationPayloadDef<Name extends string = string, Payload extends StandardSchema = StandardSchema> {
/**
* Stable notification name used by dispatchers, tests, and tooling.
*/
readonly name: Name;
/**
* Standard Schema payload validator.
*/
readonly payload: Payload;
/**
* Optional human-readable description for docs and tooling.
*/
readonly description?: string;
}
/**
* Infer the parsed payload type for a notification definition.
*/
export type InferNotificationPayload<N extends NotificationPayloadDef> = N["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;
/**
* Result for one notification channel.
*/
export interface NotificationChannelResult {
/**
* Channel name, such as `email`, `sms`, `push`, or `inApp`.
*/
channel: string;
/**
* Delivery outcome for this channel.
*/
status: "queued" | "sent" | "skipped" | "failed";
/**
* Provider delivery ID when available.
*/
id?: string;
/**
* Provider name when available.
*/
provider?: string;
/**
* Human-readable skip or failure reason.
*/
reason?: string;
/**
* Channel-specific metadata. Dispatchers should keep this safe to log.
*/
details?: Record<string, unknown>;
}
/**
* Original error captured for one failed notification channel.
*/
export interface NotificationChannelError {
channel: string;
error: unknown;
}
/**
* Arguments passed to a notification channel handler.
*/
export interface NotificationChannelHandleArgs<Payload extends StandardSchema, Ctx> {
/**
* Notification definition being delivered.
*/
notification: NotificationDef<string, Payload, Ctx>;
/**
* Parsed notification payload.
*/
payload: InferSchemaOutput<Payload>;
/**
* Handler context.
*/
ctx: Ctx;
/**
* Channel name being delivered.
*/
channel: string;
}
/**
* Handler for one notification channel.
*/
export type NotificationChannelHandler<Payload extends StandardSchema, Ctx> = (args: NotificationChannelHandleArgs<Payload, Ctx>) => MaybePromise<NotificationChannelResult | undefined>;
/**
* Notification channel handlers keyed by channel name.
*/
export type NotificationChannels<Payload extends StandardSchema, Ctx> = Record<string, NotificationChannelHandler<Payload, Ctx>>;
/**
* Arguments passed to an app-owned notification preference evaluator.
*/
export interface NotificationPreferenceArgs<Payload extends StandardSchema = StandardSchema, Ctx = unknown> extends NotificationChannelHandleArgs<Payload, Ctx> {
/**
* Optional metadata supplied by the notification sender.
*/
metadata?: Record<string, unknown>;
}
/**
* App-owned decision for one notification channel.
*/
export interface NotificationPreferenceDecision {
/**
* Whether this channel should deliver.
*/
deliver: boolean;
/**
* Optional reason recorded when delivery is skipped.
*/
reason?: string;
}
/**
* Optional app-facing port for notification channel preferences and opt-outs.
*/
export interface NotificationPreferencesPort<Ctx = unknown> {
/**
* Evaluate the current preference immediately before channel delivery.
*/
evaluate(args: NotificationPreferenceArgs<StandardSchema, Ctx>): MaybePromise<NotificationPreferenceDecision>;
}
/**
* Notification definition created by `defineNotification(...)`.
*/
export interface NotificationDef<Name extends string = string, Payload extends StandardSchema = StandardSchema, Ctx = unknown> extends NotificationPayloadDef<Name, Payload> {
/**
* Discriminator for notification definitions.
*/
readonly kind: "notification";
/**
* Channel handlers that deliver the notification.
*/
readonly channels: NotificationChannels<Payload, Ctx>;
}
/**
* Options for declaring a typed notification.
*/
export interface DefineNotificationOptions<Payload extends StandardSchema, Ctx> {
/**
* Standard Schema payload validator.
*/
payload: Payload;
/**
* Optional human-readable description for docs and tooling.
*/
description?: string;
/**
* Channel handlers that deliver the notification.
*/
channels: NotificationChannels<Payload, Ctx>;
}
/**
* Options passed when sending a notification.
*/
export interface SendNotificationOptions {
/**
* Subset of channels to deliver. Defaults to all channels on the definition.
*/
channels?: readonly string[];
/**
* Optional app metadata attached to memory deliveries and instrumentation.
*/
metadata?: Record<string, unknown>;
/**
* Request correlation ID for instrumentation.
*/
requestId?: string;
/**
* Trace identifier for instrumentation.
*/
traceId?: string;
/**
* Span identifier for instrumentation.
*/
spanId?: string;
/**
* Parent span identifier for instrumentation.
*/
parentSpanId?: string;
/**
* W3C traceparent header value for instrumentation.
*/
traceparent?: string;
}
/**
* Result returned after a notification send attempt.
*/
export interface SendNotificationResult {
/**
* Notification name.
*/
notificationName: string;
/**
* Parsed notification payload.
*/
payload: unknown;
/**
* Channels selected for delivery.
*/
channels: readonly string[];
/**
* Per-channel delivery results.
*/
results: readonly NotificationChannelResult[];
}
/**
* App-facing notification port.
*/
export interface NotificationPort {
/**
* Send a typed notification.
*/
send<N extends NotificationDef>(notification: N, payload: InferNotificationPayload<N>, options?: SendNotificationOptions): Promise<SendNotificationResult>;
}
/**
* Options for the inline notification dispatcher.
*/
export interface InlineNotificationDispatcherOptions<Ctx> {
/**
* Static notification context or factory evaluated for each send.
*/
ctx?: Ctx | (() => MaybePromise<Ctx>);
/**
* Called when a channel handler or preference check fails. A returned result
* replaces the default failed result. Observer failures are ignored so the
* remaining channels still run.
*/
onError?: (error: unknown, args: NotificationChannelHandleArgs<StandardSchema, Ctx>) => MaybePromise<NotificationChannelResult | undefined>;
/**
* How completed channel failures are surfaced. Defaults to `"report"`.
* `"throw"` still runs every selected channel before rejecting.
*/
failureMode?: "report" | "throw";
/**
* Optional app-owned notification preference evaluator.
*/
preferences?: NotificationPreferencesPort<Ctx>;
/**
* Optional devtools/provider instrumentation target.
*/
instrumentation?: ProviderInstrumentationTarget;
}
/**
* Delivery captured by the memory notification port.
*/
export interface MemoryNotificationDelivery {
/**
* Generated delivery ID.
*/
id: string;
/**
* Notification name.
*/
notificationName: string;
/**
* Parsed payload that would have been sent.
*/
payload: unknown;
/**
* Selected channels.
*/
channels: readonly string[];
/**
* Optional app metadata supplied by the caller.
*/
metadata?: Record<string, unknown>;
/**
* Timestamp assigned by the memory port.
*/
sentAt: Date;
}
/**
* In-memory notification port for tests and local examples.
*/
export interface MemoryNotificationPort extends NotificationPort {
/**
* Captured notification sends.
*/
readonly deliveries: readonly MemoryNotificationDelivery[];
/**
* Clear captured notification sends.
*/
clear(): void;
}
/**
* Options for `createMemoryNotificationPort(...)`.
*/
export interface CreateMemoryNotificationPortOptions {
/**
* Clock used for captured deliveries.
*/
now?: () => Date;
/**
* ID factory used for captured deliveries.
*/
id?: () => string;
/**
* Observer called after a delivery is captured.
*/
onSend?: (delivery: MemoryNotificationDelivery) => MaybePromise<void>;
}
/**
* Context shape required by `defineMailNotificationChannel(...)`.
*/
export interface MailNotificationContext {
ports: {
mailer: {
send(message: SendMailOptions): MaybePromise<{
id?: string;
provider?: string;
}>;
};
};
}
/**
* Render a mail message for one notification payload.
*/
export type MailNotificationRenderer<Payload extends StandardSchema, Ctx extends MailNotificationContext> = (args: NotificationChannelHandleArgs<Payload, Ctx>) => MaybePromise<SendMailOptions | undefined>;
/**
* Context-bound notification helper factory.
*/
export interface Notifications<Ctx> {
/**
* Define a notification with the bound context type.
*/
defineNotification<Name extends string, Payload extends StandardSchema>(name: Name, options: DefineNotificationOptions<Payload, Ctx>): NotificationDef<Name, Payload, Ctx>;
}
/**
* Notification definitions available to durable delivery workers.
*/
export interface NotificationRegistry<Ctx = unknown> {
/**
* Registered definitions in declaration order.
*/
readonly definitions: readonly NotificationDef<string, StandardSchema, Ctx>[];
/**
* Resolve a notification definition by its stable name.
*/
get(name: string): NotificationDef<string, StandardSchema, Ctx> | undefined;
}
/**
* Payload carried by the first-party notification delivery job.
*/
export interface NotificationDeliveryJobPayload {
notificationName: string;
channel: string;
payload: unknown;
options: Omit<SendNotificationOptions, "channels">;
}
type NotificationDeliveryPayloadSchema = StandardSchemaV1<unknown, NotificationDeliveryJobPayload>;
/**
* Job definition used by queued notification dispatchers and workers.
*/
export interface NotificationDeliveryJob<Name extends string = string, Ctx = unknown> extends JobDef<Name, NotificationDeliveryPayloadSchema, Ctx> {
/**
* Registry used by both enqueue-time checks and worker delivery.
*/
readonly registry: NotificationRegistry<Ctx>;
}
/**
* Options for the first-party notification delivery job.
*/
export interface DefineNotificationDeliveryJobOptions<Name extends string, Ctx> {
/**
* Stable job name. Defaults to `"notifications.deliver"`.
*/
name?: Name;
/**
* Notification definitions available to the worker.
*/
registry: NotificationRegistry<Ctx>;
/**
* Optional app-owned preferences evaluated when the job runs.
*/
preferences?: NotificationPreferencesPort<Ctx>;
/**
* Retry policy. Defaults to exponential backoff with three attempts.
*/
retry?: JobRetryOptions;
/**
* Optional maximum duration for each channel delivery attempt.
*/
timeout?: JobTimeoutDuration;
/**
* Optional execution hooks applied to each delivery attempt.
*/
hooks?: readonly JobHook<JobDef<Name, NotificationDeliveryPayloadSchema, Ctx>, Ctx>[];
}
/**
* Options for a notification dispatcher backed by Beignet jobs.
*/
export interface QueuedNotificationDispatcherOptions<Name extends string, Ctx> {
/**
* Job dispatcher used to enqueue one delivery job per channel.
*/
jobs: JobDispatcher;
/**
* Registered notification delivery job.
*/
deliveryJob: NotificationDeliveryJob<Name, Ctx>;
/**
* Optional devtools/provider instrumentation target.
*/
instrumentation?: ProviderInstrumentationTarget;
}
/**
* Error thrown when notification payload validation fails.
*/
export declare class NotificationValidationError extends Error {
/**
* Raw Standard Schema validation issues.
*/
readonly issues: readonly StandardSchemaV1.Issue[];
constructor(args: {
name: string;
issues: readonly StandardSchemaV1.Issue[];
});
}
/**
* Error thrown when notification delivery fails.
*/
export declare class NotificationDeliveryError extends Error {
/**
* Notification name.
*/
readonly notificationName: string;
/**
* First channel that failed, retained for concise error handling.
*/
readonly channel: string;
/**
* Original error for the first failed channel when available.
*/
readonly cause: unknown;
/**
* Complete notification result after every selected channel ran.
*/
readonly result: SendNotificationResult;
/**
* Failed channel results.
*/
readonly failures: readonly NotificationChannelResult[];
/**
* Original channel errors in delivery order.
*/
readonly errors: readonly NotificationChannelError[];
constructor(args: {
result: SendNotificationResult;
errors?: readonly NotificationChannelError[];
});
}
/**
* Error thrown when a notification registry cannot safely resolve a delivery.
*/
export declare class NotificationRegistryError extends Error {
constructor(message: string);
}
/**
* Define the notification catalog available to durable delivery workers.
* Duplicate names throw because queued delivery resolves definitions by name.
*/
export declare function defineNotificationRegistry<Ctx>(definitions: readonly NotificationDef<string, StandardSchema, Ctx>[]): NotificationRegistry<Ctx>;
/**
* Define the generic job that resolves and delivers one notification channel.
* Register the returned job with every worker or outbox registry that can
* receive queued notifications.
*/
export declare function defineNotificationDeliveryJob<Ctx, Name extends string = "notifications.deliver">(options: DefineNotificationDeliveryJobOptions<Name, Ctx>): NotificationDeliveryJob<Name, Ctx>;
/**
* Validate and parse a notification payload with the notification's Standard
* Schema.
*/
export declare function parseNotificationPayload<N extends NotificationPayloadDef>(notification: N, payload: unknown): Promise<InferNotificationPayload<N>>;
/**
* Create an inline notification dispatcher.
*
* The dispatcher validates payloads and runs selected channel handlers
* immediately. Channel failures are isolated and reported after every selected
* channel runs. Use this directly in tests and local apps, or use
* `createQueuedNotificationDispatcher(...)` for background execution.
*/
export declare function createInlineNotificationDispatcher<Ctx>(options?: InlineNotificationDispatcherOptions<Ctx>): NotificationPort;
/**
* Options for the inline notifications provider.
*/
export interface InlineNotificationsProviderOptions extends Omit<InlineNotificationDispatcherOptions<unknown>, "ctx" | "instrumentation"> {
/**
* Provider name. Defaults to "inline-notifications".
*/
name?: string;
}
/**
* Ports contributed by the inline notifications provider.
*/
export interface InlineNotificationsProviderPorts {
/**
* Beignet notification port.
*/
notifications: NotificationPort;
}
/**
* Create a provider that contributes an inline notification dispatcher.
*
* Use it as the dev-default `notifications` port in `server/providers.ts`.
* Channel handlers run with an app service context built lazily through the
* server context blueprint on each send, so the provider is safe to register
* before all providers have started. Sends are recorded as devtools events
* through the `notifications` watcher when an instrumentation port is
* installed.
*/
export declare function createInlineNotificationsProvider(options?: InlineNotificationsProviderOptions): import("../providers/provider.js").ServiceProvider<unknown, StandardSchemaV1<void, void>, {
notifications: NotificationPort;
}, unknown, void>;
/**
* Create a notification dispatcher that enqueues one delivery job per channel.
* Separate jobs keep provider retries from resending channels that already
* completed successfully.
*/
export declare function createQueuedNotificationDispatcher<Name extends string, Ctx>(options: QueuedNotificationDispatcherOptions<Name, Ctx>): NotificationPort;
/**
* Options for the queued notifications provider.
*/
export interface QueuedNotificationsProviderOptions<Name extends string, Ctx> {
/**
* Registered notification delivery job.
*/
deliveryJob: NotificationDeliveryJob<Name, Ctx>;
/**
* Provider name. Defaults to `"queued-notifications"`.
*/
name?: string;
}
/**
* Create a provider that contributes a job-backed notification dispatcher.
*/
export declare function createQueuedNotificationsProvider<Name extends string, Ctx>(options: QueuedNotificationsProviderOptions<Name, Ctx>): import("../providers/provider.js").ServiceProvider<{
jobs: JobDispatcher;
}, StandardSchemaV1<void, void>, {
notifications: NotificationPort;
}, unknown, void>;
/**
* Define a mail-backed notification channel.
*
* Return `undefined` from the renderer when the channel should be skipped, for
* example when a recipient does not have an email address.
*/
export declare function defineMailNotificationChannel<Payload extends StandardSchema, Ctx extends MailNotificationContext>(render: MailNotificationRenderer<Payload, Ctx>): NotificationChannelHandler<Payload, Ctx>;
/** Deliver a typed browser hint through the existing notification delivery pipeline.
* `sent` means provider acceptance, including when the recipient is offline.
* Return undefined to skip. Persistent inbox writes and their outbox ordering belong to the app.
*/
export declare function defineBroadcastNotificationChannel<C extends ChannelDefinition, Payload extends StandardSchema, Ctx extends {
ports: {
broadcast: BroadcastPort;
};
}>(options: {
channel: C;
render: (args: NotificationChannelHandleArgs<Payload, Ctx>) => MaybePromise<BroadcastPublication<NoInfer<C>> | undefined>;
}): NotificationChannelHandler<Payload, Ctx>;
/**
* Create an in-memory notification port for tests and examples.
*
* The memory port validates payloads and records notification intent without
* running channel handlers.
*/
export declare function createMemoryNotificationPort(options?: CreateMemoryNotificationPortOptions): MemoryNotificationPort;
/**
* Create notification helper methods bound to an application context type.
*
* Call it once in `lib/notifications.ts`:
*
* ```ts
* export const { defineNotification } = createNotifications<AppContext>();
* ```
*
* Notifications represent user-facing communication intent. Channel handlers
* decide how that intent becomes mail, SMS, push, in-app delivery, or another
* app-owned channel.
*/
export declare function createNotifications<Ctx>(): Notifications<Ctx>;
export {};
//# sourceMappingURL=index.d.ts.map