1tube 0.1.20 → 0.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "private": false,
5
5
  "description": "Self-hosted Supabase Edge Functions gateway for Deno, plus an ESLint plugin for consumers",
6
6
  "license": "MIT",
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Deno backend HMR snapshotting.
3
+ *
4
+ * Deno's in-process module cache is keyed by module URL and cannot be
5
+ * invalidated. Importing `index.ts?v=...` refreshes only the entrypoint;
6
+ * relative children such as `./types.ts` keep their old file URLs. HMR reloads
7
+ * therefore import from a fresh snapshot path per generation, which gives the
8
+ * entire relative dependency graph new module identities.
9
+ */
10
+
11
+ import { basename, dirname, join, resolve as resolvePath } from "node:path";
12
+
13
+ const DEFAULT_KEEP_GENERATIONS = 3;
14
+
15
+ export type SnapshotChanged = ReadonlySet<string> | "all";
16
+
17
+ export interface DenoHmrSnapshotOptions {
18
+ /** Real functions directory, e.g. `supabase/functions`. */
19
+ functionsDir: string;
20
+ /** Changed function names, or `"all"` for shared/full reloads. */
21
+ changed: SnapshotChanged;
22
+ /** Monotonic generation number owned by the caller. */
23
+ generation: number;
24
+ /** Cache root; defaults to `<cwd>/.1tube-cache/deno-hmr`. */
25
+ cacheDir?: string;
26
+ /** Number of newest generations to keep on disk. */
27
+ keepGenerations?: number;
28
+ }
29
+
30
+ export interface DenoHmrSnapshot {
31
+ /** Generation root, e.g. `.1tube-cache/deno-hmr/gen-4`. */
32
+ root: string;
33
+ /** Snapshot functions root passed to the discovery importer. */
34
+ functionsDir: string;
35
+ /** Names of top-level directories materialized into this snapshot. */
36
+ copiedDirs: string[];
37
+ }
38
+
39
+ function defaultCacheDir(): string {
40
+ return join(Deno.cwd(), ".1tube-cache", "deno-hmr");
41
+ }
42
+
43
+ function isSharedDir(name: string): boolean {
44
+ return name.startsWith("_") || name.endsWith("_shared");
45
+ }
46
+
47
+ async function copyEntry(src: string, dest: string): Promise<void> {
48
+ const info = await Deno.lstat(src);
49
+
50
+ if (info.isSymlink) {
51
+ const target = await Deno.readLink(src);
52
+ try {
53
+ await Deno.symlink(target, dest);
54
+ return;
55
+ } catch {
56
+ // Windows often disallows symlink creation for normal users. Fall
57
+ // through to copying the resolved target so the snapshot remains usable.
58
+ const stat = await Deno.stat(src);
59
+ if (stat.isDirectory) {
60
+ await copyDirectory(src, dest);
61
+ } else {
62
+ await Deno.copyFile(src, dest);
63
+ }
64
+ return;
65
+ }
66
+ }
67
+
68
+ if (info.isDirectory) {
69
+ await copyDirectory(src, dest);
70
+ return;
71
+ }
72
+
73
+ if (info.isFile) {
74
+ await Deno.mkdir(dirname(dest), { recursive: true });
75
+ await Deno.copyFile(src, dest);
76
+ }
77
+ }
78
+
79
+ async function copyDirectory(src: string, dest: string): Promise<void> {
80
+ await Deno.mkdir(dest, { recursive: true });
81
+ for await (const entry of Deno.readDir(src)) {
82
+ await copyEntry(join(src, entry.name), join(dest, entry.name));
83
+ }
84
+ }
85
+
86
+ async function existingTopLevelDirs(functionsDir: string): Promise<string[]> {
87
+ const dirs: string[] = [];
88
+ for await (const entry of Deno.readDir(functionsDir)) {
89
+ if (entry.isDirectory) dirs.push(entry.name);
90
+ }
91
+ dirs.sort();
92
+ return dirs;
93
+ }
94
+
95
+ async function pruneGenerations(
96
+ cacheDir: string,
97
+ keepGenerations: number,
98
+ ): Promise<void> {
99
+ const generations: Array<{ name: string; n: number }> = [];
100
+ try {
101
+ for await (const entry of Deno.readDir(cacheDir)) {
102
+ if (!entry.isDirectory) continue;
103
+ const match = /^gen-(\d+)$/.exec(entry.name);
104
+ if (match) generations.push({ name: entry.name, n: Number(match[1]) });
105
+ }
106
+ } catch (err) {
107
+ if (err instanceof Deno.errors.NotFound) return;
108
+ throw err;
109
+ }
110
+
111
+ generations.sort((a, b) => b.n - a.n);
112
+ for (const old of generations.slice(Math.max(1, keepGenerations))) {
113
+ await Deno.remove(join(cacheDir, old.name), { recursive: true }).catch(
114
+ () => {},
115
+ );
116
+ }
117
+ }
118
+
119
+ export async function createDenoHmrSnapshot(
120
+ options: DenoHmrSnapshotOptions,
121
+ ): Promise<DenoHmrSnapshot> {
122
+ const sourceRoot = await Deno.realPath(options.functionsDir);
123
+ const cacheDir = resolvePath(options.cacheDir ?? defaultCacheDir());
124
+ const keepGenerations = Math.max(
125
+ 1,
126
+ options.keepGenerations ?? DEFAULT_KEEP_GENERATIONS,
127
+ );
128
+ const root = join(cacheDir, `gen-${options.generation}`);
129
+ const snapshotFunctionsDir = join(root, basename(sourceRoot));
130
+
131
+ await Deno.remove(root, { recursive: true }).catch((err) => {
132
+ if (!(err instanceof Deno.errors.NotFound)) throw err;
133
+ });
134
+ await Deno.mkdir(snapshotFunctionsDir, { recursive: true });
135
+
136
+ const topLevelDirs = await existingTopLevelDirs(sourceRoot);
137
+ const selected = (() => {
138
+ if (options.changed === "all") return topLevelDirs;
139
+ const changed = options.changed;
140
+ return topLevelDirs.filter((name) =>
141
+ isSharedDir(name) || changed.has(name)
142
+ );
143
+ })();
144
+
145
+ for (const name of selected) {
146
+ await copyDirectory(
147
+ join(sourceRoot, name),
148
+ join(snapshotFunctionsDir, name),
149
+ );
150
+ }
151
+
152
+ await pruneGenerations(cacheDir, keepGenerations);
153
+
154
+ return {
155
+ root,
156
+ functionsDir: snapshotFunctionsDir,
157
+ copiedDirs: selected,
158
+ };
159
+ }
package/src/discovery.ts CHANGED
@@ -28,6 +28,13 @@ export interface DiscoveryProgress {
28
28
  export interface DiscoveryOptions {
29
29
  /** When set, appended as `?v=` to module URLs to force re-transpilation. */
30
30
  cacheBust?: string;
31
+ /**
32
+ * Optional alternate root for dynamic imports. Discovery and manifests still
33
+ * read from `functionsDir`; only module URLs are resolved under this root.
34
+ * Used by Deno-backend HMR snapshots so transitive relative imports get a
35
+ * fresh file URL namespace.
36
+ */
37
+ importRoot?: string;
31
38
  /** Restrict loading to this set of function names (others are skipped entirely). */
32
39
  only?: ReadonlySet<string>;
33
40
  /** Max number of concurrent dynamic imports. Defaults to 8. */
@@ -73,6 +80,9 @@ export async function discoverAndLoad(
73
80
  const errors: Array<{ name: string; error: string }> = [];
74
81
 
75
82
  const resolvedDir = await Deno.realPath(functionsDir);
83
+ const importRoot = options?.importRoot
84
+ ? await Deno.realPath(options.importRoot)
85
+ : resolvedDir;
76
86
 
77
87
  const candidates: string[] = [];
78
88
  for await (const entry of Deno.readDir(resolvedDir)) {
@@ -121,7 +131,7 @@ export async function discoverAndLoad(
121
131
  let completed = 0;
122
132
 
123
133
  const loadOne = async (name: string): Promise<void> => {
124
- const indexPath = join(resolvedDir, name, "index.ts");
134
+ const indexPath = join(importRoot, name, "index.ts");
125
135
  // pathToFileURL is the node:url equivalent of std/path's toFileUrl;
126
136
  // both produce a `file://` URL whose searchParams we mutate below for
127
137
  // HMR cache-busting.
@@ -143,7 +153,11 @@ export async function discoverAndLoad(
143
153
  // on demand. This is the single biggest startup-time win for sites
144
154
  // with many rarely-called functions.
145
155
  if (options?.lazy && !manifest.warm) {
146
- registry.registerCandidate({ name, moduleUrl: moduleUrl.href, manifest });
156
+ registry.registerCandidate({
157
+ name,
158
+ moduleUrl: moduleUrl.href,
159
+ manifest,
160
+ });
147
161
  deferred.push(name);
148
162
  const durationMs = performance.now() - start;
149
163
  options?.onProgress?.({
@@ -160,7 +174,11 @@ export async function discoverAndLoad(
160
174
  // Even in lazy mode, eagerly-warm functions still register a candidate
161
175
  // so HMR reloads can re-import via the same URL.
162
176
  if (options?.lazy && manifest.warm) {
163
- registry.registerCandidate({ name, moduleUrl: moduleUrl.href, manifest });
177
+ registry.registerCandidate({
178
+ name,
179
+ moduleUrl: moduleUrl.href,
180
+ manifest,
181
+ });
164
182
  }
165
183
  await registry.runWithCurrentFunction(name, async () => {
166
184
  await import(moduleUrl.href);
@@ -193,13 +211,16 @@ export async function discoverAndLoad(
193
211
  // internally, but we still benefit from overlapping transpile + JSR fetches.
194
212
  const concurrency = Math.max(1, options?.concurrency ?? 8);
195
213
  let cursor = 0;
196
- const workers = Array.from({ length: Math.min(concurrency, work.length) }, async () => {
197
- while (true) {
198
- const i = cursor++;
199
- if (i >= work.length) return;
200
- await loadOne(work[i]);
201
- }
202
- });
214
+ const workers = Array.from(
215
+ { length: Math.min(concurrency, work.length) },
216
+ async () => {
217
+ while (true) {
218
+ const i = cursor++;
219
+ if (i >= work.length) return;
220
+ await loadOne(work[i]);
221
+ }
222
+ },
223
+ );
203
224
  await Promise.all(workers);
204
225
 
205
226
  return { loaded, skipped, errors, removed, deferred };
package/src/server.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  // cleanly when launched from a host project's node_modules — bare
17
17
  // specifiers would otherwise require the host's deno.json to mirror our
18
18
  // import map. Same convention applies to every src/ file.
19
- import { Hono, type Context } from "npm:hono@4";
19
+ import { type Context, Hono } from "npm:hono@4";
20
20
  import { bodyLimit } from "npm:hono@4/body-limit";
21
21
  import { relative, sep as SEPARATOR } from "node:path";
22
22
  import { currentRequestStorage, FunctionRegistry } from "./registry.ts";
@@ -32,8 +32,15 @@ import { FunctionSupervisor } from "./supervisor.ts";
32
32
  import { installEnvScope, runWithEnvScope } from "./env-scope.ts";
33
33
  import { flushLogs } from "./log-buffer.ts";
34
34
  import { VERSION } from "./version.ts";
35
- import { createWorkerdBackend, type WorkerdBackend } from "./backends/workerd/backend.ts";
36
- import { createWorkerdHotReloader, type WorkerdHotReloader } from "./backends/workerd/hot-reloader.ts";
35
+ import { createDenoHmrSnapshot } from "./deno-hmr-snapshot.ts";
36
+ import {
37
+ createWorkerdBackend,
38
+ type WorkerdBackend,
39
+ } from "./backends/workerd/backend.ts";
40
+ import {
41
+ createWorkerdHotReloader,
42
+ type WorkerdHotReloader,
43
+ } from "./backends/workerd/hot-reloader.ts";
37
44
  import {
38
45
  createWorkerdWatchdog,
39
46
  recommendedBudgetBytes,
@@ -182,10 +189,15 @@ function parseArgs(): CliOpts {
182
189
  // --lazy or 1TUBE_LAZY=1 when boot speed matters more than first-hit
183
190
  // latency (e.g. sites with hundreds of rarely-called functions).
184
191
  const lazyEnv = Deno.env.get("1TUBE_LAZY");
185
- let lazy = lazyEnv === undefined ? false : (lazyEnv === "1" || lazyEnv === "true");
192
+ let lazy = lazyEnv === undefined
193
+ ? false
194
+ : (lazyEnv === "1" || lazyEnv === "true");
186
195
  const backendEnv = (Deno.env.get("1TUBE_BACKEND") ?? "").trim().toLowerCase();
187
- let backend: "deno" | "workerd" = backendEnv === "workerd" ? "workerd" : "deno";
188
- let workerdBin: string | undefined = Deno.env.get("1TUBE_WORKERD_BIN") || undefined;
196
+ let backend: "deno" | "workerd" = backendEnv === "workerd"
197
+ ? "workerd"
198
+ : "deno";
199
+ let workerdBin: string | undefined = Deno.env.get("1TUBE_WORKERD_BIN") ||
200
+ undefined;
189
201
  // Operator can either use --workerd-env=A,B,C on the CLI or set
190
202
  // 1TUBE_WORKERD_ENV. The CLI flag wins; otherwise the backend reads
191
203
  // the env var itself so a missing flag still picks up the var.
@@ -211,7 +223,8 @@ function parseArgs(): CliOpts {
211
223
  if (v === "1" || v.toLowerCase() === "true") return DEFAULT_INSPECTOR_ADDR;
212
224
  return v;
213
225
  })();
214
- let prebuiltDir: string | undefined = Deno.env.get("1TUBE_PREBUILT") || undefined;
226
+ let prebuiltDir: string | undefined = Deno.env.get("1TUBE_PREBUILT") ||
227
+ undefined;
215
228
  // Auto-kill leftover workerd processes if the boot preflight finds
216
229
  // port conflicts. CLI flag overrides env var; both default to off.
217
230
  let killStaleWorkerd: boolean = (() => {
@@ -225,19 +238,30 @@ function parseArgs(): CliOpts {
225
238
  const n = parseInt(v, 10);
226
239
  return Number.isFinite(n) && n > 0 ? n : undefined;
227
240
  })();
228
- let workerdCompatibilityDate: string | undefined = Deno.env.get("1TUBE_WORKERD_COMPAT_DATE") || undefined;
229
- const workerdCompatibilityFlags: string[] = (Deno.env.get("1TUBE_WORKERD_COMPAT_FLAGS") ?? "")
230
- .split(",")
231
- .map((s) => s.trim())
232
- .filter((s) => s.length > 0);
241
+ let workerdCompatibilityDate: string | undefined =
242
+ Deno.env.get("1TUBE_WORKERD_COMPAT_DATE") || undefined;
243
+ const workerdCompatibilityFlags: string[] =
244
+ (Deno.env.get("1TUBE_WORKERD_COMPAT_FLAGS") ?? "")
245
+ .split(",")
246
+ .map((s) => s.trim())
247
+ .filter((s) => s.length > 0);
233
248
  let workerdExperimental = envFlag("1TUBE_WORKERD_EXPERIMENTAL");
234
249
  let port = parseInt(Deno.env.get("PORT") || "3100", 10);
235
250
  let host = (Deno.env.get("1TUBE_HOST") || "127.0.0.1").trim();
236
251
  let functionsPath = Deno.env.get("FUNCTIONS_PATH") || "./supabase/functions";
237
- let defaultTimeoutMs = parseInt(Deno.env.get("FUNCTION_TIMEOUT_MS") || "150000", 10);
252
+ let defaultTimeoutMs = parseInt(
253
+ Deno.env.get("FUNCTION_TIMEOUT_MS") || "150000",
254
+ 10,
255
+ );
238
256
  const bodyLimitMb = parseFloat(Deno.env.get("1TUBE_BODY_LIMIT_MB") || "30");
239
- const bodyReadIdleMs = parseInt(Deno.env.get("1TUBE_BODY_READ_MS") || "30000", 10);
240
- const shutdownGraceMs = parseInt(Deno.env.get("1TUBE_SHUTDOWN_GRACE_MS") || "10000", 10);
257
+ const bodyReadIdleMs = parseInt(
258
+ Deno.env.get("1TUBE_BODY_READ_MS") || "30000",
259
+ 10,
260
+ );
261
+ const shutdownGraceMs = parseInt(
262
+ Deno.env.get("1TUBE_SHUTDOWN_GRACE_MS") || "10000",
263
+ 10,
264
+ );
241
265
 
242
266
  for (let i = 0; i < args.length; i++) {
243
267
  const a = args[i];
@@ -260,14 +284,22 @@ function parseArgs(): CliOpts {
260
284
  } else if (a === "--backend" && args[i + 1]) {
261
285
  const v = args[++i].toLowerCase();
262
286
  if (v !== "deno" && v !== "workerd") {
263
- console.error(`[1tube] FATAL: --backend must be 'deno' or 'workerd', got ${JSON.stringify(v)}`);
287
+ console.error(
288
+ `[1tube] FATAL: --backend must be 'deno' or 'workerd', got ${
289
+ JSON.stringify(v)
290
+ }`,
291
+ );
264
292
  Deno.exit(EXIT_CODES.USAGE);
265
293
  }
266
294
  backend = v;
267
295
  } else if (a.startsWith("--backend=")) {
268
296
  const v = a.slice("--backend=".length).toLowerCase();
269
297
  if (v !== "deno" && v !== "workerd") {
270
- console.error(`[1tube] FATAL: --backend must be 'deno' or 'workerd', got ${JSON.stringify(v)}`);
298
+ console.error(
299
+ `[1tube] FATAL: --backend must be 'deno' or 'workerd', got ${
300
+ JSON.stringify(v)
301
+ }`,
302
+ );
271
303
  Deno.exit(EXIT_CODES.USAGE);
272
304
  }
273
305
  backend = v;
@@ -322,18 +354,26 @@ function parseArgs(): CliOpts {
322
354
  workerdCompatibilityFlags.push(args[++i]);
323
355
  } else if (a.startsWith("--compat-flag=")) {
324
356
  workerdCompatibilityFlags.push(a.slice("--compat-flag=".length));
325
- } else if (a === "--workerd-experimental" || a === "--workerd-experimental=true") {
357
+ } else if (
358
+ a === "--workerd-experimental" || a === "--workerd-experimental=true"
359
+ ) {
326
360
  workerdExperimental = true;
327
- } else if (a === "--workerd-experimental=false" || a === "--no-workerd-experimental") {
361
+ } else if (
362
+ a === "--workerd-experimental=false" || a === "--no-workerd-experimental"
363
+ ) {
328
364
  workerdExperimental = false;
329
- } else if (a === "--kill-stale-workerd" || a === "--kill-stale-workerd=true") {
365
+ } else if (
366
+ a === "--kill-stale-workerd" || a === "--kill-stale-workerd=true"
367
+ ) {
330
368
  // Boolean flag — accept both bare-flag and explicit `=true` forms
331
369
  // so we don't surprise scripts that programmatically generate
332
370
  // CLI strings. `--no-kill-stale-workerd` is the explicit override
333
371
  // for cases where the env var is set globally but a particular
334
372
  // run wants the safer behavior.
335
373
  killStaleWorkerd = true;
336
- } else if (a === "--kill-stale-workerd=false" || a === "--no-kill-stale-workerd") {
374
+ } else if (
375
+ a === "--kill-stale-workerd=false" || a === "--no-kill-stale-workerd"
376
+ ) {
337
377
  killStaleWorkerd = false;
338
378
  }
339
379
  }
@@ -421,7 +461,9 @@ async function applyDevDefaults() {
421
461
  if (Deno.env.get(target)) continue;
422
462
  const value = sources
423
463
  .map((source) => statusEnv[source])
424
- .find((candidate) => typeof candidate === "string" && candidate.length > 0);
464
+ .find((candidate) =>
465
+ typeof candidate === "string" && candidate.length > 0
466
+ );
425
467
  if (!value) continue;
426
468
  Deno.env.set(target, value);
427
469
  applied++;
@@ -526,7 +568,9 @@ function isInternalRequest(req: Request): boolean {
526
568
  }
527
569
 
528
570
  function requireInternal(c: Context): Response | null {
529
- return isInternalRequest(c.req.raw) ? null : c.json({ error: "Forbidden" }, 403);
571
+ return isInternalRequest(c.req.raw)
572
+ ? null
573
+ : c.json({ error: "Forbidden" }, 403);
530
574
  }
531
575
 
532
576
  if (opts.dev) {
@@ -535,17 +579,24 @@ if (opts.dev) {
535
579
  enforceProdSecrets();
536
580
  }
537
581
 
538
- const denoVersion = `${Deno.version.deno} (V8 ${Deno.version.v8}, TS ${Deno.version.typescript})`;
582
+ const denoVersion =
583
+ `${Deno.version.deno} (V8 ${Deno.version.v8}, TS ${Deno.version.typescript})`;
539
584
  const bannerText = ` 1tube v${VERSION} · Deno ${denoVersion} `;
540
585
  const bannerLine = "─".repeat(bannerText.length);
541
586
  console.log(`\x1b[36m┌${bannerLine}┐\x1b[0m`);
542
- console.log(`\x1b[36m│\x1b[0m \x1b[1m1tube\x1b[0m v${VERSION} \x1b[2m·\x1b[0m Deno ${denoVersion} \x1b[36m│\x1b[0m`);
587
+ console.log(
588
+ `\x1b[36m│\x1b[0m \x1b[1m1tube\x1b[0m v${VERSION} \x1b[2m·\x1b[0m Deno ${denoVersion} \x1b[36m│\x1b[0m`,
589
+ );
543
590
  console.log(`\x1b[36m└${bannerLine}┘\x1b[0m`);
544
591
  console.log(
545
592
  `[1tube] mode=${opts.dev ? "dev" : "prod"} hmr=${opts.hmr ? "on" : "off"} ` +
546
593
  `lazy=${opts.lazy ? "on" : "off"} backend=${opts.backend} ` +
547
- `host=${opts.host} bodyLimit=${(opts.bodyLimitBytes / 1024 / 1024).toFixed(1)}MB ` +
548
- `bodyReadIdle=${opts.bodyReadIdleMs > 0 ? opts.bodyReadIdleMs + "ms" : "off"}`,
594
+ `host=${opts.host} bodyLimit=${
595
+ (opts.bodyLimitBytes / 1024 / 1024).toFixed(1)
596
+ }MB ` +
597
+ `bodyReadIdle=${
598
+ opts.bodyReadIdleMs > 0 ? opts.bodyReadIdleMs + "ms" : "off"
599
+ }`,
549
600
  );
550
601
 
551
602
  const registry = new FunctionRegistry();
@@ -608,18 +659,19 @@ if (enforceManifest && !allowAll) {
608
659
  // the dispatcher consults). Empty string is a sentinel for "no
609
660
  // functions dir"; the only caller that uses this path is the HMR
610
661
  // watcher, and that's gated off when prebuilt is on.
611
- const resolvedFunctionsPath = opts.prebuiltDir
612
- ? ""
613
- : await (async () => {
614
- try {
615
- return await Deno.realPath(opts.functionsPath);
616
- } catch (err) {
617
- const msg = err instanceof Error ? err.message : String(err);
618
- console.error(`[1tube] FATAL: functions directory not found: ${opts.functionsPath} (${msg})`);
619
- flushLogs();
620
- Deno.exit(EXIT_CODES.CONFIG);
621
- }
622
- })();
662
+ const resolvedFunctionsPath = opts.prebuiltDir ? "" : await (async () => {
663
+ try {
664
+ return await Deno.realPath(opts.functionsPath);
665
+ } catch (err) {
666
+ const msg = err instanceof Error ? err.message : String(err);
667
+ console.error(
668
+ `[1tube] FATAL: functions directory not found: ${opts.functionsPath} (${msg})`,
669
+ );
670
+ flushLogs();
671
+ Deno.exit(EXIT_CODES.CONFIG);
672
+ }
673
+ })();
674
+ let denoHmrGeneration = 0;
623
675
 
624
676
  /**
625
677
  * Map a changed filesystem path to either a function name or `null` ("shared",
@@ -628,7 +680,10 @@ const resolvedFunctionsPath = opts.prebuiltDir
628
680
  */
629
681
  function classifyChangedPath(absPath: string): string | null {
630
682
  const rel = relative(resolvedFunctionsPath, absPath);
631
- if (!rel || rel.startsWith("..") || rel.startsWith(SEPARATOR + "..") || rel === ".") {
683
+ if (
684
+ !rel || rel.startsWith("..") || rel.startsWith(SEPARATOR + "..") ||
685
+ rel === "."
686
+ ) {
632
687
  return null;
633
688
  }
634
689
  const first = rel.split(/[\\/]/, 1)[0];
@@ -651,27 +706,44 @@ async function reloadFunctions(
651
706
  changedFunctionNames: Set<string> | "all",
652
707
  ) {
653
708
  const isFullReload = changedFunctionNames === "all";
709
+ const isInitialBoot = reason === "initial boot";
654
710
  const start = performance.now();
711
+ let importRoot: string | undefined;
655
712
 
656
713
  if (isFullReload) {
657
714
  console.log(`[1tube] Reloading all functions (${reason})...`);
658
- registry.clear();
659
715
  } else {
660
716
  const names = [...changedFunctionNames].sort();
661
717
  if (names.length === 0) return;
662
718
  console.log(
663
- `[1tube] Reloading ${names.length} function(s) (${reason}): ${names.join(", ")}`,
719
+ `[1tube] Reloading ${names.length} function(s) (${reason}): ${
720
+ names.join(", ")
721
+ }`,
664
722
  );
665
723
  }
666
724
 
725
+ if (!isInitialBoot) {
726
+ const snapshot = await createDenoHmrSnapshot({
727
+ functionsDir: opts.functionsPath,
728
+ changed: changedFunctionNames,
729
+ generation: ++denoHmrGeneration,
730
+ });
731
+ importRoot = snapshot.functionsDir;
732
+ }
733
+
734
+ if (isFullReload) {
735
+ registry.clear();
736
+ }
737
+
667
738
  // Cache-bust only when re-importing modules that may already be in Deno's
668
739
  // module cache — i.e. any reload after the initial boot.
669
- const cacheBust =
670
- reason === "initial boot" ? undefined : `${Date.now()}-${crypto.randomUUID()}`;
740
+ const cacheBust = isInitialBoot
741
+ ? undefined
742
+ : `${Date.now()}-${crypto.randomUUID()}`;
671
743
 
672
744
  // HMR reloads always import eagerly so the change is observable on the
673
745
  // very next request — even when boot-time discovery was lazy.
674
- const useLazy = opts.lazy && isFullReload;
746
+ const useLazy = opts.lazy && isInitialBoot;
675
747
 
676
748
  // Append-only "still working" heartbeat. Does not redraw, does not race
677
749
  // with foreign console.log() from imported modules — see
@@ -691,6 +763,7 @@ async function reloadFunctions(
691
763
  registry,
692
764
  {
693
765
  cacheBust,
766
+ importRoot,
694
767
  only: isFullReload ? undefined : changedFunctionNames,
695
768
  onStart: (p) => {
696
769
  // Late-bind the total on the first start event — by then we
@@ -748,7 +821,9 @@ async function reloadFunctions(
748
821
  }
749
822
  } else {
750
823
  console.log(
751
- `[1tube] Reloaded ${loaded.length} function(s) in ${totalMs.toFixed(0)}ms ` +
824
+ `[1tube] Reloaded ${loaded.length} function(s) in ${
825
+ totalMs.toFixed(0)
826
+ }ms ` +
752
827
  `(${registry.size} total registered)`,
753
828
  );
754
829
  }
@@ -778,7 +853,8 @@ if (opts.backend === "workerd") {
778
853
  // Surface the security implication BEFORE we bring workerd up so
779
854
  // an operator who typo'd `--inspector` on a public host has a
780
855
  // chance to ctrl-C before any code runs.
781
- const isLoopback = /^(127\.0\.0\.1|localhost|\[::1\])(:|$)/.test(opts.workerdInspector) ||
856
+ const isLoopback =
857
+ /^(127\.0\.0\.1|localhost|\[::1\])(:|$)/.test(opts.workerdInspector) ||
782
858
  /^(127\.0\.0\.1|localhost):/.test(opts.workerdInspector) ||
783
859
  /^\d+$/.test(opts.workerdInspector);
784
860
  if (!isLoopback) {
@@ -808,14 +884,23 @@ if (opts.backend === "workerd") {
808
884
  `[1tube] auto-kill leftover workerd: ON (--kill-stale-workerd)`,
809
885
  );
810
886
  }
811
- if (opts.workerdCompatibilityDate || (opts.workerdCompatibilityFlags?.length ?? 0) > 0) {
887
+ if (
888
+ opts.workerdCompatibilityDate ||
889
+ (opts.workerdCompatibilityFlags?.length ?? 0) > 0
890
+ ) {
812
891
  console.log(
813
- `[1tube] workerd compatibility: date=${opts.workerdCompatibilityDate ?? "default"} ` +
814
- `flags=${(opts.workerdCompatibilityFlags ?? []).join(",") || "default"}`,
892
+ `[1tube] workerd compatibility: date=${
893
+ opts.workerdCompatibilityDate ?? "default"
894
+ } ` +
895
+ `flags=${
896
+ (opts.workerdCompatibilityFlags ?? []).join(",") || "default"
897
+ }`,
815
898
  );
816
899
  }
817
900
  if (opts.workerdExperimental) {
818
- console.log(`[1tube] workerd process experimental mode: ON (--experimental)`);
901
+ console.log(
902
+ `[1tube] workerd process experimental mode: ON (--experimental)`,
903
+ );
819
904
  }
820
905
  workerdBackend = createWorkerdBackend({
821
906
  functionsDir: opts.functionsPath,
@@ -830,7 +915,9 @@ if (opts.backend === "workerd") {
830
915
  ...(opts.workerdBasePort ? { basePort: opts.workerdBasePort } : {}),
831
916
  ...(opts.workerdInspector ? { inspectorAddr: opts.workerdInspector } : {}),
832
917
  ...(opts.workerdMaxHeapMB ? { maxHeapMB: opts.workerdMaxHeapMB } : {}),
833
- ...(opts.workerdCompatibilityDate ? { compatibilityDate: opts.workerdCompatibilityDate } : {}),
918
+ ...(opts.workerdCompatibilityDate
919
+ ? { compatibilityDate: opts.workerdCompatibilityDate }
920
+ : {}),
834
921
  ...((opts.workerdCompatibilityFlags?.length ?? 0) > 0
835
922
  ? { compatibilityFlags: opts.workerdCompatibilityFlags }
836
923
  : {}),
@@ -841,7 +928,9 @@ if (opts.backend === "workerd") {
841
928
  logLineSink: (line) => {
842
929
  // Surface workerd's own logs through stderr with a tag so they
843
930
  // interleave with [1tube] output cleanly.
844
- try { Deno.stderr.writeSync(new TextEncoder().encode(`[workerd] ${line}\n`)); } catch { /* */ }
931
+ try {
932
+ Deno.stderr.writeSync(new TextEncoder().encode(`[workerd] ${line}\n`));
933
+ } catch { /* */ }
845
934
  },
846
935
  onUnexpectedExit: ({ crashCount, expectedRetry }) => {
847
936
  if (!expectedRetry) {
@@ -893,7 +982,9 @@ if (opts.backend === "workerd") {
893
982
  }
894
983
  const wdMs = performance.now() - wdStart;
895
984
  console.log(
896
- `[1tube] workerd backend ready (v${workerdBackend.workerdVersion ?? "?"}) ` +
985
+ `[1tube] workerd backend ready (v${
986
+ workerdBackend.workerdVersion ?? "?"
987
+ }) ` +
897
988
  `· ${workerdNames.size} function(s) in ${wdMs.toFixed(0)}ms`,
898
989
  );
899
990
  if (workerdNames.size > 0) {
@@ -906,7 +997,10 @@ if (opts.backend === "workerd") {
906
997
  // yields nothing (no manifest declares a memory hint), the
907
998
  // watchdog stays off — silently, so existing deployments aren't
908
999
  // surprised by sudden recycle behaviour.
909
- const explicitCap = parseInt(Deno.env.get("1TUBE_WORKERD_MAX_RSS_MB") || "", 10);
1000
+ const explicitCap = parseInt(
1001
+ Deno.env.get("1TUBE_WORKERD_MAX_RSS_MB") || "",
1002
+ 10,
1003
+ );
910
1004
  const explicitBudget = Number.isFinite(explicitCap) && explicitCap > 0
911
1005
  ? explicitCap * 1024 * 1024
912
1006
  : null;
@@ -915,11 +1009,16 @@ if (opts.backend === "workerd") {
915
1009
  // boot log so operators know what the recommendation was based on.
916
1010
  let declaredSum = 0;
917
1011
  for (const m of workerdBackend.manifests.values()) {
918
- if (typeof m.memoryMB === "number" && m.memoryMB > 0) declaredSum += m.memoryMB;
1012
+ if (typeof m.memoryMB === "number" && m.memoryMB > 0) {
1013
+ declaredSum += m.memoryMB;
1014
+ }
919
1015
  }
920
1016
  const budget = explicitBudget ?? recommended;
921
1017
  if (budget !== null) {
922
- const intervalMs = parseInt(Deno.env.get("1TUBE_WORKERD_RSS_INTERVAL_MS") || "", 10);
1018
+ const intervalMs = parseInt(
1019
+ Deno.env.get("1TUBE_WORKERD_RSS_INTERVAL_MS") || "",
1020
+ 10,
1021
+ );
923
1022
  workerdWatchdog = createWorkerdWatchdog({
924
1023
  backend: workerdBackend,
925
1024
  budgetBytes: budget,
@@ -932,7 +1031,9 @@ if (opts.backend === "workerd") {
932
1031
  ? "1TUBE_WORKERD_MAX_RSS_MB"
933
1032
  : `manifest sum (${declaredSum}MB × 1.5 + 64MB overhead)`;
934
1033
  console.log(
935
- `[1tube] workerd memory watchdog ON: budget=${(budget / 1024 / 1024).toFixed(0)}MB (source: ${src})`,
1034
+ `[1tube] workerd memory watchdog ON: budget=${
1035
+ (budget / 1024 / 1024).toFixed(0)
1036
+ }MB (source: ${src})`,
936
1037
  );
937
1038
  } else {
938
1039
  console.log(
@@ -1052,7 +1153,9 @@ const rpmOverride = Number.isFinite(defaultRpmEnv) && defaultRpmEnv > 0
1052
1153
  ? defaultRpmEnv
1053
1154
  : null;
1054
1155
  if (rpmOverride !== null) {
1055
- console.log(`[1tube] default rpm overridden via 1TUBE_DEFAULT_RPM=${rpmOverride}`);
1156
+ console.log(
1157
+ `[1tube] default rpm overridden via 1TUBE_DEFAULT_RPM=${rpmOverride}`,
1158
+ );
1056
1159
  }
1057
1160
  const rateLimiter = createRateLimiter({
1058
1161
  registry,
@@ -1146,7 +1249,9 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1146
1249
  const decision = supervisor.admit(name);
1147
1250
  if (!decision.ok) {
1148
1251
  const headers: Record<string, string> = {};
1149
- if (decision.retryAfter) headers["Retry-After"] = String(decision.retryAfter);
1252
+ if (decision.retryAfter) {
1253
+ headers["Retry-After"] = String(decision.retryAfter);
1254
+ }
1150
1255
  return c.json(
1151
1256
  { error: "Service temporarily unavailable", reason: decision.reason },
1152
1257
  decision.status as 503,
@@ -1158,18 +1263,22 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1158
1263
  const rawBody = c.req.raw.body;
1159
1264
  const wrappedBody = rawBody !== null
1160
1265
  ? watchdogBody(rawBody, opts.bodyReadIdleMs, abort, (idleMs) => {
1161
- console.warn(
1162
- `[1tube] Body read stalled in "${name}" (no progress in ${idleMs}ms) — aborting request.`,
1163
- );
1164
- }).body
1266
+ console.warn(
1267
+ `[1tube] Body read stalled in "${name}" (no progress in ${idleMs}ms) — aborting request.`,
1268
+ );
1269
+ }).body
1165
1270
  : null;
1166
1271
  // Mirror the Deno-path URL rewrite: user code expects to see the
1167
1272
  // pathname without the `/functions/v1` prefix, matching Supabase
1168
1273
  // Edge Runtime behaviour. Without this strip, hello/echo see the
1169
1274
  // gateway prefix in their `new URL(req.url).pathname`.
1170
1275
  const originalUrl = new URL(c.req.raw.url);
1171
- const rewrittenPath = originalUrl.pathname.replace(/^\/functions\/v1/, "") || "/";
1172
- const rewrittenUrl = new URL(rewrittenPath + originalUrl.search, originalUrl.origin);
1276
+ const rewrittenPath =
1277
+ originalUrl.pathname.replace(/^\/functions\/v1/, "") || "/";
1278
+ const rewrittenUrl = new URL(
1279
+ rewrittenPath + originalUrl.search,
1280
+ originalUrl.origin,
1281
+ );
1173
1282
  const proxyReq = new Request(rewrittenUrl.toString(), {
1174
1283
  method: c.req.raw.method,
1175
1284
  headers: c.req.raw.headers,
@@ -1184,20 +1293,36 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1184
1293
  // gateway-wide default. Same precedence the Deno path uses.
1185
1294
  const timeoutMs = wdManifest?.timeoutMs ?? opts.defaultTimeoutMs;
1186
1295
  const timer = timeoutMs > 0
1187
- ? setTimeout(() => abort.abort(new Error(`workerd dispatch timed out after ${timeoutMs}ms`)), timeoutMs)
1296
+ ? setTimeout(
1297
+ () =>
1298
+ abort.abort(
1299
+ new Error(`workerd dispatch timed out after ${timeoutMs}ms`),
1300
+ ),
1301
+ timeoutMs,
1302
+ )
1188
1303
  : null;
1189
1304
 
1190
1305
  let response: Response;
1191
1306
  let errored = false;
1192
1307
  try {
1193
- response = await workerdBackend.dispatch(proxyReq, name, auth, abort.signal);
1308
+ response = await workerdBackend.dispatch(
1309
+ proxyReq,
1310
+ name,
1311
+ auth,
1312
+ abort.signal,
1313
+ );
1194
1314
  } catch (err) {
1195
1315
  errored = true;
1196
- const reasonMsg = (abort.signal.reason as Error | undefined)?.message ?? "";
1197
- if (err instanceof DOMException && err.name === "AbortError" && reasonMsg.includes("timed out")) {
1316
+ const reasonMsg = (abort.signal.reason as Error | undefined)?.message ??
1317
+ "";
1318
+ if (
1319
+ err instanceof DOMException && err.name === "AbortError" &&
1320
+ reasonMsg.includes("timed out")
1321
+ ) {
1198
1322
  response = c.json({ error: "Function execution timed out" }, 504);
1199
1323
  } else if (
1200
- err instanceof DOMException && err.name === "AbortError" && reasonMsg.includes("Body read stalled")
1324
+ err instanceof DOMException && err.name === "AbortError" &&
1325
+ reasonMsg.includes("Body read stalled")
1201
1326
  ) {
1202
1327
  response = c.json({ error: "Request body read timed out" }, 408);
1203
1328
  } else {
@@ -1218,9 +1343,7 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1218
1343
  const outcome = supervisor.record(name, errored);
1219
1344
  if (outcome.breakerJustTripped) {
1220
1345
  console.warn(
1221
- `[1tube] Circuit breaker OPEN for "${name}" (workerd) — refusing requests for ${
1222
- wdManifest.recycle.cooldownMs
1223
- }ms (errorRate >= ${wdManifest.recycle.errorRate}).`,
1346
+ `[1tube] Circuit breaker OPEN for "${name}" (workerd) — refusing requests for ${wdManifest.recycle.cooldownMs}ms (errorRate >= ${wdManifest.recycle.errorRate}).`,
1224
1347
  );
1225
1348
  }
1226
1349
  if (outcome.recycleJustRecommended) {
@@ -1253,7 +1376,9 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1253
1376
  const decision = supervisor.admit(name);
1254
1377
  if (!decision.ok) {
1255
1378
  const headers: Record<string, string> = {};
1256
- if (decision.retryAfter) headers["Retry-After"] = String(decision.retryAfter);
1379
+ if (decision.retryAfter) {
1380
+ headers["Retry-After"] = String(decision.retryAfter);
1381
+ }
1257
1382
  return c.json(
1258
1383
  { error: "Service temporarily unavailable", reason: decision.reason },
1259
1384
  decision.status as 503,
@@ -1261,12 +1386,17 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1261
1386
  );
1262
1387
  }
1263
1388
 
1264
- const timeoutMs = fn.timeoutMs ?? fn.manifest.timeoutMs ?? opts.defaultTimeoutMs;
1389
+ const timeoutMs = fn.timeoutMs ?? fn.manifest.timeoutMs ??
1390
+ opts.defaultTimeoutMs;
1265
1391
 
1266
1392
  // Strip /functions/v1 prefix to match Supabase Edge Runtime behavior.
1267
1393
  const originalUrl = new URL(c.req.raw.url);
1268
- const rewrittenPath = originalUrl.pathname.replace(/^\/functions\/v1/, "") || "/";
1269
- const rewrittenUrl = new URL(rewrittenPath + originalUrl.search, originalUrl.origin);
1394
+ const rewrittenPath = originalUrl.pathname.replace(/^\/functions\/v1/, "") ||
1395
+ "/";
1396
+ const rewrittenUrl = new URL(
1397
+ rewrittenPath + originalUrl.search,
1398
+ originalUrl.origin,
1399
+ );
1270
1400
 
1271
1401
  const abort = new AbortController();
1272
1402
 
@@ -1280,10 +1410,10 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1280
1410
  const rawBody = c.req.raw.body;
1281
1411
  const wrappedBody = rawBody !== null
1282
1412
  ? watchdogBody(rawBody, opts.bodyReadIdleMs, abort, (idleMs) => {
1283
- console.warn(
1284
- `[1tube] Body read stalled in "${name}" (no progress in ${idleMs}ms) — aborting request.`,
1285
- );
1286
- }).body
1413
+ console.warn(
1414
+ `[1tube] Body read stalled in "${name}" (no progress in ${idleMs}ms) — aborting request.`,
1415
+ );
1416
+ }).body
1287
1417
  : null;
1288
1418
 
1289
1419
  const rewrittenReq = new Request(rewrittenUrl.toString(), {
@@ -1313,10 +1443,10 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1313
1443
  // function) to the correct function — preserved across awaits in 2.7+.
1314
1444
  const inner = enforceManifest && !allowAll
1315
1445
  ? () =>
1316
- runWithEnvScope(
1317
- { functionName: name, allow: new Set(fn.manifest.permissions.env) },
1318
- invoke,
1319
- )
1446
+ runWithEnvScope(
1447
+ { functionName: name, allow: new Set(fn.manifest.permissions.env) },
1448
+ invoke,
1449
+ )
1320
1450
  : invoke;
1321
1451
 
1322
1452
  const handlerPromise = Promise.resolve(
@@ -1330,8 +1460,10 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1330
1460
  const timeout = new Promise<never>((_, reject) =>
1331
1461
  setTimeout(() => {
1332
1462
  abort.abort();
1333
- reject(new Error(`Function "${name}" timed out after ${timeoutMs}ms`));
1334
- }, timeoutMs),
1463
+ reject(
1464
+ new Error(`Function "${name}" timed out after ${timeoutMs}ms`),
1465
+ );
1466
+ }, timeoutMs)
1335
1467
  );
1336
1468
  response = await Promise.race([handlerPromise, timeout]);
1337
1469
  } else {
@@ -1361,9 +1493,7 @@ app.all("/functions/v1/:name{.+}", async (c) => {
1361
1493
  const outcome = supervisor.record(name, errored);
1362
1494
  if (outcome.breakerJustTripped) {
1363
1495
  console.warn(
1364
- `[1tube] Circuit breaker OPEN for "${name}" — refusing requests for ${
1365
- fn.manifest.recycle.cooldownMs
1366
- }ms (errorRate >= ${fn.manifest.recycle.errorRate}).`,
1496
+ `[1tube] Circuit breaker OPEN for "${name}" — refusing requests for ${fn.manifest.recycle.cooldownMs}ms (errorRate >= ${fn.manifest.recycle.errorRate}).`,
1367
1497
  );
1368
1498
  }
1369
1499
  if (outcome.recycleJustRecommended) {
@@ -1426,11 +1556,16 @@ app.get(
1426
1556
  }),
1427
1557
  );
1428
1558
 
1429
- async function runFunctionAdmin(c: Context, action: (backend: WorkerdBackend) => Promise<Response>): Promise<Response> {
1559
+ async function runFunctionAdmin(
1560
+ c: Context,
1561
+ action: (backend: WorkerdBackend) => Promise<Response>,
1562
+ ): Promise<Response> {
1430
1563
  const forbidden = requireInternal(c);
1431
1564
  if (forbidden) return forbidden;
1432
1565
  if (!workerdBackend) {
1433
- return c.json({ error: "Edge function editor is available only with backend=workerd" }, 503);
1566
+ return c.json({
1567
+ error: "Edge function editor is available only with backend=workerd",
1568
+ }, 503);
1434
1569
  }
1435
1570
  try {
1436
1571
  return await action(workerdBackend);
@@ -1440,14 +1575,23 @@ async function runFunctionAdmin(c: Context, action: (backend: WorkerdBackend) =>
1440
1575
  }
1441
1576
  }
1442
1577
 
1443
- app.get("/1tube/api/functions", (c) =>
1444
- runFunctionAdmin(c, async (backend) => c.json({
1445
- functions: await backend.listEditableFunctions(),
1446
- }))
1578
+ app.get(
1579
+ "/1tube/api/functions",
1580
+ (c) =>
1581
+ runFunctionAdmin(c, async (backend) =>
1582
+ c.json({
1583
+ functions: await backend.listEditableFunctions(),
1584
+ })),
1447
1585
  );
1448
1586
 
1449
- app.get("/1tube/api/functions/:name/source", (c) =>
1450
- runFunctionAdmin(c, async (backend) => c.json(await backend.readEditableSource(c.req.param("name"))))
1587
+ app.get(
1588
+ "/1tube/api/functions/:name/source",
1589
+ (c) =>
1590
+ runFunctionAdmin(
1591
+ c,
1592
+ async (backend) =>
1593
+ c.json(await backend.readEditableSource(c.req.param("name"))),
1594
+ ),
1451
1595
  );
1452
1596
 
1453
1597
  app.post("/1tube/api/functions", (c) =>
@@ -1457,36 +1601,58 @@ app.post("/1tube/api/functions", (c) =>
1457
1601
  return c.json({ error: "Missing function name" }, 400);
1458
1602
  }
1459
1603
  const source = await backend.createEditableFunction(body.name);
1460
- return c.json({ source, functions: await backend.listEditableFunctions() }, 201);
1461
- })
1462
- );
1463
-
1464
- app.put("/1tube/api/functions/:name/source", (c) =>
1465
- runFunctionAdmin(c, async (backend) => {
1466
- const body = await c.req.json().catch(() => ({})) as { files?: unknown };
1467
- if (!body.files || typeof body.files !== "object" || Array.isArray(body.files)) {
1468
- return c.json({ error: "Expected JSON body { files: { path: content } }" }, 400);
1469
- }
1470
- const reload = await backend.saveEditableSource(
1471
- c.req.param("name"),
1472
- { files: body.files as Record<string, string> },
1604
+ return c.json(
1605
+ { source, functions: await backend.listEditableFunctions() },
1606
+ 201,
1473
1607
  );
1474
- return c.json({ reload, functions: await backend.listEditableFunctions() });
1475
- })
1608
+ }));
1609
+
1610
+ app.put(
1611
+ "/1tube/api/functions/:name/source",
1612
+ (c) =>
1613
+ runFunctionAdmin(c, async (backend) => {
1614
+ const body = await c.req.json().catch(() => ({})) as { files?: unknown };
1615
+ if (
1616
+ !body.files || typeof body.files !== "object" ||
1617
+ Array.isArray(body.files)
1618
+ ) {
1619
+ return c.json({
1620
+ error: "Expected JSON body { files: { path: content } }",
1621
+ }, 400);
1622
+ }
1623
+ const reload = await backend.saveEditableSource(
1624
+ c.req.param("name"),
1625
+ { files: body.files as Record<string, string> },
1626
+ );
1627
+ return c.json({
1628
+ reload,
1629
+ functions: await backend.listEditableFunctions(),
1630
+ });
1631
+ }),
1476
1632
  );
1477
1633
 
1478
- app.delete("/1tube/api/functions/:name", (c) =>
1479
- runFunctionAdmin(c, async (backend) => {
1480
- const reload = await backend.deleteEditableFunction(c.req.param("name"));
1481
- return c.json({ reload, functions: await backend.listEditableFunctions() });
1482
- })
1634
+ app.delete(
1635
+ "/1tube/api/functions/:name",
1636
+ (c) =>
1637
+ runFunctionAdmin(c, async (backend) => {
1638
+ const reload = await backend.deleteEditableFunction(c.req.param("name"));
1639
+ return c.json({
1640
+ reload,
1641
+ functions: await backend.listEditableFunctions(),
1642
+ });
1643
+ }),
1483
1644
  );
1484
1645
 
1485
- app.post("/1tube/api/functions/:name/revert", (c) =>
1486
- runFunctionAdmin(c, async (backend) => {
1487
- const reload = await backend.revertEditableFunction(c.req.param("name"));
1488
- return c.json({ reload, functions: await backend.listEditableFunctions() });
1489
- })
1646
+ app.post(
1647
+ "/1tube/api/functions/:name/revert",
1648
+ (c) =>
1649
+ runFunctionAdmin(c, async (backend) => {
1650
+ const reload = await backend.revertEditableFunction(c.req.param("name"));
1651
+ return c.json({
1652
+ reload,
1653
+ functions: await backend.listEditableFunctions(),
1654
+ });
1655
+ }),
1490
1656
  );
1491
1657
 
1492
1658
  // Root liveness probe — intentionally minimal so we don't leak the registered
@@ -1515,9 +1681,12 @@ async function shutdown(reason: string) {
1515
1681
  if (shuttingDown) return;
1516
1682
  shuttingDown = true;
1517
1683
  const totalGraceMs = opts.shutdownGraceMs;
1518
- console.log(`[1tube] Shutdown requested (${reason}); draining up to ${totalGraceMs}ms...`);
1684
+ console.log(
1685
+ `[1tube] Shutdown requested (${reason}); draining up to ${totalGraceMs}ms...`,
1686
+ );
1519
1687
  const shutdownStartedAt = performance.now();
1520
- const remaining = () => Math.max(0, totalGraceMs - (performance.now() - shutdownStartedAt));
1688
+ const remaining = () =>
1689
+ Math.max(0, totalGraceMs - (performance.now() - shutdownStartedAt));
1521
1690
 
1522
1691
  watcherAbort?.abort();
1523
1692
 
@@ -1545,7 +1714,9 @@ async function shutdown(reason: string) {
1545
1714
  const drainStart = performance.now();
1546
1715
  const drained = await Promise.race([
1547
1716
  server.finished.then(() => "drained" as const),
1548
- new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), remaining())),
1717
+ new Promise<"timeout">((resolve) =>
1718
+ setTimeout(() => resolve("timeout"), remaining())
1719
+ ),
1549
1720
  ]);
1550
1721
  const drainMs = performance.now() - drainStart;
1551
1722
  if (drained === "timeout") {
@@ -1554,7 +1725,11 @@ async function shutdown(reason: string) {
1554
1725
  `forcing workerd teardown — some in-flight requests may have been dropped.`,
1555
1726
  );
1556
1727
  } else {
1557
- console.log(`[1tube] Gateway drained ${drainMs.toFixed(0)}ms; tearing down workerd...`);
1728
+ console.log(
1729
+ `[1tube] Gateway drained ${
1730
+ drainMs.toFixed(0)
1731
+ }ms; tearing down workerd...`,
1732
+ );
1558
1733
  }
1559
1734
 
1560
1735
  // Phase 3: stop the workerd subprocess. Now safe to tear down —
@@ -1571,11 +1746,15 @@ async function shutdown(reason: string) {
1571
1746
  console.warn("[1tube] workerd backend stop() error:", err);
1572
1747
  return "stopped" as const;
1573
1748
  }),
1574
- new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), wdGraceMs)),
1749
+ new Promise<"timeout">((resolve) =>
1750
+ setTimeout(() => resolve("timeout"), wdGraceMs)
1751
+ ),
1575
1752
  ]);
1576
1753
  const wdMs = performance.now() - wdStart;
1577
1754
  if (wdResult === "timeout") {
1578
- console.warn(`[1tube] workerd stop() did not return within ${wdGraceMs}ms — abandoning.`);
1755
+ console.warn(
1756
+ `[1tube] workerd stop() did not return within ${wdGraceMs}ms — abandoning.`,
1757
+ );
1579
1758
  } else {
1580
1759
  console.log(`[1tube] workerd stopped in ${wdMs.toFixed(0)}ms.`);
1581
1760
  }