dsh-all-usage 1.1.18 → 1.2.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.
- package/CHANGELOG.md +453 -394
- package/LICENSE +21 -21
- package/README.en.md +270 -0
- package/README.md +142 -279
- package/assets/model-icons/ai2-color.svg +1 -0
- package/assets/model-icons/ai21-brand-color.svg +1 -0
- package/assets/model-icons/aionlabs.svg +1 -0
- package/assets/model-icons/alephalpha.svg +1 -0
- package/assets/model-icons/baai.svg +1 -0
- package/assets/model-icons/baichuan-color.svg +1 -0
- package/assets/model-icons/baidu-color.svg +1 -0
- package/assets/model-icons/bytedance-color.svg +1 -0
- package/assets/model-icons/cohere-color.svg +1 -0
- package/assets/model-icons/hunyuan-color.svg +1 -0
- package/assets/model-icons/ibm.svg +1 -0
- package/assets/model-icons/iflytekcloud-color.svg +1 -0
- package/assets/model-icons/internlm-color.svg +1 -0
- package/assets/model-icons/kling-color.svg +1 -0
- package/assets/model-icons/liquid.svg +1 -0
- package/assets/model-icons/longcat-color.svg +1 -0
- package/assets/model-icons/manifest.json +739 -213
- package/assets/model-icons/microsoft-color.svg +1 -0
- package/assets/model-icons/mistral-color.svg +1 -0
- package/assets/model-icons/nousresearch.svg +1 -0
- package/assets/model-icons/nova-color.svg +1 -0
- package/assets/model-icons/nvidia-color.svg +1 -0
- package/assets/model-icons/perplexity-color.svg +1 -0
- package/assets/model-icons/sensenova-color.svg +1 -0
- package/assets/model-icons/skywork-color.svg +1 -0
- package/assets/model-icons/snowflake-color.svg +1 -0
- package/assets/model-icons/stepfun-color.svg +1 -0
- package/assets/model-icons/tencent-color.svg +1 -0
- package/assets/model-icons/tii-color.svg +1 -0
- package/assets/model-icons/upstage-color.svg +1 -0
- package/assets/model-icons/voyage-color.svg +1 -0
- package/assets/model-icons/xiaomimimo.svg +1 -0
- package/assets/model-icons/yandex.svg +1 -0
- package/assets/readme-architecture-en.svg +95 -0
- package/assets/readme-architecture-zh.svg +95 -0
- package/assets/readme-sync-en.svg +81 -0
- package/assets/readme-sync-zh.svg +81 -0
- package/assets/screenshots/en/01-overview.png +0 -0
- package/assets/screenshots/en/02-heatmap.png +0 -0
- package/assets/screenshots/en/03-model-usage.png +0 -0
- package/assets/screenshots/en/04-request-logs.png +0 -0
- package/assets/screenshots/en/05-cost-settings.png +0 -0
- package/assets/screenshots/zh/01-overview.png +0 -0
- package/assets/screenshots/zh/02-heatmap.png +0 -0
- package/assets/screenshots/zh/03-model-usage.png +0 -0
- package/assets/screenshots/zh/04-request-logs.png +0 -0
- package/assets/screenshots/zh/05-cost-settings.png +0 -0
- package/cordis.patch.yml +5 -5
- package/lib/aggregation.js +1842 -1112
- package/lib/client.js +1 -1
- package/lib/http.js +417 -385
- package/lib/icons.js +40 -0
- package/lib/ledger.js +1000 -531
- package/lib/plugin.js +347 -327
- package/lib/pricing-runtime.js +615 -551
- package/lib/pricing.js +1463 -1412
- package/lib/records-cache.js +113 -0
- package/lib/session-sync.js +976 -841
- package/lib/usage-core.js +33 -10
- package/lib/version-check.js +133 -133
- package/package.json +7 -3
- package/screenshots.json +10 -3
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
package/lib/plugin.js
CHANGED
|
@@ -1,327 +1,347 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
-
import { createAggregation } from './aggregation.js'
|
|
4
|
-
import { createBalance } from './balance.js'
|
|
5
|
-
import { registerRoutes } from './http.js'
|
|
6
|
-
import { RETIRED_WORKSPACE_ID, createLedger } from './ledger.js'
|
|
7
|
-
import { createPricingRuntime } from './pricing-runtime.js'
|
|
8
|
-
import { createSessionSync } from './session-sync.js'
|
|
9
|
-
import { createCostAccumulator, createEmptyPricingState } from './pricing.js'
|
|
10
|
-
import { createVersionCheck } from './version-check.js'
|
|
11
|
-
|
|
12
|
-
const name = 'dsh-all-usage'
|
|
13
|
-
// The page is served from this package, so a version mismatch between the
|
|
14
|
-
// running host and the page means DSH is still executing an older plugin (it
|
|
15
|
-
// only imports at startup). Publishing it lets the panel say so instead of
|
|
16
|
-
// leaving the user to guess why a new panel talks to an old payload.
|
|
17
|
-
const pluginVersion = (() => {
|
|
18
|
-
try {
|
|
19
|
-
const meta = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
20
|
-
return typeof meta.version === 'string' ? meta.version : ''
|
|
21
|
-
} catch (error) {
|
|
22
|
-
return ''
|
|
23
|
-
}
|
|
24
|
-
})()
|
|
25
|
-
const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'sessionPersistence', 'storage', 'webServer']
|
|
26
|
-
|
|
27
|
-
function apply(ctx) {
|
|
28
|
-
const services = {
|
|
29
|
-
credentials: ctx.get('credentials'),
|
|
30
|
-
settings: ctx.get('settings'),
|
|
31
|
-
storage: ctx.get('storage'),
|
|
32
|
-
webServer: ctx.get('webServer'),
|
|
33
|
-
sessionPersistence: ctx.get('sessionPersistence'),
|
|
34
|
-
}
|
|
35
|
-
const requestToken = randomBytes(32).toString('base64url')
|
|
36
|
-
const state = {
|
|
37
|
-
wsMeta: new Map(),
|
|
38
|
-
// True once recorded usage from a vanished workspace was moved into the
|
|
39
|
-
// shared "deleted" bucket; the bucket row is only advertised when it holds
|
|
40
|
-
// something.
|
|
41
|
-
retiredBucketUsed: false,
|
|
42
|
-
// Last client-side environment report (diagnostics only): the desktop host
|
|
43
|
-
// cannot be inspected from outside the app.
|
|
44
|
-
clientEnv: null,
|
|
45
|
-
pathIndex: new Map(),
|
|
46
|
-
memberOf: new Map(),
|
|
47
|
-
byDay: new Map(),
|
|
48
|
-
byDayUtc: new Map(),
|
|
49
|
-
perWorkspace: new Map(),
|
|
50
|
-
perModel: new Map(),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
state.
|
|
169
|
-
if (
|
|
170
|
-
if (
|
|
171
|
-
state.
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
state.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
state.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
state.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
state.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { createAggregation } from './aggregation.js'
|
|
4
|
+
import { createBalance } from './balance.js'
|
|
5
|
+
import { registerRoutes } from './http.js'
|
|
6
|
+
import { RETIRED_WORKSPACE_ID, createLedger } from './ledger.js'
|
|
7
|
+
import { createPricingRuntime } from './pricing-runtime.js'
|
|
8
|
+
import { createSessionSync } from './session-sync.js'
|
|
9
|
+
import { createCostAccumulator, createEmptyPricingState } from './pricing.js'
|
|
10
|
+
import { createVersionCheck } from './version-check.js'
|
|
11
|
+
|
|
12
|
+
const name = 'dsh-all-usage'
|
|
13
|
+
// The page is served from this package, so a version mismatch between the
|
|
14
|
+
// running host and the page means DSH is still executing an older plugin (it
|
|
15
|
+
// only imports at startup). Publishing it lets the panel say so instead of
|
|
16
|
+
// leaving the user to guess why a new panel talks to an old payload.
|
|
17
|
+
const pluginVersion = (() => {
|
|
18
|
+
try {
|
|
19
|
+
const meta = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
20
|
+
return typeof meta.version === 'string' ? meta.version : ''
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
})()
|
|
25
|
+
const inject = ['sessionQuery', 'workspaceRegistry', 'timer', 'sessionPersistence', 'storage', 'webServer']
|
|
26
|
+
|
|
27
|
+
function apply(ctx) {
|
|
28
|
+
const services = {
|
|
29
|
+
credentials: ctx.get('credentials'),
|
|
30
|
+
settings: ctx.get('settings'),
|
|
31
|
+
storage: ctx.get('storage'),
|
|
32
|
+
webServer: ctx.get('webServer'),
|
|
33
|
+
sessionPersistence: ctx.get('sessionPersistence'),
|
|
34
|
+
}
|
|
35
|
+
const requestToken = randomBytes(32).toString('base64url')
|
|
36
|
+
const state = {
|
|
37
|
+
wsMeta: new Map(),
|
|
38
|
+
// True once recorded usage from a vanished workspace was moved into the
|
|
39
|
+
// shared "deleted" bucket; the bucket row is only advertised when it holds
|
|
40
|
+
// something.
|
|
41
|
+
retiredBucketUsed: false,
|
|
42
|
+
// Last client-side environment report (diagnostics only): the desktop host
|
|
43
|
+
// cannot be inspected from outside the app.
|
|
44
|
+
clientEnv: null,
|
|
45
|
+
pathIndex: new Map(),
|
|
46
|
+
memberOf: new Map(),
|
|
47
|
+
byDay: new Map(),
|
|
48
|
+
byDayUtc: new Map(),
|
|
49
|
+
perWorkspace: new Map(),
|
|
50
|
+
perModel: new Map(),
|
|
51
|
+
// One digest per session (day/workspace/model buckets) so a session can be
|
|
52
|
+
// unwound from the cubes without keeping its detail rows in memory.
|
|
53
|
+
perSession: new Map(),
|
|
54
|
+
usageByStep: new Map(),
|
|
55
|
+
turnRecords: new Map(),
|
|
56
|
+
usageByLocalDate: new Map(),
|
|
57
|
+
usageByUtcDate: new Map(),
|
|
58
|
+
sessionModel: new Map(),
|
|
59
|
+
sessionContextTimes: new Map(),
|
|
60
|
+
totals: { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: createCostAccumulator() },
|
|
61
|
+
sessionCount: new Set(),
|
|
62
|
+
sessionSeq: new Map(),
|
|
63
|
+
chains: new Map(),
|
|
64
|
+
liveResyncPending: new Set(),
|
|
65
|
+
liveResyncTriggers: new Map(),
|
|
66
|
+
liveResyncTimers: new Map(),
|
|
67
|
+
liveResyncAttempts: new Map(),
|
|
68
|
+
scan: { started: false, done: false, scanned: 0, total: 0, failed: 0 },
|
|
69
|
+
aliases: {},
|
|
70
|
+
kvUnit: null,
|
|
71
|
+
aliasWriteChain: Promise.resolve(),
|
|
72
|
+
aliasesReady: Promise.resolve(),
|
|
73
|
+
balanceCache: { fetchedAt: 0, payload: null },
|
|
74
|
+
requestToken,
|
|
75
|
+
// The status poll must be able to notice that the capability rotated without
|
|
76
|
+
// being handed the capability itself: this id is derived from the token, so it
|
|
77
|
+
// changes exactly when the token does and reveals nothing about it.
|
|
78
|
+
capabilityId: createHash('sha256').update(requestToken).digest('base64url').slice(0, 16),
|
|
79
|
+
instanceId: randomBytes(12).toString('base64url'),
|
|
80
|
+
pluginVersion,
|
|
81
|
+
statsRevision: 0,
|
|
82
|
+
dataRevision: 0,
|
|
83
|
+
metadataRevision: 0,
|
|
84
|
+
scanRevision: 0,
|
|
85
|
+
pricingRevision: 0,
|
|
86
|
+
statsUpdatedAt: Date.now(),
|
|
87
|
+
statsDirtyScheduled: false,
|
|
88
|
+
statsDirtyKinds: new Set(),
|
|
89
|
+
sync: {
|
|
90
|
+
lastStartedAt: 0,
|
|
91
|
+
lastCompletedAt: 0,
|
|
92
|
+
lastErrorAt: 0,
|
|
93
|
+
lastErrorCode: null,
|
|
94
|
+
persistenceSnapshotsAvailable: false,
|
|
95
|
+
sessionsTotal: 0,
|
|
96
|
+
sessionsRead: 0,
|
|
97
|
+
sessionsSkippedByRevision: 0,
|
|
98
|
+
sessionsRestoredFromLedger: 0,
|
|
99
|
+
sessionsFailed: 0,
|
|
100
|
+
// Cumulative registry-probe diagnostics. They deliberately survive a
|
|
101
|
+
// baseline reset so the status endpoint can show whether the
|
|
102
|
+
// `domain/changed` fast path fires at all on this host.
|
|
103
|
+
workspaceProbeEvents: 0,
|
|
104
|
+
workspaceSyncRuns: 0,
|
|
105
|
+
workspaceSyncAdded: 0,
|
|
106
|
+
workspaceSyncRemoved: 0,
|
|
107
|
+
workspaceSyncDeferred: 0,
|
|
108
|
+
},
|
|
109
|
+
ledgerRecords: new Map(),
|
|
110
|
+
// The shard filenames are the index; a record is read (and cached, LRU by size)
|
|
111
|
+
// only when something asks for it, so the whole ledger is never resident.
|
|
112
|
+
ledgerKeys: new Set(),
|
|
113
|
+
// date -> { sessionId -> rows on that date }: how /records finds the sessions a
|
|
114
|
+
// range needs without the detail rows staying resident.
|
|
115
|
+
sessionsByLocalDate: new Map(),
|
|
116
|
+
sessionsByUtcDate: new Map(),
|
|
117
|
+
rowTouch: new Map(),
|
|
118
|
+
ledgerRecordSizes: new Map(),
|
|
119
|
+
ledgerRecordBytes: 0,
|
|
120
|
+
queryCache: new Map(),
|
|
121
|
+
recordsQueryCache: new Map(),
|
|
122
|
+
snapshotCache: null,
|
|
123
|
+
ledgerRevision: Date.now(),
|
|
124
|
+
ledgerUnit: null,
|
|
125
|
+
ledgerReady: Promise.resolve(),
|
|
126
|
+
ledgerWriteChain: Promise.resolve(),
|
|
127
|
+
ledgerPending: new Map(),
|
|
128
|
+
ledgerWriting: new Set(),
|
|
129
|
+
ledgerWriteWaiters: new Map(),
|
|
130
|
+
ledgerWriteRunning: false,
|
|
131
|
+
ledgerWriteScheduled: false,
|
|
132
|
+
ledgerWriteTimer: null,
|
|
133
|
+
ledgerDirtySessions: new Set(),
|
|
134
|
+
ledgerDirtyEpochs: new Map(),
|
|
135
|
+
ledgerWriteFailedSessions: new Set(),
|
|
136
|
+
pricingState: createEmptyPricingState(),
|
|
137
|
+
pricingResolutionCache: new Map(),
|
|
138
|
+
holidayCache: new Map(),
|
|
139
|
+
pricingUnit: null,
|
|
140
|
+
pricingReady: Promise.resolve(),
|
|
141
|
+
pricingWriteChain: Promise.resolve(),
|
|
142
|
+
pricingSyncInFlight: false,
|
|
143
|
+
pricingSyncTimer: null,
|
|
144
|
+
aggregationGeneration: 0,
|
|
145
|
+
disposed: false,
|
|
146
|
+
baselineRetryDelay: 1000,
|
|
147
|
+
baselineRetryScheduled: false,
|
|
148
|
+
baselineFallbackTimer: null,
|
|
149
|
+
knownSessionIds: new Set(),
|
|
150
|
+
reconcileHintScheduled: false,
|
|
151
|
+
reconcilePending: false,
|
|
152
|
+
reconcileInFlight: false,
|
|
153
|
+
reconcileTimer: null,
|
|
154
|
+
workspaceSyncTimer: null,
|
|
155
|
+
workspaceSyncDeferred: false,
|
|
156
|
+
workspaceProbeTimer: null,
|
|
157
|
+
workspaceSyncInFlight: false,
|
|
158
|
+
workspaceSyncPending: false,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const host = { ctx, services, state, webServer: services.webServer, aggregation: null, ledger: null, pricing: null, balance: null, sessionSync: null, aliases: null }
|
|
162
|
+
function commitPendingStats() {
|
|
163
|
+
if (!state.statsDirtyScheduled) return
|
|
164
|
+
state.statsDirtyScheduled = false
|
|
165
|
+
const kinds = state.statsDirtyKinds
|
|
166
|
+
state.statsDirtyKinds = new Set()
|
|
167
|
+
if (state.disposed) return
|
|
168
|
+
state.statsRevision += 1
|
|
169
|
+
if (kinds.has('data')) state.dataRevision += 1
|
|
170
|
+
if (kinds.has('metadata')) state.metadataRevision += 1
|
|
171
|
+
if (kinds.has('scan')) state.scanRevision += 1
|
|
172
|
+
if (kinds.has('pricing')) state.pricingRevision += 1
|
|
173
|
+
state.statsUpdatedAt = Date.now()
|
|
174
|
+
}
|
|
175
|
+
function markStatsChanged(kind = 'data') {
|
|
176
|
+
const kinds = Array.isArray(kind) ? kind : [kind]
|
|
177
|
+
let recordsChanged = false
|
|
178
|
+
for (const value of kinds) {
|
|
179
|
+
if (value !== 'data' && value !== 'metadata' && value !== 'scan' && value !== 'pricing') continue
|
|
180
|
+
state.statsDirtyKinds.add(value)
|
|
181
|
+
if (value === 'data' || value === 'pricing') recordsChanged = true
|
|
182
|
+
}
|
|
183
|
+
state.snapshotCache = null
|
|
184
|
+
// Scope results are keyed by the revision, so a superseded entry can never be
|
|
185
|
+
// read again — dropping it here keeps the cache at "live revisions only"
|
|
186
|
+
// instead of letting old 30-115 KB results sit until the size cap evicts them.
|
|
187
|
+
if (recordsChanged) state.queryCache.clear()
|
|
188
|
+
if (recordsChanged) state.recordsQueryCache.clear()
|
|
189
|
+
if (state.disposed || state.statsDirtyScheduled) return
|
|
190
|
+
state.statsDirtyScheduled = true
|
|
191
|
+
if (typeof queueMicrotask === 'function') queueMicrotask(commitPendingStats)
|
|
192
|
+
else Promise.resolve().then(commitPendingStats)
|
|
193
|
+
}
|
|
194
|
+
function resetSyncState() {
|
|
195
|
+
state.sync.lastStartedAt = 0
|
|
196
|
+
state.sync.lastCompletedAt = 0
|
|
197
|
+
state.sync.lastErrorAt = 0
|
|
198
|
+
state.sync.lastErrorCode = null
|
|
199
|
+
state.sync.persistenceSnapshotsAvailable = false
|
|
200
|
+
state.sync.sessionsTotal = 0
|
|
201
|
+
state.sync.sessionsRead = 0
|
|
202
|
+
state.sync.sessionsSkippedByRevision = 0
|
|
203
|
+
state.sync.sessionsRestoredFromLedger = 0
|
|
204
|
+
state.sync.sessionsFailed = 0
|
|
205
|
+
}
|
|
206
|
+
function beginSync() {
|
|
207
|
+
resetSyncState()
|
|
208
|
+
state.sync.lastStartedAt = Date.now()
|
|
209
|
+
markStatsChanged('scan')
|
|
210
|
+
}
|
|
211
|
+
function noteSyncError(code) {
|
|
212
|
+
state.sync.lastErrorAt = Date.now()
|
|
213
|
+
state.sync.lastErrorCode = code
|
|
214
|
+
markStatsChanged('scan')
|
|
215
|
+
}
|
|
216
|
+
function syncSnapshot() {
|
|
217
|
+
return {
|
|
218
|
+
lastStartedAt: state.sync.lastStartedAt,
|
|
219
|
+
lastCompletedAt: state.sync.lastCompletedAt,
|
|
220
|
+
lastErrorAt: state.sync.lastErrorAt,
|
|
221
|
+
lastErrorCode: state.sync.lastErrorCode,
|
|
222
|
+
persistenceSnapshotsAvailable: state.sync.persistenceSnapshotsAvailable,
|
|
223
|
+
sessionsTotal: state.sync.sessionsTotal,
|
|
224
|
+
sessionsRead: state.sync.sessionsRead,
|
|
225
|
+
sessionsSkippedByRevision: state.sync.sessionsSkippedByRevision,
|
|
226
|
+
sessionsRestoredFromLedger: state.sync.sessionsRestoredFromLedger,
|
|
227
|
+
sessionsFailed: state.sync.sessionsFailed,
|
|
228
|
+
workspaceProbeEvents: state.sync.workspaceProbeEvents,
|
|
229
|
+
workspaceSyncRuns: state.sync.workspaceSyncRuns,
|
|
230
|
+
workspaceSyncAdded: state.sync.workspaceSyncAdded,
|
|
231
|
+
workspaceSyncRemoved: state.sync.workspaceSyncRemoved,
|
|
232
|
+
workspaceSyncDeferred: state.sync.workspaceSyncDeferred,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
Object.assign(host, { commitPendingStats, markStatsChanged, resetSyncState, beginSync, noteSyncError, syncSnapshot })
|
|
236
|
+
|
|
237
|
+
host.pricingSnapshot = (...args) => host.pricing.pricingSnapshot(...args)
|
|
238
|
+
host.syncSnapshot = (...args) => syncSnapshot(...args)
|
|
239
|
+
host.aggregation = createAggregation(host)
|
|
240
|
+
host.ledger = createLedger(host)
|
|
241
|
+
host.pricing = createPricingRuntime(host)
|
|
242
|
+
host.balance = createBalance(host)
|
|
243
|
+
host.sessionSync = createSessionSync(host)
|
|
244
|
+
// The header asks the host for this, so the page never talks to the registry
|
|
245
|
+
// itself (the desktop shell would not allow it) and the answer is cached here.
|
|
246
|
+
host.versionCheck = createVersionCheck({ currentVersion: pluginVersion })
|
|
247
|
+
|
|
248
|
+
async function loadAliases() {
|
|
249
|
+
const storage = services.storage
|
|
250
|
+
if (storage === undefined || storage === null) return
|
|
251
|
+
try {
|
|
252
|
+
const backend = storage.backend.get('json')
|
|
253
|
+
if (backend === undefined || backend === null || backend.kv === undefined) return
|
|
254
|
+
const unit = await backend.kv.open({ name: 'all_usage_aliases', version: 0, tables: [], hasGlobal: true })
|
|
255
|
+
if (state.disposed) { await unit.close().catch(() => {}); return }
|
|
256
|
+
state.kvUnit = unit
|
|
257
|
+
const snap = await unit.loadAll()
|
|
258
|
+
const global = snap && snap.global
|
|
259
|
+
if (global !== null && global !== undefined && typeof global === 'object') {
|
|
260
|
+
let changed = false
|
|
261
|
+
for (const key of Object.keys(global)) {
|
|
262
|
+
const value = global[key]
|
|
263
|
+
const next = typeof value === 'string' ? value.trim() : ''
|
|
264
|
+
if (next !== '' && state.aliases[key] !== next) {
|
|
265
|
+
state.aliases[key] = next
|
|
266
|
+
changed = true
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (changed) markStatsChanged('metadata')
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error('[all-usage] alias storage unavailable:', err)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function persistAliases() {
|
|
276
|
+
if (state.disposed) return state.aliasWriteChain
|
|
277
|
+
const snapshotAliases = {}
|
|
278
|
+
for (const key of Object.keys(state.aliases)) snapshotAliases[key] = state.aliases[key]
|
|
279
|
+
state.aliasWriteChain = state.aliasWriteChain.then(() => {
|
|
280
|
+
if (state.kvUnit === null || state.kvUnit === undefined) return undefined
|
|
281
|
+
return state.kvUnit.setGlobal(snapshotAliases).catch((err) => {
|
|
282
|
+
console.error('[all-usage] alias persist failed:', err)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
return state.aliasWriteChain
|
|
286
|
+
}
|
|
287
|
+
function setAlias(wsId, raw) {
|
|
288
|
+
if (typeof wsId !== 'string' || wsId.length === 0 || wsId.length > 256) return { ok: false, message: 'invalid-workspace', aliases: Object.assign({}, state.aliases) }
|
|
289
|
+
if (typeof raw !== 'string') return { ok: false, message: 'invalid-alias', aliases: Object.assign({}, state.aliases) }
|
|
290
|
+
const alias = raw.trim().slice(0, 80)
|
|
291
|
+
if (!state.wsMeta.has(wsId) && !(state.retiredBucketUsed && wsId === RETIRED_WORKSPACE_ID)) return { ok: false, message: 'unknown-workspace', aliases: Object.assign({}, state.aliases) }
|
|
292
|
+
const previous = typeof state.aliases[wsId] === 'string' ? state.aliases[wsId] : ''
|
|
293
|
+
if (alias === previous) return { ok: true, aliases: Object.assign({}, state.aliases) }
|
|
294
|
+
if (alias === '') delete state.aliases[wsId]
|
|
295
|
+
else state.aliases[wsId] = alias
|
|
296
|
+
persistAliases()
|
|
297
|
+
markStatsChanged('metadata')
|
|
298
|
+
return { ok: true, aliases: Object.assign({}, state.aliases) }
|
|
299
|
+
}
|
|
300
|
+
host.aliases = { loadAliases, persistAliases, setAlias }
|
|
301
|
+
|
|
302
|
+
ctx.effect(() => async () => {
|
|
303
|
+
state.disposed = true
|
|
304
|
+
if (state.reconcileTimer !== null) {
|
|
305
|
+
clearTimeout(state.reconcileTimer)
|
|
306
|
+
state.reconcileTimer = null
|
|
307
|
+
}
|
|
308
|
+
if (state.baselineFallbackTimer !== null) {
|
|
309
|
+
clearTimeout(state.baselineFallbackTimer)
|
|
310
|
+
state.baselineFallbackTimer = null
|
|
311
|
+
}
|
|
312
|
+
if (state.pricingSyncTimer !== null) {
|
|
313
|
+
clearTimeout(state.pricingSyncTimer)
|
|
314
|
+
state.pricingSyncTimer = null
|
|
315
|
+
}
|
|
316
|
+
for (const timer of state.liveResyncTimers.values()) clearTimeout(timer)
|
|
317
|
+
state.liveResyncTimers.clear()
|
|
318
|
+
state.liveResyncAttempts.clear()
|
|
319
|
+
state.liveResyncPending.clear()
|
|
320
|
+
state.liveResyncTriggers.clear()
|
|
321
|
+
state.baselineRetryScheduled = false
|
|
322
|
+
if (state.ledgerWriteTimer !== null) {
|
|
323
|
+
clearTimeout(state.ledgerWriteTimer)
|
|
324
|
+
state.ledgerWriteTimer = null
|
|
325
|
+
}
|
|
326
|
+
state.ledgerWriteScheduled = false
|
|
327
|
+
state.chains.clear()
|
|
328
|
+
await Promise.all([state.aliasesReady, state.ledgerReady, state.pricingReady, state.aliasWriteChain, state.pricingWriteChain])
|
|
329
|
+
await host.ledger.drainLedgerWrites()
|
|
330
|
+
const units = [state.kvUnit, state.ledgerUnit, state.pricingUnit]
|
|
331
|
+
state.kvUnit = null
|
|
332
|
+
state.ledgerUnit = null
|
|
333
|
+
state.pricingUnit = null
|
|
334
|
+
await Promise.all(units.map((unit) => unit === null || unit === undefined ? undefined : unit.close().catch(() => {})))
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
registerRoutes(host)
|
|
338
|
+
state.ledgerReady = host.ledger.loadLedger()
|
|
339
|
+
state.pricingReady = host.pricing.loadPricing().then(() => { host.pricing.schedulePricingSync() })
|
|
340
|
+
void host.sessionSync.runBaseline()
|
|
341
|
+
host.sessionSync.scheduleReconcileTimer()
|
|
342
|
+
host.sessionSync.scheduleWorkspaceProbeTimer()
|
|
343
|
+
state.aliasesReady = loadAliases()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export { name, inject, apply }
|
|
347
|
+
export default { name, inject, apply }
|