@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/README.md +40 -106
- 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 +93 -113
- 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/in-memory.js
CHANGED
|
@@ -1,57 +1,14 @@
|
|
|
1
|
-
import { EventLogAppendFailed, EventLogCommitCursorFailed,
|
|
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:
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 (
|
|
157
|
-
|
|
158
|
-
continue;
|
|
35
|
+
if (closed) {
|
|
36
|
+
return Promise.reject(new EventLogAppendFailed(topic, 'Adapter closed'));
|
|
159
37
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
46
|
+
data,
|
|
47
|
+
partitionKey,
|
|
48
|
+
sequenceNumber: String(sequenceNumber),
|
|
49
|
+
timestamp: appendOptions?.timestamp ?? Date.now(),
|
|
50
|
+
metadata: appendOptions?.metadata,
|
|
166
51
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
169
|
+
return Promise.reject(new EventLogConnectionFailed('Connector ended'));
|
|
199
170
|
}
|
|
200
|
-
return Promise.resolve(
|
|
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
|
|
65
|
-
export type { Event, EventBatch, EventLogAdapter, EventLogConnector, } from
|
|
66
|
-
export type { AppendOptions, ConsumeOptions, EventLogOptions,
|
|
67
|
-
export * from
|
|
68
|
-
export { createInMemory } from
|
|
69
|
-
export type { InMemoryConnector, InMemoryOptions } from
|
|
70
|
-
export { Task } from
|
|
71
|
-
export type { Source, Stream } from
|
|
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
|
package/esm/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
65
|
-
export * from
|
|
66
|
-
export { createInMemory } from
|
|
67
|
-
export { Task } from
|
|
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