@emmett-community/emmett-google-firestore 0.1.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.
@@ -0,0 +1,85 @@
1
+ import { Firestore, Timestamp } from '@google-cloud/firestore';
2
+ import { F as FirestoreEventStoreOptions, a as FirestoreEventStore, E as ExpectedStreamVersion } from './types-CHnx_sMk.js';
3
+ export { A as AppendToStreamOptions, d as AppendToStreamResult, C as CollectionConfig, e as EventDocument, f as ExpectedVersionConflictError, b as FirestoreReadEvent, c as FirestoreReadEventMetadata, R as ReadStreamOptions, S as StreamMetadata } from './types-CHnx_sMk.js';
4
+ import { STREAM_DOES_NOT_EXIST } from '@event-driven-io/emmett';
5
+ export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
6
+
7
+ /**
8
+ * Factory function to create a Firestore event store
9
+ *
10
+ * @param firestore - Firestore instance
11
+ * @param options - Optional configuration
12
+ * @returns Firestore event store instance
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { Firestore } from '@google-cloud/firestore';
17
+ * import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';
18
+ *
19
+ * const firestore = new Firestore({ projectId: 'my-project' });
20
+ * const eventStore = getFirestoreEventStore(firestore);
21
+ * ```
22
+ */
23
+ declare function getFirestoreEventStore(firestore: Firestore, options?: FirestoreEventStoreOptions): FirestoreEventStore;
24
+
25
+ /**
26
+ * Pad version number with leading zeros for Firestore document IDs
27
+ * This ensures automatic ordering by version in Firestore
28
+ *
29
+ * @param version - The version number to pad
30
+ * @returns Zero-padded string of length 10
31
+ *
32
+ * @example
33
+ * padVersion(0) // "0000000000"
34
+ * padVersion(42) // "0000000042"
35
+ * padVersion(12345) // "0000012345"
36
+ */
37
+ declare function padVersion(version: number | bigint): string;
38
+ /**
39
+ * Parse a stream name into type and ID components
40
+ *
41
+ * @param streamName - Stream name in format "Type-id" or "Type-with-dashes-id"
42
+ * @returns Object with streamType and streamId
43
+ *
44
+ * @example
45
+ * parseStreamName("User-123") // { streamType: "User", streamId: "123" }
46
+ * parseStreamName("ShoppingCart-abc-def-123") // { streamType: "ShoppingCart", streamId: "abc-def-123" }
47
+ */
48
+ declare function parseStreamName(streamName: string): {
49
+ streamType: string;
50
+ streamId: string;
51
+ };
52
+ /**
53
+ * Convert Firestore Timestamp to JavaScript Date
54
+ *
55
+ * @param timestamp - Firestore Timestamp
56
+ * @returns JavaScript Date object
57
+ */
58
+ declare function timestampToDate(timestamp: Timestamp): Date;
59
+ /**
60
+ * Validate expected version against current version
61
+ *
62
+ * @param streamName - Stream name for error messages
63
+ * @param expectedVersion - Expected version constraint
64
+ * @param currentVersion - Current stream version (or STREAM_DOES_NOT_EXIST if stream doesn't exist)
65
+ * @throws ExpectedVersionConflictError if versions don't match
66
+ */
67
+ declare function assertExpectedVersionMatchesCurrent(streamName: string, expectedVersion: ExpectedStreamVersion, currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST): void;
68
+ /**
69
+ * Get the current stream version from metadata
70
+ *
71
+ * @param streamExists - Whether the stream document exists
72
+ * @param version - Version number from Firestore (if stream exists)
73
+ * @returns Current version as bigint or STREAM_DOES_NOT_EXIST
74
+ */
75
+ declare function getCurrentStreamVersion(streamExists: boolean, version?: number): bigint | typeof STREAM_DOES_NOT_EXIST;
76
+ /**
77
+ * Calculate the next expected stream version after appending events
78
+ *
79
+ * @param currentVersion - Current stream version
80
+ * @param eventCount - Number of events being appended
81
+ * @returns Next expected version as bigint
82
+ */
83
+ declare function calculateNextVersion(currentVersion: bigint | typeof STREAM_DOES_NOT_EXIST, eventCount: number): bigint;
84
+
85
+ export { ExpectedStreamVersion, FirestoreEventStore, FirestoreEventStoreOptions, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
package/dist/index.js ADDED
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ var emmett = require('@event-driven-io/emmett');
4
+
5
+ // src/eventStore/types.ts
6
+ var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
7
+ constructor(streamName, expected, actual) {
8
+ super(
9
+ `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`
10
+ );
11
+ this.streamName = streamName;
12
+ this.expected = expected;
13
+ this.actual = actual;
14
+ this.name = "ExpectedVersionConflictError";
15
+ Object.setPrototypeOf(this, _ExpectedVersionConflictError.prototype);
16
+ }
17
+ };
18
+
19
+ // src/eventStore/utils.ts
20
+ function padVersion(version) {
21
+ return version.toString().padStart(10, "0");
22
+ }
23
+ function parseStreamName(streamName) {
24
+ const firstDashIndex = streamName.indexOf("-");
25
+ if (firstDashIndex === -1) {
26
+ return {
27
+ streamType: streamName,
28
+ streamId: ""
29
+ };
30
+ }
31
+ return {
32
+ streamType: streamName.substring(0, firstDashIndex),
33
+ streamId: streamName.substring(firstDashIndex + 1)
34
+ };
35
+ }
36
+ function timestampToDate(timestamp) {
37
+ return timestamp.toDate();
38
+ }
39
+ function assertExpectedVersionMatchesCurrent(streamName, expectedVersion, currentVersion) {
40
+ if (expectedVersion === emmett.NO_CONCURRENCY_CHECK) {
41
+ return;
42
+ }
43
+ if (expectedVersion === emmett.STREAM_DOES_NOT_EXIST) {
44
+ if (currentVersion !== emmett.STREAM_DOES_NOT_EXIST) {
45
+ throw new ExpectedVersionConflictError(
46
+ streamName,
47
+ expectedVersion,
48
+ currentVersion
49
+ );
50
+ }
51
+ return;
52
+ }
53
+ if (expectedVersion === emmett.STREAM_EXISTS) {
54
+ if (currentVersion === emmett.STREAM_DOES_NOT_EXIST) {
55
+ throw new ExpectedVersionConflictError(
56
+ streamName,
57
+ expectedVersion,
58
+ currentVersion
59
+ );
60
+ }
61
+ return;
62
+ }
63
+ const expectedBigInt = BigInt(expectedVersion);
64
+ if (currentVersion === emmett.STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {
65
+ throw new ExpectedVersionConflictError(
66
+ streamName,
67
+ expectedVersion,
68
+ currentVersion
69
+ );
70
+ }
71
+ }
72
+ function getCurrentStreamVersion(streamExists, version) {
73
+ if (!streamExists) {
74
+ return emmett.STREAM_DOES_NOT_EXIST;
75
+ }
76
+ return BigInt(version ?? -1);
77
+ }
78
+ function calculateNextVersion(currentVersion, eventCount) {
79
+ if (currentVersion === emmett.STREAM_DOES_NOT_EXIST) {
80
+ return BigInt(eventCount - 1);
81
+ }
82
+ return currentVersion + BigInt(eventCount);
83
+ }
84
+
85
+ // src/eventStore/firestoreEventStore.ts
86
+ var DEFAULT_COLLECTIONS = {
87
+ streams: "streams",
88
+ counters: "_counters"
89
+ };
90
+ var FirestoreEventStoreImpl = class {
91
+ constructor(firestore, options = {}) {
92
+ this.firestore = firestore;
93
+ this.collections = {
94
+ ...DEFAULT_COLLECTIONS,
95
+ ...options.collections
96
+ };
97
+ }
98
+ collections;
99
+ /**
100
+ * Read events from a stream
101
+ */
102
+ async readStream(streamName, options = {}) {
103
+ const { from, to, maxCount } = options;
104
+ let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
105
+ if (from !== void 0) {
106
+ query = query.where("streamVersion", ">=", Number(from));
107
+ }
108
+ if (to !== void 0) {
109
+ query = query.where("streamVersion", "<=", Number(to));
110
+ }
111
+ if (maxCount !== void 0 && maxCount > 0) {
112
+ query = query.limit(maxCount);
113
+ }
114
+ const snapshot = await query.get();
115
+ return snapshot.docs.map((doc) => {
116
+ const data = doc.data();
117
+ return {
118
+ type: data.type,
119
+ data: data.data,
120
+ metadata: {
121
+ ...data.metadata,
122
+ streamName,
123
+ streamVersion: BigInt(data.streamVersion),
124
+ streamPosition: BigInt(data.streamVersion),
125
+ globalPosition: BigInt(data.globalPosition),
126
+ timestamp: timestampToDate(data.timestamp)
127
+ }
128
+ };
129
+ });
130
+ }
131
+ /**
132
+ * Aggregate stream by applying events to state
133
+ */
134
+ async aggregateStream(streamName, options) {
135
+ const { evolve, initialState, read } = options;
136
+ const events = await this.readStream(streamName, read);
137
+ const streamExists = events.length > 0;
138
+ const state = events.reduce(evolve, initialState());
139
+ const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
140
+ return {
141
+ state,
142
+ currentStreamVersion,
143
+ streamExists
144
+ };
145
+ }
146
+ /**
147
+ * Append events to a stream with optimistic concurrency control
148
+ */
149
+ async appendToStream(streamName, events, options = {}) {
150
+ if (events.length === 0) {
151
+ throw new Error("Cannot append empty event array");
152
+ }
153
+ const { expectedStreamVersion = emmett.NO_CONCURRENCY_CHECK } = options;
154
+ return await this.firestore.runTransaction(async (transaction) => {
155
+ return await this.appendToStreamInTransaction(
156
+ transaction,
157
+ streamName,
158
+ events,
159
+ expectedStreamVersion
160
+ );
161
+ });
162
+ }
163
+ /**
164
+ * Internal method to append events within a transaction
165
+ */
166
+ async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
167
+ const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
168
+ const streamDoc = await transaction.get(streamRef);
169
+ const streamExists = streamDoc.exists;
170
+ const streamData = streamDoc.data();
171
+ const currentVersion = getCurrentStreamVersion(
172
+ streamExists,
173
+ streamData?.version
174
+ );
175
+ assertExpectedVersionMatchesCurrent(
176
+ streamName,
177
+ expectedStreamVersion,
178
+ currentVersion
179
+ );
180
+ const counterRef = this.firestore.collection(this.collections.counters).doc("global_position");
181
+ const counterDoc = await transaction.get(counterRef);
182
+ let globalPosition = counterDoc.exists ? counterDoc.data()?.value ?? 0 : 0;
183
+ const baseVersion = currentVersion === emmett.STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);
184
+ const TimestampClass = this.firestore.constructor;
185
+ const now = TimestampClass.Timestamp.now();
186
+ events.forEach((event, index) => {
187
+ const eventVersion = baseVersion + 1 + index;
188
+ const eventRef = streamRef.collection("events").doc(padVersion(eventVersion));
189
+ const metadata = event.metadata;
190
+ const eventDocument = {
191
+ type: event.type,
192
+ data: event.data,
193
+ ...metadata && { metadata },
194
+ timestamp: now,
195
+ globalPosition: globalPosition++,
196
+ streamVersion: eventVersion
197
+ };
198
+ transaction.set(eventRef, eventDocument);
199
+ });
200
+ const newVersion = baseVersion + events.length;
201
+ const updatedMetadata = {
202
+ version: newVersion,
203
+ createdAt: streamData?.createdAt ?? now,
204
+ updatedAt: now
205
+ };
206
+ transaction.set(streamRef, updatedMetadata);
207
+ transaction.set(counterRef, {
208
+ value: globalPosition,
209
+ updatedAt: now
210
+ });
211
+ return {
212
+ nextExpectedStreamVersion: BigInt(newVersion),
213
+ createdNewStream: !streamExists
214
+ };
215
+ }
216
+ };
217
+ function getFirestoreEventStore(firestore, options) {
218
+ return new FirestoreEventStoreImpl(firestore, options);
219
+ }
220
+
221
+ Object.defineProperty(exports, "NO_CONCURRENCY_CHECK", {
222
+ enumerable: true,
223
+ get: function () { return emmett.NO_CONCURRENCY_CHECK; }
224
+ });
225
+ Object.defineProperty(exports, "STREAM_DOES_NOT_EXIST", {
226
+ enumerable: true,
227
+ get: function () { return emmett.STREAM_DOES_NOT_EXIST; }
228
+ });
229
+ Object.defineProperty(exports, "STREAM_EXISTS", {
230
+ enumerable: true,
231
+ get: function () { return emmett.STREAM_EXISTS; }
232
+ });
233
+ exports.ExpectedVersionConflictError = ExpectedVersionConflictError;
234
+ exports.assertExpectedVersionMatchesCurrent = assertExpectedVersionMatchesCurrent;
235
+ exports.calculateNextVersion = calculateNextVersion;
236
+ exports.getCurrentStreamVersion = getCurrentStreamVersion;
237
+ exports.getFirestoreEventStore = getFirestoreEventStore;
238
+ exports.padVersion = padVersion;
239
+ exports.parseStreamName = parseStreamName;
240
+ exports.timestampToDate = timestampToDate;
241
+ //# sourceMappingURL=index.js.map
242
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"],"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 * 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}\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 type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\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\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\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 { 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 return 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\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 if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n // Execute in transaction for atomicity\n return await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n }\n\n /**\n * Internal method to append events within a transaction\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 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 // 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"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,222 @@
1
+ import { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
2
+ export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
3
+
4
+ // src/eventStore/types.ts
5
+ var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
6
+ constructor(streamName, expected, actual) {
7
+ super(
8
+ `Expected version conflict for stream '${streamName}': expected ${String(expected)}, actual ${String(actual)}`
9
+ );
10
+ this.streamName = streamName;
11
+ this.expected = expected;
12
+ this.actual = actual;
13
+ this.name = "ExpectedVersionConflictError";
14
+ Object.setPrototypeOf(this, _ExpectedVersionConflictError.prototype);
15
+ }
16
+ };
17
+
18
+ // src/eventStore/utils.ts
19
+ function padVersion(version) {
20
+ return version.toString().padStart(10, "0");
21
+ }
22
+ function parseStreamName(streamName) {
23
+ const firstDashIndex = streamName.indexOf("-");
24
+ if (firstDashIndex === -1) {
25
+ return {
26
+ streamType: streamName,
27
+ streamId: ""
28
+ };
29
+ }
30
+ return {
31
+ streamType: streamName.substring(0, firstDashIndex),
32
+ streamId: streamName.substring(firstDashIndex + 1)
33
+ };
34
+ }
35
+ function timestampToDate(timestamp) {
36
+ return timestamp.toDate();
37
+ }
38
+ function assertExpectedVersionMatchesCurrent(streamName, expectedVersion, currentVersion) {
39
+ if (expectedVersion === NO_CONCURRENCY_CHECK) {
40
+ return;
41
+ }
42
+ if (expectedVersion === STREAM_DOES_NOT_EXIST) {
43
+ if (currentVersion !== STREAM_DOES_NOT_EXIST) {
44
+ throw new ExpectedVersionConflictError(
45
+ streamName,
46
+ expectedVersion,
47
+ currentVersion
48
+ );
49
+ }
50
+ return;
51
+ }
52
+ if (expectedVersion === STREAM_EXISTS) {
53
+ if (currentVersion === STREAM_DOES_NOT_EXIST) {
54
+ throw new ExpectedVersionConflictError(
55
+ streamName,
56
+ expectedVersion,
57
+ currentVersion
58
+ );
59
+ }
60
+ return;
61
+ }
62
+ const expectedBigInt = BigInt(expectedVersion);
63
+ if (currentVersion === STREAM_DOES_NOT_EXIST || currentVersion !== expectedBigInt) {
64
+ throw new ExpectedVersionConflictError(
65
+ streamName,
66
+ expectedVersion,
67
+ currentVersion
68
+ );
69
+ }
70
+ }
71
+ function getCurrentStreamVersion(streamExists, version) {
72
+ if (!streamExists) {
73
+ return STREAM_DOES_NOT_EXIST;
74
+ }
75
+ return BigInt(version ?? -1);
76
+ }
77
+ function calculateNextVersion(currentVersion, eventCount) {
78
+ if (currentVersion === STREAM_DOES_NOT_EXIST) {
79
+ return BigInt(eventCount - 1);
80
+ }
81
+ return currentVersion + BigInt(eventCount);
82
+ }
83
+
84
+ // src/eventStore/firestoreEventStore.ts
85
+ var DEFAULT_COLLECTIONS = {
86
+ streams: "streams",
87
+ counters: "_counters"
88
+ };
89
+ var FirestoreEventStoreImpl = class {
90
+ constructor(firestore, options = {}) {
91
+ this.firestore = firestore;
92
+ this.collections = {
93
+ ...DEFAULT_COLLECTIONS,
94
+ ...options.collections
95
+ };
96
+ }
97
+ collections;
98
+ /**
99
+ * Read events from a stream
100
+ */
101
+ async readStream(streamName, options = {}) {
102
+ const { from, to, maxCount } = options;
103
+ let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
104
+ if (from !== void 0) {
105
+ query = query.where("streamVersion", ">=", Number(from));
106
+ }
107
+ if (to !== void 0) {
108
+ query = query.where("streamVersion", "<=", Number(to));
109
+ }
110
+ if (maxCount !== void 0 && maxCount > 0) {
111
+ query = query.limit(maxCount);
112
+ }
113
+ const snapshot = await query.get();
114
+ return snapshot.docs.map((doc) => {
115
+ const data = doc.data();
116
+ return {
117
+ type: data.type,
118
+ data: data.data,
119
+ metadata: {
120
+ ...data.metadata,
121
+ streamName,
122
+ streamVersion: BigInt(data.streamVersion),
123
+ streamPosition: BigInt(data.streamVersion),
124
+ globalPosition: BigInt(data.globalPosition),
125
+ timestamp: timestampToDate(data.timestamp)
126
+ }
127
+ };
128
+ });
129
+ }
130
+ /**
131
+ * Aggregate stream by applying events to state
132
+ */
133
+ async aggregateStream(streamName, options) {
134
+ const { evolve, initialState, read } = options;
135
+ const events = await this.readStream(streamName, read);
136
+ const streamExists = events.length > 0;
137
+ const state = events.reduce(evolve, initialState());
138
+ const currentStreamVersion = streamExists ? events[events.length - 1].metadata.streamVersion : BigInt(0);
139
+ return {
140
+ state,
141
+ currentStreamVersion,
142
+ streamExists
143
+ };
144
+ }
145
+ /**
146
+ * Append events to a stream with optimistic concurrency control
147
+ */
148
+ async appendToStream(streamName, events, options = {}) {
149
+ if (events.length === 0) {
150
+ throw new Error("Cannot append empty event array");
151
+ }
152
+ const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;
153
+ return await this.firestore.runTransaction(async (transaction) => {
154
+ return await this.appendToStreamInTransaction(
155
+ transaction,
156
+ streamName,
157
+ events,
158
+ expectedStreamVersion
159
+ );
160
+ });
161
+ }
162
+ /**
163
+ * Internal method to append events within a transaction
164
+ */
165
+ async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
166
+ const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
167
+ const streamDoc = await transaction.get(streamRef);
168
+ const streamExists = streamDoc.exists;
169
+ const streamData = streamDoc.data();
170
+ const currentVersion = getCurrentStreamVersion(
171
+ streamExists,
172
+ streamData?.version
173
+ );
174
+ assertExpectedVersionMatchesCurrent(
175
+ streamName,
176
+ expectedStreamVersion,
177
+ currentVersion
178
+ );
179
+ const counterRef = this.firestore.collection(this.collections.counters).doc("global_position");
180
+ const counterDoc = await transaction.get(counterRef);
181
+ let globalPosition = counterDoc.exists ? counterDoc.data()?.value ?? 0 : 0;
182
+ const baseVersion = currentVersion === STREAM_DOES_NOT_EXIST ? -1 : Number(currentVersion);
183
+ const TimestampClass = this.firestore.constructor;
184
+ const now = TimestampClass.Timestamp.now();
185
+ events.forEach((event, index) => {
186
+ const eventVersion = baseVersion + 1 + index;
187
+ const eventRef = streamRef.collection("events").doc(padVersion(eventVersion));
188
+ const metadata = event.metadata;
189
+ const eventDocument = {
190
+ type: event.type,
191
+ data: event.data,
192
+ ...metadata && { metadata },
193
+ timestamp: now,
194
+ globalPosition: globalPosition++,
195
+ streamVersion: eventVersion
196
+ };
197
+ transaction.set(eventRef, eventDocument);
198
+ });
199
+ const newVersion = baseVersion + events.length;
200
+ const updatedMetadata = {
201
+ version: newVersion,
202
+ createdAt: streamData?.createdAt ?? now,
203
+ updatedAt: now
204
+ };
205
+ transaction.set(streamRef, updatedMetadata);
206
+ transaction.set(counterRef, {
207
+ value: globalPosition,
208
+ updatedAt: now
209
+ });
210
+ return {
211
+ nextExpectedStreamVersion: BigInt(newVersion),
212
+ createdNewStream: !streamExists
213
+ };
214
+ }
215
+ };
216
+ function getFirestoreEventStore(firestore, options) {
217
+ return new FirestoreEventStoreImpl(firestore, options);
218
+ }
219
+
220
+ export { ExpectedVersionConflictError, assertExpectedVersionMatchesCurrent, calculateNextVersion, getCurrentStreamVersion, getFirestoreEventStore, padVersion, parseStreamName, timestampToDate };
221
+ //# sourceMappingURL=index.mjs.map
222
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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 * 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}\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 type {\n AppendToStreamOptions,\n AppendToStreamResult,\n CollectionConfig,\n EventDocument,\n ExpectedStreamVersion,\n FirestoreEventStore,\n FirestoreEventStoreOptions,\n FirestoreReadEvent,\n ReadStreamOptions,\n StreamMetadata,\n} from './types';\nimport { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST } from './types';\nimport {\n assertExpectedVersionMatchesCurrent,\n getCurrentStreamVersion,\n padVersion,\n timestampToDate,\n} from './utils';\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\n constructor(\n public readonly firestore: Firestore,\n options: FirestoreEventStoreOptions = {},\n ) {\n this.collections = {\n ...DEFAULT_COLLECTIONS,\n ...options.collections,\n };\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 { 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 return 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\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 if (events.length === 0) {\n throw new Error('Cannot append empty event array');\n }\n\n const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;\n\n // Execute in transaction for atomicity\n return await this.firestore.runTransaction(async (transaction) => {\n return await this.appendToStreamInTransaction(\n transaction,\n streamName,\n events,\n expectedStreamVersion,\n );\n });\n }\n\n /**\n * Internal method to append events within a transaction\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 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 // 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"]}