@tjamescouch/gro 1.3.6 → 1.3.7
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/drivers/anthropic.js +256 -0
- package/dist/drivers/index.js +2 -0
- package/dist/drivers/streaming-openai.js +262 -0
- package/dist/drivers/types.js +1 -0
- package/dist/errors.js +79 -0
- package/dist/logger.js +30 -0
- package/dist/main.js +867 -0
- package/dist/mcp/client.js +130 -0
- package/dist/mcp/index.js +1 -0
- package/dist/memory/advanced-memory.js +210 -0
- package/dist/memory/agent-memory.js +52 -0
- package/dist/memory/agenthnsw.js +86 -0
- package/{src/memory/index.ts → dist/memory/index.js} +0 -1
- package/dist/memory/simple-memory.js +34 -0
- package/dist/memory/vector-index.js +7 -0
- package/dist/package.json +22 -0
- package/dist/session.js +110 -0
- package/dist/tools/agentpatch.js +91 -0
- package/dist/tools/bash.js +61 -0
- package/dist/tools/version.js +76 -0
- package/dist/utils/rate-limiter.js +46 -0
- package/{src/utils/retry.ts → dist/utils/retry.js} +8 -12
- package/dist/utils/timed-fetch.js +25 -0
- package/package.json +11 -3
- package/.github/workflows/ci.yml +0 -20
- package/src/drivers/anthropic.ts +0 -281
- package/src/drivers/index.ts +0 -5
- package/src/drivers/streaming-openai.ts +0 -258
- package/src/drivers/types.ts +0 -39
- package/src/errors.ts +0 -97
- package/src/logger.ts +0 -28
- package/src/main.ts +0 -905
- package/src/mcp/client.ts +0 -163
- package/src/mcp/index.ts +0 -2
- package/src/memory/advanced-memory.ts +0 -263
- package/src/memory/agent-memory.ts +0 -61
- package/src/memory/agenthnsw.ts +0 -122
- package/src/memory/simple-memory.ts +0 -41
- package/src/memory/vector-index.ts +0 -30
- package/src/session.ts +0 -150
- package/src/tools/agentpatch.ts +0 -89
- package/src/tools/bash.ts +0 -61
- package/src/tools/version.ts +0 -98
- package/src/utils/rate-limiter.ts +0 -60
- package/src/utils/timed-fetch.ts +0 -29
- package/tests/errors.test.ts +0 -246
- package/tests/memory.test.ts +0 -186
- package/tests/rate-limiter.test.ts +0 -76
- package/tests/retry.test.ts +0 -138
- package/tests/timed-fetch.test.ts +0 -104
- package/tsconfig.json +0 -13
package/src/session.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import type { ChatMessage } from "./drivers/types.js";
|
|
5
|
-
import { Logger } from "./logger.js";
|
|
6
|
-
import { groError, asError, errorLogFields } from "./errors.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Session persistence for gro.
|
|
10
|
-
*
|
|
11
|
-
* Layout:
|
|
12
|
-
* .gro/
|
|
13
|
-
* context/
|
|
14
|
-
* <session-id>/
|
|
15
|
-
* messages.json — full message history
|
|
16
|
-
* meta.json — session metadata (model, provider, timestamps)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export interface SessionMeta {
|
|
20
|
-
id: string;
|
|
21
|
-
provider: string;
|
|
22
|
-
model: string;
|
|
23
|
-
createdAt: string;
|
|
24
|
-
updatedAt: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const GRO_DIR = ".gro";
|
|
28
|
-
const CONTEXT_DIR = "context";
|
|
29
|
-
|
|
30
|
-
function groDir(): string {
|
|
31
|
-
return join(process.cwd(), GRO_DIR);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function contextDir(): string {
|
|
35
|
-
return join(groDir(), CONTEXT_DIR);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sessionDir(id: string): string {
|
|
39
|
-
return join(contextDir(), id);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Ensure the .gro/context directory exists.
|
|
44
|
-
*/
|
|
45
|
-
export function ensureGroDir(): void {
|
|
46
|
-
const dir = contextDir();
|
|
47
|
-
if (!existsSync(dir)) {
|
|
48
|
-
mkdirSync(dir, { recursive: true });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Generate a new session ID (short UUID prefix for readability).
|
|
54
|
-
*/
|
|
55
|
-
export function newSessionId(): string {
|
|
56
|
-
return randomUUID().split("-")[0];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Save a session to disk.
|
|
61
|
-
*/
|
|
62
|
-
export function saveSession(
|
|
63
|
-
id: string,
|
|
64
|
-
messages: ChatMessage[],
|
|
65
|
-
meta: Omit<SessionMeta, "updatedAt">,
|
|
66
|
-
): void {
|
|
67
|
-
const dir = sessionDir(id);
|
|
68
|
-
if (!existsSync(dir)) {
|
|
69
|
-
mkdirSync(dir, { recursive: true });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const fullMeta: SessionMeta = {
|
|
73
|
-
...meta,
|
|
74
|
-
updatedAt: new Date().toISOString(),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
writeFileSync(join(dir, "messages.json"), JSON.stringify(messages, null, 2));
|
|
78
|
-
writeFileSync(join(dir, "meta.json"), JSON.stringify(fullMeta, null, 2));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Load a session from disk. Returns null if not found.
|
|
83
|
-
*/
|
|
84
|
-
export function loadSession(id: string): { messages: ChatMessage[]; meta: SessionMeta } | null {
|
|
85
|
-
const dir = sessionDir(id);
|
|
86
|
-
const msgPath = join(dir, "messages.json");
|
|
87
|
-
const metaPath = join(dir, "meta.json");
|
|
88
|
-
|
|
89
|
-
if (!existsSync(msgPath) || !existsSync(metaPath)) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const messages = JSON.parse(readFileSync(msgPath, "utf-8"));
|
|
95
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
96
|
-
return { messages, meta };
|
|
97
|
-
} catch (e: unknown) {
|
|
98
|
-
const ge = groError("session_error", `Failed to load session ${id}: ${asError(e).message}`, { cause: e });
|
|
99
|
-
Logger.warn(ge.message, errorLogFields(ge));
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Find the most recent session (for --continue).
|
|
106
|
-
*/
|
|
107
|
-
export function findLatestSession(): string | null {
|
|
108
|
-
const dir = contextDir();
|
|
109
|
-
if (!existsSync(dir)) return null;
|
|
110
|
-
|
|
111
|
-
let latest: { id: string; mtime: number } | null = null;
|
|
112
|
-
|
|
113
|
-
for (const entry of readdirSync(dir)) {
|
|
114
|
-
const metaPath = join(dir, entry, "meta.json");
|
|
115
|
-
if (existsSync(metaPath)) {
|
|
116
|
-
const stat = statSync(metaPath);
|
|
117
|
-
if (!latest || stat.mtimeMs > latest.mtime) {
|
|
118
|
-
latest = { id: entry, mtime: stat.mtimeMs };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return latest?.id ?? null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* List all sessions, sorted by most recent first.
|
|
128
|
-
*/
|
|
129
|
-
export function listSessions(): SessionMeta[] {
|
|
130
|
-
const dir = contextDir();
|
|
131
|
-
if (!existsSync(dir)) return [];
|
|
132
|
-
|
|
133
|
-
const sessions: (SessionMeta & { mtime: number })[] = [];
|
|
134
|
-
|
|
135
|
-
for (const entry of readdirSync(dir)) {
|
|
136
|
-
const metaPath = join(dir, entry, "meta.json");
|
|
137
|
-
if (existsSync(metaPath)) {
|
|
138
|
-
try {
|
|
139
|
-
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
140
|
-
const stat = statSync(metaPath);
|
|
141
|
-
sessions.push({ ...meta, mtime: stat.mtimeMs });
|
|
142
|
-
} catch {
|
|
143
|
-
// skip corrupt sessions
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
149
|
-
return sessions.map(({ mtime: _, ...rest }) => rest);
|
|
150
|
-
}
|
package/src/tools/agentpatch.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* agentpatch tool integration for gro.
|
|
3
|
-
*
|
|
4
|
-
* Exposes a first-class file editor: `apply_patch`.
|
|
5
|
-
*
|
|
6
|
-
* This wraps the agentpatch patch grammar and applies patches via the
|
|
7
|
-
* `agentpatch/bin/apply_patch` script.
|
|
8
|
-
*/
|
|
9
|
-
import { execSync } from "node:child_process";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { Logger } from "../logger.js";
|
|
13
|
-
|
|
14
|
-
const DEFAULT_TIMEOUT = 120_000;
|
|
15
|
-
const MAX_OUTPUT = 30_000;
|
|
16
|
-
|
|
17
|
-
export function agentpatchToolDefinition(): any {
|
|
18
|
-
return {
|
|
19
|
-
type: "function",
|
|
20
|
-
function: {
|
|
21
|
-
name: "apply_patch",
|
|
22
|
-
description: "Apply a unified agentpatch-style patch to the working tree (safe, idempotent).",
|
|
23
|
-
parameters: {
|
|
24
|
-
type: "object",
|
|
25
|
-
properties: {
|
|
26
|
-
patch: { type: "string", description: "The patch text (agentpatch grammar)." },
|
|
27
|
-
dry_run: { type: "boolean", description: "If true, validate/preview without writing." },
|
|
28
|
-
verbose: { type: "boolean", description: "If true, emit debug logs from applier." },
|
|
29
|
-
allow_delete: { type: "boolean", description: "Allow *** Delete File ops." },
|
|
30
|
-
allow_rename: { type: "boolean", description: "Allow *** Rename File ops." },
|
|
31
|
-
timeout: { type: "number", description: "Timeout in ms (default 120000)." },
|
|
32
|
-
},
|
|
33
|
-
required: ["patch"],
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function truncate(s: string): string {
|
|
40
|
-
if (s.length <= MAX_OUTPUT) return s;
|
|
41
|
-
const half = Math.floor(MAX_OUTPUT / 2);
|
|
42
|
-
return s.slice(0, half) + `\n\n... (truncated ${s.length - MAX_OUTPUT} chars) ...\n\n` + s.slice(-half);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function executeAgentpatch(args: Record<string, any>): string {
|
|
46
|
-
const patch = (args.patch as string) || "";
|
|
47
|
-
if (!patch.trim()) return "Error: empty patch";
|
|
48
|
-
|
|
49
|
-
const timeout = (args.timeout as number) || DEFAULT_TIMEOUT;
|
|
50
|
-
const dryRun = args.dry_run === true;
|
|
51
|
-
const verbose = args.verbose === true;
|
|
52
|
-
const allowDelete = args.allow_delete === true;
|
|
53
|
-
const allowRename = args.allow_rename === true;
|
|
54
|
-
|
|
55
|
-
// Expected layout in this monorepo-ish runner: /home/agent/agentpatch
|
|
56
|
-
// If not present, instruct user to clone it.
|
|
57
|
-
const agentpatchPath = process.env.AGENTPATCH_PATH || join(process.env.HOME || "", "agentpatch");
|
|
58
|
-
const bin = join(agentpatchPath, "bin", "apply_patch");
|
|
59
|
-
if (!existsSync(bin)) {
|
|
60
|
-
return `Error: agentpatch not found at ${bin}. Set AGENTPATCH_PATH or clone agentpatch to ~/agentpatch.`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const cmd = [bin];
|
|
64
|
-
if (dryRun) cmd.push("--dry-run");
|
|
65
|
-
if (verbose) cmd.push("--verbose");
|
|
66
|
-
if (allowDelete) cmd.push("--allow-delete");
|
|
67
|
-
if (allowRename) cmd.push("--allow-rename");
|
|
68
|
-
|
|
69
|
-
Logger.debug(`apply_patch: ${cmd.join(" ")}`);
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const out = execSync(cmd.join(" "), {
|
|
73
|
-
shell: "/bin/bash",
|
|
74
|
-
input: patch,
|
|
75
|
-
encoding: "utf-8",
|
|
76
|
-
timeout,
|
|
77
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
78
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
79
|
-
});
|
|
80
|
-
return truncate(out || "ok");
|
|
81
|
-
} catch (e: any) {
|
|
82
|
-
let result = "";
|
|
83
|
-
if (e.stdout) result += e.stdout;
|
|
84
|
-
if (e.stderr) result += (result ? "\n" : "") + e.stderr;
|
|
85
|
-
if (!result) result = e.message || "Command failed";
|
|
86
|
-
if (e.status != null) result += `\n[exit code: ${e.status}]`;
|
|
87
|
-
return truncate(result);
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/tools/bash.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in bash tool for gro — executes shell commands and returns output.
|
|
3
|
-
* Gated behind --bash flag. Not enabled by default.
|
|
4
|
-
*/
|
|
5
|
-
import { execSync } from "node:child_process";
|
|
6
|
-
import { Logger } from "../logger.js";
|
|
7
|
-
|
|
8
|
-
const MAX_OUTPUT = 30_000;
|
|
9
|
-
const DEFAULT_TIMEOUT = 120_000;
|
|
10
|
-
|
|
11
|
-
export function bashToolDefinition(): any {
|
|
12
|
-
return {
|
|
13
|
-
type: "function",
|
|
14
|
-
function: {
|
|
15
|
-
name: "bash",
|
|
16
|
-
description: "Execute a bash command and return its output (stdout + stderr).",
|
|
17
|
-
parameters: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
command: { type: "string", description: "The bash command to execute" },
|
|
21
|
-
timeout: { type: "number", description: "Timeout in milliseconds (default: 120000)" },
|
|
22
|
-
},
|
|
23
|
-
required: ["command"],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function executeBash(args: Record<string, any>): string {
|
|
30
|
-
const command = args.command as string;
|
|
31
|
-
if (!command) return "Error: no command provided";
|
|
32
|
-
|
|
33
|
-
const timeout = (args.timeout as number) || DEFAULT_TIMEOUT;
|
|
34
|
-
|
|
35
|
-
Logger.debug(`bash: ${command}`);
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const output = execSync(command, {
|
|
39
|
-
shell: "/bin/bash",
|
|
40
|
-
encoding: "utf-8",
|
|
41
|
-
timeout,
|
|
42
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
43
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
44
|
-
});
|
|
45
|
-
return truncate(output);
|
|
46
|
-
} catch (e: any) {
|
|
47
|
-
// execSync throws on non-zero exit — capture stdout + stderr
|
|
48
|
-
let result = "";
|
|
49
|
-
if (e.stdout) result += e.stdout;
|
|
50
|
-
if (e.stderr) result += (result ? "\n" : "") + e.stderr;
|
|
51
|
-
if (!result) result = e.message || "Command failed";
|
|
52
|
-
if (e.status != null) result += `\n[exit code: ${e.status}]`;
|
|
53
|
-
return truncate(result);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function truncate(s: string): string {
|
|
58
|
-
if (s.length <= MAX_OUTPUT) return s;
|
|
59
|
-
const half = Math.floor(MAX_OUTPUT / 2);
|
|
60
|
-
return s.slice(0, half) + `\n\n... (truncated ${s.length - MAX_OUTPUT} chars) ...\n\n` + s.slice(-half);
|
|
61
|
-
}
|
package/src/tools/version.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in version/identity tool for gro.
|
|
3
|
-
*
|
|
4
|
-
* Lets agents (and humans) introspect the gro runtime — version, provider,
|
|
5
|
-
* model, uptime, process info. This is the canonical way to confirm an
|
|
6
|
-
* agent is running on gro.
|
|
7
|
-
*/
|
|
8
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
-
import { join, dirname } from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
|
|
12
|
-
const startTime = Date.now();
|
|
13
|
-
|
|
14
|
-
interface GroRuntimeInfo {
|
|
15
|
-
runtime: "gro";
|
|
16
|
-
version: string;
|
|
17
|
-
provider: string;
|
|
18
|
-
model: string;
|
|
19
|
-
pid: number;
|
|
20
|
-
uptime_seconds: number;
|
|
21
|
-
node_version: string;
|
|
22
|
-
platform: string;
|
|
23
|
-
persistent: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Read version from package.json — single source of truth. */
|
|
27
|
-
function readVersion(): string {
|
|
28
|
-
// In ESM, __dirname isn't available — derive from import.meta.url
|
|
29
|
-
let selfDir: string;
|
|
30
|
-
try {
|
|
31
|
-
selfDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
} catch {
|
|
33
|
-
selfDir = process.cwd();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const candidates = [
|
|
37
|
-
join(selfDir, "..", "package.json"), // from dist/tools/ or src/tools/
|
|
38
|
-
join(selfDir, "..", "..", "package.json"), // from deeper nesting
|
|
39
|
-
join(process.cwd(), "package.json"),
|
|
40
|
-
];
|
|
41
|
-
for (const p of candidates) {
|
|
42
|
-
if (existsSync(p)) {
|
|
43
|
-
try {
|
|
44
|
-
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
45
|
-
if (pkg.name === "@tjamescouch/gro" && pkg.version) {
|
|
46
|
-
return pkg.version;
|
|
47
|
-
}
|
|
48
|
-
} catch {
|
|
49
|
-
// try next candidate
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return "unknown";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Cache version at module load
|
|
57
|
-
const GRO_VERSION = readVersion();
|
|
58
|
-
|
|
59
|
-
export function getGroVersion(): string {
|
|
60
|
-
return GRO_VERSION;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function groVersionToolDefinition(): any {
|
|
64
|
-
return {
|
|
65
|
-
type: "function",
|
|
66
|
-
function: {
|
|
67
|
-
name: "gro_version",
|
|
68
|
-
description:
|
|
69
|
-
"Report gro runtime identity and version. Returns runtime name, version, provider, model, uptime, and process info. Use this to confirm an agent is running on gro.",
|
|
70
|
-
parameters: {
|
|
71
|
-
type: "object",
|
|
72
|
-
properties: {},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Execute the version tool. Requires runtime config to report provider/model.
|
|
80
|
-
*/
|
|
81
|
-
export function executeGroVersion(cfg: {
|
|
82
|
-
provider: string;
|
|
83
|
-
model: string;
|
|
84
|
-
persistent: boolean;
|
|
85
|
-
}): string {
|
|
86
|
-
const info: GroRuntimeInfo = {
|
|
87
|
-
runtime: "gro",
|
|
88
|
-
version: GRO_VERSION,
|
|
89
|
-
provider: cfg.provider,
|
|
90
|
-
model: cfg.model,
|
|
91
|
-
pid: process.pid,
|
|
92
|
-
uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
|
|
93
|
-
node_version: process.version,
|
|
94
|
-
platform: process.platform,
|
|
95
|
-
persistent: cfg.persistent,
|
|
96
|
-
};
|
|
97
|
-
return JSON.stringify(info, null, 2);
|
|
98
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
type LimiterState = {
|
|
2
|
-
nextAvailableMs: number;
|
|
3
|
-
tail: Promise<void>;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
function sleep(ms: number): Promise<void> {
|
|
7
|
-
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
class RateLimiter {
|
|
11
|
-
private states = new Map<string, LimiterState>();
|
|
12
|
-
private readonly now: () => number;
|
|
13
|
-
|
|
14
|
-
constructor(now?: () => number) {
|
|
15
|
-
this.now =
|
|
16
|
-
now ??
|
|
17
|
-
(() =>
|
|
18
|
-
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
19
|
-
? performance.now()
|
|
20
|
-
: Date.now());
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async limit(name: string, throughputPerSecond: number): Promise<void> {
|
|
24
|
-
if (!Number.isFinite(throughputPerSecond) || throughputPerSecond <= 0) {
|
|
25
|
-
throw new RangeError(
|
|
26
|
-
`throughputPerSecond must be a positive finite number; got ${throughputPerSecond}`
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
const key = name || "default";
|
|
30
|
-
const state = this.getState(key);
|
|
31
|
-
|
|
32
|
-
const waitPromise = state.tail.then(async () => {
|
|
33
|
-
const intervalMs = 1000 / throughputPerSecond;
|
|
34
|
-
const now = this.now();
|
|
35
|
-
const scheduledAt = Math.max(now, state.nextAvailableMs);
|
|
36
|
-
state.nextAvailableMs = scheduledAt + intervalMs;
|
|
37
|
-
const delay = scheduledAt - now;
|
|
38
|
-
if (delay > 0) await sleep(delay);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
state.tail = waitPromise.catch(() => {});
|
|
42
|
-
return waitPromise;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
reset(name?: string): void {
|
|
46
|
-
if (name) this.states.delete(name);
|
|
47
|
-
else this.states.clear();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private getState(name: string): LimiterState {
|
|
51
|
-
let s = this.states.get(name);
|
|
52
|
-
if (!s) {
|
|
53
|
-
s = { nextAvailableMs: this.now(), tail: Promise.resolve() };
|
|
54
|
-
this.states.set(name, s);
|
|
55
|
-
}
|
|
56
|
-
return s;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const rateLimiter = new RateLimiter();
|
package/src/utils/timed-fetch.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { asError } from "../errors.js";
|
|
2
|
-
|
|
3
|
-
/** Wrap fetch with timeout and location context for debugging. */
|
|
4
|
-
export async function timedFetch(
|
|
5
|
-
url: string,
|
|
6
|
-
init: RequestInit & { timeoutMs?: number; where?: string } = {}
|
|
7
|
-
) {
|
|
8
|
-
const { timeoutMs, where, ...rest } = init;
|
|
9
|
-
let controller: AbortController | null = null;
|
|
10
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
if (timeoutMs && timeoutMs > 0) {
|
|
14
|
-
controller = new AbortController();
|
|
15
|
-
(rest as any).signal = controller.signal;
|
|
16
|
-
timer = setTimeout(() => controller!.abort(), timeoutMs);
|
|
17
|
-
}
|
|
18
|
-
return await fetch(url, rest);
|
|
19
|
-
} catch (e: unknown) {
|
|
20
|
-
const wrapped = asError(e);
|
|
21
|
-
const err = new Error(
|
|
22
|
-
`[fetch timeout] ${where ?? ""} ${url} -> ${wrapped.name}: ${wrapped.message}`
|
|
23
|
-
);
|
|
24
|
-
(err as any).cause = e;
|
|
25
|
-
throw err;
|
|
26
|
-
} finally {
|
|
27
|
-
if (timer) clearTimeout(timer);
|
|
28
|
-
}
|
|
29
|
-
}
|