@openchambery/relay-server 1.18.7-beta.2 → 1.18.7-beta.3

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.
@@ -0,0 +1,103 @@
1
+ import http from 'node:http';
2
+
3
+ import { normalizePushRelayOptions, formatPushRelayUrl } from './config.js';
4
+ import { createPushRelayHandler } from './handler.js';
5
+
6
+ const STOP_DEADLINE_MS = 5_500;
7
+
8
+ export const createPushRelayServer = (options = {}) => {
9
+ const config = normalizePushRelayOptions(options);
10
+ const clock = { now: Date.now, setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, ...config.clock };
11
+ const handler = createPushRelayHandler({ ...options, claimHealthEndpoints: true });
12
+ let server = null; let startPromise = null; let stopPromise = null; let abortStart = null; let state = 'idle'; let generation = 0;
13
+
14
+ const start = () => {
15
+ if (state === 'running') return Promise.resolve();
16
+ if (state === 'stopping') return stopPromise.then(() => start());
17
+ if (startPromise) return startPromise;
18
+ state = 'starting';
19
+ const localGeneration = ++generation;
20
+ const localServer = http.createServer((request, response) => {
21
+ handler.handleRequest(request, response);
22
+ });
23
+ server = localServer;
24
+ startPromise = new Promise((resolve, rejectStart) => {
25
+ const failStart = (error) => {
26
+ if (localGeneration !== generation) return;
27
+ localServer.off('listening', ready);
28
+ cleanupStart();
29
+ rejectStart(error);
30
+ };
31
+ const ready = () => {
32
+ localServer.off('error', failStart);
33
+ if (localGeneration !== generation || state !== 'starting') return;
34
+ handler.activate();
35
+ state = 'running';
36
+ resolve();
37
+ };
38
+ const cleanupStart = () => {
39
+ localServer.close();
40
+ if (server === localServer) server = null;
41
+ state = 'stopped';
42
+ };
43
+ abortStart = () => {
44
+ if (state === 'starting') { cleanupStart(); rejectStart(new Error('push relay stopped during start')); }
45
+ };
46
+ localServer.once('error', failStart);
47
+ localServer.once('listening', ready);
48
+ localServer.listen(config.port, config.host);
49
+ }).finally(() => { startPromise = null; abortStart = null; });
50
+ return startPromise;
51
+ };
52
+
53
+ const stop = () => {
54
+ if (stopPromise) return stopPromise;
55
+ if (state === 'idle' || state === 'stopped') { state = 'stopped'; return Promise.resolve(); }
56
+ if (state === 'starting') abortStart?.();
57
+ state = 'stopping';
58
+ generation += 1;
59
+ const localServer = server;
60
+ stopPromise = Promise.resolve().then(async () => {
61
+ let deadlineTimer = null;
62
+ try {
63
+ const closed = new Promise((resolve) => {
64
+ if (!localServer) { resolve(); return; }
65
+ localServer.close(() => resolve());
66
+ try { localServer.closeIdleConnections?.(); } catch { /* ignore */ }
67
+ });
68
+ const deactivated = handler.deactivate();
69
+ const graceful = Promise.all([closed, deactivated]);
70
+ const deadline = new Promise((resolve) => {
71
+ deadlineTimer = clock.setTimeout(() => resolve('deadline'), STOP_DEADLINE_MS);
72
+ });
73
+ const winner = await Promise.race([graceful.then(() => 'graceful'), deadline]);
74
+ if (winner === 'deadline') {
75
+ try { localServer?.closeAllConnections?.(); } catch { /* ignore */ }
76
+ await deactivated;
77
+ }
78
+ } finally {
79
+ if (deadlineTimer !== null) try { clock.clearTimeout(deadlineTimer); } catch { /* ignore */ }
80
+ }
81
+ if (server === localServer) { server = null; state = 'stopped'; }
82
+ stopPromise = null;
83
+ });
84
+ return stopPromise;
85
+ };
86
+
87
+ return {
88
+ start,
89
+ stop,
90
+ address: () => server?.address(),
91
+ get url() {
92
+ const address = server?.address();
93
+ return address && typeof address === 'object' ? formatPushRelayUrl(config.host, address.port) : null;
94
+ },
95
+ getSnapshot: () => ({ ...handler.getSnapshot(), state }),
96
+ };
97
+ };
98
+
99
+ export const startPushRelayServer = async (options) => {
100
+ const server = createPushRelayServer(options);
101
+ await server.start();
102
+ return server;
103
+ };
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+
5
+ const SCHEMA = `CREATE TABLE IF NOT EXISTS push_tokens (
6
+ token TEXT PRIMARY KEY,
7
+ server_id TEXT NOT NULL,
8
+ platform TEXT NOT NULL,
9
+ updated_at INTEGER NOT NULL
10
+ ) STRICT`;
11
+
12
+ export const createTokenStore = (databasePath) => {
13
+ if (typeof databasePath !== 'string' || databasePath.length === 0 || databasePath.includes('\0')) {
14
+ throw new RangeError('invalid database path');
15
+ }
16
+ if (databasePath !== ':memory:') {
17
+ fs.mkdirSync(path.dirname(path.resolve(databasePath)), { recursive: true });
18
+ }
19
+ const db = new DatabaseSync(databasePath, { timeout: 5_000 });
20
+ db.exec('PRAGMA journal_mode = WAL');
21
+ db.exec('PRAGMA busy_timeout = 5000');
22
+ db.exec(SCHEMA);
23
+ const getStmt = db.prepare('SELECT server_id AS serverId, platform, updated_at AS updatedAt FROM push_tokens WHERE token = ?');
24
+ const upsertStmt = db.prepare(`INSERT INTO push_tokens (token, server_id, platform, updated_at) VALUES (?, ?, ?, ?)
25
+ ON CONFLICT(token) DO UPDATE SET server_id = excluded.server_id, platform = excluded.platform, updated_at = excluded.updated_at`);
26
+ const deleteStmt = db.prepare('DELETE FROM push_tokens WHERE token = ?');
27
+ const countStmt = db.prepare('SELECT COUNT(*) AS n FROM push_tokens');
28
+ let closed = false;
29
+ const assertOpen = () => { if (closed) throw new Error('token store closed'); };
30
+ return {
31
+ get(token) {
32
+ assertOpen();
33
+ const row = getStmt.get(token);
34
+ return row ? { serverId: row.serverId, platform: row.platform, updatedAt: row.updatedAt } : null;
35
+ },
36
+ upsert(token, serverId, platform, updatedAt) {
37
+ assertOpen();
38
+ upsertStmt.run(token, serverId, platform, updatedAt);
39
+ },
40
+ delete(token) {
41
+ assertOpen();
42
+ deleteStmt.run(token);
43
+ },
44
+ count() {
45
+ assertOpen();
46
+ return Number(countStmt.get().n);
47
+ },
48
+ close() {
49
+ if (closed) return;
50
+ closed = true;
51
+ try { db.close(); } catch { /* already closed */ }
52
+ },
53
+ };
54
+ };