@serfab/cadre-host 1.14.0 → 1.16.0
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 +19 -4
- package/dist/bin/host.js +32 -20
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/nat-output.d.ts +5 -0
- package/dist/bin/nat-output.js +12 -1
- package/dist/bin/nat-output.js.map +1 -1
- package/dist/bin/ui-access.d.ts +22 -0
- package/dist/bin/ui-access.js +44 -0
- package/dist/bin/ui-access.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +25 -3
- package/dist/hosted/claim-details.js +31 -37
- package/dist/hosted/claim-details.js.map +1 -1
- package/dist/hosted/hosted-node-service.d.ts +7 -0
- package/dist/hosted/hosted-node-service.js +16 -4
- package/dist/hosted/hosted-node-service.js.map +1 -1
- package/dist/hosted/hosted-node-watcher.d.ts +16 -4
- package/dist/hosted/hosted-node-watcher.js +45 -9
- package/dist/hosted/hosted-node-watcher.js.map +1 -1
- package/dist/hosted/node-status.d.ts +19 -3
- package/dist/hosted/node-status.js +21 -5
- package/dist/hosted/node-status.js.map +1 -1
- package/dist/hosted/types.d.ts +12 -5
- package/dist/hosted/types.js.map +1 -1
- package/dist/installer/config.d.ts +23 -0
- package/dist/installer/config.js +46 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +7 -16
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -2
- package/dist/installer/wizard.js +15 -7
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/index.d.ts +2 -1
- package/dist/nat/index.js +1 -0
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +22 -5
- package/dist/nat/nat-service.js +75 -26
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.js +11 -0
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/network-mode.d.ts +19 -0
- package/dist/nat/network-mode.js +32 -0
- package/dist/nat/network-mode.js.map +1 -0
- package/dist/nat/reachability.d.ts +5 -4
- package/dist/nat/reachability.js +19 -6
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +26 -6
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +3 -3
- package/dist/orchestrator/host-process-orchestrator.js +6 -15
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +4 -0
- package/dist/package-version.js.map +1 -0
- package/dist/server/routes/settings.d.ts +1 -0
- package/dist/server/routes/settings.js +15 -1
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.js +2 -32
- package/dist/server/routes/status.js.map +1 -1
- package/dist/ui/assets/index-BHd8CWZA.css +1 -0
- package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
- package/dist/ui/assets/{index-4FWuU8ye.js.map → index-Ccr2GjPT.js.map} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +4 -5
- package/src/__tests__/cli.smoke.test.ts +1 -1
- package/src/__tests__/orchestrator-claim-secret.test.ts +1 -2
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +1 -1
- package/src/__tests__/orchestrator-node-identity.test.ts +0 -5
- package/src/__tests__/orchestrator-push.test.ts +3 -3
- package/src/__tests__/orchestrator.test.ts +3 -5
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +30 -20
- package/src/bin/nat-output.ts +12 -1
- package/src/bin/ui-access.ts +50 -0
- package/src/hosted/__tests__/fake-orchestrator.test.ts +0 -1
- package/src/hosted/__tests__/fake-orchestrator.ts +263 -265
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -609
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -474
- package/src/hosted/claim-details.ts +44 -37
- package/src/hosted/hosted-node-service.ts +23 -4
- package/src/hosted/hosted-node-watcher.ts +288 -250
- package/src/hosted/node-status.ts +31 -5
- package/src/hosted/types.ts +160 -153
- package/src/installer/__tests__/installer.smoke.test.ts +184 -182
- package/src/installer/__tests__/wizard.test.ts +76 -56
- package/src/installer/config.ts +66 -0
- package/src/installer/index.ts +252 -259
- package/src/installer/wizard.ts +101 -87
- package/src/nat/__tests__/nat-service.test.ts +608 -540
- package/src/nat/__tests__/nat-store.test.ts +130 -127
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/index.ts +91 -87
- package/src/nat/nat-service.ts +79 -26
- package/src/nat/nat-store.ts +249 -237
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/reachability.ts +155 -143
- package/src/nat/types.ts +232 -209
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1098
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/fakes.ts +41 -40
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -160
- package/src/server/__tests__/settings-route.test.ts +31 -0
- package/src/server/routes/settings.ts +128 -115
- package/src/server/routes/status.ts +72 -102
- package/dist/ui/assets/index-4FWuU8ye.js +0 -19
- package/dist/ui/assets/index-Di9Q4d01.css +0 -1
|
@@ -1,115 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /api/settings — host-level settings UI surface.
|
|
3
|
-
*
|
|
4
|
-
* The persisted shape is `host.config.json` (managed by the installer).
|
|
5
|
-
* This route is a thin passthrough plus a write-whitelist:
|
|
6
|
-
*
|
|
7
|
-
* - uiPort rejected (restart required; only edit at install time)
|
|
8
|
-
* - upnpEnabled accepted → also propagated to NatService.putSettings
|
|
9
|
-
* - updates.autoApply accepted → propagated to UpdateService.putSettings
|
|
10
|
-
* - updates.manifestUrl accepted → propagated to UpdateService.putSettings
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import type {
|
|
21
|
-
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* /api/settings — host-level settings UI surface.
|
|
3
|
+
*
|
|
4
|
+
* The persisted shape is `host.config.json` (managed by the installer).
|
|
5
|
+
* This route is a thin passthrough plus a write-whitelist:
|
|
6
|
+
*
|
|
7
|
+
* - uiPort rejected (restart required; only edit at install time)
|
|
8
|
+
* - upnpEnabled accepted → also propagated to NatService.putSettings
|
|
9
|
+
* - updates.autoApply accepted → propagated to UpdateService.putSettings
|
|
10
|
+
* - updates.manifestUrl accepted → propagated to UpdateService.putSettings
|
|
11
|
+
* - claimAddresses accepted (replaced whole) → read by the next claim code
|
|
12
|
+
* - anything else 400 invalid_setting
|
|
13
|
+
*
|
|
14
|
+
* GET returns the full `HostConfigFile` (the SPA wants the full shape so it
|
|
15
|
+
* can render a read-only view of the rejected keys).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FastifyInstance } from 'fastify';
|
|
19
|
+
|
|
20
|
+
import type { NatService } from '../../nat/index.js';
|
|
21
|
+
import type { UpdateService } from '../../update/index.js';
|
|
22
|
+
import type { HostSettingsStore } from '../settings-store.js';
|
|
23
|
+
import { claimAddressSettingsProblem, type ClaimAddressSettings } from '../../installer/config.js';
|
|
24
|
+
|
|
25
|
+
const FORBIDDEN_KEYS = new Set<string>(['uiPort', 'dataDir', 'installId', 'installedAt', 'installerVersion', 'version']);
|
|
26
|
+
|
|
27
|
+
const WRITABLE_TOP_KEYS = new Set<string>(['upnpEnabled', 'updates', 'claimAddresses']);
|
|
28
|
+
const WRITABLE_UPDATES_KEYS = new Set<string>(['autoApply', 'manifestUrl']);
|
|
29
|
+
|
|
30
|
+
export interface SettingsRoutesOptions {
|
|
31
|
+
settingsStore: HostSettingsStore;
|
|
32
|
+
/** An `upnpEnabled` write takes effect on it at once. */
|
|
33
|
+
nat: NatService;
|
|
34
|
+
update?: UpdateService;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function registerSettingsRoutes(app: FastifyInstance, opts: SettingsRoutesOptions): void {
|
|
38
|
+
const { settingsStore, nat, update } = opts;
|
|
39
|
+
|
|
40
|
+
app.get('/api/settings', async () => {
|
|
41
|
+
return { ok: true, data: settingsStore.read() };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.put('/api/settings', async (request, reply) => {
|
|
45
|
+
const body = (request.body ?? {}) as Record<string, unknown>;
|
|
46
|
+
const validation = validatePatch(body);
|
|
47
|
+
if (validation.error) {
|
|
48
|
+
return reply.code(400).send({
|
|
49
|
+
ok: false,
|
|
50
|
+
error: { code: 'invalid_setting', message: validation.error },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const current = settingsStore.read();
|
|
55
|
+
const patch: { upnpEnabled?: boolean; updates?: typeof current.updates; claimAddresses?: ClaimAddressSettings } = {};
|
|
56
|
+
|
|
57
|
+
if ('upnpEnabled' in body) {
|
|
58
|
+
patch.upnpEnabled = body.upnpEnabled as boolean;
|
|
59
|
+
// Propagate the UPnP toggle into the running NatService so the change
|
|
60
|
+
// takes effect immediately. NatService persists into nat.json — that's
|
|
61
|
+
// independent of host.config.json's `upnpEnabled` (which the installer
|
|
62
|
+
// uses on first run to seed nat.json). Update both for consistency.
|
|
63
|
+
await nat.putSettings({ upnpEnabled: body.upnpEnabled as boolean });
|
|
64
|
+
}
|
|
65
|
+
if ('updates' in body) {
|
|
66
|
+
const updates = body.updates as { autoApply?: boolean; manifestUrl?: string };
|
|
67
|
+
const nextUpdates = { ...current.updates };
|
|
68
|
+
if ('autoApply' in updates) nextUpdates.autoApply = updates.autoApply as boolean;
|
|
69
|
+
if ('manifestUrl' in updates) nextUpdates.manifestUrl = updates.manifestUrl as string;
|
|
70
|
+
patch.updates = nextUpdates;
|
|
71
|
+
if (update) {
|
|
72
|
+
const propagate: { autoApply?: boolean; manifestUrl?: string } = {};
|
|
73
|
+
if ('autoApply' in updates) propagate.autoApply = updates.autoApply as boolean;
|
|
74
|
+
if ('manifestUrl' in updates) propagate.manifestUrl = updates.manifestUrl as string;
|
|
75
|
+
await update.putSettings(propagate);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ('claimAddresses' in body) {
|
|
80
|
+
const { lan, addrs } = body.claimAddresses as ClaimAddressSettings;
|
|
81
|
+
patch.claimAddresses = {
|
|
82
|
+
...(lan !== undefined ? { lan } : {}),
|
|
83
|
+
...(addrs !== undefined ? { addrs: addrs.map((a) => a.trim()).filter((a) => a.length > 0) } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const next = settingsStore.update(patch);
|
|
87
|
+
return { ok: true, data: next };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validatePatch(body: Record<string, unknown>): { error?: string } {
|
|
92
|
+
for (const key of Object.keys(body)) {
|
|
93
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
94
|
+
return { error: `Setting "${key}" is not editable at runtime; change it in host.config.json and restart cadre-host.` };
|
|
95
|
+
}
|
|
96
|
+
if (!WRITABLE_TOP_KEYS.has(key)) {
|
|
97
|
+
return { error: `Unknown setting "${key}"` };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if ('upnpEnabled' in body && typeof body.upnpEnabled !== 'boolean') {
|
|
101
|
+
return { error: '"upnpEnabled" must be a boolean' };
|
|
102
|
+
}
|
|
103
|
+
if ('claimAddresses' in body) {
|
|
104
|
+
const problem = claimAddressSettingsProblem(body.claimAddresses);
|
|
105
|
+
if (problem) return { error: problem };
|
|
106
|
+
}
|
|
107
|
+
if ('updates' in body) {
|
|
108
|
+
if (!body.updates || typeof body.updates !== 'object') {
|
|
109
|
+
return { error: '"updates" must be an object' };
|
|
110
|
+
}
|
|
111
|
+
const updates = body.updates as Record<string, unknown>;
|
|
112
|
+
for (const key of Object.keys(updates)) {
|
|
113
|
+
if (!WRITABLE_UPDATES_KEYS.has(key)) {
|
|
114
|
+
return { error: `Unknown setting "updates.${key}"` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if ('autoApply' in updates && typeof updates.autoApply !== 'boolean') {
|
|
118
|
+
return { error: '"updates.autoApply" must be a boolean' };
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
'manifestUrl' in updates &&
|
|
122
|
+
typeof updates.manifestUrl !== 'string'
|
|
123
|
+
) {
|
|
124
|
+
return { error: '"updates.manifestUrl" must be a string' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
@@ -1,102 +1,72 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /api/status — aggregated health snapshot used by the SPA's dashboard.
|
|
3
|
-
*
|
|
4
|
-
* All inputs are in-memory subsystem state — no disk reads on each call.
|
|
5
|
-
* The response budget is < 5 ms; favour `getStatus()` over `testReachability()`.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
import type {
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
function secondsSince(start: bigint): number {
|
|
74
|
-
const now = process.hrtime.bigint();
|
|
75
|
-
const elapsedNs = now - start;
|
|
76
|
-
return Number(elapsedNs / 1_000_000_000n);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let cachedVersion: string | null = null;
|
|
80
|
-
function readPackageVersion(): string {
|
|
81
|
-
if (cachedVersion) return cachedVersion;
|
|
82
|
-
try {
|
|
83
|
-
// dist/server/routes/status.js -> ../../../package.json
|
|
84
|
-
// src/server/routes/status.ts -> ../../../package.json (vitest)
|
|
85
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
86
|
-
const candidates = [
|
|
87
|
-
resolve(here, '..', '..', '..', 'package.json'),
|
|
88
|
-
resolve(here, '..', '..', 'package.json'),
|
|
89
|
-
];
|
|
90
|
-
for (const p of candidates) {
|
|
91
|
-
try {
|
|
92
|
-
const parsed = JSON.parse(readFileSync(p, 'utf8')) as { version?: string };
|
|
93
|
-
if (parsed.version) {
|
|
94
|
-
cachedVersion = parsed.version;
|
|
95
|
-
return cachedVersion;
|
|
96
|
-
}
|
|
97
|
-
} catch { /* try next */ }
|
|
98
|
-
}
|
|
99
|
-
} catch { /* ignore */ }
|
|
100
|
-
cachedVersion = '0.0.0-unknown';
|
|
101
|
-
return cachedVersion;
|
|
102
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/status — aggregated health snapshot used by the SPA's dashboard.
|
|
3
|
+
*
|
|
4
|
+
* All inputs are in-memory subsystem state — no disk reads on each call.
|
|
5
|
+
* The response budget is < 5 ms; favour `getStatus()` over `testReachability()`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FastifyInstance } from 'fastify';
|
|
9
|
+
|
|
10
|
+
import type { HostProcessOrchestrator } from '../../orchestrator/index.js';
|
|
11
|
+
import type { ManagedNodeInfo } from '../../orchestrator/types.js';
|
|
12
|
+
import type { NatService } from '../../nat/index.js';
|
|
13
|
+
import type { NatStatusSnapshot } from '../../nat/types.js';
|
|
14
|
+
import type { UpdateService } from '../../update/index.js';
|
|
15
|
+
import { packageVersion } from '../../package-version.js';
|
|
16
|
+
|
|
17
|
+
const PROCESS_STARTED_AT = process.hrtime.bigint();
|
|
18
|
+
|
|
19
|
+
export interface StatusRouteOptions {
|
|
20
|
+
orchestrator: HostProcessOrchestrator;
|
|
21
|
+
nat: NatService;
|
|
22
|
+
update?: UpdateService;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StatusResponse {
|
|
26
|
+
service: { name: 'cadre-host'; version: string; uptimeSeconds: number };
|
|
27
|
+
nodes: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
partyId: string;
|
|
30
|
+
status: ManagedNodeInfo['status'];
|
|
31
|
+
profile: ManagedNodeInfo['profile'];
|
|
32
|
+
}>;
|
|
33
|
+
connectivity: NatStatusSnapshot;
|
|
34
|
+
update?: { available?: string; lastChecked?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function registerStatusRoute(app: FastifyInstance, opts: StatusRouteOptions): void {
|
|
38
|
+
app.get('/api/status', async (): Promise<StatusResponse> => {
|
|
39
|
+
const nodes = opts.orchestrator.listNodes().map((n) => ({
|
|
40
|
+
id: n.id,
|
|
41
|
+
partyId: n.partyId,
|
|
42
|
+
status: n.status,
|
|
43
|
+
profile: n.profile,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const response: StatusResponse = {
|
|
47
|
+
service: {
|
|
48
|
+
name: 'cadre-host',
|
|
49
|
+
version: packageVersion,
|
|
50
|
+
uptimeSeconds: secondsSince(PROCESS_STARTED_AT),
|
|
51
|
+
},
|
|
52
|
+
nodes,
|
|
53
|
+
connectivity: opts.nat.getStatus(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (opts.update) {
|
|
57
|
+
const state = await opts.update.getState();
|
|
58
|
+
const update: { available?: string; lastChecked?: string } = {};
|
|
59
|
+
if (state.available?.version) update.available = state.available.version;
|
|
60
|
+
if (state.lastChecked) update.lastChecked = state.lastChecked;
|
|
61
|
+
response.update = update;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return response;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function secondsSince(start: bigint): number {
|
|
69
|
+
const now = process.hrtime.bigint();
|
|
70
|
+
const elapsedNs = now - start;
|
|
71
|
+
return Number(elapsedNs / 1_000_000_000n);
|
|
72
|
+
}
|