dsh-recent-usage-metrics 0.2.0 → 0.2.2
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 +11 -0
- package/README.en.md +455 -412
- package/README.md +409 -363
- package/client/client.js +127 -59
- package/package.json +9 -1
package/client/client.js
CHANGED
|
@@ -2155,12 +2155,12 @@ window.__ModuleLoader__.load({
|
|
|
2155
2155
|
* subscribes. Reading it inline during render froze the rows at whatever the first
|
|
2156
2156
|
* render saw — which is why the drawer showed timestamps minutes behind the log.
|
|
2157
2157
|
*/
|
|
2158
|
-
/*
|
|
2159
|
-
* The shipped Statistics view's data source for this session. `undefined` when that
|
|
2160
|
-
* view is absent — a composition without it — which is what routes the drawer to its
|
|
2161
|
-
* Chat-snapshot fallback. No switch decides this: the presence of the source does.
|
|
2162
|
-
*/
|
|
2163
|
-
const statsSource = typeof props.statsSource === 'function' ? props.statsSource() : undefined
|
|
2158
|
+
/*
|
|
2159
|
+
* The shipped Statistics view's data source for this session. `undefined` when that
|
|
2160
|
+
* view is absent — a composition without it — which is what routes the drawer to its
|
|
2161
|
+
* Chat-snapshot fallback. No switch decides this: the presence of the source does.
|
|
2162
|
+
*/
|
|
2163
|
+
const statsSource = typeof props.statsSource === 'function' ? props.statsSource() : undefined
|
|
2164
2164
|
const statsSubscribe = useCallback(
|
|
2165
2165
|
(listener) => {
|
|
2166
2166
|
if (statsSource === undefined || typeof statsSource.subscribe !== 'function') {
|
|
@@ -2183,10 +2183,45 @@ window.__ModuleLoader__.load({
|
|
|
2183
2183
|
[statsSource, sessionId],
|
|
2184
2184
|
)
|
|
2185
2185
|
const [statsSnapshot, setStatsSnapshot] = useState(statsRead)
|
|
2186
|
+
/*
|
|
2187
|
+
* Re-point the subscription at whatever target is current, then re-read.
|
|
2188
|
+
*
|
|
2189
|
+
* A conversation that is rebuilt underneath the drawer (a model switch, a reconnect, a
|
|
2190
|
+
* released binding) replaces its `BoundConversation`, and with it the view store the old
|
|
2191
|
+
* `target()` closure captured. Refreshing on every render rebinds the listener to the new
|
|
2192
|
+
* target, so the drawer cannot keep rendering a retired snapshot while the Statistics page
|
|
2193
|
+
* has already moved on.
|
|
2194
|
+
*/
|
|
2195
|
+
if (statsSource !== undefined && typeof statsSource.refresh === 'function') {
|
|
2196
|
+
statsSource.refresh()
|
|
2197
|
+
}
|
|
2186
2198
|
useEffect(() => {
|
|
2187
2199
|
setStatsSnapshot(statsRead())
|
|
2188
2200
|
return statsSubscribe(() => setStatsSnapshot(statsRead()))
|
|
2189
2201
|
}, [statsRead, statsSubscribe])
|
|
2202
|
+
/*
|
|
2203
|
+
* Follow a conversation that is rebuilt underneath the drawer.
|
|
2204
|
+
*
|
|
2205
|
+
* A model switch, a reconnect, or a released binding replaces the session's
|
|
2206
|
+
* `BoundConversation`, and with it the view store the old `target()` closure captured.
|
|
2207
|
+
* That stale closure keeps returning its last snapshot forever — which is exactly how the
|
|
2208
|
+
* drawer froze on one turn while the Statistics page moved on.
|
|
2209
|
+
*
|
|
2210
|
+
* On a slow tick this re-points the listener at the target that is current now and
|
|
2211
|
+
* re-reads the snapshot. Both are cheap (a map lookup and a field read) and neither
|
|
2212
|
+
* depends on the stale chain firing.
|
|
2213
|
+
*/
|
|
2214
|
+
useEffect(() => {
|
|
2215
|
+
if (statsSource === undefined) return undefined
|
|
2216
|
+
const sync = () => {
|
|
2217
|
+
if (typeof statsSource.refresh === 'function') statsSource.refresh()
|
|
2218
|
+
const next = statsRead()
|
|
2219
|
+
if (next !== undefined) setStatsSnapshot(next)
|
|
2220
|
+
}
|
|
2221
|
+
sync()
|
|
2222
|
+
const timer = setInterval(sync, 1000)
|
|
2223
|
+
return () => clearInterval(timer)
|
|
2224
|
+
}, [statsSource, statsRead])
|
|
2190
2225
|
/*
|
|
2191
2226
|
* A snapshot only counts while it belongs to the session being shown.
|
|
2192
2227
|
*
|
|
@@ -2940,82 +2975,115 @@ window.__ModuleLoader__.load({
|
|
|
2940
2975
|
*/
|
|
2941
2976
|
const statsSources = new Map()
|
|
2942
2977
|
/*
|
|
2943
|
-
* The Statistics target for one session
|
|
2978
|
+
* The Statistics target for one session.
|
|
2979
|
+
*
|
|
2980
|
+
* The target object is looked up FRESH on every call rather than cached alongside the
|
|
2981
|
+
* source. `uiConversation.binding(id)` keeps a `BoundConversation` per session, and that
|
|
2982
|
+
* object owns the assembler and the view store; when the conversation is rebuilt — a model
|
|
2983
|
+
* switch, a reconnect, a released binding — the old `target()` closure keeps pointing at
|
|
2984
|
+
* the retired store and returns its last snapshot forever. Caching it froze the drawer on
|
|
2985
|
+
* one turn while the Statistics page moved on.
|
|
2944
2986
|
*
|
|
2945
|
-
*
|
|
2946
|
-
*
|
|
2947
|
-
* the evicted target's subscription: without that, a long session that visits many
|
|
2948
|
-
* conversations would keep one live subscription per conversation visited, and each of
|
|
2949
|
-
* those keeps the conversation's fold active.
|
|
2987
|
+
* Only the subscriptions are cached, because those must be released on eviction and
|
|
2988
|
+
* re-subscribing on every render would multiply listeners.
|
|
2950
2989
|
*/
|
|
2951
2990
|
const STATS_SOURCE_LIMIT = 4
|
|
2952
2991
|
const statsSourceFor = (sessionId) => {
|
|
2953
2992
|
if (sessionId === null || sessionId === undefined) return undefined
|
|
2954
|
-
|
|
2993
|
+
const uiConversation = ctx.get('uiConversation')
|
|
2994
|
+
if (uiConversation === undefined || uiConversation === null) return undefined
|
|
2995
|
+
|
|
2996
|
+
/** The target that is current right now, or undefined when it cannot be resolved. */
|
|
2997
|
+
const targetNow = () => {
|
|
2998
|
+
try {
|
|
2999
|
+
return uiConversation.binding(sessionId).target(STATS_TARGET) ?? undefined
|
|
3000
|
+
} catch {
|
|
3001
|
+
// The session was released (or is inactive) — no statistics for it right now.
|
|
3002
|
+
return undefined
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
const cached = statsSources.get(sessionId)
|
|
3007
|
+
if (cached !== undefined) {
|
|
2955
3008
|
// Refresh recency so the active conversation is the last one evicted.
|
|
2956
|
-
const existing = statsSources.get(sessionId)
|
|
2957
3009
|
statsSources.delete(sessionId)
|
|
2958
|
-
statsSources.set(sessionId,
|
|
2959
|
-
return
|
|
3010
|
+
statsSources.set(sessionId, cached)
|
|
3011
|
+
return cached.source
|
|
2960
3012
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
const
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
const dispose = () => {
|
|
2980
|
-
if (subscriptions.get(listener)?.unsubscribe !== unsubscribe) return
|
|
2981
|
-
subscriptions.delete(listener)
|
|
2982
|
-
try {
|
|
2983
|
-
unsubscribe()
|
|
2984
|
-
} catch {}
|
|
2985
|
-
}
|
|
2986
|
-
subscriptions.set(listener, { unsubscribe, dispose })
|
|
2987
|
-
return dispose
|
|
2988
|
-
},
|
|
2989
|
-
release: () => {
|
|
2990
|
-
for (const [listener, entry] of [...subscriptions]) {
|
|
2991
|
-
subscriptions.delete(listener)
|
|
2992
|
-
try {
|
|
2993
|
-
entry.unsubscribe()
|
|
2994
|
-
} catch {}
|
|
2995
|
-
}
|
|
2996
|
-
},
|
|
3013
|
+
|
|
3014
|
+
/*
|
|
3015
|
+
* Each listener is bound to the target that was current when it subscribed; a listener
|
|
3016
|
+
* whose target has since been replaced is re-bound the next time it asks, which is what
|
|
3017
|
+
* keeps a drawn row tracking the live fold instead of a retired one.
|
|
3018
|
+
*/
|
|
3019
|
+
const subscriptions = new Map()
|
|
3020
|
+
|
|
3021
|
+
const bind = (listener) => {
|
|
3022
|
+
const target = targetNow()
|
|
3023
|
+
if (target === undefined) return
|
|
3024
|
+
const entry = subscriptions.get(listener)
|
|
3025
|
+
if (entry !== undefined && entry.target === target) return
|
|
3026
|
+
if (entry !== undefined) {
|
|
3027
|
+
try {
|
|
3028
|
+
entry.unsubscribe()
|
|
3029
|
+
} catch {}
|
|
3030
|
+
subscriptions.delete(listener)
|
|
2997
3031
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3032
|
+
try {
|
|
3033
|
+
subscriptions.set(listener, { target, unsubscribe: target.subscribe(listener) })
|
|
3034
|
+
} catch {}
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
const dispose = (listener) => {
|
|
3038
|
+
const entry = subscriptions.get(listener)
|
|
3039
|
+
if (entry === undefined) return
|
|
3040
|
+
subscriptions.delete(listener)
|
|
3041
|
+
try {
|
|
3042
|
+
entry.unsubscribe()
|
|
3043
|
+
} catch {}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
const source = {
|
|
3047
|
+
/** Always the newest snapshot the session's own target holds. */
|
|
3048
|
+
getSnapshot: () => {
|
|
3049
|
+
const target = targetNow()
|
|
3050
|
+
return target === undefined ? undefined : target.getSnapshot()
|
|
3051
|
+
},
|
|
3052
|
+
subscribe: (listener) => {
|
|
3053
|
+
bind(listener)
|
|
3054
|
+
return () => dispose(listener)
|
|
3055
|
+
},
|
|
3056
|
+
/**
|
|
3057
|
+
* Re-point every listener at the target that is current now.
|
|
3058
|
+
*
|
|
3059
|
+
* Called on each render: one map lookup per listener, and it is what lets the drawer
|
|
3060
|
+
* follow a conversation that was rebuilt underneath it.
|
|
3061
|
+
*/
|
|
3062
|
+
refresh: () => {
|
|
3063
|
+
for (const listener of [...subscriptions.keys()]) bind(listener)
|
|
3064
|
+
},
|
|
3065
|
+
release: () => {
|
|
3066
|
+
for (const listener of [...subscriptions.keys()]) dispose(listener)
|
|
3067
|
+
},
|
|
3001
3068
|
}
|
|
3002
|
-
|
|
3069
|
+
|
|
3070
|
+
statsSources.set(sessionId, { source })
|
|
3003
3071
|
while (statsSources.size > STATS_SOURCE_LIMIT) {
|
|
3004
3072
|
const oldest = statsSources.keys().next()
|
|
3005
3073
|
if (oldest.done === true) break
|
|
3006
3074
|
const evicted = statsSources.get(oldest.value)
|
|
3007
3075
|
statsSources.delete(oldest.value)
|
|
3008
3076
|
try {
|
|
3009
|
-
evicted?.release?.()
|
|
3077
|
+
evicted?.source?.release?.()
|
|
3010
3078
|
} catch {}
|
|
3011
3079
|
}
|
|
3012
3080
|
return source
|
|
3013
3081
|
}
|
|
3014
3082
|
ctx.effect(
|
|
3015
3083
|
() => () => {
|
|
3016
|
-
for (const
|
|
3084
|
+
for (const entry of statsSources.values()) {
|
|
3017
3085
|
try {
|
|
3018
|
-
source.release?.()
|
|
3086
|
+
entry.source.release?.()
|
|
3019
3087
|
} catch {}
|
|
3020
3088
|
}
|
|
3021
3089
|
statsSources.clear()
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-recent-usage-metrics",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Geek level for the Performance & usage setting: a live reading of the newest request (first-token latency, speed, tokens, total, amortised) plus a drawer listing the last ten, all derived from the shipped Statistics view data",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/hansomgar/dsh-recent-usage-metrics.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/hansomgar/dsh-recent-usage-metrics#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/hansomgar/dsh-recent-usage-metrics/issues"
|
|
12
|
+
},
|
|
5
13
|
"type": "module",
|
|
6
14
|
"license": "MIT",
|
|
7
15
|
"main": "./lib/index.js",
|