1tube 0.1.54 → 0.2.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.
Files changed (72) hide show
  1. package/dist/edge.js +10 -3
  2. package/package.json +21 -11
  3. package/scripts/npm-cli.js +34 -33
  4. package/src/backends/bun/dep-graph.ts +310 -0
  5. package/src/backends/bun/function-host.ts +1190 -0
  6. package/src/backends/bun/hot-reloader.ts +385 -0
  7. package/src/backends/bun/shared-runtime.ts +176 -0
  8. package/src/backends/env-hot-reloader.ts +10 -9
  9. package/src/backends/vercel/bundle-profile.ts +14 -11
  10. package/src/backends/vercel/capture-config.ts +95 -5
  11. package/src/backends/vercel/wrapper.ts +6 -4
  12. package/src/backends/workerd/backend.ts +114 -78
  13. package/src/backends/workerd/bundle-profile.ts +13 -13
  14. package/src/backends/workerd/hot-reloader.ts +10 -10
  15. package/src/backends/workerd/prebuilt.ts +1 -1
  16. package/src/backends/workerd/proc-stats.ts +35 -23
  17. package/src/backends/workerd/process.ts +67 -53
  18. package/src/backends/workerd/runtime-overrides.ts +34 -23
  19. package/src/backends/workerd/shared-runtime.ts +85 -42
  20. package/src/boot-progress.ts +11 -7
  21. package/src/bundler/core.ts +154 -53
  22. package/src/bundler/profile.ts +5 -5
  23. package/src/cli/build.ts +48 -85
  24. package/src/cli/deno-config.ts +16 -25
  25. package/src/cli/package.ts +65 -55
  26. package/src/cli/vercel-build.ts +40 -25
  27. package/src/cli.ts +1 -1
  28. package/src/compat/deno-shim.ts +540 -0
  29. package/src/compat/deps.ts +359 -0
  30. package/src/compat/environment.ts +65 -0
  31. package/src/compat/esbuild-plugin.ts +109 -0
  32. package/src/compat/globals.ts +158 -0
  33. package/src/compat/import-map.ts +183 -0
  34. package/src/compat/plugin.ts +416 -0
  35. package/src/compat/remote.ts +145 -0
  36. package/src/compat/shared-deps.ts +178 -0
  37. package/src/compat/specifiers.ts +103 -0
  38. package/src/discovery.ts +6 -7
  39. package/src/edge.ts +13 -4
  40. package/src/env-file.ts +21 -9
  41. package/src/firmware-content-hash.ts +2 -1
  42. package/src/fs-watch.ts +78 -0
  43. package/src/gateway/auth.ts +2 -2
  44. package/src/gateway/body-watchdog.ts +1 -1
  45. package/src/gateway/cors.ts +8 -8
  46. package/src/gateway/logging.ts +3 -3
  47. package/src/gateway/rate-limit.ts +4 -4
  48. package/src/health.ts +2 -2
  49. package/src/launch.ts +134 -450
  50. package/src/log-buffer.ts +12 -11
  51. package/src/logs/api.ts +1 -1
  52. package/src/logs/db.ts +2 -2
  53. package/src/logs/schema.ts +1 -1
  54. package/src/logs/writer.ts +1 -1
  55. package/src/manifest.ts +4 -5
  56. package/src/registry.ts +41 -4
  57. package/src/server.ts +452 -558
  58. package/src/stdio.ts +37 -0
  59. package/src/types/bun-module-graph.d.ts +26 -0
  60. package/src/version.ts +2 -3
  61. package/tsconfig.json +17 -0
  62. package/deno.json +0 -38
  63. package/deno.lock +0 -572
  64. package/src/backends/deno/dep-graph.ts +0 -404
  65. package/src/backends/deno/hot-reloader.ts +0 -471
  66. package/src/backends/deno/shared-runtime.ts +0 -218
  67. package/src/backends/deno/source-rewriter.ts +0 -637
  68. package/src/backends/deno/worker-entry-colocated.ts +0 -406
  69. package/src/backends/deno/worker-entry.ts +0 -375
  70. package/src/backends/deno/worker-host.ts +0 -2166
  71. package/src/cli/dep-pin.ts +0 -126
  72. package/src/env-scope.ts +0 -124
