Skip to content

Xaid-vfx/build-service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Serverless Next.js Deployment System

Overview

This system implements a serverless architecture for deploying and serving Next.js applications on AWS Lambda. The core functionality is implemented in a Lambda function that handles both static file serving and server-side rendering (SSR) through a sophisticated request processing pipeline.

Core Components

1. Lambda Handler Architecture

1.1 Environment Setup

The Lambda function requires specific environment variables:

AWS_REGION=us-east-1  // AWS region for S3 operations
JOB_ID=unique-id      // Unique identifier for the deployment
S3_BUCKET_NAME=bucket // S3 bucket containing deployments

1.2 Server Initialization

  1. Server Caching

    const serverCache = new Map();
    const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
    • Implements a server cache with 5-minute TTL
    • Cache key is based on JOB_ID
    • Prevents unnecessary server reloads
  2. Bundle Extraction

    const workDir = join(TMP_DIR, JOB_ID);
    await mkdir(workDir, { recursive: true });
    const zip = new AdmZip(bundlePath);
    zip.extractAllTo(extractDir, true);
    • Downloads bundle from S3
    • Extracts to /tmp/{JOB_ID}/extracted
    • Uses adm-zip for extraction
  3. Environment Configuration

    process.env.NODE_ENV = 'production';
    process.env.NEXT_TELEMETRY_DISABLED = '1';
    process.env.NEXT_RUNTIME = 'nodejs';
    process.env.NEXT_SHARP_PATH = join(extractDir, 'node_modules/sharp');
    process.env.NEXT_DIST_DIR = join(extractDir, '.next');
    process.env.NEXT_BUILD_ID = 'standalone';
    process.env.NEXT_PHASE = 'production';
    • Sets critical Next.js environment variables
    • Configures runtime environment
    • Manages dependencies

2. Request Processing

2.1 Request Analysis

const path = event.rawPath || event.path || '/';
const queryString = event.rawQueryString || '';
const fullPath = queryString ? `${path}?${queryString}` : path;
  • Extracts path from Lambda event
  • Handles query parameters
  • Normalizes paths

2.2 Static File Detection

function isStaticFileRequest(path) {
    if (!path) return false;
    return path.startsWith('/_next/') || 
           path.startsWith('/public/') || 
           path.startsWith('/static/');
}
  • Identifies static file requests
  • Handles Next.js static files
  • Manages public files

2.3 Request Object Creation

function createRequest(event) {
    const path = event.rawPath || event.path || '/';
    const httpMethod = event.requestContext?.http?.method || event.httpMethod || 'GET';
    const headers = event.headers || {};
    const queryStringParameters = event.queryStringParameters || {};
    const body = event.body;
    
    // Create a proper Node.js request object
    const req = {
        method: httpMethod,
        url: path,
        headers: headers,
        query: queryStringParameters,
        body: body ? JSON.parse(body) : undefined,
        cookies: event.cookies || {},
        rawHeaders: Object.entries(headers).flat(),
        rawUrl: path,
        protocol: 'https',
        hostname: event.requestContext?.domainName || 'localhost',
        ip: event.requestContext?.http?.sourceIp || '127.0.0.1',
        
        // Event emitter implementation
        _events: {},
        on: function(event, listener) {
            if (!this._events[event]) this._events[event] = [];
            this._events[event].push(listener);
            return this;
        },
        once: function(event, listener) {
            const onceWrapper = (...args) => {
                listener.apply(this, args);
                this.removeListener(event, onceWrapper);
            };
            return this.on(event, onceWrapper);
        },
        removeListener: function(event, listener) {
            if (!this._events || !this._events[event]) return this;
            this._events[event] = this._events[event].filter(l => l !== listener);
            return this;
        },
        removeAllListeners: function(event) {
            if (!this._events) return this;
            if (event) {
                delete this._events[event];
            } else {
                this._events = {};
            }
            return this;
        },
        listeners: function(event) {
            return this._events[event] || [];
        },
        emit: function(event, ...args) {
            if (!this._events || !this._events[event]) return false;
            this._events[event].forEach(listener => listener.apply(this, args));
            return true;
        },
        
        // Stream interface
        readable: true,
        read: function() {
            if (this.body) {
                const chunk = this.body;
                this.body = null;
                return chunk;
            }
            return null;
        },
        pipe: function(dest) {
            if (this.body) {
                dest.write(this.body);
            }
            dest.end();
            return dest;
        },
        
        // Socket simulation
        socket: {
            remoteAddress: event.requestContext?.http?.sourceIp || '127.0.0.1',
            remotePort: 443,
            encrypted: true,
            on: function() {},
            once: function() {},
            removeListener: function() {},
            removeAllListeners: function() {},
            listeners: function() { return []; },
            emit: function() { return true; }
        },
        connection: {
            encrypted: true,
            on: function() {},
            once: function() {},
            removeListener: function() {},
            removeAllListeners: function() {},
            listeners: function() { return []; },
            emit: function() { return true; }
        }
    };
    
    return req;
}
  • Creates Node.js-compatible request object
  • Implements event emitter interface
  • Provides stream capabilities
  • Simulates socket behavior
  • Handles headers and cookies

