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

430 lines 13.8 kB
export class MemoryEventAdapter { name = "memory"; version = "1.0.0"; capabilities = { persistence: false, clustering: false, partitioning: false, consumerGroups: false, deadLetterQueue: false, exactly_once: false, at_least_once: true, ordering: true, wildcards: true, replay: false, backpressure: true, }; subscribers = new Map(); stats = { published: 0, delivered: 0, acknowledged: 0, failed: 0, retries: 0, dlqSize: 0, activeSubscriptions: 0, throughputPerSecond: 0, averageLatency: 0, errorRate: 0, backpressureEvents: 0, lastActivity: new Date(), }; connected = false; throughputWindow = []; latencyWindow = []; errors = 0; operations = 0; // Concurrency control activeTasks = new Set(); maxConcurrency = 100; /** * Connect to the memory adapter (no-op) */ async connect() { this.connected = true; } /** * Disconnect from the memory adapter */ async disconnect() { this.connected = false; this.subscribers.clear(); this.resetStats(); } /** * Check if adapter is connected */ isConnected() { return this.connected; } /** * Publish an event to all matching subscribers */ async publish(subject, event) { if (!this.connected) { throw new Error("Memory adapter not connected"); } const startTime = Date.now(); this.stats.published++; this.stats.lastActivity = new Date(); this.operations++; try { // Find matching subscribers (including wildcards) const matchingSubscribers = this.findMatchingSubscribers(subject); if (matchingSubscribers.length === 0) { return { messageId: event.id, timestamp: new Date().toISOString(), }; } // Update throughput metrics this.updateThroughput(); // Concurrency control await this.waitForConcurrencySlot(); // Deliver to all subscribers const deliveryPromises = matchingSubscribers.map(async (subData) => { const deliveryTask = this.deliverEvent(event, subData); this.activeTasks.add(deliveryTask); try { await deliveryTask; this.stats.delivered++; } catch (error) { this.stats.failed++; this.errors++; console.error(`Failed to deliver event to ${subject}:`, error); } finally { this.activeTasks.delete(deliveryTask); } }); await Promise.allSettled(deliveryPromises); // Update latency const latency = Date.now() - startTime; this.updateLatency(latency); return { messageId: event.id, timestamp: new Date().toISOString(), }; } catch (error) { this.stats.failed++; this.errors++; throw error; } } /** * Subscribe to events with pattern matching */ async subscribe(subject, handler, options = {}) { if (!this.connected) { throw new Error("Memory adapter not connected"); } const subscriptionId = this.generateSubscriptionId(); const subData = { id: subscriptionId, subject, handler: this.wrapHandler(handler, options), options, active: true, createdAt: new Date(), messageCount: 0, lastActivity: new Date(), }; // Add to subscribers map if (!this.subscribers.has(subject)) { this.subscribers.set(subject, new Map()); } this.subscribers.get(subject).set(subscriptionId, subData); this.stats.activeSubscriptions = this.getTotalSubscribers(); return { id: subscriptionId, subject, active: true, createdAt: new Date(), unsubscribe: async () => { await this.unsubscribe({ id: subscriptionId, subject, active: true, createdAt: subData.createdAt, unsubscribe: async () => { }, }); }, }; } /** * Unsubscribe from events */ async unsubscribe(handle) { const subjectSubs = this.subscribers.get(handle.subject); if (subjectSubs) { subjectSubs.delete(handle.id); if (subjectSubs.size === 0) { this.subscribers.delete(handle.subject); } this.stats.activeSubscriptions = this.getTotalSubscribers(); } } /** * Acknowledge event delivery (no-op for memory adapter) */ async ack(event) { this.stats.acknowledged++; } /** * Negative acknowledge - requeue event (no-op for memory adapter) */ async nack(event, requeue = false) { // Memory adapter doesn't support requeuing } /** * Flush pending operations (no-op for memory adapter) */ async flush() { // Wait for all active tasks to complete await Promise.allSettled(Array.from(this.activeTasks)); } /** * Get adapter statistics */ async getStats() { this.stats.errorRate = this.operations > 0 ? this.errors / this.operations : 0; return { ...this.stats }; } /** * Health check for memory adapter */ async healthCheck() { const checks = [ { name: "connection", status: this.connected ? "pass" : "fail", message: this.connected ? "Connected" : "Not connected", }, { name: "memory_usage", status: this.getMemoryUsage() < 100 * 1024 * 1024 ? "pass" : "warn", // 100MB message: `Memory usage: ${Math.round(this.getMemoryUsage() / 1024 / 1024)}MB`, details: { memoryBytes: this.getMemoryUsage() }, }, { name: "active_subscriptions", status: this.stats.activeSubscriptions > 0 ? "pass" : "warn", message: `${this.stats.activeSubscriptions} active subscriptions`, details: { count: this.stats.activeSubscriptions }, }, { name: "error_rate", status: this.stats.errorRate < 0.05 ? "pass" : "warn", // 5% error rate message: `Error rate: ${Math.round(this.stats.errorRate * 100)}%`, details: { errorRate: this.stats.errorRate }, }, ]; const hasFailures = checks.some((c) => c.status === "fail"); const hasWarnings = checks.some((c) => c.status === "warn"); return { status: hasFailures ? "unhealthy" : hasWarnings ? "degraded" : "healthy", checks, timestamp: new Date(), }; } /** * Find subscribers matching a subject (with wildcard support) */ findMatchingSubscribers(subject) { const matches = []; for (const [pattern, subscribers] of this.subscribers) { if (this.matchesPattern(subject, pattern)) { matches.push(...Array.from(subscribers.values())); } } return matches.filter((sub) => sub.active); } /** * Check if subject matches pattern (supports * and > wildcards) */ matchesPattern(subject, pattern) { if (subject === pattern) return true; const subjectParts = subject.split("."); const patternParts = pattern.split("."); // Handle > wildcard (matches everything from that point) const greaterIndex = patternParts.indexOf(">"); if (greaterIndex !== -1) { if (greaterIndex === patternParts.length - 1) { // > is at the end, match if all previous parts match return this.matchesParts(subjectParts.slice(0, greaterIndex), patternParts.slice(0, greaterIndex)); } return false; // > must be at the end } // Exact length match required for * wildcards if (subjectParts.length !== patternParts.length) { return false; } return this.matchesParts(subjectParts, patternParts); } /** * Match subject parts with pattern parts */ matchesParts(subjectParts, patternParts) { for (let i = 0; i < patternParts.length; i++) { const patternPart = patternParts[i]; const subjectPart = subjectParts[i]; if (patternPart === "*") { continue; // * matches any single token } if (patternPart !== subjectPart) { return false; } } return true; } /** * Wrap handler with concurrency control and error handling */ wrapHandler(handler, options) { return async (event) => { // Apply filter if provided if (options.filter) { const matches = await options.filter(event); if (!matches) return; } // Apply timeout if provided const timeout = options.ackTimeout || 30000; await Promise.race([ handler(event), new Promise((_, reject) => setTimeout(() => reject(new Error("Handler timeout")), timeout)), ]); }; } /** * Deliver event to subscriber */ async deliverEvent(event, subData) { try { subData.lastActivity = new Date(); subData.messageCount++; await subData.handler(event); } catch (error) { console.error(`Handler error for subscription ${subData.id}:`, error); throw error; } } /** * Wait for concurrency slot */ async waitForConcurrencySlot() { while (this.activeTasks.size >= this.maxConcurrency) { this.stats.backpressureEvents++; await Promise.race(Array.from(this.activeTasks)); } } /** * Update throughput metrics */ updateThroughput() { const now = Date.now(); const windowStart = now - 1000; // 1 second window // Remove old entries this.throughputWindow = this.throughputWindow.filter((entry) => entry.timestamp >= windowStart); // Add current entry this.throughputWindow.push({ timestamp: now, count: 1 }); // Calculate throughput this.stats.throughputPerSecond = this.throughputWindow.reduce((sum, entry) => sum + entry.count, 0); } /** * Update latency metrics */ updateLatency(latency) { this.latencyWindow.push(latency); // Keep only last 100 measurements if (this.latencyWindow.length > 100) { this.latencyWindow = this.latencyWindow.slice(-100); } // Calculate average this.stats.averageLatency = this.latencyWindow.reduce((sum, l) => sum + l, 0) / this.latencyWindow.length; } /** * Get memory usage estimate */ getMemoryUsage() { // Rough estimate of memory usage let size = 0; for (const [subject, subs] of this.subscribers) { size += subject.length * 2; // UTF-16 size += subs.size * 1000; // Rough estimate per subscription } size += this.throughputWindow.length * 16; // timestamp + count size += this.latencyWindow.length * 8; // number return size; } /** * Get total number of subscribers */ getTotalSubscribers() { return Array.from(this.subscribers.values()).reduce((total, subs) => total + subs.size, 0); } /** * Generate unique subscription ID */ generateSubscriptionId() { return `mem-sub-${Date.now()}-${Math.random().toString(36).slice(2)}`; } /** * Reset statistics */ resetStats() { this.stats = { published: 0, delivered: 0, acknowledged: 0, failed: 0, retries: 0, dlqSize: 0, activeSubscriptions: 0, throughputPerSecond: 0, averageLatency: 0, errorRate: 0, backpressureEvents: 0, lastActivity: new Date(), }; this.throughputWindow = []; this.latencyWindow = []; this.errors = 0; this.operations = 0; } /** * Get all active subjects */ getSubjects() { return Array.from(this.subscribers.keys()); } /** * Get subscriber count for a specific subject */ getSubscriberCount(subject) { const subs = this.subscribers.get(subject); return subs ? subs.size : 0; } /** * Clear all subscribers for a subject */ clearSubject(subject) { this.subscribers.delete(subject); this.stats.activeSubscriptions = this.getTotalSubscribers(); } /** * Set max concurrency */ setMaxConcurrency(maxConcurrency) { this.maxConcurrency = Math.max(1, maxConcurrency); } } export default MemoryEventAdapter; //# sourceMappingURL=memory.js.map