@frak-labs/frame-connector
Version:
Generic, type-safe RPC communication layer for bidirectional postMessage communication
252 lines (240 loc) • 7.93 kB
text/typescript
/**
* Client-side compression middleware
*
* Compresses outgoing requests and decompresses incoming responses.
* Always uses the format: {method: string, params: unknown}
*
* @example Client side
* ```ts
* const client = createRpcClient({
* transport: iframe.contentWindow,
* targetOrigin: 'https://wallet.frak.id',
* middleware: [createClientCompressionMiddleware()]
* })
* ```
*/
export declare const createClientCompressionMiddleware: <TSchema extends RpcSchema, TContext>() => RpcMiddleware<TSchema, TContext>;
/**
* Listener-side compression middleware
*
* Decompresses incoming requests and compresses outgoing responses.
* Always uses the format: {method: string, params: unknown}
*
* @example Listener side
* ```ts
* const listener = createRpcListener({
* transport: window,
* allowedOrigins: ['https://example.com'],
* middleware: [createListenerCompressionMiddleware()]
* })
* ```
*/
export declare const createListenerCompressionMiddleware: <TSchema extends RpcSchema, TContext>() => RpcMiddleware<TSchema, TContext>;
/**
* Extract method names from a schema
*
* @typeParam TSchema - The RPC schema type
*
* @example
* ```ts
* type Methods = ExtractMethod<MySchema>
* // "greet" | "watchTime"
* ```
*/
declare type ExtractMethod<TSchema extends RpcSchema> = TSchema[number]["Method"];
/**
* RPC error object
*/
declare type RpcError = {
code: number;
message: string;
data?: unknown;
};
/**
* RPC message format (maintains backward compatibility)
* This is the exact format sent over the wire
*
* @typeParam TMethod - The method name type (defaults to string for flexibility)
*/
declare type RpcMessage<TMethod extends string = string> = {
/**
* Unique message identifier for correlating requests and responses
*/
id: string;
/**
* The RPC method name (topic for backward compatibility)
*/
topic: TMethod;
/**
* The message payload (compressed data) or raw params
*/
data: unknown;
};
/**
* Unified middleware function for RPC requests (both listener and client)
* Works on both listener-side (with context augmentation) and client-side (empty context)
*
* Key features:
* - Can mutate message.data directly for efficiency (compression, validation)
* - Can mutate response.result directly for transformation
* - Listener-side: Can augment context by returning modified context
* - Client-side: Uses TContext = {} (empty context), always returns unchanged
*
* @typeParam TSchema - The RPC schema type
* @typeParam TContext - Custom context type to augment base context (empty {} for client-side)
*
* @example Listener-side with context augmentation
* ```ts
* type WalletContext = { productId: string, sourceUrl: string }
* const contextMiddleware: RpcMiddleware<MySchema, WalletContext> = {
* onRequest: async (message, context) => {
* // Read from store and augment context
* const productId = await getProductId(context.origin)
* return { ...context, productId, sourceUrl: context.origin }
* }
* }
* ```
*
* @example Client-side (empty context)
* ```ts
* const compressionMiddleware: RpcMiddleware<MySchema> = {
* onRequest: async (message, context) => {
* // Mutate message.data directly
* message.data = compress(message.data)
* return context // Empty context, unchanged
* },
* onResponse: async (message, response, context) => {
* // Mutate response.result directly
* response.result = decompress(response.result)
* return response
* }
* }
* ```
*
* @example Shared middleware (works on both sides)
* ```ts
* const loggingMiddleware: RpcMiddleware<MySchema> = {
* onRequest: async (message, context) => {
* console.log(`[RPC] ${message.topic}`, context.origin || 'client')
* return context
* },
* onResponse: async (message, response, context) => {
* console.log(`[RPC] ${message.topic} completed`)
* return response
* }
* }
* ```
*/
declare type RpcMiddleware<TSchema extends RpcSchema, TContext = Record<string, never>> = {
/**
* Called before handler execution (listener) or before sending (client)
*
* For listener: Can augment context and mutate message
* For client: Can mutate message, context is empty {}
*
* @param message - The RPC message (can be mutated)
* @param context - Request context (listener-side) or empty (client-side)
* @returns Updated context (listener mutates this, client returns unchanged)
* @throws FrakRpcError to reject the request with a specific error code
*/
onRequest?: (message: RpcMessage<ExtractMethod<TSchema>>, context: RpcMiddlewareContext<TContext>) => Promise<RpcMiddlewareContext<TContext>> | RpcMiddlewareContext<TContext>;
/**
* Called after handler execution (listener) or after receiving (client)
*
* @param message - The original RPC message
* @param response - The response (can be mutated)
* @param context - Request context (listener-side) or empty (client-side)
* @returns Transformed response
* @throws Error to send an error response instead
*/
onResponse?: (message: RpcMessage<ExtractMethod<TSchema>>, response: RpcResponse, context: RpcMiddlewareContext<TContext>) => Promise<RpcResponse> | RpcResponse;
};
/**
* Middleware context that can be augmented with custom fields
* Generic type parameter allows domain-specific context augmentation
*
* @typeParam TCustomContext - Custom context fields to merge with base context
*
* @example
* ```ts
* type WalletContext = RpcMiddlewareContext<{
* productId: string
* sourceUrl: string
* isAutoContext: boolean
* }>
* // { origin: string, source: MessageEventSource | null, productId: string, sourceUrl: string, isAutoContext: boolean }
* ```
*/
declare type RpcMiddlewareContext<TCustomContext = Record<string, never>> = RpcRequestContext & TCustomContext;
/**
* Request context for handlers
* Contains information about the origin and source of the request
*/
declare type RpcRequestContext = {
/**
* Origin of the request
*/
origin: string;
/**
* Message source (for responding)
*/
source: MessageEventSource | null;
};
/**
* RPC response wrapper
* Contains either a successful result or an error
*/
declare type RpcResponse<TResult = unknown> = {
result: TResult;
error?: never;
} | {
result?: never;
error: RpcError;
};
/**
* An RPC schema is a readonly array of schema entries
*
* @example
* ```ts
* type MySchema = [
* {
* Method: "greet";
* Parameters: [name: string];
* ReturnType: string;
* ResponseType: "promise";
* },
* {
* Method: "watchTime";
* Parameters?: undefined;
* ReturnType: number;
* ResponseType: "stream";
* }
* ]
* ```
*/
declare type RpcSchema = readonly RpcSchemaEntry[];
/**
* Generic shape of a single RPC schema entry
*
* Each entry defines a method with its parameters, return type, and response kind
*
* @typeParam TMethod - The method name (string literal)
* @typeParam TParams - The parameters type (can be undefined for no parameters)
* @typeParam TReturn - The return type
* @typeParam TResponseKind - Either "promise" or "stream"
*/
declare type RpcSchemaEntry<TMethod extends string = string, TParams = unknown, TReturn = unknown> = {
/**
* The method name (e.g., "frak_sendInteraction")
*/
Method: TMethod;
/**
* The parameters type (undefined if no parameters)
*/
Parameters?: TParams;
/**
* The return type
*/
ReturnType: TReturn;
};
export { }