@coderbuzz/ken 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +41 -0
- package/dist/bun-GUK26ACN.js +281 -0
- package/dist/bun-GUK26ACN.js.map +1 -0
- package/dist/chunk-2BOPD5H7.js +34 -0
- package/dist/chunk-2BOPD5H7.js.map +1 -0
- package/dist/chunk-2MK26YDD.js +269 -0
- package/dist/chunk-2MK26YDD.js.map +1 -0
- package/dist/chunk-DPU3PBLP.js +815 -0
- package/dist/chunk-DPU3PBLP.js.map +1 -0
- package/dist/chunk-WTV4URUZ.js +122 -0
- package/dist/chunk-WTV4URUZ.js.map +1 -0
- package/dist/deno-LZU5JBGL.js +250 -0
- package/dist/deno-LZU5JBGL.js.map +1 -0
- package/dist/index.d.ts +2783 -0
- package/dist/index.js +2728 -0
- package/dist/index.js.map +1 -0
- package/dist/node-JLUTIPEN.js +816 -0
- package/dist/node-JLUTIPEN.js.map +1 -0
- package/dist/package.json +13 -0
- package/dist/uws-VNY2LPIZ.js +622 -0
- package/dist/uws-VNY2LPIZ.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context/types.ts","../src/core/router.ts","../src/ws/types.ts","../src/runtime/compiler.ts","../src/utils/response.ts","../src/ws/pubsub.ts"],"sourcesContent":["/**\n * Ken Framework - Context Types\n * Shared type definitions for all Context implementations\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\n/**\n * Remote information including address and port.\n */\nexport interface RemoteInfo {\n address: string;\n port: number;\n}\n\n/**\n * Lazy function to get remote info from runtime\n * Used as fallback when proxy headers are missing\n */\nexport type GetRemoteInfo = () => RemoteInfo;\n\nexport type Validator = (val: any, ctx?: any) => any;\n\n/**\n * Flatten intersection types into a single object type for better IDE display.\n * Converts A & B & C into a single flat object with all properties.\n */\nexport type Flatten<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;\n\n/**\n * Type-level utilities to extract param names from route path patterns.\n */\ntype ExtractParams<Path extends string> =\n Path extends `${infer Segment}/${infer Rest}`\n ? ExtractParams<Segment> & ExtractParams<Rest>\n : Path extends `:${infer Param}?`\n ? { [K in Param]?: string }\n : Path extends `:${infer Param}`\n ? { [K in Param]: string }\n : Path extends `*`\n ? { \"*\": string; }\n : {};\n\nexport type ParamsFromPath<Path extends string> = Flatten<ExtractParams<Path>>;\n\n/**\n * Error handler function signature.\n * Receives the thrown error and request context, returns a Response.\n * Route-level onError takes priority over app-level.\n */\nexport type ErrorHandler = (error: unknown, ctx: Context<any, any>) => Response | Promise<Response>;\n\n/**\n * Route Schema definition for validation and type inference.\n * Flat architecture for best DX.\n */\nexport interface Schema {\n onError?: ErrorHandler;\n state?: StateMiddleware;\n params?: Record<string, Validator>;\n query?: Record<string, Validator>;\n headers?: Record<string, Validator>;\n cookies?: Record<string, Validator>;\n json?: Validator;\n text?: Validator;\n form?: Record<string, Validator>;\n}\n\n/**\n * Middleware handler function signature.\n * Used for side-effect middleware (logger, CORS, etc.) that don't produce state values.\n */\nexport type MiddlewareHandler = (ctx: Context<any, any>) => void | Response | Promise<void | Response>;\n\n/**\n * State Middleware definition and type inference.\n * Middleware functions that can return values (auth, user data) or void (guards, loggers).\n */\nexport type StateMiddleware = {\n [key: string]: (ctx: Context<any, any>) => any;\n};\n\n/**\n * Infer state shape from middleware definitions.\n * Excludes middleware with void return types for better IDE hover display.\n * Unwraps Promise types from async middleware.\n */\nexport type InferState<T extends StateMiddleware> = Flatten<{\n [K in keyof T as Awaited<ReturnType<T[K]>> extends void ? never : K]: Awaited<ReturnType<T[K]>>\n}>;\n\ntype ContainsUndefined<T> = [T] extends [Exclude<T, undefined>] ? false : true;\n\nexport type InferObject<T, Default> = T extends Record<string, Validator>\n ? Flatten<\n { [K in keyof T as ContainsUndefined<ReturnType<T[K]>> extends true ? K : never]?: ReturnType<T[K]> } &\n { [K in keyof T as ContainsUndefined<ReturnType<T[K]>> extends true ? never : K]: ReturnType<T[K]> }\n >\n : Default;\n\nexport type InferValidator<T, Default> = T extends Validator\n ? ReturnType<T>\n : Default;\n\n/**\n * Pre-allocated empty objects to avoid repeated allocations\n * These are reused for common patterns to reduce GC pressure\n */\nexport const EMPTY_PARAMS: Record<string, string> = Object.freeze({}) as Record<string, string>;\nexport const EMPTY_QUERY: Record<string, string> = Object.freeze({}) as Record<string, string>;\n\n/**\n * Context Interface - What route handlers see\n * All runtime-specific Context classes must implement this\n * \n * @template S - Schema type for validation\n * @template Path - Route path string for param extraction\n * @template TState - Accumulated state from middleware\n */\nexport interface Context<S extends Schema = {}, Path extends string = string, TState = {}> {\n /**\n * Request URL\n */\n readonly url: string;\n\n /**\n * HTTP method\n */\n readonly method: string;\n\n /**\n * Raw body stream (runtime-specific)\n */\n readonly body: any;\n\n /**\n * State from executed middleware. Only includes middleware with non-void returns.\n */\n state: Flatten<TState & (S extends { state: infer M extends StateMiddleware; }\n ? { [K in keyof M as Awaited<ReturnType<M[K]>> extends void ? never : K]: Exclude<Awaited<ReturnType<M[K]>>, Response> }\n : {})>;\n\n /**\n * Remote information (IP address and port)\n */\n readonly remoteInfo: RemoteInfo;\n\n /**\n * Route parameters (lazily parsed and validated)\n */\n readonly params: InferObject<S['params'], ParamsFromPath<Path>>;\n\n /**\n * Query parameters (lazily parsed and validated)\n */\n readonly query: InferObject<S['query'], Record<string, string>>;\n\n /**\n * Headers (lazily parsed and validated)\n */\n readonly headers: InferObject<S['headers'], Record<string, string>>;\n\n /**\n * Cookies (lazily parsed and validated)\n */\n readonly cookies: InferObject<S['cookies'], Record<string, string>>;\n\n /**\n * JSON body (lazily parsed and validated)\n */\n readonly json: Promise<InferValidator<S['json'], any>>;\n\n /**\n * Text body (lazily parsed and validated)\n */\n readonly text: Promise<InferValidator<S['text'], string>>;\n\n /**\n * Form body (lazily parsed and validated)\n */\n readonly form: Promise<InferObject<S['form'], FormData>>;\n\n /**\n * Register callback to be called after handler finishes (success or failure).\n * Useful for middleware that needs to perform cleanup or logging after response.\n */\n onFinish(callback: (resp?: Response) => void): void;\n\n /**\n * Set a cookie to be sent with the response.\n * Cookies are collected and applied to the response via onFinish callback.\n * \n * @param name - Cookie name\n * @param value - Cookie value\n * @param options - Cookie options (path, domain, maxAge, expires, httpOnly, secure, sameSite)\n */\n setCookie(\n name: string,\n value: string,\n options?: {\n path?: string;\n domain?: string;\n maxAge?: number;\n expires?: Date;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Strict' | 'Lax' | 'None';\n }\n ): void;\n\n /**\n * Internal: Execute all registered onFinish callbacks.\n * Called by framework after handler completes.\n */\n _executeFinishCallbacks(resp?: Response): void;\n}\n","/**\n * Ken Framework - Core Router\n * High-performance radix tree router with type-safe route matching\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\nimport { type Schema, EMPTY_PARAMS } from '../context/types';\n\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const;\nexport type HttpMethod = (typeof HTTP_METHODS)[number];\n\n/**\n * Handler function type - receives Context and returns any value\n * The return value is converted to Response by the runtime\n */\nexport type Handler = (ctx: any) => any;\n\n/**\n * Raw route registration entry\n * Stored during registration phase, compiled by runtime\n */\nexport interface RouteRegistration {\n method: string;\n path: string;\n schema?: Schema;\n handler?: Handler;\n staticValue?: unknown;\n}\n\n/**\n * Route info returned by getRoutes()\n */\nexport interface RouteInfo {\n method: string;\n path: string;\n}\n\n/**\n * Compiled route entry used by matcher\n */\ninterface RouteEntry {\n handler: (...args: any[]) => any;\n schema?: Schema;\n response?: Response;\n /** Pre-allocated MatchResult for static routes (avoids per-request allocation) */\n cachedResult?: MatchResult;\n}\n\n/**\n * Match result from router\n */\nexport interface MatchResult {\n handler: (...args: any[]) => any;\n schema?: Schema;\n params: Record<string, string>;\n response?: Response;\n}\n\n/**\n * Radix Tree node types for dynamic segments\n */\nexport const NodeType = {\n Param: 1,\n OptionalParam: 2,\n Wildcard: 3,\n} as const;\n\nexport type NodeTypeValue = (typeof NodeType)[keyof typeof NodeType];\n\n/**\n * Route state at a specific level in the tree\n */\ninterface RouteState {\n staticChildren: Map<string, RouteState>;\n dynamicChildren: TreeNode[];\n handlers: Map<string, RouteEntry>;\n}\n\nfunction createRouteState(): RouteState {\n return {\n staticChildren: new Map(),\n dynamicChildren: [],\n handlers: new Map(),\n };\n}\n\n/**\n * Tree node for dynamic segments\n */\nclass TreeNode {\n public type: NodeTypeValue;\n public paramName: string;\n public state: RouteState;\n\n constructor(type: NodeTypeValue, paramName: string) {\n this.type = type;\n this.paramName = paramName;\n this.state = createRouteState();\n }\n}\n\n/**\n * Recursive dynamic route search function\n */\nfunction searchDynamic(\n method: string,\n parts: string[],\n index: number,\n state: RouteState,\n params: Record<string, string>\n): RouteEntry | undefined {\n // End of path\n if (index === parts.length) {\n const entry = state.handlers.get(method);\n if (entry !== undefined) return entry;\n\n // Check for optional parameter at the end\n const dynamicChildren = state.dynamicChildren;\n for (let i = 0; i < dynamicChildren.length; i++) {\n const node = dynamicChildren[i];\n if (node.type === NodeType.OptionalParam) {\n return node.state.handlers.get(method);\n }\n }\n return undefined;\n }\n\n const part = parts[index];\n\n // 1. Try static match at this level\n const nextState = state.staticChildren.get(part);\n if (nextState !== undefined) {\n const result = searchDynamic(method, parts, index + 1, nextState, params);\n if (result !== undefined) return result;\n }\n\n // 2. Try dynamic matches (empty part = trailing slash marker, skip dynamic)\n if (part === '') return undefined;\n const dynamicChildren = state.dynamicChildren;\n const len = dynamicChildren.length;\n for (let i = 0; i < len; i++) {\n const node = dynamicChildren[i];\n const nodeType = node.type;\n\n if (nodeType === NodeType.Param || nodeType === NodeType.OptionalParam) {\n const paramName = node.paramName;\n params[paramName] = part;\n const result = searchDynamic(method, parts, index + 1, node.state, params);\n if (result !== undefined) return result;\n delete params[paramName]; // Backtrack\n } else if (nodeType === NodeType.Wildcard) {\n // Join remaining parts for wildcard\n let wildcardValue = parts[index];\n for (let j = index + 1; j < parts.length; j++) {\n wildcardValue += '/' + parts[j];\n }\n params['*'] = wildcardValue;\n return node.state.handlers.get(method);\n }\n }\n\n return undefined;\n}\n\n/**\n * Router - Core routing implementation\n * \n * Stores raw route registrations during registration phase.\n * Runtimes compile routes with their native executors.\n */\nexport class Router {\n // Static routes: path -> method -> entry\n private staticRoutes: Map<string, Map<string, RouteEntry>> = new Map();\n\n // Dynamic routes tree\n private dynamicRoot: RouteState = createRouteState();\n\n // Raw route registrations (for runtime compilation)\n public routes: RouteRegistration[] = [];\n\n /**\n * Register a compiled route (called by runtime during compilation)\n */\n registerCompiled(\n method: string,\n path: string,\n handler: (...args: any[]) => any,\n schema?: Schema,\n response?: Response\n ): void {\n const isDynamic = path.includes(':') || path.includes('*');\n\n if (!isDynamic) {\n let methodMap = this.staticRoutes.get(path);\n if (!methodMap) {\n methodMap = new Map();\n this.staticRoutes.set(path, methodMap);\n }\n // Pre-allocate MatchResult for zero-allocation static route matching\n const cachedResult: MatchResult = {\n handler,\n schema,\n params: EMPTY_PARAMS as Record<string, string>,\n response,\n };\n methodMap.set(method, { handler, schema, response, cachedResult });\n return;\n }\n\n this.insertDynamic(method, path, handler, schema, response);\n }\n\n private insertDynamic(\n method: string,\n path: string,\n handler: (...args: any[]) => any,\n schema?: Schema,\n response?: Response\n ): void {\n const parts = path === '/' ? [] : path.substring(1).split('/');\n let currentState = this.dynamicRoot;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n\n if (part.startsWith(':') || part === '*') {\n let type: NodeTypeValue;\n let paramName: string;\n\n if (part === '*') {\n type = NodeType.Wildcard;\n paramName = '*';\n } else if (part.endsWith('?')) {\n type = NodeType.OptionalParam;\n paramName = part.substring(1, part.length - 1);\n } else {\n type = NodeType.Param;\n paramName = part.substring(1);\n }\n\n let node = currentState.dynamicChildren.find(\n (c) => c.type === type && c.paramName === paramName\n );\n if (!node) {\n node = new TreeNode(type, paramName);\n currentState.dynamicChildren.push(node);\n currentState.dynamicChildren.sort((a, b) => a.type - b.type);\n }\n currentState = node.state;\n } else {\n // Static segment in dynamic route\n let nextState = currentState.staticChildren.get(part);\n if (!nextState) {\n nextState = createRouteState();\n currentState.staticChildren.set(part, nextState);\n }\n currentState = nextState;\n }\n }\n\n currentState.handlers.set(method, { handler, schema, response });\n }\n\n /**\n * Create matcher function for route lookup\n */\n matcher(): (method: string, pathname: string) => MatchResult | undefined {\n const staticRoutes = this.staticRoutes;\n const dynamicRoot = this.dynamicRoot;\n\n return (method: string, pathname: string) => {\n // 1. Fast path: Full static match (zero allocation via cachedResult)\n const staticMethodMap = staticRoutes.get(pathname);\n if (staticMethodMap !== undefined) {\n const entry = staticMethodMap.get(method);\n if (entry !== undefined) {\n return entry.cachedResult!;\n }\n }\n\n // 2. Dynamic path search\n const parts = pathname === '/' ? [] : pathname.substring(1).split('/');\n const params: Record<string, string> = {};\n const result = searchDynamic(method, parts, 0, dynamicRoot, params);\n\n if (result !== undefined) {\n return {\n handler: result.handler,\n schema: result.schema,\n params,\n response: result.response,\n };\n }\n\n return undefined;\n };\n }\n\n /**\n * Dynamic-only matcher (skips static routes)\n * Used when native routing handles static routes\n */\n dynamicOnlyMatcher(): (method: string, pathname: string) => MatchResult | undefined {\n const dynamicRoot = this.dynamicRoot;\n\n return (method: string, pathname: string) => {\n const parts = pathname === '/' ? [] : pathname.substring(1).split('/');\n const params: Record<string, string> = {};\n const result = searchDynamic(method, parts, 0, dynamicRoot, params);\n\n if (result !== undefined) {\n return {\n handler: result.handler,\n schema: result.schema,\n params,\n response: result.response,\n };\n }\n\n return undefined;\n };\n }\n\n /**\n * Get all registered routes as an array of { method, path } objects.\n *\n * @example\n * ```ts\n * const routes = app.getRoutes();\n * // [{ method: 'GET', path: '/' }, { method: 'POST', path: '/users' }, ...]\n * ```\n */\n getRoutes(): RouteInfo[] {\n return this.routes.map(r => ({ method: r.method, path: r.path }));\n }\n\n /**\n * Clear all compiled routes (for recompilation)\n */\n clear(): void {\n this.staticRoutes.clear();\n this.dynamicRoot = createRouteState();\n }\n}\n","/**\n * Ken Framework - WebSocket Types\n * Runtime-agnostic WebSocket abstractions\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\n/**\n * WebSocket message data type\n */\nexport type WsMessageData = string | ArrayBuffer | Uint8Array;\n\n/**\n * WebSocket ready states\n */\nexport const WsReadyState = {\n CONNECTING: 0,\n OPEN: 1,\n CLOSING: 2,\n CLOSED: 3,\n} as const;\n\nexport type WsReadyStateValue = (typeof WsReadyState)[keyof typeof WsReadyState];\n\n/**\n * WebSocket peer - represents a single connected client.\n * \n * Provides send/close/subscribe/unsubscribe/publish methods.\n * The `data` field carries user-defined per-connection state\n * set via the `upgrade` handler.\n * \n * @template T - User-defined per-connection data type\n */\nexport interface WsPeer<T = unknown> {\n /** User-defined per-connection data (set in upgrade handler) */\n readonly data: T;\n /** Remote address of the connected client */\n readonly remoteAddress: string;\n /** Current ready state of the connection */\n readonly readyState: WsReadyStateValue;\n\n /**\n * Send a message to this peer.\n * @param data - Message to send (string, ArrayBuffer, or Uint8Array)\n * @param compress - Whether to compress this message (default: false)\n * @returns Number of bytes sent, or -1 if buffered/failed\n */\n send(data: WsMessageData, compress?: boolean): number;\n\n /**\n * Close the connection.\n * @param code - Close status code (default: 1000)\n * @param reason - Close reason string\n */\n close(code?: number, reason?: string): void;\n\n /**\n * Subscribe this peer to a topic for pub/sub messaging.\n * @param topic - Topic name to subscribe to\n */\n subscribe(topic: string): void;\n\n /**\n * Unsubscribe this peer from a topic.\n * @param topic - Topic name to unsubscribe from\n */\n unsubscribe(topic: string): void;\n\n /**\n * Publish a message to all subscribers of a topic (excluding this peer).\n * @param topic - Topic to publish to\n * @param data - Message data to publish\n * @param compress - Whether to compress this message (default: false)\n */\n publish(topic: string, data: WsMessageData, compress?: boolean): void;\n\n /**\n * Check if this peer is subscribed to a topic.\n * @param topic - Topic name to check\n */\n isSubscribed(topic: string): boolean;\n\n /**\n * Send a ping frame to this peer.\n * @param data - Optional ping payload\n */\n ping(data?: WsMessageData): void;\n\n /**\n * Send a pong frame to this peer.\n * @param data - Optional pong payload\n */\n pong(data?: WsMessageData): void;\n}\n\n/**\n * WebSocket event handlers.\n * \n * @template T - User-defined per-connection data type\n * \n * @example\n * ```ts\n * app.ws<{ userId: string }>('/chat', {\n * upgrade(req) {\n * const token = req.headers.get('authorization');\n * return { userId: verifyToken(token) };\n * },\n * open(peer) {\n * peer.subscribe('chat');\n * peer.publish('chat', `${peer.data.userId} joined`);\n * },\n * message(peer, message) {\n * peer.publish('chat', `${peer.data.userId}: ${message}`);\n * },\n * close(peer, code, reason) {\n * peer.publish('chat', `${peer.data.userId} left`);\n * },\n * });\n * ```\n */\nexport interface WsHandler<T = unknown> {\n /**\n * Called during HTTP→WebSocket upgrade.\n * Return per-connection data, or a Response to reject the upgrade.\n * If not provided, upgrades are accepted with `undefined` data.\n */\n upgrade?: (req: Request) => T | Response | Promise<T | Response>;\n\n /**\n * Called when a connection is established.\n */\n open?: (peer: WsPeer<T>) => void | Promise<void>;\n\n /**\n * Called when a message is received from the client.\n */\n message: (peer: WsPeer<T>, message: WsMessageData) => void | Promise<void>;\n\n /**\n * Called when the connection is closed.\n */\n close?: (peer: WsPeer<T>, code: number, reason: string) => void | Promise<void>;\n\n /**\n * Called when a ping frame is received.\n */\n ping?: (peer: WsPeer<T>, data: WsMessageData) => void;\n\n /**\n * Called when a pong frame is received.\n */\n pong?: (peer: WsPeer<T>, data: WsMessageData) => void;\n\n /**\n * Called when an error occurs on the connection.\n */\n error?: (peer: WsPeer<T>, error: Error) => void;\n}\n\n/**\n * WebSocket configuration options.\n */\nexport interface WsOptions {\n /**\n * Maximum message size in bytes.\n * Messages exceeding this limit will close the connection.\n * @default 16_777_216 (16 MB)\n */\n maxPayloadLength?: number;\n\n /**\n * Maximum number of bytes that can be buffered for sending.\n * @default 16_777_216 (16 MB)\n */\n backpressureLimit?: number;\n\n /**\n * Interval in seconds between server-initiated ping frames.\n * Set to 0 to disable automatic pings.\n * @default 30\n */\n pingInterval?: number;\n\n /**\n * Timeout in seconds to wait for a pong response before closing.\n * @default 10\n */\n pongTimeout?: number;\n\n /**\n * Whether to enable per-message compression.\n * @default false\n */\n perMessageDeflate?: boolean;\n\n /**\n * Idle timeout in seconds. Connections idle for this long are closed.\n * Set to 0 to disable.\n * @default 120\n */\n idleTimeout?: number;\n}\n\n/**\n * Internal WebSocket route registration\n */\nexport interface WsRoute<T = unknown> {\n path: string;\n handler: WsHandler<T>;\n options?: WsOptions;\n}\n\n/**\n * Default WebSocket configuration values\n */\nexport const WS_DEFAULTS: Required<WsOptions> = {\n maxPayloadLength: 16_777_216,\n backpressureLimit: 16_777_216,\n pingInterval: 30,\n pongTimeout: 10,\n perMessageDeflate: false,\n idleTimeout: 120,\n} as const;\n","/**\n * Ken Framework - Runtime Compiler\n * Generic executor factory for all runtimes\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\nimport type { Context, Schema, GetRemoteInfo, ErrorHandler } from '../context/types';\n\n/**\n * User handler - ALWAYS receives typed Context\n * Same signature across ALL runtimes\n */\nexport type Handler<S extends Schema = any, Path extends string = string, TState = {}> =\n (ctx: Context<S, Path, TState>) => any | Promise<any>;\n\n/**\n * Context factory signature - runtime provides this\n * ALL runtimes pass getRemoteInfo as fallback for IP detection\n */\nexport type ContextFactory<TRequest, TContext extends Context> = (\n request: TRequest,\n params: Record<string, string>,\n getRemoteInfo: GetRemoteInfo,\n schema?: Schema,\n ...extraArgs: any[]\n) => TContext;\n\n/**\n * Check if handler or any middleware is async\n */\nfunction isAsyncHandler(handler: Handler, schema?: Schema): boolean {\n if (handler.constructor.name === 'AsyncFunction') {\n return true;\n }\n\n if (schema?.state) {\n for (const key in schema.state) {\n if (schema.state[key].constructor.name === 'AsyncFunction') {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/** Pre-allocated JSON content-type header for error responses */\nconst JSON_ERROR_HEADERS: HeadersInit = { 'content-type': 'application/json' };\nconst INTERNAL_SERVER_ERROR = 'Internal Server Error';\n\n/**\n * Default error handler - returns standard JSON error response.\n * Response errors are passed through directly.\n *\n * Error response format:\n * ```json\n * { \"status\": 500, \"message\": \"Internal Server Error\" }\n * ```\n */\nexport function defaultErrorHandler(err: unknown): Response {\n if (err instanceof Response) return err;\n\n const message = err instanceof Error ? err.message : INTERNAL_SERVER_ERROR;\n return new Response(\n JSON.stringify({ status: 500, message }),\n { status: 500, headers: JSON_ERROR_HEADERS }\n );\n}\n\n/**\n * Handle errors with custom or default error handler.\n * Wraps custom handler in try-catch to prevent error handler failures from crashing.\n */\nfunction handleError(err: unknown, ctx: Context, onError?: ErrorHandler): Response | Promise<Response> {\n if (onError) {\n try {\n const result = onError(err, ctx);\n if (result && typeof (result as any).then === 'function') {\n return (result as Promise<Response>).then((resp) => {\n ctx._executeFinishCallbacks(resp);\n return resp;\n }).catch((innerErr: unknown) => {\n const resp = defaultErrorHandler(innerErr);\n ctx._executeFinishCallbacks(resp);\n return resp;\n });\n }\n ctx._executeFinishCallbacks(result as Response);\n return result as Response;\n } catch (innerErr) {\n const resp = defaultErrorHandler(innerErr);\n ctx._executeFinishCallbacks(resp);\n return resp;\n }\n }\n const resp = defaultErrorHandler(err);\n ctx._executeFinishCallbacks(resp);\n return resp;\n}\n\n/**\n * Create synchronous executor with middleware\n */\nfunction createSyncExecutorWithMiddleware<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema: Schema\n) {\n const state = schema.state!;\n const onError = schema.onError;\n\n return (request: TRequest, params: Record<string, string>, getRemoteInfo: GetRemoteInfo, ...args: any[]) => {\n const ctx = createContext(request, params, getRemoteInfo, schema, ...args);\n\n try {\n // Execute state middleware synchronously\n const stateObj = ctx.state as Record<string, any>;\n for (const key in state) {\n const result = state[key](ctx);\n if (result !== undefined) {\n if (result instanceof Response) {\n ctx._executeFinishCallbacks(result);\n return result;\n }\n stateObj[key] = result;\n }\n }\n\n // Execute handler\n const response = handler(ctx);\n ctx._executeFinishCallbacks(response);\n return response;\n\n } catch (err: any) {\n return handleError(err, ctx, onError);\n }\n };\n}\n\n/**\n * Create synchronous executor without middleware\n */\nfunction createSyncExecutorNoMiddleware<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema?: Schema\n) {\n const onError = schema?.onError;\n\n return (request: TRequest, params: Record<string, string>, getRemoteInfo: GetRemoteInfo, ...args: any[]) => {\n const ctx = createContext(request, params, getRemoteInfo, schema, ...args);\n\n try {\n const response = handler(ctx);\n ctx._executeFinishCallbacks(response);\n return response;\n } catch (err: any) {\n return handleError(err, ctx, onError);\n }\n };\n}\n\n/**\n * Create asynchronous executor with middleware\n */\nfunction createAsyncExecutorWithMiddleware<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema: Schema\n) {\n const state = schema.state!;\n const onError = schema.onError;\n\n return async (request: TRequest, params: Record<string, string>, getRemoteInfo: GetRemoteInfo, ...args: any[]) => {\n const ctx = createContext(request, params, getRemoteInfo, schema, ...args);\n\n try {\n // Execute state middleware (await any Promises)\n const stateObj = ctx.state as Record<string, any>;\n for (const key in state) {\n const result = await state[key](ctx);\n if (result !== undefined) {\n if (result instanceof Response) {\n ctx._executeFinishCallbacks(result);\n return result;\n }\n stateObj[key] = result;\n }\n }\n\n // Execute handler\n const response = await handler(ctx);\n ctx._executeFinishCallbacks(response);\n return response;\n\n } catch (err: any) {\n return handleError(err, ctx, onError);\n }\n };\n}\n\n/**\n * Create asynchronous executor without middleware\n */\nfunction createAsyncExecutorNoMiddleware<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema?: Schema\n) {\n const onError = schema?.onError;\n\n return async (request: TRequest, params: Record<string, string>, getRemoteInfo: GetRemoteInfo, ...args: any[]) => {\n const ctx = createContext(request, params, getRemoteInfo, schema, ...args);\n\n try {\n const response = await handler(ctx);\n ctx._executeFinishCallbacks(response);\n return response;\n } catch (err: any) {\n return handleError(err, ctx, onError);\n }\n };\n}\n\n/**\n * Create executor for any runtime\n * Context factory is the only runtime-specific part\n * \n * @param createContext - Factory function to create runtime-specific Context\n * @param handler - User's route handler\n * @param schema - Optional route schema with state middleware\n */\nexport function createExecutor<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema?: Schema\n): (request: TRequest, params: Record<string, string>, getRemoteInfo: GetRemoteInfo, ...args: any[]) => any {\n\n const hasMiddleware = schema?.state && Object.keys(schema.state).length > 0;\n const isAsync = isAsyncHandler(handler, schema);\n\n if (isAsync) {\n return hasMiddleware\n ? createAsyncExecutorWithMiddleware(createContext, handler, schema!)\n : createAsyncExecutorNoMiddleware(createContext, handler, schema);\n }\n\n return hasMiddleware\n ? createSyncExecutorWithMiddleware(createContext, handler, schema!)\n : createSyncExecutorNoMiddleware(createContext, handler, schema);\n}\n\n/**\n * Create a sync executor (forces sync execution, no async detection)\n */\nexport function createSyncExecutor<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema?: Schema\n) {\n const hasMiddleware = schema?.state && Object.keys(schema.state).length > 0;\n\n return hasMiddleware\n ? createSyncExecutorWithMiddleware(createContext, handler, schema!)\n : createSyncExecutorNoMiddleware(createContext, handler, schema);\n}\n\n/**\n * Create an async executor (forces async execution)\n */\nexport function createAsyncExecutor<TRequest, TContext extends Context>(\n createContext: ContextFactory<TRequest, TContext>,\n handler: Handler,\n schema?: Schema\n) {\n const hasMiddleware = schema?.state && Object.keys(schema.state).length > 0;\n\n return hasMiddleware\n ? createAsyncExecutorWithMiddleware(createContext, handler, schema!)\n : createAsyncExecutorNoMiddleware(createContext, handler, schema);\n}\n\n/**\n * Create a 404 Not Found executor with nested prefix-scoped handler support.\n * Supports custom notFound handlers configured at any app level via .notFound().\n * When no route matches, selects the most specific handler by longest prefix match.\n * \n * Priority order:\n * 1. Custom handlers from _notFoundEntries (child apps via .use()/.define())\n * 2. App's own _notFoundHandler (global fallback)\n * 3. Global middleware + default 404 response (original behavior)\n * \n * @param router - Router instance (App extends Router with notFound data)\n * @param createContext - Factory function to create runtime-specific Context\n * @returns Executor function for 404 responses, or null if no handlers/middleware\n */\nexport function createNotFoundExecutor<TRequest, TContext extends Context>(\n router: any,\n createContext: ContextFactory<TRequest, TContext>\n): ((request: TRequest, getRemoteInfo: GetRemoteInfo, pathname: string, ...args: any[]) => any) | null {\n\n // Collect custom notFound entries\n const entries: Array<{ prefix: string; handler: any; schema?: Schema; }> = [];\n\n // Add app's own handler first (highest priority at same prefix level)\n if (router._notFoundHandler) {\n entries.push({ prefix: '', handler: router._notFoundHandler });\n }\n\n // Add accumulated entries from child apps\n if (router._notFoundEntries?.length > 0) {\n entries.push(...router._notFoundEntries);\n }\n\n // Custom handlers exist — build prefix-matching executor\n if (entries.length > 0) {\n return buildPrefixNotFoundExecutor(router, createContext, entries);\n }\n\n // No custom handlers — check for global middleware (preserve original behavior)\n if (typeof router.matchMiddleware !== 'function' || typeof router.mergeSchemas !== 'function') {\n return null;\n }\n\n // Collect all middleware matching '/*' pattern\n const globalMiddleware = router.matchMiddleware('/*');\n const schema = router.mergeSchemas(globalMiddleware, undefined);\n\n // No global middleware - return null (runtime will use static 404 response)\n if (!schema || !schema.state || Object.keys(schema.state).length === 0) {\n return null;\n }\n\n // Create 404 handler\n const notFoundHandler = () => new Response('Not Found', { status: 404 });\n\n // Create executor with global middleware\n const executor = createExecutor(createContext, notFoundHandler, schema);\n\n // Return wrapper that provides empty params (pathname consumed but not forwarded)\n return (request: TRequest, getRemoteInfo: GetRemoteInfo, _pathname: string, ...args: any[]) => {\n return executor(request, {} as Record<string, string>, getRemoteInfo, ...args);\n };\n}\n\n/**\n * Build a prefix-matching not-found executor from multiple entries.\n * Entries are sorted by prefix length (most specific first).\n * At request time, the first matching prefix wins.\n */\nfunction buildPrefixNotFoundExecutor<TRequest, TContext extends Context>(\n router: any,\n createContext: ContextFactory<TRequest, TContext>,\n entries: Array<{ prefix: string; handler: any; schema?: Schema; }>\n): (request: TRequest, getRemoteInfo: GetRemoteInfo, pathname: string, ...args: any[]) => any {\n\n const hasMatchMiddleware = typeof router.matchMiddleware === 'function' && typeof router.mergeSchemas === 'function';\n\n // Compile each entry into an executor\n const compiledEntries: Array<{\n prefix: string;\n executor: (request: TRequest, params: Record<string, string>, getRemoteInfo: GetRemoteInfo, ...args: any[]) => any;\n }> = [];\n\n for (const entry of entries) {\n let schema = entry.schema;\n\n // Resolve middleware for entries without pre-merged state (from .use())\n if ((!schema || !schema.state) && hasMatchMiddleware) {\n const matched = router.matchMiddleware(entry.prefix || '/*');\n schema = router.mergeSchemas(matched, schema);\n }\n\n const executor = createExecutor(createContext, entry.handler, schema);\n compiledEntries.push({ prefix: entry.prefix, executor });\n }\n\n // Ensure there's always a global fallback\n const hasGlobalHandler = compiledEntries.some(e => e.prefix === '');\n if (!hasGlobalHandler) {\n const defaultHandler = () => new Response('Not Found', { status: 404 });\n let fallbackSchema: Schema | undefined;\n if (hasMatchMiddleware) {\n const globalMiddleware = router.matchMiddleware('/*');\n fallbackSchema = router.mergeSchemas(globalMiddleware, undefined);\n }\n const fallbackExecutor = createExecutor(createContext, defaultHandler, fallbackSchema);\n compiledEntries.push({ prefix: '', executor: fallbackExecutor });\n }\n\n // Sort by prefix length descending (most specific first)\n compiledEntries.sort((a, b) => b.prefix.length - a.prefix.length);\n\n const emptyParams = {} as Record<string, string>;\n\n // Optimization: single global handler (no prefix matching needed)\n if (compiledEntries.length === 1 && compiledEntries[0].prefix === '') {\n const singleExecutor = compiledEntries[0].executor;\n return (request: TRequest, getRemoteInfo: GetRemoteInfo, _pathname: string, ...args: any[]) => {\n return singleExecutor(request, emptyParams, getRemoteInfo, ...args);\n };\n }\n\n // Multiple handlers — prefix matching at request time\n const len = compiledEntries.length;\n return (request: TRequest, getRemoteInfo: GetRemoteInfo, pathname: string, ...args: any[]) => {\n for (let i = 0; i < len; i++) {\n const entry = compiledEntries[i];\n if (entry.prefix === '' || pathname === entry.prefix || pathname.startsWith(entry.prefix + '/')) {\n return entry.executor(request, emptyParams, getRemoteInfo, ...args);\n }\n }\n // Should not reach here (global fallback always matches)\n return new Response('Not Found', { status: 404 });\n };\n}\n","/**\n * Ken Framework - Response Utilities\n * Convert any handler return value to Response\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\n// Pre-allocated responses for common cases\nconst NO_CONTENT_RESPONSE = new Response(null, { status: 204 });\n\n/**\n * Convert any handler return value to Response\n * Used by all runtimes AFTER handler execution\n */\nexport function toResponse(value: any): Response {\n // Fast path: already a Response\n if (value instanceof Response) {\n return value;\n }\n\n // Null/undefined → 204 No Content\n if (value === null || value === undefined) {\n return NO_CONTENT_RESPONSE.clone();\n }\n\n // Object → JSON\n if (typeof value === 'object') {\n // return new Response(JSON.stringify(value), {\n // status: 200,\n // headers: { 'Content-Type': 'application/json' }\n // });\n // return Response.json(value);\n\n const textValue = JSON.stringify(value);\n return new Response(textValue, {\n status: 200,\n headers: {\n 'content-type': 'application/json',\n // 'content-length': Buffer.byteLength(textValue).toString()\n }\n });\n }\n\n // Primitives → Text\n const textValue = String(value);\n return new Response(textValue, {\n status: 200,\n headers: {\n 'content-type': 'text/plain',\n // 'content-length': Buffer.byteLength(textValue).toString()\n }\n });\n\n // return new Response(String(value));\n}\n\n/**\n * Create a cached Response for static routes\n * Returns a clone function to avoid body consumption issues\n */\nexport function createCachedResponse(value: any): Response {\n return toResponse(value);\n}\n","/**\n * Ken Framework - WebSocket Pub/Sub\n *\n * Two pub/sub abstractions:\n *\n * 1. PubSubHub — low-level exclude-sender pub/sub.\n * Used by Deno/Node peer adapters for WsPeer.subscribe / WsPeer.publish.\n * On Bun and uWS, native pub/sub is used directly by the peer adapters.\n *\n * 2. WsTopicHub — MQTT/Redis-style per-topic message routing.\n * Per-topic callbacks, include-all (broadcast) publish, dead peer detection.\n * Works uniformly across all runtimes.\n *\n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\nimport { type WsPeer, type WsMessageData, WsReadyState, type WsReadyStateValue } from './types';\n\n/**\n * Minimal peer interface for pub/sub operations.\n * Runtime adapters implement this.\n * Extended with readyState and close() to support dead peer detection.\n */\nexport interface PubSubPeer {\n /** Send data directly to this peer */\n send(data: WsMessageData, compress?: boolean): number;\n /** Current ready state of the connection */\n readonly readyState: WsReadyStateValue;\n /** Close the connection */\n close(code?: number, reason?: string): void;\n}\n\n/**\n * Topic-based pub/sub hub.\n * Manages subscriptions and message broadcast.\n */\nexport class PubSubHub {\n /** topic -> set of subscribers */\n private topics: Map<string, Set<PubSubPeer>> = new Map();\n /** peer -> set of subscribed topics (for fast cleanup) */\n private peerTopics: Map<PubSubPeer, Set<string>> = new Map();\n /** peer -> last-alive timestamp (ms) — for dead peer detection */\n private _lastPong: Map<PubSubPeer, number> = new Map();\n\n /**\n * Subscribe a peer to a topic.\n */\n subscribe(peer: PubSubPeer, topic: string): void {\n let topicSet = this.topics.get(topic);\n if (!topicSet) {\n topicSet = new Set();\n this.topics.set(topic, topicSet);\n }\n topicSet.add(peer);\n\n let peerSet = this.peerTopics.get(peer);\n if (!peerSet) {\n peerSet = new Set();\n this.peerTopics.set(peer, peerSet);\n }\n peerSet.add(topic);\n\n // Initialize liveness tracking on first subscription\n if (!this._lastPong.has(peer)) {\n this._lastPong.set(peer, Date.now());\n }\n }\n\n /**\n * Unsubscribe a peer from a topic.\n */\n unsubscribe(peer: PubSubPeer, topic: string): void {\n const topicSet = this.topics.get(topic);\n if (topicSet) {\n topicSet.delete(peer);\n if (topicSet.size === 0) {\n this.topics.delete(topic);\n }\n }\n\n const peerSet = this.peerTopics.get(peer);\n if (peerSet) {\n peerSet.delete(topic);\n if (peerSet.size === 0) {\n this.peerTopics.delete(peer);\n this._lastPong.delete(peer);\n }\n }\n }\n\n /**\n * Remove a peer from all topics.\n * Called on connection close.\n */\n removeAll(peer: PubSubPeer): void {\n const peerSet = this.peerTopics.get(peer);\n if (!peerSet) return;\n\n for (const topic of peerSet) {\n const topicSet = this.topics.get(topic);\n if (topicSet) {\n topicSet.delete(peer);\n if (topicSet.size === 0) {\n this.topics.delete(topic);\n }\n }\n }\n\n this.peerTopics.delete(peer);\n this._lastPong.delete(peer);\n }\n\n /**\n * Check if a peer is subscribed to a topic.\n */\n isSubscribed(peer: PubSubPeer, topic: string): boolean {\n const topicSet = this.topics.get(topic);\n return topicSet ? topicSet.has(peer) : false;\n }\n\n /**\n * Publish a message to all subscribers of a topic, excluding the sender.\n */\n publish(sender: PubSubPeer, topic: string, data: WsMessageData, compress?: boolean): void {\n const topicSet = this.topics.get(topic);\n if (!topicSet) return;\n\n for (const peer of topicSet) {\n if (peer !== sender) {\n peer.send(data, compress);\n }\n }\n }\n\n /**\n * Broadcast a message to ALL subscribers of a topic (including sender).\n * Used internally by the server for ping broadcasts, etc.\n */\n broadcast(topic: string, data: WsMessageData, compress?: boolean): void {\n const topicSet = this.topics.get(topic);\n if (!topicSet) return;\n\n for (const peer of topicSet) {\n peer.send(data, compress);\n }\n }\n\n /**\n * Get the number of subscribers for a topic.\n */\n subscriberCount(topic: string): number {\n return this.topics.get(topic)?.size ?? 0;\n }\n\n /**\n * Get all topics a peer is subscribed to.\n */\n getTopics(peer: PubSubPeer): ReadonlySet<string> {\n return this.peerTopics.get(peer) ?? new Set();\n }\n\n // ── Dead peer detection ─────────────────────────────────────────────────\n\n /**\n * Mark a peer as alive.\n *\n * Call from `WsHandler.pong()` on runtimes that support protocol ping/pong\n * (Bun, Node.js, uWS). On Deno, call from `WsHandler.message()` since\n * protocol pong is unavailable and any inbound message signals liveness.\n *\n * No-op if the peer is not subscribed to any topic.\n */\n markAlive(peer: PubSubPeer): void {\n if (this._lastPong.has(peer)) {\n this._lastPong.set(peer, Date.now());\n }\n }\n\n /**\n * Returns true if the peer was last marked alive within `timeoutMs` ms.\n */\n isPeerAlive(peer: PubSubPeer, timeoutMs: number): boolean {\n const last = this._lastPong.get(peer);\n return last !== undefined && (Date.now() - last) < timeoutMs;\n }\n\n /**\n * Close and evict all peers that are either:\n * - Already disconnected (`readyState !== OPEN`), or\n * - Silent for longer than `timeoutMs` milliseconds.\n *\n * Collects dead peers before mutating state to avoid iterator interference.\n * Called by the runtime adapters' `startHeartbeat()` loop, which owns the\n * timer lifecycle — PubSubHub does not manage its own interval timer.\n */\n pruneDeadPeers(timeoutMs: number): void {\n const now = Date.now();\n // Collect first — removeAll mutates _lastPong\n const dead: PubSubPeer[] = [];\n for (const [peer, lastPong] of this._lastPong) {\n if (peer.readyState !== WsReadyState.OPEN || (now - lastPong) >= timeoutMs) {\n dead.push(peer);\n }\n }\n for (const peer of dead) {\n if (peer.readyState === WsReadyState.OPEN) {\n try { peer.close(1001, 'ping timeout'); } catch { /* already closing */ }\n }\n this.removeAll(peer);\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// WsTopicHub\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Per-topic message callback.\n * Invoked when hub.dispatch(peer, message) is called for a subscribed peer.\n *\n * @template T - Per-connection data type\n */\nexport type TopicCallback<T = unknown> = (\n peer: WsPeer<T>,\n message: WsMessageData,\n) => void | Promise<void>;\n\n/**\n * WsTopicHub — MQTT/Redis-style topic-based pub/sub for WebSocket servers.\n *\n * **Key differences from WsPeer.subscribe/publish:**\n * - `hub.subscribe(peer, topic, callback)` registers a per-topic message handler\n * - `hub.publish(topic, data)` broadcasts to ALL subscribers (including sender)\n * - `hub.dispatch(peer, msg)` routes a message to the peer's topic callbacks\n * - Dead peer detection via `markAlive()` + `startDeadPeerCheck()`\n * - Works uniformly across Bun, Node, Deno, and uWS\n *\n * Uses native runtime pub/sub (Bun/uWS `peer.subscribe/publish`) for \"exclude self\"\n * semantics when callers use `peer.publish()` directly. Hub publish() always uses\n * an in-memory iterator for include-all broadcast.\n *\n * @template T - Per-connection data type\n *\n * @example\n * ```ts\n * const hub = new WsTopicHub<{ username: string }>();\n *\n * app.ws<{ username: string }>('/chat', {\n * upgrade(req) {\n * const name = new URL(req.url).searchParams.get('name') ?? 'anon';\n * return { username: name };\n * },\n * open(peer) {\n * hub.subscribe(peer, 'chat', (peer, msg) => {\n * hub.publish('chat', `${peer.data.username}: ${msg}`);\n * });\n * },\n * message(peer, msg) {\n * hub.dispatch(peer, msg); // routes to topic callbacks\n * },\n * close(peer) {\n * hub.leave(peer); // cleanup all subscriptions\n * },\n * pong(peer) {\n * hub.markAlive(peer); // dead peer detection (Bun/Node/uWS)\n * },\n * }, { pingInterval: 30 });\n *\n * // Server-initiated broadcast (from any route or CRON)\n * hub.publish('chat', 'System: restarting in 60s');\n *\n * // Start automatic dead peer cleanup\n * hub.startDeadPeerCheck(5_000, 120_000);\n * ```\n */\nexport class WsTopicHub<T = unknown> {\n /** topic → Set<peer> — O(1) publish iteration */\n private _topics: Map<string, Set<WsPeer<T>>> = new Map();\n /** peer → Map<topic, callback> — O(1) dispatch and cleanup */\n private _peerTopics: Map<WsPeer<T>, Map<string, TopicCallback<T>>> = new Map();\n /** peer → last-pong timestamp (ms) — for dead peer detection */\n private _lastPong: Map<WsPeer<T>, number> = new Map();\n\n private _deadPeerTimer: ReturnType<typeof setInterval> | null = null;\n\n // ── Subscription management ───────────────────────────────────────────────\n\n /**\n * Subscribe a peer to a topic with a per-message callback.\n *\n * The callback is invoked whenever `hub.dispatch(peer, msg)` is called while\n * the peer is subscribed to this topic.\n *\n * A peer may be subscribed to multiple topics simultaneously, each with its\n * own callback. Subsequent calls with the same topic replace the handler.\n *\n * @param peer - The connected WebSocket peer\n * @param topic - Topic name (e.g. 'chat', 'notifications', 'room/42')\n * @param handler - Called with (peer, message) on each dispatched message\n */\n subscribe(peer: WsPeer<T>, topic: string, handler: TopicCallback<T>): void {\n // topic → peer\n let topicSet = this._topics.get(topic);\n if (!topicSet) {\n topicSet = new Set();\n this._topics.set(topic, topicSet);\n }\n topicSet.add(peer);\n\n // peer → topic handler\n let peerMap = this._peerTopics.get(peer);\n if (!peerMap) {\n peerMap = new Map();\n this._peerTopics.set(peer, peerMap);\n }\n peerMap.set(topic, handler);\n\n // Initialize liveness on first subscription\n if (!this._lastPong.has(peer)) {\n this._lastPong.set(peer, Date.now());\n }\n }\n\n /**\n * Unsubscribe a peer from a specific topic.\n * The peer remains subscribed to any other topics.\n */\n unsubscribe(peer: WsPeer<T>, topic: string): void {\n const topicSet = this._topics.get(topic);\n if (topicSet) {\n topicSet.delete(peer);\n if (topicSet.size === 0) this._topics.delete(topic);\n }\n\n const peerMap = this._peerTopics.get(peer);\n if (peerMap) {\n peerMap.delete(topic);\n if (peerMap.size === 0) {\n this._peerTopics.delete(peer);\n this._lastPong.delete(peer);\n }\n }\n }\n\n /**\n * Remove a peer from ALL topics and clean up liveness state.\n * Call this from `WsHandler.close()` to prevent memory leaks.\n */\n leave(peer: WsPeer<T>): void {\n const peerMap = this._peerTopics.get(peer);\n if (!peerMap) return;\n\n for (const topic of peerMap.keys()) {\n const topicSet = this._topics.get(topic);\n if (topicSet) {\n topicSet.delete(peer);\n if (topicSet.size === 0) this._topics.delete(topic);\n }\n }\n\n this._peerTopics.delete(peer);\n this._lastPong.delete(peer);\n }\n\n // ── Message routing ───────────────────────────────────────────────────────\n\n /**\n * Route an incoming message from a peer to all its registered topic handlers.\n * Call this from `WsHandler.message()`.\n *\n * If the peer is subscribed to multiple topics, the message is dispatched\n * to each topic's callback in insertion order.\n */\n dispatch(peer: WsPeer<T>, msg: WsMessageData): void {\n const peerMap = this._peerTopics.get(peer);\n if (!peerMap) return;\n\n for (const handler of peerMap.values()) {\n try {\n handler(peer, msg);\n } catch { /* isolate per-handler errors from each other */ }\n }\n }\n\n // ── Broadcasting ──────────────────────────────────────────────────────────\n\n /**\n * Broadcast a message to ALL subscribers of a topic, including the sender.\n *\n * For \"exclude self\" semantics (the sender does not receive their own message),\n * use `peer.publish(topic, data)` instead — which on Bun and uWS routes through\n * the native C++ pub/sub engine.\n */\n publish(topic: string, data: WsMessageData, compress?: boolean): void {\n const topicSet = this._topics.get(topic);\n if (!topicSet) return;\n\n for (const peer of topicSet) {\n peer.send(data, compress);\n }\n }\n\n // ── Dead peer detection ───────────────────────────────────────────────────\n\n /**\n * Mark a peer as alive. Call from `WsHandler.pong()`.\n *\n * Works with native runtime protocol ping/pong (Bun, Node, uWS).\n * On Deno, calling this has no effect as `handler.pong` is never triggered.\n */\n markAlive(peer: WsPeer<T>): void {\n if (this._lastPong.has(peer)) {\n this._lastPong.set(peer, Date.now());\n }\n }\n\n /**\n * Returns true if the peer last responded within `timeoutMs` milliseconds.\n */\n isPeerAlive(peer: WsPeer<T>, timeoutMs: number): boolean {\n const last = this._lastPong.get(peer);\n return last !== undefined && (Date.now() - last) < timeoutMs;\n }\n\n /**\n * Close and remove all peers that have not called `markAlive()` within `timeoutMs`.\n * Recommended value: `(pingInterval + pongTimeout) * 1000`.\n */\n pruneDeadPeers(timeoutMs: number): void {\n const now = Date.now();\n for (const [peer, lastPong] of this._lastPong) {\n if ((now - lastPong) >= timeoutMs) {\n try { peer.close(1001, 'ping timeout'); } catch { /* peer may already be closed */ }\n this.leave(peer);\n }\n }\n }\n\n /**\n * Start automatic dead peer detection at a fixed interval.\n *\n * @param checkIntervalMs - How often to scan for dead peers (ms)\n * @param timeoutMs - Peers unseen longer than this are closed and removed\n *\n * @example\n * ```ts\n * // Check every 5 seconds; close peers silent for 2 minutes\n * hub.startDeadPeerCheck(5_000, 120_000);\n *\n * // Integrate with WsOptions:\n * // { pingInterval: 30, pongTimeout: 10 }\n * // → hub.startDeadPeerCheck(5_000, (30 + 10) * 1000);\n * ```\n */\n startDeadPeerCheck(checkIntervalMs: number, timeoutMs: number): void {\n this.stopDeadPeerCheck();\n this._deadPeerTimer = setInterval(() => {\n this.pruneDeadPeers(timeoutMs);\n }, checkIntervalMs);\n }\n\n /**\n * Stop the automatic dead peer check started by `startDeadPeerCheck()`.\n */\n stopDeadPeerCheck(): void {\n if (this._deadPeerTimer !== null) {\n clearInterval(this._deadPeerTimer);\n this._deadPeerTimer = null;\n }\n }\n\n // ── Introspection ─────────────────────────────────────────────────────────\n\n /** Number of peers currently subscribed to a topic. */\n subscriberCount(topic: string): number {\n return this._topics.get(topic)?.size ?? 0;\n }\n\n /** Returns true if a peer is subscribed to the given topic. */\n isSubscribed(peer: WsPeer<T>, topic: string): boolean {\n return this._topics.get(topic)?.has(peer) ?? false;\n }\n\n /** Returns an array of all active topic names. */\n topicNames(): string[] {\n return [...this._topics.keys()];\n }\n}\n"],"mappings":"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","names":["dynamicChildren","resp","textValue"]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseContext
|
|
3
|
+
} from "./chunk-2MK26YDD.js";
|
|
4
|
+
import {
|
|
5
|
+
EMPTY_PARAMS
|
|
6
|
+
} from "./chunk-DPU3PBLP.js";
|
|
7
|
+
|
|
8
|
+
// src/context/web.ts
|
|
9
|
+
var WebContext = class extends BaseContext {
|
|
10
|
+
req;
|
|
11
|
+
constructor(req, params = EMPTY_PARAMS, getRemoteInfo, schema) {
|
|
12
|
+
super(params, getRemoteInfo, schema);
|
|
13
|
+
this.req = req;
|
|
14
|
+
}
|
|
15
|
+
// Implement abstract methods
|
|
16
|
+
get url() {
|
|
17
|
+
return this.req.url;
|
|
18
|
+
}
|
|
19
|
+
get method() {
|
|
20
|
+
return this.req.method;
|
|
21
|
+
}
|
|
22
|
+
get body() {
|
|
23
|
+
return this.req.body;
|
|
24
|
+
}
|
|
25
|
+
_getRawHeaders() {
|
|
26
|
+
return this.req.headers;
|
|
27
|
+
}
|
|
28
|
+
_getRawUrl() {
|
|
29
|
+
return this.req.url;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Lazily parses and validates headers.
|
|
33
|
+
*/
|
|
34
|
+
get headers() {
|
|
35
|
+
let cached = this._headers;
|
|
36
|
+
if (cached !== null) return cached;
|
|
37
|
+
const schema = this._schema?.headers;
|
|
38
|
+
if (schema !== void 0) {
|
|
39
|
+
const headers = this.req.headers;
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const key in schema) {
|
|
42
|
+
const validator = schema[key];
|
|
43
|
+
result[key] = validator(headers.get(key) || "");
|
|
44
|
+
}
|
|
45
|
+
this._headers = result;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
const h = {};
|
|
49
|
+
this.req.headers.forEach((v, k) => h[k] = v);
|
|
50
|
+
this._headers = h;
|
|
51
|
+
return h;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Lazily parses and validates JSON body.
|
|
55
|
+
*/
|
|
56
|
+
get json() {
|
|
57
|
+
if (this._json) return this._json;
|
|
58
|
+
const validator = this._schema?.json;
|
|
59
|
+
if (validator) {
|
|
60
|
+
this._json = (async () => {
|
|
61
|
+
const data = await this.req.json();
|
|
62
|
+
try {
|
|
63
|
+
return validator(data);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new Error(`JSON Body validation failed: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
} else {
|
|
69
|
+
this._json = this.req.json().catch(() => null);
|
|
70
|
+
}
|
|
71
|
+
return this._json;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lazily parses and validates text body.
|
|
75
|
+
*/
|
|
76
|
+
get text() {
|
|
77
|
+
if (this._text) return this._text;
|
|
78
|
+
const validator = this._schema?.text;
|
|
79
|
+
if (validator) {
|
|
80
|
+
this._text = (async () => {
|
|
81
|
+
const data = await this.req.text();
|
|
82
|
+
try {
|
|
83
|
+
return validator(data);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new Error(`Text Body validation failed: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
} else {
|
|
89
|
+
this._text = this.req.text();
|
|
90
|
+
}
|
|
91
|
+
return this._text;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Lazily parses and validates form body.
|
|
95
|
+
*/
|
|
96
|
+
get form() {
|
|
97
|
+
if (this._form) return this._form;
|
|
98
|
+
const schema = this._schema?.form;
|
|
99
|
+
if (schema) {
|
|
100
|
+
this._form = (async () => {
|
|
101
|
+
const formData = await this.req.formData();
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const key in schema) {
|
|
104
|
+
try {
|
|
105
|
+
result[key] = schema[key](formData.get(key));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new Error(`Form Body validation failed for "${key}": ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
})();
|
|
112
|
+
} else {
|
|
113
|
+
this._form = this.req.formData().catch(() => null);
|
|
114
|
+
}
|
|
115
|
+
return this._form;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
WebContext
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=chunk-WTV4URUZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context/web.ts"],"sourcesContent":["/**\n * Ken Framework - Web Context\n * For Bun, Deno, and Cloudflare Workers\n * Uses Web Standard Request API\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\nimport { BaseContext, type HeadersLike } from './base';\nimport {\n type Schema,\n type GetRemoteInfo,\n type InferObject,\n type InferValidator,\n EMPTY_PARAMS,\n} from './types';\n\n/**\n * WebContext - For Bun, Deno, Cloudflare Workers\n * Uses Web Standard Request API directly\n * \n * @template S - Schema type for validation\n * @template Path - Route path string for param extraction\n * @template TState - Accumulated state from middleware\n */\nexport class WebContext<S extends Schema = {}, Path extends string = string, TState = {}>\n extends BaseContext<S, Path, TState> {\n\n public readonly req: Request;\n\n constructor(\n req: Request,\n params: Record<string, string> = EMPTY_PARAMS,\n getRemoteInfo: GetRemoteInfo,\n schema?: S\n ) {\n super(params, getRemoteInfo, schema);\n this.req = req;\n }\n\n // Implement abstract methods\n get url(): string {\n return this.req.url;\n }\n\n get method(): string {\n return this.req.method;\n }\n\n get body(): ReadableStream<Uint8Array> | null {\n return this.req.body;\n }\n\n protected _getRawHeaders(): HeadersLike {\n return this.req.headers;\n }\n\n protected _getRawUrl(): string {\n return this.req.url;\n }\n\n /**\n * Lazily parses and validates headers.\n */\n get headers(): InferObject<S['headers'], Record<string, string>> {\n let cached = this._headers;\n if (cached !== null) return cached;\n\n const schema = this._schema?.headers;\n if (schema !== undefined) {\n const headers = this.req.headers;\n const result: any = {};\n for (const key in schema) {\n const validator = schema[key];\n result[key] = validator(headers.get(key) || '');\n }\n this._headers = result;\n return result;\n }\n\n const h: Record<string, string> = {};\n this.req.headers.forEach((v, k) => h[k] = v);\n this._headers = h;\n return h as any;\n }\n\n /**\n * Lazily parses and validates JSON body.\n */\n get json(): Promise<InferValidator<S['json'], any>> {\n if (this._json) return this._json;\n\n const validator = this._schema?.json;\n if (validator) {\n this._json = (async () => {\n const data = await this.req.json();\n try {\n return validator(data);\n } catch (err: any) {\n throw new Error(`JSON Body validation failed: ${err.message}`);\n }\n })();\n } else {\n this._json = this.req.json().catch(() => null);\n }\n return this._json;\n }\n\n /**\n * Lazily parses and validates text body.\n */\n get text(): Promise<InferValidator<S['text'], string>> {\n if (this._text) return this._text;\n\n const validator = this._schema?.text;\n if (validator) {\n this._text = (async () => {\n const data = await this.req.text();\n try {\n return validator(data);\n } catch (err: any) {\n throw new Error(`Text Body validation failed: ${err.message}`);\n }\n })();\n } else {\n this._text = this.req.text();\n }\n return this._text;\n }\n\n /**\n * Lazily parses and validates form body.\n */\n get form(): Promise<InferObject<S['form'], FormData>> {\n if (this._form) return this._form;\n\n const schema = this._schema?.form;\n if (schema) {\n this._form = (async () => {\n const formData = await this.req.formData();\n const result: any = {};\n for (const key in schema) {\n try {\n result[key] = schema[key](formData.get(key));\n } catch (err: any) {\n throw new Error(`Form Body validation failed for \"${key}\": ${err.message}`);\n }\n }\n return result;\n })();\n } else {\n this._form = this.req.formData().catch(() => null);\n }\n return this._form;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WebContext
|
|
3
|
+
} from "./chunk-WTV4URUZ.js";
|
|
4
|
+
import {
|
|
5
|
+
getPathname
|
|
6
|
+
} from "./chunk-2BOPD5H7.js";
|
|
7
|
+
import "./chunk-2MK26YDD.js";
|
|
8
|
+
import {
|
|
9
|
+
PubSubHub,
|
|
10
|
+
Router,
|
|
11
|
+
WS_DEFAULTS,
|
|
12
|
+
WsReadyState,
|
|
13
|
+
createExecutor,
|
|
14
|
+
createNotFoundExecutor,
|
|
15
|
+
toResponse
|
|
16
|
+
} from "./chunk-DPU3PBLP.js";
|
|
17
|
+
|
|
18
|
+
// src/ws/deno.ts
|
|
19
|
+
var DENO_PING_MSG = '{"type":"ping"}';
|
|
20
|
+
var DenoWsPeer = class {
|
|
21
|
+
data;
|
|
22
|
+
remoteAddress;
|
|
23
|
+
/** Timestamp of last received message — used for heartbeat dead-peer detection */
|
|
24
|
+
lastActivity = Date.now();
|
|
25
|
+
ws;
|
|
26
|
+
hub;
|
|
27
|
+
constructor(ws, data, hub, remoteAddress) {
|
|
28
|
+
this.ws = ws;
|
|
29
|
+
this.data = data;
|
|
30
|
+
this.hub = hub;
|
|
31
|
+
this.remoteAddress = remoteAddress;
|
|
32
|
+
}
|
|
33
|
+
get readyState() {
|
|
34
|
+
return this.ws.readyState;
|
|
35
|
+
}
|
|
36
|
+
send(data, _compress) {
|
|
37
|
+
try {
|
|
38
|
+
this.ws.send(data);
|
|
39
|
+
return typeof data === "string" ? data.length : data.byteLength;
|
|
40
|
+
} catch {
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
close(code, reason) {
|
|
45
|
+
try {
|
|
46
|
+
this.ws.close(code, reason);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
subscribe(topic) {
|
|
51
|
+
this.hub.subscribe(this, topic);
|
|
52
|
+
}
|
|
53
|
+
unsubscribe(topic) {
|
|
54
|
+
this.hub.unsubscribe(this, topic);
|
|
55
|
+
}
|
|
56
|
+
publish(topic, data, compress) {
|
|
57
|
+
this.hub.publish(this, topic, data, compress);
|
|
58
|
+
}
|
|
59
|
+
isSubscribed(topic) {
|
|
60
|
+
return this.hub.isSubscribed(this, topic);
|
|
61
|
+
}
|
|
62
|
+
ping(_data) {
|
|
63
|
+
if (this.ws.readyState !== 1) return;
|
|
64
|
+
try {
|
|
65
|
+
this.ws.send(DENO_PING_MSG);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
pong(_data) {
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function createDenoWsHandler(handler, options = {}) {
|
|
73
|
+
const opts = { ...WS_DEFAULTS, ...options };
|
|
74
|
+
const hub = new PubSubHub();
|
|
75
|
+
const peers = /* @__PURE__ */ new Set();
|
|
76
|
+
let heartbeatInterval = null;
|
|
77
|
+
async function handleUpgrade(req, info) {
|
|
78
|
+
let data = void 0;
|
|
79
|
+
if (handler.upgrade) {
|
|
80
|
+
try {
|
|
81
|
+
const result = await handler.upgrade(req);
|
|
82
|
+
if (result instanceof Response) {
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
data = result;
|
|
86
|
+
} catch {
|
|
87
|
+
return new Response("Upgrade failed", { status: 500 });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
91
|
+
const peer = new DenoWsPeer(socket, data, hub, info.remoteAddr.hostname);
|
|
92
|
+
peers.add(peer);
|
|
93
|
+
socket.onopen = () => {
|
|
94
|
+
try {
|
|
95
|
+
handler.open?.(peer);
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
socket.onmessage = (event) => {
|
|
100
|
+
peer.lastActivity = Date.now();
|
|
101
|
+
hub.markAlive(peer);
|
|
102
|
+
try {
|
|
103
|
+
handler.message(peer, event.data);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
socket.onclose = (event) => {
|
|
108
|
+
peers.delete(peer);
|
|
109
|
+
hub.removeAll(peer);
|
|
110
|
+
try {
|
|
111
|
+
handler.close?.(peer, event.code, event.reason);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
socket.onerror = (_event) => {
|
|
116
|
+
try {
|
|
117
|
+
handler.error?.(peer, new Error("WebSocket error"));
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return response;
|
|
122
|
+
}
|
|
123
|
+
function startHeartbeat() {
|
|
124
|
+
if (opts.pingInterval <= 0) return;
|
|
125
|
+
if (heartbeatInterval) return;
|
|
126
|
+
const deadThresholdMs = (opts.pingInterval + opts.pongTimeout) * 1e3;
|
|
127
|
+
heartbeatInterval = setInterval(() => {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
for (const peer of peers) {
|
|
130
|
+
if (peer.readyState !== WsReadyState.OPEN) {
|
|
131
|
+
peers.delete(peer);
|
|
132
|
+
hub.removeAll(peer);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (now - peer.lastActivity > deadThresholdMs) {
|
|
136
|
+
try {
|
|
137
|
+
peer.close(1001, "ping timeout");
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
peers.delete(peer);
|
|
141
|
+
hub.removeAll(peer);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
peer.ping();
|
|
145
|
+
}
|
|
146
|
+
}, opts.pingInterval * 1e3);
|
|
147
|
+
}
|
|
148
|
+
function stopHeartbeat() {
|
|
149
|
+
if (heartbeatInterval) {
|
|
150
|
+
clearInterval(heartbeatInterval);
|
|
151
|
+
heartbeatInterval = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { hub, peers, handleUpgrade, startHeartbeat, stopHeartbeat };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/runtime/deno.ts
|
|
158
|
+
var webContextFactory = (req, params, getRemoteInfo, schema) => new WebContext(req, params, getRemoteInfo, schema);
|
|
159
|
+
function server({ port, hostname, router }) {
|
|
160
|
+
let ac = void 0;
|
|
161
|
+
const compiledRouter = new Router();
|
|
162
|
+
for (const route of router.routes) {
|
|
163
|
+
let mergedSchema = route.schema;
|
|
164
|
+
if (typeof router.matchMiddleware === "function" && typeof router.mergeSchemas === "function") {
|
|
165
|
+
const matchedMiddleware = router.matchMiddleware(route.path);
|
|
166
|
+
mergedSchema = router.mergeSchemas(matchedMiddleware, route.schema);
|
|
167
|
+
}
|
|
168
|
+
if (route.handler) {
|
|
169
|
+
const nativeHandler = createExecutor(webContextFactory, route.handler, mergedSchema);
|
|
170
|
+
compiledRouter.registerCompiled(route.method, route.path, nativeHandler, mergedSchema);
|
|
171
|
+
} else if (route.staticValue !== void 0) {
|
|
172
|
+
const cachedResponse = toResponse(route.staticValue);
|
|
173
|
+
compiledRouter.registerCompiled(route.method, route.path, () => cachedResponse.clone(), mergedSchema, cachedResponse);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const match = compiledRouter.matcher();
|
|
177
|
+
const notFoundExecutor = createNotFoundExecutor(router, webContextFactory);
|
|
178
|
+
const NOT_FOUND_RESPONSE = new Response("Not Found", { status: 404 });
|
|
179
|
+
const wsRoutes = router.wsRoutes || [];
|
|
180
|
+
const wsHandlers = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const wsRoute of wsRoutes) {
|
|
182
|
+
wsHandlers.set(wsRoute.path, createDenoWsHandler(wsRoute.handler, wsRoute.options));
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
async run() {
|
|
186
|
+
ac?.abort();
|
|
187
|
+
ac = new AbortController();
|
|
188
|
+
for (const wsHandler of wsHandlers.values()) {
|
|
189
|
+
wsHandler.startHeartbeat();
|
|
190
|
+
}
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
Deno.serve({
|
|
193
|
+
port,
|
|
194
|
+
hostname,
|
|
195
|
+
signal: ac?.signal,
|
|
196
|
+
onListen: ({ hostname: hostname2, port: port2 }) => {
|
|
197
|
+
resolve({ hostname: hostname2, port: port2 });
|
|
198
|
+
}
|
|
199
|
+
}, (request, info) => {
|
|
200
|
+
const pathname = getPathname(request.url);
|
|
201
|
+
if (request.headers.get("upgrade") === "websocket") {
|
|
202
|
+
const wsHandler = wsHandlers.get(pathname);
|
|
203
|
+
if (wsHandler) {
|
|
204
|
+
return wsHandler.handleUpgrade(request, info);
|
|
205
|
+
}
|
|
206
|
+
return new Response("Not Found", { status: 404 });
|
|
207
|
+
}
|
|
208
|
+
const matchResult = match(request.method, pathname);
|
|
209
|
+
if (matchResult !== void 0) {
|
|
210
|
+
if (matchResult.response !== void 0) {
|
|
211
|
+
return matchResult.response.clone();
|
|
212
|
+
}
|
|
213
|
+
const getRemoteInfo = () => ({
|
|
214
|
+
address: info.remoteAddr.hostname,
|
|
215
|
+
port: info.remoteAddr.port
|
|
216
|
+
});
|
|
217
|
+
const result = matchResult.handler(request, matchResult.params, getRemoteInfo);
|
|
218
|
+
if (result && typeof result.then === "function") {
|
|
219
|
+
return result.then(toResponse);
|
|
220
|
+
}
|
|
221
|
+
return toResponse(result);
|
|
222
|
+
}
|
|
223
|
+
if (notFoundExecutor) {
|
|
224
|
+
const getRemoteInfo = () => ({
|
|
225
|
+
address: info.remoteAddr.hostname,
|
|
226
|
+
port: info.remoteAddr.port
|
|
227
|
+
});
|
|
228
|
+
const result = notFoundExecutor(request, getRemoteInfo, pathname);
|
|
229
|
+
if (result && typeof result.then === "function") {
|
|
230
|
+
return result.then(toResponse);
|
|
231
|
+
}
|
|
232
|
+
return toResponse(result);
|
|
233
|
+
}
|
|
234
|
+
return NOT_FOUND_RESPONSE.clone();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
stop() {
|
|
239
|
+
for (const wsHandler of wsHandlers.values()) {
|
|
240
|
+
wsHandler.stopHeartbeat();
|
|
241
|
+
}
|
|
242
|
+
ac?.abort("STOP");
|
|
243
|
+
ac = void 0;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
server
|
|
249
|
+
};
|
|
250
|
+
//# sourceMappingURL=deno-LZU5JBGL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ws/deno.ts","../src/runtime/deno.ts"],"sourcesContent":["/**\n * Ken Framework - Deno WebSocket Adapter\n * Uses Deno's native WebSocket upgrade support\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\nimport {\n type WsPeer,\n type WsHandler,\n type WsOptions,\n type WsMessageData,\n type WsReadyStateValue,\n WsReadyState,\n WS_DEFAULTS,\n} from './types';\n\ndeclare const Deno: any;\nimport { PubSubHub, type PubSubPeer } from './pubsub';\n\n// ==================== DenoWsPeer ====================\n\n/** Pre-allocated app-level ping payload — reused across all heartbeat calls */\nconst DENO_PING_MSG = '{\"type\":\"ping\"}';\n\n/**\n * Deno WebSocket peer.\n * Wraps a standard WebSocket instance with Ken's peer interface and pub/sub.\n *\n * NOTE: Deno's WebSocket follows the DOM spec which does not expose protocol-level\n * ping/pong control frames. As a result:\n * - ping() sends an application-level {\"type\":\"ping\"} message (app-level keepalive)\n * - pong() is a no-op (no protocol pong to send)\n * - WsHandler.ping / WsHandler.pong callbacks are NEVER called on Deno\n */\nclass DenoWsPeer<T> implements WsPeer<T>, PubSubPeer {\n public data: T;\n public remoteAddress: string;\n /** Timestamp of last received message — used for heartbeat dead-peer detection */\n public lastActivity: number = Date.now();\n\n private ws: WebSocket;\n private hub: PubSubHub;\n\n constructor(ws: WebSocket, data: T, hub: PubSubHub, remoteAddress: string) {\n this.ws = ws;\n this.data = data;\n this.hub = hub;\n this.remoteAddress = remoteAddress;\n }\n\n get readyState(): WsReadyStateValue {\n return this.ws.readyState as WsReadyStateValue;\n }\n\n send(data: WsMessageData, _compress?: boolean): number {\n try {\n this.ws.send(data as any);\n return typeof data === 'string' ? data.length : (data as ArrayBuffer | Uint8Array).byteLength;\n } catch {\n return -1;\n }\n }\n\n close(code?: number, reason?: string): void {\n try {\n this.ws.close(code, reason);\n } catch { /* ignore */ }\n }\n\n subscribe(topic: string): void {\n this.hub.subscribe(this, topic);\n }\n\n unsubscribe(topic: string): void {\n this.hub.unsubscribe(this, topic);\n }\n\n publish(topic: string, data: WsMessageData, compress?: boolean): void {\n this.hub.publish(this, topic, data, compress);\n }\n\n isSubscribed(topic: string): boolean {\n return this.hub.isSubscribed(this, topic);\n }\n\n ping(_data?: WsMessageData): void {\n // Deno's DOM WebSocket does not expose protocol-level PING frames.\n // Send an application-level keepalive message. Clients using Ken's WSClient\n // (or any client that handles {\"type\":\"ping\"}) will respond with {\"type\":\"pong\"}.\n if (this.ws.readyState !== 1 /* OPEN */) return;\n try {\n this.ws.send(DENO_PING_MSG);\n } catch { /* ignore */ }\n }\n\n pong(_data?: WsMessageData): void {\n // Deno's DOM WebSocket does not expose protocol-level PONG frames.\n // handler.pong is never triggered on Deno — see class comment.\n }\n}\n\n// ==================== Deno WebSocket Handler ====================\n\n/**\n * Create a Deno WebSocket upgrade handler.\n * Called by the Deno runtime adapter.\n */\nexport function createDenoWsHandler<T>(\n handler: WsHandler<T>,\n options: WsOptions = {},\n): {\n hub: PubSubHub;\n peers: Set<DenoWsPeer<T>>;\n handleUpgrade: (req: Request, info: { remoteAddr: { hostname: string; }; }) => Promise<Response>;\n startHeartbeat: () => void;\n stopHeartbeat: () => void;\n} {\n const opts: Required<WsOptions> = { ...WS_DEFAULTS, ...options };\n const hub = new PubSubHub();\n const peers = new Set<DenoWsPeer<T>>();\n\n let heartbeatInterval: ReturnType<typeof setInterval> | null = null;\n\n async function handleUpgrade(req: Request, info: { remoteAddr: { hostname: string; }; }): Promise<Response> {\n // Run upgrade handler if provided\n let data: T = undefined as T;\n if (handler.upgrade) {\n try {\n const result = await handler.upgrade(req);\n if (result instanceof Response) {\n return result;\n }\n data = result;\n } catch {\n return new Response('Upgrade failed', { status: 500 });\n }\n }\n\n // Perform the upgrade using Deno.upgradeWebSocket\n const { socket, response } = (Deno as any).upgradeWebSocket(req);\n\n const peer = new DenoWsPeer<T>(socket, data, hub, info.remoteAddr.hostname);\n peers.add(peer);\n\n socket.onopen = () => {\n try {\n handler.open?.(peer);\n } catch { /* ignore */ }\n };\n\n socket.onmessage = (event: MessageEvent) => {\n // Update last-activity timestamp for dead peer detection\n peer.lastActivity = Date.now();\n // Update hub liveness — on Deno any inbound message counts as alive\n // since protocol-level pong is unavailable\n hub.markAlive(peer);\n try {\n handler.message(peer, event.data);\n } catch { /* ignore */ }\n };\n\n socket.onclose = (event: CloseEvent) => {\n peers.delete(peer);\n hub.removeAll(peer);\n try {\n handler.close?.(peer, event.code, event.reason);\n } catch { /* ignore */ }\n };\n\n socket.onerror = (_event: Event) => {\n try {\n handler.error?.(peer, new Error('WebSocket error'));\n } catch { /* ignore */ }\n };\n\n return response;\n }\n\n function startHeartbeat(): void {\n if (opts.pingInterval <= 0) return;\n if (heartbeatInterval) return;\n\n // Dead peer threshold: no message received within pingInterval + pongTimeout seconds.\n // Since Deno cannot do protocol ping/pong, we track last inbound message time.\n const deadThresholdMs = (opts.pingInterval + opts.pongTimeout) * 1000;\n\n heartbeatInterval = setInterval(() => {\n const now = Date.now();\n for (const peer of peers) {\n if (peer.readyState !== WsReadyState.OPEN) {\n peers.delete(peer);\n hub.removeAll(peer);\n continue;\n }\n // Close peers that have been silent longer than the dead threshold.\n // Note: this only catches peers that never send messages; a purely\n // listening peer is indistinguishable from a dead one on Deno without\n // protocol-level ping/pong. Use Ken's WSClient for reliable liveness.\n if (now - peer.lastActivity > deadThresholdMs) {\n try { peer.close(1001, 'ping timeout'); } catch { /* ignore */ }\n peers.delete(peer);\n hub.removeAll(peer);\n continue;\n }\n // Send app-level ping — clients can reply with {\"type\":\"pong\"}\n peer.ping();\n }\n }, opts.pingInterval * 1000);\n }\n\n function stopHeartbeat(): void {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval);\n heartbeatInterval = null;\n }\n }\n\n return { hub, peers, handleUpgrade, startHeartbeat, stopHeartbeat };\n}\n","/**\n * Ken Framework - Deno Runtime\n * Uses Web Standard APIs natively\n * \n * MIT License - Copyright (c) 2025 Indra Gunawan\n */\n\nimport { Router } from '../core/router';\nimport { WebContext } from '../context/web';\nimport { type Schema, type GetRemoteInfo } from '../context/types';\nimport { createExecutor, createNotFoundExecutor, type ContextFactory } from './compiler';\nimport { toResponse } from '../utils/response';\nimport { getPathname } from '../utils/pathname';\nimport { type WsRoute } from '../ws/types';\nimport { createDenoWsHandler } from '../ws/deno';\n\ndeclare const Deno: {\n serve(options: {\n port?: number;\n hostname?: string;\n signal?: AbortSignal;\n onListen?: (event: { hostname: string; port: number; }) => void;\n }, handler: (request: Request, info: { remoteAddr: { hostname: string; port: number; }; }) => Response | Promise<Response>): void;\n};\n\n/**\n * WebContext factory for Deno runtime\n */\nconst webContextFactory: ContextFactory<Request, WebContext> = (\n req: Request,\n params: Record<string, string>,\n getRemoteInfo: GetRemoteInfo,\n schema?: Schema\n) => new WebContext(req, params, getRemoteInfo, schema);\n\nexport function server({ port, hostname, router }: {\n port?: number;\n hostname?: string;\n router: Router;\n}) {\n let ac: AbortController | undefined = undefined;\n\n // Compile routes with executors\n const compiledRouter = new Router();\n\n for (const route of router.routes) {\n // Get merged schema\n let mergedSchema = route.schema;\n if (typeof (router as any).matchMiddleware === 'function' && typeof (router as any).mergeSchemas === 'function') {\n const matchedMiddleware = (router as any).matchMiddleware(route.path);\n mergedSchema = (router as any).mergeSchemas(matchedMiddleware, route.schema);\n }\n\n if (route.handler) {\n const nativeHandler = createExecutor(webContextFactory, route.handler, mergedSchema);\n compiledRouter.registerCompiled(route.method, route.path, nativeHandler, mergedSchema);\n } else if (route.staticValue !== undefined) {\n const cachedResponse = toResponse(route.staticValue);\n compiledRouter.registerCompiled(route.method, route.path, () => cachedResponse.clone(), mergedSchema, cachedResponse);\n }\n }\n\n // Hoist matcher\n const match = compiledRouter.matcher();\n\n // Pre-compile 404 handler with global middleware\n const notFoundExecutor = createNotFoundExecutor(router, webContextFactory);\n const NOT_FOUND_RESPONSE = new Response('Not Found', { status: 404 });\n\n // WebSocket routes\n const wsRoutes: WsRoute<any>[] = (router as any).wsRoutes || [];\n const wsHandlers = new Map<string, ReturnType<typeof createDenoWsHandler>>();\n for (const wsRoute of wsRoutes) {\n wsHandlers.set(wsRoute.path, createDenoWsHandler(wsRoute.handler, wsRoute.options));\n }\n\n return {\n async run(): Promise<{ hostname: string; port: number; }> {\n ac?.abort();\n ac = new AbortController();\n\n // Start heartbeat for all WS handlers\n for (const wsHandler of wsHandlers.values()) {\n wsHandler.startHeartbeat();\n }\n\n return new Promise((resolve) => {\n Deno.serve({\n port,\n hostname,\n signal: ac?.signal,\n onListen: ({ hostname, port }) => {\n resolve({ hostname, port });\n },\n }, (request, info) => {\n const pathname = getPathname(request.url);\n\n // Check for WebSocket upgrade\n if (request.headers.get('upgrade') === 'websocket') {\n const wsHandler = wsHandlers.get(pathname);\n if (wsHandler) {\n return wsHandler.handleUpgrade(request, info);\n }\n return new Response('Not Found', { status: 404 });\n }\n\n const matchResult = match(request.method, pathname);\n\n if (matchResult !== undefined) {\n // Fast path: static route (skip getRemoteInfo, handler call, toResponse)\n if (matchResult.response !== undefined) {\n return matchResult.response.clone();\n }\n\n // Dynamic route\n const getRemoteInfo: GetRemoteInfo = () => ({\n address: info.remoteAddr.hostname,\n port: info.remoteAddr.port,\n });\n\n const result = matchResult.handler(request, matchResult.params, getRemoteInfo);\n\n // Handle async results\n if (result && typeof result.then === 'function') {\n return result.then(toResponse);\n }\n return toResponse(result);\n }\n\n // Execute 404 with middleware if available\n if (notFoundExecutor) {\n const getRemoteInfo: GetRemoteInfo = () => ({\n address: info.remoteAddr.hostname,\n port: info.remoteAddr.port,\n });\n const result = notFoundExecutor(request, getRemoteInfo, pathname);\n if (result && typeof result.then === 'function') {\n return result.then(toResponse);\n }\n return toResponse(result);\n }\n\n return NOT_FOUND_RESPONSE.clone();\n });\n });\n },\n\n stop() {\n for (const wsHandler of wsHandlers.values()) {\n wsHandler.stopHeartbeat();\n }\n ac?.abort('STOP');\n ac = undefined;\n }\n };\n}\n\n// console.log('Deno runtime module loaded.');"],"mappings":"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","names":["hostname","port"]}
|