@anabranch/eventlog 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/esm/adapter.d.ts +83 -62
- package/esm/adapter.d.ts.map +1 -1
- package/esm/adapter.js +0 -9
- package/esm/errors.d.ts +2 -7
- package/esm/errors.d.ts.map +1 -1
- package/esm/errors.js +9 -21
- package/esm/eventlog.d.ts +74 -92
- package/esm/eventlog.d.ts.map +1 -1
- package/esm/eventlog.js +105 -148
- package/esm/in-memory.d.ts +15 -34
- package/esm/in-memory.d.ts.map +1 -1
- package/esm/in-memory.js +139 -167
- package/esm/index.d.ts +8 -8
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +4 -4
- package/package.json +1 -1
package/esm/eventlog.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EventLogAppendFailed, EventLogCloseFailed, EventLogCommitCursorFailed, EventLogConnectionFailed,
|
|
1
|
+
import { Channel, Task } from 'anabranch';
|
|
2
|
+
import { EventLogAppendFailed, EventLogCloseFailed, EventLogCommitCursorFailed, EventLogConnectionFailed, EventLogConsumeFailed, EventLogGetCursorFailed, } from './errors.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Event log wrapper with Task/Stream semantics for event-sourced systems.
|
|
5
|
+
*
|
|
6
|
+
* Provides high-level methods for appending events, consuming streams,
|
|
7
|
+
* and managing cursors. All operations return Tasks for composable error
|
|
8
|
+
* handling.
|
|
5
9
|
*
|
|
6
10
|
* @example Basic usage
|
|
7
11
|
* ```ts
|
|
@@ -11,30 +15,29 @@ import { EventLogAppendFailed, EventLogCloseFailed, EventLogCommitCursorFailed,
|
|
|
11
15
|
* const log = await EventLog.connect(connector).run();
|
|
12
16
|
*
|
|
13
17
|
* // Append an event
|
|
14
|
-
* const eventId = await log.append("users", {
|
|
15
|
-
*
|
|
16
|
-
* // Get a specific event
|
|
17
|
-
* const event = await log.get("users", 0).run();
|
|
18
|
-
*
|
|
19
|
-
* // List events
|
|
20
|
-
* const events = await log.list("users").run();
|
|
18
|
+
* const eventId = await log.append("users", { userId: 123 }).run();
|
|
21
19
|
*
|
|
22
|
-
*
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @example Consuming events as a stream
|
|
26
|
-
* ```ts
|
|
20
|
+
* // Consume events as a stream
|
|
27
21
|
* const { successes, errors } = await log
|
|
28
|
-
* .consume("users", "my-
|
|
22
|
+
* .consume("users", "my-processor")
|
|
29
23
|
* .withConcurrency(5)
|
|
30
24
|
* .map(async (batch) => {
|
|
31
25
|
* for (const event of batch.events) {
|
|
32
|
-
* await
|
|
26
|
+
* await processEvent(event.data);
|
|
33
27
|
* }
|
|
34
|
-
* // Explicitly commit after successful processing!
|
|
35
|
-
* await log.commit(batch.topic, batch.consumerGroup, batch.cursor).run();
|
|
36
28
|
* })
|
|
37
29
|
* .partition();
|
|
30
|
+
*
|
|
31
|
+
* await log.close().run();
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Manual cursor management
|
|
35
|
+
* ```ts
|
|
36
|
+
* // Get current cursor position
|
|
37
|
+
* const cursor = await log.getCommittedCursor("users", "my-processor").run();
|
|
38
|
+
*
|
|
39
|
+
* // Save cursor after processing
|
|
40
|
+
* await log.commit("users", "my-processor", batch.cursor).run();
|
|
38
41
|
* ```
|
|
39
42
|
*/
|
|
40
43
|
export class EventLog {
|
|
@@ -55,17 +58,15 @@ export class EventLog {
|
|
|
55
58
|
* ```
|
|
56
59
|
*/
|
|
57
60
|
static connect(connector) {
|
|
58
|
-
return Task.of(async () =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
throw new EventLogConnectionFailed(error instanceof Error ? error.message : String(error), error);
|
|
64
|
-
}
|
|
61
|
+
return Task.of(async () => new EventLog(await connector.connect()))
|
|
62
|
+
.mapErr((error) => {
|
|
63
|
+
return new EventLogConnectionFailed(error instanceof Error ? error.message : String(error), error);
|
|
65
64
|
});
|
|
66
65
|
}
|
|
67
66
|
/**
|
|
68
|
-
*
|
|
67
|
+
* Close the event log connection.
|
|
68
|
+
*
|
|
69
|
+
* After closing, no further operations can be performed on this instance.
|
|
69
70
|
*
|
|
70
71
|
* @example
|
|
71
72
|
* ```ts
|
|
@@ -73,178 +74,134 @@ export class EventLog {
|
|
|
73
74
|
* ```
|
|
74
75
|
*/
|
|
75
76
|
close() {
|
|
76
|
-
return Task.of(async () => {
|
|
77
|
-
|
|
78
|
-
await this.adapter.close();
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
throw new EventLogCloseFailed(error instanceof Error ? error.message : String(error), error);
|
|
82
|
-
}
|
|
77
|
+
return Task.of(async () => await this.adapter.close()).mapErr((error) => {
|
|
78
|
+
return new EventLogCloseFailed(error instanceof Error ? error.message : String(error), error);
|
|
83
79
|
});
|
|
84
80
|
}
|
|
85
81
|
/**
|
|
86
82
|
* Append an event to a topic.
|
|
87
83
|
*
|
|
88
|
-
*
|
|
89
|
-
* ```ts
|
|
90
|
-
* const eventId = await log.append("users", { action: "created", userId: 123 }).run();
|
|
91
|
-
* ```
|
|
84
|
+
* Returns the event ID which can be used for logging or correlation.
|
|
92
85
|
*
|
|
93
|
-
* @example
|
|
86
|
+
* @example
|
|
94
87
|
* ```ts
|
|
95
|
-
* const eventId = await log.append("
|
|
96
|
-
*
|
|
88
|
+
* const eventId = await log.append("users", { action: "created" }).run();
|
|
89
|
+
*
|
|
90
|
+
* // With options
|
|
91
|
+
* await log.append("orders", order, {
|
|
92
|
+
* partitionKey: order.userId,
|
|
93
|
+
* metadata: { source: "checkout" },
|
|
97
94
|
* }).run();
|
|
98
95
|
* ```
|
|
99
96
|
*/
|
|
100
97
|
append(topic, data, options) {
|
|
101
|
-
return Task.of(async () =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
throw new EventLogAppendFailed(topic, error instanceof Error ? error.message : String(error), error);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Get a single event by topic and sequence number.
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* ```ts
|
|
115
|
-
* const event = await log.get("users", 0).run();
|
|
116
|
-
* if (event) {
|
|
117
|
-
* console.log(event.data);
|
|
118
|
-
* }
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
get(topic, sequenceNumber) {
|
|
122
|
-
return Task.of(async () => {
|
|
123
|
-
try {
|
|
124
|
-
return await this.adapter.get(topic, sequenceNumber);
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
throw new EventLogGetFailed(topic, sequenceNumber, error instanceof Error ? error.message : String(error), error);
|
|
128
|
-
}
|
|
98
|
+
return Task.of(async () => await this.adapter.append(topic, data, options))
|
|
99
|
+
.mapErr((error) => {
|
|
100
|
+
return new EventLogAppendFailed(topic, error instanceof Error ? error.message : String(error), error);
|
|
129
101
|
});
|
|
130
102
|
}
|
|
131
103
|
/**
|
|
132
|
-
*
|
|
104
|
+
* Consume events from a topic as a stream.
|
|
133
105
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* ```
|
|
106
|
+
* Returns a Channel that yields batches of events. Each batch includes
|
|
107
|
+
* a cursor that can be committed to mark progress. Use stream methods
|
|
108
|
+
* like `withConcurrency()`, `map()`, and `partition()` for processing.
|
|
138
109
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* fromSequenceNumber: 100,
|
|
143
|
-
* limit: 50,
|
|
144
|
-
* }).run();
|
|
145
|
-
* ```
|
|
110
|
+
* Batches are delivered asynchronously as they become available. Use
|
|
111
|
+
* `take()` to limit iterations or pass an AbortSignal in options to
|
|
112
|
+
* cancel consumption.
|
|
146
113
|
*
|
|
147
|
-
* @example
|
|
114
|
+
* @example
|
|
148
115
|
* ```ts
|
|
149
|
-
* const
|
|
150
|
-
* partitionKey: "user-123",
|
|
151
|
-
* }).run();
|
|
152
|
-
* ```
|
|
153
|
-
*/
|
|
154
|
-
list(topic, options) {
|
|
155
|
-
return Task.of(async () => {
|
|
156
|
-
try {
|
|
157
|
-
return await this.adapter.list(topic, options);
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
throw new EventLogListFailed(topic, error instanceof Error ? error.message : String(error), error);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Consume events from a topic as a Source for streaming.
|
|
166
|
-
*
|
|
167
|
-
* Note: You must manually commit the cursor after processing to guarantee
|
|
168
|
-
* at-least-once delivery. Auto-commit is intentionally omitted to prevent
|
|
169
|
-
* data loss when using concurrent processing.
|
|
116
|
+
* const ac = new AbortController();
|
|
170
117
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* const { successes, errors } = await log
|
|
174
|
-
* .consume("users", "processor-1")
|
|
175
|
-
* .withConcurrency(5)
|
|
118
|
+
* await log.consume("users", "processor-1", { signal: ac.signal })
|
|
119
|
+
* .withConcurrency(10)
|
|
176
120
|
* .map(async (batch) => {
|
|
177
121
|
* for (const event of batch.events) {
|
|
178
|
-
* await
|
|
122
|
+
* await processUser(event.data);
|
|
179
123
|
* }
|
|
180
|
-
* await
|
|
124
|
+
* await batch.commit(); // Mark progress
|
|
181
125
|
* })
|
|
182
126
|
* .partition();
|
|
127
|
+
*
|
|
128
|
+
* ac.abort(); // Stop consumption
|
|
183
129
|
* ```
|
|
184
130
|
*
|
|
185
|
-
* @example
|
|
131
|
+
* @example Resume from a saved cursor
|
|
186
132
|
* ```ts
|
|
187
|
-
* const
|
|
188
|
-
* const
|
|
189
|
-
* .consume("users", "processor-1", { cursor: lastCursor })
|
|
190
|
-
* .tap(async (batch) => {
|
|
191
|
-
* for (const event of batch.events) {
|
|
192
|
-
* console.log(event);
|
|
193
|
-
* }
|
|
194
|
-
* })
|
|
195
|
-
* .partition();
|
|
133
|
+
* const cursor = await log.getCommittedCursor("users", "processor-1").run();
|
|
134
|
+
* const stream = log.consume("users", "processor-1", { cursor });
|
|
196
135
|
* ```
|
|
197
136
|
*/
|
|
198
137
|
consume(topic, consumerGroup, options) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
138
|
+
if (options?.bufferSize !== undefined) {
|
|
139
|
+
if (options.bufferSize <= 0 || !Number.isInteger(options.bufferSize)) {
|
|
140
|
+
throw new Error('bufferSize must be a positive integer');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (options?.batchSize !== undefined) {
|
|
144
|
+
if (options.batchSize <= 0 || !Number.isInteger(options.batchSize)) {
|
|
145
|
+
throw new Error('batchSize must be a positive integer');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const channel = new Channel({
|
|
149
|
+
onDrop: (batch) => {
|
|
150
|
+
channel.fail(new EventLogConsumeFailed(topic, consumerGroup, `Batch dropped due to full buffer (events ${batch.events
|
|
151
|
+
.map((e) => e.id)
|
|
152
|
+
.join(',')})`));
|
|
153
|
+
},
|
|
154
|
+
onClose: () => close(),
|
|
155
|
+
bufferSize: options?.bufferSize ?? Infinity,
|
|
156
|
+
signal: options?.signal,
|
|
209
157
|
});
|
|
158
|
+
const { close } = this.adapter.consume(topic, consumerGroup, async (batch) => {
|
|
159
|
+
await channel.waitForCapacity();
|
|
160
|
+
channel.send(batch);
|
|
161
|
+
}, (error) => {
|
|
162
|
+
channel.fail(new EventLogConsumeFailed(topic, consumerGroup, error instanceof Error ? error.message : String(error)));
|
|
163
|
+
}, options);
|
|
164
|
+
return channel;
|
|
210
165
|
}
|
|
211
166
|
/**
|
|
212
|
-
* Commit a cursor
|
|
167
|
+
* Commit a cursor to mark progress for a consumer group.
|
|
168
|
+
*
|
|
169
|
+
* This is for administrative use cases where you can't commit in-band, preferably when you're
|
|
170
|
+
* not actively consuming events. For example, you might want to skip ahead after a downtime or reset to the beginning for reprocessing.
|
|
171
|
+
* Do prefer to commit in-band, i.e. after processing each batch, by calling `batch.commit()`.
|
|
172
|
+
*
|
|
173
|
+
* After processing events, commit the cursor to resume from that position
|
|
174
|
+
* on the next run. Cursors are obtained from `batch.cursor` in the consume
|
|
175
|
+
* stream or from `getCommittedCursor()`.
|
|
213
176
|
*
|
|
214
177
|
* @example
|
|
215
178
|
* ```ts
|
|
216
|
-
* await log.commit("users", "processor-1", cursor).run();
|
|
179
|
+
* await log.commit("users", "processor-1", batch.cursor).run();
|
|
217
180
|
* ```
|
|
218
181
|
*/
|
|
219
182
|
commit(topic, consumerGroup, cursor) {
|
|
220
|
-
return Task.of(async () => {
|
|
221
|
-
|
|
222
|
-
await this.adapter.commitCursor(topic, consumerGroup, cursor);
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
throw new EventLogCommitCursorFailed(topic, consumerGroup, error instanceof Error ? error.message : String(error), error);
|
|
226
|
-
}
|
|
183
|
+
return Task.of(async () => await this.adapter.commitCursor(topic, consumerGroup, cursor)).mapErr((error) => {
|
|
184
|
+
return new EventLogCommitCursorFailed(topic, consumerGroup, error instanceof Error ? error.message : String(error), error);
|
|
227
185
|
});
|
|
228
186
|
}
|
|
229
187
|
/**
|
|
230
|
-
* Get the committed cursor
|
|
188
|
+
* Get the last committed cursor for a consumer group.
|
|
189
|
+
*
|
|
190
|
+
* Returns null if no cursor has been committed yet. Use this to resume
|
|
191
|
+
* consumption from the last processed position.
|
|
231
192
|
*
|
|
232
193
|
* @example
|
|
233
194
|
* ```ts
|
|
234
195
|
* const cursor = await log.getCommittedCursor("users", "processor-1").run();
|
|
235
196
|
* if (cursor) {
|
|
236
|
-
*
|
|
197
|
+
* // Resume from saved position
|
|
198
|
+
* const stream = log.consume("users", "processor-1", { cursor });
|
|
237
199
|
* }
|
|
238
200
|
* ```
|
|
239
201
|
*/
|
|
240
202
|
getCommittedCursor(topic, consumerGroup) {
|
|
241
|
-
return Task.of(async () => {
|
|
242
|
-
|
|
243
|
-
return await this.adapter.getCursor(topic, consumerGroup);
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
throw new EventLogGetCursorFailed(topic, consumerGroup, error instanceof Error ? error.message : String(error), error);
|
|
247
|
-
}
|
|
203
|
+
return Task.of(async () => await this.adapter.getCursor(topic, consumerGroup)).mapErr((error) => {
|
|
204
|
+
return new EventLogGetCursorFailed(topic, consumerGroup, error instanceof Error ? error.message : String(error), error);
|
|
248
205
|
});
|
|
249
206
|
}
|
|
250
207
|
}
|
package/esm/in-memory.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { EventLogAdapter, EventLogConnector, EventLogOptions } from
|
|
1
|
+
import type { EventLogAdapter, EventLogConnector, EventLogOptions } from './adapter.js';
|
|
2
|
+
export declare function createInMemory(options?: InMemoryOptions): InMemoryConnector;
|
|
3
|
+
/** Configuration options for in-memory event log. */
|
|
4
|
+
export interface InMemoryOptions extends EventLogOptions {
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
|
-
* Creates an in-memory event log connector
|
|
7
|
+
* Creates an in-memory event log connector for testing and development.
|
|
4
8
|
*
|
|
5
|
-
* Events are stored in memory
|
|
6
|
-
*
|
|
9
|
+
* Events are stored in memory and lost when the process exits. Ideal for
|
|
10
|
+
* unit tests, prototyping, and development environments.
|
|
7
11
|
*
|
|
8
12
|
* @example Basic usage
|
|
9
13
|
* ```ts
|
|
@@ -12,42 +16,19 @@ import type { EventLogAdapter, EventLogConnector, EventLogOptions } from "./adap
|
|
|
12
16
|
* const connector = createInMemory();
|
|
13
17
|
* const log = await EventLog.connect(connector).run();
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
* const eventId = await log.append("users", { action: "created", userId: 123 }).run();
|
|
19
|
+
* await log.append("users", { userId: 123 }).run();
|
|
17
20
|
*
|
|
18
|
-
* //
|
|
19
|
-
*
|
|
21
|
+
* // After testing, clean up
|
|
22
|
+
* await connector.end();
|
|
20
23
|
* ```
|
|
21
24
|
*
|
|
22
|
-
* @example With partition key
|
|
25
|
+
* @example With custom partition key
|
|
23
26
|
* ```ts
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* })
|
|
27
|
-
* ```
|
|
28
|
-
*
|
|
29
|
-
* @example Consuming events
|
|
30
|
-
* ```ts
|
|
31
|
-
* const connector = createInMemory();
|
|
32
|
-
* const log = await EventLog.connect(connector).run();
|
|
33
|
-
*
|
|
34
|
-
* // Append some events first
|
|
35
|
-
* await log.append("notifications", { type: "email" }).run();
|
|
36
|
-
* await log.append("notifications", { type: "sms" }).run();
|
|
37
|
-
*
|
|
38
|
-
* // Consume events
|
|
39
|
-
* for await (const batch of log.consume("notifications", "my-consumer-group")) {
|
|
40
|
-
* for (const event of batch.events) {
|
|
41
|
-
* console.log(event.data);
|
|
42
|
-
* }
|
|
43
|
-
* }
|
|
27
|
+
* const connector = createInMemory({
|
|
28
|
+
* defaultPartitionKey: "user-events",
|
|
29
|
+
* });
|
|
44
30
|
* ```
|
|
45
31
|
*/
|
|
46
|
-
export declare function createInMemory(options?: InMemoryOptions): InMemoryConnector;
|
|
47
|
-
/** In-memory event log connector options. */
|
|
48
|
-
export interface InMemoryOptions extends EventLogOptions {
|
|
49
|
-
}
|
|
50
|
-
/** In-memory event log connector. */
|
|
51
32
|
export interface InMemoryConnector extends EventLogConnector {
|
|
52
33
|
connect(): Promise<EventLogAdapter>;
|
|
53
34
|
end(): Promise<void>;
|
package/esm/in-memory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../src/in-memory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../src/in-memory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAKV,eAAe,EACf,iBAAiB,EACjB,eAAe,EAChB,MAAM,cAAc,CAAA;AAsBrB,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,iBAAiB,CA4Q3E;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAgB,SAAQ,eAAe;CAAG;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAA;IACnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACrB"}
|