npm package: @abstraks-dev/event-store
Description: Bounded in-memory event store with optional DynamoDB persistence for AWS Lambda microservices
Author: Abstraks
Latest version: 1.0.0

# @abstraks-dev/event-store

Bounded in-memory event store with optional DynamoDB persistence for AWS Lambda microservices.

## Features

- ✅ **Bounded Size**: Configurable maximum event count (default: 100)
- ✅ **Time-to-Live (TTL)**: Automatic cleanup of old events
- ✅ **Binary Search Optimization**: O(log n) TTL cleanup performance
- ✅ **Optional DynamoDB Persistence**: Survive Lambda cold starts
- ✅ **Automatic Cold-Start Loading**: Populate cache from DynamoDB on first access
- ✅ **Graceful Degradation**: Falls back to in-memory if DynamoDB fails
- ✅ **Zero Dependencies**: DynamoDB SDK is optional peer dependency
- ✅ **Lambda Container Aware**: Efficient reuse across invocations

## Installation

```bash
npm install @abstraks-dev/event-store
```

### With DynamoDB Support (Optional)

```bash
npm install @abstraks-dev/event-store @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
```

## Usage

### Simple In-Memory Store

```javascript
import { createSimpleEventStore } from '@abstraks-dev/event-store';

// Create store with defaults (100 events, 24 hour TTL)
const eventStore = createSimpleEventStore();

// Add events
await eventStore.addEvent('UserCreated', {
	userId: '123',
	email: 'user@example.com',
});

await eventStore.addEvent('PostCreated', {
	postId: '456',
	content: 'Hello world',
});

// Retrieve all events
const events = await eventStore.getEvents();
console.log(`Stored ${eventStore.getCount()} events`);
```

### Custom Configuration

```javascript
import { createSimpleEventStore } from '@abstraks-dev/event-store';

const eventStore = createSimpleEventStore(
	50, // Max 50 events
	60 * 60 * 1000 // 1 hour TTL
);
```

### With DynamoDB Persistence

```javascript
import { createEventStore } from '@abstraks-dev/event-store';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

const dynamoClient = new DynamoDBClient({ region: 'us-west-2' });
const docClient = DynamoDBDocumentClient.from(dynamoClient);

const eventStore = createEventStore({
	maxEvents: 100,
	maxAgeMs: 24 * 60 * 60 * 1000, // 24 hours
	dynamodb: {
		client: docClient,
		tableName: 'EventStore-prod',
		ttlDays: 30, // DynamoDB TTL (default: 30 days)
		maxScanItems: 1000, // Max items to load on cold start
	},
});

// Events are automatically persisted to DynamoDB
await eventStore.addEvent(
	'OrderPlaced',
	{
		orderId: '789',
		amount: 99.99,
	},
	{
		source: 'order-service',
		eventId: 'custom-event-id', // Optional
	}
);

// On first call, automatically loads from DynamoDB
const events = await eventStore.getEvents();
```

## API

### `createSimpleEventStore(maxEvents?, maxAgeMs?)`

Factory function for creating a simple in-memory event store.

**Parameters:**

- `maxEvents` (number, optional): Maximum number of events (default: 100)
- `maxAgeMs` (number, optional): Maximum age in milliseconds (default: 24 hours)

**Returns:** Event store instance

### `createEventStore(options)`

Create an event store with advanced configuration.

**Options:**

```typescript
{
	maxEvents?: number;        // Max events in memory (default: 100)
	maxAgeMs?: number;         // Max age in ms (default: 24 hours)
	dynamodb?: {
		client: DynamoDBDocumentClient;  // AWS SDK DocumentClient
		tableName: string;                // DynamoDB table name
		ttlDays?: number;                 // TTL in days (default: 30)
		maxScanItems?: number;            // Max items for cold-start scan (default: 1000)
	}
}
```

**Returns:** Event store instance

### Event Store Methods

#### `addEvent(type, data, options?)`

Add an event to the store.

**Parameters:**

- `type` (string): Event type identifier
- `data` (object): Event payload
- `options` (object, optional):
  - `source` (string): Event source identifier (default: 'unknown')
  - `eventId` (string): Custom event ID (auto-generated if omitted)

**Returns:** Promise<Event> - Added event with metadata

```javascript
const event = await eventStore.addEvent(
	'UserLoggedIn',
	{
		userId: '123',
		ipAddress: '192.168.1.1',
	},
	{
		source: 'auth-service',
	}
);

console.log(event.eventId); // Auto-generated or custom
console.log(event.timestamp); // ISO 8601 timestamp
console.log(event.timestampMs); // Unix timestamp in ms
```

#### `getEvents()`

Retrieve all stored events (with automatic cleanup and DynamoDB loading).

**Returns:** Promise<Event[]>

```javascript
const events = await eventStore.getEvents();
events.forEach((event) => {
	console.log(`${event.type} at ${event.timestamp}`);
});
```

#### `getCount()`

Get the current number of stored events.

**Returns:** number

```javascript
console.log(`Store contains ${eventStore.getCount()} events`);
```

#### `clear()`

Clear all events from the in-memory store.

**Note:** Does not affect DynamoDB persistence.

```javascript
eventStore.clear();
```

