Gateway: watch API, pairing, notifications, APNs sender, CLI, tests

This commit is contained in:
gib committed 2026-11-01 02:09:18 -05:00
1 parent 972f6e4be5
commit 6ca098b701
12 files changed
+1297 -1

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
// Starts the gateway: connects to every T3 host in the database, serves the watch API, and pushes
// notifications.
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { Apns } from "./apns.ts";
import { createRoutes } from "./api.ts";
import { loadConfig } from "./config.ts";
import { openDb } from "./db.ts";
import { Hub } from "./hub.ts";
import { Notifier, type SendPush } from "./notify.ts";
import { SummaryCache } from "./views.ts";
const HOST_SYNC_MS = 15_000;
const TOKEN_CHECK_MS = 6 * 60 * 60_000;
const log = (message: string) => console.log(`${new Date().toISOString()} ${message}`);
const config = loadConfig();
mkdirSync(dirname(config.dbPath), { recursive: true });
const db = openDb(config.dbPath);
const cache = new SummaryCache(log);
const apns = config.apns ? new Apns(config.apns) : null;
if (!apns) log("APNs is not configured. Pushes will only be logged.");
const sendPush: SendPush = async (push) => {
const devices = db.devices.active().filter((device) => device.apns_token && device.apns_env);
if (!apns || devices.length === 0) {
log(`push (not sent): [${push.category}] ${push.title}: ${push.body}`);
return;
}
const payload = {
aps: {
alert: { title: push.title, ...(push.subtitle ? { subtitle: push.subtitle } : {}), body: push.body },
sound: "default",
category: push.category,
"thread-id": push.data.threadId || push.data.hostId,
},
...push.data,
};
await Promise.all(
devices.map(async (device) => {
const result = await apns.send({
deviceToken: device.apns_token!,
environment: device.apns_env!,
payload,
collapseId: push.collapseId,
});
if (result.ok) return;
log(`APNs refused a push to ${device.name}: ${result.status} ${result.reason}`);
if (result.tokenInvalid) db.devices.clearPush(device.apns_token!);
}),
);
};
const notifier = new Notifier(sendPush, cache, log);
const hub = new Hub(db, { onThreadChange: notifier.onThreadChange, onStatus: notifier.onHostStatus, log });
hub.sync();
setInterval(() => hub.sync(), HOST_SYNC_MS);
notifier.checkTokenExpiry(hub.rows());
setInterval(() => notifier.checkTokenExpiry(hub.rows()), TOKEN_CHECK_MS);
const server = Bun.serve({
port: config.port,
routes: createRoutes({ db, hub, cache, log }),
fetch: () => Response.json({ error: "Not found." }, { status: 404 }),
});
log(`WristCode gateway listening on :${server.port} with ${hub.list().length} host(s)`);
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => {
log(`${signal}: shutting down`);
void server.stop();
hub.stop();
apns?.close();
db.close();
process.exit(0);
});
}