UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

507 lines 18.5 kB
import { normalizeHeaders, extractRequestMetadata, createEdgeError, isWebSocketUpgrade, getPlatformCapabilities, DEFAULT_ADAPTER_OPTIONS, PLATFORM_CAPABILITIES, } from "./types"; /** * Base adapter for edge runtime deployments * Provides common functionality for all edge platforms */ export class BaseEdgeAdapter { app; options; capabilities; constructor(app, options) { this.app = app; this.options = { ...DEFAULT_ADAPTER_OPTIONS, ...options, }; this.capabilities = getPlatformCapabilities(this.options.platform); this.validatePlatformSupport(); } /** * Handle WebSocket upgrade (if supported) */ async handleWebSocketUpgrade(request, context) { if (!this.capabilities.supportsWebSocket) { throw createEdgeError(`WebSocket not supported on ${this.options.platform}`, 400, this.options.platform); } return this.createWebSocketResponse(request, context); } /** * Create WebSocket response (override in platform-specific adapters) */ createWebSocketResponse(request, context) { throw createEdgeError("WebSocket implementation not available", 500, this.options.platform); } /** * Process request through Fastify */ async processRequest(request, context) { try { const metadata = extractRequestMetadata(request); // Convert edge request to Fastify inject format const injectOptions = { method: request.method, url: request.url, headers: normalizeHeaders(request.headers), payload: request.body, remoteAddress: this.getRemoteAddress(request, context), }; // Process through Fastify const response = await this.app.inject(injectOptions); let body = response.body; const headers = normalizeHeaders(response.headers); // Apply compression if enabled and appropriate if (this.options.compression && this.shouldCompress(headers, body)) { const compressed = await this.compressResponse(body, headers); body = compressed.body; Object.assign(headers, compressed.headers); } // Add Content-Length if not present if (!headers["content-length"] && typeof body === "string") { headers["content-length"] = new TextEncoder() .encode(body) .length.toString(); } return { status: response.statusCode, headers, body, }; } catch (error) { console.error("Edge processing error:", error); throw error; } } /** * Parse request body with size limits */ async parseBody(request) { if (!request.body) return undefined; const contentType = request.headers.get("content-type")?.toLowerCase(); const contentLength = request.headers.get("content-length"); // Check body size limits if (contentLength && parseInt(contentLength) > this.capabilities.maxRequestSize) { throw createEdgeError(`Request body too large: ${contentLength} bytes`, 413, this.options.platform); } try { if (contentType?.includes("application/json")) { return await request.json(); } else if (contentType?.includes("application/x-www-form-urlencoded")) { const text = await request.text(); return new URLSearchParams(text); } else if (contentType?.includes("multipart/form-data")) { return await request.formData(); } else { return await request.text(); } } catch (error) { console.warn("Failed to parse request body:", error); throw createEdgeError("Invalid request body", 400, this.options.platform); } } /** * Get remote address with fallbacks */ getRemoteAddress(request, context) { return (request.ip || request.headers["cf-connecting-ip"] || request.headers["x-real-ip"] || request.headers["x-forwarded-for"]?.split(",")[0]?.trim() || request.headers["x-vercel-forwarded-for"] || "127.0.0.1"); } /** * Determine if response should be compressed */ shouldCompress(headers, body) { if (!this.capabilities.supportsCompression) return false; if (headers["content-encoding"]) return false; // Already compressed if (typeof body !== "string") return false; if (body.length < 1024) return false; // Too small to compress const contentType = headers["content-type"]?.toLowerCase() || ""; const compressibleTypes = [ "text/", "application/json", "application/javascript", "application/xml", "application/xhtml+xml", ]; return compressibleTypes.some((type) => contentType.includes(type)); } /** * Compress response body */ async compressResponse(body, headers) { try { // Use gzip compression if available if (typeof CompressionStream !== "undefined") { const stream = new CompressionStream("gzip"); const writer = stream.writable.getWriter(); const reader = stream.readable.getReader(); await writer.write(new TextEncoder().encode(body)); await writer.close(); const chunks = []; let done = false; while (!done) { const { value, done: readerDone } = await reader.read(); done = readerDone; if (value) chunks.push(value); } const compressed = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { compressed.set(chunk, offset); offset += chunk.length; } return { body: new TextDecoder().decode(compressed), headers: { ...headers, "content-encoding": "gzip", "content-length": compressed.length.toString(), }, }; } // Fallback: no compression return { body, headers }; } catch (error) { console.warn("Compression failed:", error); return { body, headers }; } } /** * Create error response */ createErrorResponse(error) { console.error(`${this.options.platform} edge error:`, error); const status = error.status || 500; const errorResponse = { error: status >= 500 ? "Internal Server Error" : "Bad Request", message: error instanceof Error ? error.message : "Unknown error", platform: this.options.platform, timestamp: new Date().toISOString(), ...(process.env.NODE_ENV === "development" && { stack: error.stack }), }; return new Response(JSON.stringify(errorResponse), { status, headers: { "content-type": "application/json", }, }); } /** * Validate platform support */ validatePlatformSupport() { if (!PLATFORM_CAPABILITIES[this.options.platform]) { throw createEdgeError(`Unsupported platform: ${this.options.platform}`, 500, this.options.platform); } if (this.options.websocket && !this.capabilities.supportsWebSocket) { console.warn(`WebSocket requested but not supported on ${this.options.platform}`); this.options.websocket = false; } if (this.options.compression && !this.capabilities.supportsCompression) { console.warn(`Compression requested but not supported on ${this.options.platform}`); this.options.compression = false; } } /** * Log request for analytics (background task) */ async logRequest(request, response) { try { if (this.options.disableRequestLogging) return; const logData = { method: request.method, url: request.url, status: response.status, timestamp: new Date().toISOString(), platform: this.options.platform, userAgent: request.headers["user-agent"], ip: request.ip, geo: request.geo, }; console.log("Request log:", JSON.stringify(logData)); } catch (error) { console.warn("Failed to log request:", error); } } /** * Get adapter statistics */ getStats() { return { platform: this.options.platform, capabilities: this.capabilities, options: { ...this.options }, }; } } /** * Cloudflare Workers Adapter */ export class CloudflareAdapter extends BaseEdgeAdapter { createHandler() { return async (request, env, ctx) => { try { const edgeRequest = await this.parseRequest(request, env, ctx); const context = { request: edgeRequest, env, ctx, waitUntil: ctx?.waitUntil?.bind(ctx), passThroughOnException: ctx?.passThroughOnException?.bind(ctx), }; // Handle WebSocket upgrade if (this.options.websocket && isWebSocketUpgrade(edgeRequest)) { return this.handleWebSocketUpgrade(edgeRequest, context); } const edgeResponse = await this.processRequest(edgeRequest, context); const response = this.createResponse(edgeResponse); // Use ctx.waitUntil for background tasks if (ctx?.waitUntil) { ctx.waitUntil(this.logRequest(edgeRequest, edgeResponse)); } return response; } catch (error) { return this.createErrorResponse(error); } }; } async parseRequest(request, env, ctx) { return { url: request.url, method: request.method, headers: normalizeHeaders(request.headers), body: await this.parseBody(request), cf: request.cf, geo: this.extractCloudflareGeo(request.cf), ip: request.headers.get("cf-connecting-ip") || undefined, upgrade: request.headers.get("upgrade") === "websocket", }; } createResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } async createWebSocketResponse(request, context) { const WebSocketPair = globalThis.WebSocketPair; if (!WebSocketPair) { throw createEdgeError("WebSocketPair not available", 500, this.options.platform); } const webSocketPair = new WebSocketPair(); const [client, server] = [webSocketPair[0], webSocketPair[1]]; server.accept(); // Handle WebSocket events server.addEventListener("message", (event) => { console.log("WebSocket message:", event.data); }); server.addEventListener("close", (event) => { console.log("WebSocket closed:", event.code, event.reason); }); return new Response(null, { status: 101, webSocket: client, }); } extractCloudflareGeo(cf) { if (!cf) return undefined; return { country: cf.country, region: cf.region, city: cf.city, timezone: cf.timezone, latitude: cf.latitude, longitude: cf.longitude, }; } } /** * Vercel Edge Adapter */ export class VercelAdapter extends BaseEdgeAdapter { createHandler() { return async (request) => { try { const edgeRequest = await this.parseRequest(request); const context = { request: edgeRequest, }; const edgeResponse = await this.processRequest(edgeRequest, context); return this.createResponse(edgeResponse); } catch (error) { return this.createErrorResponse(error); } }; } async parseRequest(request) { const headers = normalizeHeaders(request.headers); return { url: request.url, method: request.method, headers, body: await this.parseBody(request), geo: this.extractVercelGeo(headers), ip: headers["x-vercel-forwarded-for"] || headers["x-real-ip"] || undefined, upgrade: headers["upgrade"] === "websocket", }; } createResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } extractVercelGeo(headers) { return { country: headers["x-vercel-ip-country"], region: headers["x-vercel-ip-country-region"], city: headers["x-vercel-ip-city"], latitude: headers["x-vercel-ip-latitude"], longitude: headers["x-vercel-ip-longitude"], }; } } /** * Deno Deploy Adapter */ export class DenoAdapter extends BaseEdgeAdapter { createHandler() { return async (request) => { try { const edgeRequest = await this.parseRequest(request); const context = { request: edgeRequest, }; // Handle WebSocket upgrade if (this.options.websocket && isWebSocketUpgrade(edgeRequest)) { return this.handleWebSocketUpgrade(edgeRequest, context); } const edgeResponse = await this.processRequest(edgeRequest, context); return this.createResponse(edgeResponse); } catch (error) { return this.createErrorResponse(error); } }; } async parseRequest(request) { const headers = normalizeHeaders(request.headers); return { url: request.url, method: request.method, headers, body: await this.parseBody(request), geo: this.extractDenoGeo(headers), ip: headers["x-forwarded-for"]?.split(",")[0]?.trim() || undefined, upgrade: headers["upgrade"] === "websocket", }; } createResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } async createWebSocketResponse(request, context) { // Deno has WebSocket support similar to Cloudflare const { response, socket } = globalThis.Deno.upgradeWebSocket(request); socket.onopen = () => console.log("WebSocket opened"); socket.onmessage = (event) => console.log("WebSocket message:", event.data); socket.onclose = () => console.log("WebSocket closed"); socket.onerror = (error) => console.error("WebSocket error:", error); return response; } extractDenoGeo(headers) { return { country: headers["cf-ipcountry"], region: headers["cf-region"], }; } } /** * Netlify Edge Adapter */ export class NetlifyAdapter extends BaseEdgeAdapter { createHandler() { return async (request, context) => { try { const edgeRequest = await this.parseRequest(request, context); const edgeContext = { request: edgeRequest, ctx: context, geo: context.geo, }; const edgeResponse = await this.processRequest(edgeRequest, edgeContext); return this.createResponse(edgeResponse); } catch (error) { return this.createErrorResponse(error); } }; } async parseRequest(request, context) { return { url: request.url, method: request.method, headers: normalizeHeaders(request.headers), body: await this.parseBody(request), geo: context.geo, ip: context.ip || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || undefined, upgrade: request.headers.get("upgrade") === "websocket", }; } createResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } } /** * Create platform-specific adapter */ export function createEdgeAdapter(app, platform, options = {}) { const adapterOptions = { ...options, platform }; switch (platform) { case "cloudflare": return new CloudflareAdapter(app, adapterOptions); case "vercel": return new VercelAdapter(app, adapterOptions); case "deno": return new DenoAdapter(app, adapterOptions); case "netlify": return new NetlifyAdapter(app, adapterOptions); default: throw createEdgeError(`Unsupported platform: ${platform}`, 500, platform); } } /** * Quick handler creation for simple use cases */ export function createEdgeHandler(app, platform, options = {}) { const adapter = createEdgeAdapter(app, platform, options); return adapter.createHandler(); } //# sourceMappingURL=adapter.js.map