@openparachute/hub 0.6.5-rc.3 → 0.6.5-rc.4

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": "@openparachute/hub",
3
- "version": "0.6.5-rc.3",
3
+ "version": "0.6.5-rc.4",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {
@@ -26,6 +26,9 @@
26
26
  },
27
27
  "scripts": {
28
28
  "start": "bun src/cli.ts",
29
+ "build:depcheck": "[ ! -f packages/depcheck/package.json ] || [ -f packages/depcheck/dist/index.js ] || bun run --cwd packages/depcheck build",
30
+ "prepare": "bun run build:depcheck",
31
+ "pretest": "bun run build:depcheck",
29
32
  "test": "bun test ./src",
30
33
  "lint": "biome check .",
31
34
  "lint:fix": "biome check --write .",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { renderHub, writeHubFile } from "../hub.ts";
@@ -301,4 +301,30 @@ describe("writeHubFile", () => {
301
301
  rmSync(dir, { recursive: true, force: true });
302
302
  }
303
303
  });
304
+
305
+ // hub#171: serve regenerates hub.html on EVERY start (the `!existsSync`
306
+ // guard in commands/serve.ts was dropped), so writeHubFile must be safe to
307
+ // call when the file already exists and must overwrite stale content with
308
+ // a fresh render from current code — otherwise an upgrade serves the old
309
+ // page until an unrelated `parachute expose` re-runs.
310
+ test("overwrites a pre-existing stale hub.html with a fresh render (hub#171)", () => {
311
+ const dir = mkdtempSync(join(tmpdir(), "pcli-hub-"));
312
+ try {
313
+ const path = join(dir, "well-known", "hub.html");
314
+ // First write creates the well-known dir + a real file; then plant
315
+ // stale content to simulate an old hub.html left over from a prior code
316
+ // version after `git pull` + `parachute restart hub`.
317
+ writeHubFile(path);
318
+ writeFileSync(path, "<!-- stale pre-upgrade hub.html -->");
319
+ expect(readFileSync(path, "utf8")).toContain("stale");
320
+
321
+ const written = writeHubFile(path);
322
+ expect(written).toBe(path);
323
+ const content = readFileSync(path, "utf8");
324
+ expect(content).not.toContain("stale");
325
+ expect(content).toBe(renderHub());
326
+ } finally {
327
+ rmSync(dir, { recursive: true, force: true });
328
+ }
329
+ });
304
330
  });
@@ -6,6 +6,7 @@ import {
6
6
  normalizeMount,
7
7
  notesDistCandidates,
8
8
  notesFetch,
9
+ notesServeOptions,
9
10
  resolveNotesDistFrom,
10
11
  } from "../notes-serve.ts";
11
12
 
@@ -26,6 +27,15 @@ function req(path: string): Request {
26
27
  return new Request(`http://127.0.0.1${path}`);
27
28
  }
28
29
 
30
+ describe("notesServeOptions (hub#399 residual)", () => {
31
+ test("sets idleTimeout: 255 to outlast edge keep-alive pools, matching hub-server.ts", () => {
32
+ const opts = notesServeOptions(5173, "/tmp/dist", "/notes");
33
+ expect(opts.idleTimeout).toBe(255);
34
+ expect(opts.port).toBe(5173);
35
+ expect(typeof opts.fetch).toBe("function");
36
+ });
37
+ });
38
+
29
39
  describe("normalizeMount", () => {
30
40
  test("strips trailing slashes", () => {
31
41
  expect(normalizeMount("/notes/")).toBe("/notes");
@@ -337,13 +337,22 @@ export async function serve(opts: ServeOpts = {}): Promise<{
337
337
  // hatch for setups that want loopback-only inside a sidecar.
338
338
  const hostname = env.PARACHUTE_BIND_HOST || "0.0.0.0";
339
339
 
340
- // Ensure the well-known dir exists, and seed a static hub.html so `/`
340
+ // Ensure the well-known dir exists, and (re)write the static hub.html so `/`
341
341
  // serves something coherent on a fresh disk (the dynamic path through
342
342
  // `hubFetch` takes over once a DB row exists; the disk file is the
343
343
  // signed-out fallback).
344
+ //
345
+ // Regenerate on EVERY serve start, not just when the file is absent (#171):
346
+ // hub.html is a served artifact built from current code, and code ships via
347
+ // `git pull` + `parachute restart hub`. Guarding on `!existsSync` left the
348
+ // stale post-upgrade file on disk until an unrelated `parachute expose`
349
+ // re-ran — so operators saw old hub.html after an upgrade. The write is a
350
+ // cheap, deterministic, atomic (tmp+rename) render of static signed-out
351
+ // HTML with no expose-state or DB dependency, so it's safe to call every
352
+ // start.
344
353
  if (!existsSync(WELL_KNOWN_DIR)) mkdirSync(WELL_KNOWN_DIR, { recursive: true });
345
354
  const hubHtmlPath = join(WELL_KNOWN_DIR, "hub.html");
346
- if (!existsSync(hubHtmlPath)) writeHubFile(hubHtmlPath);
355
+ writeHubFile(hubHtmlPath);
347
356
 
348
357
  const dbPath = hubDbPath();
349
358
  // Self-heal-or-die DB holder (#594). The handle lives behind a mutable
@@ -176,6 +176,29 @@ export function notesFetch(dist: string, mount: string): (req: Request) => Respo
176
176
  };
177
177
  }
178
178
 
179
+ /**
180
+ * Build the `Bun.serve` config for the notes static server.
181
+ *
182
+ * `idleTimeout: 255` matches hub-server.ts. When this static-serve sits behind
183
+ * an edge proxy that pools keep-alive connections (Render, Cloudflare, fly
184
+ * proxy), the edge's idle timeout outlasts Bun's default — the proxy reuses a
185
+ * connection we just closed and returns a "random" 502. 255s comfortably
186
+ * exceeds Render's community-observed ~120s edge pool TTL. Closes the hub#399
187
+ * residual on the second serve entrypoint (the Notes PWA path). Exported so a
188
+ * test can assert the option is set without booting a server.
189
+ */
190
+ export function notesServeOptions(
191
+ port: number,
192
+ dist: string,
193
+ mount: string,
194
+ ): { port: number; idleTimeout: number; fetch: (req: Request) => Response } {
195
+ return {
196
+ port,
197
+ idleTimeout: 255,
198
+ fetch: notesFetch(dist, mount),
199
+ };
200
+ }
201
+
179
202
  if (import.meta.main) {
180
203
  const { port, dist: distArg, mount } = parseArgs(process.argv.slice(2));
181
204
 
@@ -187,10 +210,7 @@ if (import.meta.main) {
187
210
  process.exit(1);
188
211
  }
189
212
 
190
- Bun.serve({
191
- port,
192
- fetch: notesFetch(dist, mount),
193
- });
213
+ Bun.serve(notesServeOptions(port, dist, mount));
194
214
 
195
215
  console.log(`notes static-serve listening on :${port} (dist=${dist}, mount=${mount || "/"})`);
196
216
  }