@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/esm/eventlog.js CHANGED
@@ -1,7 +1,11 @@
1
- import { Source, Task } from "anabranch";
2
- import { EventLogAppendFailed, EventLogCloseFailed, EventLogCommitCursorFailed, EventLogConnectionFailed, EventLogGetCursorFailed, EventLogGetFailed, EventLogListFailed, } from "./errors.js";
1
+ import { Channel, Task } from 'anabranch';
2
+ import { EventLogAppendFailed, EventLogCloseFailed, EventLogCommitCursorFailed, EventLogConnectionFailed, EventLogConsumeFailed, EventLogGetCursorFailed, } from './errors.js';
3
3
  /**
4
- * EventLog wrapper with Task/Stream semantics for event-sourced systems.
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", { action: "created", userId: 123 }).run();
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
- * await log.close().run();
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-consumer-group")
22
+ * .consume("users", "my-processor")
29
23
  * .withConcurrency(5)
30
24
  * .map(async (batch) => {
31
25
  * for (const event of batch.events) {
32
- * await handleEvent(event.data);
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
- try {
60
- return new EventLog(await connector.connect());
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
- * Release the connection back to its source.
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
- try {
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
- * @example Basic append
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 With partition key
86
+ * @example
94
87
  * ```ts
95
- * const eventId = await log.append("orders", orderData, {
96
- * partitionKey: orderData.userId,
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
- try {
103
- return await this.adapter.append(topic, data, options);
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
- * List events in a topic with optional filtering and pagination.
104
+ * Consume events from a topic as a stream.
133
105
  *
134
- * @example List all events
135
- * ```ts
136
- * const events = await log.list("users").run();
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
- * @example With pagination
140
- * ```ts
141
- * const events = await log.list("users", {
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 Filtered by partition key
114
+ * @example
148
115
  * ```ts
149
- * const events = await log.list("orders", {
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
- * @example Basic consumption
172
- * ```ts
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 handleEvent(event.data);
122
+ * await processUser(event.data);
179
123
  * }
180
- * await log.commit(batch.topic, batch.consumerGroup, batch.cursor).run();
124
+ * await batch.commit(); // Mark progress
181
125
  * })
182
126
  * .partition();
127
+ *
128
+ * ac.abort(); // Stop consumption
183
129
  * ```
184
130
  *
185
- * @example From specific cursor position
131
+ * @example Resume from a saved cursor
186
132
  * ```ts
187
- * const lastCursor = await log.getCommittedCursor("users", "processor-1").run();
188
- * const { successes } = await log
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
- const adapter = this.adapter;
200
- return Source.from(async function* () {
201
- try {
202
- for await (const batch of adapter.consume(topic, consumerGroup, options)) {
203
- yield batch;
204
- }
205
- }
206
- catch (error) {
207
- throw new EventLogListFailed(topic, error instanceof Error ? error.message : String(error), error);
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 position for a consumer group.
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
- try {
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 position for a consumer group.
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
- * console.log(`Resuming from cursor: ${cursor}`);
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
- try {
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
  }
@@ -1,9 +1,13 @@
1
- import type { EventLogAdapter, EventLogConnector, EventLogOptions } from "./adapter.js";
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 using a simple event store.
7
+ * Creates an in-memory event log connector for testing and development.
4
8
  *
5
- * Events are stored in memory only and will be lost on process restart.
6
- * Useful for testing and development.
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
- * // Append an event
16
- * const eventId = await log.append("users", { action: "created", userId: 123 }).run();
19
+ * await log.append("users", { userId: 123 }).run();
17
20
  *
18
- * // List events
19
- * const events = await log.list("users").run();
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
- * await log.append("orders", { orderId: 456, total: 99.99 }, {
25
- * partitionKey: "user-123"
26
- * }).run();
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>;
@@ -1 +1 @@
1
- {"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../src/in-memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKV,eAAe,EACf,iBAAiB,EACjB,eAAe,EAEhB,MAAM,cAAc,CAAC;AAsBtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,iBAAiB,CA0O3E;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAgB,SAAQ,eAAe;CAAG;AAE3D,qCAAqC;AACrC,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB"}
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"}