@frontmcp/utils 1.10.15 → 1.11.0-rc.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/README.md +1 -0
- package/browser/async-context/browser-async-context.mjs +177 -0
- package/browser/async-context/index.mjs +15 -0
- package/browser/async-context/overlap-error.mjs +10 -0
- package/browser/class-brand/class-brand.mjs +27 -0
- package/browser/class-brand/index.mjs +6 -0
- package/browser/content/content.mjs +151 -0
- package/browser/content/index.mjs +8 -0
- package/browser/crypto/browser.mjs +103 -0
- package/browser/crypto/encrypted-blob.mjs +119 -0
- package/browser/crypto/hmac-signing.mjs +70 -0
- package/browser/crypto/index.mjs +281 -0
- package/browser/crypto/jwt-alg.mjs +40 -0
- package/browser/crypto/key-persistence/factory.mjs +134 -0
- package/browser/crypto/key-persistence/index.mjs +26 -0
- package/browser/crypto/key-persistence/key-persistence.mjs +185 -0
- package/browser/crypto/key-persistence/schemas.mjs +82 -0
- package/browser/crypto/pkce/index.mjs +25 -0
- package/browser/crypto/pkce/pkce.mjs +59 -0
- package/browser/crypto/runtime.mjs +17 -0
- package/browser/crypto/secret-persistence/index.mjs +29 -0
- package/browser/crypto/secret-persistence/persistence.mjs +181 -0
- package/browser/crypto/secret-persistence/schema.mjs +40 -0
- package/browser/crypto/web-rsa.mjs +36 -0
- package/browser/env/browser-env.mjs +56 -0
- package/browser/env/browser-runtime-context.mjs +85 -0
- package/browser/env/build-target.mjs +25 -0
- package/browser/env/edge-scope.mjs +17 -0
- package/browser/env/index.mjs +47 -0
- package/browser/env/provider.mjs +46 -0
- package/browser/escape/escape.mjs +43 -0
- package/browser/escape/index.mjs +9 -0
- package/browser/event-emitter/browser-event-emitter.mjs +49 -0
- package/browser/event-emitter/index.mjs +5 -0
- package/browser/fs/fs.mjs +227 -0
- package/browser/fs/index.mjs +65 -0
- package/browser/http/cookie.mjs +85 -0
- package/browser/http/http.mjs +18 -0
- package/browser/http/index.mjs +11 -0
- package/browser/index.mjs +526 -0
- package/browser/llm/index.mjs +5 -0
- package/browser/llm/llm-tool-handlers.mjs +55 -0
- package/browser/machine-id/index.mjs +7 -0
- package/browser/machine-id/machine-id.mjs +108 -0
- package/browser/naming/index.mjs +10 -0
- package/browser/naming/naming.mjs +68 -0
- package/browser/net/index.mjs +10 -0
- package/browser/net/ip-address.mjs +128 -0
- package/browser/net/redirect.mjs +7 -0
- package/browser/path/browser-path.mjs +47 -0
- package/browser/path/index.mjs +24 -0
- package/browser/path/path.mjs +13 -0
- package/browser/process/child-process.mjs +23 -0
- package/browser/process/index.mjs +15 -0
- package/browser/process/owned-process.mjs +175 -0
- package/browser/process/process-tree.mjs +149 -0
- package/browser/process/process-watch.mjs +48 -0
- package/browser/redaction/index.mjs +6 -0
- package/browser/redaction/redaction.mjs +74 -0
- package/browser/regex/index.mjs +3 -0
- package/browser/regex/patterns.mjs +167 -0
- package/browser/regex/safe-regex.mjs +93 -0
- package/browser/serialization/index.mjs +5 -0
- package/browser/serialization/serialization.mjs +22 -0
- package/browser/stdio-bridge/errors.mjs +27 -0
- package/browser/stdio-bridge/index.mjs +24 -0
- package/browser/stdio-bridge/list-changed.mjs +21 -0
- package/browser/stdio-bridge/state-machine.mjs +211 -0
- package/browser/stdio-bridge/stdio-framer.mjs +91 -0
- package/browser/stdio-bridge/upstream-client.mjs +189 -0
- package/browser/storage/adapters/base.mjs +139 -0
- package/browser/storage/adapters/cloudflare-kv.mjs +149 -0
- package/browser/storage/adapters/filesystem.mjs +279 -0
- package/browser/storage/adapters/index.mjs +23 -0
- package/browser/storage/adapters/indexeddb.mjs +198 -0
- package/browser/storage/adapters/localstorage.mjs +222 -0
- package/browser/storage/adapters/memory.mjs +350 -0
- package/browser/storage/adapters/redis.mjs +497 -0
- package/browser/storage/adapters/upstash.mjs +299 -0
- package/browser/storage/adapters/vercel-kv.mjs +226 -0
- package/browser/storage/change-feed.mjs +189 -0
- package/browser/storage/encrypted-typed-storage.mjs +329 -0
- package/browser/storage/errors.mjs +92 -0
- package/browser/storage/factory.mjs +155 -0
- package/browser/storage/index.mjs +96 -0
- package/browser/storage/namespace.mjs +186 -0
- package/browser/storage/redis-error-listener.mjs +32 -0
- package/browser/storage/redis-url.mjs +66 -0
- package/browser/storage/typed-storage.mjs +171 -0
- package/browser/storage/utils/compare-and-delete.mjs +9 -0
- package/browser/storage/utils/index.mjs +24 -0
- package/browser/storage/utils/pattern.mjs +68 -0
- package/browser/storage/utils/ttl.mjs +48 -0
- package/browser/uri/index.mjs +19 -0
- package/browser/uri/uri-template.mjs +86 -0
- package/browser/uri/uri-validation.mjs +18 -0
- package/content/content.d.ts +8 -0
- package/content/index.d.ts +1 -1
- package/crypto/browser.d.ts +1 -13
- package/crypto/browser.js +1 -1
- package/crypto/node.js +1 -1
- package/crypto/web-rsa.d.ts +13 -0
- package/env/browser-runtime-context.js +1 -1
- package/env/runtime-context.js +1 -1
- package/esm/crypto/browser.mjs +1 -1
- package/esm/crypto/node.mjs +1 -1
- package/esm/env/browser-runtime-context.mjs +1 -1
- package/esm/env/runtime-context.mjs +1 -1
- package/esm/index.mjs +1 -1
- package/fs/fs.d.ts +45 -4
- package/fs/index.d.ts +1 -1
- package/index.d.ts +10 -4
- package/index.js +1 -1
- package/package.json +24 -8
- package/process/child-process.d.ts +2 -0
- package/process/index.d.ts +6 -0
- package/process/owned-process.d.ts +59 -0
- package/process/process-tree.d.ts +78 -0
- package/process/process-watch.d.ts +41 -0
- package/redaction/index.d.ts +2 -0
- package/redaction/redaction.d.ts +24 -0
- package/stdio-bridge/errors.d.ts +23 -0
- package/stdio-bridge/index.d.ts +9 -0
- package/stdio-bridge/list-changed.d.ts +3 -0
- package/stdio-bridge/state-machine.d.ts +61 -0
- package/stdio-bridge/stdio-framer.d.ts +49 -0
- package/stdio-bridge/types.d.ts +10 -0
- package/stdio-bridge/upstream-client.d.ts +66 -0
- package/storage/adapters/base.d.ts +2 -0
- package/storage/adapters/memory.d.ts +1 -0
- package/storage/adapters/redis.d.ts +3 -0
- package/storage/change-feed.d.ts +47 -0
- package/storage/factory.d.ts +6 -0
- package/storage/index.d.ts +3 -1
- package/storage/namespace.d.ts +2 -0
- package/storage/types.d.ts +46 -1
- package/storage/utils/compare-and-delete.d.ts +2 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// libs/utils/src/stdio-bridge/state-machine.ts
|
|
2
|
+
import { DEV_BUFFER_FULL, DEV_RELOAD_DEADLINE, DEV_SERVER_UNREACHABLE, makeDevError } from "./errors.mjs";
|
|
3
|
+
function createBridgeStateMachine(options) {
|
|
4
|
+
const { log, bufferSize, reloadDeadlineMs, respond, forward } = options;
|
|
5
|
+
let state = "Idle";
|
|
6
|
+
const buffer = [];
|
|
7
|
+
const requeued = /* @__PURE__ */ new WeakSet();
|
|
8
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
9
|
+
let reloadTimer;
|
|
10
|
+
let degradedDetail;
|
|
11
|
+
let readyGen = 0;
|
|
12
|
+
function transition(next, info) {
|
|
13
|
+
if (state === next) return;
|
|
14
|
+
log.info("state-transition", { from: state, to: next, ...info });
|
|
15
|
+
state = next;
|
|
16
|
+
}
|
|
17
|
+
function clearReloadTimer() {
|
|
18
|
+
if (reloadTimer) {
|
|
19
|
+
clearTimeout(reloadTimer);
|
|
20
|
+
reloadTimer = void 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function flushBufferAsResponses(code, reason, data) {
|
|
24
|
+
while (buffer.length > 0) {
|
|
25
|
+
const f = buffer.shift();
|
|
26
|
+
if (!f) break;
|
|
27
|
+
const id = f.id ?? null;
|
|
28
|
+
if (id === null) {
|
|
29
|
+
log.warn("drop-notification", { method: f.method, reason });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
await respond(makeDevError(id, code, { reason, ...data }));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function failInflightAsResponses(reason, data) {
|
|
36
|
+
for (const [id, req] of inflight) {
|
|
37
|
+
const idVal = typeof id === "number" ? id : id;
|
|
38
|
+
await respond(makeDevError(idVal, DEV_SERVER_UNREACHABLE, { reason, ...data, method: req.frame.method }));
|
|
39
|
+
}
|
|
40
|
+
inflight.clear();
|
|
41
|
+
}
|
|
42
|
+
const machine = {
|
|
43
|
+
get state() {
|
|
44
|
+
return state;
|
|
45
|
+
},
|
|
46
|
+
bufferDepth: () => buffer.length,
|
|
47
|
+
onBootStart() {
|
|
48
|
+
transition("Booting");
|
|
49
|
+
},
|
|
50
|
+
onChildReady() {
|
|
51
|
+
clearReloadTimer();
|
|
52
|
+
degradedDetail = void 0;
|
|
53
|
+
transition("Ready", { bufferDepth: buffer.length });
|
|
54
|
+
const drain = [...buffer];
|
|
55
|
+
buffer.length = 0;
|
|
56
|
+
const drainGen = readyGen;
|
|
57
|
+
void (async () => {
|
|
58
|
+
for (const [index, f] of drain.entries()) {
|
|
59
|
+
if (readyGen !== drainGen) {
|
|
60
|
+
await keepUnsent(drain.slice(index));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const requestId = typeof f.id === "string" || typeof f.id === "number" ? f.id : null;
|
|
64
|
+
if (requestId !== null) {
|
|
65
|
+
inflight.set(requestId, { frame: f, forwardedAt: Date.now() });
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await forward(f);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.error("forward-failed", { error: err.message });
|
|
71
|
+
if (requestId !== null && inflight.delete(requestId)) {
|
|
72
|
+
await respond(makeDevError(requestId, DEV_SERVER_UNREACHABLE, { reason: "forward_failed" }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
},
|
|
78
|
+
onChildExit(reason) {
|
|
79
|
+
log.warn("child-exit", { reason, state });
|
|
80
|
+
if (state === "Stopping") return;
|
|
81
|
+
if (state === "Ready" || state === "Booting") {
|
|
82
|
+
readyGen++;
|
|
83
|
+
transition("Reloading", { trigger: "child-exit", reason });
|
|
84
|
+
void failInflightAsResponses("child_exit", { reason });
|
|
85
|
+
scheduleReloadDeadline();
|
|
86
|
+
} else if (state === "Reloading") {
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
onUpstreamLost(reason) {
|
|
90
|
+
if (state !== "Ready" && state !== "Booting") return;
|
|
91
|
+
readyGen++;
|
|
92
|
+
transition("Reloading", { trigger: "upstream-lost", reason });
|
|
93
|
+
scheduleReloadDeadline();
|
|
94
|
+
},
|
|
95
|
+
async requeue(frame) {
|
|
96
|
+
if (frame.id !== void 0 && frame.id !== null) inflight.delete(frame.id);
|
|
97
|
+
if (state === "Ready") {
|
|
98
|
+
await machine.enqueue(frame);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
requeued.add(frame);
|
|
102
|
+
buffer.unshift(frame);
|
|
103
|
+
if (state === "Degraded" || state === "Stopping") await keepUnsent([]);
|
|
104
|
+
},
|
|
105
|
+
onWatcherEvent(trigger) {
|
|
106
|
+
if (state === "Stopping" || state === "Degraded") return;
|
|
107
|
+
log.reloadEvent("start", { trigger });
|
|
108
|
+
readyGen++;
|
|
109
|
+
transition("Reloading", { trigger });
|
|
110
|
+
void failInflightAsResponses("reload", { trigger });
|
|
111
|
+
scheduleReloadDeadline();
|
|
112
|
+
},
|
|
113
|
+
onReloadDeadline(detail) {
|
|
114
|
+
log.error("reload-deadline-elapsed", { bufferDepth: buffer.length });
|
|
115
|
+
transition("Degraded", { reason: "reload_deadline" });
|
|
116
|
+
degradedDetail = detail;
|
|
117
|
+
void flushBufferAsResponses(DEV_RELOAD_DEADLINE, "deadline", { deadlineMs: reloadDeadlineMs, ...detail });
|
|
118
|
+
},
|
|
119
|
+
async enqueue(frame) {
|
|
120
|
+
const isRequest = frame.id !== void 0 && frame.id !== null;
|
|
121
|
+
if (state === "Ready") {
|
|
122
|
+
if (isRequest) {
|
|
123
|
+
inflight.set(frame.id, { frame, forwardedAt: Date.now() });
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await forward(frame);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.error("forward-failed", { error: err.message });
|
|
129
|
+
if (isRequest && inflight.delete(frame.id)) {
|
|
130
|
+
await respond(makeDevError(frame.id ?? null, DEV_SERVER_UNREACHABLE, { reason: "forward_failed" }));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (state === "Degraded" || state === "Stopping") {
|
|
136
|
+
if (isRequest) {
|
|
137
|
+
await respond(
|
|
138
|
+
makeDevError(frame.id ?? null, DEV_SERVER_UNREACHABLE, {
|
|
139
|
+
reason: state === "Stopping" ? "stopping" : "degraded",
|
|
140
|
+
...state === "Degraded" ? degradedDetail : void 0
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
log.warn("drop-notification", {
|
|
145
|
+
method: frame.method,
|
|
146
|
+
reason: state === "Stopping" ? "stopping" : "degraded"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (buffer.length >= bufferSize) {
|
|
152
|
+
if (isRequest) {
|
|
153
|
+
await respond(makeDevError(frame.id ?? null, DEV_BUFFER_FULL, { capacity: bufferSize }));
|
|
154
|
+
} else {
|
|
155
|
+
log.warn("drop-notification", { method: frame.method, reason: "buffer_full" });
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
buffer.push(frame);
|
|
160
|
+
},
|
|
161
|
+
async relayUpstream(frame) {
|
|
162
|
+
if (frame.id !== void 0 && frame.id !== null && (frame.result !== void 0 || frame.error !== void 0)) {
|
|
163
|
+
inflight.delete(frame.id);
|
|
164
|
+
}
|
|
165
|
+
await respond(frame);
|
|
166
|
+
},
|
|
167
|
+
async stop() {
|
|
168
|
+
readyGen++;
|
|
169
|
+
transition("Stopping");
|
|
170
|
+
clearReloadTimer();
|
|
171
|
+
await failInflightAsResponses("stopping");
|
|
172
|
+
await flushBufferAsResponses(DEV_SERVER_UNREACHABLE, "stopping");
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
return machine;
|
|
176
|
+
async function keepUnsent(frames) {
|
|
177
|
+
if (state === "Ready") {
|
|
178
|
+
for (const frame of frames) await machine.enqueue(frame);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
let at = 0;
|
|
182
|
+
while (at < buffer.length && requeued.has(buffer[at])) at++;
|
|
183
|
+
buffer.splice(at, 0, ...frames);
|
|
184
|
+
if (state === "Stopping") await flushBufferAsResponses(DEV_SERVER_UNREACHABLE, "stopping");
|
|
185
|
+
if (state === "Degraded") await flushBufferAsResponses(DEV_RELOAD_DEADLINE, "deadline", degradedDetail);
|
|
186
|
+
}
|
|
187
|
+
function scheduleReloadDeadline() {
|
|
188
|
+
clearReloadTimer();
|
|
189
|
+
reloadTimer = setTimeout(() => {
|
|
190
|
+
if (state === "Reloading" || state === "Booting") {
|
|
191
|
+
log.error("reload-deadline-fired");
|
|
192
|
+
transition("Degraded", { reason: "reload_deadline" });
|
|
193
|
+
void (async () => {
|
|
194
|
+
while (buffer.length > 0) {
|
|
195
|
+
const f = buffer.shift();
|
|
196
|
+
if (!f) break;
|
|
197
|
+
const id = f.id ?? null;
|
|
198
|
+
if (id === null) {
|
|
199
|
+
log.warn("drop-notification", { method: f.method, reason: "deadline" });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
await respond(makeDevError(id, DEV_RELOAD_DEADLINE, { deadlineMs: reloadDeadlineMs }));
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
205
|
+
}
|
|
206
|
+
}, reloadDeadlineMs).unref();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export {
|
|
210
|
+
createBridgeStateMachine
|
|
211
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// libs/utils/src/stdio-bridge/stdio-framer.ts
|
|
2
|
+
import { makeDevError } from "./errors.mjs";
|
|
3
|
+
function createStdioFramer(options) {
|
|
4
|
+
const { input, output, log, onFrame, onClose } = options;
|
|
5
|
+
let buffer = "";
|
|
6
|
+
let paused = false;
|
|
7
|
+
let drainResolvers = [];
|
|
8
|
+
function flushBuffer() {
|
|
9
|
+
let nl;
|
|
10
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
11
|
+
const raw = buffer.slice(0, nl).replace(/\r$/, "");
|
|
12
|
+
buffer = buffer.slice(nl + 1);
|
|
13
|
+
if (raw.length === 0) continue;
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(raw);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
log.warn("parse-error", { raw: raw.slice(0, 200), error: err.message });
|
|
19
|
+
void write(makeDevError(null, -32700, { reason: "parse_error" }));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!parsed || typeof parsed !== "object") {
|
|
23
|
+
log.warn("parse-error", { raw: raw.slice(0, 200), reason: "not_object" });
|
|
24
|
+
void write(makeDevError(null, -32700, { reason: "not_object" }));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
void Promise.resolve(onFrame(parsed)).catch((err) => {
|
|
28
|
+
log.error("on-frame-error", {
|
|
29
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let closed = false;
|
|
35
|
+
function onInputClosed() {
|
|
36
|
+
if (closed) return;
|
|
37
|
+
closed = true;
|
|
38
|
+
onClose?.();
|
|
39
|
+
}
|
|
40
|
+
function onData(chunk) {
|
|
41
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
42
|
+
flushBuffer();
|
|
43
|
+
}
|
|
44
|
+
function write(frame) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const line = JSON.stringify(frame) + "\n";
|
|
47
|
+
const ok = output.write(line);
|
|
48
|
+
if (ok) return resolve();
|
|
49
|
+
if (!paused) {
|
|
50
|
+
paused = true;
|
|
51
|
+
input.pause?.();
|
|
52
|
+
}
|
|
53
|
+
drainResolvers.push(resolve);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function onDrain() {
|
|
57
|
+
if (paused) {
|
|
58
|
+
paused = false;
|
|
59
|
+
input.resume?.();
|
|
60
|
+
}
|
|
61
|
+
const resolvers = drainResolvers;
|
|
62
|
+
drainResolvers = [];
|
|
63
|
+
for (const r of resolvers) r();
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
start: () => {
|
|
67
|
+
input.setEncoding?.("utf-8");
|
|
68
|
+
input.on("data", onData);
|
|
69
|
+
input.on("end", onInputClosed);
|
|
70
|
+
input.on("close", onInputClosed);
|
|
71
|
+
output.on("drain", onDrain);
|
|
72
|
+
},
|
|
73
|
+
write,
|
|
74
|
+
stop: () => {
|
|
75
|
+
input.off("data", onData);
|
|
76
|
+
input.off("end", onInputClosed);
|
|
77
|
+
input.off("close", onInputClosed);
|
|
78
|
+
output.off("drain", onDrain);
|
|
79
|
+
if (paused) {
|
|
80
|
+
paused = false;
|
|
81
|
+
input.resume?.();
|
|
82
|
+
}
|
|
83
|
+
const resolvers = drainResolvers;
|
|
84
|
+
drainResolvers = [];
|
|
85
|
+
for (const r of resolvers) r();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
createStdioFramer
|
|
91
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// libs/utils/src/stdio-bridge/upstream-client.ts
|
|
2
|
+
var SESSION_HEADER = "mcp-session-id";
|
|
3
|
+
var END_SESSION_TIMEOUT_MS = 2e3;
|
|
4
|
+
var reinitCounter = 0;
|
|
5
|
+
function nextReinitId() {
|
|
6
|
+
reinitCounter += 1;
|
|
7
|
+
return `frontmcp-dev-bridge:reinitialize:${reinitCounter}`;
|
|
8
|
+
}
|
|
9
|
+
function initializedNotification() {
|
|
10
|
+
return { jsonrpc: "2.0", method: "notifications/initialized" };
|
|
11
|
+
}
|
|
12
|
+
function resultOf(frame, method) {
|
|
13
|
+
if (!frame) throw new Error(`no response to the replayed ${method}`);
|
|
14
|
+
if (frame.error) throw new Error(`replayed ${method} failed: ${frame.error.message}`);
|
|
15
|
+
return typeof frame.result === "object" && frame.result !== null ? frame.result : void 0;
|
|
16
|
+
}
|
|
17
|
+
function createHttpUpstream(options) {
|
|
18
|
+
const { log, url, onFrame } = options;
|
|
19
|
+
const abortControllers = /* @__PURE__ */ new Set();
|
|
20
|
+
let sessionId;
|
|
21
|
+
async function post(frame, deliver) {
|
|
22
|
+
const abortController = new AbortController();
|
|
23
|
+
abortControllers.add(abortController);
|
|
24
|
+
const signal = abortController.signal;
|
|
25
|
+
try {
|
|
26
|
+
const headers = {
|
|
27
|
+
...options.headers,
|
|
28
|
+
"content-type": "application/json",
|
|
29
|
+
accept: "application/json, text/event-stream"
|
|
30
|
+
};
|
|
31
|
+
if (sessionId && frame.method !== "initialize") headers[SESSION_HEADER] = sessionId;
|
|
32
|
+
const res = await (options.fetch ?? fetch)(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
redirect: "error",
|
|
35
|
+
headers,
|
|
36
|
+
body: JSON.stringify(frame),
|
|
37
|
+
signal
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
log.warn("http-upstream-non-ok", { status: res.status, method: frame.method });
|
|
41
|
+
throw new Error(`upstream returned ${res.status} for ${frame.method ?? "request"}`);
|
|
42
|
+
}
|
|
43
|
+
const issued = res.headers.get(SESSION_HEADER);
|
|
44
|
+
if (issued && frame.method === "initialize") {
|
|
45
|
+
sessionId = issued;
|
|
46
|
+
log.info("http-upstream-session");
|
|
47
|
+
}
|
|
48
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
49
|
+
if (contentType.includes("text/event-stream")) {
|
|
50
|
+
const reader = res.body?.getReader();
|
|
51
|
+
if (!reader) return;
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
let buffer = "";
|
|
54
|
+
while (true) {
|
|
55
|
+
const { done, value } = await reader.read();
|
|
56
|
+
if (done) break;
|
|
57
|
+
buffer += decoder.decode(value, { stream: true });
|
|
58
|
+
let nl;
|
|
59
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
60
|
+
const line = buffer.slice(0, nl).trim();
|
|
61
|
+
buffer = buffer.slice(nl + 1);
|
|
62
|
+
if (!line.startsWith("data:")) continue;
|
|
63
|
+
const json = line.slice(5).trim();
|
|
64
|
+
if (!json) continue;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(json);
|
|
67
|
+
await deliver(parsed);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
log.warn("sse-parse-error", { error: err.message, raw: json.slice(0, 200) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else if (contentType.includes("application/json")) {
|
|
74
|
+
const body = await res.json();
|
|
75
|
+
await deliver(body);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.name === "AbortError") {
|
|
79
|
+
log.info("http-upstream-aborted", { method: frame.method });
|
|
80
|
+
} else {
|
|
81
|
+
log.error("http-upstream-error", { error: err.message, method: frame.method });
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
} finally {
|
|
85
|
+
abortControllers.delete(abortController);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
send: (frame) => post(frame, onFrame),
|
|
90
|
+
async reinitialize(initialize, initialized) {
|
|
91
|
+
const id = nextReinitId();
|
|
92
|
+
let response;
|
|
93
|
+
await post({ ...initialize, id }, async (frame) => {
|
|
94
|
+
if (frame.id === id) response = frame;
|
|
95
|
+
else await onFrame(frame);
|
|
96
|
+
});
|
|
97
|
+
const result = resultOf(response, "initialize");
|
|
98
|
+
if (initialized) await post(initializedNotification(), onFrame);
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
async endSession() {
|
|
102
|
+
const ending = sessionId;
|
|
103
|
+
if (!ending) return;
|
|
104
|
+
sessionId = void 0;
|
|
105
|
+
try {
|
|
106
|
+
const res = await (options.fetch ?? fetch)(url, {
|
|
107
|
+
method: "DELETE",
|
|
108
|
+
redirect: "error",
|
|
109
|
+
headers: { ...options.headers, [SESSION_HEADER]: ending },
|
|
110
|
+
signal: AbortSignal.timeout(END_SESSION_TIMEOUT_MS)
|
|
111
|
+
});
|
|
112
|
+
await res.body?.cancel();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log.warn("http-upstream-end-session-failed", { error: err.message });
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
close: async () => {
|
|
118
|
+
for (const ac of abortControllers) ac.abort();
|
|
119
|
+
abortControllers.clear();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function createPipeUpstream(options) {
|
|
124
|
+
const { log, child, onFrame, onDevEvents, reinitializeTimeoutMs = 1e4 } = options;
|
|
125
|
+
const pending = /* @__PURE__ */ new Map();
|
|
126
|
+
function handleMessage(msg) {
|
|
127
|
+
if (!msg || typeof msg !== "object") {
|
|
128
|
+
log.warn("pipe-upstream-non-object", { type: typeof msg });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const control = msg;
|
|
132
|
+
if (control.__frontmcp === "dev-events" && Array.isArray(control.events)) onDevEvents?.(control.events);
|
|
133
|
+
if (control.__frontmcp !== void 0) return;
|
|
134
|
+
const frame = msg;
|
|
135
|
+
const waiter = typeof frame.id === "string" ? pending.get(frame.id) : void 0;
|
|
136
|
+
if (waiter) {
|
|
137
|
+
waiter(frame);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
void onFrame(frame);
|
|
141
|
+
}
|
|
142
|
+
child.on("message", handleMessage);
|
|
143
|
+
async function send(frame) {
|
|
144
|
+
if (!child.connected) {
|
|
145
|
+
log.warn("pipe-upstream-disconnected", { method: frame.method });
|
|
146
|
+
throw new Error(`pipe upstream disconnected for ${frame.method ?? "request"}`);
|
|
147
|
+
}
|
|
148
|
+
await new Promise((resolve, reject) => {
|
|
149
|
+
child.send(frame, (err) => {
|
|
150
|
+
if (err) reject(err);
|
|
151
|
+
else resolve();
|
|
152
|
+
});
|
|
153
|
+
}).catch((err) => {
|
|
154
|
+
log.warn("pipe-upstream-send-error", { error: err.message, method: frame.method });
|
|
155
|
+
throw err;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
send,
|
|
160
|
+
async reinitialize(initialize, initialized) {
|
|
161
|
+
const id = nextReinitId();
|
|
162
|
+
let timer;
|
|
163
|
+
const response = new Promise((resolve, reject) => {
|
|
164
|
+
pending.set(id, resolve);
|
|
165
|
+
timer = setTimeout(
|
|
166
|
+
() => reject(new Error(`no response to the replayed initialize within ${reinitializeTimeoutMs}ms`)),
|
|
167
|
+
reinitializeTimeoutMs
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
await send({ ...initialize, id });
|
|
172
|
+
const result = resultOf(await response, "initialize");
|
|
173
|
+
if (initialized) await send(initializedNotification());
|
|
174
|
+
return result;
|
|
175
|
+
} finally {
|
|
176
|
+
if (timer) clearTimeout(timer);
|
|
177
|
+
pending.delete(id);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
close: async () => {
|
|
181
|
+
child.off("message", handleMessage);
|
|
182
|
+
pending.clear();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
createHttpUpstream,
|
|
188
|
+
createPipeUpstream
|
|
189
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/base.ts
|
|
2
|
+
import { StorageNotConnectedError, StorageNotSupportedError } from "../errors.mjs";
|
|
3
|
+
import { validateOptionalTTL } from "../utils/ttl.mjs";
|
|
4
|
+
var BaseStorageAdapter = class {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the adapter is currently connected.
|
|
7
|
+
*/
|
|
8
|
+
connected = false;
|
|
9
|
+
/**
|
|
10
|
+
* Ensure adapter is connected before operations.
|
|
11
|
+
* @throws StorageNotConnectedError if not connected
|
|
12
|
+
*/
|
|
13
|
+
ensureConnected() {
|
|
14
|
+
if (!this.connected) {
|
|
15
|
+
throw new StorageNotConnectedError(this.backendName);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Set with validation.
|
|
20
|
+
*/
|
|
21
|
+
async set(key, value, options) {
|
|
22
|
+
this.ensureConnected();
|
|
23
|
+
this.validateSetOptions(options);
|
|
24
|
+
return this.doSet(key, value, options);
|
|
25
|
+
}
|
|
26
|
+
// ============================================
|
|
27
|
+
// Batch Operations (default implementations)
|
|
28
|
+
// ============================================
|
|
29
|
+
/**
|
|
30
|
+
* Default mget: sequential gets.
|
|
31
|
+
* Override for more efficient implementations.
|
|
32
|
+
*/
|
|
33
|
+
async mget(keys) {
|
|
34
|
+
this.ensureConnected();
|
|
35
|
+
return Promise.all(keys.map((k) => this.get(k)));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Default mset: sequential sets.
|
|
39
|
+
* Override for atomic/pipelined implementations.
|
|
40
|
+
*/
|
|
41
|
+
async mset(entries) {
|
|
42
|
+
this.ensureConnected();
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
this.validateSetOptions(entry.options);
|
|
45
|
+
}
|
|
46
|
+
await Promise.all(entries.map((e) => this.doSet(e.key, e.value, e.options)));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Default mdelete: sequential deletes.
|
|
50
|
+
* Override for more efficient implementations.
|
|
51
|
+
*/
|
|
52
|
+
async mdelete(keys) {
|
|
53
|
+
this.ensureConnected();
|
|
54
|
+
const results = await Promise.all(keys.map((k) => this.delete(k)));
|
|
55
|
+
return results.filter(Boolean).length;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Default count: keys().length.
|
|
59
|
+
* Override for more efficient implementations.
|
|
60
|
+
*/
|
|
61
|
+
async count(pattern = "*") {
|
|
62
|
+
const matchedKeys = await this.keys(pattern);
|
|
63
|
+
return matchedKeys.length;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Default: not supported, since a read followed by a delete is not atomic.
|
|
67
|
+
* Override in adapters whose backend can compare and delete in one step.
|
|
68
|
+
*/
|
|
69
|
+
async deleteIfEquals(_key, _expectedValue) {
|
|
70
|
+
throw new StorageNotSupportedError(
|
|
71
|
+
"deleteIfEquals",
|
|
72
|
+
this.backendName,
|
|
73
|
+
"Use the memory, Redis, Upstash, Vercel KV or SQLite adapter for an atomic compare-and-delete."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
/** Default: not supported, for the same reason as {@link deleteIfEquals}. */
|
|
77
|
+
async expireIfEquals(_key, _expectedValue, _ttlSeconds) {
|
|
78
|
+
throw new StorageNotSupportedError(
|
|
79
|
+
"expireIfEquals",
|
|
80
|
+
this.backendName,
|
|
81
|
+
"Use the memory, Redis, Upstash, Vercel KV or SQLite adapter for an atomic compare-and-expire."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
/** Default: not supported, for the same reason as {@link deleteIfEquals}. */
|
|
85
|
+
async setIfEquals(_key, _expectedValue, _value) {
|
|
86
|
+
throw new StorageNotSupportedError(
|
|
87
|
+
"setIfEquals",
|
|
88
|
+
this.backendName,
|
|
89
|
+
"Use the memory, Redis or SQLite adapter for an atomic compare-and-set."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// ============================================
|
|
93
|
+
// Pub/Sub (default: not supported)
|
|
94
|
+
// ============================================
|
|
95
|
+
/**
|
|
96
|
+
* Default: pub/sub not supported.
|
|
97
|
+
* Override in adapters that support it.
|
|
98
|
+
*/
|
|
99
|
+
supportsPubSub() {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Default: throw not supported error.
|
|
104
|
+
* Override in adapters that support pub/sub.
|
|
105
|
+
*/
|
|
106
|
+
async publish(_channel, _message) {
|
|
107
|
+
throw new StorageNotSupportedError("publish", this.backendName, this.getPubSubSuggestion());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Default: throw not supported error.
|
|
111
|
+
* Override in adapters that support pub/sub.
|
|
112
|
+
*/
|
|
113
|
+
async subscribe(_channel, _handler) {
|
|
114
|
+
throw new StorageNotSupportedError("subscribe", this.backendName, this.getPubSubSuggestion());
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get suggestion message for pub/sub not supported error.
|
|
118
|
+
* Override in specific adapters.
|
|
119
|
+
*/
|
|
120
|
+
getPubSubSuggestion() {
|
|
121
|
+
return "Use Redis or Upstash adapter for pub/sub support.";
|
|
122
|
+
}
|
|
123
|
+
// ============================================
|
|
124
|
+
// Validation Helpers
|
|
125
|
+
// ============================================
|
|
126
|
+
/**
|
|
127
|
+
* Validate set options.
|
|
128
|
+
*/
|
|
129
|
+
validateSetOptions(options) {
|
|
130
|
+
if (!options) return;
|
|
131
|
+
validateOptionalTTL(options.ttlSeconds);
|
|
132
|
+
if (options.ifNotExists && options.ifExists) {
|
|
133
|
+
throw new Error("ifNotExists and ifExists are mutually exclusive");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
export {
|
|
138
|
+
BaseStorageAdapter
|
|
139
|
+
};
|