@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Emmett Community
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,579 @@
1
+ # @emmett-community/emmett-google-firestore
2
+
3
+ Google Firestore event store implementation for [Emmett](https://event-driven-io.github.io/emmett/), the Node.js event sourcing framework.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@emmett-community/emmett-google-firestore.svg)](https://www.npmjs.com/package/@emmett-community/emmett-google-firestore)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - ✅ **Event Storage & Retrieval** - Store and read events from Google Firestore
11
+ - ✅ **Optimistic Concurrency** - Built-in version conflict detection
12
+ - ✅ **Type-Safe** - Full TypeScript support with comprehensive types
13
+ - ✅ **Minimal Boilerplate** - Simple, intuitive API
14
+ - ✅ **Subcollection-based** - Efficient Firestore-native structure (no size limits!)
15
+ - ✅ **Global Event Ordering** - Maintain total ordering across all streams
16
+ - ✅ **Testing Utilities** - Helper functions for easy testing
17
+ - ✅ **Emmett Compatible** - Works seamlessly with the Emmett ecosystem
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @emmett-community/emmett-google-firestore @google-cloud/firestore
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import { Firestore } from '@google-cloud/firestore';
29
+ import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';
30
+
31
+ // Initialize Firestore
32
+ const firestore = new Firestore({
33
+ projectId: 'your-project-id',
34
+ keyFilename: 'path/to/service-account.json',
35
+ });
36
+
37
+ // Create event store
38
+ const eventStore = getFirestoreEventStore(firestore);
39
+
40
+ // Define your events
41
+ type UserRegistered = Event<'UserRegistered', { userId: string; email: string }>;
42
+ type UserEvent = UserRegistered | /* other events */;
43
+
44
+ // Append events
45
+ await eventStore.appendToStream('User-123', [
46
+ {
47
+ type: 'UserRegistered',
48
+ data: { userId: '123', email: 'user@example.com' },
49
+ },
50
+ ]);
51
+
52
+ // Read events
53
+ const events = await eventStore.readStream<UserEvent>('User-123');
54
+
55
+ // Aggregate state
56
+ const state = await eventStore.aggregateStream(
57
+ 'User-123',
58
+ evolve,
59
+ initialState,
60
+ );
61
+ ```
62
+
63
+ ## How It Works
64
+
65
+ ### Firestore Structure
66
+
67
+ Events are stored using a **subcollection pattern** for optimal performance:
68
+
69
+ ```
70
+ /streams/ # Root collection
71
+ {streamName}/ # Stream document (metadata)
72
+ version: number
73
+ createdAt: Timestamp
74
+ updatedAt: Timestamp
75
+
76
+ /events/ # Subcollection (actual events)
77
+ 0000000000: { type, data, ... } # Zero-padded version IDs
78
+ 0000000001: { type, data, ... }
79
+ 0000000002: { type, data, ... }
80
+
81
+ /_counters/ # System collection
82
+ global_position/
83
+ value: number
84
+ ```
85
+
86
+ **Benefits of this structure:**
87
+
88
+ - ✅ No document size limits (Firestore 1MB limit doesn't apply to subcollections)
89
+ - ✅ Natural isolation per stream
90
+ - ✅ Automatic ordering (document IDs sort naturally)
91
+ - ✅ No composite indexes needed
92
+ - ✅ Efficient queries
93
+
94
+ ### Optimistic Concurrency
95
+
96
+ The event store uses **optimistic locking** to prevent conflicts:
97
+
98
+ ```typescript
99
+ // Append with version check
100
+ await eventStore.appendToStream(
101
+ 'User-123',
102
+ events,
103
+ { expectedStreamVersion: 5 } // Will fail if version ≠ 5
104
+ );
105
+
106
+ // Or use special version markers
107
+ import { NO_STREAM, STREAM_EXISTS, ANY } from '@emmett-community/emmett-google-firestore';
108
+
109
+ // Stream must not exist
110
+ await eventStore.appendToStream('User-123', events, {
111
+ expectedStreamVersion: NO_STREAM
112
+ });
113
+
114
+ // Stream must exist (any version)
115
+ await eventStore.appendToStream('User-123', events, {
116
+ expectedStreamVersion: STREAM_EXISTS
117
+ });
118
+
119
+ // No version check
120
+ await eventStore.appendToStream('User-123', events, {
121
+ expectedStreamVersion: ANY
122
+ });
123
+ ```
124
+
125
+ ## API Reference
126
+
127
+ ### `getFirestoreEventStore(firestore, options?)`
128
+
129
+ Creates a Firestore event store instance.
130
+
131
+ **Parameters:**
132
+
133
+ - `firestore`: Firestore instance
134
+ - `options`: Optional configuration
135
+ - `collections`: Custom collection names
136
+ - `streams`: Stream collection name (default: `"streams"`)
137
+ - `counters`: Counter collection name (default: `"_counters"`)
138
+
139
+ **Returns:** `FirestoreEventStore`
140
+
141
+ ```typescript
142
+ const eventStore = getFirestoreEventStore(firestore, {
143
+ collections: {
144
+ streams: 'my_streams',
145
+ counters: 'my_counters',
146
+ },
147
+ });
148
+ ```
149
+
150
+ ### `eventStore.appendToStream(streamName, events, options?)`
151
+
152
+ Appends events to a stream.
153
+
154
+ **Parameters:**
155
+
156
+ - `streamName`: Stream identifier (e.g., `"User-123"`)
157
+ - `events`: Array of events to append
158
+ - `options`: Optional append options
159
+ - `expectedStreamVersion`: Version constraint
160
+
161
+ **Returns:** `Promise<AppendToStreamResult>`
162
+
163
+ ```typescript
164
+ const result = await eventStore.appendToStream(
165
+ 'User-123',
166
+ [{ type: 'UserRegistered', data: {...} }],
167
+ { expectedStreamVersion: 0 }
168
+ );
169
+
170
+ console.log(result.nextExpectedStreamVersion); // 1
171
+ console.log(result.createdNewStream); // true/false
172
+ ```
173
+
174
+ ### `eventStore.readStream(streamName, options?)`
175
+
176
+ Reads events from a stream.
177
+
178
+ **Parameters:**
179
+
180
+ - `streamName`: Stream identifier
181
+ - `options`: Optional read options
182
+ - `from`: Start version (inclusive)
183
+ - `to`: End version (inclusive)
184
+ - `maxCount`: Maximum number of events to read
185
+
186
+ **Returns:** `Promise<FirestoreReadEvent[]>`
187
+
188
+ ```typescript
189
+ // Read all events
190
+ const events = await eventStore.readStream('User-123');
191
+
192
+ // Read from version 10 onwards
193
+ const events = await eventStore.readStream('User-123', { from: 10n });
194
+
195
+ // Read range
196
+ const events = await eventStore.readStream('User-123', {
197
+ from: 5n,
198
+ to: 10n
199
+ });
200
+
201
+ // Limit results
202
+ const events = await eventStore.readStream('User-123', {
203
+ maxCount: 100
204
+ });
205
+ ```
206
+
207
+ ### `eventStore.aggregateStream(streamName, evolve, initialState, options?)`
208
+
209
+ Aggregates stream events into state.
210
+
211
+ **Parameters:**
212
+
213
+ - `streamName`: Stream identifier
214
+ - `evolve`: Function to apply events to state
215
+ - `initialState`: Function returning initial state
216
+ - `options`: Optional read options (same as `readStream`)
217
+
218
+ **Returns:** `Promise<State>`
219
+
220
+ ```typescript
221
+ const state = await eventStore.aggregateStream(
222
+ 'User-123',
223
+ (state, event) => {
224
+ switch (event.type) {
225
+ case 'UserRegistered':
226
+ return { ...state, ...event.data };
227
+ default:
228
+ return state;
229
+ }
230
+ },
231
+ () => ({ status: 'empty' }),
232
+ );
233
+ ```
234
+
235
+ ## Testing
236
+
237
+ ### Testing Utilities
238
+
239
+ The package includes utilities to make testing easier:
240
+
241
+ ```typescript
242
+ import {
243
+ setupFirestoreTests,
244
+ getTestFirestore,
245
+ clearFirestore,
246
+ } from '@emmett-community/emmett-google-firestore/testing';
247
+
248
+ describe('My Tests', () => {
249
+ const { firestore, eventStore, cleanup, clearData } = setupFirestoreTests();
250
+
251
+ afterAll(cleanup);
252
+ beforeEach(clearData);
253
+
254
+ it('should work', async () => {
255
+ await eventStore.appendToStream('test-stream', [/* events */]);
256
+ // ... assertions
257
+ });
258
+ });
259
+ ```
260
+
261
+ ### Running Tests
262
+
263
+ ```bash
264
+ # Unit tests
265
+ npm run test:unit
266
+
267
+ # Integration tests (requires Firestore Emulator)
268
+ npm run test:integration
269
+
270
+ # All tests
271
+ npm test
272
+
273
+ # Coverage
274
+ npm run test:coverage
275
+ ```
276
+
277
+ ### Using Firestore Emulator
278
+
279
+ For local development and testing:
280
+
281
+ ```bash
282
+ # Install Firebase CLI
283
+ npm install -g firebase-tools
284
+
285
+ # Start emulator
286
+ firebase emulators:start --only firestore
287
+
288
+ # Or use the provided script
289
+ ./scripts/start-emulator.sh
290
+ ```
291
+
292
+ Set environment variables:
293
+
294
+ ```bash
295
+ export FIRESTORE_PROJECT_ID=test-project
296
+ export FIRESTORE_EMULATOR_HOST=localhost:8080
297
+ ```
298
+
299
+ ## Examples
300
+
301
+ ### Complete Shopping Cart Example
302
+
303
+ See [examples/shopping-cart](./examples/shopping-cart) for a full application including:
304
+
305
+ - Event-sourced shopping cart
306
+ - Express.js API with OpenAPI spec
307
+ - Docker Compose setup
308
+ - Unit, integration, and E2E tests
309
+
310
+ ```bash
311
+ cd examples/shopping-cart
312
+ docker-compose up
313
+ ```
314
+
315
+ ### Basic Usage Example
316
+
317
+ ```typescript
318
+ import { Firestore } from '@google-cloud/firestore';
319
+ import { getFirestoreEventStore } from '@emmett-community/emmett-google-firestore';
320
+ import type { Event } from '@event-driven-io/emmett';
321
+
322
+ // Define events
323
+ type AccountOpened = Event<'AccountOpened', {
324
+ accountId: string;
325
+ initialBalance: number;
326
+ }>;
327
+
328
+ type MoneyDeposited = Event<'MoneyDeposited', {
329
+ accountId: string;
330
+ amount: number;
331
+ }>;
332
+
333
+ type BankAccountEvent = AccountOpened | MoneyDeposited;
334
+
335
+ // Define state
336
+ type BankAccount = {
337
+ accountId: string;
338
+ balance: number;
339
+ status: 'open' | 'closed';
340
+ };
341
+
342
+ // Evolve function
343
+ const evolve = (state: BankAccount, event: BankAccountEvent): BankAccount => {
344
+ switch (event.type) {
345
+ case 'AccountOpened':
346
+ return {
347
+ accountId: event.data.accountId,
348
+ balance: event.data.initialBalance,
349
+ status: 'open',
350
+ };
351
+ case 'MoneyDeposited':
352
+ return {
353
+ ...state,
354
+ balance: state.balance + event.data.amount,
355
+ };
356
+ default:
357
+ return state;
358
+ }
359
+ };
360
+
361
+ const initialState = (): BankAccount => ({
362
+ accountId: '',
363
+ balance: 0,
364
+ status: 'closed',
365
+ });
366
+
367
+ // Usage
368
+ const firestore = new Firestore({ projectId: 'my-project' });
369
+ const eventStore = getFirestoreEventStore(firestore);
370
+
371
+ // Open account
372
+ await eventStore.appendToStream('BankAccount-123', [
373
+ {
374
+ type: 'AccountOpened',
375
+ data: { accountId: '123', initialBalance: 100 }
376
+ },
377
+ ]);
378
+
379
+ // Deposit money
380
+ await eventStore.appendToStream('BankAccount-123', [
381
+ {
382
+ type: 'MoneyDeposited',
383
+ data: { accountId: '123', amount: 50 }
384
+ },
385
+ ]);
386
+
387
+ // Get current state
388
+ const account = await eventStore.aggregateStream(
389
+ 'BankAccount-123',
390
+ evolve,
391
+ initialState,
392
+ );
393
+
394
+ console.log(account.balance); // 150
395
+ ```
396
+
397
+ ## Configuration
398
+
399
+ ### Custom Collection Names
400
+
401
+ ```typescript
402
+ const eventStore = getFirestoreEventStore(firestore, {
403
+ collections: {
404
+ streams: 'app_streams',
405
+ counters: 'app_counters',
406
+ },
407
+ });
408
+ ```
409
+
410
+ ### Firestore Emulator (Development)
411
+
412
+ ```typescript
413
+ const firestore = new Firestore({
414
+ projectId: 'demo-project',
415
+ host: 'localhost:8080',
416
+ ssl: false,
417
+ });
418
+ ```
419
+
420
+ ### Production Configuration
421
+
422
+ ```typescript
423
+ const firestore = new Firestore({
424
+ projectId: process.env.GCP_PROJECT_ID,
425
+ keyFilename: process.env.GCP_KEY_FILE,
426
+ // Optional: specify database
427
+ databaseId: '(default)',
428
+ });
429
+ ```
430
+
431
+ ## Architecture
432
+
433
+ ### Event Sourcing Pattern
434
+
435
+ This package implements the **Event Sourcing** pattern:
436
+
437
+ 1. **Commands** → Validate and create events
438
+ 2. **Events** → Immutable facts that happened
439
+ 3. **State** → Rebuilt by replaying events
440
+
441
+ ```
442
+ Command → Decide → Events → Append to Firestore → Evolve → State
443
+ ```
444
+
445
+ ### Firestore Transaction Flow
446
+
447
+ When appending events, the following happens atomically:
448
+
449
+ 1. Read current stream version
450
+ 2. Validate expected version
451
+ 3. Increment global position counter
452
+ 4. Append events to subcollection
453
+ 5. Update stream metadata
454
+ 6. Commit transaction
455
+
456
+ If any step fails or versions don't match, the entire transaction is rolled back.
457
+
458
+ ## Performance Considerations
459
+
460
+ ### Batch Size
461
+
462
+ Firestore transactions are limited to 500 operations. When appending many events:
463
+
464
+ ```typescript
465
+ // Good: Small batches
466
+ await eventStore.appendToStream('stream', events.slice(0, 100));
467
+
468
+ // Avoid: Very large batches (>400 events)
469
+ ```
470
+
471
+ ### Query Optimization
472
+
473
+ ```typescript
474
+ // Good: Use range queries
475
+ const recent = await eventStore.readStream('stream', {
476
+ from: lastKnownVersion,
477
+ });
478
+
479
+ // Good: Limit results
480
+ const events = await eventStore.readStream('stream', {
481
+ maxCount: 100
482
+ });
483
+ ```
484
+
485
+ ### Firestore Costs
486
+
487
+ - **Reads**: Each document read counts (events + metadata)
488
+ - **Writes**: Each event appended counts
489
+ - **Storage**: Charged per GB stored
490
+
491
+ Use the emulator for development to avoid costs!
492
+
493
+ ## Error Handling
494
+
495
+ ```typescript
496
+ import { ExpectedVersionConflictError } from '@emmett-community/emmett-google-firestore';
497
+
498
+ try {
499
+ await eventStore.appendToStream('stream', events, {
500
+ expectedStreamVersion: 5
501
+ });
502
+ } catch (error) {
503
+ if (error instanceof ExpectedVersionConflictError) {
504
+ console.log('Version conflict:', error.expected, 'vs', error.actual);
505
+ // Handle conflict (retry, merge, etc.)
506
+ }
507
+ }
508
+ ```
509
+
510
+ ## TypeScript Support
511
+
512
+ The package is written in TypeScript and includes full type definitions:
513
+
514
+ ```typescript
515
+ import type {
516
+ FirestoreEventStore,
517
+ FirestoreReadEvent,
518
+ AppendToStreamOptions,
519
+ ExpectedStreamVersion,
520
+ } from '@emmett-community/emmett-google-firestore';
521
+ ```
522
+
523
+ ## Compatibility
524
+
525
+ - **Node.js**: >= 18.0.0
526
+ - **Emmett**: ^0.39.0
527
+ - **Firestore**: ^7.10.0
528
+
529
+ ## Contributing
530
+
531
+ Contributions are welcome! Please:
532
+
533
+ 1. Fork the repository
534
+ 2. Create a feature branch
535
+ 3. Add tests for new functionality
536
+ 4. Ensure all tests pass
537
+ 5. Submit a pull request
538
+
539
+ ## Development
540
+
541
+ ```bash
542
+ # Install dependencies
543
+ npm install
544
+
545
+ # Build
546
+ npm run build
547
+
548
+ # Run tests
549
+ npm test
550
+
551
+ # Lint
552
+ npm run lint
553
+
554
+ # Format
555
+ npm run format
556
+ ```
557
+
558
+ ## License
559
+
560
+ MIT © Emmett Community
561
+
562
+ ## Resources
563
+
564
+ - [Emmett Documentation](https://event-driven-io.github.io/emmett/)
565
+ - [Event Sourcing Guide](https://event-driven.io/en/event_sourcing_basics/)
566
+ - [Google Firestore Docs](https://cloud.google.com/firestore/docs)
567
+ - [GitHub Repository](https://github.com/emmett-community/emmett-google-firestore)
568
+
569
+ ## Support
570
+
571
+ - **Issues**: [GitHub Issues](https://github.com/emmett-community/emmett-google-firestore/issues)
572
+ - **Discussions**: [GitHub Discussions](https://github.com/emmett-community/emmett-google-firestore/discussions)
573
+ - **Emmett Discord**: [Join Discord](https://discord.gg/fTpqUTMmVa)
574
+
575
+ ## Acknowledgments
576
+
577
+ - Built for the [Emmett](https://event-driven-io.github.io/emmett/) framework by [Oskar Dudycz](https://github.com/oskardudycz)
578
+ - Inspired by [emmett-mongodb](https://github.com/event-driven-io/emmett/tree/main/src/packages/emmett-mongodb)
579
+ - Part of the [Emmett Community](https://github.com/emmett-community)
@@ -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.mjs';
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.mjs';
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 };