@soulbatical/tetra-core 0.5.3 → 0.5.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/dist/shared/telemetry/heartbeat.d.ts +10 -2
- package/dist/shared/telemetry/heartbeat.d.ts.map +1 -1
- package/dist/shared/telemetry/heartbeat.js +53 -18
- package/dist/shared/telemetry/heartbeat.js.map +1 -1
- package/dist/shared/telemetry/index.d.ts +1 -1
- package/dist/shared/telemetry/index.d.ts.map +1 -1
- package/dist/shared/telemetry/index.js +1 -1
- package/dist/shared/telemetry/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* are tracked. Non-blocking: failures are silently ignored so the app
|
|
6
6
|
* always works, with or without connectivity.
|
|
7
7
|
*
|
|
8
|
+
* The server controls the heartbeat interval via nextIntervalMs in the
|
|
9
|
+
* response. This allows remote adjustment without redeploying clients.
|
|
10
|
+
*
|
|
8
11
|
* Opt-out: set TETRA_TELEMETRY=false env var.
|
|
9
12
|
*
|
|
10
13
|
* @module @soulbatical/tetra-core/telemetry
|
|
@@ -24,15 +27,20 @@ export declare function isTelemetryEnabled(config?: {
|
|
|
24
27
|
export declare function buildHeartbeatPayload(config: TelemetryConfig): HeartbeatPayload | null;
|
|
25
28
|
/**
|
|
26
29
|
* Send a single heartbeat. Non-blocking: never throws.
|
|
30
|
+
* Returns the server-suggested next interval in ms, or null on failure.
|
|
27
31
|
*/
|
|
28
|
-
export declare function sendHeartbeat(config: TelemetryConfig): Promise<
|
|
32
|
+
export declare function sendHeartbeat(config: TelemetryConfig): Promise<number | null>;
|
|
29
33
|
/**
|
|
30
34
|
* Start periodic heartbeat. Call once at server startup.
|
|
31
|
-
* Returns a cleanup function to stop the
|
|
35
|
+
* Returns a cleanup function to stop the timer.
|
|
32
36
|
*/
|
|
33
37
|
export declare function startHeartbeat(config: TelemetryConfig): () => void;
|
|
34
38
|
/**
|
|
35
39
|
* Stop the heartbeat timer (for testing or shutdown).
|
|
36
40
|
*/
|
|
37
41
|
export declare function stopHeartbeat(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Get the current heartbeat interval (for observability).
|
|
44
|
+
*/
|
|
45
|
+
export declare function getCurrentInterval(): number;
|
|
38
46
|
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../../src/shared/telemetry/heartbeat.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../../src/shared/telemetry/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAWpE;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAK3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CA+BtF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkCnF;AAoBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,IAAI,CAwBlE;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* are tracked. Non-blocking: failures are silently ignored so the app
|
|
6
6
|
* always works, with or without connectivity.
|
|
7
7
|
*
|
|
8
|
+
* The server controls the heartbeat interval via nextIntervalMs in the
|
|
9
|
+
* response. This allows remote adjustment without redeploying clients.
|
|
10
|
+
*
|
|
8
11
|
* Opt-out: set TETRA_TELEMETRY=false env var.
|
|
9
12
|
*
|
|
10
13
|
* @module @soulbatical/tetra-core/telemetry
|
|
@@ -12,9 +15,12 @@
|
|
|
12
15
|
import crypto from 'node:crypto';
|
|
13
16
|
import { hostname } from 'node:os';
|
|
14
17
|
import { validateLicense } from '../license/validator.js';
|
|
15
|
-
const
|
|
18
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
19
|
+
const MIN_INTERVAL = 60 * 1000; // 1 minute minimum (prevent abuse)
|
|
20
|
+
const MAX_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 week maximum
|
|
16
21
|
const DEFAULT_HEARTBEAT_URL = 'https://ralph.soulbatical.com/api/public/tetra/heartbeat';
|
|
17
22
|
let heartbeatTimer = null;
|
|
23
|
+
let currentIntervalMs = ONE_HOUR;
|
|
18
24
|
/**
|
|
19
25
|
* Check if telemetry is enabled.
|
|
20
26
|
* Disabled by TETRA_TELEMETRY=false or config.disabled=true.
|
|
@@ -64,14 +70,15 @@ export function buildHeartbeatPayload(config) {
|
|
|
64
70
|
}
|
|
65
71
|
/**
|
|
66
72
|
* Send a single heartbeat. Non-blocking: never throws.
|
|
73
|
+
* Returns the server-suggested next interval in ms, or null on failure.
|
|
67
74
|
*/
|
|
68
75
|
export async function sendHeartbeat(config) {
|
|
69
76
|
try {
|
|
70
77
|
if (!isTelemetryEnabled(config))
|
|
71
|
-
return
|
|
78
|
+
return null;
|
|
72
79
|
const payload = buildHeartbeatPayload(config);
|
|
73
80
|
if (!payload)
|
|
74
|
-
return
|
|
81
|
+
return null;
|
|
75
82
|
const url = config.heartbeatUrl || DEFAULT_HEARTBEAT_URL;
|
|
76
83
|
const controller = new AbortController();
|
|
77
84
|
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
@@ -82,37 +89,59 @@ export async function sendHeartbeat(config) {
|
|
|
82
89
|
signal: controller.signal,
|
|
83
90
|
});
|
|
84
91
|
clearTimeout(timeout);
|
|
85
|
-
|
|
92
|
+
if (!response.ok)
|
|
93
|
+
return null;
|
|
94
|
+
// Read server-controlled interval from response
|
|
95
|
+
const body = await response.json();
|
|
96
|
+
if (body.nextIntervalMs && body.nextIntervalMs >= MIN_INTERVAL && body.nextIntervalMs <= MAX_INTERVAL) {
|
|
97
|
+
return body.nextIntervalMs;
|
|
98
|
+
}
|
|
99
|
+
return currentIntervalMs;
|
|
86
100
|
}
|
|
87
101
|
catch {
|
|
88
102
|
// Silently ignore — telemetry must never break the app
|
|
89
|
-
return
|
|
103
|
+
return null;
|
|
90
104
|
}
|
|
91
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Schedule the next heartbeat using setTimeout (not setInterval).
|
|
108
|
+
* This allows dynamic interval changes based on server response.
|
|
109
|
+
*/
|
|
110
|
+
function scheduleNext(config) {
|
|
111
|
+
heartbeatTimer = setTimeout(async () => {
|
|
112
|
+
const nextInterval = await sendHeartbeat(config);
|
|
113
|
+
if (nextInterval != null) {
|
|
114
|
+
currentIntervalMs = nextInterval;
|
|
115
|
+
}
|
|
116
|
+
// Schedule again with (possibly updated) interval
|
|
117
|
+
scheduleNext(config);
|
|
118
|
+
}, currentIntervalMs);
|
|
119
|
+
// Don't prevent process exit
|
|
120
|
+
if (heartbeatTimer.unref)
|
|
121
|
+
heartbeatTimer.unref();
|
|
122
|
+
}
|
|
92
123
|
/**
|
|
93
124
|
* Start periodic heartbeat. Call once at server startup.
|
|
94
|
-
* Returns a cleanup function to stop the
|
|
125
|
+
* Returns a cleanup function to stop the timer.
|
|
95
126
|
*/
|
|
96
127
|
export function startHeartbeat(config) {
|
|
97
128
|
if (!isTelemetryEnabled(config)) {
|
|
98
129
|
return () => { };
|
|
99
130
|
}
|
|
100
|
-
|
|
131
|
+
currentIntervalMs = config.intervalMs ?? ONE_HOUR;
|
|
101
132
|
// Initial heartbeat (delayed 5s to let the server fully start)
|
|
102
|
-
const initialTimeout = setTimeout(() => {
|
|
103
|
-
sendHeartbeat(config);
|
|
133
|
+
const initialTimeout = setTimeout(async () => {
|
|
134
|
+
const nextInterval = await sendHeartbeat(config);
|
|
135
|
+
if (nextInterval != null) {
|
|
136
|
+
currentIntervalMs = nextInterval;
|
|
137
|
+
}
|
|
138
|
+
// Start the recurring schedule
|
|
139
|
+
scheduleNext(config);
|
|
104
140
|
}, 5_000);
|
|
105
|
-
// Periodic heartbeat
|
|
106
|
-
heartbeatTimer = setInterval(() => {
|
|
107
|
-
sendHeartbeat(config);
|
|
108
|
-
}, intervalMs);
|
|
109
|
-
// Don't prevent process exit
|
|
110
|
-
if (heartbeatTimer.unref)
|
|
111
|
-
heartbeatTimer.unref();
|
|
112
141
|
return () => {
|
|
113
142
|
clearTimeout(initialTimeout);
|
|
114
143
|
if (heartbeatTimer) {
|
|
115
|
-
|
|
144
|
+
clearTimeout(heartbeatTimer);
|
|
116
145
|
heartbeatTimer = null;
|
|
117
146
|
}
|
|
118
147
|
};
|
|
@@ -122,8 +151,14 @@ export function startHeartbeat(config) {
|
|
|
122
151
|
*/
|
|
123
152
|
export function stopHeartbeat() {
|
|
124
153
|
if (heartbeatTimer) {
|
|
125
|
-
|
|
154
|
+
clearTimeout(heartbeatTimer);
|
|
126
155
|
heartbeatTimer = null;
|
|
127
156
|
}
|
|
128
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Get the current heartbeat interval (for observability).
|
|
160
|
+
*/
|
|
161
|
+
export function getCurrentInterval() {
|
|
162
|
+
return currentIntervalMs;
|
|
163
|
+
}
|
|
129
164
|
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../../src/shared/telemetry/heartbeat.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../../src/shared/telemetry/heartbeat.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
* @module @soulbatical/tetra-core/telemetry
|
|
9
9
|
*/
|
|
10
10
|
export type { HeartbeatPayload, TelemetryConfig } from './types.js';
|
|
11
|
-
export { isTelemetryEnabled, buildHeartbeatPayload, sendHeartbeat, startHeartbeat, stopHeartbeat, } from './heartbeat.js';
|
|
11
|
+
export { isTelemetryEnabled, buildHeartbeatPayload, sendHeartbeat, startHeartbeat, stopHeartbeat, getCurrentInterval, } from './heartbeat.js';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/telemetry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/telemetry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,aAAa,EACb,kBAAkB,GACnB,MAAM,gBAAgB,CAAC"}
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module @soulbatical/tetra-core/telemetry
|
|
9
9
|
*/
|
|
10
|
-
export { isTelemetryEnabled, buildHeartbeatPayload, sendHeartbeat, startHeartbeat, stopHeartbeat, } from './heartbeat.js';
|
|
10
|
+
export { isTelemetryEnabled, buildHeartbeatPayload, sendHeartbeat, startHeartbeat, stopHeartbeat, getCurrentInterval, } from './heartbeat.js';
|
|
11
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/telemetry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,aAAa,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/telemetry/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,aAAa,EACb,kBAAkB,GACnB,MAAM,gBAAgB,CAAC"}
|