@delta-base/toolkit
Version:
Application-level event sourcing toolkit for delta-base
1,510 lines (1,505 loc) • 56.7 kB
JavaScript
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}` :