## DynamoDB Table Schema

If using DynamoDB persistence, create a table with the following schema:

```javascript
{
	TableName: 'EventStore-prod',
	KeySchema: [
		{ AttributeName: 'eventId', KeyType: 'HASH' },  // Partition key
		{ AttributeName: 'timestamp', KeyType: 'RANGE' } // Sort key
	],
	AttributeDefinitions: [
		{ AttributeName: 'eventId', AttributeType: 'S' },
		{ AttributeName: 'timestamp', AttributeType: 'S' }
	],
	BillingMode: 'PAY_PER_REQUEST',
	TimeToLiveSpecification: {
		Enabled: true,
		AttributeName: 'ttl'  // Unix timestamp for auto-deletion
	}
}
```

### AWS CDK Example

```typescript
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

const eventTable = new dynamodb.Table(this, 'EventStore', {
	partitionKey: { name: 'eventId', type: dynamodb.AttributeType.STRING },
	sortKey: { name: 'timestamp', type: dynamodb.AttributeType.STRING },
	billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
	timeToLiveAttribute: 'ttl',
	removalPolicy: RemovalPolicy.DESTROY, // For dev/test
});
```

## Performance Characteristics

| Operation                  | Time Complexity | Notes                                 |
| -------------------------- | --------------- | ------------------------------------- |
| `addEvent()`               | O(n) amortized  | Binary search cleanup + FIFO eviction |
| `getEvents()`              | O(n)            | Binary search cleanup + array copy    |
| `getCount()`               | O(1)            | Direct array length                   |
| `clear()`                  | O(1)            | Array truncation                      |
| DynamoDB Scan (cold start) | O(n)            | Limited by `maxScanItems`             |

**Binary Search Optimization:** TTL cleanup uses binary search to find the cutoff index (O(log n)), then removes all old events in one splice operation (O(n)). This is significantly faster than iterating and removing individually (O(n²)).

## Best Practices

### 1. Choose Appropriate Bounds

```javascript
// For debugging/monitoring (small, short-lived)
const debugStore = createSimpleEventStore(50, 60 * 60 * 1000); // 50 events, 1 hour

// For event replay (large, persistent)
const replayStore = createEventStore({
	maxEvents: 1000,
	maxAgeMs: 7 * 24 * 60 * 60 * 1000, // 7 days
	dynamodb: {
		/* ... */
	},
});
```

### 2. Use DynamoDB for Production

In-memory storage is lost on Lambda cold starts. Use DynamoDB for:

- Event replay capabilities
- Cross-invocation persistence
- Audit trails
- Historical analysis

### 3. Monitor Memory Usage

Lambda memory limits apply. For large event counts or payloads:

- Adjust `maxEvents` based on Lambda memory allocation
- Use `maxAgeMs` to prevent unbounded growth
- Consider separating hot/cold storage tiers

### 4. Handle DynamoDB Errors

The store degrades gracefully but log errors for monitoring:

```javascript
// Custom error handling (optional)
const store = createEventStore({
	maxEvents: 100,
	dynamodb: {
		client: docClient,
		tableName: process.env.EVENT_TABLE,
	},
});

// Monitor via CloudWatch Logs
await store.addEvent('CriticalEvent', data); // Logs DynamoDB errors internally
```

### 5. Environment-Specific Configuration

```javascript
const isDev = process.env.ENVIRONMENT === 'dev';

const eventStore = createEventStore({
	maxEvents: isDev ? 50 : 500,
	maxAgeMs: isDev ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000,
	dynamodb: isDev
		? null
		: {
				client: docClient,
				tableName: `EventStore-${process.env.ENVIRONMENT}`,
			},
});
```

## Migration from Existing Event Stores

### From Auth/Media Pattern

```javascript
// Before
const events = [];
export const addEvent = (type, data) => {
	events.push({ type, data, timestamp: new Date().toISOString() });
};
export const getEvents = () => events;

// After
import { createSimpleEventStore } from '@abstraks-dev/event-store';
const { addEvent, getEvents } = createSimpleEventStore();
```

### From Event-Bus Pattern (with DynamoDB)

```javascript
// Before
const events = [];
// ... manual DynamoDB PutCommand logic

// After
import { createEventStore } from '@abstraks-dev/event-store';
const eventStore = createEventStore({
	maxEvents: 100,
	dynamodb: {
		client: docClient,
		tableName: process.env.EVENTS_TABLE_NAME,
		ttlDays: 30,
	},
});
```

## Troubleshooting

### Events not persisting across Lambda invocations

- **Cause:** DynamoDB not configured or table doesn't exist
- **Solution:** Verify `dynamodb.client` and `dynamodb.tableName` are set correctly

### High memory usage

- **Cause:** `maxEvents` too high or large event payloads
- **Solution:** Reduce `maxEvents` or implement payload size limits

### DynamoDB throttling

- **Cause:** High event volume with on-demand billing
- **Solution:** Use provisioned capacity or batch writes

### Old events not cleaning up

- **Cause:** `getEvents()` not called frequently (cleanup is lazy)
- **Solution:** Cleanup runs automatically on `addEvent()` and `getEvents()`

## License

MIT © Abstraks