@j0hanz/superfetch 2.6.0 → 2.7.0
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/dist/cache.js +14 -12
- package/dist/config.js +51 -39
- package/dist/dom-noise-removal.js +4 -1
- package/dist/fetch.d.ts +1 -0
- package/dist/fetch.js +160 -97
- package/dist/http-native.js +31 -14
- package/dist/language-detection.js +28 -4
- package/dist/mcp.js +7 -1
- package/dist/tasks.d.ts +1 -0
- package/dist/tasks.js +129 -95
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +4 -3
- package/dist/transform-types.d.ts +1 -0
- package/dist/transform.js +122 -17
- package/package.json +1 -1
package/dist/http-native.js
CHANGED
|
@@ -5,6 +5,8 @@ import { isIP } from 'node:net';
|
|
|
5
5
|
import { freemem, hostname, totalmem } from 'node:os';
|
|
6
6
|
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
|
|
7
7
|
import process from 'node:process';
|
|
8
|
+
import { Writable } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
10
|
import { setInterval as setIntervalPromise } from 'node:timers/promises';
|
|
9
11
|
import { InvalidTokenError, ServerError, } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
10
12
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
@@ -248,7 +250,7 @@ class JsonBodyReader {
|
|
|
248
250
|
const { chunks, size } = await this.collectChunks(req, limit, signal);
|
|
249
251
|
if (chunks.length === 0)
|
|
250
252
|
return undefined;
|
|
251
|
-
return Buffer.concat(chunks, size).toString();
|
|
253
|
+
return Buffer.concat(chunks, size).toString('utf8');
|
|
252
254
|
}
|
|
253
255
|
finally {
|
|
254
256
|
this.detachAbortListener(signal, abortListener);
|
|
@@ -286,19 +288,34 @@ class JsonBodyReader {
|
|
|
286
288
|
async collectChunks(req, limit, signal) {
|
|
287
289
|
let size = 0;
|
|
288
290
|
const chunks = [];
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
const sink = new Writable({
|
|
292
|
+
write: (chunk, _encoding, callback) => {
|
|
293
|
+
try {
|
|
294
|
+
if (signal?.aborted || req.destroyed) {
|
|
295
|
+
callback(new JsonBodyError('read-failed', 'Request aborted'));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const buf = this.normalizeChunk(chunk);
|
|
299
|
+
size += buf.length;
|
|
300
|
+
if (size > limit) {
|
|
301
|
+
req.destroy();
|
|
302
|
+
callback(new JsonBodyError('payload-too-large', 'Payload too large'));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
chunks.push(buf);
|
|
306
|
+
callback();
|
|
293
307
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (size > limit) {
|
|
297
|
-
req.destroy();
|
|
298
|
-
throw new JsonBodyError('payload-too-large', 'Payload too large');
|
|
308
|
+
catch (err) {
|
|
309
|
+
callback(err instanceof Error ? err : new Error(String(err)));
|
|
299
310
|
}
|
|
300
|
-
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
try {
|
|
314
|
+
if (signal?.aborted || req.destroyed) {
|
|
315
|
+
throw new JsonBodyError('read-failed', 'Request aborted');
|
|
301
316
|
}
|
|
317
|
+
await pipeline(req, sink, signal ? { signal } : undefined);
|
|
318
|
+
return { chunks, size };
|
|
302
319
|
}
|
|
303
320
|
catch (err) {
|
|
304
321
|
if (err instanceof JsonBodyError)
|
|
@@ -308,13 +325,12 @@ class JsonBodyReader {
|
|
|
308
325
|
}
|
|
309
326
|
throw new JsonBodyError('read-failed', err instanceof Error ? err.message : String(err));
|
|
310
327
|
}
|
|
311
|
-
return { chunks, size };
|
|
312
328
|
}
|
|
313
329
|
normalizeChunk(chunk) {
|
|
314
330
|
if (Buffer.isBuffer(chunk))
|
|
315
331
|
return chunk;
|
|
316
332
|
if (typeof chunk === 'string')
|
|
317
|
-
return Buffer.from(chunk);
|
|
333
|
+
return Buffer.from(chunk, 'utf8');
|
|
318
334
|
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
319
335
|
}
|
|
320
336
|
}
|
|
@@ -556,8 +572,9 @@ class AuthService {
|
|
|
556
572
|
return clean.href;
|
|
557
573
|
}
|
|
558
574
|
buildBasicAuthHeader(clientId, clientSecret) {
|
|
575
|
+
// Base64 is only an encoding for header transport; it is NOT encryption.
|
|
559
576
|
const credentials = `${clientId}:${clientSecret ?? ''}`;
|
|
560
|
-
return `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
577
|
+
return `Basic ${Buffer.from(credentials, 'utf8').toString('base64')}`;
|
|
561
578
|
}
|
|
562
579
|
buildIntrospectionRequest(token, resourceUrl, clientId, clientSecret) {
|
|
563
580
|
const body = new URLSearchParams({
|
|
@@ -71,8 +71,10 @@ const HTML_TAGS = [
|
|
|
71
71
|
];
|
|
72
72
|
const RUST_REGEX = /\b(?:fn|impl|struct|enum)\b/;
|
|
73
73
|
const JS_REGEX = /\b(?:const|let|var|function|class|async|await|export|import)\b/;
|
|
74
|
-
const
|
|
74
|
+
const PYTHON_UNIQUE_REGEX = /\b(?:def |elif |except |finally:|yield |lambda |raise |pass$)/m;
|
|
75
|
+
const JS_SIGNAL_REGEX = /\b(?:const |let |var |function |require\(|=>|===|!==|console\.)/;
|
|
75
76
|
const CSS_REGEX = /@media|@import|@keyframes/;
|
|
77
|
+
const CSS_PROPERTY_REGEX = /^\s*[a-z][\w-]*\s*:/;
|
|
76
78
|
function containsJsxTag(code) {
|
|
77
79
|
const len = code.length;
|
|
78
80
|
for (let i = 0; i < len - 1; i++) {
|
|
@@ -129,7 +131,11 @@ function detectCssStructure(lines) {
|
|
|
129
131
|
continue;
|
|
130
132
|
const hasSelector = (trimmed.startsWith('.') || trimmed.startsWith('#')) &&
|
|
131
133
|
trimmed.includes('{');
|
|
132
|
-
if (hasSelector
|
|
134
|
+
if (hasSelector)
|
|
135
|
+
return true;
|
|
136
|
+
if (trimmed.includes(';') &&
|
|
137
|
+
CSS_PROPERTY_REGEX.test(trimmed) &&
|
|
138
|
+
!trimmed.includes('(')) {
|
|
133
139
|
return true;
|
|
134
140
|
}
|
|
135
141
|
}
|
|
@@ -214,7 +220,25 @@ const LANGUAGES = [
|
|
|
214
220
|
const l = ctx.lower;
|
|
215
221
|
if (l.includes('print(') || l.includes('__name__'))
|
|
216
222
|
return true;
|
|
217
|
-
|
|
223
|
+
if (l.includes('self.') || l.includes('elif '))
|
|
224
|
+
return true;
|
|
225
|
+
// Check for Python's None/True/False using original case (they are capitalized in Python)
|
|
226
|
+
if (ctx.code.includes('None') ||
|
|
227
|
+
ctx.code.includes('True') ||
|
|
228
|
+
ctx.code.includes('False')) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// Python-unique keywords that JS doesn't have
|
|
232
|
+
if (PYTHON_UNIQUE_REGEX.test(l))
|
|
233
|
+
return true;
|
|
234
|
+
// Shared keywords (import, from, class) — only match if no JS signals present
|
|
235
|
+
if (/\b(?:import|from|class)\b/.test(l) &&
|
|
236
|
+
!JS_SIGNAL_REGEX.test(l) &&
|
|
237
|
+
!l.includes('{') &&
|
|
238
|
+
!l.includes("from '")) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
218
242
|
},
|
|
219
243
|
},
|
|
220
244
|
{
|
|
@@ -238,7 +262,7 @@ const LANGUAGES = [
|
|
|
238
262
|
},
|
|
239
263
|
{
|
|
240
264
|
lang: 'javascript',
|
|
241
|
-
weight:
|
|
265
|
+
weight: 15,
|
|
242
266
|
match: (ctx) => JS_REGEX.test(ctx.lower),
|
|
243
267
|
},
|
|
244
268
|
{
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -17,8 +17,14 @@ import { isObject } from './type-guards.js';
|
|
|
17
17
|
* Icons + server info
|
|
18
18
|
* ------------------------------------------------------------------------------------------------- */
|
|
19
19
|
async function getLocalIcons(signal) {
|
|
20
|
+
const MAX_ICON_BYTES = 10 * 1024;
|
|
20
21
|
try {
|
|
21
22
|
const iconPath = new URL('../assets/logo.svg', import.meta.url);
|
|
23
|
+
if (signal?.aborted)
|
|
24
|
+
return undefined;
|
|
25
|
+
const { size } = await stat(iconPath);
|
|
26
|
+
if (size > MAX_ICON_BYTES)
|
|
27
|
+
return undefined;
|
|
22
28
|
const base64 = await readFile(iconPath, {
|
|
23
29
|
encoding: 'base64',
|
|
24
30
|
...(signal ? { signal } : {}),
|
package/dist/tasks.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ declare class TaskManager {
|
|
|
40
40
|
getTask(taskId: string, ownerKey?: string): TaskState | undefined;
|
|
41
41
|
updateTask(taskId: string, updates: Partial<Omit<TaskState, 'taskId' | 'createdAt'>>): void;
|
|
42
42
|
cancelTask(taskId: string, ownerKey?: string): TaskState | undefined;
|
|
43
|
+
private collectPage;
|
|
43
44
|
listTasks(options: {
|
|
44
45
|
ownerKey: string;
|
|
45
46
|
cursor?: string;
|
package/dist/tasks.js
CHANGED
|
@@ -4,18 +4,21 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { setInterval } from 'node:timers';
|
|
5
5
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { createUnrefTimeout } from './timer-utils.js';
|
|
7
|
-
const DEFAULT_TTL_MS =
|
|
8
|
-
const DEFAULT_POLL_INTERVAL_MS =
|
|
7
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
9
9
|
const DEFAULT_OWNER_KEY = 'default';
|
|
10
10
|
const DEFAULT_PAGE_SIZE = 50;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
'failed',
|
|
14
|
-
'cancelled',
|
|
15
|
-
]);
|
|
11
|
+
const CLEANUP_INTERVAL_MS = 60_000;
|
|
12
|
+
const MAX_CURSOR_LENGTH = 256;
|
|
16
13
|
function isTerminalStatus(status) {
|
|
17
|
-
return
|
|
14
|
+
return (status === 'completed' || status === 'failed' || status === 'cancelled');
|
|
18
15
|
}
|
|
16
|
+
const asyncLocalStorageSnapshot = AsyncLocalStorage.snapshot;
|
|
17
|
+
const snapshotRunInContext = typeof asyncLocalStorageSnapshot === 'function'
|
|
18
|
+
? asyncLocalStorageSnapshot
|
|
19
|
+
: () => (fn) => {
|
|
20
|
+
fn();
|
|
21
|
+
};
|
|
19
22
|
class TaskManager {
|
|
20
23
|
tasks = new Map();
|
|
21
24
|
waiters = new Map();
|
|
@@ -24,28 +27,30 @@ class TaskManager {
|
|
|
24
27
|
}
|
|
25
28
|
startCleanupLoop() {
|
|
26
29
|
const interval = setInterval(() => {
|
|
30
|
+
const now = Date.now();
|
|
27
31
|
for (const [id, task] of this.tasks) {
|
|
28
|
-
if (
|
|
32
|
+
if (now - task._createdAtMs > task.ttl) {
|
|
29
33
|
this.tasks.delete(id);
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
|
-
},
|
|
36
|
+
}, CLEANUP_INTERVAL_MS);
|
|
33
37
|
interval.unref();
|
|
34
38
|
}
|
|
35
39
|
createTask(options, statusMessage = 'Task started', ownerKey = DEFAULT_OWNER_KEY) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const createdAt = now.toISOString();
|
|
38
42
|
const task = {
|
|
39
|
-
taskId,
|
|
43
|
+
taskId: randomUUID(),
|
|
40
44
|
ownerKey,
|
|
41
45
|
status: 'working',
|
|
42
46
|
statusMessage,
|
|
43
|
-
createdAt
|
|
44
|
-
lastUpdatedAt:
|
|
47
|
+
createdAt,
|
|
48
|
+
lastUpdatedAt: createdAt,
|
|
45
49
|
ttl: options?.ttl ?? DEFAULT_TTL_MS,
|
|
46
50
|
pollInterval: DEFAULT_POLL_INTERVAL_MS,
|
|
51
|
+
_createdAtMs: now.getTime(),
|
|
47
52
|
};
|
|
48
|
-
this.tasks.set(taskId, task);
|
|
53
|
+
this.tasks.set(task.taskId, task);
|
|
49
54
|
return task;
|
|
50
55
|
}
|
|
51
56
|
getTask(taskId, ownerKey) {
|
|
@@ -78,9 +83,7 @@ class TaskManager {
|
|
|
78
83
|
const task = this.getTask(taskId, ownerKey);
|
|
79
84
|
if (!task)
|
|
80
85
|
return undefined;
|
|
81
|
-
if (task.status
|
|
82
|
-
task.status === 'failed' ||
|
|
83
|
-
task.status === 'cancelled') {
|
|
86
|
+
if (isTerminalStatus(task.status)) {
|
|
84
87
|
throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task: already in terminal status '${task.status}'`);
|
|
85
88
|
}
|
|
86
89
|
this.updateTask(taskId, {
|
|
@@ -89,6 +92,27 @@ class TaskManager {
|
|
|
89
92
|
});
|
|
90
93
|
return this.tasks.get(taskId);
|
|
91
94
|
}
|
|
95
|
+
collectPage(ownerKey, startIndex, pageSize) {
|
|
96
|
+
const page = [];
|
|
97
|
+
let currentIndex = 0;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
for (const task of this.tasks.values()) {
|
|
100
|
+
if (task.ownerKey !== ownerKey)
|
|
101
|
+
continue;
|
|
102
|
+
if (now - task._createdAtMs > task.ttl) {
|
|
103
|
+
this.tasks.delete(task.taskId);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (currentIndex >= startIndex) {
|
|
107
|
+
page.push(task);
|
|
108
|
+
if (page.length > pageSize) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
currentIndex++;
|
|
113
|
+
}
|
|
114
|
+
return page;
|
|
115
|
+
}
|
|
92
116
|
listTasks(options) {
|
|
93
117
|
const { ownerKey, cursor, limit } = options;
|
|
94
118
|
const pageSize = limit && limit > 0 ? limit : DEFAULT_PAGE_SIZE;
|
|
@@ -96,36 +120,36 @@ class TaskManager {
|
|
|
96
120
|
if (startIndex === null) {
|
|
97
121
|
throw new McpError(ErrorCode.InvalidParams, 'Invalid cursor');
|
|
98
122
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
const page = allTasks.slice(startIndex, startIndex + pageSize);
|
|
109
|
-
const nextIndex = startIndex + page.length;
|
|
110
|
-
const nextCursor = nextIndex < allTasks.length ? this.encodeCursor(nextIndex) : undefined;
|
|
123
|
+
const page = this.collectPage(ownerKey, startIndex, pageSize);
|
|
124
|
+
const hasMore = page.length > pageSize;
|
|
125
|
+
if (hasMore) {
|
|
126
|
+
page.pop();
|
|
127
|
+
}
|
|
128
|
+
const nextCursor = hasMore
|
|
129
|
+
? this.encodeCursor(startIndex + page.length)
|
|
130
|
+
: undefined;
|
|
111
131
|
return nextCursor ? { tasks: page, nextCursor } : { tasks: page };
|
|
112
132
|
}
|
|
113
133
|
async waitForTerminalTask(taskId, ownerKey, signal) {
|
|
114
|
-
const task = this.
|
|
134
|
+
const task = this.tasks.get(taskId);
|
|
115
135
|
if (!task)
|
|
116
136
|
return undefined;
|
|
137
|
+
if (ownerKey && task.ownerKey !== ownerKey)
|
|
138
|
+
return undefined;
|
|
139
|
+
if (this.isExpired(task)) {
|
|
140
|
+
this.tasks.delete(taskId);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
117
143
|
if (isTerminalStatus(task.status))
|
|
118
144
|
return task;
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
: Number.NaN;
|
|
123
|
-
if (Number.isFinite(deadlineMs) && deadlineMs <= Date.now()) {
|
|
145
|
+
const deadlineMs = task._createdAtMs + task.ttl;
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (deadlineMs <= now) {
|
|
124
148
|
this.tasks.delete(taskId);
|
|
125
149
|
return undefined;
|
|
126
150
|
}
|
|
127
151
|
return new Promise((resolve, reject) => {
|
|
128
|
-
const runInContext =
|
|
152
|
+
const runInContext = snapshotRunInContext();
|
|
129
153
|
const resolveInContext = (value) => {
|
|
130
154
|
runInContext(() => {
|
|
131
155
|
resolve(value);
|
|
@@ -133,30 +157,12 @@ class TaskManager {
|
|
|
133
157
|
};
|
|
134
158
|
const rejectInContext = (error) => {
|
|
135
159
|
runInContext(() => {
|
|
136
|
-
|
|
137
|
-
reject(error);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
reject(new Error(String(error)));
|
|
141
|
-
}
|
|
160
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
142
161
|
});
|
|
143
162
|
};
|
|
144
163
|
let settled = false;
|
|
145
164
|
let waiter = null;
|
|
146
165
|
let deadlineTimeout;
|
|
147
|
-
const settle = (fn) => {
|
|
148
|
-
if (settled)
|
|
149
|
-
return;
|
|
150
|
-
settled = true;
|
|
151
|
-
fn();
|
|
152
|
-
};
|
|
153
|
-
const onAbort = () => {
|
|
154
|
-
settle(() => {
|
|
155
|
-
cleanup();
|
|
156
|
-
removeWaiter();
|
|
157
|
-
rejectInContext(new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled'));
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
166
|
const cleanup = () => {
|
|
161
167
|
if (deadlineTimeout) {
|
|
162
168
|
deadlineTimeout.cancel();
|
|
@@ -167,17 +173,35 @@ class TaskManager {
|
|
|
167
173
|
}
|
|
168
174
|
};
|
|
169
175
|
const removeWaiter = () => {
|
|
170
|
-
|
|
171
|
-
|
|
176
|
+
if (waiter) {
|
|
177
|
+
const set = this.waiters.get(taskId);
|
|
178
|
+
if (set) {
|
|
179
|
+
set.delete(waiter);
|
|
180
|
+
if (set.size === 0)
|
|
181
|
+
this.waiters.delete(taskId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const settleOnce = (fn) => {
|
|
186
|
+
if (settled)
|
|
172
187
|
return;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
settled = true;
|
|
189
|
+
fn();
|
|
190
|
+
};
|
|
191
|
+
const onAbort = () => {
|
|
192
|
+
settleOnce(() => {
|
|
193
|
+
cleanup();
|
|
194
|
+
removeWaiter();
|
|
195
|
+
rejectInContext(new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled'));
|
|
196
|
+
});
|
|
177
197
|
};
|
|
178
198
|
waiter = (updated) => {
|
|
179
|
-
|
|
199
|
+
settleOnce(() => {
|
|
180
200
|
cleanup();
|
|
201
|
+
if (updated.ownerKey !== ownerKey) {
|
|
202
|
+
resolveInContext(undefined);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
181
205
|
resolveInContext(updated);
|
|
182
206
|
});
|
|
183
207
|
};
|
|
@@ -185,35 +209,27 @@ class TaskManager {
|
|
|
185
209
|
onAbort();
|
|
186
210
|
return;
|
|
187
211
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
212
|
+
let set = this.waiters.get(taskId);
|
|
213
|
+
if (!set) {
|
|
214
|
+
set = new Set();
|
|
215
|
+
this.waiters.set(taskId, set);
|
|
216
|
+
}
|
|
217
|
+
set.add(waiter);
|
|
191
218
|
if (signal) {
|
|
192
219
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
193
220
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
void deadlineTimeout.promise
|
|
207
|
-
.then(() => {
|
|
208
|
-
settle(() => {
|
|
209
|
-
cleanup();
|
|
210
|
-
removeWaiter();
|
|
211
|
-
this.tasks.delete(taskId);
|
|
212
|
-
resolveInContext(undefined);
|
|
213
|
-
});
|
|
214
|
-
})
|
|
215
|
-
.catch(rejectInContext);
|
|
216
|
-
}
|
|
221
|
+
const timeoutMs = Math.max(0, deadlineMs - Date.now());
|
|
222
|
+
deadlineTimeout = createUnrefTimeout(timeoutMs, { timeout: true });
|
|
223
|
+
void deadlineTimeout.promise
|
|
224
|
+
.then(() => {
|
|
225
|
+
settleOnce(() => {
|
|
226
|
+
cleanup();
|
|
227
|
+
removeWaiter();
|
|
228
|
+
this.tasks.delete(taskId);
|
|
229
|
+
resolveInContext(undefined);
|
|
230
|
+
});
|
|
231
|
+
})
|
|
232
|
+
.catch(rejectInContext);
|
|
217
233
|
});
|
|
218
234
|
}
|
|
219
235
|
notifyWaiters(task) {
|
|
@@ -227,17 +243,18 @@ class TaskManager {
|
|
|
227
243
|
waiter(task);
|
|
228
244
|
}
|
|
229
245
|
isExpired(task) {
|
|
230
|
-
|
|
231
|
-
if (!Number.isFinite(createdAt))
|
|
232
|
-
return false;
|
|
233
|
-
return Date.now() - createdAt > task.ttl;
|
|
246
|
+
return Date.now() - task._createdAtMs > task.ttl;
|
|
234
247
|
}
|
|
235
248
|
encodeCursor(index) {
|
|
236
|
-
return Buffer.from(String(index)).toString('base64url');
|
|
249
|
+
return Buffer.from(String(index), 'utf8').toString('base64url');
|
|
237
250
|
}
|
|
238
251
|
decodeCursor(cursor) {
|
|
239
252
|
try {
|
|
253
|
+
if (!isValidBase64UrlCursor(cursor))
|
|
254
|
+
return null;
|
|
240
255
|
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
256
|
+
if (!/^\d+$/u.test(decoded))
|
|
257
|
+
return null;
|
|
241
258
|
const value = Number.parseInt(decoded, 10);
|
|
242
259
|
if (!Number.isFinite(value) || value < 0)
|
|
243
260
|
return null;
|
|
@@ -248,4 +265,21 @@ class TaskManager {
|
|
|
248
265
|
}
|
|
249
266
|
}
|
|
250
267
|
}
|
|
268
|
+
function isValidBase64UrlCursor(cursor) {
|
|
269
|
+
if (!cursor)
|
|
270
|
+
return false;
|
|
271
|
+
if (cursor.length > MAX_CURSOR_LENGTH)
|
|
272
|
+
return false;
|
|
273
|
+
if (!/^[A-Za-z0-9_-]+={0,2}$/u.test(cursor))
|
|
274
|
+
return false;
|
|
275
|
+
const firstPaddingIndex = cursor.indexOf('=');
|
|
276
|
+
if (firstPaddingIndex !== -1) {
|
|
277
|
+
for (let i = firstPaddingIndex; i < cursor.length; i += 1) {
|
|
278
|
+
if (cursor[i] !== '=')
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
return cursor.length % 4 === 0;
|
|
282
|
+
}
|
|
283
|
+
return cursor.length % 4 !== 1;
|
|
284
|
+
}
|
|
251
285
|
export const taskManager = new TaskManager();
|
package/dist/tools.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface FetchPipelineOptions<T> {
|
|
|
47
47
|
transform: (input: {
|
|
48
48
|
buffer: Uint8Array;
|
|
49
49
|
encoding: string;
|
|
50
|
+
truncated?: boolean;
|
|
50
51
|
}, url: string) => T | Promise<T>;
|
|
51
52
|
serialize?: (result: T) => string;
|
|
52
53
|
deserialize?: (cached: string) => T | undefined;
|
|
@@ -116,6 +117,7 @@ interface SharedFetchOptions<T extends {
|
|
|
116
117
|
readonly transform: (input: {
|
|
117
118
|
buffer: Uint8Array;
|
|
118
119
|
encoding: string;
|
|
120
|
+
truncated?: boolean;
|
|
119
121
|
}, normalizedUrl: string) => T | Promise<T>;
|
|
120
122
|
readonly serialize?: (result: T) => string;
|
|
121
123
|
readonly deserialize?: (cached: string) => T | undefined;
|
package/dist/tools.js
CHANGED
|
@@ -453,8 +453,8 @@ export async function executeFetchPipeline(options) {
|
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
logDebug('Fetching URL', { url: resolvedUrl.normalizedUrl });
|
|
456
|
-
const { buffer, encoding } = await fetchNormalizedUrlBuffer(resolvedUrl.normalizedUrl, withSignal(options.signal));
|
|
457
|
-
const data = await options.transform({ buffer, encoding }, resolvedUrl.normalizedUrl);
|
|
456
|
+
const { buffer, encoding, truncated } = await fetchNormalizedUrlBuffer(resolvedUrl.normalizedUrl, withSignal(options.signal));
|
|
457
|
+
const data = await options.transform({ buffer, encoding, ...(truncated ? { truncated: true } : {}) }, resolvedUrl.normalizedUrl);
|
|
458
458
|
if (cache.isEnabled()) {
|
|
459
459
|
persistCache({
|
|
460
460
|
cacheKey,
|
|
@@ -572,7 +572,8 @@ const markdownTransform = async (input, url, signal, skipNoiseRemoval) => {
|
|
|
572
572
|
...withSignal(signal),
|
|
573
573
|
...(skipNoiseRemoval ? { skipNoiseRemoval: true } : {}),
|
|
574
574
|
});
|
|
575
|
-
|
|
575
|
+
const truncated = Boolean(result.truncated || input.truncated);
|
|
576
|
+
return { ...result, content: result.markdown, truncated };
|
|
576
577
|
};
|
|
577
578
|
function serializeMarkdownResult(result) {
|
|
578
579
|
return JSON.stringify({
|