1tube 0.1.33 → 0.1.35

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/README.md CHANGED
@@ -120,7 +120,12 @@ Deferred boot (default whenever `--dev` or `--hmr` is on) flips the order:
120
120
  the gateway starts serving right away. Time-to-first-request drops from
121
121
  "sum of all worker spawns" to effectively zero.
122
122
  2. Workers spawn in the background with the usual bounded concurrency
123
- (`--deno-worker-concurrency`, default 8).
123
+ (`--deno-worker-concurrency`, default 8), **most-recently-used first**:
124
+ the queue is ordered by each function's last invocation time from the
125
+ invocation log store, so the functions you were actually hitting before
126
+ the restart are warm within the first few seconds and the long tail of
127
+ rarely-called functions loads last. First run (empty store) falls back
128
+ to name order.
124
129
  3. A request for a function whose Worker isn't ready yet **bumps it to the
125
130
  front of the queue** and spawns it immediately, out-of-band. If it becomes
126
131
  ready within the grace window (`--warmup-grace-ms`, default 250 ms) the
@@ -190,6 +195,15 @@ little as possible:
190
195
  `X-1tube-Stale: 1` (CORS-exposed) so tooling can tell the difference.
191
196
  - **Targeted re-discovery.** Reloading one function stats only that function's
192
197
  dir instead of re-scanning the whole functions root.
198
+ - **Local-only dep graphs.** The module graph that maps file changes to
199
+ affected functions stops at the local-file boundary: `npm:`/`jsr:`/`https:`
200
+ subtrees are marked external instead of being fetched and parsed. Only
201
+ `file://` modules can fire watcher events, so nothing is lost — and graph
202
+ builds for npm-heavy functions drop from seconds to milliseconds.
203
+ - **One batch graph per deferred boot.** With shared modules active, deferred
204
+ boot builds a single module graph for all functions in the background
205
+ instead of letting every spawn crawl its own (which re-parsed the same
206
+ `_shared` files once per function).
193
207
 
194
208
  Notes:
195
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
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",
@@ -164,6 +164,38 @@ function resolveAgainst(target: string, base: string): string {
164
164
  return new URL(target, baseUrl).href;
165
165
  }
166
166
 
