@frontmcp/utils 1.8.3 → 1.8.5
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/async-context/browser-async-context.d.ts +24 -6
- package/async-context/browser-async-context.js +179 -14
- package/async-context/index.d.ts +1 -1
- package/async-context/node-async-context.d.ts +11 -0
- package/async-context/node-async-context.js +29 -2
- package/async-context/overlap-error.d.ts +8 -0
- package/esm/async-context/browser-async-context.mjs +172 -13
- package/esm/async-context/node-async-context.mjs +24 -1
- package/esm/index.mjs +11 -1
- package/index.d.ts +1 -1
- package/index.js +8 -0
- package/package.json +2 -2
|
@@ -1,12 +1,30 @@
|
|
|
1
|
+
export { AsyncContextOverlapError } from './overlap-error';
|
|
2
|
+
export type AsyncContextMode = 'native' | 'serialized';
|
|
3
|
+
export declare function getAsyncContextMode(): AsyncContextMode;
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* browser environments. Does not support `enterWith()` or `disable()`
|
|
6
|
-
* (neither is used in the FrontMCP codebase).
|
|
5
|
+
* Run one top-level request. With native `AsyncContext` it only calls `fn`. Without it, the request
|
|
6
|
+
* waits for its turn, runs with its own stacks, and keeps the turn until `fn` settles and every
|
|
7
|
+
* store it opened has unwound, so work it left running cannot read the next request's context.
|
|
7
8
|
*/
|
|
9
|
+
export declare function runRequestExclusive<R>(fn: () => R | Promise<R>): Promise<R>;
|
|
10
|
+
/**
|
|
11
|
+
* Await something the running request waits on from outside, such as its client's answer to an
|
|
12
|
+
* elicitation. Without native `AsyncContext` the request steps aside while it waits, so the client
|
|
13
|
+
* can call the server before answering, and takes its turn back afterwards. Outside a request, or
|
|
14
|
+
* with native `AsyncContext`, it only awaits.
|
|
15
|
+
*/
|
|
16
|
+
export declare function awaitOutsideRequest<T>(promise: PromiseLike<T>): Promise<T>;
|
|
8
17
|
export declare class AsyncLocalStorage<T> {
|
|
9
|
-
private
|
|
18
|
+
private readonly native?;
|
|
19
|
+
private readonly stacks;
|
|
20
|
+
constructor();
|
|
10
21
|
run<R>(store: T, callback: (...args: unknown[]) => R, ...args: unknown[]): R;
|
|
11
22
|
getStore(): T | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Remove `frame` from its request's stack. Returns false when it was not the innermost frame: a
|
|
25
|
+
* later run of this storage in the same request was still open, so the two overlapped and reads
|
|
26
|
+
* in between may have seen the other run's store.
|
|
27
|
+
*/
|
|
28
|
+
private leave;
|
|
29
|
+
private overlapError;
|
|
12
30
|
}
|
|
@@ -20,30 +20,195 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// libs/utils/src/async-context/browser-async-context.ts
|
|
21
21
|
var browser_async_context_exports = {};
|
|
22
22
|
__export(browser_async_context_exports, {
|
|
23
|
-
|
|
23
|
+
AsyncContextOverlapError: () => AsyncContextOverlapError,
|
|
24
|
+
AsyncLocalStorage: () => AsyncLocalStorage,
|
|
25
|
+
awaitOutsideRequest: () => awaitOutsideRequest,
|
|
26
|
+
getAsyncContextMode: () => getAsyncContextMode,
|
|
27
|
+
runRequestExclusive: () => runRequestExclusive
|
|
24
28
|
});
|
|
25
29
|
module.exports = __toCommonJS(browser_async_context_exports);
|
|
30
|
+
|
|
31
|
+
// libs/utils/src/async-context/overlap-error.ts
|
|
32
|
+
var AsyncContextOverlapError = class extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "AsyncContextOverlapError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// libs/utils/src/async-context/browser-async-context.ts
|
|
40
|
+
var nativeAsyncContext = (() => {
|
|
41
|
+
const candidate = globalThis.AsyncContext;
|
|
42
|
+
return typeof candidate?.Variable === "function" ? candidate : void 0;
|
|
43
|
+
})();
|
|
44
|
+
function getAsyncContextMode() {
|
|
45
|
+
return nativeAsyncContext ? "native" : "serialized";
|
|
46
|
+
}
|
|
47
|
+
var SLOW_TURN_WARNING_MS = 1e4;
|
|
48
|
+
var RequestSlot = class {
|
|
49
|
+
/** Frames of every storage that are still running in this request. */
|
|
50
|
+
live = 0;
|
|
51
|
+
/** Storages whose runs overlapped in this request; they refuse to answer until they unwind. */
|
|
52
|
+
tainted = /* @__PURE__ */ new Set();
|
|
53
|
+
idleWaiters = [];
|
|
54
|
+
enter() {
|
|
55
|
+
this.live++;
|
|
56
|
+
}
|
|
57
|
+
leave() {
|
|
58
|
+
this.live--;
|
|
59
|
+
if (this.live === 0) {
|
|
60
|
+
for (const resolve of this.idleWaiters.splice(0)) resolve();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Resolves once no frame of this request is running any more. */
|
|
64
|
+
idle() {
|
|
65
|
+
return this.live === 0 ? Promise.resolve() : new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var ROOT = new RequestSlot();
|
|
69
|
+
var active = ROOT;
|
|
70
|
+
var owner;
|
|
71
|
+
var turnQueue = [];
|
|
72
|
+
var slowTurnWarned = false;
|
|
73
|
+
var overlapReported = false;
|
|
74
|
+
function takeTurn(slot) {
|
|
75
|
+
if (!owner) {
|
|
76
|
+
owner = slot;
|
|
77
|
+
active = slot;
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
}
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
if (slowTurnWarned) return;
|
|
83
|
+
slowTurnWarned = true;
|
|
84
|
+
console.warn(
|
|
85
|
+
"[frontmcp] A request has waited more than 10s for another one to finish. This runtime has no AsyncContext, so FrontMCP runs requests one at a time to keep their contexts apart. A tool that calls its own server through a client (instead of this.scope flows) waits for itself."
|
|
86
|
+
);
|
|
87
|
+
}, SLOW_TURN_WARNING_MS);
|
|
88
|
+
timer.unref?.();
|
|
89
|
+
turnQueue.push({
|
|
90
|
+
slot,
|
|
91
|
+
start: () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function passTurn(slot) {
|
|
99
|
+
if (owner !== slot) return;
|
|
100
|
+
const next = turnQueue.shift();
|
|
101
|
+
if (next) {
|
|
102
|
+
owner = next.slot;
|
|
103
|
+
active = next.slot;
|
|
104
|
+
next.start();
|
|
105
|
+
} else {
|
|
106
|
+
owner = void 0;
|
|
107
|
+
active = ROOT;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function runRequestExclusive(fn) {
|
|
111
|
+
if (nativeAsyncContext) return fn();
|
|
112
|
+
const slot = new RequestSlot();
|
|
113
|
+
await takeTurn(slot);
|
|
114
|
+
try {
|
|
115
|
+
return await fn();
|
|
116
|
+
} finally {
|
|
117
|
+
await slot.idle();
|
|
118
|
+
passTurn(slot);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function awaitOutsideRequest(promise) {
|
|
122
|
+
if (nativeAsyncContext) return promise;
|
|
123
|
+
const slot = owner;
|
|
124
|
+
if (!slot || active !== slot) return promise;
|
|
125
|
+
passTurn(slot);
|
|
126
|
+
try {
|
|
127
|
+
return await promise;
|
|
128
|
+
} finally {
|
|
129
|
+
await takeTurn(slot);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
26
132
|
var AsyncLocalStorage = class {
|
|
27
|
-
|
|
133
|
+
native;
|
|
134
|
+
stacks = /* @__PURE__ */ new WeakMap();
|
|
135
|
+
constructor() {
|
|
136
|
+
if (nativeAsyncContext) this.native = new nativeAsyncContext.Variable();
|
|
137
|
+
}
|
|
28
138
|
run(store, callback, ...args) {
|
|
29
|
-
this.
|
|
139
|
+
if (this.native) return this.native.run(store, callback, ...args);
|
|
140
|
+
const slot = active;
|
|
141
|
+
let stack = this.stacks.get(slot);
|
|
142
|
+
if (!stack) {
|
|
143
|
+
stack = [];
|
|
144
|
+
this.stacks.set(slot, stack);
|
|
145
|
+
}
|
|
146
|
+
const frame = { store };
|
|
147
|
+
stack.push(frame);
|
|
148
|
+
slot.enter();
|
|
149
|
+
let result;
|
|
30
150
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.stack.pop();
|
|
36
|
-
return result;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
this.stack.pop();
|
|
39
|
-
throw err;
|
|
151
|
+
result = callback(...args);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.leave(slot, frame);
|
|
154
|
+
throw error;
|
|
40
155
|
}
|
|
156
|
+
if (result instanceof Promise) {
|
|
157
|
+
return result.then(
|
|
158
|
+
(value) => {
|
|
159
|
+
if (!this.leave(slot, frame)) throw this.overlapError();
|
|
160
|
+
return value;
|
|
161
|
+
},
|
|
162
|
+
(error) => {
|
|
163
|
+
this.leave(slot, frame);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
this.leave(slot, frame);
|
|
169
|
+
return result;
|
|
41
170
|
}
|
|
42
171
|
getStore() {
|
|
43
|
-
|
|
172
|
+
if (this.native) return this.native.get();
|
|
173
|
+
const slot = active;
|
|
174
|
+
if (slot.tainted.has(this)) throw this.overlapError();
|
|
175
|
+
const stack = this.stacks.get(slot);
|
|
176
|
+
return stack && stack.length > 0 ? stack[stack.length - 1].store : void 0;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Remove `frame` from its request's stack. Returns false when it was not the innermost frame: a
|
|
180
|
+
* later run of this storage in the same request was still open, so the two overlapped and reads
|
|
181
|
+
* in between may have seen the other run's store.
|
|
182
|
+
*/
|
|
183
|
+
leave(slot, frame) {
|
|
184
|
+
const stack = this.stacks.get(slot) ?? [];
|
|
185
|
+
const index = stack.lastIndexOf(frame);
|
|
186
|
+
const innermost = index === stack.length - 1;
|
|
187
|
+
if (index !== -1) stack.splice(index, 1);
|
|
188
|
+
if (!innermost) {
|
|
189
|
+
slot.tainted.add(this);
|
|
190
|
+
if (!overlapReported) {
|
|
191
|
+
overlapReported = true;
|
|
192
|
+
console.error(
|
|
193
|
+
"[frontmcp] Two concurrent runs of one async context overlapped inside a request. This runtime has no AsyncContext, so the context of each cannot be told apart; reads fail for the rest of that request instead of returning the wrong value."
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (stack.length === 0) slot.tainted.delete(this);
|
|
198
|
+
slot.leave();
|
|
199
|
+
return innermost;
|
|
200
|
+
}
|
|
201
|
+
overlapError() {
|
|
202
|
+
return new AsyncContextOverlapError(
|
|
203
|
+
"Concurrent runs of an async context overlapped in one request, and this runtime has no AsyncContext to tell them apart. Run the calls one after another, or use a runtime with AsyncContext."
|
|
204
|
+
);
|
|
44
205
|
}
|
|
45
206
|
};
|
|
46
207
|
// Annotate the CommonJS export names for ESM import in node:
|
|
47
208
|
0 && (module.exports = {
|
|
48
|
-
|
|
209
|
+
AsyncContextOverlapError,
|
|
210
|
+
AsyncLocalStorage,
|
|
211
|
+
awaitOutsideRequest,
|
|
212
|
+
getAsyncContextMode,
|
|
213
|
+
runRequestExclusive
|
|
49
214
|
});
|
package/async-context/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { AsyncLocalStorage } from '#async-context';
|
|
1
|
+
export { AsyncContextOverlapError, AsyncLocalStorage, awaitOutsideRequest, getAsyncContextMode, runRequestExclusive, type AsyncContextMode, } from '#async-context';
|
|
@@ -1 +1,12 @@
|
|
|
1
1
|
export { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
export { AsyncContextOverlapError } from './overlap-error';
|
|
3
|
+
/** How the runtime keeps async context apart: Node and Workers propagate it natively. */
|
|
4
|
+
export type AsyncContextMode = 'native' | 'serialized';
|
|
5
|
+
export declare function getAsyncContextMode(): AsyncContextMode;
|
|
6
|
+
/**
|
|
7
|
+
* Run one top-level request. Node propagates `AsyncLocalStorage` across awaits, so requests may
|
|
8
|
+
* overlap freely and this only calls `fn`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runRequestExclusive<R>(fn: () => R | Promise<R>): Promise<R>;
|
|
11
|
+
/** Await something a request waits on from outside (a client's answer). A pass-through on Node. */
|
|
12
|
+
export declare function awaitOutsideRequest<T>(promise: PromiseLike<T>): Promise<T>;
|
|
@@ -20,11 +20,38 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// libs/utils/src/async-context/node-async-context.ts
|
|
21
21
|
var node_async_context_exports = {};
|
|
22
22
|
__export(node_async_context_exports, {
|
|
23
|
-
|
|
23
|
+
AsyncContextOverlapError: () => AsyncContextOverlapError,
|
|
24
|
+
AsyncLocalStorage: () => import_node_async_hooks.AsyncLocalStorage,
|
|
25
|
+
awaitOutsideRequest: () => awaitOutsideRequest,
|
|
26
|
+
getAsyncContextMode: () => getAsyncContextMode,
|
|
27
|
+
runRequestExclusive: () => runRequestExclusive
|
|
24
28
|
});
|
|
25
29
|
module.exports = __toCommonJS(node_async_context_exports);
|
|
26
30
|
var import_node_async_hooks = require("node:async_hooks");
|
|
31
|
+
|
|
32
|
+
// libs/utils/src/async-context/overlap-error.ts
|
|
33
|
+
var AsyncContextOverlapError = class extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "AsyncContextOverlapError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// libs/utils/src/async-context/node-async-context.ts
|
|
41
|
+
function getAsyncContextMode() {
|
|
42
|
+
return "native";
|
|
43
|
+
}
|
|
44
|
+
async function runRequestExclusive(fn) {
|
|
45
|
+
return fn();
|
|
46
|
+
}
|
|
47
|
+
async function awaitOutsideRequest(promise) {
|
|
48
|
+
return promise;
|
|
49
|
+
}
|
|
27
50
|
// Annotate the CommonJS export names for ESM import in node:
|
|
28
51
|
0 && (module.exports = {
|
|
29
|
-
|
|
52
|
+
AsyncContextOverlapError,
|
|
53
|
+
AsyncLocalStorage,
|
|
54
|
+
awaitOutsideRequest,
|
|
55
|
+
getAsyncContextMode,
|
|
56
|
+
runRequestExclusive
|
|
30
57
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by the browser `AsyncLocalStorage` when two concurrent runs of one storage in the same
|
|
3
|
+
* request overlapped. Without a runtime-provided async context the storage cannot tell which of them
|
|
4
|
+
* a later read belongs to, so it refuses to answer instead of returning the other run's value.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AsyncContextOverlapError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
@@ -1,26 +1,185 @@
|
|
|
1
1
|
import { createRequire as __frontmcpCreateRequire } from 'module';
|
|
2
2
|
const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
|
|
3
|
+
// libs/utils/src/async-context/overlap-error.ts
|
|
4
|
+
var AsyncContextOverlapError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "AsyncContextOverlapError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
3
11
|
// libs/utils/src/async-context/browser-async-context.ts
|
|
12
|
+
var nativeAsyncContext = (() => {
|
|
13
|
+
const candidate = globalThis.AsyncContext;
|
|
14
|
+
return typeof candidate?.Variable === "function" ? candidate : void 0;
|
|
15
|
+
})();
|
|
16
|
+
function getAsyncContextMode() {
|
|
17
|
+
return nativeAsyncContext ? "native" : "serialized";
|
|
18
|
+
}
|
|
19
|
+
var SLOW_TURN_WARNING_MS = 1e4;
|
|
20
|
+
var RequestSlot = class {
|
|
21
|
+
/** Frames of every storage that are still running in this request. */
|
|
22
|
+
live = 0;
|
|
23
|
+
/** Storages whose runs overlapped in this request; they refuse to answer until they unwind. */
|
|
24
|
+
tainted = /* @__PURE__ */ new Set();
|
|
25
|
+
idleWaiters = [];
|
|
26
|
+
enter() {
|
|
27
|
+
this.live++;
|
|
28
|
+
}
|
|
29
|
+
leave() {
|
|
30
|
+
this.live--;
|
|
31
|
+
if (this.live === 0) {
|
|
32
|
+
for (const resolve of this.idleWaiters.splice(0)) resolve();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Resolves once no frame of this request is running any more. */
|
|
36
|
+
idle() {
|
|
37
|
+
return this.live === 0 ? Promise.resolve() : new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var ROOT = new RequestSlot();
|
|
41
|
+
var active = ROOT;
|
|
42
|
+
var owner;
|
|
43
|
+
var turnQueue = [];
|
|
44
|
+
var slowTurnWarned = false;
|
|
45
|
+
var overlapReported = false;
|
|
46
|
+
function takeTurn(slot) {
|
|
47
|
+
if (!owner) {
|
|
48
|
+
owner = slot;
|
|
49
|
+
active = slot;
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
if (slowTurnWarned) return;
|
|
55
|
+
slowTurnWarned = true;
|
|
56
|
+
console.warn(
|
|
57
|
+
"[frontmcp] A request has waited more than 10s for another one to finish. This runtime has no AsyncContext, so FrontMCP runs requests one at a time to keep their contexts apart. A tool that calls its own server through a client (instead of this.scope flows) waits for itself."
|
|
58
|
+
);
|
|
59
|
+
}, SLOW_TURN_WARNING_MS);
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
turnQueue.push({
|
|
62
|
+
slot,
|
|
63
|
+
start: () => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function passTurn(slot) {
|
|
71
|
+
if (owner !== slot) return;
|
|
72
|
+
const next = turnQueue.shift();
|
|
73
|
+
if (next) {
|
|
74
|
+
owner = next.slot;
|
|
75
|
+
active = next.slot;
|
|
76
|
+
next.start();
|
|
77
|
+
} else {
|
|
78
|
+
owner = void 0;
|
|
79
|
+
active = ROOT;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function runRequestExclusive(fn) {
|
|
83
|
+
if (nativeAsyncContext) return fn();
|
|
84
|
+
const slot = new RequestSlot();
|
|
85
|
+
await takeTurn(slot);
|
|
86
|
+
try {
|
|
87
|
+
return await fn();
|
|
88
|
+
} finally {
|
|
89
|
+
await slot.idle();
|
|
90
|
+
passTurn(slot);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function awaitOutsideRequest(promise) {
|
|
94
|
+
if (nativeAsyncContext) return promise;
|
|
95
|
+
const slot = owner;
|
|
96
|
+
if (!slot || active !== slot) return promise;
|
|
97
|
+
passTurn(slot);
|
|
98
|
+
try {
|
|
99
|
+
return await promise;
|
|
100
|
+
} finally {
|
|
101
|
+
await takeTurn(slot);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
4
104
|
var AsyncLocalStorage = class {
|
|
5
|
-
|
|
105
|
+
native;
|
|
106
|
+
stacks = /* @__PURE__ */ new WeakMap();
|
|
107
|
+
constructor() {
|
|
108
|
+
if (nativeAsyncContext) this.native = new nativeAsyncContext.Variable();
|
|
109
|
+
}
|
|
6
110
|
run(store, callback, ...args) {
|
|
7
|
-
this.
|
|
111
|
+
if (this.native) return this.native.run(store, callback, ...args);
|
|
112
|
+
const slot = active;
|
|
113
|
+
let stack = this.stacks.get(slot);
|
|
114
|
+
if (!stack) {
|
|
115
|
+
stack = [];
|
|
116
|
+
this.stacks.set(slot, stack);
|
|
117
|
+
}
|
|
118
|
+
const frame = { store };
|
|
119
|
+
stack.push(frame);
|
|
120
|
+
slot.enter();
|
|
121
|
+
let result;
|
|
8
122
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.stack.pop();
|
|
14
|
-
return result;
|
|
15
|
-
} catch (err) {
|
|
16
|
-
this.stack.pop();
|
|
17
|
-
throw err;
|
|
123
|
+
result = callback(...args);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.leave(slot, frame);
|
|
126
|
+
throw error;
|
|
18
127
|
}
|
|
128
|
+
if (result instanceof Promise) {
|
|
129
|
+
return result.then(
|
|
130
|
+
(value) => {
|
|
131
|
+
if (!this.leave(slot, frame)) throw this.overlapError();
|
|
132
|
+
return value;
|
|
133
|
+
},
|
|
134
|
+
(error) => {
|
|
135
|
+
this.leave(slot, frame);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
this.leave(slot, frame);
|
|
141
|
+
return result;
|
|
19
142
|
}
|
|
20
143
|
getStore() {
|
|
21
|
-
|
|
144
|
+
if (this.native) return this.native.get();
|
|
145
|
+
const slot = active;
|
|
146
|
+
if (slot.tainted.has(this)) throw this.overlapError();
|
|
147
|
+
const stack = this.stacks.get(slot);
|
|
148
|
+
return stack && stack.length > 0 ? stack[stack.length - 1].store : void 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Remove `frame` from its request's stack. Returns false when it was not the innermost frame: a
|
|
152
|
+
* later run of this storage in the same request was still open, so the two overlapped and reads
|
|
153
|
+
* in between may have seen the other run's store.
|
|
154
|
+
*/
|
|
155
|
+
leave(slot, frame) {
|
|
156
|
+
const stack = this.stacks.get(slot) ?? [];
|
|
157
|
+
const index = stack.lastIndexOf(frame);
|
|
158
|
+
const innermost = index === stack.length - 1;
|
|
159
|
+
if (index !== -1) stack.splice(index, 1);
|
|
160
|
+
if (!innermost) {
|
|
161
|
+
slot.tainted.add(this);
|
|
162
|
+
if (!overlapReported) {
|
|
163
|
+
overlapReported = true;
|
|
164
|
+
console.error(
|
|
165
|
+
"[frontmcp] Two concurrent runs of one async context overlapped inside a request. This runtime has no AsyncContext, so the context of each cannot be told apart; reads fail for the rest of that request instead of returning the wrong value."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (stack.length === 0) slot.tainted.delete(this);
|
|
170
|
+
slot.leave();
|
|
171
|
+
return innermost;
|
|
172
|
+
}
|
|
173
|
+
overlapError() {
|
|
174
|
+
return new AsyncContextOverlapError(
|
|
175
|
+
"Concurrent runs of an async context overlapped in one request, and this runtime has no AsyncContext to tell them apart. Run the calls one after another, or use a runtime with AsyncContext."
|
|
176
|
+
);
|
|
22
177
|
}
|
|
23
178
|
};
|
|
24
179
|
export {
|
|
25
|
-
|
|
180
|
+
AsyncContextOverlapError,
|
|
181
|
+
AsyncLocalStorage,
|
|
182
|
+
awaitOutsideRequest,
|
|
183
|
+
getAsyncContextMode,
|
|
184
|
+
runRequestExclusive
|
|
26
185
|
};
|
|
@@ -2,6 +2,29 @@ import { createRequire as __frontmcpCreateRequire } from 'module';
|
|
|
2
2
|
const require = __frontmcpCreateRequire(import.meta.url || 'file:///');
|
|
3
3
|
// libs/utils/src/async-context/node-async-context.ts
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
|
|
6
|
+
// libs/utils/src/async-context/overlap-error.ts
|
|
7
|
+
var AsyncContextOverlapError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "AsyncContextOverlapError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// libs/utils/src/async-context/node-async-context.ts
|
|
15
|
+
function getAsyncContextMode() {
|
|
16
|
+
return "native";
|
|
17
|
+
}
|
|
18
|
+
async function runRequestExclusive(fn) {
|
|
19
|
+
return fn();
|
|
20
|
+
}
|
|
21
|
+
async function awaitOutsideRequest(promise) {
|
|
22
|
+
return promise;
|
|
23
|
+
}
|
|
5
24
|
export {
|
|
6
|
-
|
|
25
|
+
AsyncContextOverlapError,
|
|
26
|
+
AsyncLocalStorage,
|
|
27
|
+
awaitOutsideRequest,
|
|
28
|
+
getAsyncContextMode,
|
|
29
|
+
runRequestExclusive
|
|
7
30
|
};
|
package/esm/index.mjs
CHANGED
|
@@ -4786,7 +4786,13 @@ function safeStringify(value, space) {
|
|
|
4786
4786
|
init_crypto();
|
|
4787
4787
|
|
|
4788
4788
|
// libs/utils/src/async-context/index.ts
|
|
4789
|
-
import {
|
|
4789
|
+
import {
|
|
4790
|
+
AsyncContextOverlapError,
|
|
4791
|
+
AsyncLocalStorage,
|
|
4792
|
+
awaitOutsideRequest,
|
|
4793
|
+
getAsyncContextMode,
|
|
4794
|
+
runRequestExclusive
|
|
4795
|
+
} from "#async-context";
|
|
4790
4796
|
|
|
4791
4797
|
// libs/utils/src/event-emitter/index.ts
|
|
4792
4798
|
import { EventEmitter as EventEmitter2 } from "#event-emitter";
|
|
@@ -5866,6 +5872,7 @@ function setMachineIdOverride(id) {
|
|
|
5866
5872
|
machineIdOverride = id;
|
|
5867
5873
|
}
|
|
5868
5874
|
export {
|
|
5875
|
+
AsyncContextOverlapError,
|
|
5869
5876
|
AsyncLocalStorage,
|
|
5870
5877
|
BaseStorageAdapter,
|
|
5871
5878
|
DEFAULT_CODE_VERIFIER_LENGTH,
|
|
@@ -5902,6 +5909,7 @@ export {
|
|
|
5902
5909
|
assertNode,
|
|
5903
5910
|
asymmetricAlgSchema,
|
|
5904
5911
|
asymmetricKeyDataSchema,
|
|
5912
|
+
awaitOutsideRequest,
|
|
5905
5913
|
base64Decode,
|
|
5906
5914
|
base64Encode,
|
|
5907
5915
|
base64urlDecode,
|
|
@@ -5960,6 +5968,7 @@ export {
|
|
|
5960
5968
|
generatePkcePair,
|
|
5961
5969
|
generateRsaKeyPair2 as generateRsaKeyPair,
|
|
5962
5970
|
generateSecret,
|
|
5971
|
+
getAsyncContextMode,
|
|
5963
5972
|
getCookie,
|
|
5964
5973
|
getCrypto,
|
|
5965
5974
|
getCwd2 as getCwd,
|
|
@@ -6039,6 +6048,7 @@ export {
|
|
|
6039
6048
|
rsaVerify2 as rsaVerify,
|
|
6040
6049
|
rsaVerifySync2 as rsaVerifySync,
|
|
6041
6050
|
runCmd,
|
|
6051
|
+
runRequestExclusive,
|
|
6042
6052
|
safeExec,
|
|
6043
6053
|
safeJsonForScript,
|
|
6044
6054
|
safeMatch,
|
package/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { escapeHtml, escapeHtmlAttr, escapeJsString, escapeScriptClose, safeJson
|
|
|
21
21
|
export { safeStringify } from './serialization';
|
|
22
22
|
export { getCrypto, rsaVerify, rsaVerifySync, pemToPublicJwk, generateRsaKeyPair, rsaSignBase64Url, type RsaJwk, type RsaKeyPair, jwtAlgToNodeAlg, isRsaPssAlg, randomUUID, randomBytes, sha256, sha256Hex, sha256Base64url, hmacSha256, hkdfSha256, encryptAesGcm, decryptAesGcm, timingSafeEqual, bytesToHex, base64urlEncode, base64urlDecode, base64Encode, base64Decode, isNode, isBrowser, assertNode, EncryptedBlobError, encryptValue, decryptValue, tryDecryptValue, serializeBlob, deserializeBlob, tryDeserializeBlob, isValidEncryptedBlob, encryptAndSerialize, deserializeAndDecrypt, tryDeserializeAndDecrypt, generateCodeVerifier, generateCodeChallenge, verifyCodeChallenge, generatePkcePair, isValidCodeVerifier, isValidCodeChallenge, MIN_CODE_VERIFIER_LENGTH, MAX_CODE_VERIFIER_LENGTH, DEFAULT_CODE_VERIFIER_LENGTH, PkceError, type PkcePair, type SecretData, type SecretPersistenceOptions, type SecretValidationResult, secretDataSchema, validateSecretData, parseSecretData, isSecretPersistenceEnabled, resolveSecretPath, loadSecret, saveSecret, deleteSecret, generateSecret, createSecretData, getOrCreateSecret, clearCachedSecret, isSecretCached, type SignedData, type HmacSigningConfig, signData, verifyData, isSignedData, verifyOrParseData, type BaseKeyData, type SecretKeyData, type AsymmetricKeyData, type AnyKeyData, type KeyPersistenceOptions, type CreateKeyPersistenceOptions, type CreateSecretOptions, type CreateAsymmetricOptions, type KeyValidationResult, asymmetricAlgSchema, secretKeyDataSchema, asymmetricKeyDataSchema, anyKeyDataSchema, validateKeyData, parseKeyData, isSecretKeyData, isAsymmetricKeyData, KeyPersistence, createKeyPersistence, createKeyPersistenceWithStorage, } from './crypto';
|
|
23
23
|
export type { CryptoProvider, EncBlob, EncryptedBlob } from './crypto';
|
|
24
|
-
export { AsyncLocalStorage } from './async-context';
|
|
24
|
+
export { AsyncContextOverlapError, AsyncLocalStorage, awaitOutsideRequest, getAsyncContextMode, runRequestExclusive, type AsyncContextMode, } from './async-context';
|
|
25
25
|
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';
|
package/index.js
CHANGED
|
@@ -3977,6 +3977,7 @@ var init_cloudflare_kv = __esm({
|
|
|
3977
3977
|
// libs/utils/src/index.ts
|
|
3978
3978
|
var index_exports = {};
|
|
3979
3979
|
__export(index_exports, {
|
|
3980
|
+
AsyncContextOverlapError: () => import_async_context.AsyncContextOverlapError,
|
|
3980
3981
|
AsyncLocalStorage: () => import_async_context.AsyncLocalStorage,
|
|
3981
3982
|
BaseStorageAdapter: () => BaseStorageAdapter,
|
|
3982
3983
|
DEFAULT_CODE_VERIFIER_LENGTH: () => DEFAULT_CODE_VERIFIER_LENGTH,
|
|
@@ -4013,6 +4014,7 @@ __export(index_exports, {
|
|
|
4013
4014
|
assertNode: () => assertNode,
|
|
4014
4015
|
asymmetricAlgSchema: () => asymmetricAlgSchema,
|
|
4015
4016
|
asymmetricKeyDataSchema: () => asymmetricKeyDataSchema,
|
|
4017
|
+
awaitOutsideRequest: () => import_async_context.awaitOutsideRequest,
|
|
4016
4018
|
base64Decode: () => base64Decode,
|
|
4017
4019
|
base64Encode: () => base64Encode,
|
|
4018
4020
|
base64urlDecode: () => base64urlDecode,
|
|
@@ -4071,6 +4073,7 @@ __export(index_exports, {
|
|
|
4071
4073
|
generatePkcePair: () => generatePkcePair,
|
|
4072
4074
|
generateRsaKeyPair: () => generateRsaKeyPair2,
|
|
4073
4075
|
generateSecret: () => generateSecret,
|
|
4076
|
+
getAsyncContextMode: () => import_async_context.getAsyncContextMode,
|
|
4074
4077
|
getCookie: () => getCookie,
|
|
4075
4078
|
getCrypto: () => getCrypto,
|
|
4076
4079
|
getCwd: () => import_env3.getCwd,
|
|
@@ -4150,6 +4153,7 @@ __export(index_exports, {
|
|
|
4150
4153
|
rsaVerify: () => rsaVerify2,
|
|
4151
4154
|
rsaVerifySync: () => rsaVerifySync2,
|
|
4152
4155
|
runCmd: () => runCmd,
|
|
4156
|
+
runRequestExclusive: () => import_async_context.runRequestExclusive,
|
|
4153
4157
|
safeExec: () => safeExec,
|
|
4154
4158
|
safeJsonForScript: () => safeJsonForScript,
|
|
4155
4159
|
safeMatch: () => safeMatch,
|
|
@@ -6086,6 +6090,7 @@ function setMachineIdOverride(id) {
|
|
|
6086
6090
|
}
|
|
6087
6091
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6088
6092
|
0 && (module.exports = {
|
|
6093
|
+
AsyncContextOverlapError,
|
|
6089
6094
|
AsyncLocalStorage,
|
|
6090
6095
|
BaseStorageAdapter,
|
|
6091
6096
|
DEFAULT_CODE_VERIFIER_LENGTH,
|
|
@@ -6122,6 +6127,7 @@ function setMachineIdOverride(id) {
|
|
|
6122
6127
|
assertNode,
|
|
6123
6128
|
asymmetricAlgSchema,
|
|
6124
6129
|
asymmetricKeyDataSchema,
|
|
6130
|
+
awaitOutsideRequest,
|
|
6125
6131
|
base64Decode,
|
|
6126
6132
|
base64Encode,
|
|
6127
6133
|
base64urlDecode,
|
|
@@ -6180,6 +6186,7 @@ function setMachineIdOverride(id) {
|
|
|
6180
6186
|
generatePkcePair,
|
|
6181
6187
|
generateRsaKeyPair,
|
|
6182
6188
|
generateSecret,
|
|
6189
|
+
getAsyncContextMode,
|
|
6183
6190
|
getCookie,
|
|
6184
6191
|
getCrypto,
|
|
6185
6192
|
getCwd,
|
|
@@ -6259,6 +6266,7 @@ function setMachineIdOverride(id) {
|
|
|
6259
6266
|
rsaVerify,
|
|
6260
6267
|
rsaVerifySync,
|
|
6261
6268
|
runCmd,
|
|
6269
|
+
runRequestExclusive,
|
|
6262
6270
|
safeExec,
|
|
6263
6271
|
safeJsonForScript,
|
|
6264
6272
|
safeMatch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/utils",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.5",
|
|
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.
|
|
35
|
+
"@frontmcp/lazy-zod": "1.8.5",
|
|
36
36
|
"@vercel/kv": "^2.0.0 || ^3.0.0",
|
|
37
37
|
"ioredis": "^5.0.0"
|
|
38
38
|
},
|