@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
JavaScript
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