@frontmcp/utils 1.12.0 → 1.12.1

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.
@@ -34,7 +34,32 @@ function safeJsonForScript(value) {
34
34
  return '{"error":"Value could not be serialized"}';
35
35
  }
36
36
  }
37
+ function isControlCharacter(code) {
38
+ return code <= 31 && code !== 9 || code >= 127 && code <= 159 || code === 8232 || code === 8233;
39
+ }
40
+ var NAMED_CONTROL_ESCAPES = {
41
+ "\n": "\\n",
42
+ "\r": "\\r",
43
+ "\b": "\\b",
44
+ "\f": "\\f",
45
+ "\v": "\\v"
46
+ };
47
+ function escapeControlCharacters(value) {
48
+ if (value === null || value === void 0) return "";
49
+ const text = typeof value === "string" ? value : String(value);
50
+ let escaped = "";
51
+ let copiedUpTo = 0;
52
+ for (let index = 0; index < text.length; index++) {
53
+ const code = text.charCodeAt(index);
54
+ if (!isControlCharacter(code)) continue;
55
+ escaped += text.slice(copiedUpTo, index);
56
+ escaped += NAMED_CONTROL_ESCAPES[text[index]] ?? `\\u${code.toString(16).padStart(4, "0")}`;
57
+ copiedUpTo = index + 1;
58
+ }
59
+ return copiedUpTo === 0 ? text : escaped + text.slice(copiedUpTo);
60
+ }
37
61
  export {
62
+ escapeControlCharacters,
38
63
  escapeHtml,
39
64
  escapeHtmlAttr,
40
65
  escapeJsString,
@@ -1,6 +1,14 @@
1
1
  // libs/utils/src/escape/index.ts
2
- import { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJsonForScript } from "./escape.mjs";
2
+ import {
3
+ escapeHtml,
4
+ escapeHtmlAttr,
5
+ escapeJsString,
6
+ escapeScriptClose,
7
+ safeJsonForScript,
8
+ escapeControlCharacters
9
+ } from "./escape.mjs";
3
10
  export {
11
+ escapeControlCharacters,
4
12
  escapeHtml,
5
13
  escapeHtmlAttr,
6
14
  escapeJsString,
@@ -1,11 +1,15 @@
1
1
  // libs/utils/src/http/index.ts
2
2
  import { validateBaseUrl } from "./http.mjs";
3
+ import { readBodyWithLimit, readTextWithLimit, ResponseTooLargeError } from "./limited-body.mjs";
3
4
  import { buildSetCookie, parseCookies, getCookie, isLocalhost, isSecureRequest } from "./cookie.mjs";
4
5
  export {
6
+ ResponseTooLargeError,
5
7
  buildSetCookie,
6
8
  getCookie,
7
9
  isLocalhost,
8
10
  isSecureRequest,
9
11
  parseCookies,
12
+ readBodyWithLimit,
13
+ readTextWithLimit,
10
14
  validateBaseUrl
11
15
  };
@@ -0,0 +1,74 @@
1
+ // libs/utils/src/http/limited-body.ts
2
+ var ResponseTooLargeError = class extends Error {
3
+ maxBytes;
4
+ constructor(maxBytes) {
5
+ super(`Response body exceeds the limit of ${maxBytes} bytes`);
6
+ this.name = "ResponseTooLargeError";
7
+ this.maxBytes = maxBytes;
8
+ }
9
+ };
10
+ function checkLimit(maxBytes) {
11
+ if (!Number.isFinite(maxBytes) || maxBytes < 0) {
12
+ throw new RangeError(`maxBytes must be a non-negative finite number, got ${maxBytes}`);
13
+ }
14
+ }
15
+ function concat(chunks, total) {
16
+ if (chunks.length === 1) return chunks[0];
17
+ const out = new Uint8Array(total);
18
+ let offset = 0;
19
+ for (const chunk of chunks) {
20
+ out.set(chunk, offset);
21
+ offset += chunk.byteLength;
22
+ }
23
+ return out;
24
+ }
25
+ async function readBodyWithLimit(response, maxBytes) {
26
+ checkLimit(maxBytes);
27
+ const declared = response.headers?.get("content-length");
28
+ if (declared !== null && declared !== void 0 && declared.trim() !== "") {
29
+ const length = Number(declared);
30
+ if (Number.isFinite(length) && length > maxBytes) {
31
+ await response.body?.cancel().catch(() => void 0);
32
+ throw new ResponseTooLargeError(maxBytes);
33
+ }
34
+ }
35
+ const body = response.body;
36
+ if (!body) {
37
+ let bytes;
38
+ if (typeof response.arrayBuffer === "function") {
39
+ bytes = new Uint8Array(await response.arrayBuffer());
40
+ } else if (typeof response.text === "function") {
41
+ bytes = new TextEncoder().encode(await response.text());
42
+ } else {
43
+ bytes = new Uint8Array(0);
44
+ }
45
+ if (bytes.byteLength > maxBytes) throw new ResponseTooLargeError(maxBytes);
46
+ return bytes;
47
+ }
48
+ const reader = body.getReader();
49
+ const chunks = [];
50
+ let total = 0;
51
+ try {
52
+ for (; ; ) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+ total += value.byteLength;
56
+ if (total > maxBytes) {
57
+ await reader.cancel().catch(() => void 0);
58
+ throw new ResponseTooLargeError(maxBytes);
59
+ }
60
+ chunks.push(value);
61
+ }
62
+ } finally {
63
+ reader.releaseLock();
64
+ }
65
+ return concat(chunks, total);
66
+ }
67
+ async function readTextWithLimit(response, maxBytes) {
68
+ return new TextDecoder().decode(await readBodyWithLimit(response, maxBytes));
69
+ }
70
+ export {
71
+ ResponseTooLargeError,
72
+ readBodyWithLimit,
73
+ readTextWithLimit
74
+ };
package/browser/index.mjs CHANGED
@@ -23,7 +23,17 @@ import {
23
23
  sep
24
24
  } from "./path/index.mjs";
25
25
  import { sanitizeToJson, inferMimeType, findNonFiniteNumber, findDroppedKeys } from "./content/index.mjs";
26
- import { validateBaseUrl, buildSetCookie, parseCookies, getCookie, isLocalhost, isSecureRequest } from "./http/index.mjs";
26
+ import {
27
+ validateBaseUrl,
28
+ buildSetCookie,
29
+ parseCookies,
30
+ getCookie,
31
+ isLocalhost,
32
+ isSecureRequest,
33
+ readBodyWithLimit,
34
+ readTextWithLimit,
35
+ ResponseTooLargeError
36
+ } from "./http/index.mjs";
27
37
  import {
28
38
  classifyIpAddress,
29
39
  expandIpv6,
@@ -94,8 +104,16 @@ import {
94
104
  createPipeUpstream
95
105
  } from "./stdio-bridge/index.mjs";
96
106
  import { redactFields, redactJson } from "./redaction/index.mjs";
97
- import { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJsonForScript } from "./escape/index.mjs";
107
+ import {
108
+ escapeHtml,
109
+ escapeHtmlAttr,
110
+ escapeJsString,
111
+ escapeScriptClose,
112
+ safeJsonForScript,
113
+ escapeControlCharacters
114
+ } from "./escape/index.mjs";
98
115
  import { brandClass, isBrandedInstance } from "./class-brand/index.mjs";
116
+ import { registerOptionalModule, requireOptionalModule } from "./optional-module/index.mjs";
99
117
  import { safeStringify, canonicalJson } from "./serialization/index.mjs";
100
118
  import {
101
119
  getCrypto,
@@ -264,6 +282,7 @@ import {
264
282
  FileSystemStorageAdapter,
265
283
  TypedStorage,
266
284
  EncryptedTypedStorage,
285
+ createGlobMatcher,
267
286
  globToRegex,
268
287
  matchesPattern,
269
288
  validatePattern,
@@ -313,6 +332,7 @@ export {
313
332
  PkceError,
314
333
  REDOS_THRESHOLDS,
315
334
  RedisStorageAdapter,
335
+ ResponseTooLargeError,
316
336
  StorageConfigError,
317
337
  StorageConnectionError,
318
338
  StorageError,
@@ -357,6 +377,7 @@ export {
357
377
  cp,
358
378
  createBridgeStateMachine,
359
379
  createChangeFeed,
380
+ createGlobMatcher,
360
381
  createHttpUpstream,
361
382
  createKeyPersistence,
362
383
  createKeyPersistenceWithStorage,
@@ -385,6 +406,7 @@ export {
385
406
  ensureDirSync,
386
407
  ensureMaxLen,
387
408
  entryAvailabilitySchema,
409
+ escapeControlCharacters,
388
410
  escapeGlob,
389
411
  escapeHtml,
390
412
  escapeHtmlAttr,
@@ -487,17 +509,21 @@ export {
487
509
  processTreeSpawnOptions,
488
510
  randomBytes,
489
511
  randomUUID,
512
+ readBodyWithLimit,
490
513
  readFile,
491
514
  readFileBuffer,
492
515
  readFileSync,
493
516
  readJSON,
517
+ readTextWithLimit,
494
518
  readdir,
495
519
  readdirSync,
496
520
  realpath,
497
521
  reapOrphans,
498
522
  redactFields,
499
523
  redactJson,
524
+ registerOptionalModule,
500
525
  rename,
526
+ requireOptionalModule,
501
527
  resetRuntimeContext,
502
528
  resolveSecretPath,
503
529
  rm,
@@ -0,0 +1,6 @@
1
+ // libs/utils/src/optional-module/index.ts
2
+ import { registerOptionalModule, requireOptionalModule } from "./optional-module.mjs";
3
+ export {
4
+ registerOptionalModule,
5
+ requireOptionalModule
6
+ };
@@ -0,0 +1,18 @@
1
+ // libs/utils/src/optional-module/optional-module.ts
2
+ var BUNDLED_OPTIONAL_MODULES = /* @__PURE__ */ Symbol.for("frontmcp.bundledOptionalModules");
3
+ function bundledOptionalModules() {
4
+ const registryHost = globalThis;
5
+ registryHost[BUNDLED_OPTIONAL_MODULES] ??= /* @__PURE__ */ new Map();
6
+ return registryHost[BUNDLED_OPTIONAL_MODULES];
7
+ }
8
+ function registerOptionalModule(moduleName, moduleExports) {
9
+ bundledOptionalModules().set(moduleName, moduleExports);
10
+ }
11
+ function requireOptionalModule(moduleName, load) {
12
+ const registry = bundledOptionalModules();
13
+ return registry.has(moduleName) ? registry.get(moduleName) : load();
14
+ }
15
+ export {
16
+ registerOptionalModule,
17
+ requireOptionalModule
18
+ };
@@ -1,5 +1,6 @@
1
1
  // libs/utils/src/redaction/redaction.ts
2
2
  var REDACTED = "[REDACTED]";
3
+ var TRUNCATED = "[TRUNCATED]";
3
4
  function redactFields(obj, fields, maxDepth = 5) {
4
5
  if (typeof fields !== "function" && fields.length === 0) return obj;
5
6
  return redactRecord(obj, 0, { isSensitive: sensitiveKeyTest(fields), maxDepth, jsonText: false });
@@ -14,23 +15,24 @@ function sensitiveKeyTest(fields) {
14
15
  }
15
16
  function redactValue(value, depth, options) {
16
17
  if (typeof value === "string") return options.jsonText ? redactJsonText(value, depth, options) : value;
17
- if (Array.isArray(value)) return value.map((item) => redactValue(item, depth, options));
18
+ const isList = Array.isArray(value);
19
+ if ((isList || isPlainObject(value)) && depth >= options.maxDepth) return TRUNCATED;
20
+ if (isList) return value.map((item) => redactValue(item, depth + 1, options));
18
21
  if (isPlainObject(value)) return redactRecord(value, depth, options);
19
22
  return value;
20
23
  }
21
24
  function redactRecord(obj, depth, options) {
22
- if (depth >= options.maxDepth) return obj;
23
25
  const result = {};
24
26
  for (const [key, value] of Object.entries(obj)) {
25
27
  result[key] = options.isSensitive(key) ? REDACTED : redactValue(value, depth + 1, options);
26
28
  }
27
29
  return result;
28
30
  }
29
- var AUTH_SCHEME_CREDENTIAL = /\b(Bearer|Basic)([ \t]+)(?=[\w.~+/-]*[\d.~+/_-])[\w.~+/-]{8,}=*/gi;
31
+ var AUTH_SCHEME_CREDENTIAL = /\b(Bearer|Basic)([ \t]+)(?:(?=[\w.~+/-]*[\d.~+/_-])[\w.~+/-]{8,}|[A-Za-z]{20,})=*/gi;
30
32
  var JWT = /(?<![\w-])eyJ[\w-]+(?:\.[\w-]*){2,4}/g;
31
33
  var URL_CREDENTIALS = /(?<![a-z\d+.-])([a-z][a-z\d+.-]*:\/\/)[^\s/?#@]+@/gi;
32
34
  var VALUE_NAME = /(?:^|[?&#;,{("'\s])(["']?)([A-Za-z_][\w.-]*)\1[ \t]*[:=][ \t]*/g;
33
- var NAMED_VALUE = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|[^\s"'&#;?,)}]+/y;
35
+ var NAMED_VALUE = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|(?:(?:Bearer|Basic)[ \t]+)?[^\s"'&#;?,)}]+/iy;
34
36
  function redactNamedValues(text, isSensitive) {
35
37
  let redacted = "";
36
38
  let copiedUpTo = 0;
@@ -4,6 +4,7 @@ async function relaySseFrames(body, deliver, log, maxEventChars = MAX_EVENT_CHAR
4
4
  const reader = body.getReader();
5
5
  const decoder = new TextDecoder();
6
6
  let buffer = "";
7
+ let searchedUpTo = 0;
7
8
  let eventData = [];
8
9
  let eventChars = 0;
9
10
  async function deliverEvent() {
@@ -24,7 +25,7 @@ async function relaySseFrames(body, deliver, log, maxEventChars = MAX_EVENT_CHAR
24
25
  return;
25
26
  }
26
27
  buffer += decoder.decode(value, { stream: true });
27
- let newline = buffer.indexOf("\n");
28
+ let newline = buffer.indexOf("\n", searchedUpTo);
28
29
  while (newline >= 0) {
29
30
  const line = buffer.slice(0, newline).trim();
30
31
  buffer = buffer.slice(newline + 1);
@@ -37,6 +38,7 @@ async function relaySseFrames(body, deliver, log, maxEventChars = MAX_EVENT_CHAR
37
38
  eventChars += data.length + 1;
38
39
  }
39
40
  }
41
+ searchedUpTo = buffer.length;
40
42
  if (eventChars + buffer.length > maxEventChars) {
41
43
  await reader.cancel();
42
44
  throw new Error(`an SSE event is larger than ${maxEventChars} characters`);
@@ -3,13 +3,15 @@ import { makeDevError } from "./errors.mjs";
3
3
  function createStdioFramer(options) {
4
4
  const { input, output, log, onFrame, onClose } = options;
5
5
  let buffer = "";
6
+ let searchedUpTo = 0;
6
7
  let paused = false;
7
8
  let drainResolvers = [];
8
9
  function flushBuffer() {
9
10
  let nl;
10
- while ((nl = buffer.indexOf("\n")) >= 0) {
11
+ while ((nl = buffer.indexOf("\n", searchedUpTo)) >= 0) {
11
12
  const raw = buffer.slice(0, nl).replace(/\r$/, "");
12
13
  buffer = buffer.slice(nl + 1);
14
+ searchedUpTo = 0;
13
15
  if (raw.length === 0) continue;
14
16
  let parsed;
15
17
  try {
@@ -30,6 +32,7 @@ function createStdioFramer(options) {
30
32
  });
31
33
  });
32
34
  }
35
+ searchedUpTo = buffer.length;
33
36
  }
34
37
  let closed = false;
35
38
  function onInputClosed() {
@@ -1,8 +1,8 @@
1
1
  // libs/utils/src/storage/adapters/filesystem.ts
2
2
  import { bytesToHex, randomBytes } from "../../crypto/index.mjs";
3
- import { ensureDir, fileExists, mkdir, readdir, readJSON, rename, unlink, writeFile } from "../../fs/index.mjs";
3
+ import { fileExists, mkdir, readdir, readJSON, rename, unlink, writeFile } from "../../fs/index.mjs";
4
4
  import { StorageOperationError } from "../errors.mjs";
5
- import { globToRegex } from "../utils/pattern.mjs";
5
+ import { createGlobMatcher } from "../utils/pattern.mjs";
6
6
  import { BaseStorageAdapter } from "./base.mjs";
7
7
  var FileSystemStorageAdapter = class extends BaseStorageAdapter {
8
8
  backendName = "filesystem";
@@ -10,6 +10,8 @@ var FileSystemStorageAdapter = class extends BaseStorageAdapter {
10
10
  extension;
11
11
  dirMode;
12
12
  fileMode;
13
+ /** The last update queued for each key, so a counter's read and write never interleave with another update. */
14
+ keyUpdates = /* @__PURE__ */ new Map();
13
15
  constructor(options) {
14
16
  super();
15
17
  this.baseDir = options.baseDir;
@@ -22,11 +24,7 @@ var FileSystemStorageAdapter = class extends BaseStorageAdapter {
22
24
  // ============================================
23
25
  async connect() {
24
26
  if (this.connected) return;
25
- await ensureDir(this.baseDir);
26
- try {
27
- await mkdir(this.baseDir, { recursive: true, mode: this.dirMode });
28
- } catch {
29
- }
27
+ await mkdir(this.baseDir, { recursive: true, mode: this.dirMode });
30
28
  this.connected = true;
31
29
  }
32
30
  async disconnect() {
@@ -104,6 +102,10 @@ var FileSystemStorageAdapter = class extends BaseStorageAdapter {
104
102
  // ============================================
105
103
  async expire(key, ttlSeconds) {
106
104
  this.ensureConnected();
105
+ return this.updateInTurn(key, () => this.expireNow(key, ttlSeconds));
106
+ }
107
+ /** Set the expiry of `key` from its current entry; run only through {@link updateInTurn}. */
108
+ async expireNow(key, ttlSeconds) {
107
109
  const filePath = this.keyToPath(key);
108
110
  const entry = await this.readEntry(filePath);
109
111
  if (!entry || this.isExpired(entry)) {
@@ -132,14 +134,14 @@ var FileSystemStorageAdapter = class extends BaseStorageAdapter {
132
134
  // ============================================
133
135
  async keys(pattern = "*") {
134
136
  this.ensureConnected();
135
- const regex = globToRegex(pattern);
137
+ const matches = createGlobMatcher(pattern);
136
138
  const result = [];
137
139
  try {
138
140
  const files = await readdir(this.baseDir);
139
141
  for (const file of files) {
140
142
  if (!file.endsWith(this.extension)) continue;
141
143
  const key = this.pathToKey(file);
142
- if (!regex.test(key)) continue;
144
+ if (!matches(key)) continue;
143
145
  const filePath = this.keyToPath(key);
144
146
  const entry = await this.readEntry(filePath);
145
147
  if (entry && !this.isExpired(entry)) {
@@ -168,6 +170,10 @@ var FileSystemStorageAdapter = class extends BaseStorageAdapter {
168
170
  }
169
171
  async incrBy(key, amount) {
170
172
  this.ensureConnected();
173
+ return this.updateInTurn(key, () => this.incrByNow(key, amount));
174
+ }
175
+ /** Add `amount` to the counter at `key`; run only through {@link updateInTurn}. */
176
+ async incrByNow(key, amount) {
171
177
  const filePath = this.keyToPath(key);
172
178
  const entry = await this.readEntry(filePath);
173
179
  let currentValue = 0;
@@ -189,6 +195,22 @@ var FileSystemStorageAdapter = class extends BaseStorageAdapter {
189
195
  // ============================================
190
196
  // Internal Helpers
191
197
  // ============================================
198
+ /**
199
+ * Run `update` once the updates of `key` queued before it in this process have finished, so two updates of
200
+ * one key never read the same entry. Processes that share a directory are not coordinated.
201
+ */
202
+ updateInTurn(key, update) {
203
+ const result = (this.keyUpdates.get(key) ?? Promise.resolve()).then(update);
204
+ const settled = result.then(
205
+ () => void 0,
206
+ () => void 0
207
+ );
208
+ this.keyUpdates.set(key, settled);
209
+ void settled.then(() => {
210
+ if (this.keyUpdates.get(key) === settled) this.keyUpdates.delete(key);
211
+ });
212
+ return result;
213
+ }
192
214
  /**
193
215
  * Convert a storage key to a file path.
194
216
  * Sanitizes the key to be filesystem-safe.
@@ -1,7 +1,7 @@
1
1
  // libs/utils/src/storage/adapters/memory.ts
2
2
  import { EventEmitter } from "../../event-emitter/browser-event-emitter.mjs";
3
3
  import { StorageOperationError } from "../errors.mjs";
4
- import { globToRegex } from "../utils/pattern.mjs";
4
+ import { createGlobMatcher } from "../utils/pattern.mjs";
5
5
  import { expiresAtToTTL, isExpired, ttlToExpiresAt, validateTTL } from "../utils/ttl.mjs";
6
6
  import { BaseStorageAdapter } from "./base.mjs";
7
7
  var DEFAULT_SWEEP_INTERVAL_SECONDS = 60;
@@ -12,8 +12,8 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
12
12
  emitter = new EventEmitter();
13
13
  sweepInterval;
14
14
  options;
15
- // LRU tracking (simple linked list via insertion order in Map)
16
- accessOrder = [];
15
+ // LRU tracking: a Set keeps insertion order, and adding a key again moves it to the end
16
+ accessOrder = /* @__PURE__ */ new Set();
17
17
  constructor(options = {}) {
18
18
  super();
19
19
  this.options = {
@@ -39,7 +39,7 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
39
39
  this.stopSweeper();
40
40
  this.clearAllTimeouts();
41
41
  this.store.clear();
42
- this.accessOrder = [];
42
+ this.accessOrder.clear();
43
43
  this.emitter.removeAllListeners();
44
44
  this.connected = false;
45
45
  }
@@ -83,11 +83,7 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
83
83
  }
84
84
  }
85
85
  }
86
- if (this.options.maxEntries > 0 && !this.store.has(key)) {
87
- while (this.store.size >= this.options.maxEntries) {
88
- this.evictOldest();
89
- }
90
- }
86
+ this.makeRoomFor(key);
91
87
  this.store.set(key, entry);
92
88
  this.touchLRU(key);
93
89
  }
@@ -165,14 +161,14 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
165
161
  // ============================================
166
162
  async keys(pattern = "*") {
167
163
  this.ensureConnected();
168
- const regex = globToRegex(pattern);
164
+ const matches = createGlobMatcher(pattern);
169
165
  const result = [];
170
166
  for (const [key, entry] of this.store) {
171
167
  if (isExpired(entry.expiresAt)) {
172
168
  this.deleteEntry(key);
173
169
  continue;
174
170
  }
175
- if (regex.test(key)) {
171
+ if (matches(key)) {
176
172
  result.push(key);
177
173
  }
178
174
  }
@@ -213,6 +209,7 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
213
209
  }
214
210
  }
215
211
  }
212
+ this.makeRoomFor(key);
216
213
  this.store.set(key, newEntry);
217
214
  this.touchLRU(key);
218
215
  return newValue;
@@ -274,31 +271,33 @@ var MemoryStorageAdapter = class extends BaseStorageAdapter {
274
271
  */
275
272
  touchLRU(key) {
276
273
  if (this.options.maxEntries <= 0) return;
277
- const idx = this.accessOrder.indexOf(key);
278
- if (idx !== -1) {
279
- this.accessOrder.splice(idx, 1);
280
- }
281
- this.accessOrder.push(key);
274
+ this.accessOrder.delete(key);
275
+ this.accessOrder.add(key);
282
276
  }
283
277
  /**
284
278
  * Remove key from LRU tracking.
285
279
  */
286
280
  removeLRU(key) {
287
- if (this.options.maxEntries <= 0) return;
288
- const idx = this.accessOrder.indexOf(key);
289
- if (idx !== -1) {
290
- this.accessOrder.splice(idx, 1);
281
+ this.accessOrder.delete(key);
282
+ }
283
+ /**
284
+ * Evict least recently used entries until a new key fits within maxEntries.
285
+ */
286
+ makeRoomFor(key) {
287
+ if (this.options.maxEntries <= 0 || this.store.has(key)) return;
288
+ while (this.store.size >= this.options.maxEntries) {
289
+ if (!this.evictOldest()) return;
291
290
  }
292
291
  }
293
292
  /**
294
- * Evict oldest entry (LRU).
293
+ * Evict the least recently used entry. Returns false when no entry is left to evict.
295
294
  */
296
295
  evictOldest() {
297
- if (this.accessOrder.length === 0) return;
298
- const oldestKey = this.accessOrder.shift();
299
- if (oldestKey) {
300
- this.deleteEntry(oldestKey);
301
- }
296
+ const oldest = this.accessOrder.values().next();
297
+ if (oldest.done) return false;
298
+ this.accessOrder.delete(oldest.value);
299
+ this.deleteEntry(oldest.value);
300
+ return true;
302
301
  }
303
302
  /**
304
303
  * Start background sweeper.
@@ -12,6 +12,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  import { z } from "@frontmcp/lazy-zod";
13
13
  import { getEnv, isProduction } from "../env/browser-env.mjs";
14
14
  import { getRuntimeContext } from "../env/browser-runtime-context.mjs";
15
+ import { requireOptionalModule } from "../optional-module/index.mjs";
15
16
  import { MemoryStorageAdapter } from "./adapters/memory.mjs";
16
17
  import { StorageConfigError } from "./errors.mjs";
17
18
  import { createNamespacedStorage, createRootStorage } from "./namespace.mjs";
@@ -32,7 +33,7 @@ function parseSqliteOptions(options) {
32
33
  }
33
34
  function loadSqliteStorage() {
34
35
  try {
35
- return __require(SQLITE_STORAGE_PACKAGE);
36
+ return requireOptionalModule(SQLITE_STORAGE_PACKAGE, () => __require(SQLITE_STORAGE_PACKAGE));
36
37
  } catch (error) {
37
38
  const missing = error.code === "MODULE_NOT_FOUND";
38
39
  if (!missing || !String(error.message).includes(SQLITE_STORAGE_PACKAGE)) throw error;
@@ -35,7 +35,7 @@ import {
35
35
  LocalStorageAdapter,
36
36
  IndexedDBStorageAdapter
37
37
  } from "./adapters/index.mjs";
38
- import { globToRegex, matchesPattern, validatePattern, escapeGlob } from "./utils/pattern.mjs";
38
+ import { createGlobMatcher, globToRegex, matchesPattern, validatePattern, escapeGlob } from "./utils/pattern.mjs";
39
39
  import {
40
40
  MAX_TTL_SECONDS,
41
41
  validateTTL,
@@ -74,6 +74,7 @@ export {
74
74
  attachRedisErrorListener,
75
75
  buildPrefix,
76
76
  createChangeFeed,
77
+ createGlobMatcher,
77
78
  createMemoryStorage,
78
79
  createNamespacedStorage,
79
80
  createRedisClient,
@@ -1,5 +1,5 @@
1
1
  // libs/utils/src/storage/utils/index.ts
2
- import { globToRegex, matchesPattern, validatePattern, escapeGlob } from "./pattern.mjs";
2
+ import { createGlobMatcher, globToRegex, matchesPattern, validatePattern, escapeGlob } from "./pattern.mjs";
3
3
  import {
4
4
  MAX_TTL_SECONDS,
5
5
  validateTTL,
@@ -11,6 +11,7 @@ import {
11
11
  } from "./ttl.mjs";
12
12
  export {
13
13
  MAX_TTL_SECONDS,
14
+ createGlobMatcher,
14
15
  escapeGlob,
15
16
  expiresAtToTTL,
16
17
  globToRegex,