167
+ /**
168
+ * Local-only module loader for `createGraph`.
169
+ *
170
+ * The graph exists solely to map fs-watcher events to affected
171
+ * functions, and only `file://` modules can ever fire those events.
172
+ * deno_graph's DEFAULT loader, however, follows every specifier —
173
+ * reading npm/jsr packages and even `fetch()`ing `https:` imports —
174
+ * which made big functions pay multi-second graph builds for data we
175
+ * throw away. Marking every non-`file:` specifier as external stops
176
+ * the crawl at the local-file boundary: remote subtrees are never
177
+ * read, parsed, or fetched.
178
+ */
179
+ async function loadLocalOnly(
180
+ specifier: string,
181
+ ): Promise<
182
+ | { kind: "module"; specifier: string; content: Uint8Array }
183
+ | { kind: "external"; specifier: string }
184
+ | undefined
185
+ > {
186
+ if (!specifier.startsWith("file://")) {
187
+ return { kind: "external", specifier };
188
+ }
189
+ try {
190
+ const content = await Deno.readFile(fileURLToPath(specifier));
191
+ return { kind: "module", specifier, content };
192
+ } catch {
193
+ // Missing file mid-edit: report "not found" and let deno_graph
194
+ // record a load error for this module instead of throwing.
195
+ return undefined;
196
+ }
197
+ }
198
+
167
199
  export function createDepGraph(options?: DepGraphOptions): DepGraph {
168
200
  const perName = new Map<string, Set<string>>();
169
201
  const reverse = new Map<string, Set<string>>();
@@ -247,7 +279,10 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
247
279
  const entry = normalize(entryFileUrl);
248
280
  let graph;
249
281
  try {
250
- graph = await createGraph([entry], resolve ? { resolve } : undefined);
282
+ graph = await createGraph([entry], {
283
+ load: loadLocalOnly,
284
+ ...(resolve ? { resolve } : {}),
285
+ });
251
286
  } catch {
252
287
  // A save mid-flush can leave a syntactically broken file on
253
288
  // disk; deno_graph throws. Drop the graph for `name` so the
@@ -281,7 +316,10 @@ export function createDepGraph(options?: DepGraphOptions): DepGraph {
281
316
  try {
282
317
  graph = await createGraph(
283
318
  normalized.map((e) => e.entry),
284
- resolve ? { resolve } : undefined,
319
+ {
320
+ load: loadLocalOnly,
321
+ ...(resolve ? { resolve } : {}),
322
+ },
285
323
  );
286
324
  } catch {
287
325
  for (const e of normalized) replaceGraph(e.name, minimalGraph(e.entry));
@@ -93,6 +93,14 @@ export interface DenoWorkerHostOptions {
93
93
  captureConsole?: boolean;
94
94
  /** Sink for captured console lines + worker runtime errors. */
95
95
  onConsole?: (event: WorkerConsoleEvent) => void;
96
+ /**
97
+ * Last-dispatch timestamp per function from a PREVIOUS run (e.g. the
98
+ * invocation log store). Seeds the MRU ordering used by deferred
99
+ * boot and HMR respawns, so the functions the developer was hitting
100
+ * before a gateway restart warm up first instead of alphabetically.
101
+ * Fresh in-process dispatches overwrite the seed as traffic arrives.
102
+ */
103
+ initialRecency?: ReadonlyMap<string, number>;
96
104
  }
97
105
 
98
106
  export interface ReloadSummary {
@@ -595,7 +603,7 @@ export function createDenoWorkerHost(
595
603
  /** Spawns started out-of-band by prioritize(), above the lane bound. */
596
604
  const outOfBandSpawns = new Set<Promise<void>>();
597
605
  /** Last gateway dispatch per function — drives MRU reload ordering. */
598
- const lastDispatch = new Map<string, number>();
606
+ const lastDispatch = new Map<string, number>(opts.initialRecency ?? []);
599
607
  /**
600
608
  * Per-name spawn serialization. Deferred boot and HMR reload can both
601
609
  * want to spawn the same function concurrently (e.g. an edit lands
@@ -818,9 +826,9 @@ export function createDenoWorkerHost(
818
826
  manifest: handle.manifest,
819
827
  isPublic: handle.isPublic,
820
828
  timeoutMs: handle.timeoutMs,
821
- dispatch: (req, auth, signal) => {
829
+ dispatch: (req, auth, signal, invocationId) => {
822
830
  lastDispatch.set(cand.name, Date.now());
823
- return handle.dispatch(req, auth, signal);
831
+ return handle.dispatch(req, auth, signal, invocationId);
824
832
  },
825
833
  terminate: () => handle.terminate(),
826
834
  });
@@ -955,6 +963,13 @@ export function createDenoWorkerHost(
955
963
  loaded: string[];
956
964
  errors: Array<{ name: string; error: string }>;
957
965
  nextIndex: number;
966
+ /**
967
+ * Resolves when the boot-wide batch dep-graph (shared-rewrite mode
968
+ * only) has landed. Spawns wait on it instead of each paying a
969
+ * full per-function graph crawl — one batch graph parses every
970
+ * `_shared` file once instead of once per function.
971
+ */
972
+ batchGraphReady: Promise<void> | null;
958
973
  }
959
974
  let deferredCtx: DeferredCtx | null = null;
960
975
 
@@ -967,7 +982,11 @@ export function createDenoWorkerHost(
967
982
  const index = ++ctx.nextIndex;
968
983
  ctx.startOpts.onSpawnStart?.({ index, total: ctx.total, name: cand.name });
969
984
  const t0 = performance.now();
970
- const r = await runSerialized(cand.name, () => spawnAndRegister(cand));
985
+ if (ctx.batchGraphReady) await ctx.batchGraphReady;
986
+ const r = await runSerialized(
987
+ cand.name,
988
+ () => spawnAndRegister(cand, { graphReady: ctx.batchGraphReady !== null }),
989
+ );
971
990
  const durationMs = performance.now() - t0;
972
991
  if ("error" in r) {
973
992
  ctx.errors.push({ name: cand.name, error: r.error });
@@ -1003,12 +1022,44 @@ export function createDenoWorkerHost(
1003
1022
  startOpts: StartOptions = {},
1004
1023
  ): Promise<DeferredStart> {
1005
1024
  const candidates = await discoverCandidates(opts.functionsDir);
1025
+ // MRU-first warm-up: the background queue boots the functions the
1026
+ // developer was hitting most recently (seeded from the invocation
1027
+ // log store across restarts) before the ones nobody calls. Ties —
1028
+ // including everything on a first run — keep name order.
1029
+ candidates.sort((a, b) =>
1030
+ (lastDispatch.get(b.name) ?? 0) - (lastDispatch.get(a.name) ?? 0) ||
1031
+ a.name.localeCompare(b.name)
1032
+ );
1033
+ // Shared-rewrite mode needs each function's file list BEFORE its
1034
+ // worker spawns. Build ONE batch graph in the background (the
1035
+ // gateway still starts serving immediately) rather than letting
1036
+ // every spawn crawl its own graph — the per-function crawls
1037
+ // re-parse the same shared files over and over.
1038
+ const useSharedRewrite = Boolean(
1039
+ rewriteCache && sharedRuntime && sharedRuntime.list().length > 0,
1040
+ );
1041
+ let batchGraphReady: Promise<void> | null = null;
1042
+ if (useSharedRewrite && candidates.length > 0) {
1043
+ const graphStart = performance.now();
1044
+ batchGraphReady = depGraph
1045
+ .refreshMany(
1046
+ candidates.map((c) => ({ name: c.name, entryFileUrl: c.entryUrl })),
1047
+ )
1048
+ .then(() => {
1049
+ startOpts.onBatchGraph?.({
1050
+ total: candidates.length,
1051
+ durationMs: performance.now() - graphStart,
1052
+ });
1053
+ })
1054
+ .catch(() => {});
1055
+ }
1006
1056
  const ctx: DeferredCtx = {
1007
1057
  total: candidates.length,
1008
1058
  startOpts,
1009
1059
  loaded: [],
1010
1060
  errors: [],
1011
1061
  nextIndex: 0,
1062
+ batchGraphReady,
1012
1063
  };
1013
1064
  deferredCtx = ctx;
1014
1065
  const items: ClaimItem[] = [];
package/src/logs/query.ts CHANGED
@@ -195,6 +195,12 @@ export interface LogQuery {
195
195
  logsSince(afterId: number, limit?: number): LogRecord[];
196
196
  /** Distinct function names seen in the store (for filter dropdowns). */
197
197
  functionNames(): string[];
198
+ /**
199
+ * Most recent invocation timestamp per function, across the whole
200
+ * retention window. Seeds the Deno worker host's MRU boot ordering so
201
+ * deferred boot warms the functions the developer actually uses first.
202
+ */
203
+ lastDispatchByFunction(): Map<string, number>;
198
204
  }
199
205
 
200
206
  export function createLogQuery(db: LogDb): LogQuery {
@@ -368,5 +374,23 @@ export function createLogQuery(db: LogDb): LogQuery {
368
374
  return rows.map((r) => r.function_name);
369
375
  }
370
376
 
371
- return { queryInvocations, getInvocation, searchLogs, logsSince, functionNames };
377
+ function lastDispatchByFunction(): Map<string, number> {
378
+ // Satisfied entirely by idx_invocations_fn_ts (fn, ts) — one index
379
+ // scan, no table reads.
380
+ const rows = raw.prepare(
381
+ `SELECT function_name, MAX(ts_ms) AS last_ts FROM invocations GROUP BY function_name`,
382
+ ).all() as unknown as Array<{ function_name: string; last_ts: number }>;
383
+ const out = new Map<string, number>();
384
+ for (const r of rows) out.set(r.function_name, Number(r.last_ts));
385
+ return out;
386
+ }
387
+
388
+ return {
389
+ queryInvocations,
390
+ getInvocation,
391
+ searchLogs,
392
+ logsSince,
393
+ functionNames,
394
+ lastDispatchByFunction,
395
+ };
372
396
  }
package/src/server.ts CHANGED
@@ -1329,6 +1329,18 @@ if (opts.backend === "workerd") {
1329
1329
  );
1330
1330
  }
1331
1331
 
1332
+ // MRU warm-up seed: the invocation log store remembers which
1333
+ // functions were hit before the restart; deferred boot warms those
1334
+ // first instead of going alphabetically.
1335
+ let bootRecency: Map<string, number> | undefined;
1336
+ if (logDbHandle) {
1337
+ try {
1338
+ bootRecency = createLogQuery(logDbHandle).lastDispatchByFunction();
1339
+ } catch {
1340
+ // Best-effort: a broken store only costs the smarter boot order.
1341
+ }
1342
+ }
1343
+
1332
1344
  denoWorkerHost = createDenoWorkerHost({
1333
1345
  functionsDir: resolvedFunctionsPath,
1334
1346
  registry,
@@ -1351,6 +1363,9 @@ if (opts.backend === "workerd") {
1351
1363
  ...(denoImportMapOptions ?? {}),
1352
1364
  ...(denoSharedRuntime ? { sharedRuntime: denoSharedRuntime } : {}),
1353
1365
  ...(denoRewriteCache ? { rewriteCache: denoRewriteCache } : {}),
1366
+ ...(bootRecency && bootRecency.size > 0
1367
+ ? { initialRecency: bootRecency }
1368
+ : {}),
1354
1369
  });
1355
1370
 
1356
1371
  // Boot progress: append-only `[i/N] ✓ name (123ms)` per worker plus