@dobby.ai/dobby 0.1.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/.env.example +9 -0
- package/AGENTS.md +267 -0
- package/README.md +382 -0
- package/ROADMAP.md +34 -0
- package/config/cron.example.json +9 -0
- package/config/gateway.example.json +128 -0
- package/config/models.custom.example.json +27 -0
- package/dist/src/agent/event-forwarder.js +341 -0
- package/dist/src/agent/tests/event-forwarder.test.js +113 -0
- package/dist/src/cli/commands/config.js +243 -0
- package/dist/src/cli/commands/configure.js +61 -0
- package/dist/src/cli/commands/cron.js +288 -0
- package/dist/src/cli/commands/doctor.js +189 -0
- package/dist/src/cli/commands/extension.js +151 -0
- package/dist/src/cli/commands/init.js +286 -0
- package/dist/src/cli/commands/start.js +177 -0
- package/dist/src/cli/commands/topology.js +254 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/program.js +386 -0
- package/dist/src/cli/shared/config-io.js +223 -0
- package/dist/src/cli/shared/config-mutators.js +345 -0
- package/dist/src/cli/shared/config-path.js +207 -0
- package/dist/src/cli/shared/config-schema.js +159 -0
- package/dist/src/cli/shared/config-types.js +1 -0
- package/dist/src/cli/shared/configure-sections.js +429 -0
- package/dist/src/cli/shared/discord-config.js +12 -0
- package/dist/src/cli/shared/init-catalog.js +115 -0
- package/dist/src/cli/shared/init-models-file.js +65 -0
- package/dist/src/cli/shared/presets.js +86 -0
- package/dist/src/cli/shared/runtime.js +29 -0
- package/dist/src/cli/shared/schema-prompts.js +325 -0
- package/dist/src/cli/tests/config-command.test.js +42 -0
- package/dist/src/cli/tests/config-io.test.js +64 -0
- package/dist/src/cli/tests/config-mutators.test.js +47 -0
- package/dist/src/cli/tests/config-path.test.js +21 -0
- package/dist/src/cli/tests/discord-config.test.js +23 -0
- package/dist/src/cli/tests/doctor.test.js +107 -0
- package/dist/src/cli/tests/init-catalog.test.js +87 -0
- package/dist/src/cli/tests/presets.test.js +41 -0
- package/dist/src/cli/tests/program-options.test.js +92 -0
- package/dist/src/cli/tests/routing-config.test.js +199 -0
- package/dist/src/cli/tests/routing-legacy.test.js +191 -0
- package/dist/src/core/control-command.js +12 -0
- package/dist/src/core/dedup-store.js +92 -0
- package/dist/src/core/gateway.js +432 -0
- package/dist/src/core/routing.js +306 -0
- package/dist/src/core/runtime-registry.js +119 -0
- package/dist/src/core/tests/control-command.test.js +17 -0
- package/dist/src/core/tests/gateway-update-strategy.test.js +167 -0
- package/dist/src/core/tests/runtime-registry.test.js +116 -0
- package/dist/src/core/tests/typing-controller.test.js +103 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/typing-controller.js +88 -0
- package/dist/src/cron/config.js +114 -0
- package/dist/src/cron/schedule.js +49 -0
- package/dist/src/cron/service.js +196 -0
- package/dist/src/cron/store.js +142 -0
- package/dist/src/cron/types.js +1 -0
- package/dist/src/extension/loader.js +97 -0
- package/dist/src/extension/manager.js +269 -0
- package/dist/src/extension/manifest.js +21 -0
- package/dist/src/extension/registry.js +137 -0
- package/dist/src/main.js +6 -0
- package/dist/src/sandbox/executor.js +1 -0
- package/dist/src/sandbox/host-executor.js +111 -0
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +175 -0
- package/docs/CRON_SCHEDULER_DESIGN.md +374 -0
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +77 -0
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +119 -0
- package/docs/MVP.md +135 -0
- package/docs/RUNBOOK.md +242 -0
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +440 -0
- package/package.json +43 -0
- package/plugins/connector-discord/dobby.manifest.json +18 -0
- package/plugins/connector-discord/index.js +1 -0
- package/plugins/connector-discord/package-lock.json +360 -0
- package/plugins/connector-discord/package.json +38 -0
- package/plugins/connector-discord/src/connector.ts +350 -0
- package/plugins/connector-discord/src/contribution.ts +21 -0
- package/plugins/connector-discord/src/mapper.ts +102 -0
- package/plugins/connector-discord/tsconfig.json +19 -0
- package/plugins/connector-feishu/dobby.manifest.json +18 -0
- package/plugins/connector-feishu/index.js +1 -0
- package/plugins/connector-feishu/package-lock.json +618 -0
- package/plugins/connector-feishu/package.json +38 -0
- package/plugins/connector-feishu/src/connector.ts +343 -0
- package/plugins/connector-feishu/src/contribution.ts +26 -0
- package/plugins/connector-feishu/src/mapper.ts +401 -0
- package/plugins/connector-feishu/tsconfig.json +19 -0
- package/plugins/plugin-sdk/index.d.ts +261 -0
- package/plugins/plugin-sdk/index.js +1 -0
- package/plugins/plugin-sdk/package-lock.json +12 -0
- package/plugins/plugin-sdk/package.json +22 -0
- package/plugins/provider-claude/dobby.manifest.json +17 -0
- package/plugins/provider-claude/index.js +1 -0
- package/plugins/provider-claude/package-lock.json +3398 -0
- package/plugins/provider-claude/package.json +39 -0
- package/plugins/provider-claude/src/contribution.ts +1018 -0
- package/plugins/provider-claude/tsconfig.json +19 -0
- package/plugins/provider-claude-cli/dobby.manifest.json +17 -0
- package/plugins/provider-claude-cli/index.js +1 -0
- package/plugins/provider-claude-cli/package-lock.json +2898 -0
- package/plugins/provider-claude-cli/package.json +38 -0
- package/plugins/provider-claude-cli/src/contribution.ts +1673 -0
- package/plugins/provider-claude-cli/tsconfig.json +19 -0
- package/plugins/provider-pi/dobby.manifest.json +17 -0
- package/plugins/provider-pi/index.js +1 -0
- package/plugins/provider-pi/package-lock.json +3877 -0
- package/plugins/provider-pi/package.json +40 -0
- package/plugins/provider-pi/src/contribution.ts +476 -0
- package/plugins/provider-pi/tsconfig.json +19 -0
- package/plugins/sandbox-core/boxlite.js +1 -0
- package/plugins/sandbox-core/dobby.manifest.json +17 -0
- package/plugins/sandbox-core/docker.js +1 -0
- package/plugins/sandbox-core/package-lock.json +136 -0
- package/plugins/sandbox-core/package.json +39 -0
- package/plugins/sandbox-core/src/boxlite-context.ts +2 -0
- package/plugins/sandbox-core/src/boxlite-contribution.ts +53 -0
- package/plugins/sandbox-core/src/boxlite-executor.ts +911 -0
- package/plugins/sandbox-core/src/docker-contribution.ts +43 -0
- package/plugins/sandbox-core/src/docker-executor.ts +217 -0
- package/plugins/sandbox-core/tsconfig.json +19 -0
- package/scripts/local-extensions.mjs +168 -0
- package/src/agent/event-forwarder.ts +414 -0
- package/src/cli/commands/config.ts +328 -0
- package/src/cli/commands/configure.ts +92 -0
- package/src/cli/commands/cron.ts +410 -0
- package/src/cli/commands/doctor.ts +230 -0
- package/src/cli/commands/extension.ts +205 -0
- package/src/cli/commands/init.ts +396 -0
- package/src/cli/commands/start.ts +223 -0
- package/src/cli/commands/topology.ts +383 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/program.ts +465 -0
- package/src/cli/shared/config-io.ts +277 -0
- package/src/cli/shared/config-mutators.ts +440 -0
- package/src/cli/shared/config-schema.ts +228 -0
- package/src/cli/shared/config-types.ts +121 -0
- package/src/cli/shared/configure-sections.ts +551 -0
- package/src/cli/shared/discord-config.ts +14 -0
- package/src/cli/shared/init-catalog.ts +189 -0
- package/src/cli/shared/init-models-file.ts +77 -0
- package/src/cli/shared/runtime.ts +33 -0
- package/src/cli/shared/schema-prompts.ts +414 -0
- package/src/cli/tests/config-command.test.ts +56 -0
- package/src/cli/tests/config-io.test.ts +92 -0
- package/src/cli/tests/config-mutators.test.ts +59 -0
- package/src/cli/tests/doctor.test.ts +120 -0
- package/src/cli/tests/init-catalog.test.ts +96 -0
- package/src/cli/tests/program-options.test.ts +113 -0
- package/src/cli/tests/routing-config.test.ts +209 -0
- package/src/core/control-command.ts +12 -0
- package/src/core/dedup-store.ts +103 -0
- package/src/core/gateway.ts +607 -0
- package/src/core/routing.ts +379 -0
- package/src/core/runtime-registry.ts +141 -0
- package/src/core/tests/control-command.test.ts +20 -0
- package/src/core/tests/runtime-registry.test.ts +140 -0
- package/src/core/tests/typing-controller.test.ts +129 -0
- package/src/core/types.ts +318 -0
- package/src/core/typing-controller.ts +119 -0
- package/src/cron/config.ts +154 -0
- package/src/cron/schedule.ts +61 -0
- package/src/cron/service.ts +249 -0
- package/src/cron/store.ts +155 -0
- package/src/cron/types.ts +60 -0
- package/src/extension/loader.ts +145 -0
- package/src/extension/manager.ts +355 -0
- package/src/extension/manifest.ts +26 -0
- package/src/extension/registry.ts +229 -0
- package/src/main.ts +8 -0
- package/src/sandbox/executor.ts +44 -0
- package/src/sandbox/host-executor.ts +118 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { loadGatewayConfig } from "../../core/routing.js";
|
|
7
|
+
|
|
8
|
+
async function writeTempConfig(payload: unknown): Promise<string> {
|
|
9
|
+
const dir = await mkdtemp(join(tmpdir(), "dobby-routing-"));
|
|
10
|
+
const configPath = join(dir, "gateway.json");
|
|
11
|
+
await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
12
|
+
return configPath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function writeRepoTempConfig(payload: unknown): Promise<{ repoRoot: string; configPath: string }> {
|
|
16
|
+
const repoRoot = await mkdtemp(join(tmpdir(), "dobby-routing-repo-"));
|
|
17
|
+
const configDir = join(repoRoot, "config");
|
|
18
|
+
await mkdir(configDir, { recursive: true });
|
|
19
|
+
await mkdir(join(repoRoot, "scripts"), { recursive: true });
|
|
20
|
+
await writeFile(join(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
21
|
+
await writeFile(join(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
22
|
+
|
|
23
|
+
const configPath = join(configDir, "gateway.json");
|
|
24
|
+
await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
25
|
+
return { repoRoot, configPath };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validConfig(): Record<string, unknown> {
|
|
29
|
+
return {
|
|
30
|
+
extensions: { allowList: [] },
|
|
31
|
+
providers: {
|
|
32
|
+
default: "pi.main",
|
|
33
|
+
items: {
|
|
34
|
+
"pi.main": {
|
|
35
|
+
type: "provider.pi",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
connectors: {
|
|
40
|
+
items: {
|
|
41
|
+
"discord.main": {
|
|
42
|
+
type: "connector.discord",
|
|
43
|
+
botName: "dobby-main",
|
|
44
|
+
botToken: "token",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
sandboxes: {
|
|
49
|
+
default: "host.builtin",
|
|
50
|
+
items: {},
|
|
51
|
+
},
|
|
52
|
+
routes: {
|
|
53
|
+
defaults: {
|
|
54
|
+
provider: "pi.main",
|
|
55
|
+
sandbox: "host.builtin",
|
|
56
|
+
tools: "full",
|
|
57
|
+
mentions: "required",
|
|
58
|
+
},
|
|
59
|
+
items: {
|
|
60
|
+
main: {
|
|
61
|
+
projectRoot: "./workspace/project-a",
|
|
62
|
+
systemPromptFile: "./prompts/main.md",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
bindings: {
|
|
67
|
+
items: {
|
|
68
|
+
"discord.main.main": {
|
|
69
|
+
connector: "discord.main",
|
|
70
|
+
source: {
|
|
71
|
+
type: "channel",
|
|
72
|
+
id: "123",
|
|
73
|
+
},
|
|
74
|
+
route: "main",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
data: {
|
|
79
|
+
rootDir: "./data",
|
|
80
|
+
dedupTtlMs: 604800000,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test("loadGatewayConfig applies route defaults and resolves relative paths", async () => {
|
|
86
|
+
const payload = validConfig();
|
|
87
|
+
const configPath = await writeTempConfig(payload);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const loaded = await loadGatewayConfig(configPath);
|
|
91
|
+
const configDir = dirname(configPath);
|
|
92
|
+
|
|
93
|
+
assert.equal(loaded.providers.default, "pi.main");
|
|
94
|
+
assert.deepEqual(loaded.routes.defaults, {
|
|
95
|
+
provider: "pi.main",
|
|
96
|
+
sandbox: "host.builtin",
|
|
97
|
+
tools: "full",
|
|
98
|
+
mentions: "required",
|
|
99
|
+
});
|
|
100
|
+
assert.deepEqual(loaded.routes.items.main, {
|
|
101
|
+
projectRoot: join(configDir, "workspace/project-a"),
|
|
102
|
+
systemPromptFile: join(configDir, "prompts/main.md"),
|
|
103
|
+
provider: "pi.main",
|
|
104
|
+
sandbox: "host.builtin",
|
|
105
|
+
tools: "full",
|
|
106
|
+
mentions: "required",
|
|
107
|
+
});
|
|
108
|
+
assert.equal(loaded.data.rootDir, join(configDir, "data"));
|
|
109
|
+
assert.deepEqual(loaded.bindings.items["discord.main.main"], {
|
|
110
|
+
connector: "discord.main",
|
|
111
|
+
source: {
|
|
112
|
+
type: "channel",
|
|
113
|
+
id: "123",
|
|
114
|
+
},
|
|
115
|
+
route: "main",
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
await rm(dirname(configPath), { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("loadGatewayConfig resolves data.rootDir from repo root for repo-local config/gateway.json", async () => {
|
|
123
|
+
const payload = validConfig();
|
|
124
|
+
const { repoRoot, configPath } = await writeRepoTempConfig(payload);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const loaded = await loadGatewayConfig(configPath);
|
|
128
|
+
const mainRoute = loaded.routes.items.main;
|
|
129
|
+
assert.ok(mainRoute);
|
|
130
|
+
assert.equal(loaded.data.rootDir, join(repoRoot, "data"));
|
|
131
|
+
assert.equal(mainRoute.projectRoot, join(repoRoot, "workspace/project-a"));
|
|
132
|
+
} finally {
|
|
133
|
+
await rm(repoRoot, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("loadGatewayConfig rejects connector fields reserved by the host", async () => {
|
|
138
|
+
const payload = validConfig();
|
|
139
|
+
payload.connectors = {
|
|
140
|
+
items: {
|
|
141
|
+
"discord.main": {
|
|
142
|
+
type: "connector.discord",
|
|
143
|
+
botName: "dobby-main",
|
|
144
|
+
botToken: "token",
|
|
145
|
+
botChannelMap: {
|
|
146
|
+
"123": "main",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const configPath = await writeTempConfig(payload);
|
|
153
|
+
try {
|
|
154
|
+
await assert.rejects(loadGatewayConfig(configPath), /must not include 'botChannelMap'/);
|
|
155
|
+
} finally {
|
|
156
|
+
await rm(dirname(configPath), { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("loadGatewayConfig rejects connector env indirection fields reserved by the host", async () => {
|
|
161
|
+
const payload = validConfig();
|
|
162
|
+
payload.connectors = {
|
|
163
|
+
items: {
|
|
164
|
+
"discord.main": {
|
|
165
|
+
type: "connector.discord",
|
|
166
|
+
botName: "dobby-main",
|
|
167
|
+
botTokenEnv: "DISCORD_BOT_TOKEN",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const configPath = await writeTempConfig(payload);
|
|
173
|
+
try {
|
|
174
|
+
await assert.rejects(loadGatewayConfig(configPath), /must not include 'botTokenEnv'/);
|
|
175
|
+
} finally {
|
|
176
|
+
await rm(dirname(configPath), { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("loadGatewayConfig fails fast on duplicate binding sources", async () => {
|
|
181
|
+
const payload = validConfig();
|
|
182
|
+
payload.bindings = {
|
|
183
|
+
items: {
|
|
184
|
+
"discord.main.main": {
|
|
185
|
+
connector: "discord.main",
|
|
186
|
+
source: {
|
|
187
|
+
type: "channel",
|
|
188
|
+
id: "123",
|
|
189
|
+
},
|
|
190
|
+
route: "main",
|
|
191
|
+
},
|
|
192
|
+
"discord.main.duplicate": {
|
|
193
|
+
connector: "discord.main",
|
|
194
|
+
source: {
|
|
195
|
+
type: "channel",
|
|
196
|
+
id: "123",
|
|
197
|
+
},
|
|
198
|
+
route: "main",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const configPath = await writeTempConfig(payload);
|
|
204
|
+
try {
|
|
205
|
+
await assert.rejects(loadGatewayConfig(configPath), /duplicates source 'discord\.main:channel:123'/);
|
|
206
|
+
} finally {
|
|
207
|
+
await rm(dirname(configPath), { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ControlCommand = "cancel" | "new_session";
|
|
2
|
+
|
|
3
|
+
const CANCEL_COMMANDS = new Set(["stop", "/stop", "/cancel"]);
|
|
4
|
+
const NEW_SESSION_COMMANDS = new Set(["/new", "/reset"]);
|
|
5
|
+
|
|
6
|
+
export function parseControlCommand(text: string): ControlCommand | null {
|
|
7
|
+
const normalized = text.trim().toLowerCase();
|
|
8
|
+
if (normalized.length === 0) return null;
|
|
9
|
+
if (CANCEL_COMMANDS.has(normalized)) return "cancel";
|
|
10
|
+
if (NEW_SESSION_COMMANDS.has(normalized)) return "new_session";
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { GatewayLogger } from "./types.js";
|
|
4
|
+
|
|
5
|
+
interface DedupSnapshot {
|
|
6
|
+
version: 1;
|
|
7
|
+
entries: Record<string, number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class DedupStore {
|
|
11
|
+
private readonly entries = new Map<string, number>();
|
|
12
|
+
private dirty = false;
|
|
13
|
+
private flushTimer: NodeJS.Timeout | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly filePath: string,
|
|
17
|
+
private readonly ttlMs: number,
|
|
18
|
+
private readonly logger: GatewayLogger,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
async load(): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(this.filePath, "utf-8");
|
|
24
|
+
const snapshot = JSON.parse(raw) as DedupSnapshot;
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
for (const [key, expiresAt] of Object.entries(snapshot.entries ?? {})) {
|
|
27
|
+
if (expiresAt > now) {
|
|
28
|
+
this.entries.set(key, expiresAt);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// First start or malformed file; start clean.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
startAutoFlush(intervalMs = 15000): void {
|
|
37
|
+
if (this.flushTimer) return;
|
|
38
|
+
this.flushTimer = setInterval(() => {
|
|
39
|
+
void this.flush();
|
|
40
|
+
}, intervalMs);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
stopAutoFlush(): void {
|
|
44
|
+
if (!this.flushTimer) return;
|
|
45
|
+
clearInterval(this.flushTimer);
|
|
46
|
+
this.flushTimer = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
has(key: string): boolean {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const expiresAt = this.entries.get(key);
|
|
52
|
+
if (!expiresAt) return false;
|
|
53
|
+
if (expiresAt <= now) {
|
|
54
|
+
this.entries.delete(key);
|
|
55
|
+
this.dirty = true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
add(key: string): void {
|
|
62
|
+
const expiresAt = Date.now() + this.ttlMs;
|
|
63
|
+
this.entries.set(key, expiresAt);
|
|
64
|
+
this.dirty = true;
|
|
65
|
+
|
|
66
|
+
if (this.entries.size % 100 === 0) {
|
|
67
|
+
this.sweepExpired();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sweepExpired(): void {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
let removed = 0;
|
|
74
|
+
|
|
75
|
+
for (const [key, expiresAt] of this.entries.entries()) {
|
|
76
|
+
if (expiresAt <= now) {
|
|
77
|
+
this.entries.delete(key);
|
|
78
|
+
removed += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (removed > 0) {
|
|
83
|
+
this.dirty = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async flush(): Promise<void> {
|
|
88
|
+
if (!this.dirty) return;
|
|
89
|
+
|
|
90
|
+
const snapshot: DedupSnapshot = {
|
|
91
|
+
version: 1,
|
|
92
|
+
entries: Object.fromEntries(this.entries.entries()),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
97
|
+
await writeFile(this.filePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
98
|
+
this.dirty = false;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.logger.error({ err: error, filePath: this.filePath }, "Failed to flush dedup store");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|