3. Response Handling

3.1 Response Object Creation

function createResponse() {
    const response = {
        statusCode: 200,
        headers: {},
        body: '',
        _chunks: [],
        _ended: false,
        _events: {},
        _implicitHeader: function() {
            if (!this.headersSent) {
                this.writeHead(this.statusCode);
            }
        },
        _header: '',
        _headerSent: false,
        headersSent: false,
        
        // Response methods
        setHeader: function(name, value) {
            this.headers[name] = value;
            return this;
        },
        getHeader: function(name) {
            return this.headers[name];
        },
        removeHeader: function(name) {
            delete this.headers[name];
            return this;
        },
        write: function(chunk) {
            if (this._ended) return false;
            if (typeof chunk === 'string') {
                chunk = Buffer.from(chunk);
            }
            this._chunks.push(chunk);
            return true;
        },
        end: function(chunk) {
            if (chunk) {
                if (typeof chunk === 'string') {
                    chunk = Buffer.from(chunk);
                }
                this._chunks.push(chunk);
            }
            this._ended = true;
            return this;
        },
        writeHead: function(statusCode, headers) {
            this.statusCode = statusCode;
            if (headers) {
                Object.assign(this.headers, headers);
            }
            this.headersSent = true;
            this._headerSent = true;
            return this;
        },
        flushHeaders: function() {
            if (!this.headersSent) {
                this.writeHead(this.statusCode);
            }
            return this;
        },

        // Event emitter implementation
        on: function(event, listener) {
            if (!this._events[event]) this._events[event] = [];
            this._events[event].push(listener);
            return this;
        },
        once: function(event, listener) {
            const onceWrapper = (...args) => {
                listener.apply(this, args);
                this.removeListener(event, onceWrapper);
            };
            return this.on(event, onceWrapper);
        },
        removeListener: function(event, listener) {
            if (!this._events[event]) return this;
            this._events[event] = this._events[event].filter(l => l !== listener);
            return this;
        },
        removeAllListeners: function(event) {
            if (event) {
                delete this._events[event];
            } else {
                this._events = {};
            }
            return this;
        },
        listeners: function(event) {
            return this._events[event] || [];
        },
        emit: function(event, ...args) {
            if (!this._events[event]) return false;
            this._events[event].forEach(listener => listener.apply(this, args));
            return true;
        },
        off: function(event, listener) {
            return this.removeListener(event, listener);
        }
    };
    
    return response;
}
  • Implements Node.js response interface
  • Handles chunked responses
  • Manages headers and status codes
  • Provides event emitter capabilities

3.2 Response Collection

const responsePromise = new Promise((resolve, reject) => {
    let responseData = {
        statusCode: 200,
        headers: {},
        body: '',
        chunks: []
    };
    
    // Set up response event handlers
    const handleResponseComplete = () => {
        console.log('βœ… Response complete');
        responseData.statusCode = res.statusCode || 200;
        responseData.headers = res.headers;
        
        // Join chunks and convert to string
        const buffer = Buffer.concat(res._chunks);
        responseData.body = buffer.toString('utf8');
        responseData.isBase64Encoded = false;
        
        resolve(responseData);
    };
    
    // Set up write handler to collect chunks
    const originalWrite = res.write;
    res.write = function(chunk, encoding, callback) {
        console.log('πŸ“ Writing chunk:', { 
            chunkLength: chunk ? chunk.length : 0,
            encoding,
            hasCallback: !!callback,
            isBuffer: Buffer.isBuffer(chunk)
        });
        if (chunk) {
            if (!Buffer.isBuffer(chunk)) {
                chunk = Buffer.from(chunk);
            }
            res._chunks.push(chunk);
        }
        const result = originalWrite.call(this, chunk, encoding, callback);
        return result;
    };
    
    // Set up end handler
    const originalEnd = res.end;
    res.end = function(chunk, encoding, callback) {
        console.log('πŸ“ Ending response:', {
            chunkLength: chunk ? chunk.length : 0,
            encoding,
            hasCallback: !!callback,
            isBuffer: chunk ? Buffer.isBuffer(chunk) : false
        });
        if (chunk) {
            if (!Buffer.isBuffer(chunk)) {
                chunk = Buffer.from(chunk);
            }
            res._chunks.push(chunk);
        }
        const result = originalEnd.call(this, chunk, encoding, callback);
        handleResponseComplete();
        return result;
    };
    
    // Set up error handler
    res.on('error', (error) => {
        console.error('❌ Response error:', error);
        reject(error);
    });
    
    // Set up finish handler
    res.on('finish', () => {
        console.log('βœ… Response finished');
        handleResponseComplete();
    });
    
    // Set up close handler
    res.on('close', () => {
        console.log('βœ… Response closed');
        handleResponseComplete();
    });
});
  • Collects response chunks
  • Handles timeouts
  • Manages partial responses
  • Converts to Lambda response format

