/** * RpcPromise - A promise wrapper that supports capnweb magic map and pipelining * * This class enables: * - Magic map support for N+1 query elimination * - Promise pipelining for batched operations * - Lazy execution (queries don't execute until awaited or .map()'d) * * @example Magic Map * ```typescript * const usersWithOrders = await db.query('SELECT * FROM users') * .map(users => Promise.all( * users.map(u => db.query('SELECT * FROM orders WHERE user_id = $1', [u.id])) * )) * ``` * * @example Promise Pipelining * ```typescript * const user = db.query('SELECT * FROM users WHERE id = $1', [id]) * const posts = user.map(u => db.query('SELECT * FROM posts WHERE author = $1', [u.id])) * const comments = posts.map(p => db.query('SELECT * FROM comments WHERE post_id = ANY($1)', [p.map(x => x.id)])) * const result = await comments // Single round trip! * ``` */ import type { Row } from '../types' /** * Execution context for RPC pipelining * Accumulates queries that can be batched together */ export interface RpcExecutionContext { /** Add a query to the batch */ addQuery(sql: string, params?: unknown[]): number /** Execute all batched queries */ executeBatch(): Promise /** Get a pending result by query ID */ getResult(queryId: number): Promise /** Check if the context is currently collecting queries */ isCollecting(): boolean /** Start collecting queries for batching */ startCollecting(): void /** Stop collecting and execute batch */ stopCollecting(): Promise } /** * Result from a batched query */ export interface BatchResult { queryId: number rows: T[] fields: Array<{ name: string; dataTypeID: number }> rowCount: number durationMs: number } /** * Map function signature for RpcPromise */ export type MapFn = (value: T) => U | Promise /** * RpcPromise implementation * Wraps a query operation with magic map and pipelining support */ export class RpcPromise implements PromiseLike { private executor: () => Promise private cachedResult: T | undefined private executed = false private executing: Promise | null = null private context: RpcExecutionContext | null constructor(executor: () => Promise, context?: RpcExecutionContext | null) { this.executor = executor this.context = context ?? null } /** * Map over the result with magic map support * * The map function receives the resolved value and can return: * - A plain value * - A Promise * - Another RpcPromise (for pipelining) * * When the callback returns RpcPromises, they are batched together * with the original query for efficient execution. * * @example * ```typescript * const usersWithPosts = db.query('SELECT * FROM users') * .map(users => Promise.all( * users.map(u => db.query('SELECT * FROM posts WHERE user_id = $1', [u.id])) * )) * ``` */ map(fn: MapFn): RpcPromise { return new RpcPromise(async () => { // If we have a context and it's collecting, use batched execution if (this.context?.isCollecting()) { const result = await this.execute() const mapped = fn(result) // If the mapped result is also collecting queries, batch them together if (mapped instanceof RpcPromise) { return mapped.execute() } return mapped } // Standard execution path const result = await this.execute() const mapped = fn(result) if (mapped instanceof Promise || mapped instanceof RpcPromise) { return mapped as Promise } return mapped }, this.context) } /** * FlatMap for when the map function returns an RpcPromise */ flatMap(fn: (value: T) => RpcPromise): RpcPromise { return new RpcPromise(async () => { const result = await this.execute() const mapped = fn(result) return mapped.execute() }, this.context) } /** * Execute the query and return the result */ async execute(): Promise { // Return cached result if already executed if (this.executed && this.cachedResult !== undefined) { return this.cachedResult } // If already executing, wait for that execution if (this.executing) { return this.executing } // Start execution this.executing = this.executor() try { this.cachedResult = await this.executing this.executed = true return this.cachedResult } finally { this.executing = null } } /** * Reset the execution state to allow re-execution */ reset(): void { this.executed = false this.cachedResult = undefined this.executing = null } /** * Check if the promise has been executed */ isExecuted(): boolean { return this.executed } /** * PromiseLike implementation - makes this thenable */ then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null ): Promise { return this.execute().then(onfulfilled, onrejected) } /** * Catch handler */ catch( onrejected?: ((reason: unknown) => TResult | PromiseLike) | null ): Promise { return this.execute().catch(onrejected) } /** * Finally handler */ finally(onfinally?: (() => void) | null): Promise { return this.execute().finally(onfinally) } } /** * Create an RpcPromise from a query function */ export function createRpcPromise( executor: () => Promise, context?: RpcExecutionContext | null ): RpcPromise { return new RpcPromise(executor, context) } /** * Query result type for typed queries */ export interface RpcQueryResult { rows: T[] fields: Array<{ name: string; dataTypeID: number }> rowCount: number durationMs: number } /** * Create an RpcPromise that extracts rows from a query result */ export function createRowsPromise( executor: () => Promise>, context?: RpcExecutionContext | null ): RpcPromise { return new RpcPromise(async () => { const result = await executor() return result.rows }, context) } /** * Batch multiple RpcPromises for efficient execution * * This collects all queries from the provided promises and executes * them in a single batch, then distributes the results. * * @example * ```typescript * const [users, orders, products] = await batchExecute([ * db.query('SELECT * FROM users'), * db.query('SELECT * FROM orders'), * db.query('SELECT * FROM products'), * ]) * ``` */ export async function batchExecute[]>( promises: T ): Promise<{ [K in keyof T]: T[K] extends RpcPromise ? U : never }> { // Execute all promises in parallel const results = await Promise.all(promises.map(p => p.execute())) return results as { [K in keyof T]: T[K] extends RpcPromise ? U : never } } /** * Create a magic map wrapper that batches nested queries * * This enables the N+1 query elimination pattern: * ```typescript * const usersWithOrders = await magicMap( * db.query('SELECT * FROM users'), * users => users.map(u => db.query('SELECT * FROM orders WHERE user_id = $1', [u.id])) * ) * ``` */ export async function magicMap( source: RpcPromise, mapFn: (items: T[]) => RpcPromise[] ): Promise { const items = await source.execute() const promises = mapFn(items) return Promise.all(promises.map(p => p.execute())) }