@anabranch/eventlog 0.1.2 → 0.2.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/in-memory.js CHANGED
@@ -1,57 +1,14 @@
1
- import { EventLogAppendFailed, EventLogCommitCursorFailed, EventLogGetCursorFailed, EventLogGetFailed, EventLogListFailed, } from "./errors.js";
1
+ import { EventLogAppendFailed, EventLogCommitCursorFailed, EventLogConnectionFailed, EventLogConsumeFailed, EventLogGetCursorFailed, } from './errors.js';
2
2
  function generateId() {
3
3
  return crypto.randomUUID();
4
4
  }
5
- /**
6
- * Creates an in-memory event log connector using a simple event store.
7
- *
8
- * Events are stored in memory only and will be lost on process restart.
9
- * Useful for testing and development.
10
- *
11
- * @example Basic usage
12
- * ```ts
13
- * import { EventLog, createInMemory } from "@anabranch/eventlog";
14
- *
15
- * const connector = createInMemory();
16
- * const log = await EventLog.connect(connector).run();
17
- *
18
- * // Append an event
19
- * const eventId = await log.append("users", { action: "created", userId: 123 }).run();
20
- *
21
- * // List events
22
- * const events = await log.list("users").run();
23
- * ```
24
- *
25
- * @example With partition key
26
- * ```ts
27
- * await log.append("orders", { orderId: 456, total: 99.99 }, {
28
- * partitionKey: "user-123"
29
- * }).run();
30
- * ```
31
- *
32
- * @example Consuming events
33
- * ```ts
34
- * const connector = createInMemory();
35
- * const log = await EventLog.connect(connector).run();
36
- *
37
- * // Append some events first
38
- * await log.append("notifications", { type: "email" }).run();
39
- * await log.append("notifications", { type: "sms" }).run();
40
- *
41
- * // Consume events
42
- * for await (const batch of log.consume("notifications", "my-consumer-group")) {
43
- * for (const event of batch.events) {
44
- * console.log(event.data);
45
- * }
46
- * }
47
- * ```
48
- */
49
5
  export function createInMemory(options) {
50
6
  const topics = new Map();
51
7
  const consumerGroups = new Map();
8
+ const listeners = new Set();
52
9
  let ended = false;
53
10
  const defaultOptions = {
54
- defaultPartitionKey: "default",
11
+ defaultPartitionKey: 'default',
55
12
  };
56
13
  const opts = {
57
14
  ...defaultOptions,
@@ -68,141 +25,156 @@ export function createInMemory(options) {
68
25
  }
69
26
  return state;
70
27
  };
71
- const adapter = {
72
- append(topic, data, appendOptions) {
73
- if (ended) {
74
- return Promise.reject(new EventLogAppendFailed(topic, "Connector ended"));
75
- }
76
- const state = getOrCreateTopic(topic);
77
- const id = generateId();
78
- const sequenceNumber = state.nextSequenceNumber++;
79
- const partitionKey = appendOptions?.partitionKey ??
80
- opts.defaultPartitionKey;
81
- const event = {
82
- id,
83
- topic,
84
- data,
85
- partitionKey,
86
- sequenceNumber,
87
- timestamp: appendOptions?.timestamp ?? Date.now(),
88
- metadata: appendOptions?.metadata,
89
- };
90
- state.events.push(event);
91
- return Promise.resolve(id);
92
- },
93
- get(topic, sequenceNumber) {
94
- if (ended) {
95
- return Promise.reject(new EventLogGetFailed(topic, sequenceNumber, "Connector ended"));
96
- }
97
- const state = topics.get(topic);
98
- if (!state) {
99
- return Promise.resolve(null);
100
- }
101
- const event = state.events[sequenceNumber];
102
- return Promise.resolve(event ?? null);
103
- },
104
- list(topic, listOptions) {
105
- if (ended) {
106
- return Promise.reject(new EventLogListFailed(topic, "Connector ended"));
107
- }
108
- const state = topics.get(topic);
109
- if (!state) {
110
- return Promise.resolve([]);
111
- }
112
- let events = state.events;
113
- if (listOptions?.fromSequenceNumber !== undefined) {
114
- events = events.filter((e) => e.sequenceNumber >= listOptions.fromSequenceNumber);
115
- }
116
- if (listOptions?.partitionKey !== undefined) {
117
- events = events.filter((e) => e.partitionKey === listOptions.partitionKey);
118
- }
119
- if (listOptions?.limit !== undefined) {
120
- events = events.slice(0, listOptions.limit);
121
- }
122
- return Promise.resolve(events);
123
- },
124
- async *consume(topic, consumerGroup, consumeOptions) {
125
- if (ended) {
126
- throw new Error("Connector ended");
127
- }
128
- let state = topics.get(topic);
129
- if (!state) {
130
- state = {
131
- events: [],
132
- nextSequenceNumber: 0,
133
- };
134
- topics.set(topic, state);
135
- }
136
- let consumerState = consumerGroups.get(consumerGroup);
137
- if (!consumerState) {
138
- consumerState = { cursors: new Map() };
139
- consumerGroups.set(consumerGroup, consumerState);
140
- }
141
- const batchSize = consumeOptions?.batchSize ?? 10;
142
- const startSequence = consumeOptions?.cursor
143
- ? parseInt(consumeOptions.cursor, 10) + 1
144
- : 0;
145
- const signal = consumeOptions?.signal;
146
- let currentSequence = startSequence;
147
- while (!signal?.aborted) {
148
- const events = [];
149
- for (let i = 0; i < batchSize; i++) {
150
- const event = state.events[currentSequence];
151
- if (!event)
152
- break;
153
- events.push(event);
154
- currentSequence++;
28
+ const createAdapter = () => {
29
+ let closed = false;
30
+ return {
31
+ append(topic, data, appendOptions) {
32
+ if (ended) {
33
+ return Promise.reject(new EventLogAppendFailed(topic, 'Connector ended'));
155
34
  }
156
- if (events.length === 0) {
157
- await new Promise((resolve) => setTimeout(resolve, 100));
158
- continue;
35
+ if (closed) {
36
+ return Promise.reject(new EventLogAppendFailed(topic, 'Adapter closed'));
159
37
  }
160
- const cursor = String(currentSequence - 1);
161
- yield {
38
+ const state = getOrCreateTopic(topic);
39
+ const id = generateId();
40
+ const sequenceNumber = state.nextSequenceNumber++;
41
+ const partitionKey = appendOptions?.partitionKey ??
42
+ opts.defaultPartitionKey;
43
+ const event = {
44
+ id,
162
45
  topic,
163
- consumerGroup,
164
- events,
165
- cursor,
46
+ data,
47
+ partitionKey,
48
+ sequenceNumber: String(sequenceNumber),
49
+ timestamp: appendOptions?.timestamp ?? Date.now(),
50
+ metadata: appendOptions?.metadata,
166
51
  };
167
- }
168
- },
169
- commitCursor(topic, consumerGroup, cursor) {
170
- if (ended) {
171
- return Promise.reject(new EventLogCommitCursorFailed(topic, consumerGroup, "Connector ended"));
172
- }
173
- let state = consumerGroups.get(consumerGroup);
174
- if (!state) {
175
- state = { cursors: new Map() };
176
- consumerGroups.set(consumerGroup, state);
177
- }
178
- state.cursors.set(topic, cursor);
179
- return Promise.resolve();
180
- },
181
- getCursor(topic, consumerGroup) {
182
- if (ended) {
183
- return Promise.reject(new EventLogGetCursorFailed(topic, consumerGroup, "Connector ended"));
184
- }
185
- const state = consumerGroups.get(consumerGroup);
186
- if (!state) {
187
- return Promise.resolve(null);
188
- }
189
- return Promise.resolve(state.cursors.get(topic) ?? null);
190
- },
191
- close() {
192
- return Promise.resolve();
193
- },
52
+ state.events.push(event);
53
+ for (const listener of listeners) {
54
+ if (listener.topic === topic) {
55
+ listener.push();
56
+ }
57
+ }
58
+ return Promise.resolve(id);
59
+ },
60
+ consume(topic, consumerGroup, onBatch, onError, consumeOptions) {
61
+ if (ended) {
62
+ throw new EventLogConsumeFailed(topic, 'Connector ended');
63
+ }
64
+ if (closed) {
65
+ throw new EventLogConsumeFailed(topic, 'Adapter closed');
66
+ }
67
+ let consumerState = consumerGroups.get(consumerGroup);
68
+ if (!consumerState) {
69
+ consumerState = { cursors: new Map() };
70
+ consumerGroups.set(consumerGroup, consumerState);
71
+ }
72
+ const batchSize = consumeOptions?.batchSize ?? 10;
73
+ const signal = consumeOptions?.signal;
74
+ let currentSequence = consumeOptions?.cursor
75
+ ? parseInt(consumeOptions.cursor, 10) + 1
76
+ : 0;
77
+ let consumerClosed = false;
78
+ const drive = async () => {
79
+ if (consumerClosed || closed || ended)
80
+ return;
81
+ const state = topics.get(topic);
82
+ if (!state)
83
+ return;
84
+ const events = [];
85
+ while (events.length < batchSize && currentSequence < state.events.length) {
86
+ events.push(state.events[currentSequence]);
87
+ currentSequence++;
88
+ }
89
+ if (events.length > 0) {
90
+ try {
91
+ const cursor = String(currentSequence - 1);
92
+ await onBatch({
93
+ topic,
94
+ consumerGroup,
95
+ events,
96
+ cursor,
97
+ commit: () => {
98
+ if (ended || closed) {
99
+ throw new EventLogCommitCursorFailed(topic, consumerGroup, ended ? 'Connector ended' : 'Adapter closed');
100
+ }
101
+ consumerState.cursors.set(topic, cursor);
102
+ return Promise.resolve();
103
+ },
104
+ });
105
+ if (currentSequence < state.events.length &&
106
+ !consumerClosed &&
107
+ !closed &&
108
+ !ended) {
109
+ void drive();
110
+ }
111
+ }
112
+ catch (error) {
113
+ await onError(new EventLogConsumeFailed(topic, error instanceof Error ? error.message : String(error), error));
114
+ }
115
+ }
116
+ };
117
+ const listener = { topic, push: drive };
118
+ listeners.add(listener);
119
+ drive();
120
+ const closeConsumer = () => {
121
+ if (consumerClosed)
122
+ return Promise.resolve();
123
+ consumerClosed = true;
124
+ listeners.delete(listener);
125
+ return Promise.resolve();
126
+ };
127
+ signal?.addEventListener('abort', closeConsumer, { once: true });
128
+ return { close: closeConsumer };
129
+ },
130
+ getCursor(topic, consumerGroup) {
131
+ if (ended) {
132
+ return Promise.reject(new EventLogGetCursorFailed(topic, consumerGroup, 'Connector ended'));
133
+ }
134
+ if (closed) {
135
+ return Promise.reject(new EventLogGetCursorFailed(topic, consumerGroup, 'Adapter closed'));
136
+ }
137
+ const state = consumerGroups.get(consumerGroup);
138
+ if (!state) {
139
+ return Promise.resolve(null);
140
+ }
141
+ return Promise.resolve(state.cursors.get(topic) ?? null);
142
+ },
143
+ commitCursor(topic, consumerGroup, cursor) {
144
+ if (ended) {
145
+ return Promise.reject(new EventLogCommitCursorFailed(topic, consumerGroup, 'Connector ended'));
146
+ }
147
+ if (closed) {
148
+ return Promise.reject(new EventLogCommitCursorFailed(topic, consumerGroup, 'Adapter closed'));
149
+ }
150
+ let consumerState = consumerGroups.get(consumerGroup);
151
+ if (!consumerState) {
152
+ consumerState = { cursors: new Map() };
153
+ consumerGroups.set(consumerGroup, consumerState);
154
+ }
155
+ consumerState.cursors.set(topic, cursor);
156
+ return Promise.resolve();
157
+ },
158
+ close() {
159
+ if (closed)
160
+ return Promise.resolve();
161
+ closed = true;
162
+ return Promise.resolve();
163
+ },
164
+ };
194
165
  };
195
166
  return {
196
167
  connect() {
197
168
  if (ended) {
198
- return Promise.reject(new Error("Connector ended"));
169
+ return Promise.reject(new EventLogConnectionFailed('Connector ended'));
199
170
  }
200
- return Promise.resolve(adapter);
171
+ return Promise.resolve(createAdapter());
201
172
  },
202
173
  end() {
203
174
  ended = true;
204
175
  topics.clear();
205
176
  consumerGroups.clear();
177
+ listeners.clear();
206
178
  return Promise.resolve();
207
179
  },
208
180
  };
package/esm/index.d.ts CHANGED
@@ -61,12 +61,12 @@
61
61
  *
62
62
  * @module
63
63
  */
64
- export { EventLog } from "./eventlog.js";
65
- export type { Event, EventBatch, EventLogAdapter, EventLogConnector, } from "./adapter.js";
66
- export type { AppendOptions, ConsumeOptions, EventLogOptions, ListOptions, } from "./adapter.js";
67
- export * from "./errors.js";
68
- export { createInMemory } from "./in-memory.js";
69
- export type { InMemoryConnector, InMemoryOptions } from "./in-memory.js";
70
- export { Task } from "anabranch";
71
- export type { Source, Stream } from "anabranch";
64
+ export { EventLog } from './eventlog.js';
65
+ export type { Event, EventBatch, EventLogAdapter, EventLogConnector, } from './adapter.js';
66
+ export type { AppendOptions, ConsumeOptions, EventLogOptions, } from './adapter.js';
67
+ export * from './errors.js';
68
+ export { createInMemory } from './in-memory.js';
69
+ export type { InMemoryConnector, InMemoryOptions } from './in-memory.js';
70
+ export { Task } from 'anabranch';
71
+ export type { Source, Stream } from 'anabranch';
72
72
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,KAAK,EACL,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,YAAY,EACV,KAAK,EACL,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,cAAc,CAAA;AACrB,cAAc,aAAa,CAAA;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA"}
package/esm/index.js CHANGED
@@ -61,7 +61,7 @@
61
61
  *
62
62
  * @module
63
63
  */
64
- export { EventLog } from "./eventlog.js";
65
- export * from "./errors.js";
66
- export { createInMemory } from "./in-memory.js";
67
- export { Task } from "anabranch";
64
+ export { EventLog } from './eventlog.js';
65
+ export * from './errors.js';
66
+ export { createInMemory } from './in-memory.js';
67
+ export { Task } from 'anabranch';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anabranch/eventlog",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Event log with Task/Stream semantics. In-memory adapter for event-sourced systems with cursor-based consumption.",
5
5
  "repository": {
6
6
  "type": "git",