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

526 lines 19.2 kB
import { normalizeHeaders, extractRequestMetadata, createEdgeError, isWebSocketUpgrade, getPlatformCapabilities, DEFAULT_ADAPTER_OPTIONS, } from "./types"; export class FastifyEdgeAdapter { app; options; compressionEnabled; websocketEnabled; constructor(app, options) { this.app = app; this.options = { ...DEFAULT_ADAPTER_OPTIONS, ...options, }; const capabilities = getPlatformCapabilities(this.options.platform); this.compressionEnabled = this.options.compression === true && capabilities.supportsCompression; this.websocketEnabled = this.options.websocket === true && capabilities.supportsWebSocket; } /** * Create an edge-compatible handler for the given platform */ createHandler() { switch (this.options.platform) { case "cloudflare": return this.createCloudflareHandler(); case "vercel": return this.createVercelHandler(); case "deno": return this.createDenoHandler(); case "netlify": return this.createNetlifyHandler(); default: throw createEdgeError(`Unsupported platform: ${this.options.platform}`, 500, this.options.platform); } } /** * Cloudflare Workers handler with WebSocket support */ createCloudflareHandler() { return async (request, env, ctx) => { try { const edgeRequest = await this.parseCloudflareRequest(request, env); const context = { request: edgeRequest, env, ctx, waitUntil: ctx?.waitUntil?.bind(ctx), passThroughOnException: ctx?.passThroughOnException?.bind(ctx), }; // Handle WebSocket upgrade if (this.websocketEnabled && isWebSocketUpgrade(edgeRequest)) { return this.handleWebSocketUpgrade(edgeRequest, context); } const edgeResponse = await this.processRequest(edgeRequest, context); const response = this.createCloudflareResponse(edgeResponse); // Use ctx.waitUntil for background tasks if (ctx?.waitUntil) { ctx.waitUntil(this.logRequest(edgeRequest, edgeResponse)); } return response; } catch (error) { return this.createErrorResponse(error, "cloudflare"); } }; } /** * Vercel Edge handler */ createVercelHandler() { return async (request) => { try { const edgeRequest = await this.parseVercelRequest(request); const context = { request: edgeRequest, }; const edgeResponse = await this.processRequest(edgeRequest, context); return this.createVercelResponse(edgeResponse); } catch (error) { return this.createErrorResponse(error, "vercel"); } }; } /** * Deno Deploy handler with WebSocket support */ createDenoHandler() { return async (request) => { try { const edgeRequest = await this.parseDenoRequest(request); const context = { request: edgeRequest, }; // Handle WebSocket upgrade for Deno if (this.websocketEnabled && isWebSocketUpgrade(edgeRequest)) { return this.handleWebSocketUpgrade(edgeRequest, context); } const edgeResponse = await this.processRequest(edgeRequest, context); return this.createDenoResponse(edgeResponse); } catch (error) { return this.createErrorResponse(error, "deno"); } }; } /** * Netlify Edge handler */ createNetlifyHandler() { return async (request, context) => { try { const edgeRequest = await this.parseNetlifyRequest(request, context); const edgeContext = { request: edgeRequest, ctx: context, geo: context.geo, }; const edgeResponse = await this.processRequest(edgeRequest, edgeContext); return this.createNetlifyResponse(edgeResponse); } catch (error) { return this.createErrorResponse(error, "netlify"); } }; } /** * Handle WebSocket upgrade */ async handleWebSocketUpgrade(request, context) { if (!this.websocketEnabled) { throw createEdgeError("WebSocket not supported on this platform", 400, this.options.platform); } try { // Create WebSocket pair for Cloudflare/Deno if (this.options.platform === "cloudflare" || this.options.platform === "deno") { // Use platform-specific WebSocket creation const WebSocketPair = globalThis.WebSocketPair; if (!WebSocketPair) { throw createEdgeError("WebSocketPair not available on this platform", 500, this.options.platform); } const webSocketPair = new WebSocketPair(); const [client, server] = [ webSocketPair.client || webSocketPair[0], webSocketPair.server || webSocketPair[1], ]; if (!client || !server) { throw createEdgeError("WebSocket pair creation failed", 500, this.options.platform); } // Process the upgrade through Fastify const injectOptions = { method: "GET", url: request.url, headers: { ...normalizeHeaders(request.headers), upgrade: "websocket", connection: "Upgrade", }, remoteAddress: this.getRemoteAddress(request, context), }; // Accept the WebSocket connection server.accept(); // You would typically handle WebSocket events here server.addEventListener("message", (event) => { // Handle incoming WebSocket messages console.log("WebSocket message:", event.data); }); server.addEventListener("close", (event) => { // Handle WebSocket close console.log("WebSocket closed:", event.code, event.reason); }); return new Response(null, { status: 101, webSocket: client, }); } throw createEdgeError("WebSocket not supported on this platform", 400, this.options.platform); } catch (error) { console.error("WebSocket upgrade failed:", error); throw createEdgeError("WebSocket upgrade failed", 500, this.options.platform); } } /** * Process request through Fastify with compression support */ 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.compressionEnabled && 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; } } /** * Determine if response should be compressed */ shouldCompress(headers, body) { if (!this.compressionEnabled) 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 }; } } /** * Parse Cloudflare Workers request */ async parseCloudflareRequest(request, env) { return { url: request.url, method: request.method, headers: normalizeHeaders(request.headers), body: await this.parseBody(request), cf: request.cf, geo: this.extractGeo(request.cf), ip: request.headers.get("cf-connecting-ip") || undefined, upgrade: request.headers.get("upgrade") === "websocket", }; } /** * Parse Vercel Edge request */ async parseVercelRequest(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", }; } /** * Parse Deno Deploy request */ async parseDenoRequest(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", }; } /** * Parse Netlify Edge request */ async parseNetlifyRequest(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", }; } /** * Parse request body with better error handling */ 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 const maxSize = getPlatformCapabilities(this.options.platform).maxRequestSize; if (contentLength && parseInt(contentLength) > maxSize) { 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 from request with better fallbacks */ getRemoteAddress(request, context) { // Try various edge-specific headers in order of preference 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"); } /** * Extract geographic information from Cloudflare */ extractGeo(cf) { if (!cf) return undefined; return { country: cf.country, region: cf.region, city: cf.city, timezone: cf.timezone, latitude: cf.latitude, longitude: cf.longitude, }; } /** * Extract geographic information from Vercel */ 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"], }; } /** * Extract geographic information from Deno */ extractDenoGeo(headers) { return { country: headers["cf-ipcountry"], region: headers["cf-region"], }; } /** * Create Cloudflare Workers Response */ createCloudflareResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } /** * Create Vercel Edge Response */ createVercelResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } /** * Create Deno Deploy Response */ createDenoResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } /** * Create Netlify Edge Response */ createNetlifyResponse(edgeResponse) { return new Response(edgeResponse.body, { status: edgeResponse.status, headers: edgeResponse.headers, }); } /** * Create error response with proper logging */ createErrorResponse(error, platform) { console.error(`${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, timestamp: new Date().toISOString(), ...(process.env.NODE_ENV === "development" && { stack: error.stack }), }; return new Response(JSON.stringify(errorResponse), { status, headers: { "content-type": "application/json", }, }); } /** * 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); } } } /** * Create a Fastify edge adapter */ export function createFastifyEdgeAdapter(app, options) { return new FastifyEdgeAdapter(app, options); } /** * Create platform-specific handler */ export function createEdgeHandler(app, platform) { const adapter = createFastifyEdgeAdapter(app, { platform }); return adapter.createHandler(); } //# sourceMappingURL=fastify-adapter.js.map