@emmett-community/emmett-google-firestore 0.1.0 → 0.3.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 +64 -30
- package/dist/index.d.mts +138 -4
- package/dist/index.d.ts +138 -4
- package/dist/index.js +133 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +133 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -19
- package/dist/testing/index.d.mts +0 -115
- package/dist/testing/index.d.ts +0 -115
- package/dist/testing/index.js +0 -296
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/index.mjs +0 -289
- package/dist/testing/index.mjs.map +0 -1
- package/dist/types-CHnx_sMk.d.mts +0 -122
- package/dist/types-CHnx_sMk.d.ts +0 -122
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"],"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"]}
|
|
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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
|
1
2
|
import { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
2
3
|
export { NO_CONCURRENCY_CHECK, STREAM_DOES_NOT_EXIST, STREAM_EXISTS } from '@event-driven-io/emmett';
|
|
3
4
|
|
|
4
|
-
// src/eventStore/
|
|
5
|
+
// src/eventStore/firestoreEventStore.ts
|
|
5
6
|
var ExpectedVersionConflictError = class _ExpectedVersionConflictError extends Error {
|
|
6
7
|
constructor(streamName, expected, actual) {
|
|
7
8
|
super(
|
|
@@ -82,6 +83,14 @@ function calculateNextVersion(currentVersion, eventCount) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// src/eventStore/firestoreEventStore.ts
|
|
86
|
+
var tracer = trace.getTracer("@emmett-community/emmett-google-firestore");
|
|
87
|
+
function safeLog(logger, level, msg, data) {
|
|
88
|
+
if (!logger) return;
|
|
89
|
+
const logFn = logger[level];
|
|
90
|
+
if (typeof logFn === "function") {
|
|
91
|
+
logFn.call(logger, msg, data);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
85
94
|
var DEFAULT_COLLECTIONS = {
|
|
86
95
|
streams: "streams",
|
|
87
96
|
counters: "_counters"
|
|
@@ -93,39 +102,70 @@ var FirestoreEventStoreImpl = class {
|
|
|
93
102
|
...DEFAULT_COLLECTIONS,
|
|
94
103
|
...options.collections
|
|
95
104
|
};
|
|
105
|
+
this.logger = options.observability?.logger;
|
|
106
|
+
safeLog(this.logger, "info", "FirestoreEventStore initialized");
|
|
96
107
|
}
|
|
97
108
|
collections;
|
|
109
|
+
logger;
|
|
98
110
|
/**
|
|
99
111
|
* Read events from a stream
|
|
100
112
|
*/
|
|
101
113
|
async readStream(streamName, options = {}) {
|
|
102
|
-
const
|
|
103
|
-
|
|
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
|
-
};
|
|
114
|
+
const span = tracer.startSpan("emmett.firestore.read_stream", {
|
|
115
|
+
attributes: { "emmett.stream_name": streamName }
|
|
128
116
|
});
|
|
117
|
+
try {
|
|
118
|
+
safeLog(this.logger, "debug", "Reading stream", {
|
|
119
|
+
streamName,
|
|
120
|
+
from: options.from?.toString(),
|
|
121
|
+
to: options.to?.toString(),
|
|
122
|
+
maxCount: options.maxCount
|
|
123
|
+
});
|
|
124
|
+
const { from, to, maxCount } = options;
|
|
125
|
+
let query = this.firestore.collection(this.collections.streams).doc(streamName).collection("events").orderBy("streamVersion", "asc");
|
|
126
|
+
if (from !== void 0) {
|
|
127
|
+
query = query.where("streamVersion", ">=", Number(from));
|
|
128
|
+
}
|
|
129
|
+
if (to !== void 0) {
|
|
130
|
+
query = query.where("streamVersion", "<=", Number(to));
|
|
131
|
+
}
|
|
132
|
+
if (maxCount !== void 0 && maxCount > 0) {
|
|
133
|
+
query = query.limit(maxCount);
|
|
134
|
+
}
|
|
135
|
+
const snapshot = await query.get();
|
|
136
|
+
const events = snapshot.docs.map((doc) => {
|
|
137
|
+
const data = doc.data();
|
|
138
|
+
return {
|
|
139
|
+
type: data.type,
|
|
140
|
+
data: data.data,
|
|
141
|
+
metadata: {
|
|
142
|
+
...data.metadata,
|
|
143
|
+
streamName,
|
|
144
|
+
streamVersion: BigInt(data.streamVersion),
|
|
145
|
+
streamPosition: BigInt(data.streamVersion),
|
|
146
|
+
globalPosition: BigInt(data.globalPosition),
|
|
147
|
+
timestamp: timestampToDate(data.timestamp)
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
span.setAttribute("emmett.event_count", events.length);
|
|
152
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
153
|
+
safeLog(this.logger, "debug", "Stream read completed", {
|
|
154
|
+
streamName,
|
|
155
|
+
eventCount: events.length
|
|
156
|
+
});
|
|
157
|
+
return events;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
span.recordException(error);
|
|
160
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
161
|
+
safeLog(this.logger, "error", "Failed to read stream", {
|
|
162
|
+
streamName,
|
|
163
|
+
error
|
|
164
|
+
});
|
|
165
|
+
throw error;
|
|
166
|
+
} finally {
|
|
167
|
+
span.end();
|
|
168
|
+
}
|
|
129
169
|
}
|
|
130
170
|
/**
|
|
131
171
|
* Aggregate stream by applying events to state
|
|
@@ -146,21 +186,66 @@ var FirestoreEventStoreImpl = class {
|
|
|
146
186
|
* Append events to a stream with optimistic concurrency control
|
|
147
187
|
*/
|
|
148
188
|
async appendToStream(streamName, events, options = {}) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return await this.appendToStreamInTransaction(
|
|
155
|
-
transaction,
|
|
156
|
-
streamName,
|
|
157
|
-
events,
|
|
158
|
-
expectedStreamVersion
|
|
159
|
-
);
|
|
189
|
+
const span = tracer.startSpan("emmett.firestore.append_to_stream", {
|
|
190
|
+
attributes: {
|
|
191
|
+
"emmett.stream_name": streamName,
|
|
192
|
+
"emmett.event_count": events.length
|
|
193
|
+
}
|
|
160
194
|
});
|
|
195
|
+
try {
|
|
196
|
+
if (events.length === 0) {
|
|
197
|
+
throw new Error("Cannot append empty event array");
|
|
198
|
+
}
|
|
199
|
+
const { expectedStreamVersion = NO_CONCURRENCY_CHECK } = options;
|
|
200
|
+
safeLog(this.logger, "debug", "Appending to stream", {
|
|
201
|
+
streamName,
|
|
202
|
+
eventCount: events.length,
|
|
203
|
+
eventTypes: events.map((e) => e.type),
|
|
204
|
+
expectedVersion: String(expectedStreamVersion)
|
|
205
|
+
});
|
|
206
|
+
const result = await this.firestore.runTransaction(async (transaction) => {
|
|
207
|
+
return await this.appendToStreamInTransaction(
|
|
208
|
+
transaction,
|
|
209
|
+
streamName,
|
|
210
|
+
events,
|
|
211
|
+
expectedStreamVersion
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
span.setAttribute("emmett.new_version", Number(result.nextExpectedStreamVersion));
|
|
215
|
+
span.setAttribute("emmett.created_new_stream", result.createdNewStream);
|
|
216
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
217
|
+
safeLog(this.logger, "debug", "Append completed", {
|
|
218
|
+
streamName,
|
|
219
|
+
newVersion: result.nextExpectedStreamVersion.toString(),
|
|
220
|
+
createdNewStream: result.createdNewStream
|
|
221
|
+
});
|
|
222
|
+
return result;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
span.recordException(error);
|
|
225
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
226
|
+
if (error instanceof ExpectedVersionConflictError) {
|
|
227
|
+
safeLog(this.logger, "warn", "Version conflict during append", {
|
|
228
|
+
streamName,
|
|
229
|
+
expected: String(error.expected),
|
|
230
|
+
actual: String(error.actual)
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
safeLog(this.logger, "error", "Failed to append to stream", {
|
|
234
|
+
streamName,
|
|
235
|
+
error
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
} finally {
|
|
240
|
+
span.end();
|
|
241
|
+
}
|
|
161
242
|
}
|
|
162
243
|
/**
|
|
163
244
|
* Internal method to append events within a transaction
|
|
245
|
+
*
|
|
246
|
+
* Note: No separate span here - this method runs inside appendToStream's span,
|
|
247
|
+
* and Firestore transaction operations are atomic. The parent span captures
|
|
248
|
+
* the full transaction duration.
|
|
164
249
|
*/
|
|
165
250
|
async appendToStreamInTransaction(transaction, streamName, events, expectedStreamVersion) {
|
|
166
251
|
const streamRef = this.firestore.collection(this.collections.streams).doc(streamName);
|
|
@@ -171,6 +256,11 @@ var FirestoreEventStoreImpl = class {
|
|
|
171
256
|
streamExists,
|
|
172
257
|
streamData?.version
|
|
173
258
|
);
|
|
259
|
+
safeLog(this.logger, "debug", "Read stream metadata", {
|
|
260
|
+
streamName,
|
|
261
|
+
exists: streamExists,
|
|
262
|
+
currentVersion: currentVersion === STREAM_DOES_NOT_EXIST ? "none" : currentVersion.toString()
|
|
263
|
+
});
|
|
174
264
|
assertExpectedVersionMatchesCurrent(
|
|
175
265
|
streamName,
|
|
176
266
|
expectedStreamVersion,
|
|
@@ -207,6 +297,11 @@ var FirestoreEventStoreImpl = class {
|
|
|
207
297
|
value: globalPosition,
|
|
208
298
|
updatedAt: now
|
|
209
299
|
});
|
|
300
|
+
safeLog(this.logger, "debug", "Events written to transaction", {
|
|
301
|
+
streamName,
|
|
302
|
+
count: events.length,
|
|
303
|
+
newVersion
|
|
304
|
+
});
|
|
210
305
|
return {
|
|
211
306
|
nextExpectedStreamVersion: BigInt(newVersion),
|
|
212
307
|
createdNewStream: !streamExists
|
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 * 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"]}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emmett-community/emmett-google-firestore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Google Firestore event store implementation for Emmett",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -15,16 +15,6 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
17
|
}
|
|
18
|
-
},
|
|
19
|
-
"./testing": {
|
|
20
|
-
"import": {
|
|
21
|
-
"types": "./dist/testing/index.d.mts",
|
|
22
|
-
"default": "./dist/testing/index.mjs"
|
|
23
|
-
},
|
|
24
|
-
"require": {
|
|
25
|
-
"types": "./dist/testing/index.d.ts",
|
|
26
|
-
"default": "./dist/testing/index.js"
|
|
27
|
-
}
|
|
28
18
|
}
|
|
29
19
|
},
|
|
30
20
|
"files": [
|
|
@@ -33,12 +23,13 @@
|
|
|
33
23
|
"scripts": {
|
|
34
24
|
"build": "tsup",
|
|
35
25
|
"build:ts": "tsc --noEmit",
|
|
36
|
-
"test": "
|
|
37
|
-
"test:unit": "jest
|
|
38
|
-
"test:
|
|
39
|
-
"test:
|
|
40
|
-
"test:
|
|
41
|
-
"test:
|
|
26
|
+
"test": "npm run test:unit && npm run test:int && npm run test:e2e",
|
|
27
|
+
"test:unit": "jest --testMatch \"**/*.unit.spec.ts\" --watchman=false",
|
|
28
|
+
"test:int": "jest --testMatch \"**/*.int.spec.ts\" --watchman=false",
|
|
29
|
+
"test:integration": "npm run test:int",
|
|
30
|
+
"test:e2e": "jest --testMatch \"**/*.e2e.spec.ts\" --watchman=false",
|
|
31
|
+
"test:watch": "jest --watch --watchman=false",
|
|
32
|
+
"test:coverage": "jest --coverage --watchman=false",
|
|
42
33
|
"lint": "eslint . --ext .ts",
|
|
43
34
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
44
35
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
@@ -68,13 +59,20 @@
|
|
|
68
59
|
},
|
|
69
60
|
"homepage": "https://github.com/emmett-community/emmett-google-firestore#readme",
|
|
70
61
|
"peerDependencies": {
|
|
71
|
-
"@event-driven-io/emmett": "^0.39.0"
|
|
62
|
+
"@event-driven-io/emmett": "^0.39.0",
|
|
63
|
+
"@opentelemetry/api": "^1.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@opentelemetry/api": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
72
69
|
},
|
|
73
70
|
"dependencies": {
|
|
74
71
|
"@google-cloud/firestore": "^7.10.0"
|
|
75
72
|
},
|
|
76
73
|
"devDependencies": {
|
|
77
74
|
"@event-driven-io/emmett": "^0.39.1",
|
|
75
|
+
"@opentelemetry/api": "^1.9.0",
|
|
78
76
|
"@types/jest": "^29.5.14",
|
|
79
77
|
"@types/node": "^22.10.1",
|
|
80
78
|
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
|
@@ -84,6 +82,7 @@
|
|
|
84
82
|
"eslint-plugin-prettier": "^5.2.1",
|
|
85
83
|
"jest": "^29.7.0",
|
|
86
84
|
"prettier": "^3.4.2",
|
|
85
|
+
"testcontainers": "^10.28.0",
|
|
87
86
|
"ts-jest": "^29.2.5",
|
|
88
87
|
"ts-node": "^10.9.2",
|
|
89
88
|
"tsup": "^8.3.5",
|
|
@@ -92,5 +91,7 @@
|
|
|
92
91
|
"engines": {
|
|
93
92
|
"node": ">=18.0.0"
|
|
94
93
|
},
|
|
95
|
-
"publishConfig": {
|
|
94
|
+
"publishConfig": {
|
|
95
|
+
"access": "public"
|
|
96
|
+
}
|
|
96
97
|
}
|