package/dist/edge.js CHANGED
@@ -152,9 +152,16 @@ export function normalizeRateLimit(rl) {
152
152
  export function getStandaloneServe() {
153
153
  const runtime = globalThis;
154
154
  const standaloneServe = runtime.Deno?.serve;
155
- return typeof standaloneServe === "function"
156
- ? standaloneServe.bind(runtime.Deno)
157
- : null;
155
+ if (typeof standaloneServe === "function")
156
+ return standaloneServe.bind(runtime.Deno);
157
+ // `bun supabase/functions/<name>/index.ts` outside the gateway.
158
+ const bunServe = runtime.Bun?.serve;
159
+ if (typeof bunServe === "function") {
160
+ return (handler) => {
161
+ bunServe.call(runtime.Bun, { fetch: handler });
162
+ };
163
+ }
164
+ return null;
158
165
  }
159
166
  export function normalizeRoutePath(path) {
160
167
  const trimmed = path.trim();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.54",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
- "description": "Self-hosted Supabase Edge Functions gateway for Deno, plus an ESLint plugin for consumers",
5
+ "description": "Self-hosted Supabase Edge Functions gateway on Bun, plus an ESLint plugin for consumers",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "repository": {
@@ -41,8 +41,7 @@
41
41
  "drizzle",
42
42
  "scripts/npm-cli.js",
43
43
  "src",
44
- "deno.json",
45
- "deno.lock",
44
+ "tsconfig.json",
46
45
  "README.md",
47
46
  "LICENSE",
48
47
  ".env.example"
@@ -51,20 +50,31 @@
51
50
  "access": "public"
52
51
  },
53
52
  "scripts": {
54
- "dev": "deno task dev",
55
- "serve": "deno task serve",
56
- "build:edge": "deno run -A npm:typescript@latest/tsc --target ES2022 --module ESNext --moduleResolution Bundler --declaration --outDir dist src/edge.ts src/edge-caller.ts src/warmup-client.ts",
53
+ "dev": "bun --watch src/cli.ts",
54
+ "serve": "bun src/cli.ts",
55
+ "play": "bun src/cli.ts --dev --hmr --functions ./playground --port 3199",
56
+ "play:workerd": "bun src/cli.ts --backend workerd --dev --hmr --functions ./playground --port 3199",
57
+ "build": "bun src/cli.ts build",
58
+ "test": "bun test --timeout 60000",
59
+ "build:edge": "bunx tsc --ignoreConfig --target ES2022 --module ESNext --moduleResolution Bundler --declaration --outDir dist src/edge.ts src/edge-caller.ts src/warmup-client.ts",
57
60
  "prepack": "bun run build:edge",
58
- "typecheck": "deno check src/server.ts src/cli.ts src/launch.ts src/edge.ts src/edge-caller.ts src/warmup-client.ts eslint-plugin.js",
59
- "check": "deno cache --allow-scripts=npm:protobufjs src/server.ts",
61
+ "typecheck": "bun check",
60
62
  "smoke:bin": "node ./scripts/npm-cli.js --version",
61
- "prepublishOnly": "bun run build:edge && bun run typecheck && bun run check && bun run smoke:bin"
63
+ "prepublishOnly": "bun run build:edge && bun run typecheck && bun run smoke:bin"
62
64
  },
63
65
  "dependencies": {
66
+ "@zip.js/zip.js": "^2.8.0",
64
67
  "drizzle-orm": "1.0.0-rc.4",
68
+ "es-module-lexer": "^2",
69
+ "esbuild": "^0.28.1",
65
70
  "hono": "^4.12.30"
66
71
  },
67
72
  "devDependencies": {
68
- "drizzle-kit": "1.0.0-rc.4"
73
+ "drizzle-kit": "1.0.0-rc.4",
74
+ "@types/bun": "latest",
75
+ "typescript": "^6"
76
+ },
77
+ "engines": {
78
+ "bun": ">=1.4.3"
69
79
  }
70
80
  }
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * `1tube` npm bin — a tiny Node shim that locates Deno and runs the single
5
- * launcher (`src/launch.ts`). All argument handling, the managed lockfile,
6
- * dependency pinning, minimum-dependency-age, and live reload live in the
7
- * launcher itself, so this shim stays trivial: find deno, forward argv, wire
8
- * up signals + exit code.
4
+ * `1tube` npm bin — a tiny Node shim that locates Bun and runs the single
5
+ * launcher (`src/launch.ts`). All argument handling, the managed dependency
6
+ * store, dependency pinning, minimum-dependency-age, and live reload live in
7
+ * the launcher itself, so this shim stays trivial: find bun, forward argv,
8
+ * wire up signals + exit code.
9
9
  *
10
- * Machines using this need Deno on PATH (or set `DENO_BIN`). Arguments are
11
- * forwarded verbatim — the launcher recognizes its own flags (`--lock`,
12
- * `--config`, `--env-file`, `--node-modules-dir`, `--minimum-dependency-age`,
13
- * `--no-lock`, `--no-pin`, `--no-dep-watch`, …) and forwards everything else
14
- * to the gateway. `1TUBE_DENO_ARGS` (space-separated) is appended for projects
15
- * that prefer to keep flags out of the command line.
10
+ * Machines using this need Bun on PATH (or set `BUN_BIN`). Arguments are
11
+ * forwarded verbatim — the launcher recognizes its own flags (`--deps-dir`,
12
+ * `--refresh-deps`, `--config`, `--env-file`, `--minimum-dependency-age`,
13
+ * `--no-pin`, `--no-dep-watch`, …) and forwards everything else to the
14
+ * gateway. `1TUBE_BUN_ARGS` (space-separated) is appended for projects that
15
+ * prefer to keep flags out of the command line; the Deno-era
16
+ * `1TUBE_DENO_ARGS` is still honoured.
17
+ *
18
+ * The package's own dependencies (hono, esbuild, …) are installed next to it
19
+ * by the host's package manager, so Bun resolves them from node_modules.
16
20
  */
17
21
 
18
22
  import { spawn } from "node:child_process";
@@ -23,43 +27,40 @@ import { fileURLToPath } from "node:url";
23
27
 
24
28
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
25
29
  const launcherEntrypoint = resolve(packageRoot, "src", "launch.ts");
26
- const denoBin = process.env.DENO_BIN || "deno";
30
+ const bunBin = process.env.BUN_BIN || "bun";
27
31
 
28
- // The launcher needs no import map or lockfile of its own (it imports only
29
- // node: builtins + relative source), and it owns the gateway child's lock,
30
- // config and npm-config — so the outer `deno run` stays minimal and fast.
31
- const denoArgs = [
32
- "run",
33
- "--quiet",
34
- "-A",
35
- "--node-modules-dir=false",
32
+ // The launcher imports only node: builtins + relative source, and it owns the
33
+ // gateway child's dependency store and env — so the outer `bun` stays minimal.
34
+ const bunArgs = [
36
35
  launcherEntrypoint,
37
36
  ...tokenizeArgs(
38
- process.env.ONETUBE_DENO_ARGS ?? process.env["1TUBE_DENO_ARGS"] ?? "",
37
+ process.env.ONETUBE_BUN_ARGS ?? process.env["1TUBE_BUN_ARGS"] ??
38
+ process.env.ONETUBE_DENO_ARGS ?? process.env["1TUBE_DENO_ARGS"] ?? "",
39
39
  ),
40
40
  ...process.argv.slice(2),
41
41
  ];
42
42
 
43
- // Resolve the real deno binary up front. Node ≥18.20 refuses to spawn a
44
- // `.cmd`/`.bat` shim without a shell, but spawning a real `.exe` (or a bare
45
- // binary on POSIX) with `shell:false` lets Node escape arguments properly —
46
- // important since we forward arbitrary flags, some carrying paths with spaces.
47
- const resolvedDeno = resolveExecutable(denoBin);
48
- const needsShell = resolvedDeno
49
- ? /\.(cmd|bat)$/i.test(resolvedDeno)
43
+ // Resolve the real bun binary up front. Node ≥18.20 refuses to spawn a
44
+ // `.cmd`/`.bat` shim (e.g. npm's global `bun.cmd`) without a shell, but
45
+ // spawning a real `.exe` (or a bare binary on POSIX) with `shell:false` lets
46
+ // Node escape arguments properly — important since we forward arbitrary
47
+ // flags, some carrying paths with spaces.
48
+ const resolvedBun = resolveExecutable(bunBin);
49
+ const needsShell = resolvedBun
50
+ ? /\.(cmd|bat)$/i.test(resolvedBun)
50
51
  : process.platform === "win32";
51
- const spawnBin = resolvedDeno ?? denoBin;
52
+ const spawnBin = resolvedBun ?? bunBin;
52
53
 
53
54
  let child;
54
55
  if (needsShell) {
55
- const command = [spawnBin, ...denoArgs].map(quoteForCmd).join(" ");
56
+ const command = [spawnBin, ...bunArgs].map(quoteForCmd).join(" ");
56
57
  child = spawn(command, {
57
58
  stdio: "inherit",
58
59
  shell: true,
59
60
  windowsHide: false,
60
61
  });
61
62
  } else {
62
- child = spawn(spawnBin, denoArgs, {
63
+ child = spawn(spawnBin, bunArgs, {
63
64
  stdio: "inherit",
64
65
  shell: false,
65
66
  windowsHide: false,
@@ -75,11 +76,11 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
75
76
  child.on("error", (err) => {
76
77
  if (err && "code" in err && err.code === "ENOENT") {
77
78
  console.error(
78
- "[1tube] Deno was not found on PATH. Install Deno or set DENO_BIN to the deno executable.",
79
+ "[1tube] Bun was not found on PATH. Install Bun (https://bun.sh) or set BUN_BIN",
79
80
  );
80
81
  process.exit(127);
81
82
  }
82
- console.error(`[1tube] failed to start Deno: ${err.message}`);
83
+ console.error(`[1tube] failed to start Bun: ${err.message}`);
83
84
  process.exit(1);
84
85
  });
85
86
 
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Per-function dependency graph for the Bun backend.
3
+ *
4
+ * Two jobs:
5
+ *
6
+ * 1. HMR precision. A file change maps to exactly the functions that
7
+ * transitively import it, so touching `_shared/x.ts` reloads only its
8
+ * importers instead of everything.
9
+ * 2. Dependency pre-install. While walking, every `npm:` / `jsr:` request
10
+ * reachable from a function's local files is collected, so the host can
11
+ * install them in one batch BEFORE importing (the compat plugin's
12
+ * resolver is synchronous and would otherwise block per package).
13
+ *
14
+ * Only local files are walked. Imports are discovered with
15
+ * `Bun.Transpiler#scanImports` (the same scanner Bun's bundler uses), and
16
+ * resolved with Bun's resolver after import-map rewriting. Anything under
17
+ * `node_modules`, `node:`/`bun:` builtins and remote URLs is a leaf: none of
18
+ * them can fire a local fs event.
19
+ *
20
+ * Parsed imports are cached per file keyed by mtime + size, so building
21
+ * graphs for 100 functions that share `_shared/*` parses each shared file
22
+ * once, and an HMR refresh only re-reads what changed.
23
+ */
24
+
25
+ import { statSync } from "node:fs";
26
+ import { readFile } from "node:fs/promises";
27
+ import { dirname, extname } from "node:path";
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
+ import { type ImportMap, resolveImportMap } from "../../compat/import-map.ts";
30
+ import {
31
+ isRemoteSpecifier,
32
+ type PackageRequest,
33
+ packageKey,
34
+ parsePackageSpecifier,
35
+ } from "../../compat/specifiers.ts";
36
+
37
+ export { fileURLToPath, pathToFileURL };
38
+
39
+ export interface DepGraphOptions {
40
+ /** The project's deno.json import map, if any. */
41
+ importMap?: ImportMap;
42
+ }
43
+
44
+ export interface DepGraph {
45
+ /**
46
+ * Build (or rebuild) the graph for one function entry (path or file URL).
47
+ * Unreadable / half-written files are tolerated: they become leaves.
48
+ */
49
+ refresh(name: string, entry: string): Promise<void>;
50
+ /** {@link refresh} for many entries, sharing the parse cache. */
51
+ refreshMany(entries: Iterable<{ name: string; entry: string }>): Promise<void>;
52
+ /** Drop a function's graph (its entry was deleted). */
53
+ forget(name: string): void;
54
+ /** Names whose graph contains any of `changedFiles` (paths or file URLs). */
55
+ affected(changedFiles: Iterable<string>): Set<string>;
56
+ /** Absolute paths of every local file in `name`'s graph, entry included. */
57
+ filesFor(name: string): readonly string[];
58
+ /** `npm:`/`jsr:` requests reachable from `name`'s local files. */
59
+ packagesFor(name: string): readonly PackageRequest[];
60
+ /** Remote (`https:`) modules imported directly from local files. */
61
+ remotesFor(name: string): readonly string[];
62
+ /** Total number of distinct local files tracked. */
63
+ readonly size: number;
64
+ }
65
+
66
+ interface ParsedFile {
67
+ mtimeMs: number;
68
+ size: number;
69
+ /** Resolved local dependency paths. */
70
+ files: string[];
71
+ packages: PackageRequest[];
72
+ remotes: string[];
73
+ }
74
+
75
+ interface FunctionGraph {
76
+ files: Set<string>;
77
+ packages: Map<string, PackageRequest>;
78
+ remotes: Set<string>;
79
+ }
80
+
81
+ const LOADERS: Record<string, "ts" | "tsx" | "js" | "jsx"> = {
82
+ ".ts": "ts",
83
+ ".mts": "ts",
84
+ ".cts": "ts",
85
+ ".tsx": "tsx",
86
+ ".js": "js",
87
+ ".mjs": "js",
88
+ ".cjs": "js",
89
+ ".jsx": "jsx",
90
+ };
91
+
92
+ const transpilers = new Map<string, Bun.Transpiler>();
93
+ function transpilerFor(path: string): Bun.Transpiler | null {
94
+ const loader = LOADERS[extname(path).toLowerCase()];
95
+ if (!loader) return null;
96
+ let t = transpilers.get(loader);
97
+ if (!t) {
98
+ t = new Bun.Transpiler({ loader });
99
+ transpilers.set(loader, t);
100
+ }
101
+ return t;
102
+ }
103
+
104
+ function toPath(pathOrUrl: string): string {
105
+ if (pathOrUrl.startsWith("file://")) return fileURLToPath(pathOrUrl);
106
+ return pathOrUrl;
107
+ }
108
+
109
+ /**
110
+ * Reverse-index key. Watcher paths and resolver output can disagree on
111
+ * case and separators on Windows; elsewhere paths are compared verbatim.
112
+ */
113
+ const fileKey = process.platform === "win32"
114
+ ? (p: string) => p.replace(/\\/g, "/").toLowerCase()
115
+ : (p: string) => p;
116
+
117
+ function isDependencyPath(p: string): boolean {
118
+ return /[\\/]node_modules[\\/]/.test(p);
119
+ }
120
+
121
+ export function createDepGraph(options: DepGraphOptions = {}): DepGraph {
122
+ const perName = new Map<string, FunctionGraph>();
123
+ const reverse = new Map<string, Set<string>>();
124
+ const parsed = new Map<string, ParsedFile>();
125
+
126
+ /** Classify + resolve one specifier imported from `importer`. */
127
+ function classify(
128
+ spec: string,
129
+ importer: string,
130
+ out: { files: string[]; packages: PackageRequest[]; remotes: string[] },
131
+ depth = 0,
132
+ ): void {
133
+ if (depth > 4) return;
134
+ if (spec.startsWith("node:") || spec.startsWith("bun:")) return;
135
+ const pkg = parsePackageSpecifier(spec);
136
+ if (pkg) {
137
+ out.packages.push(pkg);
138
+ return;
139
+ }
140
+ if (isRemoteSpecifier(spec)) {
141
+ out.remotes.push(spec);
142
+ return;
143
+ }
144
+ if (spec.startsWith("file://")) {
145
+ out.files.push(fileURLToPath(spec));
146
+ return;
147
+ }
148
+ const pathLike = spec.startsWith("./") || spec.startsWith("../") ||
149
+ spec.startsWith("/");
150
+ if (!pathLike) {
151
+ const mapped = resolveImportMap(
152
+ spec,
153
+ pathToFileURL(importer).href,
154
+ options.importMap,
155
+ );
156
+ if (mapped !== null) {
157
+ classify(mapped, importer, out, depth + 1);
158
+ return;
159
+ }
160
+ }
161
+ try {
162
+ const resolved = Bun.resolveSync(spec, dirname(importer));
163
+ if (!isDependencyPath(resolved)) out.files.push(resolved);
164
+ } catch {
165
+ // Bare package or a not-yet-written file: a leaf either way. A file
166
+ // that appears later is picked up by the next refresh.
167
+ if (pathLike) {
168
+ out.files.push(fileURLToPath(new URL(spec, pathToFileURL(importer))));
169
+ }
170
+ }
171
+ }
172
+
173
+ async function parseFile(path: string): Promise<ParsedFile | null> {
174
+ let st;
175
+ try {
176
+ st = statSync(path);
177
+ } catch {
178
+ parsed.delete(path);
179
+ return null;
180
+ }
181
+ const cached = parsed.get(path);
182
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
183
+ return cached;
184
+ }
185
+ const transpiler = transpilerFor(path);
186
+ const entry: ParsedFile = {
187
+ mtimeMs: st.mtimeMs,
188
+ size: st.size,
189
+ files: [],
190
+ packages: [],
191
+ remotes: [],
192
+ };
193
+ if (transpiler) {
194
+ try {
195
+ const code = await readFile(path, "utf8");
196
+ for (const imp of transpiler.scanImports(code)) {
197
+ classify(imp.path, path, entry);
198
+ }
199
+ } catch {
200
+ // Syntax error mid-edit: no edges until the next clean save.
201
+ }
202
+ }
203
+ parsed.set(path, entry);
204
+ return entry;
205
+ }
206
+
207
+ /**
208
+ * Per-refresh memo: within one refresh every file is stat-validated (and,
209
+ * on a miss, parsed) once, however many functions share it. Without it a
210
+ * 200-function project with a common `_shared` tree pays one stat per
211
+ * file per function — tens of seconds on a slow disk.
212
+ */
213
+ type Batch = Map<string, Promise<ParsedFile | null>>;
214
+
215
+ function parseOnce(batch: Batch, path: string): Promise<ParsedFile | null> {
216
+ let p = batch.get(path);
217
+ if (!p) batch.set(path, p = parseFile(path));
218
+ return p;
219
+ }
220
+
221
+ async function walk(entry: string, batch: Batch = new Map()): Promise<FunctionGraph> {
222
+ const g: FunctionGraph = {
223
+ files: new Set([entry]),
224
+ packages: new Map(),
225
+ remotes: new Set(),
226
+ };
227
+ let frontier = [entry];
228
+ while (frontier.length > 0) {
229
+ const results = await Promise.all(frontier.map((f) => parseOnce(batch, f)));
230
+ const next: string[] = [];
231
+ for (const r of results) {
232
+ if (!r) continue;
233
+ for (const pkg of r.packages) g.packages.set(packageKey(pkg), pkg);
234
+ for (const url of r.remotes) g.remotes.add(url);
235
+ for (const f of r.files) {
236
+ if (g.files.has(f)) continue;
237
+ g.files.add(f);
238
+ next.push(f);
239
+ }
240
+ }
241
+ frontier = next;
242
+ }
243
+ return g;
244
+ }
245
+
246
+ function indexAdd(name: string, file: string): void {
247
+ const k = fileKey(file);
248
+ let set = reverse.get(k);
249
+ if (!set) reverse.set(k, set = new Set());
250
+ set.add(name);
251
+ }
252
+
253
+ function indexRemove(name: string, file: string): void {
254
+ const k = fileKey(file);
255
+ const set = reverse.get(k);
256
+ if (!set) return;
257
+ set.delete(name);
258
+ if (set.size === 0) reverse.delete(k);
259
+ }
260
+
261
+ function replace(name: string, fresh: FunctionGraph): void {
262
+ const old = perName.get(name);
263
+ if (old) {
264
+ for (const f of old.files) if (!fresh.files.has(f)) indexRemove(name, f);
265
+ }
266
+ for (const f of fresh.files) {
267
+ if (!old || !old.files.has(f)) indexAdd(name, f);
268
+ }
269
+ perName.set(name, fresh);
270
+ }
271
+
272
+ return {
273
+ async refresh(name, entry) {
274
+ replace(name, await walk(toPath(entry)));
275
+ },
276
+ async refreshMany(entries) {
277
+ const batch: Batch = new Map();
278
+ await Promise.all(
279
+ [...entries].map(async (e) => replace(e.name, await walk(toPath(e.entry), batch))),
280
+ );
281
+ },
282
+ forget(name) {
283
+ const old = perName.get(name);
284
+ if (!old) return;
285
+ for (const f of old.files) indexRemove(name, f);
286
+ perName.delete(name);
287
+ },
288
+ affected(changedFiles) {
289
+ const out = new Set<string>();
290
+ for (const raw of changedFiles) {
291
+ const owners = reverse.get(fileKey(toPath(raw)));
292
+ if (!owners) continue;
293
+ for (const n of owners) out.add(n);
294
+ }
295
+ return out;
296
+ },
297
+ filesFor(name) {
298
+ return [...(perName.get(name)?.files ?? [])];
299
+ },
300
+ packagesFor(name) {
301
+ return [...(perName.get(name)?.packages.values() ?? [])];
302
+ },
303
+ remotesFor(name) {
304
+ return [...(perName.get(name)?.remotes ?? [])];
305
+ },
306
+ get size() {
307
+ return reverse.size;
308
+ },
309
+ };
310
+ }