1tube 0.1.7 → 0.1.9

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/deno.json CHANGED
@@ -14,6 +14,13 @@
14
14
  "@std/assert": "jsr:@std/assert@^1",
15
15
  "hono": "npm:hono@4"
16
16
  },
17
+ "lint": {
18
+ "rules": {
19
+ "exclude": [
20
+ "no-import-prefix"
21
+ ]
22
+ }
23
+ },
17
24
  "nodeModulesDir": "auto",
18
25
  "allowScripts": {
19
26
  "allow": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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,140 @@
1
+ /**
2
+ * TTY-aware live boot progress for function discovery.
3
+ *
4
+ * Problem: with `concurrency=8` (the discovery default), all eight imports
5
+ * start in parallel and don't finish for several seconds. The previous
6
+ * "print one line per completion" UI showed nothing during that window —
7
+ * users assumed the process was hung. This module fills the gap with a
8
+ * single in-place updated status line that shows what's currently being
9
+ * compiled, plus elapsed time. Completed lines still print above it as
10
+ * they finish, so scrollback is preserved.
11
+ *
12
+ * On non-TTY stdouts (piped output, CI logs) the spinner is silent and we
13
+ * fall back to printing "starting" markers — same total information, no
14
+ * cursor manipulation that would render as garbage in a log file.
15
+ */
16
+
17
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
18
+ const REFRESH_MS = 100;
19
+ /** Truncate the in-flight name list so the line fits a typical terminal. */
20
+ const MAX_LINE = 100;
21
+
22
+ export interface BootProgress {
23
+ /** Initialise / reset for a new batch of `total` items. */
24
+ start(total: number): void;
25
+ /** An import has begun. */
26
+ onStart(name: string): void;
27
+ /** An item finished — print `line` (the formatted completion) above the spinner. */
28
+ onFinish(line: string, name: string): void;
29
+ /** Stop refreshing and clear the live line. Call from a try/finally. */
30
+ stop(): void;
31
+ }
32
+
33
+ interface Sink {
34
+ isTerminal(): boolean;
35
+ writeSync(chunk: Uint8Array): number;
36
+ }
37
+
38
+ /**
39
+ * Build a progress renderer. `sink` defaults to Deno.stdout but can be
40
+ * overridden in tests.
41
+ */
42
+ export function createBootProgress(sink: Sink = Deno.stdout): BootProgress {
43
+ const enc = new TextEncoder();
44
+ const isTty = (() => {
45
+ try {
46
+ return sink.isTerminal();
47
+ } catch {
48
+ return false;
49
+ }
50
+ })();
51
+
52
+ const active = new Set<string>();
53
+ let completed = 0;
54
+ let total = 0;
55
+ let startedAt = 0;
56
+ let timer: number | undefined;
57
+ let frame = 0;
58
+ // Track how many cells the live line currently occupies so we can erase
59
+ // it cleanly even if it wrapped.
60
+ let lastDrawn = "";
61
+
62
+ const write = (s: string) => {
63
+ try {
64
+ sink.writeSync(enc.encode(s));
65
+ } catch {
66
+ // closed pipe — give up silently rather than crash boot.
67
+ }
68
+ };
69
+
70
+ const eraseLive = () => {
71
+ if (!isTty || lastDrawn.length === 0) return;
72
+ // CR + clear-to-end-of-line. Works in any VT100-compatible terminal.
73
+ write("\r\x1b[K");
74
+ lastDrawn = "";
75
+ };
76
+
77
+ const draw = () => {
78
+ if (!isTty) return;
79
+ const elapsed = ((performance.now() - startedAt) / 1000).toFixed(1);
80
+ const spin = SPINNER[frame++ % SPINNER.length];
81
+ const names = [...active];
82
+ let inflight: string;
83
+ if (names.length === 0) {
84
+ inflight = "waiting…";
85
+ } else if (names.length <= 4) {
86
+ inflight = names.join(", ");
87
+ } else {
88
+ inflight = names.slice(0, 4).join(", ") + ` +${names.length - 4} more`;
89
+ }
90
+ let line =
91
+ `\x1b[36m${spin}\x1b[0m \x1b[1m${completed}/${total}\x1b[0m loaded, ` +
92
+ `${active.size} in progress: \x1b[2m${inflight}\x1b[0m \x1b[2m(${elapsed}s)\x1b[0m`;
93
+ if (line.length > MAX_LINE) {
94
+ line = line.slice(0, MAX_LINE - 1) + "…";
95
+ }
96
+ eraseLive();
97
+ write(line);
98
+ lastDrawn = line;
99
+ };
100
+
101
+ return {
102
+ start(t) {
103
+ total = t;
104
+ completed = 0;
105
+ active.clear();
106
+ startedAt = performance.now();
107
+ frame = 0;
108
+ lastDrawn = "";
109
+ if (isTty && t > 0) {
110
+ timer = setInterval(draw, REFRESH_MS) as unknown as number;
111
+ // Draw immediately so users see *something* before the first onStart.
112
+ draw();
113
+ }
114
+ },
115
+ onStart(name) {
116
+ active.add(name);
117
+ // Non-TTY: announce starts inline so logs still show progress between
118
+ // completions. We rate-limit to one announcement per batch by only
119
+ // emitting when nothing else is in-flight yet.
120
+ if (!isTty && active.size <= 4) {
121
+ write(`[1tube] \x1b[2m→ compiling ${name}\x1b[0m\n`);
122
+ }
123
+ draw();
124
+ },
125
+ onFinish(line, name) {
126
+ active.delete(name);
127
+ completed++;
128
+ eraseLive();
129
+ write(line + "\n");
130
+ draw();
131
+ },
132
+ stop() {
133
+ if (timer !== undefined) {
134
+ clearInterval(timer);
135
+ timer = undefined;
136
+ }
137
+ eraseLive();
138
+ },
139
+ };
140
+ }
package/src/discovery.ts CHANGED
@@ -34,6 +34,14 @@ export interface DiscoveryOptions {
34
34
  concurrency?: number;
35
35
  /** Invoked once per function as it finishes (success or failure). */
36
36
  onProgress?: (p: DiscoveryProgress) => void;
37
+ /**
38
+ * Invoked once per function the moment its dynamic import is dispatched
39
+ * (or, in lazy mode, the moment it's registered as a candidate). Useful
40
+ * for live progress UIs that want to show what's currently in-flight,
41
+ * not just what's completed — important because a single transpile can
42
+ * block a worker for several seconds with no completion events.
43
+ */
44
+ onStart?: (p: { name: string; index: number; total: number }) => void;
37
45
  /**
38
46
  * When true, candidates are registered for lazy import on first dispatch.
39
47
  * Functions whose manifest sets `warm: true` are still imported eagerly
@@ -109,6 +117,7 @@ export async function discoverAndLoad(
109
117
  }
110
118
 
111
119
  const total = work.length;
120
+ let started = 0;
112
121
  let completed = 0;
113
122
 
114
123
  const loadOne = async (name: string): Promise<void> => {
@@ -121,6 +130,7 @@ export async function discoverAndLoad(
121
130
  moduleUrl.searchParams.set("v", options.cacheBust);
122
131
  }
123
132
 
133
+ options?.onStart?.({ name, index: ++started, total });
124
134
  const start = performance.now();
125
135
  try {
126
136
  // Load the manifest first so it is attached to the registry before the
@@ -8,7 +8,7 @@
8
8
  * requests).
9
9
  */
10
10
 
11
- import type { Context, Next } from "hono";
11
+ import type { Context, Next } from "npm:hono@4";
12
12
 
13
13
  const ALLOWED_HEADERS =
14
14
  "authorization, x-client-info, apikey, content-type, x-application-name, user-agent";
@@ -9,7 +9,7 @@
9
9
  * grow it unboundedly with synthetic high-cardinality function names.
10
10
  */
11
11
 
12
- import type { Context, Next } from "hono";
12
+ import type { Context, Next } from "npm:hono@4";
13
13
  import { logError, logInfo } from "../log-buffer.ts";
14
14
 
15
15
  export interface FunctionMetrics {
@@ -8,8 +8,8 @@
8
8
  * mint fresh buckets.
9
9
  */
10
10
 
11
- import type { Context, Next } from "hono";
12
- import { getConnInfo } from "hono/deno";
11
+ import type { Context, Next } from "npm:hono@4";
12
+ import { getConnInfo } from "npm:hono@4/deno";
13
13
  import type { FunctionRegistry } from "../registry.ts";
14
14
 
15
15
  interface Bucket {
package/src/health.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * strings leak into access logs, browser history, and proxy buffers).
9
9
  */
10
10
 
11
- import type { Context } from "hono";
11
+ import type { Context } from "npm:hono@4";
12
12
  import type { FunctionRegistry } from "./registry.ts";
13
13
  import type { FunctionSupervisor } from "./supervisor.ts";
14
14
  import { getCollectedMetrics, getPrometheusMetrics } from "./gateway/logging.ts";
package/src/server.ts CHANGED
@@ -11,14 +11,17 @@
11
11
  * deno run --allow-all src/server.ts --dev --hmr # local development
12
12
  */
13
13
 
14
- import { Hono } from "hono";
15
- import { bodyLimit } from "hono/body-limit";
16
- // Use node: specifiers (not "@std/path") so this entry point also works when
17
- // 1tube is launched from a host project's node_modules — bare specifiers
18
- // would otherwise require the host's deno.json to mirror our import map.
14
+ // All third-party imports below use `npm:` / `node:` / `jsr:` specifiers
15
+ // directly (not bare aliases from our deno.json) so this entry point loads
16
+ // cleanly when launched from a host project's node_modules — bare
17
+ // specifiers would otherwise require the host's deno.json to mirror our
18
+ // import map. Same convention applies to every src/ file.
19
+ import { Hono } from "npm:hono@4";
20
+ import { bodyLimit } from "npm:hono@4/body-limit";
19
21
  import { relative, sep as SEPARATOR } from "node:path";
20
22
  import { currentRequestStorage, FunctionRegistry } from "./registry.ts";
21
23
  import { discoverAndLoad, type DiscoveryProgress } from "./discovery.ts";
24
+ import { createBootProgress } from "./boot-progress.ts";
22
25
  import { validateRequest } from "./gateway/auth.ts";
23
26
  import { watchdogBody } from "./gateway/body-watchdog.ts";
24
27
  import { corsMiddleware } from "./gateway/cors.ts";
@@ -62,9 +65,12 @@ function parseArgs(): CliOpts {
62
65
 
63
66
  let dev = envFlag("1TUBE_DEV");
64
67
  let hmr = envFlag("1TUBE_HMR");
65
- // Lazy-load is on by default; opt out with --eager / 1TUBE_LAZY=0.
68
+ // Eager loading is the default — first-request latency reflects real
69
+ // function cost (no on-demand transpile tax). Opt in to lazy with
70
+ // --lazy or 1TUBE_LAZY=1 when boot speed matters more than first-hit
71
+ // latency (e.g. sites with hundreds of rarely-called functions).
66
72
  const lazyEnv = Deno.env.get("1TUBE_LAZY");
67
- let lazy = lazyEnv === undefined ? true : !(lazyEnv === "0" || lazyEnv === "false");
73
+ let lazy = lazyEnv === undefined ? false : (lazyEnv === "1" || lazyEnv === "true");
68
74
  let port = parseInt(Deno.env.get("PORT") || "3100", 10);
69
75
  let host = (Deno.env.get("1TUBE_HOST") || "127.0.0.1").trim();
70
76
  let functionsPath = Deno.env.get("FUNCTIONS_PATH") || "./supabase/functions";
@@ -296,16 +302,45 @@ async function reloadFunctions(
296
302
  // HMR reloads always import eagerly so the change is observable on the
297
303
  // very next request — even when boot-time discovery was lazy.
298
304
  const useLazy = opts.lazy && isFullReload;
299
- const { loaded, skipped, errors, removed, deferred } = await discoverAndLoad(
300
- opts.functionsPath,
301
- registry,
302
- {
303
- cacheBust,
304
- only: isFullReload ? undefined : changedFunctionNames,
305
- onProgress: (p) => console.log(progressLine(p)),
306
- lazy: useLazy,
307
- },
308
- );
305
+
306
+ // Live progress: shows a spinner with currently in-flight imports so the
307
+ // operator doesn't stare at a silent terminal during the multi-second
308
+ // first-transpile window. Falls back to plain "starting…" lines when
309
+ // stdout isn't a TTY (CI logs, output redirection).
310
+ const progress = createBootProgress();
311
+ // Skip the live spinner for tiny HMR reloads (one or two functions) —
312
+ // the existing per-completion line is enough and a spinner would just
313
+ // flicker on screen.
314
+ const useSpinner = isFullReload ||
315
+ (changedFunctionNames instanceof Set && changedFunctionNames.size > 2);
316
+
317
+ let result;
318
+ try {
319
+ if (useSpinner) progress.start(0); // total filled in once we see the first onStart
320
+ result = await discoverAndLoad(
321
+ opts.functionsPath,
322
+ registry,
323
+ {
324
+ cacheBust,
325
+ only: isFullReload ? undefined : changedFunctionNames,
326
+ onStart: useSpinner
327
+ ? (p) => {
328
+ // Late-bind the total on the first start event — by then we
329
+ // know the real work-list size.
330
+ if (p.index === 1) progress.start(p.total);
331
+ progress.onStart(p.name);
332
+ }
333
+ : undefined,
334
+ onProgress: useSpinner
335
+ ? (p) => progress.onFinish(progressLine(p), p.name)
336
+ : (p) => console.log(progressLine(p)),
337
+ lazy: useLazy,
338
+ },
339
+ );
340
+ } finally {
341
+ progress.stop();
342
+ }
343
+ const { loaded, skipped, errors, removed, deferred } = result;
309
344
 
310
345
  for (const name of removed) {
311
346
  if (registry.delete(name)) {