UNPKG

@delta-base/toolkit

Version:

Application-level event sourcing toolkit for delta-base

1,510 lines (1,505 loc) 56.7 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/core/types.ts var event = /* @__PURE__ */ __name((...args) => { const [type, data, metadata] = args; return metadata !== void 0 ? { type, data, metadata } : { type, data }; }, "event"); var command = /* @__PURE__ */ __name((...args) => { const [type, data, metadata] = args; return metadata !== void 0 ? { type, data, metadata } : { type, data }; }, "command"); var STREAM_EXISTS = "stream_exists"; var STREAM_DOES_NOT_EXIST = "no_stream"; var NO_CONCURRENCY_CHECK = "any"; var matchesExpectedVersion = /* @__PURE__ */ __name((current, expected, defaultVersion) => { if (expected === NO_CONCURRENCY_CHECK) return true; if (expected === STREAM_DOES_NOT_EXIST) return current === defaultVersion; if (expected === STREAM_EXISTS) return current !== defaultVersion; return current === expected; }, "matchesExpectedVersion"); // src/database/in-memory-event-store.ts var _InMemoryEventStore = class _InMemoryEventStore { constructor() { this.streams = /* @__PURE__ */ new Map(); this.streamMetadata = /* @__PURE__ */ new Map(); this.globalPositionCounter = 0; } /** * Append events to a stream * * @param streamId - The stream identifier * @param events - Events to append * @param options - Append options including expected version * @returns Promise resolving to the append result */ async appendToStream(streamId, events, options) { if (!streamId) { throw new Error("Stream id is required"); } if (!Array.isArray(events) || events.length === 0) { throw new Error("Events array cannot be empty"); } const existingEvents = this.streams.get(streamId) || []; const currentVersion = existingEvents.length; const streamExists = this.streams.has(streamId); if (options?.expectedStreamVersion !== void 0) { this.assertExpectedVersionMatchesCurrent( currentVersion, options.expectedStreamVersion, streamExists ); } const now = (/* @__PURE__ */ new Date()).toISOString(); const storedEvents = events.map((event2, index) => ({ event: event2, streamId, streamPosition: currentVersion + index + 1, globalPosition: ++this.globalPositionCounter, eventId: crypto.randomUUID(), schemaVersion: "metadata" in event2 && typeof event2.metadata === "object" && event2.metadata !== null ? "schemaVersion" in event2.metadata ? String(event2.metadata.schemaVersion) : "1.0.0" : "1.0.0", transactionId: "metadata" in event2 && typeof event2.metadata === "object" && event2.metadata !== null ? "transactionId" in event2.metadata && event2.metadata.transactionId ? String(event2.metadata.transactionId) : crypto.randomUUID() : crypto.randomUUID(), metadata: "metadata" in event2 ? event2.metadata || {} : {}, createdAt: now })); const newEvents = [...existingEvents, ...storedEvents]; this.streams.set(streamId, newEvents); const existing = this.streamMetadata.get(streamId); this.streamMetadata.set(streamId, { streamId, version: newEvents.length, createdAt: existing?.createdAt || now, updatedAt: now, metadata: { ...existing?.metadata } }); return { nextExpectedStreamVersion: newEvents.length, createdNewStream: !streamExists }; } /** * Helper method to assert expected version matches current version * @private */ assertExpectedVersionMatchesCurrent(currentVersion, expectedVersion, streamExists) { if (typeof expectedVersion === "number") { if (expectedVersion !== currentVersion) { throw new Error( `Expected stream version ${expectedVersion} but current version is ${currentVersion}` ); } } else if (expectedVersion === "no_stream" && streamExists) { throw new Error("Expected no stream but stream exists"); } else if (expectedVersion === "stream_exists" && !streamExists) { throw new Error("Expected stream to exist but it does not"); } } /** * Get all stream IDs (utility method for testing) * * @returns Array of all stream identifiers * @public */ getAllStreamIds() { return Array.from(this.streams.keys()); } /** * Get total event count across all streams (utility method for testing) * * @returns Total number of events stored * @public */ getTotalEventCount() { return Array.from(this.streams.values()).reduce( (total, events) => total + events.length, 0 ); } /** * Clear all data (utility method for testing) * @public */ clear() { this.streams.clear(); this.streamMetadata.clear(); this.globalPositionCounter = 0; } /** * Get all events across all streams (utility method for testing) * * @returns Array of all events ordered by global position * @public */ getAllEvents() { const allEvents = []; for (const events of this.streams.values()) { allEvents.push(...events); } const sortedEvents = allEvents.sort( (a, b) => a.globalPosition - b.globalPosition ); return sortedEvents.map( (stored) => ({ type: stored.event.type, data: stored.event.data, metadata: "metadata" in stored.event ? stored.event.metadata : void 0, streamId: stored.streamId, streamPosition: stored.streamPosition, globalPosition: stored.globalPosition, eventId: stored.eventId, schemaVersion: stored.schemaVersion, transactionId: stored.transactionId, createdAt: stored.createdAt }) ); } /** * Check if a stream exists (utility method for testing) * * @param streamId - The stream identifier * @returns Promise resolving to boolean indicating existence * @public */ async streamExists(streamId) { return this.streams.has(streamId); } /** * Aggregate events from a stream to rebuild state * * @param streamId - The stream identifier * @param options - Aggregation options including initial state and evolution function * @returns Promise resolving to the aggregated state and stream metadata */ async aggregateStream(streamId, options) { const streamExists = this.streams.has(streamId); const readResult = await this.readStream(streamId, options.read); let state = options.initialState(); for (const event2 of readResult.events) { state = options.evolve(state, event2); } return { currentStreamVersion: readResult.currentStreamVersion, state, streamExists }; } /** * Read events from a stream with options * * @param streamId - The stream identifier * @param options - Read options for filtering and pagination * @returns Promise resolving to read result with events and metadata */ async readStream(streamId, options) { if (!streamId) { throw new Error("Stream id is required"); } const storedEvents = this.streams.get(streamId) || []; const streamExists = this.streams.has(streamId); const currentStreamVersion = storedEvents.length; if (options?.expectedStreamVersion !== void 0) { this.assertExpectedVersionMatchesCurrent( currentStreamVersion, options.expectedStreamVersion, streamExists ); } if (!streamExists) { return { currentStreamVersion: 0, events: [], streamExists: false }; } let filteredEvents = [...storedEvents]; if (options) { if ("from" in options && options.from !== void 0) { const fromIndex = Math.max(0, Number(options.from) - 1); filteredEvents = filteredEvents.slice(fromIndex); } if ("to" in options && options.to !== void 0) { const fromIndex = "from" in options && options.from !== void 0 ? Math.max(0, Number(options.from) - 1) : 0; const toIndex = Math.min(storedEvents.length, Number(options.to)); filteredEvents = storedEvents.slice(fromIndex, toIndex); } if ("maxCount" in options && options.maxCount !== void 0 && options.maxCount > 0) { filteredEvents = filteredEvents.slice(0, options.maxCount); } } const events = filteredEvents.map((stored) => ({ type: stored.event.type, data: stored.event.data, metadata: "metadata" in stored.event ? stored.event.metadata : void 0, streamId: stored.streamId, streamPosition: stored.streamPosition, globalPosition: stored.globalPosition, eventId: stored.eventId, schemaVersion: stored.schemaVersion, transactionId: stored.transactionId, createdAt: stored.createdAt })); return { currentStreamVersion, events, streamExists }; } /** * Query events across all streams with flexible filtering options * * @param options - Query parameters for filtering events * @returns Promise resolving to the query result with events and pagination info */ async queryEvents(options = {}) { let allEvents = this.getAllEvents(); if (options.streamId) { allEvents = allEvents.filter( (event2) => event2.streamId === options.streamId ); } if (options.eventId) { allEvents = allEvents.filter( (event2) => event2.eventId === options.eventId ); } if (options.type) { if (Array.isArray(options.type)) { allEvents = allEvents.filter( (event2) => options.type.includes(event2.type) ); } else { allEvents = allEvents.filter((event2) => event2.type === options.type); } } if (options.transactionId) { allEvents = allEvents.filter( (event2) => event2.transactionId === options.transactionId ); } if (options.fromPosition !== void 0) { allEvents = allEvents.filter( (event2) => event2.globalPosition >= options.fromPosition ); } if (options.toPosition !== void 0) { allEvents = allEvents.filter( (event2) => event2.globalPosition <= options.toPosition ); } if (options.fromDate) { allEvents = allEvents.filter( (event2) => event2.createdAt >= options.fromDate ); } if (options.toDate) { allEvents = allEvents.filter( (event2) => event2.createdAt <= options.toDate ); } const sortBy = options.sortBy || "globalPosition"; const sortDirection = options.sortDirection || "asc"; allEvents.sort((a, b) => { let comparison = 0; switch (sortBy) { case "globalPosition": comparison = a.globalPosition - b.globalPosition; break; case "streamPosition": comparison = a.streamPosition - b.streamPosition; break; case "createdAt": comparison = a.createdAt.localeCompare(b.createdAt); break; } return sortDirection === "desc" ? -comparison : comparison; }); const total = allEvents.length; const limit = Math.min(options.limit || 100, 1e3); const offset = options.offset || 0; const paginatedEvents = allEvents.slice(offset, offset + limit); return { events: paginatedEvents, pagination: { limit, offset, total: options.includeCount ? total : void 0, hasMore: offset + limit < total } }; } }; __name(_InMemoryEventStore, "InMemoryEventStore"); var InMemoryEventStore = _InMemoryEventStore; // src/errors/index.ts var _DeltaBaseError = class _DeltaBaseError extends Error { constructor(message, status, body, errorType, details) { super(message); this.name = this.constructor.name; this.status = status; this.body = body; this.errorType = errorType; this.details = details; } }; __name(_DeltaBaseError, "DeltaBaseError"); var DeltaBaseError = _DeltaBaseError; var _VersionConflictError = class _VersionConflictError extends DeltaBaseError { constructor(message, body, currentVersion, expectedVersion) { super(message, 409, body, "Version conflict", { currentVersion, expectedVersion }); this.currentVersion = currentVersion; this.expectedVersion = expectedVersion; } }; __name(_VersionConflictError, "VersionConflictError"); var VersionConflictError = _VersionConflictError; var _ValidationError = class _ValidationError extends DeltaBaseError { constructor(message, body, validationErrors) { super(message, 400, body || message, "Invalid request body", { validationErrors }); this.validationErrors = validationErrors; } }; __name(_ValidationError, "ValidationError"); var ValidationError = _ValidationError; var _EventStoreNotFoundError = class _EventStoreNotFoundError extends DeltaBaseError { constructor(message, body, eventStoreId) { super(message, 404, body, "EVENT_STORE_NOT_FOUND", { eventStoreId }); this.eventStoreId = eventStoreId; } }; __name(_EventStoreNotFoundError, "EventStoreNotFoundError"); var EventStoreNotFoundError = _EventStoreNotFoundError; var _SubscriptionNotFoundError = class _SubscriptionNotFoundError extends DeltaBaseError { constructor(message, body, subscriptionId, eventStoreId) { super(message, 404, body, "SUBSCRIPTION_NOT_FOUND", { subscriptionId, eventStoreId }); this.subscriptionId = subscriptionId; this.eventStoreId = eventStoreId; } }; __name(_SubscriptionNotFoundError, "SubscriptionNotFoundError"); var SubscriptionNotFoundError = _SubscriptionNotFoundError; var _AuthenticationError = class _AuthenticationError extends DeltaBaseError { constructor(message, body) { super(message, 401, body, "AUTHENTICATION_FAILED"); } }; __name(_AuthenticationError, "AuthenticationError"); var AuthenticationError = _AuthenticationError; var _AuthorizationError = class _AuthorizationError extends DeltaBaseError { constructor(message, body) { super(message, 403, body, "AUTHORIZATION_FAILED"); } }; __name(_AuthorizationError, "AuthorizationError"); var AuthorizationError = _AuthorizationError; var _RateLimitError = class _RateLimitError extends DeltaBaseError { constructor(message, body, retryAfter) { super(message, 429, body, "RATE_LIMIT_EXCEEDED", { retryAfter }); this.retryAfter = retryAfter; } }; __name(_RateLimitError, "RateLimitError"); var RateLimitError = _RateLimitError; var _EventStoreAlreadyExistsError = class _EventStoreAlreadyExistsError extends DeltaBaseError { constructor(message, body, name) { super(message, 409, body, "EVENT_STORE_ALREADY_EXISTS", { name }); this.name = name; } }; __name(_EventStoreAlreadyExistsError, "EventStoreAlreadyExistsError"); var EventStoreAlreadyExistsError = _EventStoreAlreadyExistsError; var _InvalidSubscriptionConfigError = class _InvalidSubscriptionConfigError extends DeltaBaseError { constructor(message, body, configError) { super(message, 400, body, "INVALID_SUBSCRIPTION_CONFIG", { configError }); this.configError = configError; } }; __name(_InvalidSubscriptionConfigError, "InvalidSubscriptionConfigError"); var InvalidSubscriptionConfigError = _InvalidSubscriptionConfigError; var _TimeoutError = class _TimeoutError extends DeltaBaseError { constructor(message, body, timeoutMs, operation) { super(message, 408, body, "TIMEOUT_ERROR", { timeoutMs, operation }); this.timeoutMs = timeoutMs; this.operation = operation; } }; __name(_TimeoutError, "TimeoutError"); var TimeoutError = _TimeoutError; var _StorageError = class _StorageError extends DeltaBaseError { constructor(message, body, storageType, operation) { super(message, 500, body, "STORAGE_ERROR", { storageType, operation }); this.storageType = storageType; this.operation = operation; } }; __name(_StorageError, "StorageError"); var StorageError = _StorageError; var _StreamNotFoundError = class _StreamNotFoundError extends DeltaBaseError { constructor(message, body, streamId, eventStoreId) { super(message, 404, body, "STREAM_NOT_FOUND", { streamId, eventStoreId }); this.streamId = streamId; this.eventStoreId = eventStoreId; } }; __name(_StreamNotFoundError, "StreamNotFoundError"); var StreamNotFoundError = _StreamNotFoundError; var _SerializationError = class _SerializationError extends DeltaBaseError { constructor(message, body, dataType, originalData) { super(message, 400, body, "SERIALIZATION_ERROR", { dataType, originalData }); this.dataType = dataType; this.originalData = originalData; } }; __name(_SerializationError, "SerializationError"); var SerializationError = _SerializationError; var _InternalServerError = class _InternalServerError extends DeltaBaseError { constructor(message, body) { super(message, 500, body, "INTERNAL_SERVER_ERROR"); } }; __name(_InternalServerError, "InternalServerError"); var InternalServerError = _InternalServerError; var _IllegalStateError = class _IllegalStateError extends DeltaBaseError { constructor(message, body, currentState, attemptedOperation) { super(message, 400, body, "ILLEGAL_STATE", { currentState, attemptedOperation }); this.currentState = currentState; this.attemptedOperation = attemptedOperation; } }; __name(_IllegalStateError, "IllegalStateError"); var IllegalStateError = _IllegalStateError; var _NotFoundError = class _NotFoundError extends DeltaBaseError { constructor(options) { const message = options?.message ?? (options?.id && options?.type ? `${options.type} with ID '${options.id}' was not found` : options?.type ? `${options.type} was not found` : "Resource was not found"); super(message, 404, options?.body || message, "NOT_FOUND", { resourceId: options?.id, resourceType: options?.type }); this.resourceId = options?.id; this.resourceType = options?.type; } }; __name(_NotFoundError, "NotFoundError"); var NotFoundError = _NotFoundError; function isDeltaBaseError(error) { return error instanceof DeltaBaseError; } __name(isDeltaBaseError, "isDeltaBaseError"); function isVersionConflictError(error) { return error instanceof VersionConflictError; } __name(isVersionConflictError, "isVersionConflictError"); function isValidationError(error) { return error instanceof ValidationError; } __name(isValidationError, "isValidationError"); function isEventStoreNotFoundError(error) { return error instanceof EventStoreNotFoundError; } __name(isEventStoreNotFoundError, "isEventStoreNotFoundError"); function isSubscriptionNotFoundError(error) { return error instanceof SubscriptionNotFoundError; } __name(isSubscriptionNotFoundError, "isSubscriptionNotFoundError"); function isAuthenticationError(error) { return error instanceof AuthenticationError; } __name(isAuthenticationError, "isAuthenticationError"); function isAuthorizationError(error) { return error instanceof AuthorizationError; } __name(isAuthorizationError, "isAuthorizationError"); function isRateLimitError(error) { return error instanceof RateLimitError; } __name(isRateLimitError, "isRateLimitError"); function isEventStoreAlreadyExistsError(error) { return error instanceof EventStoreAlreadyExistsError; } __name(isEventStoreAlreadyExistsError, "isEventStoreAlreadyExistsError"); function isInvalidSubscriptionConfigError(error) { return error instanceof InvalidSubscriptionConfigError; } __name(isInvalidSubscriptionConfigError, "isInvalidSubscriptionConfigError"); function isTimeoutError(error) { return error instanceof TimeoutError; } __name(isTimeoutError, "isTimeoutError"); function isStorageError(error) { return error instanceof StorageError; } __name(isStorageError, "isStorageError"); function isStreamNotFoundError(error) { return error instanceof StreamNotFoundError; } __name(isStreamNotFoundError, "isStreamNotFoundError"); function isSerializationError(error) { return error instanceof SerializationError; } __name(isSerializationError, "isSerializationError"); function isNotFoundError(error) { return error instanceof NotFoundError; } __name(isNotFoundError, "isNotFoundError"); function isInternalServerError(error) { return error instanceof InternalServerError; } __name(isInternalServerError, "isInternalServerError"); function isIllegalStateError(error) { return error instanceof IllegalStateError; } __name(isIllegalStateError, "isIllegalStateError"); var _StreamVersionConflictError = class _StreamVersionConflictError extends VersionConflictError { constructor(current, expected, message) { const currentVersion = typeof current === "number" ? current : -1; const expectedVersion = typeof expected === "number" ? expected : -1; super( message ?? `Expected stream version ${expected} does not match current ${current}`, { current, expected }, currentVersion, expectedVersion ); this.current = current; this.expected = expected; } }; __name(_StreamVersionConflictError, "StreamVersionConflictError"); var StreamVersionConflictError = _StreamVersionConflictError; function isStreamVersionConflictError(error) { return error instanceof StreamVersionConflictError; } __name(isStreamVersionConflictError, "isStreamVersionConflictError"); var ValidationErrors = /* @__PURE__ */ ((ValidationErrors2) => { ValidationErrors2["NOT_A_NONEMPTY_STRING"] = "NOT_A_NONEMPTY_STRING"; ValidationErrors2["NOT_A_POSITIVE_NUMBER"] = "NOT_A_POSITIVE_NUMBER"; ValidationErrors2["NOT_AN_UNSIGNED_BIGINT"] = "NOT_AN_UNSIGNED_BIGINT"; return ValidationErrors2; })(ValidationErrors || {}); var isNumber = /* @__PURE__ */ __name((val) => typeof val === "number" && !Number.isNaN(val), "isNumber"); var isString = /* @__PURE__ */ __name((val) => typeof val === "string", "isString"); var assertNotEmptyString = /* @__PURE__ */ __name((value) => { if (!isString(value) || value.length === 0) { throw new ValidationError("NOT_A_NONEMPTY_STRING" /* NOT_A_NONEMPTY_STRING */); } return value; }, "assertNotEmptyString"); var assertPositiveNumber = /* @__PURE__ */ __name((value) => { if (!isNumber(value) || value <= 0) { throw new ValidationError("NOT_A_POSITIVE_NUMBER" /* NOT_A_POSITIVE_NUMBER */); } return value; }, "assertPositiveNumber"); var assertUnsignedBigInt = /* @__PURE__ */ __name((value) => { const number = BigInt(value); if (number < 0) { throw new ValidationError("NOT_AN_UNSIGNED_BIGINT" /* NOT_AN_UNSIGNED_BIGINT */); } return number; }, "assertUnsignedBigInt"); // src/patterns/command-handler.ts var CommandHandlerStreamVersionConflictRetryOptions = { retries: 3, minTimeout: 100, factor: 1.5, shouldRetryError: isStreamVersionConflictError }; async function retry(fn, options) { let lastError; for (let attempt = 0; attempt <= options.retries; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt === options.retries || options.shouldRetryError && !options.shouldRetryError(lastError)) { throw lastError; } const delay = options.minTimeout * options.factor ** attempt; await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError ?? new Error("Retry failed with unknown error"); } __name(retry, "retry"); async function handleCommand(eventStore, streamId, handler, options) { const operation = /* @__PURE__ */ __name(async () => { const { evolve, initialState, expectedStreamVersion } = options; const aggregationResult = await eventStore.aggregateStream( streamId, { evolve, initialState } ); const { state: currentState, currentStreamVersion, streamExists } = aggregationResult; const handlerResult = await handler(currentState); const newEvents = Array.isArray(handlerResult) ? handlerResult : [handlerResult]; if (newEvents.length === 0) { return { newState: currentState, newEvents: [], appendResult: { nextExpectedStreamVersion: currentStreamVersion, createdNewStream: false } }; } let newState = currentState; for (let i = 0; i < newEvents.length; i++) { const event2 = newEvents[i]; if (!event2) continue; const readEvent = { type: event2.type, data: event2.data, metadata: event2.metadata, streamId, streamPosition: currentStreamVersion + i + 1, globalPosition: 0, // Will be set by event store eventId: "", // Will be set by event store schemaVersion: "1.0.0", transactionId: "", createdAt: (/* @__PURE__ */ new Date()).toISOString() }; newState = evolve(newState, readEvent); } const finalExpectedStreamVersion = expectedStreamVersion ?? (streamExists ? currentStreamVersion : "no_stream"); const appendResult = await eventStore.appendToStream(streamId, newEvents, { expectedStreamVersion: finalExpectedStreamVersion }); return { newState, newEvents, appendResult }; }, "operation"); if (options.retryOptions) { return retry(operation, options.retryOptions); } return operation(); } __name(handleCommand, "handleCommand"); async function handleCommandWithDecider(eventStore, streamId, command2, decider, options) { const operation = /* @__PURE__ */ __name(async () => { const { expectedStreamVersion } = options || {}; const { evolve, initialState, decide } = decider; const aggregationResult = await eventStore.aggregateStream( streamId, { evolve, initialState } ); const { state: currentState, currentStreamVersion, streamExists } = aggregationResult; const decideResult = decide(command2, currentState); const newEvents = Array.isArray(decideResult) ? decideResult : [decideResult]; if (newEvents.length === 0) { return { newState: currentState, newEvents: [], appendResult: { nextExpectedStreamVersion: currentStreamVersion, createdNewStream: false } }; } let newState = currentState; for (let i = 0; i < newEvents.length; i++) { const event2 = newEvents[i]; if (!event2) continue; const readEvent = { type: event2.type, data: event2.data, metadata: event2.metadata, streamId, streamPosition: currentStreamVersion + i + 1, globalPosition: 0, // Will be set by event store eventId: "", // Will be set by event store schemaVersion: "1.0.0", transactionId: "", createdAt: (/* @__PURE__ */ new Date()).toISOString() }; newState = evolve(newState, readEvent); } const finalExpectedStreamVersion = expectedStreamVersion ?? (streamExists ? currentStreamVersion : "no_stream"); const appendResult = await eventStore.appendToStream(streamId, newEvents, { expectedStreamVersion: finalExpectedStreamVersion }); return { newState, newEvents, appendResult }; }, "operation"); if (options?.retryOptions) { return retry(operation, options.retryOptions); } return operation(); } __name(handleCommandWithDecider, "handleCommandWithDecider"); function handleCommandWithRetry(eventStore, streamId, handler, options) { return handleCommand(eventStore, streamId, handler, { ...options, retryOptions: options.retryOptions || CommandHandlerStreamVersionConflictRetryOptions }); } __name(handleCommandWithRetry, "handleCommandWithRetry"); function handleCommandWithDeciderAndRetry(eventStore, streamId, command2, decider, options) { return handleCommandWithDecider(eventStore, streamId, command2, decider, { ...options, retryOptions: options?.retryOptions || CommandHandlerStreamVersionConflictRetryOptions }); } __name(handleCommandWithDeciderAndRetry, "handleCommandWithDeciderAndRetry"); // src/patterns/projection.ts var _InMemoryReadModelStore = class _InMemoryReadModelStore { constructor() { this.store = /* @__PURE__ */ new Map(); this.metadata = /* @__PURE__ */ new Map(); } async put(key, value, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); this.store.set(prefixedKey, value); if (options?.expiration || options?.expirationTtl || options?.metadata) { const meta = {}; if (options.expiration) { meta.expiration = options.expiration; } else if (options.expirationTtl) { meta.expiration = Math.floor(Date.now() / 1e3) + options.expirationTtl; } if (options.metadata) { meta.metadata = options.metadata; } this.metadata.set(prefixedKey, meta); } } async get(key, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); const meta = this.metadata.get(prefixedKey); if (meta?.expiration && meta.expiration <= Math.floor(Date.now() / 1e3)) { this.store.delete(prefixedKey); this.metadata.delete(prefixedKey); return null; } const value = this.store.get(prefixedKey); return value === void 0 ? null : value; } async getAll(options) { const result = []; const prefix = this.buildPrefix(options?.tableName, options?.prefix); for (const [key, value] of this.store.entries()) { if (!prefix || key.startsWith(prefix)) { const meta = this.metadata.get(key); if (meta?.expiration && meta.expiration <= Math.floor(Date.now() / 1e3)) { this.store.delete(key); this.metadata.delete(key); continue; } const originalKey = this.removePrefix(key, options?.tableName); result.push({ key: originalKey, value }); } } if (options?.limit) { return result.slice(0, options.limit); } return result; } async delete(key, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); const existed = this.store.has(prefixedKey); this.store.delete(prefixedKey); this.metadata.delete(prefixedKey); return existed; } async listKeys(options) { const keys = []; const prefix = this.buildPrefix(options?.tableName, options?.prefix); for (const [key] of this.store.entries()) { if (!prefix || key.startsWith(prefix)) { const meta = this.metadata.get(key); keys.push({ name: key, expiration: meta?.expiration, metadata: meta?.metadata }); } } const limit = options?.limit; if (limit) { return { keys: keys.slice(0, limit), list_complete: keys.length <= limit }; } return { keys, list_complete: true }; } async batchGet(keys, options) { return Promise.all( keys.map(async (key) => ({ key, value: await this.get(key, options) })) ); } async batchPut(items, options) { await Promise.all( items.map(async (item) => { await this.put(item.key, item.value, options); }) ); } async batchDelete(keys, options) { return Promise.all( keys.map(async (key) => await this.delete(key, options)) ); } async query(options) { const allItems = await this.getAll(options); if (!options.filter) { return allItems; } if (typeof options.filter === "function") { return allItems.filter( (item) => ( // biome-ignore lint/complexity/noBannedTypes: Function type needed for filter callback compatibility options.filter(item.value) ) ); } return allItems.filter((item) => { const filter = options.filter; for (const [field, condition] of Object.entries(filter)) { const value = item.value?.[field]; if (typeof condition === "object" && condition !== null) { if ("$eq" in condition && value !== condition.$eq) return false; if ("$ne" in condition && value === condition.$ne) return false; if ("$gt" in condition && value <= condition.$gt) return false; if ("$gte" in condition && value < condition.$gte) return false; if ("$lt" in condition && value >= condition.$lt) return false; if ("$lte" in condition && value > condition.$lte) return false; if ("$in" in condition && !condition.$in?.includes(value)) return false; if ("$nin" in condition && condition.$nin?.includes(value)) return false; if ("$exists" in condition) { const exists = value !== void 0 && value !== null; if (condition.$exists !== exists) return false; } if ("$beginsWith" in condition && typeof value === "string") { if (!value.startsWith(condition.$beginsWith)) return false; } } else { if (value !== condition) return false; } } return true; }); } getCapabilities() { return { storeType: "in-memory", features: { transactions: false, advancedQueries: true, aggregation: false, conditionalWrites: false, ttl: true, streaming: false }, limits: { maxBatchSize: 1e3, maxItemSize: 64 * 1024 * 1024 // 64MB }, queryOperators: [ "$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$nin", "$exists", "$beginsWith" ], indexTypes: [] }; } getNativeClient() { return this.store; } /** * Clear all data (useful for testing) */ clear() { this.store.clear(); this.metadata.clear(); } getPrefixedKey(key, tableName) { return tableName ? `${tableName}:${key}` : key; } removePrefix(key, tableName) { if (tableName && key.startsWith(`${tableName}:`)) { return key.substring(tableName.length + 1); } return key; } buildPrefix(tableName, prefix) { if (tableName && prefix) { return `${tableName}:${prefix}`; } if (tableName) { return `${tableName}:`; } return prefix; } }; __name(_InMemoryReadModelStore, "InMemoryReadModelStore"); var InMemoryReadModelStore = _InMemoryReadModelStore; var _KVReadModelStore = class _KVReadModelStore { /** * Creates a new KV read model store. * * @param namespace - The Cloudflare KV namespace to use for storage * @param options - Optional configuration for the store */ constructor(namespace, options = {}) { this.namespace = namespace; this.options = options; } /** * Stores a value with the given key. * * @param key - The key under which to store the value * @param value - The value to store (will be serialized to JSON if it's not a string or ArrayBuffer) * @param options - Optional settings for the storage operation * @returns A promise that resolves when the value has been stored */ async put(key, value, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); let serializedValue; if (typeof value === "string") { serializedValue = value; } else if (value instanceof ArrayBuffer) { serializedValue = value; } else if (value instanceof ReadableStream) { serializedValue = value; } else { serializedValue = JSON.stringify(value); } const kvOptions = {}; if (options?.expiration) { kvOptions.expiration = options.expiration; } if (options?.expirationTtl) { kvOptions.expirationTtl = options.expirationTtl; } if (options?.metadata) { kvOptions.metadata = options.metadata; } try { await this.namespace.put(prefixedKey, serializedValue, kvOptions); } catch (error) { throw new Error( `Failed to store key "${key}": ${error instanceof Error ? error.message : String(error)}` ); } } /** * Retrieves a value by its key. * * @param key - The key of the value to retrieve * @param options - Optional settings for the retrieval operation * @returns A promise that resolves with the value, or null if not found */ async get(key, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); const type = options?.type || "json"; try { let result; if (type === "json") { result = await this.namespace.get(prefixedKey, { type: "json", cacheTtl: options?.cacheTtl }); } else if (type === "text") { result = await this.namespace.get(prefixedKey, { type: "text", cacheTtl: options?.cacheTtl }); } else if (type === "arrayBuffer") { result = await this.namespace.get(prefixedKey, { type: "arrayBuffer", cacheTtl: options?.cacheTtl }); } else if (type === "stream") { result = await this.namespace.get(prefixedKey, { type: "stream", cacheTtl: options?.cacheTtl }); } else { result = await this.namespace.get(prefixedKey, { cacheTtl: options?.cacheTtl }); } return result; } catch (error) { throw new Error( `Failed to retrieve key "${key}": ${error instanceof Error ? error.message : String(error)}` ); } } /** * Retrieves all values, optionally filtered by a key prefix. * * @param options - Optional settings for retrieving all values * @returns A promise that resolves with an array of key-value pairs */ async getAll(options) { const result = []; let cursor; const limit = options?.limit || 1e3; let remainingLimit = limit; const prefix = this.buildListPrefix(options?.tableName, options?.prefix); try { do { const listResult = await this.namespace.list({ prefix, limit: Math.min(remainingLimit, 1e3), cursor }); if (listResult.keys.length === 0) { break; } const keyNames = listResult.keys.map((k) => k.name); const batchResult = await this.batchGetInternal(keyNames, options); for (const item of batchResult) { if (item.value !== null) { const originalKey = this.removePrefix(item.key, options?.tableName); result.push({ key: originalKey, value: item.value }); } } cursor = listResult.cursor; remainingLimit -= keyNames.length; } while (cursor && remainingLimit > 0); return result; } catch (error) { throw new Error( `Failed to retrieve all keys: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Deletes a value by its key. * * @param key - The key of the value to delete * @param options - Optional settings for the delete operation * @returns A promise that resolves to true if the key existed and was deleted, false otherwise */ async delete(key, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); try { const exists = await this.namespace.get(prefixedKey) !== null; await this.namespace.delete(prefixedKey); return exists; } catch (error) { throw new Error( `Failed to delete key "${key}": ${error instanceof Error ? error.message : String(error)}` ); } } /** * Lists keys in the store, optionally filtered by prefix. * * @param options - Optional settings for the list operation * @returns A promise that resolves with the list result containing keys and metadata */ async listKeys(options) { const prefix = this.buildListPrefix(options?.tableName, options?.prefix); try { const result = await this.namespace.list({ prefix, limit: options?.limit || 1e3, cursor: options?.cursor }); return { keys: result.keys.map( (key) => ({ name: key.name, expiration: key.expiration, metadata: key.metadata }) ), list_complete: result.list_complete, cursor: result.cursor }; } catch (error) { throw new Error( `Failed to list keys: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Native batch retrieval of multiple items by their keys using KV's new batch API * This is much more efficient than individual get operations * * @param keys - Array of keys to retrieve (max 100 keys) * @param options - Optional settings for the batch get operation * @returns A promise that resolves with an array of key-value pairs */ async batchGet(keys, options) { if (keys.length === 0) { return []; } const chunks = []; for (let i = 0; i < keys.length; i += 100) { chunks.push(keys.slice(i, i + 100)); } const results = []; try { for (const chunk of chunks) { const chunkResults = await this.batchGetInternal(chunk, options); results.push(...chunkResults); } return results; } catch (error) { throw new Error( `Failed to batch get keys: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Internal method for batch get operations */ async batchGetInternal(keys, options) { const prefixedKeys = keys.map( (key) => this.getPrefixedKey(key, options?.tableName) ); const type = options?.type || "json"; let batchResult; if (type === "json") { batchResult = await this.namespace.get(prefixedKeys, { type: "json", cacheTtl: options?.cacheTtl }); } else if (type === "text") { batchResult = await this.namespace.get(prefixedKeys, { type: "text", cacheTtl: options?.cacheTtl }); } else { return Promise.all( keys.map(async (key) => { const value = await this.get(key, options); return { key, value }; }) ); } return keys.map((originalKey) => { const prefixedKey = this.getPrefixedKey(originalKey, options?.tableName); const value = batchResult.get(prefixedKey) || null; return { key: originalKey, value }; }); } /** * Batch storage of multiple items */ async batchPut(items, options) { const concurrency = 25; const chunks = []; for (let i = 0; i < items.length; i += concurrency) { chunks.push(items.slice(i, i + concurrency)); } try { for (const chunk of chunks) { await Promise.all( chunk.map((item) => this.put(item.key, item.value, options)) ); } } catch (error) { throw new Error( `Failed to batch put items: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Batch deletion of multiple items */ async batchDelete(keys, options) { if (keys.length === 0) { return []; } const concurrency = 25; const chunks = []; for (let i = 0; i < keys.length; i += concurrency) { chunks.push(keys.slice(i, i + concurrency)); } const results = []; try { for (const chunk of chunks) { const chunkResults = await Promise.all( chunk.map((key) => this.delete(key, options)) ); results.push(...chunkResults); } return results; } catch (error) { throw new Error( `Failed to batch delete keys: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Enhanced query with better prefix support */ async query(options) { if (options.prefix) { return this.getAll(options); } const allItems = await this.getAll(options); if (!options.filter) { return allItems; } if (typeof options.filter === "function") { return allItems.filter( (item) => ( // biome-ignore lint/complexity/noBannedTypes: Function type needed for filter callback compatibility options.filter(item.value) ) ); } const filter = options.filter; return allItems.filter((item) => { for (const [field, condition] of Object.entries(filter)) { const value = item.value?.[field]; if (typeof condition === "object" && condition !== null) { if ("$eq" in condition && value !== condition.$eq) return false; if ("$ne" in condition && value === condition.$ne) return false; if ("$beginsWith" in condition && typeof value === "string") { if (!value.startsWith(condition.$beginsWith)) return false; } } else { if (value !== condition) return false; } } return true; }); } /** * Returns enhanced capabilities for KV store */ getCapabilities() { return { storeType: "cloudflare-kv", features: { transactions: false, advancedQueries: false, // Limited to prefix-based queries aggregation: false, conditionalWrites: false, ttl: true, streaming: true }, limits: { maxItemSize: 25 * 1024 * 1024, // 25MB maxBatchSize: 100, // Native batch get limit maxTransactionSize: 0 }, queryOperators: ["$eq", "$ne", "$beginsWith"], indexTypes: ["prefix"] }; } /** * Gets access to the native KV namespace */ // biome-ignore lint/suspicious/noExplicitAny: placing any here to remove dependency on KVNamespace type getNativeClient() { return this.namespace; } /** * Helper method to add metadata support */ async getWithMetadata(key, options) { const prefixedKey = this.getPrefixedKey(key, options?.tableName); const type = options?.type || "json"; try { const result = await this.namespace.getWithMetadata(prefixedKey, { type, cacheTtl: options?.cacheTtl }); return { value: result.value, metadata: result.metadata }; } catch (error) { throw new Error( `Failed to retrieve key with metadata "${key}": ${error instanceof Error ? error.message : String(error)}` ); } } /** * Enhanced batch get with metadata support */ async batchGetWithMetadata(keys, options) { if (keys.length === 0) { return []; } const chunks = []; for (let i = 0; i < keys.length; i += 100) { chunks.push(keys.slice(i, i + 100)); } const results = []; try { for (const chunk of chunks) { const prefixedKeys = chunk.map( (key) => this.getPrefixedKey(key, options?.tableName) ); const type = options?.type || "json"; let batchResult; if (type === "json" || type === "text") { batchResult = await this.namespace.getWithMetadata(prefixedKeys, { type, cacheTtl: options?.cacheTtl }); } else { const chunkResults = await Promise.all( chunk.map(async (key) => { const result = await this.getWithMetadata(key, options); return { key, value: result.value, metadata: result.metadata }; }) ); results.push(...chunkResults); continue; } for (const originalKey of chunk) { const prefixedKey = this.getPrefixedKey( originalKey, options?.tableName ); const result = batchResult.get(prefixedKey); results.push({ key: originalKey, value: result?.value || null, metadata: result?.metadata || null }); } } return results; } catch (error) { throw new Error( `Failed to batch get with metadata: ${error instanceof Error ? error.message : String(error)}` ); } } // Helper methods getPrefixedKey(key, tableName) { if (tableName) { return `${tableName}:${key}`; } if (this.options.defaultPrefix) { return `${this.options.defaultPrefix}${key}`; } return key; } removePrefix(key, tableName) { if (tableName && key.startsWith(`${tableName}:`)) { return key.substring(tableName.length + 1); } if (this.options.defaultPrefix && key.startsWith(this.options.defaultPrefix)) { return key.substring(this.options.defaultPrefix.length); } return key; } buildListPrefix(tableName, prefix) { if (tableName && prefix) { return `${tableName}:${prefix}`; } if (tableName) { return `${tableName}:`; } if (this.options.defaultPrefix) { if (prefix) { return `${this.options.defaultPrefix}${prefix}`; } return this.options.defaultPrefix; } return prefix; } }; __name(_KVReadModelStore, "KVReadModelStore"); var KVReadModelStore = _KVReadModelStore; var _HttpReadModelStore = class _HttpReadModelStore { constructor(baseUrl, headers = {}) { this.baseUrl = baseUrl; this.headers = headers; } async get(key, options) { try { const tableName = options?.tableName; const url = tableName ? `${this.baseUrl}/${tableName}/${key}` : `${this.baseUrl}/${key}`; const response = await fetch(url, { method: "GET", headers: this.headers }); if (response.status === 404) return null; if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); return await response.json(); } catch (error) { if (error instanceof Error && error.message.includes("404")) { return null; } throw error; } } async put(key, value, options) { const tableName = options?.tableName; const url = tableName ? `${this.baseUrl}/${tableName}/${key}` :