@frontmcp/utils 1.8.5 → 1.8.7

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.
@@ -16,7 +16,7 @@
16
16
  */
17
17
  /**
18
18
  * Snapshot of the current runtime environment. Detected once and cached
19
- * for the lifetime of the process.
19
+ * for the lifetime of the process (`env` is read live from `NODE_ENV`).
20
20
  */
21
21
  export interface RuntimeContext {
22
22
  /** OS platform: 'darwin', 'linux', 'win32', etc. (issue #417: renamed from platform) */
@@ -134,7 +134,11 @@ export declare function checkEntryAvailability(availability: EntryAvailability |
134
134
  export declare function detectRuntimeContext(): RuntimeContext;
135
135
  /**
136
136
  * Get the current runtime context (lazy singleton).
137
- * The context is detected on first call and cached for the process lifetime.
137
+ * The context is detected on first call and cached for the process lifetime,
138
+ * except `env`, which always reflects the live `NODE_ENV`. A Cloudflare Worker's
139
+ * `[vars]` reach `process.env` on the first request, after module evaluation has
140
+ * already read this context, so a frozen `env` would report `development` in a
141
+ * production deployment.
138
142
  */
139
143
  export declare function getRuntimeContext(): RuntimeContext;
140
144
  /**
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/async-context/overlap-error.ts
4
5
  var AsyncContextOverlapError = class extends Error {
5
6
  constructor(message) {
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/async-context/node-async-context.ts
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
5
6
 
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/crypto/browser.ts
4
5
  import { sha256 as sha256Hash } from "@noble/hashes/sha2.js";
5
6
  import { hmac } from "@noble/hashes/hmac.js";
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/crypto/node.ts
4
5
  import crypto from "node:crypto";
5
6
 
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/env/edge-scope.ts
4
5
  function hasEdgeGlobalScope() {
5
6
  if (typeof globalThis === "undefined") return false;
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/env/edge-scope.ts
4
5
  function hasEdgeGlobalScope() {
5
6
  if (typeof globalThis === "undefined") return false;
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/event-emitter/browser-event-emitter.ts
4
5
  var EventEmitter = class {
5
6
  events = /* @__PURE__ */ new Map();
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  // libs/utils/src/event-emitter/node-event-emitter.ts
4
5
  import { EventEmitter } from "events";
5
6
  export {
package/esm/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { createRequire as __frontmcpCreateRequire } from 'module';
2
- const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
1
+ const __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -2962,6 +2963,41 @@ var init_crypto = __esm({
2962
2963
  }
2963
2964
  });
2964
2965
 
2966
+ // libs/utils/src/storage/redis-error-listener.ts
2967
+ function attachRedisErrorListener(client, options = {}) {
2968
+ const label = options.label ?? "redis";
2969
+ const intervalMs = options.intervalMs ?? DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS;
2970
+ let lastLoggedAt = 0;
2971
+ let suppressed = 0;
2972
+ const listener = (error) => {
2973
+ const now = Date.now();
2974
+ if (lastLoggedAt !== 0 && now - lastLoggedAt < intervalMs) {
2975
+ suppressed++;
2976
+ return;
2977
+ }
2978
+ const extra = suppressed > 0 ? ` (${suppressed} similar error(s) suppressed)` : "";
2979
+ suppressed = 0;
2980
+ lastLoggedAt = now;
2981
+ const message = `[${label}] Redis connection error: ${error?.message ?? String(error)}${extra}`;
2982
+ if (options.logger) {
2983
+ options.logger.warn(message);
2984
+ } else {
2985
+ console.warn(message);
2986
+ }
2987
+ };
2988
+ client.on("error", listener);
2989
+ return () => {
2990
+ client.removeListener("error", listener);
2991
+ };
2992
+ }
2993
+ var DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS;
2994
+ var init_redis_error_listener = __esm({
2995
+ "libs/utils/src/storage/redis-error-listener.ts"() {
2996
+ "use strict";
2997
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS = 3e4;
2998
+ }
2999
+ });
3000
+
2965
3001
  // libs/utils/src/storage/utils/index.ts
2966
3002
  var init_utils = __esm({
2967
3003
  "libs/utils/src/storage/utils/index.ts"() {
@@ -2973,7 +3009,8 @@ var init_utils = __esm({
2973
3009
  // libs/utils/src/storage/adapters/redis.ts
2974
3010
  var redis_exports = {};
2975
3011
  __export(redis_exports, {
2976
- RedisStorageAdapter: () => RedisStorageAdapter
3012
+ RedisStorageAdapter: () => RedisStorageAdapter,
3013
+ createRedisClient: () => createRedisClient
2977
3014
  });
2978
3015
  function getRedisClass() {
2979
3016
  try {
@@ -2982,25 +3019,45 @@ function getRedisClass() {
2982
3019
  const msg = error instanceof Error ? error.message : String(error);
2983
3020
  if (msg.includes("Dynamic require") || msg.includes("require is not defined")) {
2984
3021
  throw new Error(
2985
- `Failed to load ioredis: ${msg}. This typically happens with ESM bundlers (esbuild, Vite). Ensure your bundler externalizes ioredis or use CJS mode.`
3022
+ `Failed to load ioredis: ${msg}. This typically happens with ESM bundlers (esbuild, Vite). Ensure your bundler externalizes ioredis or use CJS mode.`,
3023
+ { cause: error }
2986
3024
  );
2987
3025
  }
2988
- throw new Error("ioredis is required for Redis storage adapter. Install it with: npm install ioredis");
3026
+ throw new Error("ioredis is required for Redis storage adapter. Install it with: npm install ioredis", {
3027
+ cause: error
3028
+ });
2989
3029
  }
2990
3030
  }
3031
+ function createRedisClient(options) {
3032
+ const RedisClass = getRedisClass();
3033
+ const baseOptions = { lazyConnect: false, maxRetriesPerRequest: 3 };
3034
+ const client = options.url ? new RedisClass(options.url, baseOptions) : new RedisClass({
3035
+ ...baseOptions,
3036
+ host: options.host ?? "localhost",
3037
+ port: options.port ?? 6379,
3038
+ password: options.password,
3039
+ db: options.db ?? 0,
3040
+ tls: options.tls ? {} : void 0
3041
+ });
3042
+ attachRedisErrorListener(client, { label: options.label ?? "redis", logger: options.logger });
3043
+ return client;
3044
+ }
2991
3045
  var RedisStorageAdapter;
2992
3046
  var init_redis = __esm({
2993
3047
  "libs/utils/src/storage/adapters/redis.ts"() {
2994
3048
  "use strict";
2995
- init_base();
2996
3049
  init_errors();
3050
+ init_redis_error_listener();
2997
3051
  init_utils();
3052
+ init_base();
2998
3053
  RedisStorageAdapter = class extends BaseStorageAdapter {
2999
3054
  backendName = "redis";
3000
3055
  client;
3001
3056
  subscriber;
3002
3057
  options;
3003
3058
  ownsClient;
3059
+ detachErrorListener;
3060
+ connecting;
3004
3061
  keyPrefix;
3005
3062
  subscriptionHandlers = /* @__PURE__ */ new Map();
3006
3063
  constructor(options = {}) {
@@ -3030,21 +3087,45 @@ var init_redis = __esm({
3030
3087
  // ============================================
3031
3088
  async connect() {
3032
3089
  if (this.connected) return;
3090
+ this.connecting ??= this.openConnection().finally(() => {
3091
+ this.connecting = void 0;
3092
+ });
3093
+ return this.connecting;
3094
+ }
3095
+ async openConnection() {
3096
+ let socketError;
3097
+ const recordSocketError = (error) => {
3098
+ socketError = error;
3099
+ };
3100
+ let client;
3033
3101
  try {
3034
3102
  if (this.options.client) {
3035
- this.client = this.options.client;
3103
+ client = this.options.client;
3036
3104
  } else {
3037
3105
  const RedisClass = getRedisClass();
3038
3106
  if (this.options.url) {
3039
- this.client = new RedisClass(this.options.url, this.buildRedisOptions());
3107
+ client = new RedisClass(this.options.url, this.buildRedisOptions());
3040
3108
  } else {
3041
- this.client = new RedisClass(this.buildRedisOptions());
3109
+ client = new RedisClass(this.buildRedisOptions());
3042
3110
  }
3111
+ client.on("error", recordSocketError);
3043
3112
  }
3044
- await this.client.ping();
3113
+ await client.ping();
3114
+ this.client = client;
3045
3115
  this.connected = true;
3116
+ if (this.ownsClient) {
3117
+ client.removeListener("error", recordSocketError);
3118
+ this.detachErrorListener = attachRedisErrorListener(client, { label: "RedisStorage" });
3119
+ }
3046
3120
  } catch (e) {
3047
- throw new StorageConnectionError("Failed to connect to Redis", e instanceof Error ? e : void 0, "redis");
3121
+ if (this.ownsClient && client) {
3122
+ try {
3123
+ client.disconnect();
3124
+ } catch {
3125
+ }
3126
+ }
3127
+ const cause = socketError ?? (e instanceof Error ? e : void 0);
3128
+ throw new StorageConnectionError("Failed to connect to Redis", cause, "redis");
3048
3129
  }
3049
3130
  }
3050
3131
  async disconnect() {
@@ -3056,6 +3137,8 @@ var init_redis = __esm({
3056
3137
  if (this.ownsClient && this.client) {
3057
3138
  await this.client.quit();
3058
3139
  }
3140
+ this.detachErrorListener?.();
3141
+ this.detachErrorListener = void 0;
3059
3142
  this.client = void 0;
3060
3143
  this.connected = false;
3061
3144
  this.subscriptionHandlers.clear();
@@ -3283,6 +3366,7 @@ var init_redis = __esm({
3283
3366
  } else {
3284
3367
  throw new StorageConfigError("redis", "Cannot create subscriber without url, config, or client");
3285
3368
  }
3369
+ attachRedisErrorListener(subscriber, { label: "RedisStorage:subscriber" });
3286
3370
  subscriber.on("message", (channel, message) => {
3287
3371
  const handlers = this.subscriptionHandlers.get(channel);
3288
3372
  if (handlers) {
@@ -4952,7 +5036,13 @@ function detectRuntimeContext() {
4952
5036
  var cached3;
4953
5037
  function getRuntimeContext() {
4954
5038
  if (!cached3) {
4955
- cached3 = detectRuntimeContext();
5039
+ const detected = detectRuntimeContext();
5040
+ Object.defineProperty(detected, "env", {
5041
+ get: () => process.env["NODE_ENV"] || "development",
5042
+ enumerable: true,
5043
+ configurable: true
5044
+ });
5045
+ cached3 = detected;
4956
5046
  }
4957
5047
  return cached3;
4958
5048
  }
@@ -5223,6 +5313,7 @@ function getDetectedStorageType() {
5223
5313
  }
5224
5314
 
5225
5315
  // libs/utils/src/storage/index.ts
5316
+ init_redis_error_listener();
5226
5317
  init_errors();
5227
5318
 
5228
5319
  // libs/utils/src/storage/typed-storage.ts
@@ -5877,6 +5968,7 @@ export {
5877
5968
  BaseStorageAdapter,
5878
5969
  DEFAULT_CODE_VERIFIER_LENGTH,
5879
5970
  DEFAULT_MAX_INPUT_LENGTH,
5971
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
5880
5972
  EncryptedBlobError,
5881
5973
  EncryptedStorageError,
5882
5974
  EncryptedTypedStorage,
@@ -5909,6 +6001,7 @@ export {
5909
6001
  assertNode,
5910
6002
  asymmetricAlgSchema,
5911
6003
  asymmetricKeyDataSchema,
6004
+ attachRedisErrorListener,
5912
6005
  awaitOutsideRequest,
5913
6006
  base64Decode,
5914
6007
  base64Encode,
@@ -5929,6 +6022,7 @@ export {
5929
6022
  createKeyPersistenceWithStorage,
5930
6023
  createMemoryStorage,
5931
6024
  createNamespacedStorage,
6025
+ createRedisClient,
5932
6026
  createRootStorage,
5933
6027
  createSafeRegExp,
5934
6028
  createSecretData,
package/index.d.ts CHANGED
@@ -26,8 +26,8 @@ export { EventEmitter } from './event-emitter';
26
26
  export { getEnv, getCwd, isProduction, isDevelopment, getEnvFlag, isDebug, setEnv, isEdgeRuntime, isServerless, supportsAnsi, getRuntimeContext, resetRuntimeContext, detectRuntimeContext, isEntryAvailable, checkEntryAvailability, entryAvailabilitySchema, } from './env';
27
27
  export type { RuntimeContext, EntryAvailability } from './env';
28
28
  export { analyzePattern, isPatternSafe, createSafeRegExp, safeTest, safeMatch, safeReplace, safeExec, isInputLengthSafe, DEFAULT_MAX_INPUT_LENGTH, REDOS_THRESHOLDS, trimLeading, trimTrailing, trimBoth, trimChars, extractBracedParams, expandTemplate, hasTemplatePlaceholders, collapseChar, collapseWhitespace, type SafeRegexOptions, type PatternAnalysisResult, } from './regex';
29
- export { createStorage, createMemoryStorage, getDetectedStorageType, NamespacedStorageImpl, createRootStorage, createNamespacedStorage, buildPrefix, NAMESPACE_SEPARATOR, StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError, BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, VercelKvStorageAdapter, UpstashStorageAdapter, FileSystemStorageAdapter, type FileSystemAdapterOptions, TypedStorage, type TypedStorageOptions, type TypedSetOptions, type TypedSetEntry, EncryptedTypedStorage, type EncryptedTypedStorageOptions, type EncryptedSetOptions, type EncryptedSetEntry, type EncryptionKey, type StoredEncryptedBlob, type ClientKeyBinding, globToRegex, matchesPattern, validatePattern, escapeGlob, MAX_TTL_SECONDS, validateTTL, validateOptionalTTL, ttlToExpiresAt, expiresAtToTTL, isExpired, normalizeTTL, } from './storage';
30
- export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, } from './storage';
29
+ export { createStorage, createMemoryStorage, getDetectedStorageType, NamespacedStorageImpl, createRootStorage, createNamespacedStorage, buildPrefix, NAMESPACE_SEPARATOR, StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError, BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, createRedisClient, VercelKvStorageAdapter, UpstashStorageAdapter, FileSystemStorageAdapter, type FileSystemAdapterOptions, TypedStorage, type TypedStorageOptions, type TypedSetOptions, type TypedSetEntry, EncryptedTypedStorage, type EncryptedTypedStorageOptions, type EncryptedSetOptions, type EncryptedSetEntry, type EncryptionKey, type StoredEncryptedBlob, type ClientKeyBinding, globToRegex, matchesPattern, validatePattern, escapeGlob, MAX_TTL_SECONDS, validateTTL, validateOptionalTTL, ttlToExpiresAt, expiresAtToTTL, isExpired, normalizeTTL, attachRedisErrorListener, DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS, } from './storage';
30
+ export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, RedisErrorListenerOptions, CreateRedisClientOptions, ErrorEmitterClient, } from './storage';
31
31
  export { processPlatformToolCalls } from './llm';
32
32
  export type { CallToolFn, OpenAIToolCallItem, OpenAIToolResponse, ClaudeToolUseBlock, ClaudeToolResultBlock, VercelToolCallInfo, PlatformToolCallsInput, PlatformToolCallsOutput, SupportedPlatform, } from './llm';
33
33
  export { getMachineId, setMachineIdOverride } from './machine-id';
package/index.js CHANGED
@@ -2965,6 +2965,41 @@ var init_crypto = __esm({
2965
2965
  }
2966
2966
  });
2967
2967
 
2968
+ // libs/utils/src/storage/redis-error-listener.ts
2969
+ function attachRedisErrorListener(client, options = {}) {
2970
+ const label = options.label ?? "redis";
2971
+ const intervalMs = options.intervalMs ?? DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS;
2972
+ let lastLoggedAt = 0;
2973
+ let suppressed = 0;
2974
+ const listener = (error) => {
2975
+ const now = Date.now();
2976
+ if (lastLoggedAt !== 0 && now - lastLoggedAt < intervalMs) {
2977
+ suppressed++;
2978
+ return;
2979
+ }
2980
+ const extra = suppressed > 0 ? ` (${suppressed} similar error(s) suppressed)` : "";
2981
+ suppressed = 0;
2982
+ lastLoggedAt = now;
2983
+ const message = `[${label}] Redis connection error: ${error?.message ?? String(error)}${extra}`;
2984
+ if (options.logger) {
2985
+ options.logger.warn(message);
2986
+ } else {
2987
+ console.warn(message);
2988
+ }
2989
+ };
2990
+ client.on("error", listener);
2991
+ return () => {
2992
+ client.removeListener("error", listener);
2993
+ };
2994
+ }
2995
+ var DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS;
2996
+ var init_redis_error_listener = __esm({
2997
+ "libs/utils/src/storage/redis-error-listener.ts"() {
2998
+ "use strict";
2999
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS = 3e4;
3000
+ }
3001
+ });
3002
+
2968
3003
  // libs/utils/src/storage/utils/index.ts
2969
3004
  var init_utils = __esm({
2970
3005
  "libs/utils/src/storage/utils/index.ts"() {
@@ -2976,7 +3011,8 @@ var init_utils = __esm({
2976
3011
  // libs/utils/src/storage/adapters/redis.ts
2977
3012
  var redis_exports = {};
2978
3013
  __export(redis_exports, {
2979
- RedisStorageAdapter: () => RedisStorageAdapter
3014
+ RedisStorageAdapter: () => RedisStorageAdapter,
3015
+ createRedisClient: () => createRedisClient
2980
3016
  });
2981
3017
  function getRedisClass() {
2982
3018
  try {
@@ -2985,25 +3021,45 @@ function getRedisClass() {
2985
3021
  const msg = error instanceof Error ? error.message : String(error);
2986
3022
  if (msg.includes("Dynamic require") || msg.includes("require is not defined")) {
2987
3023
  throw new Error(
2988
- `Failed to load ioredis: ${msg}. This typically happens with ESM bundlers (esbuild, Vite). Ensure your bundler externalizes ioredis or use CJS mode.`
3024
+ `Failed to load ioredis: ${msg}. This typically happens with ESM bundlers (esbuild, Vite). Ensure your bundler externalizes ioredis or use CJS mode.`,
3025
+ { cause: error }
2989
3026
  );
2990
3027
  }
2991
- throw new Error("ioredis is required for Redis storage adapter. Install it with: npm install ioredis");
3028
+ throw new Error("ioredis is required for Redis storage adapter. Install it with: npm install ioredis", {
3029
+ cause: error
3030
+ });
2992
3031
  }
2993
3032
  }
3033
+ function createRedisClient(options) {
3034
+ const RedisClass = getRedisClass();
3035
+ const baseOptions = { lazyConnect: false, maxRetriesPerRequest: 3 };
3036
+ const client = options.url ? new RedisClass(options.url, baseOptions) : new RedisClass({
3037
+ ...baseOptions,
3038
+ host: options.host ?? "localhost",
3039
+ port: options.port ?? 6379,
3040
+ password: options.password,
3041
+ db: options.db ?? 0,
3042
+ tls: options.tls ? {} : void 0
3043
+ });
3044
+ attachRedisErrorListener(client, { label: options.label ?? "redis", logger: options.logger });
3045
+ return client;
3046
+ }
2994
3047
  var RedisStorageAdapter;
2995
3048
  var init_redis = __esm({
2996
3049
  "libs/utils/src/storage/adapters/redis.ts"() {
2997
3050
  "use strict";
2998
- init_base();
2999
3051
  init_errors();
3052
+ init_redis_error_listener();
3000
3053
  init_utils();
3054
+ init_base();
3001
3055
  RedisStorageAdapter = class extends BaseStorageAdapter {
3002
3056
  backendName = "redis";
3003
3057
  client;
3004
3058
  subscriber;
3005
3059
  options;
3006
3060
  ownsClient;
3061
+ detachErrorListener;
3062
+ connecting;
3007
3063
  keyPrefix;
3008
3064
  subscriptionHandlers = /* @__PURE__ */ new Map();
3009
3065
  constructor(options = {}) {
@@ -3033,21 +3089,45 @@ var init_redis = __esm({
3033
3089
  // ============================================
3034
3090
  async connect() {
3035
3091
  if (this.connected) return;
3092
+ this.connecting ??= this.openConnection().finally(() => {
3093
+ this.connecting = void 0;
3094
+ });
3095
+ return this.connecting;
3096
+ }
3097
+ async openConnection() {
3098
+ let socketError;
3099
+ const recordSocketError = (error) => {
3100
+ socketError = error;
3101
+ };
3102
+ let client;
3036
3103
  try {
3037
3104
  if (this.options.client) {
3038
- this.client = this.options.client;
3105
+ client = this.options.client;
3039
3106
  } else {
3040
3107
  const RedisClass = getRedisClass();
3041
3108
  if (this.options.url) {
3042
- this.client = new RedisClass(this.options.url, this.buildRedisOptions());
3109
+ client = new RedisClass(this.options.url, this.buildRedisOptions());
3043
3110
  } else {
3044
- this.client = new RedisClass(this.buildRedisOptions());
3111
+ client = new RedisClass(this.buildRedisOptions());
3045
3112
  }
3113
+ client.on("error", recordSocketError);
3046
3114
  }
3047
- await this.client.ping();
3115
+ await client.ping();
3116
+ this.client = client;
3048
3117
  this.connected = true;
3118
+ if (this.ownsClient) {
3119
+ client.removeListener("error", recordSocketError);
3120
+ this.detachErrorListener = attachRedisErrorListener(client, { label: "RedisStorage" });
3121
+ }
3049
3122
  } catch (e) {
3050
- throw new StorageConnectionError("Failed to connect to Redis", e instanceof Error ? e : void 0, "redis");
3123
+ if (this.ownsClient && client) {
3124
+ try {
3125
+ client.disconnect();
3126
+ } catch {
3127
+ }
3128
+ }
3129
+ const cause = socketError ?? (e instanceof Error ? e : void 0);
3130
+ throw new StorageConnectionError("Failed to connect to Redis", cause, "redis");
3051
3131
  }
3052
3132
  }
3053
3133
  async disconnect() {
@@ -3059,6 +3139,8 @@ var init_redis = __esm({
3059
3139
  if (this.ownsClient && this.client) {
3060
3140
  await this.client.quit();
3061
3141
  }
3142
+ this.detachErrorListener?.();
3143
+ this.detachErrorListener = void 0;
3062
3144
  this.client = void 0;
3063
3145
  this.connected = false;
3064
3146
  this.subscriptionHandlers.clear();
@@ -3286,6 +3368,7 @@ var init_redis = __esm({
3286
3368
  } else {
3287
3369
  throw new StorageConfigError("redis", "Cannot create subscriber without url, config, or client");
3288
3370
  }
3371
+ attachRedisErrorListener(subscriber, { label: "RedisStorage:subscriber" });
3289
3372
  subscriber.on("message", (channel, message) => {
3290
3373
  const handlers = this.subscriptionHandlers.get(channel);
3291
3374
  if (handlers) {
@@ -3982,6 +4065,7 @@ __export(index_exports, {
3982
4065
  BaseStorageAdapter: () => BaseStorageAdapter,
3983
4066
  DEFAULT_CODE_VERIFIER_LENGTH: () => DEFAULT_CODE_VERIFIER_LENGTH,
3984
4067
  DEFAULT_MAX_INPUT_LENGTH: () => DEFAULT_MAX_INPUT_LENGTH,
4068
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS: () => DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
3985
4069
  EncryptedBlobError: () => EncryptedBlobError,
3986
4070
  EncryptedStorageError: () => EncryptedStorageError,
3987
4071
  EncryptedTypedStorage: () => EncryptedTypedStorage,
@@ -4014,6 +4098,7 @@ __export(index_exports, {
4014
4098
  assertNode: () => assertNode,
4015
4099
  asymmetricAlgSchema: () => asymmetricAlgSchema,
4016
4100
  asymmetricKeyDataSchema: () => asymmetricKeyDataSchema,
4101
+ attachRedisErrorListener: () => attachRedisErrorListener,
4017
4102
  awaitOutsideRequest: () => import_async_context.awaitOutsideRequest,
4018
4103
  base64Decode: () => base64Decode,
4019
4104
  base64Encode: () => base64Encode,
@@ -4034,6 +4119,7 @@ __export(index_exports, {
4034
4119
  createKeyPersistenceWithStorage: () => createKeyPersistenceWithStorage,
4035
4120
  createMemoryStorage: () => createMemoryStorage,
4036
4121
  createNamespacedStorage: () => createNamespacedStorage,
4122
+ createRedisClient: () => createRedisClient,
4037
4123
  createRootStorage: () => createRootStorage,
4038
4124
  createSafeRegExp: () => createSafeRegExp,
4039
4125
  createSecretData: () => createSecretData,
@@ -5169,7 +5255,13 @@ function detectRuntimeContext() {
5169
5255
  var cached3;
5170
5256
  function getRuntimeContext() {
5171
5257
  if (!cached3) {
5172
- cached3 = detectRuntimeContext();
5258
+ const detected = detectRuntimeContext();
5259
+ Object.defineProperty(detected, "env", {
5260
+ get: () => process.env["NODE_ENV"] || "development",
5261
+ enumerable: true,
5262
+ configurable: true
5263
+ });
5264
+ cached3 = detected;
5173
5265
  }
5174
5266
  return cached3;
5175
5267
  }
@@ -5440,6 +5532,7 @@ function getDetectedStorageType() {
5440
5532
  }
5441
5533
 
5442
5534
  // libs/utils/src/storage/index.ts
5535
+ init_redis_error_listener();
5443
5536
  init_errors();
5444
5537
 
5445
5538
  // libs/utils/src/storage/typed-storage.ts
@@ -6095,6 +6188,7 @@ function setMachineIdOverride(id) {
6095
6188
  BaseStorageAdapter,
6096
6189
  DEFAULT_CODE_VERIFIER_LENGTH,
6097
6190
  DEFAULT_MAX_INPUT_LENGTH,
6191
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
6098
6192
  EncryptedBlobError,
6099
6193
  EncryptedStorageError,
6100
6194
  EncryptedTypedStorage,
@@ -6127,6 +6221,7 @@ function setMachineIdOverride(id) {
6127
6221
  assertNode,
6128
6222
  asymmetricAlgSchema,
6129
6223
  asymmetricKeyDataSchema,
6224
+ attachRedisErrorListener,
6130
6225
  awaitOutsideRequest,
6131
6226
  base64Decode,
6132
6227
  base64Encode,
@@ -6147,6 +6242,7 @@ function setMachineIdOverride(id) {
6147
6242
  createKeyPersistenceWithStorage,
6148
6243
  createMemoryStorage,
6149
6244
  createNamespacedStorage,
6245
+ createRedisClient,
6150
6246
  createRootStorage,
6151
6247
  createSafeRegExp,
6152
6248
  createSecretData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/utils",
3
- "version": "1.8.5",
3
+ "version": "1.8.7",
4
4
  "description": "Shared utility functions for FrontMCP - string manipulation, URI handling, path utilities, and more",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "@noble/hashes": "^2.0.1"
33
33
  },
34
34
  "peerDependencies": {
35
- "@frontmcp/lazy-zod": "1.8.5",
35
+ "@frontmcp/lazy-zod": "1.8.7",
36
36
  "@vercel/kv": "^2.0.0 || ^3.0.0",
37
37
  "ioredis": "^5.0.0"
38
38
  },
@@ -5,7 +5,8 @@
5
5
  */
6
6
  export { BaseStorageAdapter } from './base';
7
7
  export { MemoryStorageAdapter } from './memory';
8
- export { RedisStorageAdapter } from './redis';
8
+ export { RedisStorageAdapter, createRedisClient } from './redis';
9
+ export type { CreateRedisClientOptions } from './redis';
9
10
  export { VercelKvStorageAdapter } from './vercel-kv';
10
11
  export { UpstashStorageAdapter } from './upstash';
11
12
  export { CloudflareKvStorageAdapter } from './cloudflare-kv';
@@ -4,8 +4,9 @@
4
4
  * Redis-based storage implementation for production use.
5
5
  * Uses ioredis with dynamic import for browser compatibility.
6
6
  */
7
+ import { type RedisErrorListenerOptions } from '../redis-error-listener';
8
+ import type { MessageHandler, RedisAdapterOptions, SetOptions, Unsubscribe } from '../types';
7
9
  import { BaseStorageAdapter } from './base';
8
- import type { RedisAdapterOptions, SetOptions, MessageHandler, Unsubscribe } from '../types';
9
10
  type Redis = import('ioredis').Redis;
10
11
  /**
11
12
  * Redis storage adapter.
@@ -34,10 +35,13 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
34
35
  private subscriber?;
35
36
  private readonly options;
36
37
  private readonly ownsClient;
38
+ private detachErrorListener?;
39
+ private connecting?;
37
40
  private readonly keyPrefix;
38
41
  private readonly subscriptionHandlers;
39
42
  constructor(options?: RedisAdapterOptions);
40
43
  connect(): Promise<void>;
44
+ private openConnection;
41
45
  disconnect(): Promise<void>;
42
46
  ping(): Promise<boolean>;
43
47
  /**
@@ -85,4 +89,20 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
85
89
  */
86
90
  getClient(): Redis | undefined;
87
91
  }
92
+ export interface CreateRedisClientOptions {
93
+ url?: string;
94
+ host?: string;
95
+ port?: number;
96
+ password?: string;
97
+ db?: number;
98
+ tls?: boolean;
99
+ /** Prefix for the rate-limited connection-error log line. */
100
+ label?: string;
101
+ logger?: RedisErrorListenerOptions['logger'];
102
+ }
103
+ /**
104
+ * Create a plain ioredis client that reconnects on its own and never emits an unhandled
105
+ * 'error' event. The caller owns the client and must `quit()`/`disconnect()` it.
106
+ */
107
+ export declare function createRedisClient(options: CreateRedisClientOptions): Redis;
88
108
  export {};
@@ -6,13 +6,15 @@
6
6
  */
7
7
  export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, } from './types';
8
8
  export { createStorage, createMemoryStorage, getDetectedStorageType } from './factory';
9
+ export { attachRedisErrorListener, DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS } from './redis-error-listener';
10
+ export type { RedisErrorListenerOptions, ErrorEmitterClient } from './redis-error-listener';
9
11
  export { NamespacedStorageImpl, createRootStorage, createNamespacedStorage, buildPrefix, NAMESPACE_SEPARATOR, } from './namespace';
10
12
  export { StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError, } from './errors';
11
13
  export { TypedStorage } from './typed-storage';
12
14
  export type { TypedStorageOptions, TypedSetOptions, TypedSetEntry } from './typed-storage.types';
13
15
  export { EncryptedTypedStorage } from './encrypted-typed-storage';
14
16
  export type { EncryptedTypedStorageOptions, EncryptedSetOptions, EncryptedSetEntry, EncryptionKey, StoredEncryptedBlob, ClientKeyBinding, } from './encrypted-typed-storage.types';
15
- export { BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, VercelKvStorageAdapter, UpstashStorageAdapter, FileSystemStorageAdapter, LocalStorageAdapter, IndexedDBStorageAdapter, } from './adapters';
16
- export type { FileSystemAdapterOptions, LocalStorageAdapterOptions, IndexedDBAdapterOptions } from './adapters';
17
+ export { BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, createRedisClient, VercelKvStorageAdapter, UpstashStorageAdapter, FileSystemStorageAdapter, LocalStorageAdapter, IndexedDBStorageAdapter, } from './adapters';
18
+ export type { CreateRedisClientOptions, FileSystemAdapterOptions, LocalStorageAdapterOptions, IndexedDBAdapterOptions, } from './adapters';
17
19
  export { globToRegex, matchesPattern, validatePattern, escapeGlob } from './utils/pattern';
18
20
  export { MAX_TTL_SECONDS, validateTTL, validateOptionalTTL, ttlToExpiresAt, expiresAtToTTL, isExpired, normalizeTTL, } from './utils/ttl';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Permanent, rate-limited 'error' listener for ioredis clients.
3
+ *
4
+ * ioredis emits 'error' for every failed reconnect attempt. With no listener it
5
+ * prints "[ioredis] Unhandled error event" once per attempt, which floods logs
6
+ * while Redis is down. This listener swallows the event and logs at most once
7
+ * per interval, counting the errors it suppressed.
8
+ */
9
+ export interface RedisErrorListenerOptions {
10
+ /** Name used in the log line, e.g. 'HA'. */
11
+ label?: string;
12
+ /** Logger to write to. Falls back to `console.warn`. */
13
+ logger?: {
14
+ warn: (message: string, ...meta: unknown[]) => void;
15
+ };
16
+ /** Minimum time between two log lines. Defaults to 30 seconds. */
17
+ intervalMs?: number;
18
+ }
19
+ export interface ErrorEmitterClient {
20
+ on(event: 'error', listener: (error: Error) => void): unknown;
21
+ removeListener(event: 'error', listener: (error: Error) => void): unknown;
22
+ }
23
+ export declare const DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS = 30000;
24
+ /**
25
+ * Attach the listener and return a function that removes it.
26
+ */
27
+ export declare function attachRedisErrorListener(client: ErrorEmitterClient, options?: RedisErrorListenerOptions): () => void;