Skip to content

Commit 92aa386

Browse files
committed
feat: use object.groupBy as much as possible
1 parent 186247f commit 92aa386

1 file changed

Lines changed: 150 additions & 98 deletions

File tree

src/data-loader.ts

Lines changed: 150 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ export async function loadDailyUsageData(
146146
const mode = options?.mode || "auto";
147147
const modelPricing = mode === "display" ? {} : await fetchModelPricing();
148148

149-
const dailyMap = new Map<string, DailyUsage>();
149+
// Collect all valid data entries first
150+
const allEntries: { data: UsageData; date: string; cost: number }[] = [];
150151

151152
for (const file of files) {
152153
const content = await readFile(file, "utf-8");
@@ -165,35 +166,49 @@ export async function loadDailyUsageData(
165166
const data = result.output;
166167

167168
const date = formatDate(data.timestamp);
168-
const existing = dailyMap.get(date) || {
169-
date,
170-
inputTokens: 0,
171-
outputTokens: 0,
172-
cacheCreationTokens: 0,
173-
cacheReadTokens: 0,
174-
totalCost: 0,
175-
};
176-
177-
existing.inputTokens += data.message.usage.input_tokens ?? 0;
178-
existing.outputTokens += data.message.usage.output_tokens ?? 0;
179-
existing.cacheCreationTokens +=
180-
data.message.usage.cache_creation_input_tokens ?? 0;
181-
existing.cacheReadTokens +=
182-
data.message.usage.cache_read_input_tokens ?? 0;
183-
184-
// Calculate cost based on mode
185169
const cost = calculateCostForEntry(data, mode, modelPricing);
186-
existing.totalCost += cost;
187170

188-
dailyMap.set(date, existing);
171+
allEntries.push({ data, date, cost });
189172
} catch (e) {
190173
// Skip invalid JSON lines
191174
}
192175
}
193176
}
194177

195-
// Convert map to array and filter by date range
196-
let results = Array.from(dailyMap.values());
178+
// Group by date using Object.groupBy
179+
const groupedByDate = Object.groupBy(allEntries, (entry) => entry.date);
180+
181+
// Aggregate each group
182+
let results = Object.entries(groupedByDate)
183+
.map(([date, entries]) => {
184+
if (!entries) return null;
185+
186+
return entries.reduce(
187+
(acc, entry) => ({
188+
date,
189+
inputTokens:
190+
acc.inputTokens + (entry.data.message.usage.input_tokens ?? 0),
191+
outputTokens:
192+
acc.outputTokens + (entry.data.message.usage.output_tokens ?? 0),
193+
cacheCreationTokens:
194+
acc.cacheCreationTokens +
195+
(entry.data.message.usage.cache_creation_input_tokens ?? 0),
196+
cacheReadTokens:
197+
acc.cacheReadTokens +
198+
(entry.data.message.usage.cache_read_input_tokens ?? 0),
199+
totalCost: acc.totalCost + entry.cost,
200+
}),
201+
{
202+
date,
203+
inputTokens: 0,
204+
outputTokens: 0,
205+
cacheCreationTokens: 0,
206+
cacheReadTokens: 0,
207+
totalCost: 0,
208+
},
209+
);
210+
})
211+
.filter((item): item is DailyUsage => item !== null);
197212