4. Error Handling

try {
    // Handle request
} catch (error) {
    console.error('❌ Error handling request:', error);
    
    // If we have partial response data, try to return it
    if (error.message === 'Response timeout' && res._chunks && res._chunks.length > 0) {
        console.log('⚠️ Returning partial response due to timeout');
        return {
            statusCode: res.statusCode || 200,
            headers: {
                ...res.headers,
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'GET, OPTIONS',
                'Access-Control-Allow-Headers': 'Content-Type, Authorization'
            },
            body: res._chunks.join(''),
            isBase64Encoded: res.headers['Content-Encoding'] === 'gzip'
        };
    }
    
    return {
        statusCode: error.message === 'Response timeout' ? 504 : 500,
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({
            error: error.message === 'Response timeout' ? 'Gateway Timeout' : 'Internal Server Error',
            message: error.message,
            stack: error.stack
        })
    };
}
  • Implements comprehensive error handling
  • Provides detailed error responses
  • Handles timeouts gracefully
  • Manages partial responses

5. Static File Serving

5.1 File Types Support

const contentTypes = {
    // Images
    'png': 'image/png',
    'jpg': 'image/jpeg',
    'jpeg': 'image/jpeg',
    'gif': 'image/gif',
    'svg': 'image/svg+xml',
    'webp': 'image/webp',
    'ico': 'image/x-icon',
    
    // Fonts
    'woff2': 'font/woff2',
    'woff': 'font/woff',
    'ttf': 'font/ttf',
    'eot': 'application/vnd.ms-fontobject',
    'otf': 'font/otf',
    
    // Documents
    'html': 'text/html',
    'htm': 'text/html',
    'css': 'text/css',
    'js': 'application/javascript',
    'json': 'application/json',
    'xml': 'application/xml',
    'pdf': 'application/pdf',
    
    // Text
    'txt': 'text/plain',
    'md': 'text/markdown',
    'csv': 'text/csv',
    
    // Archives
    'zip': 'application/zip',
    'tar': 'application/x-tar',
    'gz': 'application/gzip',
    
    // Other
    'mp4': 'video/mp4',
    'webm': 'video/webm',
    'mp3': 'audio/mpeg',
    'wav': 'audio/wav'
};
  • Comprehensive MIME type support
  • Proper content type mapping

5.2 Image Optimization

if (path.startsWith('/_next/image')) {
    const url = new URL(fullPath, 'http://localhost');
    const width = parseInt(url.searchParams.get('w') || '0');
    const quality = parseInt(url.searchParams.get('q') || '75');

    if (width > 0) {
        try {
            console.log('Processing image:', { width, quality, contentType });
            const sharp = require('sharp');
            const processedImage = await sharp(buffer)
                .resize(width, null, { withoutEnlargement: true })
                .jpeg({ quality })
                .toBuffer();
            buffer = processedImage;
            contentType = 'image/jpeg';
        } catch (error) {
            console.error('Error processing image:', error);
            // If image processing fails, return the original image
        }
    }
}
  • Uses Sharp for image processing
  • Resizes images based on width parameter
  • Optimizes JPEG quality
  • Handles processing failures gracefully

6. Logging

console.log('πŸ“ Processing request:', { path, fullPath, event });
console.log('πŸ“¦ Handling static file request');
console.log('Static file info:', staticFileInfo);
console.error('❌ Error handling request:', error);
console.error('Error fetching static file:', error, { path, fullPath, staticFileInfo });
  • Implements comprehensive logging
  • Tracks request lifecycle
  • Logs errors and warnings
  • Monitors performance

About

Build service for next.js deployment

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors