@frontmcp/utils 1.8.6 → 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 {
@@ -2991,11 +3028,26 @@ function getRedisClass() {
2991
3028
  });
2992
3029
  }
2993
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
+ }
2994
3045
  var RedisStorageAdapter;
2995
3046
  var init_redis = __esm({
2996
3047
  "libs/utils/src/storage/adapters/redis.ts"() {
2997
3048
  "use strict";
2998
3049
  init_errors();
3050
+ init_redis_error_listener();
2999
3051
  init_utils();
3000
3052
  init_base();
3001
3053
  RedisStorageAdapter = class extends BaseStorageAdapter {
@@ -3004,6 +3056,7 @@ var init_redis = __esm({
3004
3056
  subscriber;
3005
3057
  options;
3006
3058
  ownsClient;
3059
+ detachErrorListener;
3007
3060
  connecting;
3008
3061
  keyPrefix;
3009
3062
  subscriptionHandlers = /* @__PURE__ */ new Map();
@@ -3060,7 +3113,10 @@ var init_redis = __esm({
3060
3113
  await client.ping();
3061
3114
  this.client = client;
3062
3115
  this.connected = true;
3063
- if (this.ownsClient) client.removeListener("error", recordSocketError);
3116
+ if (this.ownsClient) {
3117
+ client.removeListener("error", recordSocketError);
3118
+ this.detachErrorListener = attachRedisErrorListener(client, { label: "RedisStorage" });
3119
+ }
3064
3120
  } catch (e) {
3065
3121
  if (this.ownsClient && client) {
3066
3122
  try {
@@ -3081,6 +3137,8 @@ var init_redis = __esm({
3081
3137
  if (this.ownsClient && this.client) {
3082
3138
  await this.client.quit();
3083
3139
  }
3140
+ this.detachErrorListener?.();
3141
+ this.detachErrorListener = void 0;
3084
3142
  this.client = void 0;
3085
3143
  this.connected = false;
3086
3144
  this.subscriptionHandlers.clear();
@@ -3308,6 +3366,7 @@ var init_redis = __esm({
3308
3366
  } else {
3309
3367
  throw new StorageConfigError("redis", "Cannot create subscriber without url, config, or client");
3310
3368
  }
3369
+ attachRedisErrorListener(subscriber, { label: "RedisStorage:subscriber" });
3311
3370
  subscriber.on("message", (channel, message) => {
3312
3371
  const handlers = this.subscriptionHandlers.get(channel);
3313
3372
  if (handlers) {
@@ -4977,7 +5036,13 @@ function detectRuntimeContext() {
4977
5036
  var cached3;
4978
5037
  function getRuntimeContext() {
4979
5038
  if (!cached3) {
4980
- 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;
4981
5046
  }
4982
5047
  return cached3;
4983
5048
  }
@@ -5248,6 +5313,7 @@ function getDetectedStorageType() {
5248
5313
  }
5249
5314
 
5250
5315
  // libs/utils/src/storage/index.ts
5316
+ init_redis_error_listener();
5251
5317
  init_errors();
5252
5318
 
5253
5319
  // libs/utils/src/storage/typed-storage.ts
@@ -5902,6 +5968,7 @@ export {
5902
5968
  BaseStorageAdapter,
5903
5969
  DEFAULT_CODE_VERIFIER_LENGTH,
5904
5970
  DEFAULT_MAX_INPUT_LENGTH,
5971
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
5905
5972
  EncryptedBlobError,
5906
5973
  EncryptedStorageError,
5907
5974
  EncryptedTypedStorage,
@@ -5934,6 +6001,7 @@ export {
5934
6001
  assertNode,
5935
6002
  asymmetricAlgSchema,
5936
6003
  asymmetricKeyDataSchema,
6004
+ attachRedisErrorListener,
5937
6005
  awaitOutsideRequest,
5938
6006
  base64Decode,
5939
6007
  base64Encode,
@@ -5954,6 +6022,7 @@ export {
5954
6022
  createKeyPersistenceWithStorage,
5955
6023
  createMemoryStorage,
5956
6024
  createNamespacedStorage,
6025
+ createRedisClient,
5957
6026
  createRootStorage,
5958
6027
  createSafeRegExp,
5959
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 {
@@ -2994,11 +3030,26 @@ function getRedisClass() {
2994
3030
  });
2995
3031
  }
2996
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
+ }
2997
3047
  var RedisStorageAdapter;
2998
3048
  var init_redis = __esm({
2999
3049
  "libs/utils/src/storage/adapters/redis.ts"() {
3000
3050
  "use strict";
3001
3051
  init_errors();
3052
+ init_redis_error_listener();
3002
3053
  init_utils();
3003
3054
  init_base();
3004
3055
  RedisStorageAdapter = class extends BaseStorageAdapter {
@@ -3007,6 +3058,7 @@ var init_redis = __esm({
3007
3058
  subscriber;
3008
3059
  options;
3009
3060
  ownsClient;
3061
+ detachErrorListener;
3010
3062
  connecting;
3011
3063
  keyPrefix;
3012
3064
  subscriptionHandlers = /* @__PURE__ */ new Map();
@@ -3063,7 +3115,10 @@ var init_redis = __esm({
3063
3115
  await client.ping();
3064
3116
  this.client = client;
3065
3117
  this.connected = true;
3066
- if (this.ownsClient) client.removeListener("error", recordSocketError);
3118
+ if (this.ownsClient) {
3119
+ client.removeListener("error", recordSocketError);
3120
+ this.detachErrorListener = attachRedisErrorListener(client, { label: "RedisStorage" });
3121
+ }
3067
3122
  } catch (e) {
3068
3123
  if (this.ownsClient && client) {
3069
3124
  try {
@@ -3084,6 +3139,8 @@ var init_redis = __esm({
3084
3139
  if (this.ownsClient && this.client) {
3085
3140
  await this.client.quit();
3086
3141
  }
3142
+ this.detachErrorListener?.();
3143
+ this.detachErrorListener = void 0;
3087
3144
  this.client = void 0;
3088
3145
  this.connected = false;
3089
3146
  this.subscriptionHandlers.clear();
@@ -3311,6 +3368,7 @@ var init_redis = __esm({
3311
3368
  } else {
3312
3369
  throw new StorageConfigError("redis", "Cannot create subscriber without url, config, or client");
3313
3370
  }
3371
+ attachRedisErrorListener(subscriber, { label: "RedisStorage:subscriber" });
3314
3372
  subscriber.on("message", (channel, message) => {
3315
3373
  const handlers = this.subscriptionHandlers.get(channel);
3316
3374
  if (handlers) {
@@ -4007,6 +4065,7 @@ __export(index_exports, {
4007
4065
  BaseStorageAdapter: () => BaseStorageAdapter,
4008
4066
  DEFAULT_CODE_VERIFIER_LENGTH: () => DEFAULT_CODE_VERIFIER_LENGTH,
4009
4067
  DEFAULT_MAX_INPUT_LENGTH: () => DEFAULT_MAX_INPUT_LENGTH,
4068
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS: () => DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
4010
4069
  EncryptedBlobError: () => EncryptedBlobError,
4011
4070
  EncryptedStorageError: () => EncryptedStorageError,
4012
4071
  EncryptedTypedStorage: () => EncryptedTypedStorage,
@@ -4039,6 +4098,7 @@ __export(index_exports, {
4039
4098
  assertNode: () => assertNode,
4040
4099
  asymmetricAlgSchema: () => asymmetricAlgSchema,
4041
4100
  asymmetricKeyDataSchema: () => asymmetricKeyDataSchema,
4101
+ attachRedisErrorListener: () => attachRedisErrorListener,
4042
4102
  awaitOutsideRequest: () => import_async_context.awaitOutsideRequest,
4043
4103
  base64Decode: () => base64Decode,
4044
4104
  base64Encode: () => base64Encode,
@@ -4059,6 +4119,7 @@ __export(index_exports, {
4059
4119
  createKeyPersistenceWithStorage: () => createKeyPersistenceWithStorage,
4060
4120
  createMemoryStorage: () => createMemoryStorage,
4061
4121
  createNamespacedStorage: () => createNamespacedStorage,
4122
+ createRedisClient: () => createRedisClient,
4062
4123
  createRootStorage: () => createRootStorage,
4063
4124
  createSafeRegExp: () => createSafeRegExp,
4064
4125
  createSecretData: () => createSecretData,
@@ -5194,7 +5255,13 @@ function detectRuntimeContext() {
5194
5255
  var cached3;
5195
5256
  function getRuntimeContext() {
5196
5257
  if (!cached3) {
5197
- 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;
5198
5265
  }
5199
5266
  return cached3;
5200
5267
  }
@@ -5465,6 +5532,7 @@ function getDetectedStorageType() {
5465
5532
  }
5466
5533
 
5467
5534
  // libs/utils/src/storage/index.ts
5535
+ init_redis_error_listener();
5468
5536
  init_errors();
5469
5537
 
5470
5538
  // libs/utils/src/storage/typed-storage.ts
@@ -6120,6 +6188,7 @@ function setMachineIdOverride(id) {
6120
6188
  BaseStorageAdapter,
6121
6189
  DEFAULT_CODE_VERIFIER_LENGTH,
6122
6190
  DEFAULT_MAX_INPUT_LENGTH,
6191
+ DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS,
6123
6192
  EncryptedBlobError,
6124
6193
  EncryptedStorageError,
6125
6194
  EncryptedTypedStorage,
@@ -6152,6 +6221,7 @@ function setMachineIdOverride(id) {
6152
6221
  assertNode,
6153
6222
  asymmetricAlgSchema,
6154
6223
  asymmetricKeyDataSchema,
6224
+ attachRedisErrorListener,
6155
6225
  awaitOutsideRequest,
6156
6226
  base64Decode,
6157
6227
  base64Encode,
@@ -6172,6 +6242,7 @@ function setMachineIdOverride(id) {
6172
6242
  createKeyPersistenceWithStorage,
6173
6243
  createMemoryStorage,
6174
6244
  createNamespacedStorage,
6245
+ createRedisClient,
6175
6246
  createRootStorage,
6176
6247
  createSafeRegExp,
6177
6248
  createSecretData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/utils",
3
- "version": "1.8.6",
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.6",
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,6 +4,7 @@
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';
7
8
  import type { MessageHandler, RedisAdapterOptions, SetOptions, Unsubscribe } from '../types';
8
9
  import { BaseStorageAdapter } from './base';
9
10
  type Redis = import('ioredis').Redis;
@@ -34,6 +35,7 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
34
35
  private subscriber?;
35
36
  private readonly options;
36
37
  private readonly ownsClient;
38
+ private detachErrorListener?;
37
39
  private connecting?;
38
40
  private readonly keyPrefix;
39
41
  private readonly subscriptionHandlers;
@@ -87,4 +89,20 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
87
89
  */
88
90
  getClient(): Redis | undefined;
89
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;
90
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;