- Ordered Processing Queue: Use
Array for sequential event consumption. Arrays provide contiguous memory layout and O(1) push/pop operations, making them ideal for stack/queue patterns.
- Deduplication Layer: Use
Set for tracking processed event IDs. Hash-based lookup guarantees O(1) membership testing, preventing duplicate processing without linear scans.
- State Registry: Use
Map for correlating events with metadata. Maps support arbitrary key types, maintain insertion order, and avoid prototype pollution.
- Static Configuration: Use plain
Object for immutable settings. Objects serialize cleanly to JSON and provide fast property access for known string keys.
- External Reference Tracking: Use
WeakMap for attaching lifecycle metadata to DOM nodes or external handles without preventing garbage collection.
Implementation
interface EventPayload {
id: string;
timestamp: number;
category: 'user' | 'system' | 'network';
payload: Record<string, unknown>;
}
interface ProcessingConfig {
maxQueueSize: number;
dedupWindowMs: number;
enableLogging: boolean;
}
class EventPipeline {
private readonly config: ProcessingConfig;
private readonly processingQueue: Array<EventPayload>;
private readonly processedIds: Set<string>;
private readonly stateRegistry: Map<string, EventPayload>;
private readonly elementMetadata: WeakMap<HTMLElement, { lastProcessed: number; retryCount: number }>;
constructor(config: Partial<ProcessingConfig> = {}) {
this.config = {
maxQueueSize: 500,
dedupWindowMs: 30000,
enableLogging: false,
...config,
};
this.processingQueue = [];
this.processedIds = new Set();
this.stateRegistry = new Map();
this.elementMetadata = new WeakMap();
}
public enqueue(event: EventPayload, sourceElement?: HTMLElement): void {
if (this.processedIds.has(event.id)) {
if (this.config.enableLogging) {
console.warn(`Duplicate event rejected: ${event.id}`);
}
return;
}
if (this.processingQueue.length >= this.config.maxQueueSize) {
this.processingQueue.shift(); // O(n) but acceptable for bounded queue
}
this.processingQueue.push(event);
this.processedIds.add(event.id);
this.stateRegistry.set(event.id, event);
if (sourceElement) {
const meta = this.elementMetadata.get(sourceElement) ?? { lastProcessed: 0, retryCount: 0 };
meta.lastProcessed = Date.now();
meta.retryCount++;
this.elementMetadata.set(sourceElement, meta);
}
}
public processNext(): EventPayload | undefined {
const next = this.processingQueue.shift();
if (!next) return undefined;
this.stateRegistry.delete(next.id);
return next;
}
public getRegistrySnapshot(): ReadonlyMap<string, EventPayload> {
return new Map(this.stateRegistry);
}
}
Rationale
- Array for Queue: The pipeline requires FIFO processing.
push() and shift() align with queue semantics. While shift() is O(n), the bounded maxQueueSize constraint keeps the operation predictable and avoids unbounded memory growth.
- Set for Deduplication: Event IDs are unique identifiers.
Set.prototype.has() executes in constant time regardless of collection size. This eliminates the O(n) scan that would occur with Array.includes().
- Map for State Registry: The registry requires fast key-based retrieval, dynamic size tracking, and iteration over active events. Maps provide O(1)
get/set/delete, expose .size directly, and preserve insertion order for debugging.
- Object for Config: Configuration objects have a fixed schema, require JSON serialization for persistence, and never undergo dynamic key addition. Plain objects are optimal here.
- WeakMap for Element Metadata: DOM elements are frequently removed from the document. Storing metadata in a regular
Map would create strong references, preventing garbage collection and causing memory leaks. WeakMap keys are held weakly, so when the element is detached and unreferenced, the metadata entry is automatically reclaimed.
Pitfall Guide
1. Linear Membership Testing in Hot Paths
Explanation: Using Array.includes() or Array.some() for frequent existence checks forces the engine to traverse the entire collection. In loops or request handlers, this creates O(n²) complexity when nested.
Fix: Migrate to Set for membership testing. Convert arrays to sets once during initialization, then use Set.has() for O(1) lookups.
2. Dynamic Property Deletion on Objects
Explanation: The delete operator removes properties but invalidates V8 hidden classes. Once a hidden class is invalidated, the engine falls back to dictionary mode for property access, significantly slowing subsequent reads/writes.
Fix: Use Map for collections requiring frequent additions/removals. If using objects, mark properties as undefined or null instead of deleting them, or pre-allocate the shape.
3. Assuming Object Key Order
Explanation: While ES2015+ guarantees insertion order for string keys, relying on this for business logic creates fragile code. Symbol keys and numeric keys follow different ordering rules, and iteration behavior varies across engines for mixed key types.
Fix: Use Map when insertion order is a functional requirement. Maps guarantee deterministic iteration regardless of key type.
4. Strong References to Transient Objects
Explanation: Storing DOM nodes, WebSocket instances, or temporary buffers in Array, Set, or Map creates strong references. Even after the object is logically discarded, the collection keeps it alive, increasing heap size and GC pressure.
Fix: Use WeakMap or WeakSet for metadata or tracking flags on objects with independent lifecycles. The engine automatically cleans up entries when the key object is unreachable.
5. Overhead of Object.keys().length
Explanation: Calculating collection size via Object.keys(obj).length allocates a new array, copies all enumerable keys, and iterates them. This is O(n) and generates garbage.
Fix: Use Map or Set and access the .size property directly. It is O(1) and requires no allocation.
6. Ignoring Prototype Chain Lookups
Explanation: Plain objects inherit from Object.prototype. Checking for keys like constructor, toString, or hasOwnProperty can trigger prototype chain traversal or return unexpected inherited values.
Fix: Use Object.hasOwn(obj, key) for safe property checks, or switch to Map which has no prototype pollution risk.
7. Unbounded Array Growth
Explanation: Continuously pushing to an array without bounds checking or cleanup leads to memory exhaustion and degraded iteration performance. Arrays do not automatically evict old entries.
Fix: Implement explicit capacity limits, use circular buffers for fixed-size windows, or switch to Map/Set with eviction policies (e.g., LRU) for cache-like behavior.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Frequent existence checks on unique identifiers | Set | O(1) hash lookup eliminates linear scans | Reduces CPU cycles by 100-1000x in hot paths |
| Dynamic key-value storage with frequent mutations | Map | Preserves hidden classes, O(1) mutation, no prototype pollution | Prevents V8 deoptimization and dictionary mode fallback |
| Fixed schema data requiring JSON serialization | Object | Native JSON compatibility, fast property access, predictable shape | Zero overhead for serialization, optimal for config/records |
| Ordered sequential processing with bounded size | Array | Contiguous memory layout, O(1) end mutations, native iteration | Predictable performance, minimal allocation |
| Metadata attachment to transient DOM/external objects | WeakMap | Weak key references enable automatic GC cleanup | Eliminates manual cleanup routines and memory leaks |
| Tracking processed items without storing full objects | WeakSet | Weak references prevent retention, O(1) membership | Reduces heap pressure in long-running processes |
Configuration Template
// collection-factory.ts
// Production-ready typed collection utilities with built-in safeguards
export function createBoundedQueue<T>(maxSize: number): Array<T> {
const queue: Array<T> = [];
return new Proxy(queue, {
set(target, property, value) {
if (property === 'push' && target.length >= maxSize) {
target.shift();
}
return Reflect.set(target, property, value);
},
}) as unknown as Array<T>;
}
export function createDeduplicationSet<T>(initial?: Iterable<T>): Set<T> {
return new Set(initial);
}
export function createLookupRegistry<K, V>(initial?: Iterable<[K, V]>): Map<K, V> {
return new Map(initial);
}
export function createWeakMetadataStore<T extends object, V>(): WeakMap<T, V> {
return new WeakMap();
}
// Usage example with type safety
const eventQueue = createBoundedQueue<EventPayload>(1000);
const activeSessions = createDeduplicationSet<string>();
const requestCache = createLookupRegistry<string, Response>();
const domTimings = createWeakMetadataStore<HTMLElement, number>();
Quick Start Guide
- Identify Access Patterns: Map out how your data is created, read, updated, and deleted. Note frequency, key types, and whether order or uniqueness matters.
- Select Base Collections: Match patterns to structures:
Array for sequences, Set for uniqueness, Map for dynamic key-value, Object for fixed schemas, WeakMap/WeakSet for transient references.
- Implement Type-Safe Wrappers: Use TypeScript interfaces to enforce shape constraints. Wrap native constructors with factory functions that apply bounds or validation.
- Integrate into Hot Paths: Replace existing array/object usage in performance-critical loops or request handlers. Run benchmarks to verify complexity improvements.
- Monitor Memory & GC: Use Chrome DevTools Memory panel or Node.js
--inspect to track heap growth. Confirm that WeakMap/WeakSet entries are reclaimed when keys are dereferenced.