dsh-all-usage 1.1.9 → 1.1.12
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 +38 -0
- package/README.md +16 -38
- package/lib/aggregation.js +1 -1
- package/lib/client.js +1 -1
- package/lib/http.js +32 -0
- package/lib/plugin.js +19 -0
- package/lib/session-sync.js +70 -34
- package/package.json +1 -1
package/lib/http.js
CHANGED
|
@@ -249,6 +249,38 @@ export function registerRoutes(host) {
|
|
|
249
249
|
sendJson(res, 200, statusSnapshot())
|
|
250
250
|
},
|
|
251
251
|
}))
|
|
252
|
+
ctx.effect(() => webServer.register({
|
|
253
|
+
kind: 'exact',
|
|
254
|
+
path: '/api/all-usage/client-env',
|
|
255
|
+
handler: (req, res) => {
|
|
256
|
+
if (req.method !== 'GET') { res.statusCode = 405; res.end(); return }
|
|
257
|
+
if (!isTrustedLocalApiRequest(req, false)) { rejectRequest(res); return }
|
|
258
|
+
// Diagnostics only: the client reports what the host exposes (user agent,
|
|
259
|
+
// window-controls-overlay geometry, the strip it reserved). Truncated and
|
|
260
|
+
// kept in memory for the life of the process.
|
|
261
|
+
try {
|
|
262
|
+
const url = new URL(req.url ?? '/', 'http://x')
|
|
263
|
+
const read = (name, limit) => {
|
|
264
|
+
const value = url.searchParams.get(name)
|
|
265
|
+
return value === null ? null : value.slice(0, limit)
|
|
266
|
+
}
|
|
267
|
+
state.clientEnv = {
|
|
268
|
+
reportedAt: Date.now(),
|
|
269
|
+
ua: read('ua', 200),
|
|
270
|
+
wco: read('wco', 40),
|
|
271
|
+
inset: read('inset', 12),
|
|
272
|
+
insetSource: read('src', 24),
|
|
273
|
+
titlebar: read('tb', 24),
|
|
274
|
+
viewport: read('vp', 24),
|
|
275
|
+
modalRect: read('modal', 40),
|
|
276
|
+
dialogRect: read('dlg', 40),
|
|
277
|
+
headRect: read('head', 40),
|
|
278
|
+
closeRect: read('close', 40),
|
|
279
|
+
}
|
|
280
|
+
} catch (err) { /* ignore a malformed diagnostic */ }
|
|
281
|
+
sendJson(res, 200, { ok: true })
|
|
282
|
+
},
|
|
283
|
+
}))
|
|
252
284
|
ctx.effect(() => webServer.register({
|
|
253
285
|
kind: 'exact',
|
|
254
286
|
path: '/api/all-usage',
|
package/lib/plugin.js
CHANGED
|
@@ -24,6 +24,9 @@ function apply(ctx) {
|
|
|
24
24
|
// shared "deleted" bucket; the bucket row is only advertised when it holds
|
|
25
25
|
// something.
|
|
26
26
|
retiredBucketUsed: false,
|
|
27
|
+
// Last client-side environment report (diagnostics only): the desktop host
|
|
28
|
+
// cannot be inspected from outside the app.
|
|
29
|
+
clientEnv: null,
|
|
27
30
|
pathIndex: new Map(),
|
|
28
31
|
memberOf: new Map(),
|
|
29
32
|
byDay: new Map(),
|
|
@@ -70,6 +73,14 @@ function apply(ctx) {
|
|
|
70
73
|
sessionsSkippedByRevision: 0,
|
|
71
74
|
sessionsRestoredFromLedger: 0,
|
|
72
75
|
sessionsFailed: 0,
|
|
76
|
+
// Cumulative registry-probe diagnostics. They deliberately survive a
|
|
77
|
+
// baseline reset so the status endpoint can show whether the
|
|
78
|
+
// `domain/changed` fast path fires at all on this host.
|
|
79
|
+
workspaceProbeEvents: 0,
|
|
80
|
+
workspaceSyncRuns: 0,
|
|
81
|
+
workspaceSyncAdded: 0,
|
|
82
|
+
workspaceSyncRemoved: 0,
|
|
83
|
+
workspaceSyncDeferred: 0,
|
|
73
84
|
},
|
|
74
85
|
ledgerRecords: new Map(),
|
|
75
86
|
queryCache: new Map(),
|
|
@@ -105,6 +116,8 @@ function apply(ctx) {
|
|
|
105
116
|
reconcileInFlight: false,
|
|
106
117
|
reconcileTimer: null,
|
|
107
118
|
workspaceSyncTimer: null,
|
|
119
|
+
workspaceSyncDeferred: false,
|
|
120
|
+
workspaceProbeTimer: null,
|
|
108
121
|
workspaceSyncInFlight: false,
|
|
109
122
|
workspaceSyncPending: false,
|
|
110
123
|
}
|
|
@@ -172,6 +185,11 @@ function apply(ctx) {
|
|
|
172
185
|
sessionsSkippedByRevision: state.sync.sessionsSkippedByRevision,
|
|
173
186
|
sessionsRestoredFromLedger: state.sync.sessionsRestoredFromLedger,
|
|
174
187
|
sessionsFailed: state.sync.sessionsFailed,
|
|
188
|
+
workspaceProbeEvents: state.sync.workspaceProbeEvents,
|
|
189
|
+
workspaceSyncRuns: state.sync.workspaceSyncRuns,
|
|
190
|
+
workspaceSyncAdded: state.sync.workspaceSyncAdded,
|
|
191
|
+
workspaceSyncRemoved: state.sync.workspaceSyncRemoved,
|
|
192
|
+
workspaceSyncDeferred: state.sync.workspaceSyncDeferred,
|
|
175
193
|
}
|
|
176
194
|
}
|
|
177
195
|
Object.assign(host, { commitPendingStats, markStatsChanged, resetSyncState, beginSync, noteSyncError, syncSnapshot })
|
|
@@ -277,6 +295,7 @@ function apply(ctx) {
|
|
|
277
295
|
state.pricingReady = host.pricing.loadPricing().then(() => { host.pricing.schedulePricingSync() })
|
|
278
296
|
void host.sessionSync.runBaseline()
|
|
279
297
|
host.sessionSync.scheduleReconcileTimer()
|
|
298
|
+
host.sessionSync.scheduleWorkspaceProbeTimer()
|
|
280
299
|
state.aliasesReady = loadAliases()
|
|
281
300
|
}
|
|
282
301
|
|
package/lib/session-sync.js
CHANGED
|
@@ -4,6 +4,10 @@ import { contextTimeKey, extractUsageEvent, pickPricingTime, touchContextTimes,
|
|
|
4
4
|
const RECONCILE_INTERVAL_MS = 120000
|
|
5
5
|
const RECONCILE_HINT_DELAY_MS = 3000
|
|
6
6
|
const WORKSPACE_SYNC_DEBOUNCE_MS = 300
|
|
7
|
+
// The registry event is a fast path, not a guarantee: a change that lands while a
|
|
8
|
+
// baseline runs used to be dropped outright, and a missed event left the plugin
|
|
9
|
+
// stale until the next restart. The (in-memory) registry list is polled as well.
|
|
10
|
+
const WORKSPACE_PROBE_INTERVAL_MS = 30000
|
|
7
11
|
|
|
8
12
|
export function createSessionSync(host) {
|
|
9
13
|
const { ctx, state } = host
|
|
@@ -154,6 +158,25 @@ export function createSessionSync(host) {
|
|
|
154
158
|
if (typeof cwd !== 'string' || cwd === '') return undefined
|
|
155
159
|
return state.pathIndex.get(workspacePathKey(cwd))
|
|
156
160
|
}
|
|
161
|
+
// DSH 0.1.5 replaced \`sessionPersistence.listSnapshots()\` with \`list()\`; both
|
|
162
|
+
// return per-session \`{ header, revision }\` rows. Without a revision the
|
|
163
|
+
// ledger-reuse fast path never fires, so every baseline re-reads every log.
|
|
164
|
+
async function loadSessionRevisions() {
|
|
165
|
+
const source = sessionPersistence
|
|
166
|
+
if (source === undefined || source === null) return null
|
|
167
|
+
const read = typeof source.list === 'function'
|
|
168
|
+
? () => source.list()
|
|
169
|
+
: (typeof source.listSnapshots === 'function' ? () => source.listSnapshots() : null)
|
|
170
|
+
if (read === null) return null
|
|
171
|
+
const rows = await read()
|
|
172
|
+
if (!Array.isArray(rows)) return null
|
|
173
|
+
const revisions = new Map()
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
176
|
+
if (rid !== undefined && row && typeof row.revision === 'string') revisions.set(rid, row.revision)
|
|
177
|
+
}
|
|
178
|
+
return revisions
|
|
179
|
+
}
|
|
157
180
|
function wsForLiveSession(session, sid) {
|
|
158
181
|
let wsId = state.memberOf.get(sid)
|
|
159
182
|
if (wsId !== undefined) return wsId
|
|
@@ -406,24 +429,18 @@ export function createSessionSync(host) {
|
|
|
406
429
|
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
407
430
|
// v1.0.8: cheap per-session change signal (header line + stat, no full-log read)
|
|
408
431
|
let snapshots = null
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
snapshots = new Map()
|
|
416
|
-
for (const row of rows) {
|
|
417
|
-
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
418
|
-
if (rid !== undefined && row && typeof row.revision === 'string') snapshots.set(rid, row.revision)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
} catch (err) {
|
|
422
|
-
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
423
|
-
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
424
|
-
state.sync.persistenceSnapshotsAvailable = false
|
|
425
|
-
markStatsChanged('scan')
|
|
432
|
+
try {
|
|
433
|
+
const revisions = await loadSessionRevisions()
|
|
434
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
435
|
+
if (revisions !== null) {
|
|
436
|
+
state.sync.persistenceSnapshotsAvailable = true
|
|
437
|
+
snapshots = revisions
|
|
426
438
|
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
441
|
+
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
442
|
+
state.sync.persistenceSnapshotsAvailable = false
|
|
443
|
+
markStatsChanged('scan')
|
|
427
444
|
}
|
|
428
445
|
if (state.disposed || generation !== state.aggregationGeneration) return
|
|
429
446
|
if (setupFailed || !Array.isArray(records)) {
|
|
@@ -504,6 +521,10 @@ export function createSessionSync(host) {
|
|
|
504
521
|
state.knownSessionIds.clear()
|
|
505
522
|
for (const sid of listedSessionIds) state.knownSessionIds.add(sid)
|
|
506
523
|
state.scan.done = true
|
|
524
|
+
if (state.workspaceSyncDeferred) {
|
|
525
|
+
state.workspaceSyncDeferred = false
|
|
526
|
+
scheduleWorkspaceSync()
|
|
527
|
+
}
|
|
507
528
|
state.sync.lastCompletedAt = Date.now()
|
|
508
529
|
if (state.sync.sessionsFailed === 0) {
|
|
509
530
|
state.sync.lastErrorAt = 0
|
|
@@ -529,6 +550,7 @@ export function createSessionSync(host) {
|
|
|
529
550
|
return
|
|
530
551
|
}
|
|
531
552
|
state.workspaceSyncInFlight = true
|
|
553
|
+
state.sync.workspaceSyncRuns += 1
|
|
532
554
|
try {
|
|
533
555
|
await synchronizeWorkspaceRegistry()
|
|
534
556
|
} catch (err) {
|
|
@@ -543,9 +565,15 @@ export function createSessionSync(host) {
|
|
|
543
565
|
}
|
|
544
566
|
}
|
|
545
567
|
async function synchronizeWorkspaceRegistry() {
|
|
546
|
-
|
|
547
|
-
//
|
|
548
|
-
|
|
568
|
+
if (state.disposed) return
|
|
569
|
+
// A registry change that lands while a baseline is running used to be
|
|
570
|
+
// dropped outright, which is why a new workspace could stay invisible until
|
|
571
|
+
// the next restart. Remember it and re-run once the scan settles.
|
|
572
|
+
if (!state.scan.done) {
|
|
573
|
+
state.workspaceSyncDeferred = true
|
|
574
|
+
state.sync.workspaceSyncDeferred += 1
|
|
575
|
+
return
|
|
576
|
+
}
|
|
549
577
|
let workspaces
|
|
550
578
|
try {
|
|
551
579
|
workspaces = ctx.workspaceRegistry.list()
|
|
@@ -559,6 +587,8 @@ export function createSessionSync(host) {
|
|
|
559
587
|
for (const id of state.wsMeta.keys()) if (!next.wsMeta.has(id)) removedWs.add(id)
|
|
560
588
|
const addedWs = new Set()
|
|
561
589
|
for (const id of next.wsMeta.keys()) if (!state.wsMeta.has(id)) addedWs.add(id)
|
|
590
|
+
state.sync.workspaceSyncAdded += addedWs.size
|
|
591
|
+
state.sync.workspaceSyncRemoved += removedWs.size
|
|
562
592
|
// Reuse path: no membership change — metadata only, zero rescan.
|
|
563
593
|
if (removedWs.size === 0 && addedWs.size === 0) {
|
|
564
594
|
installWorkspaceSnapshot(next)
|
|
@@ -606,20 +636,10 @@ export function createSessionSync(host) {
|
|
|
606
636
|
state.sync.sessionsTotal = records.length
|
|
607
637
|
markStatsChanged('scan')
|
|
608
638
|
let snapshots = null
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
snapshots = new Map()
|
|
614
|
-
for (const row of rows) {
|
|
615
|
-
const rid = row && row.header && typeof row.header.id === 'string' ? row.header.id : undefined
|
|
616
|
-
if (rid !== undefined && row && typeof row.revision === 'string') snapshots.set(rid, row.revision)
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
} catch (err) {
|
|
620
|
-
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
621
|
-
markStatsChanged('scan')
|
|
622
|
-
}
|
|
639
|
+
try {
|
|
640
|
+
snapshots = await loadSessionRevisions()
|
|
641
|
+
} catch (err) {
|
|
642
|
+
console.error('[all-usage] session persistence snapshots unavailable:', err)
|
|
623
643
|
}
|
|
624
644
|
for (const record of records) {
|
|
625
645
|
if (state.disposed) return
|
|
@@ -698,6 +718,20 @@ export function createSessionSync(host) {
|
|
|
698
718
|
}, RECONCILE_INTERVAL_MS)
|
|
699
719
|
if (state.reconcileTimer && typeof state.reconcileTimer.unref === 'function') state.reconcileTimer.unref()
|
|
700
720
|
}
|
|
721
|
+
// Belt and braces for the registry probe: the event fast path can be missed
|
|
722
|
+
// (dropped while scanning, or never delivered), and unlike the event this
|
|
723
|
+
// cannot silently leave the workspace list stale until a restart.
|
|
724
|
+
function scheduleWorkspaceProbeTimer() {
|
|
725
|
+
if (state.disposed || state.workspaceProbeTimer !== null) return
|
|
726
|
+
state.workspaceProbeTimer = setTimeout(() => {
|
|
727
|
+
state.workspaceProbeTimer = null
|
|
728
|
+
if (!state.disposed) {
|
|
729
|
+
void runWorkspaceSync()
|
|
730
|
+
scheduleWorkspaceProbeTimer()
|
|
731
|
+
}
|
|
732
|
+
}, WORKSPACE_PROBE_INTERVAL_MS)
|
|
733
|
+
if (state.workspaceProbeTimer && typeof state.workspaceProbeTimer.unref === 'function') state.workspaceProbeTimer.unref()
|
|
734
|
+
}
|
|
701
735
|
|
|
702
736
|
// ---------- live feed ----------
|
|
703
737
|
ctx.on('session/event', (session, event) => {
|
|
@@ -775,6 +809,7 @@ export function createSessionSync(host) {
|
|
|
775
809
|
if (change === undefined || change === null) return
|
|
776
810
|
if (change.domain !== 'workspace') return
|
|
777
811
|
if (change.table !== 'workspaces' && change.table !== '') return
|
|
812
|
+
state.sync.workspaceProbeEvents += 1
|
|
778
813
|
scheduleWorkspaceSync()
|
|
779
814
|
})
|
|
780
815
|
|
|
@@ -800,6 +835,7 @@ export function createSessionSync(host) {
|
|
|
800
835
|
reconcileSessions,
|
|
801
836
|
scheduleReconcileHint,
|
|
802
837
|
scheduleReconcileTimer,
|
|
838
|
+
scheduleWorkspaceProbeTimer,
|
|
803
839
|
markLedgerDirty
|
|
804
840
|
}
|
|
805
841
|
}
|