@emmett-community/emmett-google-firestore 0.3.0 → 0.5.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 +23 -10
- package/dist/index.d.mts +50 -8
- package/dist/index.d.ts +50 -8
- package/dist/index.js +94 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +94 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,15 @@ const eventStore = getFirestoreEventStore(firestore, {
|
|
|
147
147
|
});
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
### `asEventStore(firestoreEventStore)`
|
|
151
|
+
|
|
152
|
+
Wraps a `FirestoreEventStore` instance in Emmett's `EventStore` interface so it can be passed into helpers that expect the canonical contract (CommandHandlers, workflows, etc.).
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const firestoreEventStore = getFirestoreEventStore(firestore);
|
|
156
|
+
const eventStore = asEventStore(firestoreEventStore);
|
|
157
|
+
```
|
|
158
|
+
|
|
150
159
|
### `eventStore.appendToStream(streamName, events, options?)`
|
|
151
160
|
|
|
152
161
|
Appends events to a stream.
|
|
@@ -560,16 +569,6 @@ import type {
|
|
|
560
569
|
- **Emmett**: ^0.39.0
|
|
561
570
|
- **Firestore**: ^7.10.0
|
|
562
571
|
|
|
563
|
-
## Contributing
|
|
564
|
-
|
|
565
|
-
Contributions are welcome! Please:
|
|
566
|
-
|
|
567
|
-
1. Fork the repository
|
|
568
|
-
2. Create a feature branch
|
|
569
|
-
3. Add tests for new functionality
|
|
570
|
-
4. Ensure all tests pass
|
|
571
|
-
5. Submit a pull request
|
|
572
|
-
|
|
573
572
|
## Development
|
|
574
573
|
|
|
575
574
|
```bash
|
|
@@ -589,6 +588,16 @@ npm run lint
|
|
|
589
588
|
npm run format
|
|
590
589
|
```
|
|
591
590
|
|
|
591
|
+
## Contributing
|
|
592
|
+
|
|
593
|
+
Contributions are welcome! Please:
|
|
594
|
+
|
|
595
|
+
1. Fork the repository
|
|
596
|
+
2. Create a feature branch
|
|
597
|
+
3. Add tests for new functionality
|
|
598
|
+
4. Ensure all tests pass
|
|
599
|
+
5. Submit a pull request
|
|
600
|
+
|
|
592
601
|
## License
|
|
593
602
|
|
|
594
603
|
MIT © Emmett Community
|
|
@@ -611,3 +620,7 @@ MIT © Emmett Community
|
|
|
611
620
|
- Built for the [Emmett](https://event-driven-io.github.io/emmett/) framework by [Oskar Dudycz](https://github.com/oskardudycz)
|
|
612
621
|
- Inspired by [emmett-mongodb](https://github.com/event-driven-io/emmett/tree/main/src/packages/emmett-mongodb)
|
|
613
622
|
- Part of the [Emmett Community](https://github.com/emmett-community)
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
Made with ❤️ by the Emmett Community
|
package/dist/index.d.mts
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
import { Firestore, Timestamp } from '@google-cloud/firestore';
|
|
2
|
-
import { Event, ReadEvent, ReadEventMetadataWithGlobalPosition, ExpectedStreamVersion as ExpectedStreamVersion$1, STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
|
|
2
|
+
import { Event, ReadEvent, ReadEventMetadataWithGlobalPosition, ExpectedStreamVersion as ExpectedStreamVersion$1, STREAM_DOES_NOT_EXIST, EventStore } from '@event-driven-io/emmett';
|
|
3
3
|
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Canonical Logger contract for the Emmett ecosystem.
|
|
7
|
+
*
|
|
8
|
+
* DO NOT MODIFY this interface without updating ALL packages in the ecosystem.
|
|
9
|
+
*
|
|
10
|
+
* This package defines the canonical Logger interface.
|
|
11
|
+
* Implementations (Pino, Winston, etc.) MUST adapt to this contract.
|
|
12
|
+
* This contract MUST NOT adapt to any specific implementation.
|
|
13
|
+
*
|
|
14
|
+
* Semantic Rules:
|
|
15
|
+
* - context (first parameter): ALWAYS structured data as Record<string, unknown>
|
|
16
|
+
* - message (second parameter): ALWAYS the human-readable log message
|
|
17
|
+
* - The order is NEVER inverted
|
|
18
|
+
* - The (message, data) form is NOT valid for this contract
|
|
19
|
+
* - Error objects MUST use the 'err' key (frozen semantic)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Pino - native compatibility
|
|
24
|
+
* import pino from 'pino';
|
|
25
|
+
* const logger = pino();
|
|
26
|
+
* // logger.info({ orderId }, 'Order created') matches our contract
|
|
27
|
+
* ```
|
|
8
28
|
*/
|
|
9
29
|
interface Logger {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Log debug-level message with structured context.
|
|
32
|
+
* @param context - Structured data to include in the log entry
|
|
33
|
+
* @param message - Optional human-readable message
|
|
34
|
+
*/
|
|
35
|
+
debug(context: Record<string, unknown>, message?: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Log info-level message with structured context.
|
|
38
|
+
* @param context - Structured data to include in the log entry
|
|
39
|
+
* @param message - Optional human-readable message
|
|
40
|
+
*/
|
|
41
|
+
info(context: Record<string, unknown>, message?: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Log warn-level message with structured context.
|
|
44
|
+
* @param context - Structured data to include in the log entry
|
|
45
|
+
* @param message - Optional human-readable message
|
|
46
|
+
*/
|
|
47
|
+
warn(context: Record<string, unknown>, message?: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Log error-level message with structured context.
|
|
50
|
+
* @param context - Structured data to include in the log entry (MUST use 'err' key for Error objects)
|
|
51
|
+
* @param message - Optional human-readable message
|
|
52
|
+
*/
|
|
53
|
+
error(context: Record<string, unknown>, message?: string): void;
|
|
14
54
|
}
|
|
15
55
|
/**
|
|
16
56
|
* Observability configuration options
|
|
@@ -44,6 +84,7 @@ interface AppendToStreamResult {
|
|
|
44
84
|
}
|
|
45
85
|
/**
|
|
46
86
|
* Options for reading events from a stream
|
|
87
|
+
* Reuses the canonical `ReadStreamOptions` shape from the Emmett core types.
|
|
47
88
|
*/
|
|
48
89
|
interface ReadStreamOptions {
|
|
49
90
|
from?: bigint;
|
|
@@ -138,6 +179,7 @@ declare class ExpectedVersionConflictError extends Error {
|
|
|
138
179
|
constructor(streamName: string, expected: ExpectedStreamVersion, actual: bigint | typeof STREAM_DOES_NOT_EXIST);
|
|
139
180
|
}
|
|
140
181
|
|
|
182
|
+
declare function asEventStore(store: FirestoreEventStore): EventStore<FirestoreReadEventMetadata>;
|
|
141
183
|
/**
|
|
142
184
|
* Factory function to create a Firestore event store
|
|
143
185
|
*
|
|
@@ -216,4 +258,4 @@ declare function getCurrentStreamVersion(streamExists: boolean, version?: number
|
|
|
216
258
|
*/
|
|
217
259
|
declare function calculateNextVersion(currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST, eventCount: number): bigint;
|
|
218
260
|
|
|
219
|
-
export { type AppendToStreamOptions, type AppendToStreamResult, type CollectionConfig, type EventDocument, type ExpectedStreamVersion, ExpectedVersionConflictError, type FirestoreEventStore, type FirestoreEventStoreOptions, type FirestoreReadEvent, type FirestoreReadEventMetadata, type Logger, type ObservabilityOptions, type ReadStreamOptions, type StreamMetadata, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
261
|
+
export { type AppendToStreamOptions, type AppendToStreamResult, type CollectionConfig, type EventDocument, type ExpectedStreamVersion, ExpectedVersionConflictError, type FirestoreEventStore, type FirestoreEventStoreOptions, type FirestoreReadEvent, type FirestoreReadEventMetadata, type Logger, type ObservabilityOptions, type ReadStreamOptions, type StreamMetadata, asEventStore, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
import { Firestore, Timestamp } from '@google-cloud/firestore';
|
|
2
|
-
import { Event, ReadEvent, ReadEventMetadataWithGlobalPosition, ExpectedStreamVersion as ExpectedStreamVersion$1, STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
|
|
2
|
+
import { Event, ReadEvent, ReadEventMetadataWithGlobalPosition, ExpectedStreamVersion as ExpectedStreamVersion$1, STREAM_DOES_NOT_EXIST, EventStore } from '@event-driven-io/emmett';
|
|
3
3
|
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Canonical Logger contract for the Emmett ecosystem.
|
|
7
|
+
*
|
|
8
|
+
* DO NOT MODIFY this interface without updating ALL packages in the ecosystem.
|
|
9
|
+
*
|
|
10
|
+
* This package defines the canonical Logger interface.
|
|
11
|
+
* Implementations (Pino, Winston, etc.) MUST adapt to this contract.
|
|
12
|
+
* This contract MUST NOT adapt to any specific implementation.
|
|
13
|
+
*
|
|
14
|
+
* Semantic Rules:
|
|
15
|
+
* - context (first parameter): ALWAYS structured data as Record<string, unknown>
|
|
16
|
+
* - message (second parameter): ALWAYS the human-readable log message
|
|
17
|
+
* - The order is NEVER inverted
|
|
18
|
+
* - The (message, data) form is NOT valid for this contract
|
|
19
|
+
* - Error objects MUST use the 'err' key (frozen semantic)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Pino - native compatibility
|
|
24
|
+
* import pino from 'pino';
|
|
25
|
+
* const logger = pino();
|
|
26
|
+
* // logger.info({ orderId }, 'Order created') matches our contract
|
|
27
|
+
* ```
|
|
8
28
|
*/
|
|
9
29
|
interface Logger {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Log debug-level message with structured context.
|
|
32
|
+
* @param context - Structured data to include in the log entry
|
|
33
|
+
* @param message - Optional human-readable message
|
|
34
|
+
*/
|
|
35
|
+
debug(context: Record<string, unknown>, message?: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Log info-level message with structured context.
|
|
38
|
+
* @param context - Structured data to include in the log entry
|
|
39
|
+
* @param message - Optional human-readable message
|
|
40
|
+
*/
|
|
41
|
+
info(context: Record<string, unknown>, message?: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Log warn-level message with structured context.
|
|
44
|
+
* @param context - Structured data to include in the log entry
|
|
45
|
+
* @param message - Optional human-readable message
|
|
46
|
+
*/
|
|
47
|
+
warn(context: Record<string, unknown>, message?: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Log error-level message with structured context.
|
|
50
|
+
* @param context - Structured data to include in the log entry (MUST use 'err' key for Error objects)
|
|
51
|
+
* @param message - Optional human-readable message
|
|
52
|
+
*/
|
|
53
|
+
error(context: Record<string, unknown>, message?: string): void;
|
|
14
54
|
}
|
|
15
55
|
/**
|
|
16
56
|
* Observability configuration options
|
|
@@ -44,6 +84,7 @@ interface AppendToStreamResult {
|
|
|
44
84
|
}
|
|
45
85
|
/**
|
|
46
86
|
* Options for reading events from a stream
|
|
87
|
+
* Reuses the canonical `ReadStreamOptions` shape from the Emmett core types.
|
|
47
88
|
*/
|
|
48
89
|
interface ReadStreamOptions {
|
|
49
90
|
from?: bigint;
|
|
@@ -138,6 +179,7 @@ declare class ExpectedVersionConflictError extends Error {
|
|
|
138
179
|
constructor(streamName: string, expected: ExpectedStreamVersion, actual: bigint | typeof STREAM_DOES_NOT_EXIST);
|
|
139
180
|
}
|
|
140
181
|
|
|
182
|
+
declare function asEventStore(store: FirestoreEventStore): EventStore<FirestoreReadEventMetadata>;
|
|
141
183
|
/**
|
|
142
184
|
* Factory function to create a Firestore event store
|
|
143
185
|
*
|
|
@@ -216,4 +258,4 @@ declare function getCurrentStreamVersion(streamExists: boolean, version?: number
|
|
|
216
258
|
*/
|
|
217
259
|
declare function calculateNextVersion(currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST, eventCount: number): bigint;
|
|
218
260
|
|
|
219
|
-
export { type AppendToStreamOptions, type AppendToStreamResult, type CollectionConfig, type EventDocument, type ExpectedStreamVersion, ExpectedVersionConflictError, type FirestoreEventStore, type FirestoreEventStoreOptions, type FirestoreReadEvent, type FirestoreReadEventMetadata, type Logger, type ObservabilityOptions, type ReadStreamOptions, type StreamMetadata, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
261
|
+
export { type AppendToStreamOptions, type AppendToStreamResult, type CollectionConfig, type EventDocument, type ExpectedStreamVersion, ExpectedVersionConflictError, type FirestoreEventStore, type FirestoreEventStoreOptions, type FirestoreReadEvent, type FirestoreReadEventMetadata, type Logger, type ObservabilityOptions, type ReadStreamOptions, type StreamMetadata, asEventStore, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
package/dist/index.js
CHANGED
|
@@ -85,13 +85,41 @@ function calculateNextVersion(currentVersion, eventCount) {
|
|
|
85
85
|
|
|
86
86
|
// src/eventStore/firestoreEventStore.ts
|
|
87
87
|
var tracer = api.trace.getTracer("@emmett-community/emmett-google-firestore");
|
|
88
|
-
function
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
logFn.call(logger, msg, data);
|
|
88
|
+
function normalizeContext(data) {
|
|
89
|
+
if (data === void 0 || data === null) return {};
|
|
90
|
+
if (typeof data === "object" && !Array.isArray(data)) {
|
|
91
|
+
return { ...data };
|
|
93
92
|
}
|
|
93
|
+
return { data };
|
|
94
94
|
}
|
|
95
|
+
function normalizeErrorContext(error) {
|
|
96
|
+
if (error === void 0 || error === null) return {};
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
return { err: error };
|
|
99
|
+
}
|
|
100
|
+
if (typeof error === "object" && !Array.isArray(error)) {
|
|
101
|
+
return { ...error };
|
|
102
|
+
}
|
|
103
|
+
return { err: error };
|
|
104
|
+
}
|
|
105
|
+
var safeLog = {
|
|
106
|
+
debug: (logger, msg, data) => {
|
|
107
|
+
if (!logger) return;
|
|
108
|
+
logger.debug(normalizeContext(data), msg);
|
|
109
|
+
},
|
|
110
|
+
info: (logger, msg, data) => {
|
|
111
|
+
if (!logger) return;
|
|
112
|
+
logger.info(normalizeContext(data), msg);
|
|
113
|
+
},
|
|
114
|
+
warn: (logger, msg, data) => {
|
|
115
|
+
if (!logger) return;
|
|
116
|
+
logger.warn(normalizeContext(data), msg);
|
|
117
|
+
},
|
|
118
|
+
error: (logger, msg, error) => {
|
|
119
|
+
if (!logger) return;
|
|
120
|
+
logger.error(normalizeErrorContext(error), msg);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
95
123
|
var DEFAULT_COLLECTIONS = {
|
|
96
124
|
streams: "streams",
|
|
97
125
|
counters: "_counters"
|
|
@@ -104,25 +132,27 @@ var FirestoreEventStoreImpl = class {
|
|
|
104
132
|
...options.collections
|
|
105
133
|
};
|
|
106
134
|
this.logger = options.observability?.logger;
|
|
107
|
-
safeLog(this.logger, "
|
|
135
|
+
safeLog.info(this.logger, "FirestoreEventStore initialized");
|
|
108
136
|
}
|
|
109
137
|
collections;
|
|
110
138
|
logger;
|
|
111
139
|
/**
|
|
112
140
|
* Read events from a stream
|
|
113
141
|
*/
|
|
114
|
-
async readStream(streamName, options
|
|
142
|
+
async readStream(streamName, options) {
|
|
115
143
|
const span = tracer.startSpan("emmett.firestore.read_stream", {
|
|
116
144
|
attributes: { "emmett.stream_name": streamName }
|
|
117
145
|
});
|
|
118
146
|
try {
|
|
119
|
-
|
|
147
|
+
const from = options && "from" in options ? options.from : void 0;
|
|
148
|
+
const to = options && "to" in options ? options.to : void 0;
|
|
149
|
+
const maxCountLimit = options && "maxCount" in options && options.maxCount !== void 0 ? Number(options.maxCount) : void 0;
|
|
150
|
+
safeLog.debug(this.logger, "Reading stream", {
|
|
120
151
|
streamName,
|
|
121
|
-
from:
|
|
122
|
-
to:
|
|
123
|
-
maxCount:
|
|
152
|
+
from: from?.toString(),
|
|
153
|
+
to: to?.toString(),
|
|
154
|
+
maxCount: maxCountLimit
|
|
124
155
|
});
|
|
125
|
-
const { from, to, maxCount } = options;
|
|
126
156
|
let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
|
|
127
157
|
if (from !== void 0) {
|
|
128
158
|
query = query.where("streamVersion", ">=", Number(from));
|
|
@@ -130,8 +160,8 @@ var FirestoreEventStoreImpl = class {
|
|
|
130
160
|
if (to !== void 0) {
|
|
131
161
|
query = query.where("streamVersion", "<=", Number(to));
|
|
132
162
|
}
|
|
133
|
-
if (
|
|
134
|
-
query = query.limit(
|
|
163
|
+
if (maxCountLimit !== void 0 && maxCountLimit > 0) {
|
|
164
|
+
query = query.limit(maxCountLimit);
|
|
135
165
|
}
|
|
136
166
|
const snapshot = await query.get();
|
|
137
167
|
const events = snapshot.docs.map((doc) => {
|
|
@@ -151,7 +181,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
151
181
|
});
|
|
152
182
|
span.setAttribute("emmett.event_count", events.length);
|
|
153
183
|
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
154
|
-
safeLog(this.logger, "
|
|
184
|
+
safeLog.debug(this.logger, "Stream read completed", {
|
|
155
185
|
streamName,
|
|
156
186
|
eventCount: events.length
|
|
157
187
|
});
|
|
@@ -159,7 +189,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
159
189
|
} catch (error) {
|
|
160
190
|
span.recordException(error);
|
|
161
191
|
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
162
|
-
safeLog(this.logger, "
|
|
192
|
+
safeLog.error(this.logger, "Failed to read stream", {
|
|
163
193
|
streamName,
|
|
164
194
|
error
|
|
165
195
|
});
|
|
@@ -198,7 +228,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
198
228
|
throw new Error("Cannot append empty event array");
|
|
199
229
|
}
|
|
200
230
|
const { expectedStreamVersion = emmett.NO_CONCURRENCY_CHECK } = options;
|
|
201
|
-
safeLog(this.logger, "
|
|
231
|
+
safeLog.debug(this.logger, "Appending to stream", {
|
|
202
232
|
streamName,
|
|
203
233
|
eventCount: events.length,
|
|
204
234
|
eventTypes: events.map((e) => e.type),
|
|
@@ -215,7 +245,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
215
245
|
span.setAttribute("emmett.new_version", Number(result.nextExpectedStreamVersion));
|
|
216
246
|
span.setAttribute("emmett.created_new_stream", result.createdNewStream);
|
|
217
247
|
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
218
|
-
safeLog(this.logger, "
|
|
248
|
+
safeLog.debug(this.logger, "Append completed", {
|
|
219
249
|
streamName,
|
|
220
250
|
newVersion: result.nextExpectedStreamVersion.toString(),
|
|
221
251
|
createdNewStream: result.createdNewStream
|
|
@@ -225,13 +255,13 @@ var FirestoreEventStoreImpl = class {
|
|
|
225
255
|
span.recordException(error);
|
|
226
256
|
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
227
257
|
if (error instanceof ExpectedVersionConflictError) {
|
|
228
|
-
safeLog(this.logger, "
|
|
258
|
+
safeLog.warn(this.logger, "Version conflict during append", {
|
|
229
259
|
streamName,
|
|
230
260
|
expected: String(error.expected),
|
|
231
261
|
actual: String(error.actual)
|
|
232
262
|
});
|
|
233
263
|
} else {
|
|
234
|
-
safeLog(this.logger, "
|
|
264
|
+
safeLog.error(this.logger, "Failed to append to stream", {
|
|
235
265
|
streamName,
|
|
236
266
|
error
|
|
237
267
|
});
|
|
@@ -257,7 +287,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
257
287
|
streamExists,
|
|
258
288
|
streamData?.version
|
|
259
289
|
);
|
|
260
|
-
safeLog(this.logger, "
|
|
290
|
+
safeLog.debug(this.logger, "Read stream metadata", {
|
|
261
291
|
streamName,
|
|
262
292
|
exists: streamExists,
|
|
263
293
|
currentVersion: currentVersion === emmett.STREAM_DOES_NOT_EXIST ? "none" : currentVersion.toString()
|
|
@@ -298,7 +328,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
298
328
|
value: globalPosition,
|
|
299
329
|
updatedAt: now
|
|
300
330
|
});
|
|
301
|
-
safeLog(this.logger, "
|
|
331
|
+
safeLog.debug(this.logger, "Events written to transaction", {
|
|
302
332
|
streamName,
|
|
303
333
|
count: events.length,
|
|
304
334
|
newVersion
|
|
@@ -309,6 +339,47 @@ var FirestoreEventStoreImpl = class {
|
|
|
309
339
|
};
|
|
310
340
|
}
|
|
311
341
|
};
|
|
342
|
+
function normalizeReadOptions(options) {
|
|
343
|
+
if (!options) return void 0;
|
|
344
|
+
const result = {};
|
|
345
|
+
if ("from" in options) {
|
|
346
|
+
result.from = options.from;
|
|
347
|
+
if ("maxCount" in options && options.maxCount !== void 0) {
|
|
348
|
+
result.maxCount = Number(options.maxCount);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if ("to" in options) {
|
|
352
|
+
result.to = options.to;
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
function asEventStore(store) {
|
|
357
|
+
async function readStream(streamName, options) {
|
|
358
|
+
const events = await store.readStream(streamName, normalizeReadOptions(options));
|
|
359
|
+
const streamExists = events.length > 0;
|
|
360
|
+
const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
|
|
361
|
+
return {
|
|
362
|
+
currentStreamVersion,
|
|
363
|
+
events,
|
|
364
|
+
streamExists
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function aggregateStream(streamName, options) {
|
|
368
|
+
return store.aggregateStream(streamName, {
|
|
369
|
+
evolve: options.evolve,
|
|
370
|
+
initialState: options.initialState,
|
|
371
|
+
read: normalizeReadOptions(options.read)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function appendToStream(streamName, events, options) {
|
|
375
|
+
return store.appendToStream(streamName, events, options);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
readStream,
|
|
379
|
+
aggregateStream,
|
|
380
|
+
appendToStream
|
|
381
|
+
};
|
|
382
|
+
}
|
|
312
383
|
function getFirestoreEventStore(firestore, options) {
|
|
313
384
|
return new FirestoreEventStoreImpl(firestore, options);
|
|
314
385
|
}
|
|
@@ -326,6 +397,7 @@ Object.defineProperty(exports, "STREAM_EXISTS", {
|
|
|
326
397
|
get: function () { return emmett.STREAM_EXISTS; }
|
|
327
398
|
});
|
|
328
399
|
exports.ExpectedVersionConflictError = ExpectedVersionConflictError;
|
|
400
|
+
exports.asEventStore = asEventStore;
|
|
329
401
|
exports.assertExpectedVersionMatchesCurrent = assertExpectedVersionMatchesCurrent;
|
|
330
402
|
exports.calculateNextVersion = calculateNextVersion;
|
|
331
403
|
exports.getCurrentStreamVersion = getCurrentStreamVersion;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/eventStore/types.ts","../src/eventStore/utils.ts","../src/eventStore/firestoreEventStore.ts"],"names":["NO_CONCURRENCY_CHECK","STREAM_DOES_NOT_EXIST","STREAM_EXISTS","trace","SpanStatusCode"],"mappings":"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","file":"index.js","sourcesContent":["import type { Firestore, Timestamp } from '@google-cloud/firestore';\nimport type { Event, ReadEvent, ReadEventMetadataWithGlobalPosition } from '@event-driven-io/emmett';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n type ExpectedStreamVersion as EmmettExpectedStreamVersion,\n} from '@event-driven-io/emmett';\n\n/**\n * Minimal logger interface compatible with Pino and other loggers.\n * All methods are optional to support varying logger implementations.\n */\nexport interface Logger {\n debug?(msg: string, data?: unknown): void;\n info?(msg: string, data?: unknown): void;\n warn?(msg: string, data?: unknown): void;\n error?(msg: string, err?: unknown): void;\n}\n\n/**\n * Observability configuration options\n */\nexport interface ObservabilityOptions {\n /** Optional logger instance. If not provided, no logging occurs. */\n logger?: Logger;\n}\n\n/**\n * Expected version for stream operations\n * Uses Emmett's standard version constants for full compatibility\n * - number | bigint: Expect specific version\n * - STREAM_DOES_NOT_EXIST: Stream must not exist\n * - STREAM_EXISTS: Stream must exist (any version)\n * - NO_CONCURRENCY_CHECK: No version check\n */\nexport type ExpectedStreamVersion = EmmettExpectedStreamVersion<bigint>;\n\n// Re-export Emmett constants for convenience\nexport { STREAM_DOES_NOT_EXIST, STREAM_EXISTS, NO_CONCURRENCY_CHECK };\n\n/**\n * Options for appending events to a stream\n */\nexport interface AppendToStreamOptions {\n expectedStreamVersion?: ExpectedStreamVersion;\n}\n\n/**\n * Result of appending events to a stream\n */\nexport interface AppendToStreamResult {\n nextExpectedStreamVersion: bigint;\n createdNewStream: boolean;\n}\n\n/**\n * Options for reading events from a stream\n */\nexport interface ReadStreamOptions {\n from?: bigint;\n to?: bigint;\n maxCount?: number;\n}\n\n/**\n * Metadata stored in Firestore stream document\n */\nexport interface StreamMetadata {\n version: number;\n createdAt: Timestamp;\n updatedAt: Timestamp;\n}\n\n/**\n * Event document structure in Firestore\n */\nexport interface EventDocument {\n type: string;\n data: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n timestamp: Timestamp;\n globalPosition: number;\n streamVersion: number;\n}\n\n/**\n * Firestore-specific read event metadata\n */\nexport interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {\n streamName: string;\n streamVersion: bigint;\n timestamp: Date;\n}\n\n/**\n * Firestore read event\n */\nexport type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<\n EventType,\n FirestoreReadEventMetadata\n>;\n\n/**\n * Collection configuration for Firestore event store\n */\nexport interface CollectionConfig {\n streams: string;\n counters: string;\n}\n\n/**\n * Firestore event store options\n */\nexport interface FirestoreEventStoreOptions {\n collections?: Partial<CollectionConfig>;\n observability?: ObservabilityOptions;\n}\n\n/**\n * Firestore event store interface\n */\nexport interface FirestoreEventStore {\n /**\n * The underlying Firestore instance\n */\n readonly firestore: Firestore;\n\n /**\n * Collection names configuration\n */\n readonly collections: CollectionConfig;\n\n /**\n * Read events from a stream\n */\n readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]>;\n\n /**\n * Aggregate stream by applying events to state\n */\n aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }>;\n\n /**\n * Append events to a stream\n */\n appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult>;\n}\n\n/**\n * Error thrown when expected version doesn't match current version\n */\nexport class ExpectedVersionConflictError extends Error {\n constructor(\n public readonly streamName: string,\n public readonly expected: ExpectedStreamVersion,\n public readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST,\n ) {\n super(\n `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`,\n );\n this.name = 'ExpectedVersionConflictError';\n Object.setPrototypeOf(this, ExpectedVersionConflictError.prototype);\n }\n}\n","import type { Timestamp } from '@google-cloud/firestore';\nimport type { ExpectedStreamVersion } from './types';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n ExpectedVersionConflictError,\n} from './types';\n\n/**\n * Pad version number with leading zeros for Firestore document IDs\n * This ensures automatic ordering by version in Firestore\n *\n * @param version - The version number to pad\n * @returns Zero-padded string of length 10\n *\n * @example\n * padVersion(0) // \"0000000000\"\n * padVersion(42) // \"0000000042\"\n * padVersion(12345) // \"0000012345\"\n */\nexport function padVersion(version: number | bigint): string {\n return version.toString().padStart(10, '0');\n}\n\n/**\n * Parse a stream name into type and ID components\n *\n * @param streamName - Stream name in format \"Type-id\" or \"Type-with-dashes-id\"\n * @returns Object with streamType and streamId\n *\n * @example\n * parseStreamName(\"User-123\") // { streamType: \"User\", streamId: \"123\" }\n * parseStreamName(\"ShoppingCart-abc-def-123\") // { streamType: \"ShoppingCart\", streamId: \"abc-def-123\" }\n */\nexport function parseStreamName(streamName: string): {\n streamType: string;\n streamId: string;\n} {\n const firstDashIndex = streamName.indexOf('-');\n\n if (firstDashIndex === -1) {\n return {\n streamType: streamName,\n streamId: '',\n };\n }\n\n return {\n streamType: streamName.substring(0, firstDashIndex),\n streamId: streamName.substring(firstDashIndex + 1),\n };\n}\n\n/**\n * Convert Firestore Timestamp to JavaScript Date\n *\n * @param timestamp - Firestore Timestamp\n * @returns JavaScript Date object\n */\nexport function timestampToDate(timestamp: Timestamp): Date {\n return timestamp.toDate();\n}\n\n/**\n * Validate expected version against current version\n *\n * @param streamName - Stream name for error messages\n * @param expectedVersion - Expected version constraint\n * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)\n * @throws ExpectedVersionConflictError if versions don't match\n */\nexport function assertExpectedVersionMatchesCurrent(\n streamName: string,\n expectedVersion: ExpectedStreamVersion,\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n): void {\n // NO_CONCURRENCY_CHECK - no validation needed\n if (expectedVersion === NO_CONCURRENCY_CHECK) {\n return;\n }\n\n // STREAM_DOES_NOT_EXIST - stream must not exist\n if (expectedVersion === STREAM_DOES_NOT_EXIST) {\n if (currentVersion !== STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // STREAM_EXISTS - stream must exist\n if (expectedVersion === STREAM_EXISTS) {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // Specific version number\n const expectedBigInt = BigInt(expectedVersion);\n if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n}\n\n/**\n * Get the current stream version from metadata\n *\n * @param streamExists - Whether the stream document exists\n * @param version - Version number from Firestore (if stream exists)\n * @returns Current version as bigint or STREAM_DOES_NOT_EXIST\n */\nexport function getCurrentStreamVersion(\n streamExists: boolean,\n version?: number,\n): bigint | typeof STREAM_DOES_NOT_EXIST {\n if (!streamExists) {\n return STREAM_DOES_NOT_EXIST;\n }\n return BigInt(version ?? -1);\n}\n\n/**\n * Calculate the next expected stream version after appending events\n *\n * @param currentVersion - Current stream version\n * @param eventCount - Number of events being appended\n * @returns Next expected version as bigint\n */\nexport function calculateNextVersion(\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n eventCount: number,\n): bigint {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n return BigInt(eventCount - 1);\n }\n // Type assertion needed because TypeScript doesn't narrow ExpectedStreamVersionGeneral properly\n return (currentVersion as bigint) + BigInt(eventCount);\n}\n","import type { Firestore, Transaction, Timestamp } from '@google-cloud/firestore';\nimport type { Event } from '@event-driven-io/emmett';\nimport { trace, SpanStatusCode } from '@opentelemetry/api';\nimport type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n Logger,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, ExpectedVersionConflictError } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\n\nconst tracer = trace.getTracer('@emmett-community/emmett-google-firestore');\n\n/**\n * Safe logging helper that handles undefined logger methods\n */\nfunction safeLog(\n logger: Logger | undefined,\n level: keyof Logger,\n msg: string,\n data?: unknown,\n): void {\n if (!logger) return;\n const logFn = logger[level];\n if (typeof logFn === 'function') {\n logFn.call(logger, msg, data);\n }\n}\n\nconst DEFAULT_COLLECTIONS: CollectionConfig = {\n streams: 'streams',\n counters: '_counters',\n};\n\n/**\n * Firestore Event Store Implementation\n *\n * Stores events in Firestore using a subcollection pattern:\n * - /streams/{streamName} - Stream metadata (version, timestamps)\n * - /streams/{streamName}/events/{version} - Individual events\n * - /_counters/global_position - Global event counter\n */\nexport class FirestoreEventStoreImpl implements FirestoreEventStore {\n public readonly collections: CollectionConfig;\n private readonly logger: Logger | undefined;\n\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\n this.logger = options.observability?.logger;\n\n safeLog(this.logger, 'info', 'FirestoreEventStore initialized');\n }\n\n /**\n * Read events from a stream\n */\n async readStream<EventType extends Event>(\n streamName: string,\n options: ReadStreamOptions = {},\n ): Promise<FirestoreReadEvent<EventType>[]> {\n const span = tracer.startSpan('emmett.firestore.read_stream', {\n attributes: { 'emmett.stream_name': streamName },\n });\n\n try {\n safeLog(this.logger, 'debug', 'Reading stream', {\n streamName,\n from: options.from?.toString(),\n to: options.to?.toString(),\n maxCount: options.maxCount,\n });\n\n const { from, to, maxCount } = options;\n\n // Reference to events subcollection\n let query = this.firestore\n .collection(this.collections.streams)\n .doc(streamName)\n .collection('events')\n .orderBy('streamVersion', 'asc');\n\n // Apply range filters\n if (from !== undefined) {\n query = query.where('streamVersion', '>=', Number(from));\n }\n if (to !== undefined) {\n query = query.where('streamVersion', '<=', Number(to));\n }\n if (maxCount !== undefined && maxCount > 0) {\n query = query.limit(maxCount);\n }\n\n // Execute query\n const snapshot = await query.get();\n\n // Transform Firestore documents to events\n const events = snapshot.docs.map((doc) => {\n const data = doc.data() as EventDocument;\n return {\n type: data.type,\n data: data.data,\n metadata: {\n ...data.metadata,\n streamName,\n streamVersion: BigInt(data.streamVersion),\n streamPosition: BigInt(data.streamVersion),\n globalPosition: BigInt(data.globalPosition),\n timestamp: timestampToDate(data.timestamp),\n },\n } as FirestoreReadEvent<EventType>;\n });\n\n span.setAttribute('emmett.event_count', events.length);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog(this.logger, 'debug', 'Stream read completed', {\n streamName,\n eventCount: events.length,\n });\n\n return events;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n safeLog(this.logger, 'error', 'Failed to read stream', {\n streamName,\n error,\n });\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Aggregate stream by applying events to state\n */\n async aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }> {\n const { evolve, initialState, read } = options;\n const events = await this.readStream<EventType>(streamName, read);\n\n const streamExists = events.length > 0;\n const state = events.reduce(evolve, initialState());\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n state,\n currentStreamVersion,\n streamExists,\n };\n }\n\n /**\n * Append events to a stream with optimistic concurrency control\n */\n async appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options: AppendToStreamOptions = {},\n ): Promise<AppendToStreamResult> {\n const span = tracer.startSpan('emmett.firestore.append_to_stream', {\n attributes: {\n 'emmett.stream_name': streamName,\n 'emmett.event_count': events.length,\n },\n });\n\n try {\n if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n safeLog(this.logger, 'debug', 'Appending to stream', {\n streamName,\n eventCount: events.length,\n eventTypes: events.map((e) => e.type),\n expectedVersion: String(expectedStreamVersion),\n });\n\n // Execute in transaction for atomicity\n const result = await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n\n span.setAttribute('emmett.new_version', Number(result.nextExpectedStreamVersion));\n span.setAttribute('emmett.created_new_stream', result.createdNewStream);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog(this.logger, 'debug', 'Append completed', {\n streamName,\n newVersion: result.nextExpectedStreamVersion.toString(),\n createdNewStream: result.createdNewStream,\n });\n\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n if (error instanceof ExpectedVersionConflictError) {\n safeLog(this.logger, 'warn', 'Version conflict during append', {\n streamName,\n expected: String(error.expected),\n actual: String(error.actual),\n });\n } else {\n safeLog(this.logger, 'error', 'Failed to append to stream', {\n streamName,\n error,\n });\n }\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Internal method to append events within a transaction\n *\n * Note: No separate span here - this method runs inside appendToStream's span,\n * and Firestore transaction operations are atomic. The parent span captures\n * the full transaction duration.\n */\n private async appendToStreamInTransaction<EventType extends Event>(\n transaction: Transaction,\n streamName: string,\n events: EventType[],\n expectedStreamVersion: ExpectedStreamVersion,\n ): Promise<AppendToStreamResult> {\n // 1. Get stream metadata reference\n const streamRef = this.firestore\n .collection(this.collections.streams)\n .doc(streamName);\n\n const streamDoc = await transaction.get(streamRef);\n const streamExists = streamDoc.exists;\n const streamData = streamDoc.data() as StreamMetadata | undefined;\n\n // 2. Get current version and validate expected version\n const currentVersion = getCurrentStreamVersion(\n streamExists,\n streamData?.version,\n );\n\n safeLog(this.logger, 'debug', 'Read stream metadata', {\n streamName,\n exists: streamExists,\n currentVersion: currentVersion === STREAM_DOES_NOT_EXIST ? 'none' : currentVersion.toString(),\n });\n\n assertExpectedVersionMatchesCurrent(\n streamName,\n expectedStreamVersion,\n currentVersion,\n );\n\n // 3. Get and increment global position counter\n const counterRef = this.firestore\n .collection(this.collections.counters)\n .doc('global_position');\n\n const counterDoc = await transaction.get(counterRef);\n let globalPosition = counterDoc.exists\n ? (counterDoc.data()?.value as number) ?? 0\n : 0;\n\n // 4. Calculate starting version for new events\n const baseVersion =\n currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);\n\n // 5. Append events to subcollection\n const TimestampClass = this.firestore.constructor as unknown as { Timestamp: typeof Timestamp };\n const now = TimestampClass.Timestamp.now();\n\n events.forEach((event, index) => {\n const eventVersion = baseVersion + 1 + index;\n const eventRef = streamRef\n .collection('events')\n .doc(padVersion(eventVersion));\n\n const metadata = (event as { metadata?: Record<string, unknown> }).metadata;\n const eventDocument: EventDocument = {\n type: event.type,\n data: event.data,\n ...(metadata && { metadata }),\n timestamp: now,\n globalPosition: globalPosition++,\n streamVersion: eventVersion,\n };\n\n transaction.set(eventRef, eventDocument);\n });\n\n // 6. Update stream metadata\n const newVersion = baseVersion + events.length;\n const updatedMetadata: StreamMetadata = {\n version: newVersion,\n createdAt: streamData?.createdAt ?? now,\n updatedAt: now,\n };\n\n transaction.set(streamRef, updatedMetadata);\n\n // 7. Update global position counter\n transaction.set(counterRef, {\n value: globalPosition,\n updatedAt: now,\n });\n\n safeLog(this.logger, 'debug', 'Events written to transaction', {\n streamName,\n count: events.length,\n newVersion,\n });\n\n // 8. Return result\n return {\n nextExpectedStreamVersion: BigInt(newVersion),\n createdNewStream: !streamExists,\n };\n }\n}\n\n/**\n * Factory function to create a Firestore event store\n *\n * @param firestore - Firestore instance\n * @param options - Optional configuration\n * @returns Firestore event store instance\n *\n * @example\n * ```typescript\n * import { Firestore } from '@google-cloud/firestore';\n * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';\n *\n * const firestore = new Firestore({ projectId: 'my-project' });\n * const eventStore = getFirestoreEventStore(firestore);\n * ```\n */\nexport function getFirestoreEventStore(\n firestore: Firestore,\n options?: FirestoreEventStoreOptions,\n): FirestoreEventStore {\n return new FirestoreEventStoreImpl(firestore, options);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/eventStore/types.ts","../src/eventStore/utils.ts","../src/eventStore/firestoreEventStore.ts"],"names":["NO_CONCURRENCY_CHECK","STREAM_DOES_NOT_EXIST","STREAM_EXISTS","trace","SpanStatusCode"],"mappings":"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","file":"index.js","sourcesContent":["import type { Firestore, Timestamp } from '@google-cloud/firestore';\nimport type { Event, ReadEvent, ReadEventMetadataWithGlobalPosition } from '@event-driven-io/emmett';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n type ExpectedStreamVersion as EmmettExpectedStreamVersion,\n} from '@event-driven-io/emmett';\n\n/**\n * Canonical Logger contract for the Emmett ecosystem.\n *\n * DO NOT MODIFY this interface without updating ALL packages in the ecosystem.\n *\n * This package defines the canonical Logger interface.\n * Implementations (Pino, Winston, etc.) MUST adapt to this contract.\n * This contract MUST NOT adapt to any specific implementation.\n *\n * Semantic Rules:\n * - context (first parameter): ALWAYS structured data as Record<string, unknown>\n * - message (second parameter): ALWAYS the human-readable log message\n * - The order is NEVER inverted\n * - The (message, data) form is NOT valid for this contract\n * - Error objects MUST use the 'err' key (frozen semantic)\n *\n * @example\n * ```typescript\n * // Pino - native compatibility\n * import pino from 'pino';\n * const logger = pino();\n * // logger.info({ orderId }, 'Order created') matches our contract\n * ```\n */\nexport interface Logger {\n /**\n * Log debug-level message with structured context.\n * @param context - Structured data to include in the log entry\n * @param message - Optional human-readable message\n */\n debug(context: Record<string, unknown>, message?: string): void;\n\n /**\n * Log info-level message with structured context.\n * @param context - Structured data to include in the log entry\n * @param message - Optional human-readable message\n */\n info(context: Record<string, unknown>, message?: string): void;\n\n /**\n * Log warn-level message with structured context.\n * @param context - Structured data to include in the log entry\n * @param message - Optional human-readable message\n */\n warn(context: Record<string, unknown>, message?: string): void;\n\n /**\n * Log error-level message with structured context.\n * @param context - Structured data to include in the log entry (MUST use 'err' key for Error objects)\n * @param message - Optional human-readable message\n */\n error(context: Record<string, unknown>, message?: string): void;\n}\n\n/**\n * Observability configuration options\n */\nexport interface ObservabilityOptions {\n /** Optional logger instance. If not provided, no logging occurs. */\n logger?: Logger;\n}\n\n/**\n * Expected version for stream operations\n * Uses Emmett's standard version constants for full compatibility\n * - number | bigint: Expect specific version\n * - STREAM_DOES_NOT_EXIST: Stream must not exist\n * - STREAM_EXISTS: Stream must exist (any version)\n * - NO_CONCURRENCY_CHECK: No version check\n */\nexport type ExpectedStreamVersion = EmmettExpectedStreamVersion<bigint>;\n\n// Re-export Emmett constants for convenience\nexport { STREAM_DOES_NOT_EXIST, STREAM_EXISTS, NO_CONCURRENCY_CHECK };\n\n/**\n * Options for appending events to a stream\n */\nexport interface AppendToStreamOptions {\n expectedStreamVersion?: ExpectedStreamVersion;\n}\n\n/**\n * Result of appending events to a stream\n */\nexport interface AppendToStreamResult {\n nextExpectedStreamVersion: bigint;\n createdNewStream: boolean;\n}\n\n/**\n * Options for reading events from a stream\n * Reuses the canonical `ReadStreamOptions` shape from the Emmett core types.\n */\nexport interface ReadStreamOptions {\n from?: bigint;\n to?: bigint;\n maxCount?: number;\n}\n\n/**\n * Metadata stored in Firestore stream document\n */\nexport interface StreamMetadata {\n version: number;\n createdAt: Timestamp;\n updatedAt: Timestamp;\n}\n\n/**\n * Event document structure in Firestore\n */\nexport interface EventDocument {\n type: string;\n data: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n timestamp: Timestamp;\n globalPosition: number;\n streamVersion: number;\n}\n\n/**\n * Firestore-specific read event metadata\n */\nexport interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {\n streamName: string;\n streamVersion: bigint;\n timestamp: Date;\n}\n\n/**\n * Firestore read event\n */\nexport type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<\n EventType,\n FirestoreReadEventMetadata\n>;\n\n/**\n * Collection configuration for Firestore event store\n */\nexport interface CollectionConfig {\n streams: string;\n counters: string;\n}\n\n/**\n * Firestore event store options\n */\nexport interface FirestoreEventStoreOptions {\n collections?: Partial<CollectionConfig>;\n observability?: ObservabilityOptions;\n}\n\n/**\n * Firestore event store interface\n */\nexport interface FirestoreEventStore {\n /**\n * The underlying Firestore instance\n */\n readonly firestore: Firestore;\n\n /**\n * Collection names configuration\n */\n readonly collections: CollectionConfig;\n\n /**\n * Read events from a stream\n */\n readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]>;\n\n /**\n * Aggregate stream by applying events to state\n */\n aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }>;\n\n /**\n * Append events to a stream\n */\n appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult>;\n}\n\n/**\n * Error thrown when expected version doesn't match current version\n */\nexport class ExpectedVersionConflictError extends Error {\n constructor(\n public readonly streamName: string,\n public readonly expected: ExpectedStreamVersion,\n public readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST,\n ) {\n super(\n `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`,\n );\n this.name = 'ExpectedVersionConflictError';\n Object.setPrototypeOf(this, ExpectedVersionConflictError.prototype);\n }\n}\n","import type { Timestamp } from '@google-cloud/firestore';\nimport type { ExpectedStreamVersion } from './types';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n ExpectedVersionConflictError,\n} from './types';\n\n/**\n * Pad version number with leading zeros for Firestore document IDs\n * This ensures automatic ordering by version in Firestore\n *\n * @param version - The version number to pad\n * @returns Zero-padded string of length 10\n *\n * @example\n * padVersion(0) // \"0000000000\"\n * padVersion(42) // \"0000000042\"\n * padVersion(12345) // \"0000012345\"\n */\nexport function padVersion(version: number | bigint): string {\n return version.toString().padStart(10, '0');\n}\n\n/**\n * Parse a stream name into type and ID components\n *\n * @param streamName - Stream name in format \"Type-id\" or \"Type-with-dashes-id\"\n * @returns Object with streamType and streamId\n *\n * @example\n * parseStreamName(\"User-123\") // { streamType: \"User\", streamId: \"123\" }\n * parseStreamName(\"ShoppingCart-abc-def-123\") // { streamType: \"ShoppingCart\", streamId: \"abc-def-123\" }\n */\nexport function parseStreamName(streamName: string): {\n streamType: string;\n streamId: string;\n} {\n const firstDashIndex = streamName.indexOf('-');\n\n if (firstDashIndex === -1) {\n return {\n streamType: streamName,\n streamId: '',\n };\n }\n\n return {\n streamType: streamName.substring(0, firstDashIndex),\n streamId: streamName.substring(firstDashIndex + 1),\n };\n}\n\n/**\n * Convert Firestore Timestamp to JavaScript Date\n *\n * @param timestamp - Firestore Timestamp\n * @returns JavaScript Date object\n */\nexport function timestampToDate(timestamp: Timestamp): Date {\n return timestamp.toDate();\n}\n\n/**\n * Validate expected version against current version\n *\n * @param streamName - Stream name for error messages\n * @param expectedVersion - Expected version constraint\n * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)\n * @throws ExpectedVersionConflictError if versions don't match\n */\nexport function assertExpectedVersionMatchesCurrent(\n streamName: string,\n expectedVersion: ExpectedStreamVersion,\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n): void {\n // NO_CONCURRENCY_CHECK - no validation needed\n if (expectedVersion === NO_CONCURRENCY_CHECK) {\n return;\n }\n\n // STREAM_DOES_NOT_EXIST - stream must not exist\n if (expectedVersion === STREAM_DOES_NOT_EXIST) {\n if (currentVersion !== STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // STREAM_EXISTS - stream must exist\n if (expectedVersion === STREAM_EXISTS) {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // Specific version number\n const expectedBigInt = BigInt(expectedVersion);\n if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n}\n\n/**\n * Get the current stream version from metadata\n *\n * @param streamExists - Whether the stream document exists\n * @param version - Version number from Firestore (if stream exists)\n * @returns Current version as bigint or STREAM_DOES_NOT_EXIST\n */\nexport function getCurrentStreamVersion(\n streamExists: boolean,\n version?: number,\n): bigint | typeof STREAM_DOES_NOT_EXIST {\n if (!streamExists) {\n return STREAM_DOES_NOT_EXIST;\n }\n return BigInt(version ?? -1);\n}\n\n/**\n * Calculate the next expected stream version after appending events\n *\n * @param currentVersion - Current stream version\n * @param eventCount - Number of events being appended\n * @returns Next expected version as bigint\n */\nexport function calculateNextVersion(\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n eventCount: number,\n): bigint {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n return BigInt(eventCount - 1);\n }\n // Type assertion needed because TypeScript doesn't narrow ExpectedStreamVersionGeneral properly\n return (currentVersion as bigint) + BigInt(eventCount);\n}\n","import type { Firestore, Transaction, Timestamp } from '@google-cloud/firestore';\nimport type {\n AggregateStreamOptions,\n AggregateStreamResult,\n Event,\n EventStore,\n ReadStreamOptions as EmmettReadStreamOptions,\n ReadStreamResult,\n} from '@event-driven-io/emmett';\nimport { trace, SpanStatusCode } from '@opentelemetry/api';\nimport type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n FirestoreReadEventMetadata,\n Logger,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, ExpectedVersionConflictError } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\n\nconst tracer = trace.getTracer('@emmett-community/emmett-google-firestore');\n\n/**\n * @internal - NOT part of public API\n * Normalizes data to a context object for the Logger contract.\n */\nfunction normalizeContext(data: unknown): Record<string, unknown> {\n if (data === undefined || data === null) return {};\n if (typeof data === 'object' && !Array.isArray(data)) {\n return { ...(data as Record<string, unknown>) };\n }\n return { data };\n}\n\n/**\n * @internal - NOT part of public API\n * Normalizes error data to a context object for the Logger contract.\n * Uses 'err' key for Error instances (Pino compatibility).\n */\nfunction normalizeErrorContext(error: unknown): Record<string, unknown> {\n if (error === undefined || error === null) return {};\n if (error instanceof Error) {\n return { err: error };\n }\n if (typeof error === 'object' && !Array.isArray(error)) {\n return { ...(error as Record<string, unknown>) };\n }\n return { err: error };\n}\n\n/**\n * @internal - NOT part of public API\n *\n * Internal helper for ergonomic logging within this package.\n * Translates internal (msg, data) calls to canonical (context, message) contract.\n *\n * This is the ONLY point where translation from internal format to contract format occurs.\n */\nconst safeLog = {\n debug: (logger: Logger | undefined, msg: string, data?: unknown): void => {\n if (!logger) return;\n logger.debug(normalizeContext(data), msg);\n },\n info: (logger: Logger | undefined, msg: string, data?: unknown): void => {\n if (!logger) return;\n logger.info(normalizeContext(data), msg);\n },\n warn: (logger: Logger | undefined, msg: string, data?: unknown): void => {\n if (!logger) return;\n logger.warn(normalizeContext(data), msg);\n },\n error: (logger: Logger | undefined, msg: string, error?: unknown): void => {\n if (!logger) return;\n logger.error(normalizeErrorContext(error), msg);\n },\n};\n\nconst DEFAULT_COLLECTIONS: CollectionConfig = {\n streams: 'streams',\n counters: '_counters',\n};\n\n/**\n * Firestore Event Store Implementation\n *\n * Stores events in Firestore using a subcollection pattern:\n * - /streams/{streamName} - Stream metadata (version, timestamps)\n * - /streams/{streamName}/events/{version} - Individual events\n * - /_counters/global_position - Global event counter\n */\nexport class FirestoreEventStoreImpl implements FirestoreEventStore {\n public readonly collections: CollectionConfig;\n private readonly logger: Logger | undefined;\n\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\n this.logger = options.observability?.logger;\n\n safeLog.info(this.logger, 'FirestoreEventStore initialized');\n }\n\n /**\n * Read events from a stream\n */\n async readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]> {\n const span = tracer.startSpan('emmett.firestore.read_stream', {\n attributes: { 'emmett.stream_name': streamName },\n });\n\n try {\n const from = options && 'from' in options ? options.from : undefined;\n const to = options && 'to' in options ? options.to : undefined;\n const maxCountLimit =\n options && 'maxCount' in options && options.maxCount !== undefined\n ? Number(options.maxCount)\n : undefined;\n\n safeLog.debug(this.logger, 'Reading stream', {\n streamName,\n from: from?.toString(),\n to: to?.toString(),\n maxCount: maxCountLimit,\n });\n\n // Reference to events subcollection\n let query = this.firestore\n .collection(this.collections.streams)\n .doc(streamName)\n .collection('events')\n .orderBy('streamVersion', 'asc');\n\n // Apply range filters\n if (from !== undefined) {\n query = query.where('streamVersion', '>=', Number(from));\n }\n if (to !== undefined) {\n query = query.where('streamVersion', '<=', Number(to));\n }\n if (maxCountLimit !== undefined && maxCountLimit > 0) {\n query = query.limit(maxCountLimit);\n }\n\n // Execute query\n const snapshot = await query.get();\n\n // Transform Firestore documents to events\n const events = snapshot.docs.map((doc) => {\n const data = doc.data() as EventDocument;\n return {\n type: data.type,\n data: data.data,\n metadata: {\n ...data.metadata,\n streamName,\n streamVersion: BigInt(data.streamVersion),\n streamPosition: BigInt(data.streamVersion),\n globalPosition: BigInt(data.globalPosition),\n timestamp: timestampToDate(data.timestamp),\n },\n } as FirestoreReadEvent<EventType>;\n });\n\n span.setAttribute('emmett.event_count', events.length);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog.debug(this.logger, 'Stream read completed', {\n streamName,\n eventCount: events.length,\n });\n\n return events;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n safeLog.error(this.logger, 'Failed to read stream', {\n streamName,\n error,\n });\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Aggregate stream by applying events to state\n */\n async aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }> {\n const { evolve, initialState, read } = options;\n const events = await this.readStream<EventType>(streamName, read);\n\n const streamExists = events.length > 0;\n const state = events.reduce(evolve, initialState());\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n state,\n currentStreamVersion,\n streamExists,\n };\n }\n\n /**\n * Append events to a stream with optimistic concurrency control\n */\n async appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options: AppendToStreamOptions = {},\n ): Promise<AppendToStreamResult> {\n const span = tracer.startSpan('emmett.firestore.append_to_stream', {\n attributes: {\n 'emmett.stream_name': streamName,\n 'emmett.event_count': events.length,\n },\n });\n\n try {\n if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n safeLog.debug(this.logger, 'Appending to stream', {\n streamName,\n eventCount: events.length,\n eventTypes: events.map((e) => e.type),\n expectedVersion: String(expectedStreamVersion),\n });\n\n // Execute in transaction for atomicity\n const result = await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n\n span.setAttribute('emmett.new_version', Number(result.nextExpectedStreamVersion));\n span.setAttribute('emmett.created_new_stream', result.createdNewStream);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog.debug(this.logger, 'Append completed', {\n streamName,\n newVersion: result.nextExpectedStreamVersion.toString(),\n createdNewStream: result.createdNewStream,\n });\n\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n if (error instanceof ExpectedVersionConflictError) {\n safeLog.warn(this.logger, 'Version conflict during append', {\n streamName,\n expected: String(error.expected),\n actual: String(error.actual),\n });\n } else {\n safeLog.error(this.logger, 'Failed to append to stream', {\n streamName,\n error,\n });\n }\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Internal method to append events within a transaction\n *\n * Note: No separate span here - this method runs inside appendToStream's span,\n * and Firestore transaction operations are atomic. The parent span captures\n * the full transaction duration.\n */\n private async appendToStreamInTransaction<EventType extends Event>(\n transaction: Transaction,\n streamName: string,\n events: EventType[],\n expectedStreamVersion: ExpectedStreamVersion,\n ): Promise<AppendToStreamResult> {\n // 1. Get stream metadata reference\n const streamRef = this.firestore\n .collection(this.collections.streams)\n .doc(streamName);\n\n const streamDoc = await transaction.get(streamRef);\n const streamExists = streamDoc.exists;\n const streamData = streamDoc.data() as StreamMetadata | undefined;\n\n // 2. Get current version and validate expected version\n const currentVersion = getCurrentStreamVersion(\n streamExists,\n streamData?.version,\n );\n\n safeLog.debug(this.logger, 'Read stream metadata', {\n streamName,\n exists: streamExists,\n currentVersion: currentVersion === STREAM_DOES_NOT_EXIST ? 'none' : currentVersion.toString(),\n });\n\n assertExpectedVersionMatchesCurrent(\n streamName,\n expectedStreamVersion,\n currentVersion,\n );\n\n // 3. Get and increment global position counter\n const counterRef = this.firestore\n .collection(this.collections.counters)\n .doc('global_position');\n\n const counterDoc = await transaction.get(counterRef);\n let globalPosition = counterDoc.exists\n ? (counterDoc.data()?.value as number) ?? 0\n : 0;\n\n // 4. Calculate starting version for new events\n const baseVersion =\n currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);\n\n // 5. Append events to subcollection\n const TimestampClass = this.firestore.constructor as unknown as { Timestamp: typeof Timestamp };\n const now = TimestampClass.Timestamp.now();\n\n events.forEach((event, index) => {\n const eventVersion = baseVersion + 1 + index;\n const eventRef = streamRef\n .collection('events')\n .doc(padVersion(eventVersion));\n\n const metadata = (event as { metadata?: Record<string, unknown> }).metadata;\n const eventDocument: EventDocument = {\n type: event.type,\n data: event.data,\n ...(metadata && { metadata }),\n timestamp: now,\n globalPosition: globalPosition++,\n streamVersion: eventVersion,\n };\n\n transaction.set(eventRef, eventDocument);\n });\n\n // 6. Update stream metadata\n const newVersion = baseVersion + events.length;\n const updatedMetadata: StreamMetadata = {\n version: newVersion,\n createdAt: streamData?.createdAt ?? now,\n updatedAt: now,\n };\n\n transaction.set(streamRef, updatedMetadata);\n\n // 7. Update global position counter\n transaction.set(counterRef, {\n value: globalPosition,\n updatedAt: now,\n });\n\n safeLog.debug(this.logger, 'Events written to transaction', {\n streamName,\n count: events.length,\n newVersion,\n });\n\n // 8. Return result\n return {\n nextExpectedStreamVersion: BigInt(newVersion),\n createdNewStream: !streamExists,\n };\n }\n}\n\n/**\n * Creates an Emmett-compatible `EventStore` wrapper around a Firestore event store.\n */\nfunction normalizeReadOptions(\n options?: EmmettReadStreamOptions<bigint>,\n): ReadStreamOptions | undefined {\n if (!options) return undefined;\n\n const result: ReadStreamOptions = {};\n\n if ('from' in options) {\n result.from = options.from;\n if ('maxCount' in options && options.maxCount !== undefined) {\n result.maxCount = Number(options.maxCount);\n }\n }\n\n if ('to' in options) {\n result.to = options.to;\n }\n\n return result;\n}\n\nexport function asEventStore(\n store: FirestoreEventStore,\n): EventStore<FirestoreReadEventMetadata> {\n async function readStream<EventType extends Event>(\n streamName: string,\n options?: EmmettReadStreamOptions<bigint>,\n ): Promise<ReadStreamResult<EventType, FirestoreReadEventMetadata>> {\n const events = await store.readStream<EventType>(streamName, normalizeReadOptions(options));\n const streamExists = events.length > 0;\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n currentStreamVersion,\n events,\n streamExists,\n };\n }\n\n function aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: AggregateStreamOptions<State, EventType, FirestoreReadEventMetadata>,\n ): Promise<AggregateStreamResult<State, bigint>> {\n return store.aggregateStream(streamName, {\n evolve: options.evolve,\n initialState: options.initialState,\n read: normalizeReadOptions(options.read),\n });\n }\n\n function appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult> {\n return store.appendToStream(streamName, events, options);\n }\n\n return {\n readStream,\n aggregateStream,\n appendToStream,\n };\n}\n\n/**\n * Factory function to create a Firestore event store\n *\n * @param firestore - Firestore instance\n * @param options - Optional configuration\n * @returns Firestore event store instance\n *\n * @example\n * ```typescript\n * import { Firestore } from '@google-cloud/firestore';\n * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';\n *\n * const firestore = new Firestore({ projectId: 'my-project' });\n * const eventStore = getFirestoreEventStore(firestore);\n * ```\n */\nexport function getFirestoreEventStore(\n firestore: Firestore,\n options?: FirestoreEventStoreOptions,\n): FirestoreEventStore {\n return new FirestoreEventStoreImpl(firestore, options);\n}\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -84,13 +84,41 @@ function calculateNextVersion(currentVersion, eventCount) {
|
|
|
84
84
|
|
|
85
85
|
// src/eventStore/firestoreEventStore.ts
|
|
86
86
|
var tracer = trace.getTracer("@emmett-community/emmett-google-firestore");
|
|
87
|
-
function
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
logFn.call(logger, msg, data);
|
|
87
|
+
function normalizeContext(data) {
|
|
88
|
+
if (data === void 0 || data === null) return {};
|
|
89
|
+
if (typeof data === "object" && !Array.isArray(data)) {
|
|
90
|
+
return { ...data };
|
|
92
91
|
}
|
|
92
|
+
return { data };
|
|
93
93
|
}
|
|
94
|
+
function normalizeErrorContext(error) {
|
|
95
|
+
if (error === void 0 || error === null) return {};
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
return { err: error };
|
|
98
|
+
}
|
|
99
|
+
if (typeof error === "object" && !Array.isArray(error)) {
|
|
100
|
+
return { ...error };
|
|
101
|
+
}
|
|
102
|
+
return { err: error };
|
|
103
|
+
}
|
|
104
|
+
var safeLog = {
|
|
105
|
+
debug: (logger, msg, data) => {
|
|
106
|
+
if (!logger) return;
|
|
107
|
+
logger.debug(normalizeContext(data), msg);
|
|
108
|
+
},
|
|
109
|
+
info: (logger, msg, data) => {
|
|
110
|
+
if (!logger) return;
|
|
111
|
+
logger.info(normalizeContext(data), msg);
|
|
112
|
+
},
|
|
113
|
+
warn: (logger, msg, data) => {
|
|
114
|
+
if (!logger) return;
|
|
115
|
+
logger.warn(normalizeContext(data), msg);
|
|
116
|
+
},
|
|
117
|
+
error: (logger, msg, error) => {
|
|
118
|
+
if (!logger) return;
|
|
119
|
+
logger.error(normalizeErrorContext(error), msg);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
94
122
|
var DEFAULT_COLLECTIONS = {
|
|
95
123
|
streams: "streams",
|
|
96
124
|
counters: "_counters"
|
|
@@ -103,25 +131,27 @@ var FirestoreEventStoreImpl = class {
|
|
|
103
131
|
...options.collections
|
|
104
132
|
};
|
|
105
133
|
this.logger = options.observability?.logger;
|
|
106
|
-
safeLog(this.logger, "
|
|
134
|
+
safeLog.info(this.logger, "FirestoreEventStore initialized");
|
|
107
135
|
}
|
|
108
136
|
collections;
|
|
109
137
|
logger;
|
|
110
138
|
/**
|
|
111
139
|
* Read events from a stream
|
|
112
140
|
*/
|
|
113
|
-
async readStream(streamName, options
|
|
141
|
+
async readStream(streamName, options) {
|
|
114
142
|
const span = tracer.startSpan("emmett.firestore.read_stream", {
|
|
115
143
|
attributes: { "emmett.stream_name": streamName }
|
|
116
144
|
});
|
|
117
145
|
try {
|
|
118
|
-
|
|
146
|
+
const from = options && "from" in options ? options.from : void 0;
|
|
147
|
+
const to = options && "to" in options ? options.to : void 0;
|
|
148
|
+
const maxCountLimit = options && "maxCount" in options && options.maxCount !== void 0 ? Number(options.maxCount) : void 0;
|
|
149
|
+
safeLog.debug(this.logger, "Reading stream", {
|
|
119
150
|
streamName,
|
|
120
|
-
from:
|
|
121
|
-
to:
|
|
122
|
-
maxCount:
|
|
151
|
+
from: from?.toString(),
|
|
152
|
+
to: to?.toString(),
|
|
153
|
+
maxCount: maxCountLimit
|
|
123
154
|
});
|
|
124
|
-
const { from, to, maxCount } = options;
|
|
125
155
|
let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
|
|
126
156
|
if (from !== void 0) {
|
|
127
157
|
query = query.where("streamVersion", ">=", Number(from));
|
|
@@ -129,8 +159,8 @@ var FirestoreEventStoreImpl = class {
|
|
|
129
159
|
if (to !== void 0) {
|
|
130
160
|
query = query.where("streamVersion", "<=", Number(to));
|
|
131
161
|
}
|
|
132
|
-
if (
|
|
133
|
-
query = query.limit(
|
|
162
|
+
if (maxCountLimit !== void 0 && maxCountLimit > 0) {
|
|
163
|
+
query = query.limit(maxCountLimit);
|
|
134
164
|
}
|
|
135
165
|
const snapshot = await query.get();
|
|
136
166
|
const events = snapshot.docs.map((doc) => {
|
|
@@ -150,7 +180,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
150
180
|
});
|
|
151
181
|
span.setAttribute("emmett.event_count", events.length);
|
|
152
182
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
153
|
-
safeLog(this.logger, "
|
|
183
|
+
safeLog.debug(this.logger, "Stream read completed", {
|
|
154
184
|
streamName,
|
|
155
185
|
eventCount: events.length
|
|
156
186
|
});
|
|
@@ -158,7 +188,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
158
188
|
} catch (error) {
|
|
159
189
|
span.recordException(error);
|
|
160
190
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
161
|
-
safeLog(this.logger, "
|
|
191
|
+
safeLog.error(this.logger, "Failed to read stream", {
|
|
162
192
|
streamName,
|
|
163
193
|
error
|
|
164
194
|
});
|
|
@@ -197,7 +227,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
197
227
|
throw new Error("Cannot append empty event array");
|
|
198
228
|
}
|
|
199
229
|
const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;
|
|
200
|
-
safeLog(this.logger, "
|
|
230
|
+
safeLog.debug(this.logger, "Appending to stream", {
|
|
201
231
|
streamName,
|
|
202
232
|
eventCount: events.length,
|
|
203
233
|
eventTypes: events.map((e) => e.type),
|
|
@@ -214,7 +244,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
214
244
|
span.setAttribute("emmett.new_version", Number(result.nextExpectedStreamVersion));
|
|
215
245
|
span.setAttribute("emmett.created_new_stream", result.createdNewStream);
|
|
216
246
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
217
|
-
safeLog(this.logger, "
|
|
247
|
+
safeLog.debug(this.logger, "Append completed", {
|
|
218
248
|
streamName,
|
|
219
249
|
newVersion: result.nextExpectedStreamVersion.toString(),
|
|
220
250
|
createdNewStream: result.createdNewStream
|
|
@@ -224,13 +254,13 @@ var FirestoreEventStoreImpl = class {
|
|
|
224
254
|
span.recordException(error);
|
|
225
255
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
226
256
|
if (error instanceof ExpectedVersionConflictError) {
|
|
227
|
-
safeLog(this.logger, "
|
|
257
|
+
safeLog.warn(this.logger, "Version conflict during append", {
|
|
228
258
|
streamName,
|
|
229
259
|
expected: String(error.expected),
|
|
230
260
|
actual: String(error.actual)
|
|
231
261
|
});
|
|
232
262
|
} else {
|
|
233
|
-
safeLog(this.logger, "
|
|
263
|
+
safeLog.error(this.logger, "Failed to append to stream", {
|
|
234
264
|
streamName,
|
|
235
265
|
error
|
|
236
266
|
});
|
|
@@ -256,7 +286,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
256
286
|
streamExists,
|
|
257
287
|
streamData?.version
|
|
258
288
|
);
|
|
259
|
-
safeLog(this.logger, "
|
|
289
|
+
safeLog.debug(this.logger, "Read stream metadata", {
|
|
260
290
|
streamName,
|
|
261
291
|
exists: streamExists,
|
|
262
292
|
currentVersion: currentVersion === STREAM_DOES_NOT_EXIST ? "none" : currentVersion.toString()
|
|
@@ -297,7 +327,7 @@ var FirestoreEventStoreImpl = class {
|
|
|
297
327
|
value: globalPosition,
|
|
298
328
|
updatedAt: now
|
|
299
329
|
});
|
|
300
|
-
safeLog(this.logger, "
|
|
330
|
+
safeLog.debug(this.logger, "Events written to transaction", {
|
|
301
331
|
streamName,
|
|
302
332
|
count: events.length,
|
|
303
333
|
newVersion
|
|
@@ -308,10 +338,51 @@ var FirestoreEventStoreImpl = class {
|
|
|
308
338
|
};
|
|
309
339
|
}
|
|
310
340
|
};
|
|
341
|
+
function normalizeReadOptions(options) {
|
|
342
|
+
if (!options) return void 0;
|
|
343
|
+
const result = {};
|
|
344
|
+
if ("from" in options) {
|
|
345
|
+
result.from = options.from;
|
|
346
|
+
if ("maxCount" in options && options.maxCount !== void 0) {
|
|
347
|
+
result.maxCount = Number(options.maxCount);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if ("to" in options) {
|
|
351
|
+
result.to = options.to;
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
function asEventStore(store) {
|
|
356
|
+
async function readStream(streamName, options) {
|
|
357
|
+
const events = await store.readStream(streamName, normalizeReadOptions(options));
|
|
358
|
+
const streamExists = events.length > 0;
|
|
359
|
+
const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
|
|
360
|
+
return {
|
|
361
|
+
currentStreamVersion,
|
|
362
|
+
events,
|
|
363
|
+
streamExists
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function aggregateStream(streamName, options) {
|
|
367
|
+
return store.aggregateStream(streamName, {
|
|
368
|
+
evolve: options.evolve,
|
|
369
|
+
initialState: options.initialState,
|
|
370
|
+
read: normalizeReadOptions(options.read)
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function appendToStream(streamName, events, options) {
|
|
374
|
+
return store.appendToStream(streamName, events, options);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
readStream,
|
|
378
|
+
aggregateStream,
|
|
379
|
+
appendToStream
|
|
380
|
+
};
|
|
381
|
+
}
|
|
311
382
|
function getFirestoreEventStore(firestore, options) {
|
|
312
383
|
return new FirestoreEventStoreImpl(firestore, options);
|
|
313
384
|
}
|
|
314
385
|
|
|
315
|
-
export { ExpectedVersionConflictError, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
386
|
+
export { ExpectedVersionConflictError, asEventStore, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
|
|
316
387
|
//# sourceMappingURL=index.mjs.map
|
|
317
388
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/eventStore/types.ts","../src/eventStore/utils.ts","../src/eventStore/firestoreEventStore.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { Firestore, Timestamp } from '@google-cloud/firestore';\nimport type { Event, ReadEvent, ReadEventMetadataWithGlobalPosition } from '@event-driven-io/emmett';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n type ExpectedStreamVersion as EmmettExpectedStreamVersion,\n} from '@event-driven-io/emmett';\n\n/**\n * Minimal logger interface compatible with Pino and other loggers.\n * All methods are optional to support varying logger implementations.\n */\nexport interface Logger {\n debug?(msg: string, data?: unknown): void;\n info?(msg: string, data?: unknown): void;\n warn?(msg: string, data?: unknown): void;\n error?(msg: string, err?: unknown): void;\n}\n\n/**\n * Observability configuration options\n */\nexport interface ObservabilityOptions {\n /** Optional logger instance. If not provided, no logging occurs. */\n logger?: Logger;\n}\n\n/**\n * Expected version for stream operations\n * Uses Emmett's standard version constants for full compatibility\n * - number | bigint: Expect specific version\n * - STREAM_DOES_NOT_EXIST: Stream must not exist\n * - STREAM_EXISTS: Stream must exist (any version)\n * - NO_CONCURRENCY_CHECK: No version check\n */\nexport type ExpectedStreamVersion = EmmettExpectedStreamVersion<bigint>;\n\n// Re-export Emmett constants for convenience\nexport { STREAM_DOES_NOT_EXIST, STREAM_EXISTS, NO_CONCURRENCY_CHECK };\n\n/**\n * Options for appending events to a stream\n */\nexport interface AppendToStreamOptions {\n expectedStreamVersion?: ExpectedStreamVersion;\n}\n\n/**\n * Result of appending events to a stream\n */\nexport interface AppendToStreamResult {\n nextExpectedStreamVersion: bigint;\n createdNewStream: boolean;\n}\n\n/**\n * Options for reading events from a stream\n */\nexport interface ReadStreamOptions {\n from?: bigint;\n to?: bigint;\n maxCount?: number;\n}\n\n/**\n * Metadata stored in Firestore stream document\n */\nexport interface StreamMetadata {\n version: number;\n createdAt: Timestamp;\n updatedAt: Timestamp;\n}\n\n/**\n * Event document structure in Firestore\n */\nexport interface EventDocument {\n type: string;\n data: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n timestamp: Timestamp;\n globalPosition: number;\n streamVersion: number;\n}\n\n/**\n * Firestore-specific read event metadata\n */\nexport interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {\n streamName: string;\n streamVersion: bigint;\n timestamp: Date;\n}\n\n/**\n * Firestore read event\n */\nexport type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<\n EventType,\n FirestoreReadEventMetadata\n>;\n\n/**\n * Collection configuration for Firestore event store\n */\nexport interface CollectionConfig {\n streams: string;\n counters: string;\n}\n\n/**\n * Firestore event store options\n */\nexport interface FirestoreEventStoreOptions {\n collections?: Partial<CollectionConfig>;\n observability?: ObservabilityOptions;\n}\n\n/**\n * Firestore event store interface\n */\nexport interface FirestoreEventStore {\n /**\n * The underlying Firestore instance\n */\n readonly firestore: Firestore;\n\n /**\n * Collection names configuration\n */\n readonly collections: CollectionConfig;\n\n /**\n * Read events from a stream\n */\n readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]>;\n\n /**\n * Aggregate stream by applying events to state\n */\n aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }>;\n\n /**\n * Append events to a stream\n */\n appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult>;\n}\n\n/**\n * Error thrown when expected version doesn't match current version\n */\nexport class ExpectedVersionConflictError extends Error {\n constructor(\n public readonly streamName: string,\n public readonly expected: ExpectedStreamVersion,\n public readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST,\n ) {\n super(\n `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`,\n );\n this.name = 'ExpectedVersionConflictError';\n Object.setPrototypeOf(this, ExpectedVersionConflictError.prototype);\n }\n}\n","import type { Timestamp } from '@google-cloud/firestore';\nimport type { ExpectedStreamVersion } from './types';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n ExpectedVersionConflictError,\n} from './types';\n\n/**\n * Pad version number with leading zeros for Firestore document IDs\n * This ensures automatic ordering by version in Firestore\n *\n * @param version - The version number to pad\n * @returns Zero-padded string of length 10\n *\n * @example\n * padVersion(0) // \"0000000000\"\n * padVersion(42) // \"0000000042\"\n * padVersion(12345) // \"0000012345\"\n */\nexport function padVersion(version: number | bigint): string {\n return version.toString().padStart(10, '0');\n}\n\n/**\n * Parse a stream name into type and ID components\n *\n * @param streamName - Stream name in format \"Type-id\" or \"Type-with-dashes-id\"\n * @returns Object with streamType and streamId\n *\n * @example\n * parseStreamName(\"User-123\") // { streamType: \"User\", streamId: \"123\" }\n * parseStreamName(\"ShoppingCart-abc-def-123\") // { streamType: \"ShoppingCart\", streamId: \"abc-def-123\" }\n */\nexport function parseStreamName(streamName: string): {\n streamType: string;\n streamId: string;\n} {\n const firstDashIndex = streamName.indexOf('-');\n\n if (firstDashIndex === -1) {\n return {\n streamType: streamName,\n streamId: '',\n };\n }\n\n return {\n streamType: streamName.substring(0, firstDashIndex),\n streamId: streamName.substring(firstDashIndex + 1),\n };\n}\n\n/**\n * Convert Firestore Timestamp to JavaScript Date\n *\n * @param timestamp - Firestore Timestamp\n * @returns JavaScript Date object\n */\nexport function timestampToDate(timestamp: Timestamp): Date {\n return timestamp.toDate();\n}\n\n/**\n * Validate expected version against current version\n *\n * @param streamName - Stream name for error messages\n * @param expectedVersion - Expected version constraint\n * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)\n * @throws ExpectedVersionConflictError if versions don't match\n */\nexport function assertExpectedVersionMatchesCurrent(\n streamName: string,\n expectedVersion: ExpectedStreamVersion,\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n): void {\n // NO_CONCURRENCY_CHECK - no validation needed\n if (expectedVersion === NO_CONCURRENCY_CHECK) {\n return;\n }\n\n // STREAM_DOES_NOT_EXIST - stream must not exist\n if (expectedVersion === STREAM_DOES_NOT_EXIST) {\n if (currentVersion !== STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // STREAM_EXISTS - stream must exist\n if (expectedVersion === STREAM_EXISTS) {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // Specific version number\n const expectedBigInt = BigInt(expectedVersion);\n if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n}\n\n/**\n * Get the current stream version from metadata\n *\n * @param streamExists - Whether the stream document exists\n * @param version - Version number from Firestore (if stream exists)\n * @returns Current version as bigint or STREAM_DOES_NOT_EXIST\n */\nexport function getCurrentStreamVersion(\n streamExists: boolean,\n version?: number,\n): bigint | typeof STREAM_DOES_NOT_EXIST {\n if (!streamExists) {\n return STREAM_DOES_NOT_EXIST;\n }\n return BigInt(version ?? -1);\n}\n\n/**\n * Calculate the next expected stream version after appending events\n *\n * @param currentVersion - Current stream version\n * @param eventCount - Number of events being appended\n * @returns Next expected version as bigint\n */\nexport function calculateNextVersion(\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n eventCount: number,\n): bigint {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n return BigInt(eventCount - 1);\n }\n // Type assertion needed because TypeScript doesn't narrow ExpectedStreamVersionGeneral properly\n return (currentVersion as bigint) + BigInt(eventCount);\n}\n","import type { Firestore, Transaction, Timestamp } from '@google-cloud/firestore';\nimport type { Event } from '@event-driven-io/emmett';\nimport { trace, SpanStatusCode } from '@opentelemetry/api';\nimport type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n Logger,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, ExpectedVersionConflictError } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\n\nconst tracer = trace.getTracer('@emmett-community/emmett-google-firestore');\n\n/**\n * Safe logging helper that handles undefined logger methods\n */\nfunction safeLog(\n logger: Logger | undefined,\n level: keyof Logger,\n msg: string,\n data?: unknown,\n): void {\n if (!logger) return;\n const logFn = logger[level];\n if (typeof logFn === 'function') {\n logFn.call(logger, msg, data);\n }\n}\n\nconst DEFAULT_COLLECTIONS: CollectionConfig = {\n streams: 'streams',\n counters: '_counters',\n};\n\n/**\n * Firestore Event Store Implementation\n *\n * Stores events in Firestore using a subcollection pattern:\n * - /streams/{streamName} - Stream metadata (version, timestamps)\n * - /streams/{streamName}/events/{version} - Individual events\n * - /_counters/global_position - Global event counter\n */\nexport class FirestoreEventStoreImpl implements FirestoreEventStore {\n public readonly collections: CollectionConfig;\n private readonly logger: Logger | undefined;\n\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\n this.logger = options.observability?.logger;\n\n safeLog(this.logger, 'info', 'FirestoreEventStore initialized');\n }\n\n /**\n * Read events from a stream\n */\n async readStream<EventType extends Event>(\n streamName: string,\n options: ReadStreamOptions = {},\n ): Promise<FirestoreReadEvent<EventType>[]> {\n const span = tracer.startSpan('emmett.firestore.read_stream', {\n attributes: { 'emmett.stream_name': streamName },\n });\n\n try {\n safeLog(this.logger, 'debug', 'Reading stream', {\n streamName,\n from: options.from?.toString(),\n to: options.to?.toString(),\n maxCount: options.maxCount,\n });\n\n const { from, to, maxCount } = options;\n\n // Reference to events subcollection\n let query = this.firestore\n .collection(this.collections.streams)\n .doc(streamName)\n .collection('events')\n .orderBy('streamVersion', 'asc');\n\n // Apply range filters\n if (from !== undefined) {\n query = query.where('streamVersion', '>=', Number(from));\n }\n if (to !== undefined) {\n query = query.where('streamVersion', '<=', Number(to));\n }\n if (maxCount !== undefined && maxCount > 0) {\n query = query.limit(maxCount);\n }\n\n // Execute query\n const snapshot = await query.get();\n\n // Transform Firestore documents to events\n const events = snapshot.docs.map((doc) => {\n const data = doc.data() as EventDocument;\n return {\n type: data.type,\n data: data.data,\n metadata: {\n ...data.metadata,\n streamName,\n streamVersion: BigInt(data.streamVersion),\n streamPosition: BigInt(data.streamVersion),\n globalPosition: BigInt(data.globalPosition),\n timestamp: timestampToDate(data.timestamp),\n },\n } as FirestoreReadEvent<EventType>;\n });\n\n span.setAttribute('emmett.event_count', events.length);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog(this.logger, 'debug', 'Stream read completed', {\n streamName,\n eventCount: events.length,\n });\n\n return events;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n safeLog(this.logger, 'error', 'Failed to read stream', {\n streamName,\n error,\n });\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Aggregate stream by applying events to state\n */\n async aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }> {\n const { evolve, initialState, read } = options;\n const events = await this.readStream<EventType>(streamName, read);\n\n const streamExists = events.length > 0;\n const state = events.reduce(evolve, initialState());\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n state,\n currentStreamVersion,\n streamExists,\n };\n }\n\n /**\n * Append events to a stream with optimistic concurrency control\n */\n async appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options: AppendToStreamOptions = {},\n ): Promise<AppendToStreamResult> {\n const span = tracer.startSpan('emmett.firestore.append_to_stream', {\n attributes: {\n 'emmett.stream_name': streamName,\n 'emmett.event_count': events.length,\n },\n });\n\n try {\n if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n safeLog(this.logger, 'debug', 'Appending to stream', {\n streamName,\n eventCount: events.length,\n eventTypes: events.map((e) => e.type),\n expectedVersion: String(expectedStreamVersion),\n });\n\n // Execute in transaction for atomicity\n const result = await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n\n span.setAttribute('emmett.new_version', Number(result.nextExpectedStreamVersion));\n span.setAttribute('emmett.created_new_stream', result.createdNewStream);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog(this.logger, 'debug', 'Append completed', {\n streamName,\n newVersion: result.nextExpectedStreamVersion.toString(),\n createdNewStream: result.createdNewStream,\n });\n\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n if (error instanceof ExpectedVersionConflictError) {\n safeLog(this.logger, 'warn', 'Version conflict during append', {\n streamName,\n expected: String(error.expected),\n actual: String(error.actual),\n });\n } else {\n safeLog(this.logger, 'error', 'Failed to append to stream', {\n streamName,\n error,\n });\n }\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Internal method to append events within a transaction\n *\n * Note: No separate span here - this method runs inside appendToStream's span,\n * and Firestore transaction operations are atomic. The parent span captures\n * the full transaction duration.\n */\n private async appendToStreamInTransaction<EventType extends Event>(\n transaction: Transaction,\n streamName: string,\n events: EventType[],\n expectedStreamVersion: ExpectedStreamVersion,\n ): Promise<AppendToStreamResult> {\n // 1. Get stream metadata reference\n const streamRef = this.firestore\n .collection(this.collections.streams)\n .doc(streamName);\n\n const streamDoc = await transaction.get(streamRef);\n const streamExists = streamDoc.exists;\n const streamData = streamDoc.data() as StreamMetadata | undefined;\n\n // 2. Get current version and validate expected version\n const currentVersion = getCurrentStreamVersion(\n streamExists,\n streamData?.version,\n );\n\n safeLog(this.logger, 'debug', 'Read stream metadata', {\n streamName,\n exists: streamExists,\n currentVersion: currentVersion === STREAM_DOES_NOT_EXIST ? 'none' : currentVersion.toString(),\n });\n\n assertExpectedVersionMatchesCurrent(\n streamName,\n expectedStreamVersion,\n currentVersion,\n );\n\n // 3. Get and increment global position counter\n const counterRef = this.firestore\n .collection(this.collections.counters)\n .doc('global_position');\n\n const counterDoc = await transaction.get(counterRef);\n let globalPosition = counterDoc.exists\n ? (counterDoc.data()?.value as number) ?? 0\n : 0;\n\n // 4. Calculate starting version for new events\n const baseVersion =\n currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);\n\n // 5. Append events to subcollection\n const TimestampClass = this.firestore.constructor as unknown as { Timestamp: typeof Timestamp };\n const now = TimestampClass.Timestamp.now();\n\n events.forEach((event, index) => {\n const eventVersion = baseVersion + 1 + index;\n const eventRef = streamRef\n .collection('events')\n .doc(padVersion(eventVersion));\n\n const metadata = (event as { metadata?: Record<string, unknown> }).metadata;\n const eventDocument: EventDocument = {\n type: event.type,\n data: event.data,\n ...(metadata && { metadata }),\n timestamp: now,\n globalPosition: globalPosition++,\n streamVersion: eventVersion,\n };\n\n transaction.set(eventRef, eventDocument);\n });\n\n // 6. Update stream metadata\n const newVersion = baseVersion + events.length;\n const updatedMetadata: StreamMetadata = {\n version: newVersion,\n createdAt: streamData?.createdAt ?? now,\n updatedAt: now,\n };\n\n transaction.set(streamRef, updatedMetadata);\n\n // 7. Update global position counter\n transaction.set(counterRef, {\n value: globalPosition,\n updatedAt: now,\n });\n\n safeLog(this.logger, 'debug', 'Events written to transaction', {\n streamName,\n count: events.length,\n newVersion,\n });\n\n // 8. Return result\n return {\n nextExpectedStreamVersion: BigInt(newVersion),\n createdNewStream: !streamExists,\n };\n }\n}\n\n/**\n * Factory function to create a Firestore event store\n *\n * @param firestore - Firestore instance\n * @param options - Optional configuration\n * @returns Firestore event store instance\n *\n * @example\n * ```typescript\n * import { Firestore } from '@google-cloud/firestore';\n * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';\n *\n * const firestore = new Firestore({ projectId: 'my-project' });\n * const eventStore = getFirestoreEventStore(firestore);\n * ```\n */\nexport function getFirestoreEventStore(\n firestore: Firestore,\n options?: FirestoreEventStoreOptions,\n): FirestoreEventStore {\n return new FirestoreEventStoreImpl(firestore, options);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/eventStore/types.ts","../src/eventStore/utils.ts","../src/eventStore/firestoreEventStore.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { Firestore, Timestamp } from '@google-cloud/firestore';\nimport type { Event, ReadEvent, ReadEventMetadataWithGlobalPosition } from '@event-driven-io/emmett';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n type ExpectedStreamVersion as EmmettExpectedStreamVersion,\n} from '@event-driven-io/emmett';\n\n/**\n * Canonical Logger contract for the Emmett ecosystem.\n *\n * DO NOT MODIFY this interface without updating ALL packages in the ecosystem.\n *\n * This package defines the canonical Logger interface.\n * Implementations (Pino, Winston, etc.) MUST adapt to this contract.\n * This contract MUST NOT adapt to any specific implementation.\n *\n * Semantic Rules:\n * - context (first parameter): ALWAYS structured data as Record<string, unknown>\n * - message (second parameter): ALWAYS the human-readable log message\n * - The order is NEVER inverted\n * - The (message, data) form is NOT valid for this contract\n * - Error objects MUST use the 'err' key (frozen semantic)\n *\n * @example\n * ```typescript\n * // Pino - native compatibility\n * import pino from 'pino';\n * const logger = pino();\n * // logger.info({ orderId }, 'Order created') matches our contract\n * ```\n */\nexport interface Logger {\n /**\n * Log debug-level message with structured context.\n * @param context - Structured data to include in the log entry\n * @param message - Optional human-readable message\n */\n debug(context: Record<string, unknown>, message?: string): void;\n\n /**\n * Log info-level message with structured context.\n * @param context - Structured data to include in the log entry\n * @param message - Optional human-readable message\n */\n info(context: Record<string, unknown>, message?: string): void;\n\n /**\n * Log warn-level message with structured context.\n * @param context - Structured data to include in the log entry\n * @param message - Optional human-readable message\n */\n warn(context: Record<string, unknown>, message?: string): void;\n\n /**\n * Log error-level message with structured context.\n * @param context - Structured data to include in the log entry (MUST use 'err' key for Error objects)\n * @param message - Optional human-readable message\n */\n error(context: Record<string, unknown>, message?: string): void;\n}\n\n/**\n * Observability configuration options\n */\nexport interface ObservabilityOptions {\n /** Optional logger instance. If not provided, no logging occurs. */\n logger?: Logger;\n}\n\n/**\n * Expected version for stream operations\n * Uses Emmett's standard version constants for full compatibility\n * - number | bigint: Expect specific version\n * - STREAM_DOES_NOT_EXIST: Stream must not exist\n * - STREAM_EXISTS: Stream must exist (any version)\n * - NO_CONCURRENCY_CHECK: No version check\n */\nexport type ExpectedStreamVersion = EmmettExpectedStreamVersion<bigint>;\n\n// Re-export Emmett constants for convenience\nexport { STREAM_DOES_NOT_EXIST, STREAM_EXISTS, NO_CONCURRENCY_CHECK };\n\n/**\n * Options for appending events to a stream\n */\nexport interface AppendToStreamOptions {\n expectedStreamVersion?: ExpectedStreamVersion;\n}\n\n/**\n * Result of appending events to a stream\n */\nexport interface AppendToStreamResult {\n nextExpectedStreamVersion: bigint;\n createdNewStream: boolean;\n}\n\n/**\n * Options for reading events from a stream\n * Reuses the canonical `ReadStreamOptions` shape from the Emmett core types.\n */\nexport interface ReadStreamOptions {\n from?: bigint;\n to?: bigint;\n maxCount?: number;\n}\n\n/**\n * Metadata stored in Firestore stream document\n */\nexport interface StreamMetadata {\n version: number;\n createdAt: Timestamp;\n updatedAt: Timestamp;\n}\n\n/**\n * Event document structure in Firestore\n */\nexport interface EventDocument {\n type: string;\n data: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n timestamp: Timestamp;\n globalPosition: number;\n streamVersion: number;\n}\n\n/**\n * Firestore-specific read event metadata\n */\nexport interface FirestoreReadEventMetadata extends ReadEventMetadataWithGlobalPosition {\n streamName: string;\n streamVersion: bigint;\n timestamp: Date;\n}\n\n/**\n * Firestore read event\n */\nexport type FirestoreReadEvent<EventType extends Event = Event> = ReadEvent<\n EventType,\n FirestoreReadEventMetadata\n>;\n\n/**\n * Collection configuration for Firestore event store\n */\nexport interface CollectionConfig {\n streams: string;\n counters: string;\n}\n\n/**\n * Firestore event store options\n */\nexport interface FirestoreEventStoreOptions {\n collections?: Partial<CollectionConfig>;\n observability?: ObservabilityOptions;\n}\n\n/**\n * Firestore event store interface\n */\nexport interface FirestoreEventStore {\n /**\n * The underlying Firestore instance\n */\n readonly firestore: Firestore;\n\n /**\n * Collection names configuration\n */\n readonly collections: CollectionConfig;\n\n /**\n * Read events from a stream\n */\n readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]>;\n\n /**\n * Aggregate stream by applying events to state\n */\n aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }>;\n\n /**\n * Append events to a stream\n */\n appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult>;\n}\n\n/**\n * Error thrown when expected version doesn't match current version\n */\nexport class ExpectedVersionConflictError extends Error {\n constructor(\n public readonly streamName: string,\n public readonly expected: ExpectedStreamVersion,\n public readonly actual: bigint | typeof STREAM_DOES_NOT_EXIST,\n ) {\n super(\n `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`,\n );\n this.name = 'ExpectedVersionConflictError';\n Object.setPrototypeOf(this, ExpectedVersionConflictError.prototype);\n }\n}\n","import type { Timestamp } from '@google-cloud/firestore';\nimport type { ExpectedStreamVersion } from './types';\nimport {\n STREAM_DOES_NOT_EXIST,\n STREAM_EXISTS,\n NO_CONCURRENCY_CHECK,\n ExpectedVersionConflictError,\n} from './types';\n\n/**\n * Pad version number with leading zeros for Firestore document IDs\n * This ensures automatic ordering by version in Firestore\n *\n * @param version - The version number to pad\n * @returns Zero-padded string of length 10\n *\n * @example\n * padVersion(0) // \"0000000000\"\n * padVersion(42) // \"0000000042\"\n * padVersion(12345) // \"0000012345\"\n */\nexport function padVersion(version: number | bigint): string {\n return version.toString().padStart(10, '0');\n}\n\n/**\n * Parse a stream name into type and ID components\n *\n * @param streamName - Stream name in format \"Type-id\" or \"Type-with-dashes-id\"\n * @returns Object with streamType and streamId\n *\n * @example\n * parseStreamName(\"User-123\") // { streamType: \"User\", streamId: \"123\" }\n * parseStreamName(\"ShoppingCart-abc-def-123\") // { streamType: \"ShoppingCart\", streamId: \"abc-def-123\" }\n */\nexport function parseStreamName(streamName: string): {\n streamType: string;\n streamId: string;\n} {\n const firstDashIndex = streamName.indexOf('-');\n\n if (firstDashIndex === -1) {\n return {\n streamType: streamName,\n streamId: '',\n };\n }\n\n return {\n streamType: streamName.substring(0, firstDashIndex),\n streamId: streamName.substring(firstDashIndex + 1),\n };\n}\n\n/**\n * Convert Firestore Timestamp to JavaScript Date\n *\n * @param timestamp - Firestore Timestamp\n * @returns JavaScript Date object\n */\nexport function timestampToDate(timestamp: Timestamp): Date {\n return timestamp.toDate();\n}\n\n/**\n * Validate expected version against current version\n *\n * @param streamName - Stream name for error messages\n * @param expectedVersion - Expected version constraint\n * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)\n * @throws ExpectedVersionConflictError if versions don't match\n */\nexport function assertExpectedVersionMatchesCurrent(\n streamName: string,\n expectedVersion: ExpectedStreamVersion,\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n): void {\n // NO_CONCURRENCY_CHECK - no validation needed\n if (expectedVersion === NO_CONCURRENCY_CHECK) {\n return;\n }\n\n // STREAM_DOES_NOT_EXIST - stream must not exist\n if (expectedVersion === STREAM_DOES_NOT_EXIST) {\n if (currentVersion !== STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // STREAM_EXISTS - stream must exist\n if (expectedVersion === STREAM_EXISTS) {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n return;\n }\n\n // Specific version number\n const expectedBigInt = BigInt(expectedVersion);\n if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {\n throw new ExpectedVersionConflictError(\n streamName,\n expectedVersion,\n currentVersion,\n );\n }\n}\n\n/**\n * Get the current stream version from metadata\n *\n * @param streamExists - Whether the stream document exists\n * @param version - Version number from Firestore (if stream exists)\n * @returns Current version as bigint or STREAM_DOES_NOT_EXIST\n */\nexport function getCurrentStreamVersion(\n streamExists: boolean,\n version?: number,\n): bigint | typeof STREAM_DOES_NOT_EXIST {\n if (!streamExists) {\n return STREAM_DOES_NOT_EXIST;\n }\n return BigInt(version ?? -1);\n}\n\n/**\n * Calculate the next expected stream version after appending events\n *\n * @param currentVersion - Current stream version\n * @param eventCount - Number of events being appended\n * @returns Next expected version as bigint\n */\nexport function calculateNextVersion(\n currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST,\n eventCount: number,\n): bigint {\n if (currentVersion === STREAM_DOES_NOT_EXIST) {\n return BigInt(eventCount - 1);\n }\n // Type assertion needed because TypeScript doesn't narrow ExpectedStreamVersionGeneral properly\n return (currentVersion as bigint) + BigInt(eventCount);\n}\n","import type { Firestore, Transaction, Timestamp } from '@google-cloud/firestore';\nimport type {\n AggregateStreamOptions,\n AggregateStreamResult,\n Event,\n EventStore,\n ReadStreamOptions as EmmettReadStreamOptions,\n ReadStreamResult,\n} from '@event-driven-io/emmett';\nimport { trace, SpanStatusCode } from '@opentelemetry/api';\nimport type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n FirestoreReadEventMetadata,\n Logger,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, ExpectedVersionConflictError } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\n\nconst tracer = trace.getTracer('@emmett-community/emmett-google-firestore');\n\n/**\n * @internal - NOT part of public API\n * Normalizes data to a context object for the Logger contract.\n */\nfunction normalizeContext(data: unknown): Record<string, unknown> {\n if (data === undefined || data === null) return {};\n if (typeof data === 'object' && !Array.isArray(data)) {\n return { ...(data as Record<string, unknown>) };\n }\n return { data };\n}\n\n/**\n * @internal - NOT part of public API\n * Normalizes error data to a context object for the Logger contract.\n * Uses 'err' key for Error instances (Pino compatibility).\n */\nfunction normalizeErrorContext(error: unknown): Record<string, unknown> {\n if (error === undefined || error === null) return {};\n if (error instanceof Error) {\n return { err: error };\n }\n if (typeof error === 'object' && !Array.isArray(error)) {\n return { ...(error as Record<string, unknown>) };\n }\n return { err: error };\n}\n\n/**\n * @internal - NOT part of public API\n *\n * Internal helper for ergonomic logging within this package.\n * Translates internal (msg, data) calls to canonical (context, message) contract.\n *\n * This is the ONLY point where translation from internal format to contract format occurs.\n */\nconst safeLog = {\n debug: (logger: Logger | undefined, msg: string, data?: unknown): void => {\n if (!logger) return;\n logger.debug(normalizeContext(data), msg);\n },\n info: (logger: Logger | undefined, msg: string, data?: unknown): void => {\n if (!logger) return;\n logger.info(normalizeContext(data), msg);\n },\n warn: (logger: Logger | undefined, msg: string, data?: unknown): void => {\n if (!logger) return;\n logger.warn(normalizeContext(data), msg);\n },\n error: (logger: Logger | undefined, msg: string, error?: unknown): void => {\n if (!logger) return;\n logger.error(normalizeErrorContext(error), msg);\n },\n};\n\nconst DEFAULT_COLLECTIONS: CollectionConfig = {\n streams: 'streams',\n counters: '_counters',\n};\n\n/**\n * Firestore Event Store Implementation\n *\n * Stores events in Firestore using a subcollection pattern:\n * - /streams/{streamName} - Stream metadata (version, timestamps)\n * - /streams/{streamName}/events/{version} - Individual events\n * - /_counters/global_position - Global event counter\n */\nexport class FirestoreEventStoreImpl implements FirestoreEventStore {\n public readonly collections: CollectionConfig;\n private readonly logger: Logger | undefined;\n\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\n this.logger = options.observability?.logger;\n\n safeLog.info(this.logger, 'FirestoreEventStore initialized');\n }\n\n /**\n * Read events from a stream\n */\n async readStream<EventType extends Event>(\n streamName: string,\n options?: ReadStreamOptions,\n ): Promise<FirestoreReadEvent<EventType>[]> {\n const span = tracer.startSpan('emmett.firestore.read_stream', {\n attributes: { 'emmett.stream_name': streamName },\n });\n\n try {\n const from = options && 'from' in options ? options.from : undefined;\n const to = options && 'to' in options ? options.to : undefined;\n const maxCountLimit =\n options && 'maxCount' in options && options.maxCount !== undefined\n ? Number(options.maxCount)\n : undefined;\n\n safeLog.debug(this.logger, 'Reading stream', {\n streamName,\n from: from?.toString(),\n to: to?.toString(),\n maxCount: maxCountLimit,\n });\n\n // Reference to events subcollection\n let query = this.firestore\n .collection(this.collections.streams)\n .doc(streamName)\n .collection('events')\n .orderBy('streamVersion', 'asc');\n\n // Apply range filters\n if (from !== undefined) {\n query = query.where('streamVersion', '>=', Number(from));\n }\n if (to !== undefined) {\n query = query.where('streamVersion', '<=', Number(to));\n }\n if (maxCountLimit !== undefined && maxCountLimit > 0) {\n query = query.limit(maxCountLimit);\n }\n\n // Execute query\n const snapshot = await query.get();\n\n // Transform Firestore documents to events\n const events = snapshot.docs.map((doc) => {\n const data = doc.data() as EventDocument;\n return {\n type: data.type,\n data: data.data,\n metadata: {\n ...data.metadata,\n streamName,\n streamVersion: BigInt(data.streamVersion),\n streamPosition: BigInt(data.streamVersion),\n globalPosition: BigInt(data.globalPosition),\n timestamp: timestampToDate(data.timestamp),\n },\n } as FirestoreReadEvent<EventType>;\n });\n\n span.setAttribute('emmett.event_count', events.length);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog.debug(this.logger, 'Stream read completed', {\n streamName,\n eventCount: events.length,\n });\n\n return events;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n safeLog.error(this.logger, 'Failed to read stream', {\n streamName,\n error,\n });\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Aggregate stream by applying events to state\n */\n async aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: {\n evolve: (state: State, event: FirestoreReadEvent<EventType>) => State;\n initialState: () => State;\n read?: ReadStreamOptions;\n },\n ): Promise<{\n state: State;\n currentStreamVersion: bigint;\n streamExists: boolean;\n }> {\n const { evolve, initialState, read } = options;\n const events = await this.readStream<EventType>(streamName, read);\n\n const streamExists = events.length > 0;\n const state = events.reduce(evolve, initialState());\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n state,\n currentStreamVersion,\n streamExists,\n };\n }\n\n /**\n * Append events to a stream with optimistic concurrency control\n */\n async appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options: AppendToStreamOptions = {},\n ): Promise<AppendToStreamResult> {\n const span = tracer.startSpan('emmett.firestore.append_to_stream', {\n attributes: {\n 'emmett.stream_name': streamName,\n 'emmett.event_count': events.length,\n },\n });\n\n try {\n if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n safeLog.debug(this.logger, 'Appending to stream', {\n streamName,\n eventCount: events.length,\n eventTypes: events.map((e) => e.type),\n expectedVersion: String(expectedStreamVersion),\n });\n\n // Execute in transaction for atomicity\n const result = await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n\n span.setAttribute('emmett.new_version', Number(result.nextExpectedStreamVersion));\n span.setAttribute('emmett.created_new_stream', result.createdNewStream);\n span.setStatus({ code: SpanStatusCode.OK });\n\n safeLog.debug(this.logger, 'Append completed', {\n streamName,\n newVersion: result.nextExpectedStreamVersion.toString(),\n createdNewStream: result.createdNewStream,\n });\n\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n\n if (error instanceof ExpectedVersionConflictError) {\n safeLog.warn(this.logger, 'Version conflict during append', {\n streamName,\n expected: String(error.expected),\n actual: String(error.actual),\n });\n } else {\n safeLog.error(this.logger, 'Failed to append to stream', {\n streamName,\n error,\n });\n }\n\n throw error;\n } finally {\n span.end();\n }\n }\n\n /**\n * Internal method to append events within a transaction\n *\n * Note: No separate span here - this method runs inside appendToStream's span,\n * and Firestore transaction operations are atomic. The parent span captures\n * the full transaction duration.\n */\n private async appendToStreamInTransaction<EventType extends Event>(\n transaction: Transaction,\n streamName: string,\n events: EventType[],\n expectedStreamVersion: ExpectedStreamVersion,\n ): Promise<AppendToStreamResult> {\n // 1. Get stream metadata reference\n const streamRef = this.firestore\n .collection(this.collections.streams)\n .doc(streamName);\n\n const streamDoc = await transaction.get(streamRef);\n const streamExists = streamDoc.exists;\n const streamData = streamDoc.data() as StreamMetadata | undefined;\n\n // 2. Get current version and validate expected version\n const currentVersion = getCurrentStreamVersion(\n streamExists,\n streamData?.version,\n );\n\n safeLog.debug(this.logger, 'Read stream metadata', {\n streamName,\n exists: streamExists,\n currentVersion: currentVersion === STREAM_DOES_NOT_EXIST ? 'none' : currentVersion.toString(),\n });\n\n assertExpectedVersionMatchesCurrent(\n streamName,\n expectedStreamVersion,\n currentVersion,\n );\n\n // 3. Get and increment global position counter\n const counterRef = this.firestore\n .collection(this.collections.counters)\n .doc('global_position');\n\n const counterDoc = await transaction.get(counterRef);\n let globalPosition = counterDoc.exists\n ? (counterDoc.data()?.value as number) ?? 0\n : 0;\n\n // 4. Calculate starting version for new events\n const baseVersion =\n currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);\n\n // 5. Append events to subcollection\n const TimestampClass = this.firestore.constructor as unknown as { Timestamp: typeof Timestamp };\n const now = TimestampClass.Timestamp.now();\n\n events.forEach((event, index) => {\n const eventVersion = baseVersion + 1 + index;\n const eventRef = streamRef\n .collection('events')\n .doc(padVersion(eventVersion));\n\n const metadata = (event as { metadata?: Record<string, unknown> }).metadata;\n const eventDocument: EventDocument = {\n type: event.type,\n data: event.data,\n ...(metadata && { metadata }),\n timestamp: now,\n globalPosition: globalPosition++,\n streamVersion: eventVersion,\n };\n\n transaction.set(eventRef, eventDocument);\n });\n\n // 6. Update stream metadata\n const newVersion = baseVersion + events.length;\n const updatedMetadata: StreamMetadata = {\n version: newVersion,\n createdAt: streamData?.createdAt ?? now,\n updatedAt: now,\n };\n\n transaction.set(streamRef, updatedMetadata);\n\n // 7. Update global position counter\n transaction.set(counterRef, {\n value: globalPosition,\n updatedAt: now,\n });\n\n safeLog.debug(this.logger, 'Events written to transaction', {\n streamName,\n count: events.length,\n newVersion,\n });\n\n // 8. Return result\n return {\n nextExpectedStreamVersion: BigInt(newVersion),\n createdNewStream: !streamExists,\n };\n }\n}\n\n/**\n * Creates an Emmett-compatible `EventStore` wrapper around a Firestore event store.\n */\nfunction normalizeReadOptions(\n options?: EmmettReadStreamOptions<bigint>,\n): ReadStreamOptions | undefined {\n if (!options) return undefined;\n\n const result: ReadStreamOptions = {};\n\n if ('from' in options) {\n result.from = options.from;\n if ('maxCount' in options && options.maxCount !== undefined) {\n result.maxCount = Number(options.maxCount);\n }\n }\n\n if ('to' in options) {\n result.to = options.to;\n }\n\n return result;\n}\n\nexport function asEventStore(\n store: FirestoreEventStore,\n): EventStore<FirestoreReadEventMetadata> {\n async function readStream<EventType extends Event>(\n streamName: string,\n options?: EmmettReadStreamOptions<bigint>,\n ): Promise<ReadStreamResult<EventType, FirestoreReadEventMetadata>> {\n const events = await store.readStream<EventType>(streamName, normalizeReadOptions(options));\n const streamExists = events.length > 0;\n const currentStreamVersion = streamExists\n ? events[events.length - 1].metadata.streamVersion\n : BigInt(0);\n\n return {\n currentStreamVersion,\n events,\n streamExists,\n };\n }\n\n function aggregateStream<State, EventType extends Event>(\n streamName: string,\n options: AggregateStreamOptions<State, EventType, FirestoreReadEventMetadata>,\n ): Promise<AggregateStreamResult<State, bigint>> {\n return store.aggregateStream(streamName, {\n evolve: options.evolve,\n initialState: options.initialState,\n read: normalizeReadOptions(options.read),\n });\n }\n\n function appendToStream<EventType extends Event>(\n streamName: string,\n events: EventType[],\n options?: AppendToStreamOptions,\n ): Promise<AppendToStreamResult> {\n return store.appendToStream(streamName, events, options);\n }\n\n return {\n readStream,\n aggregateStream,\n appendToStream,\n };\n}\n\n/**\n * Factory function to create a Firestore event store\n *\n * @param firestore - Firestore instance\n * @param options - Optional configuration\n * @returns Firestore event store instance\n *\n * @example\n * ```typescript\n * import { Firestore } from '@google-cloud/firestore';\n * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';\n *\n * const firestore = new Firestore({ projectId: 'my-project' });\n * const eventStore = getFirestoreEventStore(firestore);\n * ```\n */\nexport function getFirestoreEventStore(\n firestore: Firestore,\n options?: FirestoreEventStoreOptions,\n): FirestoreEventStore {\n return new FirestoreEventStoreImpl(firestore, options);\n}\n"]}
|