@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.
- package/browser/escape/escape.mjs +25 -0
- package/browser/escape/index.mjs +9 -1
- package/browser/http/index.mjs +4 -0
- package/browser/http/limited-body.mjs +74 -0
- package/browser/index.mjs +28 -2
- package/browser/optional-module/index.mjs +6 -0
- package/browser/optional-module/optional-module.mjs +18 -0
- package/browser/redaction/redaction.mjs +6 -4
- package/browser/stdio-bridge/sse-frames.mjs +3 -1
- package/browser/stdio-bridge/stdio-framer.mjs +4 -1
- package/browser/storage/adapters/filesystem.mjs +31 -9
- package/browser/storage/adapters/memory.mjs +25 -26
- package/browser/storage/factory.mjs +2 -1
- package/browser/storage/index.mjs +2 -1
- package/browser/storage/utils/index.mjs +2 -1
- package/browser/storage/utils/pattern.mjs +71 -24
- package/browser/uri/uri-template.mjs +93 -15
- package/crypto/key-persistence/types.d.ts +4 -1
- package/escape/escape.d.ts +19 -0
- package/escape/index.d.ts +1 -1
- package/esm/index.mjs +1 -1
- package/http/index.d.ts +2 -0
- package/http/limited-body.d.ts +56 -0
- package/index.d.ts +5 -4
- package/index.js +1 -1
- package/optional-module/index.d.ts +1 -0
- package/optional-module/optional-module.d.ts +12 -0
- package/package.json +3 -3
- package/redaction/redaction.d.ts +1 -1
- package/storage/adapters/filesystem.d.ts +11 -0
- package/storage/adapters/memory.d.ts +6 -2
- package/storage/index.d.ts +1 -1
- package/storage/utils/index.d.ts +1 -1
- package/storage/utils/pattern.d.ts +31 -4
|
@@ -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,
|
package/browser/escape/index.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// libs/utils/src/escape/index.ts
|
|
2
|
-
import {
|
|
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,
|
package/browser/http/index.mjs
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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,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
|
-
|
|
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"'&#;?,)}]+/
|
|
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 {
|
|
3
|
+
import { fileExists, mkdir, readdir, readJSON, rename, unlink, writeFile } from "../../fs/index.mjs";
|
|
4
4
|
import { StorageOperationError } from "../errors.mjs";
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
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 (!
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
293
|
+
* Evict the least recently used entry. Returns false when no entry is left to evict.
|
|
295
294
|
*/
|
|
296
295
|
evictOldest() {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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,
|