@vibescore/tracker 0.0.2 → 0.0.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.
- package/bin/tracker.js +9 -0
- package/package.json +1 -1
- package/src/commands/sync.js +38 -4
- package/src/lib/vibescore-api.js +33 -2
package/bin/tracker.js
CHANGED
|
@@ -9,10 +9,19 @@ if (debug) process.env.VIBESCORE_DEBUG = '1';
|
|
|
9
9
|
run(argv).catch((err) => {
|
|
10
10
|
console.error(err?.stack || String(err));
|
|
11
11
|
if (debug) {
|
|
12
|
+
if (typeof err?.status === 'number') {
|
|
13
|
+
console.error(`Status: ${err.status}`);
|
|
14
|
+
}
|
|
15
|
+
if (typeof err?.code === 'string' && err.code.trim()) {
|
|
16
|
+
console.error(`Code: ${err.code.trim()}`);
|
|
17
|
+
}
|
|
12
18
|
const original = err?.originalMessage;
|
|
13
19
|
if (original && original !== err?.message) {
|
|
14
20
|
console.error(`Original error: ${original}`);
|
|
15
21
|
}
|
|
22
|
+
if (typeof err?.nextActions === 'string' && err.nextActions.trim()) {
|
|
23
|
+
console.error(`Next actions: ${err.nextActions.trim()}`);
|
|
24
|
+
}
|
|
16
25
|
}
|
|
17
26
|
process.exitCode = 1;
|
|
18
27
|
});
|
package/package.json
CHANGED
package/src/commands/sync.js
CHANGED
|
@@ -6,6 +6,7 @@ const { ensureDir, readJson, writeJson, openLock } = require('../lib/fs');
|
|
|
6
6
|
const { listRolloutFiles, parseRolloutIncremental } = require('../lib/rollout');
|
|
7
7
|
const { drainQueueToCloud } = require('../lib/uploader');
|
|
8
8
|
const { createProgress, renderBar, formatNumber, formatBytes } = require('../lib/progress');
|
|
9
|
+
const { syncHeartbeat } = require('../lib/vibescore-api');
|
|
9
10
|
const {
|
|
10
11
|
DEFAULTS: UPLOAD_DEFAULTS,
|
|
11
12
|
normalizeState: normalizeUploadState,
|
|
@@ -130,11 +131,19 @@ async function cmdSync(argv) {
|
|
|
130
131
|
progress?.stop();
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const afterState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
135
|
+
const queueSize = await safeStatSize(queuePath);
|
|
136
|
+
const pendingBytes = Math.max(0, queueSize - Number(afterState.offset || 0));
|
|
137
|
+
|
|
138
|
+
await maybeSendHeartbeat({
|
|
139
|
+
baseUrl,
|
|
140
|
+
deviceToken,
|
|
141
|
+
trackerDir,
|
|
142
|
+
uploadResult,
|
|
143
|
+
pendingBytes
|
|
144
|
+
});
|
|
137
145
|
|
|
146
|
+
if (!opts.auto) {
|
|
138
147
|
process.stdout.write(
|
|
139
148
|
[
|
|
140
149
|
'Sync finished:',
|
|
@@ -185,3 +194,28 @@ async function safeStatSize(p) {
|
|
|
185
194
|
return 0;
|
|
186
195
|
}
|
|
187
196
|
}
|
|
197
|
+
|
|
198
|
+
async function maybeSendHeartbeat({ baseUrl, deviceToken, trackerDir, uploadResult, pendingBytes }) {
|
|
199
|
+
if (!deviceToken || !uploadResult) return;
|
|
200
|
+
if (pendingBytes > 0) return;
|
|
201
|
+
if (Number(uploadResult.inserted || 0) !== 0) return;
|
|
202
|
+
|
|
203
|
+
const heartbeatPath = path.join(trackerDir, 'sync.heartbeat.json');
|
|
204
|
+
const heartbeatState = await readJson(heartbeatPath);
|
|
205
|
+
const lastPingAt = Date.parse(heartbeatState?.lastPingAt || '');
|
|
206
|
+
const nowMs = Date.now();
|
|
207
|
+
if (Number.isFinite(lastPingAt) && nowMs - lastPingAt < HEARTBEAT_MIN_INTERVAL_MS) return;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await syncHeartbeat({ baseUrl, deviceToken });
|
|
211
|
+
await writeJson(heartbeatPath, {
|
|
212
|
+
lastPingAt: new Date(nowMs).toISOString(),
|
|
213
|
+
minIntervalMinutes: HEARTBEAT_MIN_INTERVAL_MINUTES
|
|
214
|
+
});
|
|
215
|
+
} catch (_e) {
|
|
216
|
+
// best-effort heartbeat; ignore failures
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const HEARTBEAT_MIN_INTERVAL_MINUTES = 30;
|
|
221
|
+
const HEARTBEAT_MIN_INTERVAL_MS = HEARTBEAT_MIN_INTERVAL_MINUTES * 60 * 1000;
|
package/src/lib/vibescore-api.js
CHANGED
|
@@ -52,10 +52,28 @@ async function ingestEvents({ baseUrl, deviceToken, events }) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
async function syncHeartbeat({ baseUrl, deviceToken }) {
|
|
56
|
+
const data = await invokeFunction({
|
|
57
|
+
baseUrl,
|
|
58
|
+
accessToken: deviceToken,
|
|
59
|
+
slug: 'vibescore-sync-ping',
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: {},
|
|
62
|
+
errorPrefix: 'Sync heartbeat failed'
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
updated: Boolean(data?.updated),
|
|
67
|
+
last_sync_at: typeof data?.last_sync_at === 'string' ? data.last_sync_at : null,
|
|
68
|
+
min_interval_minutes: Number(data?.min_interval_minutes || 0)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
module.exports = {
|
|
56
73
|
signInWithPassword,
|
|
57
74
|
issueDeviceToken,
|
|
58
|
-
ingestEvents
|
|
75
|
+
ingestEvents,
|
|
76
|
+
syncHeartbeat
|
|
59
77
|
};
|
|
60
78
|
|
|
61
79
|
async function invokeFunction({ baseUrl, accessToken, slug, method, body, errorPrefix }) {
|
|
@@ -82,17 +100,30 @@ async function invokeFunctionWithRetry({ baseUrl, accessToken, slug, method, bod
|
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
function normalizeSdkError(error, errorPrefix) {
|
|
85
|
-
const raw =
|
|
103
|
+
const raw = extractSdkErrorMessage(error);
|
|
86
104
|
const msg = normalizeBackendErrorMessage(raw);
|
|
87
105
|
const err = new Error(errorPrefix ? `${errorPrefix}: ${msg}` : msg);
|
|
88
106
|
const status = error?.statusCode ?? error?.status;
|
|
107
|
+
const code = typeof error?.error === 'string' ? error.error.trim() : '';
|
|
89
108
|
if (typeof status === 'number') err.status = status;
|
|
109
|
+
if (code) err.code = code;
|
|
90
110
|
err.retryable = isRetryableStatus(status) || isRetryableMessage(raw);
|
|
91
111
|
if (msg !== raw) err.originalMessage = raw;
|
|
92
112
|
if (error?.nextActions) err.nextActions = error.nextActions;
|
|
93
113
|
return err;
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
function extractSdkErrorMessage(error) {
|
|
117
|
+
if (!error) return 'Unknown error';
|
|
118
|
+
const message = typeof error.message === 'string' ? error.message.trim() : '';
|
|
119
|
+
const code = typeof error.error === 'string' ? error.error.trim() : '';
|
|
120
|
+
if (message && message !== 'InsForgeError') return message;
|
|
121
|
+
if (code && code !== 'REQUEST_FAILED') return code;
|
|
122
|
+
if (message) return message;
|
|
123
|
+
if (code) return code;
|
|
124
|
+
return String(error);
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
function normalizeBackendErrorMessage(message) {
|
|
97
128
|
if (!isBackendRuntimeDownMessage(message)) return String(message || 'Unknown error');
|
|
98
129
|
return 'Backend runtime unavailable (InsForge). Please retry later.';
|