198213
if (options?.since || options?.until) {
199214
results = results.filter((data) => {
@@ -230,27 +245,31 @@ export async function loadSessionData(
230245
const mode = options?.mode || "auto";
231246
const modelPricing = mode === "display" ? {} : await fetchModelPricing();
232247

233-
const sessionMap = new Map<
234-
string,
235-
SessionUsage & { versionSet: Set<string> }
236-
>();
248+
// Collect all valid data entries with session info first
249+
const allEntries: Array<{
250+
data: UsageData;
251+
sessionKey: string;
252+
sessionId: string;
253+
projectPath: string;
254+
cost: number;
255+
timestamp: string;
256+
}> = [];
237257

238258
for (const file of files) {
239259
// Extract session info from file path
240260
const relativePath = path.relative(claudeDir, file);
241261
const parts = relativePath.split(path.sep);
242262

243263
// Session ID is the directory name containing the JSONL file
244-
const sessionId = parts[parts.length - 2];
264+
const sessionId = parts[parts.length - 2] || "unknown";
245265
// Project path is everything before the session ID
246-
const projectPath = parts.slice(0, -2).join(path.sep);
266+
const projectPath = parts.slice(0, -2).join(path.sep) || "Unknown Project";
247267

248268
const content = await readFile(file, "utf-8");
249269
const lines = content
250270
.trim()
251271
.split("\n")
252272
.filter((line) => line.length > 0);
253-
let lastTimestamp = "";
254273

255274
for (const line of lines) {
256275
try {
@@ -261,58 +280,84 @@ export async function loadSessionData(
261280
}
262281
const data = result.output;
263282

264-
const key = `${projectPath}/${sessionId}`;
265-
const existing = sessionMap.get(key) || {
266-
sessionId: sessionId || "unknown",
267-
projectPath: projectPath || "Unknown Project",
268-
inputTokens: 0,
269-
outputTokens: 0,
270-
cacheCreationTokens: 0,
271-
cacheReadTokens: 0,
272-
totalCost: 0,
273-
lastActivity: "",
274-
versions: [],
275-
versionSet: new Set<string>(),
276-
};
277-
278-
existing.inputTokens += data.message.usage.input_tokens ?? 0;
279-
existing.outputTokens += data.message.usage.output_tokens ?? 0;
280-
existing.cacheCreationTokens +=
281-
data.message.usage.cache_creation_input_tokens ?? 0;
282-
existing.cacheReadTokens +=
283-
data.message.usage.cache_read_input_tokens ?? 0;
284-
285-
// Calculate cost based on mode
283+
const sessionKey = `${projectPath}/${sessionId}`;
286284
const cost = calculateCostForEntry(data, mode, modelPricing);
287-
existing.totalCost += cost;
288285

289-
// Keep track of the latest timestamp
290-
if (data.timestamp > lastTimestamp) {
291-
lastTimestamp = data.timestamp;
292-
existing.lastActivity = formatDate(data.timestamp);
293-
}
294-
295-
// Collect version information
296-
if (data.version) {
297-
existing.versionSet.add(data.version);
298-
}
299-
300-
sessionMap.set(key, existing);
286+
allEntries.push({
287+
data,
288+
sessionKey,
289+
sessionId,
290+
projectPath,
291+
cost,
292+
timestamp: data.timestamp,
293+
});
301294
} catch (e) {
302295
// Skip invalid JSON lines
303296
}
304297
}
305298
}
306299

307-
// Convert map to array and filter by date range
308-
let results = Array.from(sessionMap.values()).map((session) => {
309-
// Convert Set to sorted array and remove temporary versionSet property
310-
const { versionSet, ...sessionData } = session;
311-
return {
312-
...sessionData,
313-
versions: Array.from(versionSet).sort(),
314-
};
315-
});
300+
// Group by session using Object.groupBy
301+
const groupedBySessions = Object.groupBy(
302+
allEntries,
303+
(entry) => entry.sessionKey,
304+
);
305+
306+
// Aggregate each session group
307+
let results = Object.entries(groupedBySessions)
308+
.map(([_, entries]) => {
309+
if (entries == null) {
310+
return undefined;
311+
}
312+
313+
// Find the latest timestamp for lastActivity
314+
const latestEntry = entries.reduce((latest, current) =>
315+
current.timestamp > latest.timestamp ? current : latest,
316+
);
317+
318+
// Collect all unique versions
319+
const versionSet = new Set<string>();
320+
for (const entry of entries) {
321+
if (entry.data.version) {
322+
versionSet.add(entry.data.version);
323+
}
324+
}
325+
326+
// Aggregate totals
327+
const aggregated = entries.reduce(
328+
(acc, entry) => ({
329+
sessionId: latestEntry.sessionId,
330+
projectPath: latestEntry.projectPath,
331+
inputTokens:
332+
acc.inputTokens + (entry.data.message.usage.input_tokens ?? 0),
333+
outputTokens:
334+
acc.outputTokens + (entry.data.message.usage.output_tokens ?? 0),
335+
cacheCreationTokens:
336+
acc.cacheCreationTokens +
337+
(entry.data.message.usage.cache_creation_input_tokens ?? 0),
338+
cacheReadTokens:
339+
acc.cacheReadTokens +
340+
(entry.data.message.usage.cache_read_input_tokens ?? 0),
341+
totalCost: acc.totalCost + entry.cost,
342+
lastActivity: formatDate(latestEntry.timestamp),
343+
versions: Array.from(versionSet).sort(),
344+
}),
345+
{
346+
sessionId: latestEntry.sessionId,
347+
projectPath: latestEntry.projectPath,
348+
inputTokens: 0,
349+
outputTokens: 0,
350+
cacheCreationTokens: 0,
351+
cacheReadTokens: 0,
352+
totalCost: 0,
353+
lastActivity: formatDate(latestEntry.timestamp),
354+
versions: Array.from(versionSet).sort(),
355+
},
356+
);
357+
358+
return aggregated;
359+
})
360+
.filter((item): item is SessionUsage => item !== null);
316361

317362
if (options?.since || options?.until) {
318363
results = results.filter((session) => {
@@ -336,32 +381,39 @@ export async function loadMonthlyUsageData(
336381
): Promise<MonthlyUsage[]> {
337382
const dailyData = await loadDailyUsageData(options);
338383

339-
const monthlyMap = new Map<string, MonthlyUsage>();
340-
341-
for (const data of dailyData) {
342-
// Extract YYYY-MM from YYYY-MM-DD
343-
const month = data.date.substring(0, 7);
344-
345-
const existing = monthlyMap.get(month) || {
346-
month,
347-
inputTokens: 0,
348-
outputTokens: 0,
349-
cacheCreationTokens: 0,
350-
cacheReadTokens: 0,
351-
totalCost: 0,
352-
};
353-
354-
existing.inputTokens += data.inputTokens;
355-
existing.outputTokens += data.outputTokens;
356-
existing.cacheCreationTokens += data.cacheCreationTokens;
357-
existing.cacheReadTokens += data.cacheReadTokens;
358-
existing.totalCost += data.totalCost;
359-
360-
monthlyMap.set(month, existing);
361-
}
384+
// Group daily data by month using Object.groupBy
385+
const groupedByMonth = Object.groupBy(dailyData, (data) =>
386+
data.date.substring(0, 7),
387+
);
388+
389+
// Aggregate each month group
390+
const monthlyArray = Object.entries(groupedByMonth)
391+
.map(([month, dailyEntries]) => {
392+
if (!dailyEntries) return null;
393+
394+
return dailyEntries.reduce(
395+
(acc, data) => ({
396+
month,
397+
inputTokens: acc.inputTokens + data.inputTokens,
398+
outputTokens: acc.outputTokens + data.outputTokens,
399+
cacheCreationTokens:
400+
acc.cacheCreationTokens + data.cacheCreationTokens,
401+
cacheReadTokens: acc.cacheReadTokens + data.cacheReadTokens,
402+
totalCost: acc.totalCost + data.totalCost,
403+
}),
404+
{
405+
month,
406+
inputTokens: 0,
407+
outputTokens: 0,
408+
cacheCreationTokens: 0,
409+
cacheReadTokens: 0,
410+
totalCost: 0,
411+
},
412+
);
413+
})
414+
.filter((item): item is MonthlyUsage => item !== null);
362415

363-
// Convert to array and sort by month based on sortOrder
364-
const monthlyArray = Array.from(monthlyMap.values());
416+
// Sort by month based on sortOrder
365417
const sortOrder = options?.order || "desc";
366418
const sortedMonthly = sort(monthlyArray);
367419
return sortOrder === "desc"

0 commit comments

Comments
 (0)