@podway/relay 0.2.4 → 0.2.6

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/FAQ.md CHANGED
@@ -33,15 +33,31 @@ normal login shell. If `npx` works after you log in but fails in a script, set
33
33
 
34
34
  ## How do I make the relay survive a reboot?
35
35
 
36
- The relay itself does not need root. Use whichever startup mechanism your host
37
- provides:
36
+ The relay itself does not need root. For a machine that starts it at boot, **install it once** instead
37
+ of running it through `npx`: `npx` unpacks the package into a cache on every start, and a start that is
38
+ cut off (a reboot, two starts at once) can leave that cache broken so the next start fails with
39
+ `ENOTEMPTY`.
40
+
41
+ ```bash
42
+ npm i -g @podway/relay@latest # update later with the same command
43
+ relay status # check it runs
44
+ ```
45
+
46
+ If `npm i -g` says "permission denied", give npm a folder in your home first:
47
+
48
+ ```bash
49
+ npm config set prefix ~/.npm-global
50
+ echo 'export PATH=$HOME/.npm-global/bin:$PATH' >> ~/.profile && . ~/.profile
51
+ ```
52
+
53
+ Then use whichever startup mechanism your host provides:
38
54
 
39
55
  - **Synology DSM**: Go to Control Panel → Task Scheduler → Create →
40
56
  Triggered Task → User-defined script, then choose **Boot-up**. Run:
41
57
 
42
58
  ```bash
43
- export PATH=/path/to/node/bin:$PATH # wherever npx actually lives, see above
44
- npx @podway/relay@latest start --gateway <url> --accept
59
+ export PATH=$HOME/.npm-global/bin:/path/to/node/bin:$PATH # where relay and node live
60
+ relay start --gateway <url> --accept
45
61
  ```
46
62
 
47
63
  - **A systemd host**: If you have root, create a service that runs the same
@@ -52,6 +68,9 @@ provides:
52
68
  The scheduler only needs to run the command once. `start` moves the relay into
53
69
  the background and exits.
54
70
 
71
+ It is safe to run `relay stop` first, "just in case": it returns within a few seconds even on a
72
+ slow NAS (it waits up to 5 s for the old relay to exit, then force-stops it).
73
+
55
74
  ## Do I need to pass `--code` again every time it starts?
56
75
 
57
76
  No. `--code` is only used for the first pairing. The relay saves a reconnect
@@ -85,3 +104,33 @@ every authorized SSH user, not only Podway, and requires administrator access:
85
104
  sudo sed -i 's/^AllowTcpForwarding no/AllowTcpForwarding yes/' /etc/ssh/sshd_config
86
105
  sudo systemctl restart sshd # or: synosystemctl restart sshd, on some DSM versions
