Fix the session id never reaching the host, and the split lost on restart
Two real defects, both found by testing rather than reading: 1. The client sent sessionId in the POST body while the route read it from the query string, so the host always saw an empty id and answered total:null -- the pill was correct to stay silent. The client now sends the id in both places and the route reads both. 2. The ledger stored only a session total, so a restart kept the sum but dropped the per-purpose split and the call count (9.99 total against 6.24 in parts, seen on the live route). Sessions are now stored as objects; a bare number from an older ledger is still accepted. Also made the client report why it cannot read a total: swallowing the error made 'nothing spent yet' indistinguishable from 'request failed'.
This commit is contained in:
+49
-6
@@ -149,11 +149,17 @@ export default {
|
||||
for (const [id, cost] of Object.entries(parsed.costs ?? {})) {
|
||||
costByGeneration.set(id, cost);
|
||||
}
|
||||
for (const [id, total] of Object.entries(parsed.sessions ?? {})) {
|
||||
// Sessions are stored as objects so the per-purpose split survives a
|
||||
// restart; older ledgers wrote a bare number, which is still accepted.
|
||||
for (const [id, value] of Object.entries(parsed.sessions ?? {})) {
|
||||
if (typeof value === 'number') {
|
||||
sessionTotals.set(id, { total: finiteAmount(value) ?? 0, calls: 0, byPurpose: {} });
|
||||
continue;
|
||||
}
|
||||
sessionTotals.set(id, {
|
||||
total: finiteAmount(total) ?? 0,
|
||||
calls: 0,
|
||||
byPurpose: {},
|
||||
total: finiteAmount(value?.total) ?? 0,
|
||||
calls: typeof value?.calls === 'number' ? value.calls : 0,
|
||||
byPurpose: value?.byPurpose && typeof value.byPurpose === 'object' ? { ...value.byPurpose } : {},
|
||||
});
|
||||
}
|
||||
for (const id of parsed.settled ?? []) settled.add(id);
|
||||
@@ -172,7 +178,10 @@ export default {
|
||||
version: 1,
|
||||
costs: Object.fromEntries(costByGeneration),
|
||||
sessions: Object.fromEntries(
|
||||
[...sessionTotals].map(([id, value]) => [id, value.total]),
|
||||
[...sessionTotals].map(([id, value]) => [
|
||||
id,
|
||||
{ total: value.total, calls: value.calls, byPurpose: value.byPurpose },
|
||||
]),
|
||||
),
|
||||
settled: [...settled],
|
||||
};
|
||||
@@ -417,6 +426,36 @@ export default {
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
/** Read a request body, capped: this route only ever carries a session id. */
|
||||
function readJsonBody(req, limit = 16 * 1024) {
|
||||
return new Promise((resolve) => {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
req.on('data', (chunk) => {
|
||||
size += chunk.length;
|
||||
if (size > limit) {
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
||||
if (raw.length === 0) {
|
||||
resolve({});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
resolve(parsed && typeof parsed === 'object' ? parsed : {});
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
req.on('error', () => resolve({}));
|
||||
});
|
||||
}
|
||||
|
||||
ctx.inject(['connection', 'webServer'], (webCtx) => {
|
||||
webCtx.effect(() => {
|
||||
const dispose = webCtx.webServer.register({
|
||||
@@ -435,7 +474,11 @@ export default {
|
||||
respond(res, 404, { error: 'unknown method' });
|
||||
return;
|
||||
}
|
||||
const sessionId = url.searchParams.get('sessionId') ?? '';
|
||||
// The client posts a JSON body; the query string is kept as a
|
||||
// fallback so either shape works.
|
||||
const body = req.method === 'POST' || req.method === 'PUT' ? await readJsonBody(req) : {};
|
||||
const raw = body.sessionId ?? url.searchParams.get('sessionId') ?? '';
|
||||
const sessionId = typeof raw === 'string' ? raw : String(raw ?? '');
|
||||
respond(res, 200, tail === '/session' ? snapshot(sessionId) : snapshot(''));
|
||||
} catch (error) {
|
||||
respond(res, 500, { error: String(error?.message ?? error) });
|
||||
|
||||
Reference in New Issue
Block a user