dsh-sessions-manager 3.2.2 → 3.4.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.
@@ -0,0 +1,94 @@
1
+ // Storage usage aggregation (pure, no I/O).
2
+ //
3
+ // Kept out of the host bundle so it can be unit-tested directly
4
+ // (tests/storage-stats.test.js). Takes the session items the host already
5
+ // builds (with `sizeBytes` filled in) and rolls them up per workspace plus a
6
+ // "largest sessions" leaderboard.
7
+ //
8
+ // Sessions without a workspace path land in a single "未分组" bucket — that is
9
+ // DSH's own label for orphans, so the panel speaks the same language as the
10
+ // sidebar.
11
+
12
+ export const UNGROUPED_KEY = '__ungrouped__'
13
+
14
+ function isFiniteSize(value) {
15
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0
16
+ }
17
+
18
+ /**
19
+ * Roll session sizes up per workspace and pick the largest sessions.
20
+ *
21
+ * @param {Array<{sessionId: string, title?: string|null, workspacePath?: string|null,
22
+ * workspaceTitle?: string|null, sizeBytes?: number|null}>} items
23
+ * @param {object} [options]
24
+ * @param {number} [options.topN=10] - How many entries the leaderboard holds.
25
+ * @returns {{
26
+ * totalBytes: number,
27
+ * sessionCount: number,
28
+ * sizedSessions: number,
29
+ * unknownSessions: number,
30
+ * workspaces: Array<{key: string, path: string|null, title: string|null,
31
+ * bytes: number, sessions: number, share: number}>,
32
+ * top: Array<{sessionId: string, title: string|null, workspacePath: string|null,
33
+ * workspaceTitle: string|null, sizeBytes: number}>
34
+ * }}
35
+ */
36
+ export function aggregateStorage(items, options = {}) {
37
+ const topN = Number.isInteger(options.topN) && options.topN > 0 ? options.topN : 10
38
+ const list = Array.isArray(items) ? items : []
39
+
40
+ const buckets = new Map()
41
+ const sized = []
42
+ let totalBytes = 0
43
+ let unknownSessions = 0
44
+ // Counted entries only: sessionCount must always equal
45
+ // sizedSessions + unknownSessions, or the panel would show a total that
46
+ // disagrees with its own breakdown.
47
+ let counted = 0
48
+
49
+ for (const item of list) {
50
+ if (!item || item.sessionId == null) continue
51
+ counted++
52
+ const id = String(item.sessionId)
53
+ const path = item.workspacePath ? String(item.workspacePath) : null
54
+ const key = path || UNGROUPED_KEY
55
+
56
+ let bucket = buckets.get(key)
57
+ if (!bucket) {
58
+ bucket = { key, path, title: item.workspaceTitle ? String(item.workspaceTitle) : null, bytes: 0, sessions: 0 }
59
+ buckets.set(key, bucket)
60
+ }
61
+ bucket.sessions++
62
+
63
+ if (isFiniteSize(item.sizeBytes)) {
64
+ bucket.bytes += item.sizeBytes
65
+ totalBytes += item.sizeBytes
66
+ sized.push({
67
+ sessionId: id,
68
+ title: item.title || null,
69
+ workspacePath: path,
70
+ workspaceTitle: bucket.title,
71
+ sizeBytes: item.sizeBytes,
72
+ })
73
+ } else {
74
+ unknownSessions++
75
+ }
76
+ }
77
+
78
+ const workspaces = [...buckets.values()]
79
+ .sort((a, b) => (b.bytes - a.bytes) || (b.sessions - a.sessions) || a.key.localeCompare(b.key))
80
+ .map((bucket) => ({ ...bucket, share: totalBytes > 0 ? bucket.bytes / totalBytes : 0 }))
81
+
82
+ const top = sized
83
+ .sort((a, b) => (b.sizeBytes - a.sizeBytes) || a.sessionId.localeCompare(b.sessionId))
84
+ .slice(0, topN)
85
+
86
+ return {
87
+ totalBytes,
88
+ sessionCount: counted,
89
+ sizedSessions: sized.length,
90
+ unknownSessions,
91
+ workspaces,
92
+ top,
93
+ }
94
+ }