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