@codeforbreakfast/eventsourcing-protocol-default
Version:
Default implementation of the event sourcing protocol over any transport - Standard protocol implementation with message serialization, command correlation, and subscription management
209 lines (149 loc) • 5.45 kB
Markdown
# Event Sourcing Protocol
Protocol implementation for event sourcing over transport abstractions.
## Protocol Layer Responsibilities
The protocol layer acts as a **generic RPC mechanism for event sourcing** - it's a dumb pipe that shuffles messages between client and server without knowing anything about your domain.
### What the protocol SHOULD know:
1. **Message structure** - How to wrap messages in envelopes (IDs, timestamps, correlation)
2. **Message types** - Basic routing types: "command", "event", "subscription"
3. **Wire format** - How to serialize/deserialize for transport
4. **Message correlation** - Matching responses to requests via IDs
5. **Connection state** - Managing subscriptions and pending commands
### What it SHOULDN'T know:
1. **Domain logic** - What an "UpdateProfile" command actually does
2. **Event schemas** - The actual shape of your business events
3. **Aggregate rules** - Validation, business invariants, etc.
4. **Command handlers** - How commands get processed
Think of it like HTTP - HTTP doesn't care what your JSON payload contains, it just delivers it. The protocol's `Command` and `Event` interfaces are just **transport containers** with metadata fields (id, aggregate, position) while the actual `payload` and `data` fields remain `unknown`.
The protocol's core responsibilities:
- **Message correlation** - Track which responses belong to which requests
- **Subscription management** - Route events to the right subscribers
- **Timeout handling** - Fail commands that take too long
- **Error propagation** - Pass errors back from server to client
- **Transport abstraction** - Hide WebSocket/HTTP/whatever details
## Installation
```bash
bun add @codeforbreakfast/eventsourcing-protocol-default
```
## Usage
```typescript
import { createProtocol } from '@codeforbreakfast/eventsourcing-protocol-default';
import { createTransport } from '@codeforbreakfast/eventsourcing-transport-websocket';
// Create protocol with a transport
const protocol = await Effect.runPromise(createTransport(url).pipe(Effect.flatMap(createProtocol)));
// Send commands
const result = await Effect.runPromise(
protocol.sendCommand({
id: crypto.randomUUID(),
aggregate: 'user-123',
name: 'UpdateProfile',
payload: { name: 'John Doe' },
})
);
// Subscribe to events
const eventStream = await Effect.runPromise(protocol.subscribe('user-123'));
await Effect.runPromise(
Stream.runForEach(eventStream, (event) =>
Effect.log(`Event: ${event.type} at position ${event.position.eventNumber}`)
)
);
```
## API
### `createProtocol(transport)`
Creates a protocol instance from a transport connection.
**Parameters:**
- `transport: Client.Transport` - Connected transport instance
**Returns:** `Effect<Protocol, TransportError, never>`
### `Protocol.sendCommand(command)`
Sends a command and waits for the result.
**Parameters:**
- `command: Command` - Command to send
- `id: string` - Unique command identifier
- `aggregate: string` - Aggregate identifier
- `name: string` - Command name
- `payload: unknown` - Command data
**Returns:** `Effect<CommandResult, TransportError, never>`
Result contains:
- `success: boolean` - Whether command succeeded
- `position?: EventStreamPosition` - New stream position after command
- `error?: string` - Error message if command failed
Commands automatically timeout after 10 seconds.
### `Protocol.subscribe(streamId)`
Subscribes to events from a specific stream.
**Parameters:**
- `streamId: string` - Stream identifier to subscribe to
**Returns:** `Effect<Stream<Event>, TransportError, never>`
Each event contains:
- `position: EventStreamPosition` - Event position in stream
- `type: string` - Event type name
- `data: unknown` - Event payload
- `timestamp: Date` - When event occurred
## Message Format
The protocol exchanges JSON messages over the transport.
### Command Message
```json
{
"type": "command",
"id": "cmd-123",
"aggregate": "user-456",
"name": "UpdateProfile",
"payload": { "name": "John" }
}
```
### Command Result Message
```json
{
"type": "command_result",
"commandId": "cmd-123",
"success": true,
"position": { "streamId": "user-456", "eventNumber": 42 }
}
```
### Subscribe Message
```json
{
"type": "subscribe",
"streamId": "user-456"
}
```
### Event Message
```json
{
"type": "event",
"streamId": "user-456",
"position": { "streamId": "user-456", "eventNumber": 42 },
"eventType": "ProfileUpdated",
"data": { "name": "John" },
"timestamp": "2024-01-01T00:00:00Z"
}
```
## Error Handling
All operations return Effects that handle:
- Transport errors (connection failures, network issues)
- Timeout errors (commands timing out after 10 seconds)
- Parsing errors (gracefully caught and logged)
## Types
```typescript
interface Command {
readonly id: string;
readonly aggregate: string;
readonly name: string;
readonly payload: unknown;
}
interface Event {
readonly position: EventStreamPosition;
readonly type: string;
readonly data: unknown;
readonly timestamp: Date;
}
interface CommandResult {
readonly success: boolean;
readonly position?: EventStreamPosition;
readonly error?: string;
}
interface Protocol {
readonly sendCommand: (command: Command) => Effect<CommandResult, TransportError, never>;
readonly subscribe: (streamId: string) => Effect<Stream<Event>, TransportError, never>;
}
```
## License
MIT