/** * @sylphx/lens-core - Subscription Builder * * Fluent interface for defining subscriptions. * * Subscription is for event streams - no initial data, only pushes events. */ import type { Publisher } from "../resolvers/resolver-types.js"; import type { InferReturnType, ReturnSpec, ZodLikeSchema } from "./types.js"; // ============================================================================= // Subscription Definition // ============================================================================= /** Subscription definition - event stream only (no initial data) */ export interface SubscriptionDef { _type: "subscription"; /** Subscription name (optional - derived from export key if not provided) */ _name?: string | undefined; _input?: ZodLikeSchema | undefined; _output?: ReturnSpec | undefined; /** Branded phantom types for inference */ _brand: { input: TInput; output: TOutput }; /** Method syntax for bivariance - allows flexible context types */ _subscriber?(ctx: { args: TInput; ctx: TContext }): Publisher; } // ============================================================================= // Subscription Builder Interface // ============================================================================= /** Subscription builder - fluent interface */ export interface SubscriptionBuilder { /** Define args validation schema (optional for subscriptions) */ args(schema: ZodLikeSchema): SubscriptionBuilder; /** Define return type (optional - for entity outputs) */ returns(spec: R): SubscriptionBuilder, TContext>; /** * Define subscription publisher (returns Publisher). * The Publisher receives { emit, onCleanup } callbacks. * Returns only event stream - no initial data fetch. * * @example * ```typescript * // Event-only subscription * subscription() * .args(z.object({ authorId: z.string().optional() })) * .returns(Post) * .subscribe(({ args, ctx }) => ({ emit, onCleanup }) => { * const unsub = ctx.events.on("post:created", (post) => { * if (!args.authorId || post.authorId === args.authorId) { * emit(post); * } * }); * onCleanup(unsub); * }); * ``` */ subscribe( fn: (ctx: { args: TInput; ctx: TContext }) => Publisher, ): SubscriptionDef; } // ============================================================================= // Subscription Builder Implementation // ============================================================================= export class SubscriptionBuilderImpl implements SubscriptionBuilder { private _name?: string | undefined; private _inputSchema?: ZodLikeSchema | undefined; private _outputSpec?: ReturnSpec | undefined; constructor(name?: string) { this._name = name; } args(schema: ZodLikeSchema): SubscriptionBuilder { const builder = new SubscriptionBuilderImpl(this._name); builder._inputSchema = schema; builder._outputSpec = this._outputSpec; return builder; } returns( spec: R, ): SubscriptionBuilder, TContext> { const builder = new SubscriptionBuilderImpl, TContext>(this._name); builder._inputSchema = this._inputSchema as ZodLikeSchema | undefined; builder._outputSpec = spec; return builder; } subscribe( fn: (ctx: { args: TInput; ctx: TContext }) => Publisher, ): SubscriptionDef { return { _type: "subscription", _name: this._name, _input: this._inputSchema, _output: this._outputSpec, _brand: {} as { input: TInput; output: T }, _subscriber: fn, }; } } // ============================================================================= // Factory Function // ============================================================================= /** * Create a subscription builder * * @example * ```typescript * // Basic usage * export const onPostCreated = subscription() * .args(z.object({ authorId: z.string().optional() })) * .returns(Post) * .subscribe(({ args, ctx }) => ({ emit, onCleanup }) => { * const unsub = ctx.events.on("post:created", (post) => { * if (!args.authorId || post.authorId === args.authorId) { * emit(post); * } * }); * onCleanup(unsub); * }); * * // With typed context * export const onUserStatusChange = subscription() * .args(z.object({ userId: z.string() })) * .returns(UserStatus) * .subscribe(({ args, ctx }) => ({ emit, onCleanup }) => { * const unsub = ctx.statusService.watch(args.userId, emit); * onCleanup(unsub); * }); * ``` */ export function subscription(): SubscriptionBuilder; export function subscription( name: string, ): SubscriptionBuilder; export function subscription( name?: string, ): SubscriptionBuilder { return new SubscriptionBuilderImpl(name); } // ============================================================================= // Type Guard // ============================================================================= /** Check if value is a subscription definition */ export function isSubscriptionDef(value: unknown): value is SubscriptionDef { return ( typeof value === "object" && value !== null && (value as SubscriptionDef)._type === "subscription" ); }