87
106
  ```
107
+
108
+ ## `npm error code ENOTEMPTY` when I run `npx @podway/relay`
109
+
110
+ A previous `npx` run was cut off and left its cache folder half-written. Delete the cache (npm downloads
111
+ it again) and switch to the installed relay (see "How do I make the relay survive a reboot?"):
112
+
113
+ ```bash
114
+ rm -rf ~/.npm/_npx
115
+ npm i -g @podway/relay@latest
116
+ relay status
117
+ ```
118
+
119
+ ## How do I see what the relay did without the web dashboard?
120
+
121
+ `relay status` prints it as tables — useful on a NAS where the dashboard's local page is out of reach:
122
+
123
+ ```
124
+ STATE UPTIME LINK REQ 24h OK% DATA
125
+ running 2d 4h connected 412 98% 61 MB
126
+
127
+ POD REQ OK% LAST
128
+ afisha crawler 301 99% 12 s ago
129
+ crypto sail club 88 93% 3 min ago
130
+
131
+ SITE REQ OK% LAST ERROR
132
+ polymarket.com 145 97% 403 refused, 26 min ago
133
+ ```
134
+
135
+ `--since 1h|24h|7d` changes the window, `--all` shows every row, `--json` is for scripts. DATA counts
136
+ tunnel traffic only (plain fetches have no size recorded).
package/README.md CHANGED
@@ -46,7 +46,7 @@ Once started, the relay runs in the background and handles both modes.
46
46
  | `relay start --code <code> --gateway <url> --accept` | start the relay in the background |
47
47
  | `relay login <domain>` | allow signed-in fetches for one domain |
48
48
  | `relay dashboard` | open the local command center |
49
- | `relay status` | show the relay, gateway, and signed-in site status |
49
+ | `relay status` | the relay at a glance, then requests per pod and per site (OK %, last error); `--since 1h\|24h\|7d`, `--all`, `--json` |
50
50
  | `relay stop` | stop it |
51
51
  | `relay reset` | remove saved sessions, logins, and pairing |
52
52
 
package/dist/main.js CHANGED
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import WebSocket from "ws";
3
2
  import { spawn } from "node:child_process";
4
3
  import { writeFileSync, readFileSync, unlinkSync, mkdirSync } from "node:fs";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import { load, save, addLoginDomain, resetProfile, profileDir, pidFile, dashboardRuntimeFile, isDomainBlocked, isPodPaused, revokeLoginDomain, } from "./config.js";
7
- import { RelayClient } from "./relay-client.js";
8
- import { RelayTunnel } from "./relay-tunnel.js";
9
- import { keepAwake } from "./keep-awake.js";
10
- import { BrowserFetcher } from "./browser-fetcher.js";
11
- import { RelayEventStore } from "./audit.js";
12
- import { serveDashboard } from "./dashboard.js";
13
- import { RelayRuntime } from "./runtime.js";
14
- import { attachHeartbeat } from "./heartbeat.js";
15
6
  import { DISCLOSURE } from "./disclosure.js";
16
7
  import { c, rows } from "./colors.js";
17
8
  import { normalizeDomain } from "./domain.js";
9
+ import { stopPid } from "./stop.js";
10
+ import { renderStatus, statusData, WINDOWS } from "./status.js";
11
+ // Only `start` (the daemon) and `dashboard` need the transport, browser and dashboard modules.
12
+ // They load lazily so `relay stop` / `relay status` stay quick on a slow NAS CPU (a DS118 runs the
13
+ // boot task's `relay stop` before every start).
18
14
  /**
19
15
  * `relay` — one command starts a background relay; login/dashboard/stop modify it
20
16
  * through disk state (config, audit log, pidfile). No per-request approval: once
@@ -126,6 +122,10 @@ async function cmdStart(a) {
126
122
  log("");
127
123
  }
128
124
  async function runDaemon(a) {
125
+ const [{ default: WebSocket }, { RelayClient }, { RelayTunnel }, { keepAwake }, { BrowserFetcher }, { RelayEventStore }, { serveDashboard }, { RelayRuntime }, { attachHeartbeat }] = await Promise.all([
126
+ import("ws"), import("./relay-client.js"), import("./relay-tunnel.js"), import("./keep-awake.js"), import("./browser-fetcher.js"),
127
+ import("./audit.js"), import("./dashboard.js"), import("./runtime.js"), import("./heartbeat.js"),
128
+ ]);
129
129
  const gateway = arg(a, "--gateway");
130
130
  const code = arg(a, "--code") ?? "";
131
131
  const browser = new BrowserFetcher({
@@ -145,10 +145,12 @@ async function runDaemon(a) {
145
145
  if (stopping)
146
146
  return;
147
147
  stopping = true;
148
+ // Never hang on the way out: whatever the cleanup does, the process is gone in 4 s.
149
+ setTimeout(() => process.exit(0), 4000).unref();
148
150
  releaseKeepAwake();
149
151
  runtime.clearActive();
150
152
  dashboardClose?.();
151
- await browser.close();
153
+ await Promise.race([browser.close().catch(() => undefined), new Promise((r) => setTimeout(r, 3000))]);
152
154
  try {
153
155
  unlinkSync(pidFile());
154
156
  }
@@ -352,6 +354,9 @@ async function cmdDashboard(a) {
352
354
  return;
353
355
  }
354
356
  }
357
+ const [{ RelayEventStore }, { RelayRuntime }, { serveDashboard }] = await Promise.all([
358
+ import("./audit.js"), import("./runtime.js"), import("./dashboard.js"),
359
+ ]);
355
360
  const cfg = load();
356
361
  const store = preview ? undefined : new RelayEventStore({ retentionDays: cfg.retentionDays ?? 30 }).init();
357
362
  const runtime = preview ? undefined : new RelayRuntime({ daemon: "stopped" });
@@ -363,22 +368,27 @@ async function cmdDashboard(a) {
363
368
  openBrowser(url);
364
369
  await new Promise(() => { }); // stay up until Ctrl-C
365
370
  }
366
- async function cmdStatus() {
371
+ async function cmdStatus(a = []) {
372
+ const flag = (n) => a.includes(n);
373
+ const sinceArg = a[a.indexOf("--since") + 1];
374
+ const window = a.includes("--since") && sinceArg && sinceArg in WINDOWS ? sinceArg : "24h";
367
375
  const pid = isRunning();
368
376
  const cfg = load();
369
- // Pad wider than the longest label ("dashboard", 9) so every value has a gutter —
370
- // padEnd(9) left "dashboard" flush against its URL.
371
- const label = (s) => c.dim(s.padEnd(11));
377
+ let startedAt;
378
+ try {
379
+ startedAt = JSON.parse(readFileSync(dashboardRuntimeFile(), "utf8")).startedAt;
380
+ }
381
+ catch { }
382
+ // audit.js is fs-only; reload() reads the saved history without pruning or writing.
383
+ const { RelayEventStore } = await import("./audit.js");
384
+ const store = new RelayEventStore({ retentionDays: 30 });
385
+ store.reload();
386
+ const data = statusData(store.query({ limit: 10_000 }), { state: pid ? "running" : "stopped", startedAt, link: pid ? await gatewayLinkState() : undefined }, window);
387
+ if (flag("--json"))
388
+ return log(JSON.stringify({ ...data, pid, gateway: cfg.gatewayUrl ?? null, asYou: cfg.loginDomains }, null, 2));
389
+ log(renderStatus(data, { limit: flag("--all") ? Infinity : 10 }));
372
390
  log("");
373
- log(`${label("relay")}${pid ? c.green(`running`) + c.dim(` (pid ${pid})`) : c.yellow("not running")}`);
374
- // The PROCESS being alive is not the same as the gateway LINK being up — a slept host
375
- // leaves a dead link the process can't see. Ask the daemon's own dashboard for the
376
- // live state so status stops reporting "running" over a dead link.
377
- if (pid)
378
- log(`${label("link")}${await gatewayLinkState()}`);
379
- log(`${label("gateway")}${cfg.gatewayUrl ? c.cyan(cfg.gatewayUrl) : c.dim("(unset)")}`);
380
- log(`${label("dashboard")}${dashboardUrl() ? c.cyan(dashboardUrl()) : c.dim("relay dashboard opens saved history")}`);
381
- log(`${label("as-you")}${cfg.loginDomains.length ? cfg.loginDomains.join(", ") : c.dim("no sites signed in — all fetches are clean")}`);
391
+ log(c.dim(`gateway ${cfg.gatewayUrl ?? "(unset)"} · as-you ${cfg.loginDomains.length ? cfg.loginDomains.join(", ") : "none"} · --since 1h|24h|7d · --all · --json`));
382
392
  log("");
383
393
  }
384
394
  /** Live gateway-link state, read from the running daemon's local dashboard (`/data`).
@@ -388,22 +398,16 @@ async function cmdStatus() {
388
398
  async function gatewayLinkState() {
389
399
  const base = dashboardUrl();
390
400
  if (!base)
391
- return c.dim("unknown (dashboard not up yet)");
401
+ return "starting";
392
402
  try {
393
403
  const ac = new AbortController();
394
404
  const timer = setTimeout(() => ac.abort(), 1500);
395
405
  const res = await fetch(`${base}/data`, { signal: ac.signal }).finally(() => clearTimeout(timer));
396
406
  const data = (await res.json());
397
- if (data.state === "connected")
398
- return c.green("connected");
399
- if (data.state === "reconnecting")
400
- return c.yellow("reconnecting… (host asleep or offline)");
401
- if (data.state === "stopped")
402
- return c.yellow("stopped");
403
- return c.dim("unknown");
407
+ return data.state === "connected" || data.state === "reconnecting" || data.state === "stopped" ? data.state : "unknown";
404
408
  }
405
409
  catch {
406
- return c.yellow("unreachable — the relay daemon isn't responding (try: relay restart)");
410
+ return "unreachable"; // the daemon isn't answering — relay restart
407
411
  }
408
412
  }
409
413
  /** Stop-then-start, reusing the stored gateway + reconnect token. The escape hatch for a
@@ -411,12 +415,7 @@ async function gatewayLinkState() {
411
415
  async function cmdRestart(a) {
412
416
  const pid = isRunning();
413
417
  if (pid) {
414
- process.kill(pid, "SIGTERM");
415
- try {
416
- unlinkSync(pidFile());
417
- }
418
- catch { }
419
- await new Promise((r) => setTimeout(r, 600)); // let it release the pid + dashboard port
418
+ await stopPid(pid, { files: [pidFile(), dashboardRuntimeFile()] }); // waits until it is gone (≤ 5 s)
420
419
  log(c.dim("restarting the relay…"));
421
420
  }
422
421
  else {
@@ -424,23 +423,21 @@ async function cmdRestart(a) {
424
423
  }
425
424
  return cmdStart(a);
426
425
  }
427
- function cmdStop() {
426
+ async function cmdStop() {
428
427
  const pid = isRunning();
429
428
  if (!pid)
430
429
  return log(c.dim("no relay running."));
431
- process.kill(pid, "SIGTERM");
432
- try {
433
- unlinkSync(pidFile());
434
- }
435
- catch { }
436
- log(`${c.green("✓")} relay stopped.`);
430
+ const how = await stopPid(pid, { files: [pidFile(), dashboardRuntimeFile()] });
431
+ log(how === "killed"
432
+ ? `${c.green("✓")} relay stopped ${c.dim("(it did not exit within 5 s, so it was killed)")}.`
433
+ : `${c.green("✓")} relay stopped.`);
437
434
  }
438
435
  async function dispatch(a) {
439
436
  switch (a[0]) {
440
437
  case "start": return cmdStart(a.slice(1));
441
438
  case "login": return cmdLogin(a.slice(1));
442
439
  case "dashboard": return cmdDashboard(a.slice(1));
443
- case "status": return cmdStatus();
440
+ case "status": return cmdStatus(a.slice(1));
444
441
  case "stop": return cmdStop();
445
442
  case "restart": return cmdRestart(a.slice(1));
446
443
  case "reset":
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `relay status` stats as docker-ps style tables (owner, 2026-11-08: the web dashboard is not reachable on
3
+ * a headless NAS, so the terminal has to show what the relay did). Pure: events in, text/JSON out. It reads
4
+ * only the saved event history — no transport, browser or dashboard module — so status stays fast.
5
+ */
6
+ import type { RelayEventV2 } from "./audit.js";
7
+ export declare const WINDOWS: {
8
+ readonly "1h": 3600000;
9
+ readonly "24h": 86400000;
10
+ readonly "7d": number;
11
+ };
12
+ export type StatusWindow = keyof typeof WINDOWS;
13
+ export interface StatusHead {
14
+ state: "running" | "stopped";
15
+ startedAt?: string;
16
+ link?: string;
17
+ }
18
+ export interface StatusData {
19
+ window: StatusWindow;
20
+ state: StatusHead["state"];
21
+ uptimeMs: number | null;
22
+ link: string | null;
23
+ requests: number;
24
+ okPct: number | null;
25
+ bytes: number | null;
26
+ pods: {
27
+ name: string;
28
+ requests: number;
29
+ okPct: number;
30
+ lastAt: string;
31
+ }[];
32
+ sites: {
33
+ host: string;
34
+ requests: number;
35
+ okPct: number;
36
+ lastError: string | null;
37
+ lastErrorAt: string | null;
38
+ }[];
39
+ }
40
+ export declare function statusData(events: RelayEventV2[], head: StatusHead, window: StatusWindow, now?: number): StatusData;
41
+ export declare function ago(ms: number): string;
42
+ export declare function renderStatus(d: StatusData, opts?: {
43
+ limit: number;
44
+ now?: number;
45
+ }): string;
package/dist/status.js ADDED
@@ -0,0 +1,109 @@
1
+ import { c } from "./colors.js";
2
+ export const WINDOWS = { "1h": 3_600_000, "24h": 86_400_000, "7d": 7 * 86_400_000 };
3
+ const ERROR_WORD = {
4
+ "site-refused": "refused",
5
+ "owner-blocked": "blocked by you",
6
+ "safety-blocked": "blocked (safety)",
7
+ "rate-limited": "rate limited",
8
+ "network-error": "network error",
9
+ };
10
+ function errorText(e) {
11
+ return [e.httpStatus, ERROR_WORD[e.outcome] ?? e.outcome].filter(Boolean).join(" ");
12
+ }
13
+ function podName(e) {
14
+ if (!e.source)
15
+ return "(unknown)";
16
+ if ("system" in e.source)
17
+ return null; // the relay's own health checks are not a pod
18
+ return e.source.podName || e.source.podId;
19
+ }
20
+ function group(events, key) {
21
+ const by = new Map();
22
+ for (const e of events) {
23
+ const k = key(e);
24
+ if (!k)
25
+ continue;
26
+ const t = Date.parse(e.startedAt);
27
+ const r = by.get(k) ?? { name: k, req: 0, ok: 0, last: 0 };
28
+ r.req++;
29
+ if (e.outcome === "ok")
30
+ r.ok++;
31
+ else if (!r.lastError || t > Date.parse(r.lastError.startedAt))
32
+ r.lastError = e;
33
+ r.last = Math.max(r.last, t);
34
+ by.set(k, r);
35
+ }
36
+ return [...by.values()].sort((a, b) => b.req - a.req || b.last - a.last);
37
+ }
38
+ const pct = (ok, n) => (n ? Math.round((ok / n) * 100) : 0);
39
+ export function statusData(events, head, window, now = Date.now()) {
40
+ const inWindow = events.filter((e) => Date.parse(e.startedAt) >= now - WINDOWS[window]);
41
+ const counted = inWindow.filter((e) => podName(e) !== null);
42
+ const ok = counted.filter((e) => e.outcome === "ok").length;
43
+ return {
44
+ window,
45
+ state: head.state,
46
+ uptimeMs: head.state === "running" && head.startedAt ? Math.max(0, now - Date.parse(head.startedAt)) : null,
47
+ link: head.state === "running" ? (head.link ?? "unknown") : null,
48
+ requests: counted.length,
49
+ okPct: counted.length ? pct(ok, counted.length) : null,
50
+ bytes: counted.some((e) => e.mode === "tunnel") ? counted.filter((e) => e.mode === "tunnel").reduce((s, e) => s + (e.bytesUp ?? 0) + (e.bytesDown ?? 0), 0) : null,
51
+ pods: group(counted, podName).map((r) => ({ name: r.name, requests: r.req, okPct: pct(r.ok, r.req), lastAt: new Date(r.last).toISOString() })),
52
+ sites: group(counted, (e) => e.host).map((r) => ({
53
+ host: r.name, requests: r.req, okPct: pct(r.ok, r.req),
54
+ lastError: r.lastError ? errorText(r.lastError) : null,
55
+ lastErrorAt: r.lastError ? r.lastError.startedAt : null,
56
+ })),
57
+ };
58
+ }
59
+ export function ago(ms) {
60
+ const s = Math.max(0, Math.round(ms / 1000));
61
+ if (s < 60)
62
+ return `${s} s ago`;
63
+ if (s < 3600)
64
+ return `${Math.round(s / 60)} min ago`;
65
+ if (s < 86_400)
66
+ return `${Math.round(s / 3600)} h ago`;
67
+ return `${Math.round(s / 86_400)} d ago`;
68
+ }
69
+ function duration(ms) {
70
+ const m = Math.floor(ms / 60_000), h = Math.floor(m / 60), d = Math.floor(h / 24);
71
+ return d ? `${d}d ${h % 24}h` : h ? `${h}h ${m % 60}m` : `${m}m`;
72
+ }
73
+ function size(b) {
74
+ if (b < 1024)
75
+ return `${b} B`;
76
+ if (b < 1024 ** 2)
77
+ return `${Math.round(b / 1024)} KB`;
78
+ if (b < 1024 ** 3)
79
+ return `${Math.round(b / 1024 ** 2)} MB`;
80
+ return `${(b / 1024 ** 3).toFixed(1)} GB`;
81
+ }
82
+ const cut = (s, w) => (s.length > w ? s.slice(0, w - 1) + "…" : s);
83
+ /** Columns padded to fixed widths that fit 80 columns; numbers right-aligned. */
84
+ function table(head, widths, right, body) {
85
+ const line = (cells) => cells.map((v, i) => (i === cells.length - 1 ? v : right[i] ? v.padStart(widths[i]) : cut(v, widths[i]).padEnd(widths[i]))).join(" ").trimEnd();
86
+ return [c.dim(line(head)), ...body.map(line)];
87
+ }
88
+ export function renderStatus(d, opts = { limit: 10 }) {
89
+ const now = opts.now ?? Date.now();
90
+ const out = [""];
91
+ const stateWord = d.state === "running" ? c.green("running") : c.yellow("stopped");
92
+ const head = table(["STATE", "UPTIME", "LINK", `REQ ${d.window}`, "OK%", "DATA"], [8, 8, 12, 8, 4, 0], [false, false, false, true, true, false], [[d.state, d.uptimeMs === null ? "—" : duration(d.uptimeMs), d.link ?? "—", String(d.requests), d.okPct === null ? "—" : `${d.okPct}%`, d.bytes === null ? "—" : size(d.bytes)]]);
93
+ head[1] = head[1].replace(/^(running|stopped)/, stateWord);
94
+ out.push(...head);
95
+ if (!d.requests) {
96
+ out.push("", c.dim(`no pod requests in the last ${d.window}`));
97
+ }
98
+ else {
99
+ out.push("");
100
+ out.push(...table(["POD", "REQ", "OK%", "LAST"], [24, 6, 4, 0], [false, true, true, false], d.pods.slice(0, opts.limit).map((p) => [p.name, String(p.requests), `${p.okPct}%`, ago(now - Date.parse(p.lastAt))])));
101
+ out.push("");
102
+ out.push(...table(["SITE", "REQ", "OK%", "LAST ERROR"], [24, 6, 4, 0], [false, true, true, false], d.sites.slice(0, opts.limit).map((s) => [s.host, String(s.requests), `${s.okPct}%`,
103
+ s.lastError ? `${s.lastError}, ${ago(now - Date.parse(s.lastErrorAt))}` : "—"])));
104
+ const hidden = Math.max(0, d.pods.length - opts.limit) + Math.max(0, d.sites.length - opts.limit);
105
+ if (hidden)
106
+ out.push("", c.dim(`${hidden} more rows — relay status --all`));
107
+ }
108
+ return out.join("\n");
109
+ }
package/dist/stop.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stop a relay daemon and WAIT until it is gone, but never for long: SIGTERM, poll for up to
3
+ * `graceMs`, then SIGKILL. A boot script runs `relay stop` before `relay start`, so returning
4
+ * while the old daemon still holds the dashboard port or the browser profile would race the new
5
+ * one — and a daemon stuck in a slow shutdown (a swapping NAS) must not hang the script.
6
+ */
7
+ export declare function stopPid(pid: number, opts?: {
8
+ graceMs?: number;
9
+ pollMs?: number;
10
+ files?: string[];
11
+ }): Promise<"not-running" | "stopped" | "killed">;
package/dist/stop.js ADDED
@@ -0,0 +1,46 @@
1
+ import { unlinkSync } from "node:fs";
2
+ const alive = (pid) => {
3
+ try {
4
+ process.kill(pid, 0);
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ };
11
+ /**
12
+ * Stop a relay daemon and WAIT until it is gone, but never for long: SIGTERM, poll for up to
13
+ * `graceMs`, then SIGKILL. A boot script runs `relay stop` before `relay start`, so returning
14
+ * while the old daemon still holds the dashboard port or the browser profile would race the new
15
+ * one — and a daemon stuck in a slow shutdown (a swapping NAS) must not hang the script.
16
+ */
17
+ export async function stopPid(pid, opts = {}) {
18
+ const { graceMs = 5000, pollMs = 100, files = [] } = opts;
19
+ const clean = () => { for (const f of files)
20
+ try {
21
+ unlinkSync(f);
22
+ }
23
+ catch { /* gone */ } };
24
+ if (!alive(pid)) {
25
+ clean();
26
+ return "not-running";
27
+ }
28
+ try {
29
+ process.kill(pid, "SIGTERM");
30
+ }
31
+ catch { /* exited meanwhile */ }
32
+ const deadline = Date.now() + graceMs;
33
+ while (Date.now() < deadline) {
34
+ if (!alive(pid)) {
35
+ clean();
36
+ return "stopped";
37
+ }
38
+ await new Promise((r) => setTimeout(r, pollMs));
39
+ }
40
+ try {
41
+ process.kill(pid, "SIGKILL");
42
+ }
43
+ catch { /* exited at the last moment */ }
44
+ clean();
45
+ return "killed";
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podway/relay",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Podway relay \u2014 runs a fetch relay on your machine so a pod can read pages from your network, using sessions you sign into. Install: npm i -g @podway/relay, then `relay start`.",
5
5
  "type": "module",
6
6
  "bin": {