Spaces:
Running
feat: optimize bucket data caching and document refresh behavior
Browse files- Add private five-minute browser caching for SvelteKit page-data responses:
- Ranking,
- Details,
- Visualizations.
- Set Cache-Control to private, max-age=300.
- Remove the stale-while-revalidate window so expired route data is refreshed before rendering.
- Support cache handling for both localized and non-localized __data.json routes.
- Add gzip compression for page-data responses larger than 1 KB.
- Respect the client Accept-Encoding header and gzip quality value.
- Set Content-Encoding and Vary headers for compressed responses.
- Preserve uncompressed responses for clients without gzip support.
- Reduce transferred payload sizes approximately:
- Ranking from 171 KB to 58 KB,
- Details from 643 KB to 158 KB,
- Visualizations from 709 KB to 122 KB.
- Refresh the small bucket manifest whenever a browser cache miss reaches the server.
- Add refreshManifest snapshot option to separate manifest checks from full payload refreshes.
- Reuse catalog, leaderboard, and adapted datasets when snapshot_id is unchanged.
- Download and rebuild payloads only after detecting a new snapshot_id.
- Remove independent Ranking and Details TTL checks that could compound cache freshness delays.
- Keep the expected freshness window tied to the five-minute browser cache.
- Key ranking snapshot in-flight requests by snapshot_id.
- Prevent a request for a newly published snapshot from joining an older snapshot download.
- Add a shared Details snapshot cache keyed by snapshot_id.
- Reuse details_matrix between Ranking partial-run detection and the Details page.
- Deduplicate concurrent Details snapshot downloads.
- Refresh the Tools manifest after a browser cache miss while retaining cached visualization data
for an unchanged snapshot.
- Add detailed English and Russian documentation for the current data flow:
- bucket configuration and server-only authentication,
- manifest and snapshot_id versioning,
- route-to-payload mapping,
- browser and in-memory cache behavior,
- gzip response handling,
- data freshness guarantees,
- validation and stale fallback behavior,
- cold starts and multiple replicas,
- atomic snapshot publishing requirements,
- operational verification commands,
- implementation file map.
- Document that every bucket publish must use a new unique snapshot_id and update manifest.json
only after all referenced payload files are available.
- docs/data-loading-cache_en.md +355 -0
- docs/data-loading-cache_ru.md +355 -0
- src/hooks.server.ts +68 -3
- src/lib/server/hf-bucket/details-cache.ts +3 -21
- src/lib/server/hf-bucket/details-snapshot.ts +23 -0
- src/lib/server/hf-bucket/ranking-cache.ts +3 -21
- src/lib/server/hf-bucket/ranking-snapshot.ts +16 -13
- src/lib/server/hf-bucket/tools-cache.ts +1 -9
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data loading and caching
|
| 2 |
+
|
| 3 |
+
This document describes the current implementation of data loading and caching in the
|
| 4 |
+
HiveTrace Guardrail Leaderboard frontend.
|
| 5 |
+
|
| 6 |
+
The implementation has three goals:
|
| 7 |
+
|
| 8 |
+
- keep the private Hugging Face token on the SvelteKit server;
|
| 9 |
+
- avoid downloading and adapting unchanged bucket payloads;
|
| 10 |
+
- make repeated client-side navigation fast while providing a predictable data freshness window.
|
| 11 |
+
|
| 12 |
+
## Architecture
|
| 13 |
+
|
| 14 |
+
```text
|
| 15 |
+
Browser
|
| 16 |
+
-> SvelteKit page or __data.json request
|
| 17 |
+
-> Hugging Face Space Node process
|
| 18 |
+
-> private Hugging Face bucket
|
| 19 |
+
-> latest/manifest.json
|
| 20 |
+
-> payload files referenced by the manifest
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
The browser never reads the private bucket directly. All bucket requests are made by the
|
| 24 |
+
SvelteKit server with the server-only `HF_TOKEN` environment variable.
|
| 25 |
+
|
| 26 |
+
## Data source
|
| 27 |
+
|
| 28 |
+
The bucket is configured with these runtime variables:
|
| 29 |
+
|
| 30 |
+
```text
|
| 31 |
+
HF_TOKEN=<private read token>
|
| 32 |
+
HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2
|
| 33 |
+
HF_BUCKET_PREFIX=latest
|
| 34 |
+
HF_BUCKET_REQUEST_TIMEOUT_MS=10000
|
| 35 |
+
HF_BUCKET_ENDPOINT=https://huggingface.co
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
`HF_BUCKET_ENDPOINT` and `HF_BUCKET_REQUEST_TIMEOUT_MS` are optional. The values above are
|
| 39 |
+
their defaults.
|
| 40 |
+
|
| 41 |
+
`HF_BUCKET_CACHE_TTL_MS` still controls the regular in-memory manifest cache and the direct
|
| 42 |
+
Tools snapshot helper. The main Ranking, Details, and Visualizations page-data flow explicitly
|
| 43 |
+
refreshes the manifest after the browser cache misses, so this variable does not add another
|
| 44 |
+
freshness delay to normal client-side page transitions.
|
| 45 |
+
|
| 46 |
+
## Manifest as the version pointer
|
| 47 |
+
|
| 48 |
+
`latest/manifest.json` is the single version pointer used by the frontend. Its important fields
|
| 49 |
+
are:
|
| 50 |
+
|
| 51 |
+
- `snapshot_id`: the identity of the published dataset;
|
| 52 |
+
- `files`: paths to all payload files;
|
| 53 |
+
- `hashes`: SHA-256 hashes of the payload files;
|
| 54 |
+
- `schema_version`: the bucket contract version;
|
| 55 |
+
- model, group, and dataset counts used for validation.
|
| 56 |
+
|
| 57 |
+
The frontend decides whether payload data changed by comparing `snapshot_id`. A new publish
|
| 58 |
+
must always use a new, unique `snapshot_id`.
|
| 59 |
+
|
| 60 |
+
Changing payload files or hashes without changing `snapshot_id` is not supported. In that case,
|
| 61 |
+
the frontend considers the snapshot unchanged and can continue using the old in-memory payload
|
| 62 |
+
until the Space process restarts.
|
| 63 |
+
|
| 64 |
+
## Routes and payloads
|
| 65 |
+
|
| 66 |
+
| Route | Server loader | Bucket data |
|
| 67 |
+
| -------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
| 68 |
+
| `/` | `getHfBucketRankingState()` | catalog, leaderboard, and details matrix |
|
| 69 |
+
| `/details` | `getHfBucketDetailsState()` | catalog, leaderboard, and details matrix |
|
| 70 |
+
| `/tools` | `getHfBucketToolsState()` | catalog, leaderboard, drilldown index, radar, scatter, heatmap, grouped bars, Pareto, performance, and robustness |
|
| 71 |
+
| `/methodology` | no bucket page loader | no bucket payload |
|
| 72 |
+
|
| 73 |
+
Ranking also uses the details matrix because the `partial` model status is derived from
|
| 74 |
+
`metrics_evaluated_samples < sample_count`.
|
| 75 |
+
|
| 76 |
+
The root `+layout.server.ts` reads the manifest separately to expose public bucket status such
|
| 77 |
+
as snapshot date and model count. That status uses the regular manifest cache. It is metadata
|
| 78 |
+
for the layout and does not control page-data freshness.
|
| 79 |
+
|
| 80 |
+
## Client-side navigation
|
| 81 |
+
|
| 82 |
+
SvelteKit intercepts internal menu links and requests route data through endpoints such as:
|
| 83 |
+
|
| 84 |
+
```text
|
| 85 |
+
/__data.json
|
| 86 |
+
/details/__data.json
|
| 87 |
+
/tools/__data.json
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
Localized variants are recognized as well, for example `/ru/details/__data.json`.
|
| 91 |
+
|
| 92 |
+
The response optimization in `src/hooks.server.ts` applies only to these three page-data
|
| 93 |
+
routes. It does not apply to Methodology, static assets, errors, or arbitrary API responses.
|
| 94 |
+
|
| 95 |
+
### Browser cache
|
| 96 |
+
|
| 97 |
+
Successful page-data responses use:
|
| 98 |
+
|
| 99 |
+
```http
|
| 100 |
+
Cache-Control: private, max-age=300
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
This means:
|
| 104 |
+
|
| 105 |
+
- each browser can reuse a route response for five minutes;
|
| 106 |
+
- the response is not a public CDN or shared-proxy cache entry;
|
| 107 |
+
- no `stale-while-revalidate` window is used;
|
| 108 |
+
- after five minutes, the next navigation must contact the SvelteKit server before rendering
|
| 109 |
+
that route.
|
| 110 |
+
|
| 111 |
+
The cache is per response URL, including SvelteKit query parameters. Ranking, Details, and
|
| 112 |
+
Visualizations therefore have independent browser cache entries.
|
| 113 |
+
|
| 114 |
+
An already rendered page does not update itself. Fresh data is applied on a later navigation or
|
| 115 |
+
full page reload.
|
| 116 |
+
|
| 117 |
+
### Gzip compression
|
| 118 |
+
|
| 119 |
+
The same hook compresses a page-data response when all conditions are true:
|
| 120 |
+
|
| 121 |
+
- the response is successful and has a body;
|
| 122 |
+
- its declared size is at least 1,024 bytes;
|
| 123 |
+
- the client accepts `gzip`.
|
| 124 |
+
|
| 125 |
+
The compressed response contains:
|
| 126 |
+
|
| 127 |
+
```http
|
| 128 |
+
Content-Encoding: gzip
|
| 129 |
+
Vary: Accept-Encoding
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
`Content-Length` is removed because the compressed body is streamed. Clients without gzip
|
| 133 |
+
support receive the original body with the same five-minute private cache policy.
|
| 134 |
+
|
| 135 |
+
Approximate measured sizes for the current dataset are:
|
| 136 |
+
|
| 137 |
+
| Route data | Uncompressed | Gzip |
|
| 138 |
+
| -------------- | -----------: | -----: |
|
| 139 |
+
| Ranking | 171 KB | 58 KB |
|
| 140 |
+
| Details | 643 KB | 158 KB |
|
| 141 |
+
| Visualizations | 709 KB | 122 KB |
|
| 142 |
+
|
| 143 |
+
These sizes depend on the bucket contents and will change as models and datasets are added.
|
| 144 |
+
|
| 145 |
+
## Server request flow
|
| 146 |
+
|
| 147 |
+
When the browser does not have a fresh page-data response, the server follows this flow:
|
| 148 |
+
|
| 149 |
+
1. The page loader calls the corresponding Ranking, Details, or Tools state function.
|
| 150 |
+
2. The state function calls `fetchBucketRankingSnapshot({ refreshManifest: true })`.
|
| 151 |
+
3. `refreshManifest: true` bypasses the regular manifest TTL and reads the current small
|
| 152 |
+
`manifest.json` from Hugging Face.
|
| 153 |
+
4. If the returned `snapshot_id` matches the in-memory ranking snapshot, catalog and leaderboard
|
| 154 |
+
are reused without another payload download.
|
| 155 |
+
5. If `snapshot_id` changed, catalog and leaderboard are downloaded in parallel, their hashes
|
| 156 |
+
and schemas are validated, and the ranking snapshot is replaced.
|
| 157 |
+
6. Route-specific data is reused or rebuilt for the same new `snapshot_id`.
|
| 158 |
+
7. SvelteKit serializes the adapted route data, and the server hook applies the private cache
|
| 159 |
+
header and gzip compression.
|
| 160 |
+
|
| 161 |
+
The important distinction between the snapshot options is:
|
| 162 |
+
|
| 163 |
+
- `refreshManifest: true` always checks the manifest but reuses payloads when `snapshot_id` is
|
| 164 |
+
unchanged;
|
| 165 |
+
- `forceRefresh: true` also bypasses payload reuse and rebuilds the ranking snapshot.
|
| 166 |
+
|
| 167 |
+
Normal page transitions use `refreshManifest`, not `forceRefresh`.
|
| 168 |
+
|
| 169 |
+
## In-memory caches
|
| 170 |
+
|
| 171 |
+
All server caches are module-level memory in the running Node process. They are private to a
|
| 172 |
+
single Space replica and are cleared when the container restarts, sleeps, or is redeployed.
|
| 173 |
+
|
| 174 |
+
### Manifest cache
|
| 175 |
+
|
| 176 |
+
`src/lib/server/hf-bucket/cache.ts` stores:
|
| 177 |
+
|
| 178 |
+
- the parsed manifest;
|
| 179 |
+
- the time it was fetched;
|
| 180 |
+
- its expiration time;
|
| 181 |
+
- one shared in-flight manifest request.
|
| 182 |
+
|
| 183 |
+
Regular callers use `HF_BUCKET_CACHE_TTL_MS`, with a five-minute default. Page-data refreshes use
|
| 184 |
+
the forced manifest path described above. Concurrent forced checks join the same in-flight
|
| 185 |
+
request when they overlap.
|
| 186 |
+
|
| 187 |
+
### Ranking snapshot
|
| 188 |
+
|
| 189 |
+
The ranking snapshot contains the manifest, catalog, and leaderboard. It is cached by
|
| 190 |
+
`snapshot_id`.
|
| 191 |
+
|
| 192 |
+
Concurrent downloads of the same snapshot share one promise. In-flight requests are also keyed
|
| 193 |
+
by `snapshot_id`, so a request for a newly published snapshot does not accidentally reuse a
|
| 194 |
+
download for an older snapshot.
|
| 195 |
+
|
| 196 |
+
### Details snapshot
|
| 197 |
+
|
| 198 |
+
The details snapshot adds `details_matrix` to the ranking snapshot and is cached by
|
| 199 |
+
`snapshot_id`.
|
| 200 |
+
|
| 201 |
+
This cache is shared by Ranking and Details. Ranking needs the matrix for `partial` detection, so
|
| 202 |
+
opening Details after Ranking does not download and validate `details_matrix` a second time.
|
| 203 |
+
Concurrent requests for the same details snapshot also share one promise.
|
| 204 |
+
|
| 205 |
+
### Adapted Ranking and Details datasets
|
| 206 |
+
|
| 207 |
+
The UI-ready Ranking and Details objects are cached by `snapshot_id`. When the manifest is
|
| 208 |
+
unchanged, the server returns the already adapted object. A new snapshot is parsed, validated,
|
| 209 |
+
and adapted once per Node process.
|
| 210 |
+
|
| 211 |
+
### Visualizations snapshot and adapted Tools dataset
|
| 212 |
+
|
| 213 |
+
For a new snapshot, the visualization files are downloaded in parallel. The raw visualization
|
| 214 |
+
snapshot and the adapted Tools dataset are cached separately by `snapshot_id`.
|
| 215 |
+
|
| 216 |
+
The direct `getHfBucketToolsSnapshot()` helper retains its TTL-based fast path. The `/tools`
|
| 217 |
+
page uses the state path, which refreshes the manifest after a browser cache miss.
|
| 218 |
+
|
| 219 |
+
## Validation
|
| 220 |
+
|
| 221 |
+
Payload reuse is allowed only after the manifest version check. New payloads go through:
|
| 222 |
+
|
| 223 |
+
- JSON parsing;
|
| 224 |
+
- schema parsing;
|
| 225 |
+
- SHA-256 verification when a hash is present;
|
| 226 |
+
- manifest count checks;
|
| 227 |
+
- model, group, and dataset reference checks;
|
| 228 |
+
- matrix completeness and duplicate-pair checks;
|
| 229 |
+
- visualization reference checks.
|
| 230 |
+
|
| 231 |
+
A validation error is handled in the same way as another refresh failure.
|
| 232 |
+
|
| 233 |
+
## Data freshness guarantee
|
| 234 |
+
|
| 235 |
+
Assume a valid new snapshot is published at time `T` and the bucket remains reachable.
|
| 236 |
+
|
| 237 |
+
### Route response is already in the browser cache
|
| 238 |
+
|
| 239 |
+
The old route response can be used until its individual five-minute `max-age` expires. The first
|
| 240 |
+
navigation to that route after expiration reads the fresh manifest. If `snapshot_id` changed,
|
| 241 |
+
the same navigation waits for and receives the new payload.
|
| 242 |
+
|
| 243 |
+
Therefore, the expected guarantee is:
|
| 244 |
+
|
| 245 |
+
> No later than the first navigation after the route's five-minute browser cache expires, the
|
| 246 |
+
> user receives the new snapshot.
|
| 247 |
+
|
| 248 |
+
There is no additional stale response window.
|
| 249 |
+
|
| 250 |
+
### Route response is not in the browser cache
|
| 251 |
+
|
| 252 |
+
The navigation immediately contacts the server and checks the manifest. If the server already
|
| 253 |
+
has the new payload, it reuses it. Otherwise, the navigation waits while the new snapshot is
|
| 254 |
+
downloaded and adapted.
|
| 255 |
+
|
| 256 |
+
### Full page reload
|
| 257 |
+
|
| 258 |
+
The five-minute policy targets SvelteKit `__data.json` navigation responses. A normal full HTML
|
| 259 |
+
request is not covered by this page-data cache and runs the server page loader again.
|
| 260 |
+
|
| 261 |
+
### Exceptions
|
| 262 |
+
|
| 263 |
+
The five-minute expectation does not apply when:
|
| 264 |
+
|
| 265 |
+
- Hugging Face or the network is unavailable;
|
| 266 |
+
- the new payload fails parsing, hash checks, or validation;
|
| 267 |
+
- the publisher reused the old `snapshot_id`;
|
| 268 |
+
- the manifest was published before all referenced files became readable.
|
| 269 |
+
|
| 270 |
+
## Failure behavior
|
| 271 |
+
|
| 272 |
+
The cache is also a resilience mechanism:
|
| 273 |
+
|
| 274 |
+
- if a refresh fails and a usable route dataset exists, the server returns cached data;
|
| 275 |
+
- if a new manifest is readable but a new payload is invalid or unavailable, the route state is
|
| 276 |
+
returned as `stale` and the UI can show a warning;
|
| 277 |
+
- if no usable cache exists, the route returns an `unavailable` state with an empty fallback
|
| 278 |
+
dataset;
|
| 279 |
+
- simultaneous requests share in-flight work to avoid duplicate bucket downloads.
|
| 280 |
+
|
| 281 |
+
One implementation detail is worth noting: when a forced manifest read fails but an older
|
| 282 |
+
manifest is cached, the manifest layer can return that stale manifest. The existing route data
|
| 283 |
+
can then continue to be served. The current route state does not always propagate this specific
|
| 284 |
+
manifest fallback as a visible `stale` warning.
|
| 285 |
+
|
| 286 |
+
## Publishing a new snapshot
|
| 287 |
+
|
| 288 |
+
The producer must publish atomically from the frontend's point of view:
|
| 289 |
+
|
| 290 |
+
1. Generate all payload files.
|
| 291 |
+
2. Use immutable or snapshot-specific file paths where practical.
|
| 292 |
+
3. Calculate and write the SHA-256 hashes.
|
| 293 |
+
4. Upload every payload file and verify it is readable.
|
| 294 |
+
5. Create a manifest with a new unique `snapshot_id`, correct paths, hashes, counts, and schema
|
| 295 |
+
version.
|
| 296 |
+
6. Upload `latest/manifest.json` last.
|
| 297 |
+
|
| 298 |
+
Publishing the manifest last prevents the frontend from seeing a new version that references
|
| 299 |
+
files which are not available yet.
|
| 300 |
+
|
| 301 |
+
## Cold starts and replicas
|
| 302 |
+
|
| 303 |
+
After a Space restart or cold start, no in-memory snapshot exists. The first request must read
|
| 304 |
+
the manifest and all payloads required by that route. Later requests reuse the prepared data.
|
| 305 |
+
|
| 306 |
+
If the Space runs multiple replicas, each replica has its own memory cache and warms
|
| 307 |
+
independently. The browser cache remains local to each user.
|
| 308 |
+
|
| 309 |
+
## Operational checks
|
| 310 |
+
|
| 311 |
+
To inspect the page-data response headers, use an authenticated request to the Space:
|
| 312 |
+
|
| 313 |
+
```bash
|
| 314 |
+
curl -sS \
|
| 315 |
+
-H "Authorization: Bearer $HF_TOKEN" \
|
| 316 |
+
-H "Accept-Encoding: gzip" \
|
| 317 |
+
-D - \
|
| 318 |
+
-o /dev/null \
|
| 319 |
+
"https://<space-domain>/details/__data.json"
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
Expected headers include:
|
| 323 |
+
|
| 324 |
+
```http
|
| 325 |
+
Cache-Control: private, max-age=300
|
| 326 |
+
Content-Encoding: gzip
|
| 327 |
+
Vary: Accept-Encoding
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
`curl` does not reproduce the browser's navigation cache unless an explicit curl cache is used.
|
| 331 |
+
Repeated curl requests therefore reach the server and can trigger repeated manifest checks.
|
| 332 |
+
|
| 333 |
+
When verifying a publish, check all of the following:
|
| 334 |
+
|
| 335 |
+
- the manifest has a new `snapshot_id`;
|
| 336 |
+
- every referenced file exists;
|
| 337 |
+
- hashes match the published contents;
|
| 338 |
+
- the Space logs contain no bucket validation errors;
|
| 339 |
+
- the first route navigation after browser cache expiration shows the new snapshot.
|
| 340 |
+
|
| 341 |
+
## Implementation map
|
| 342 |
+
|
| 343 |
+
| Responsibility | File |
|
| 344 |
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
| 345 |
+
| HTTP bucket client and server-only token | `src/lib/server/hf-bucket/client.ts` |
|
| 346 |
+
| Regular manifest cache and in-flight request | `src/lib/server/hf-bucket/cache.ts` |
|
| 347 |
+
| Ranking snapshot download and validation | `src/lib/server/hf-bucket/ranking-snapshot.ts` |
|
| 348 |
+
| Shared details snapshot | `src/lib/server/hf-bucket/details-snapshot.ts` |
|
| 349 |
+
| Visualization snapshot download | `src/lib/server/hf-bucket/tools-snapshot.ts` |
|
| 350 |
+
| Adapted Ranking cache | `src/lib/server/hf-bucket/ranking-cache.ts` |
|
| 351 |
+
| Adapted Details cache | `src/lib/server/hf-bucket/details-cache.ts` |
|
| 352 |
+
| Raw and adapted Tools caches | `src/lib/server/hf-bucket/tools-cache.ts` |
|
| 353 |
+
| Browser cache headers and gzip | `src/hooks.server.ts` |
|
| 354 |
+
| Root public bucket status | `src/routes/+layout.server.ts` |
|
| 355 |
+
| Route orchestration | `src/routes/+page.server.ts`, `src/routes/details/+page.server.ts`, `src/routes/tools/+page.server.ts` |
|
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Загрузка данных и кеширование
|
| 2 |
+
|
| 3 |
+
Этот документ описывает текущую реализацию загрузки данных и кеширования во frontend
|
| 4 |
+
HiveTrace Guardrail Leaderboard.
|
| 5 |
+
|
| 6 |
+
У реализации три основные цели:
|
| 7 |
+
|
| 8 |
+
- хранить приватный Hugging Face token только на сервере SvelteKit;
|
| 9 |
+
- не скачивать и не адаптировать повторно неизменившиеся payload-файлы bucket;
|
| 10 |
+
- ускорить повторные переходы между страницами и сохранить предсказуемое окно актуальности
|
| 11 |
+
данных.
|
| 12 |
+
|
| 13 |
+
## Архитектура
|
| 14 |
+
|
| 15 |
+
```text
|
| 16 |
+
Browser
|
| 17 |
+
-> SvelteKit page или __data.json request
|
| 18 |
+
-> Node process в Hugging Face Space
|
| 19 |
+
-> private Hugging Face bucket
|
| 20 |
+
-> latest/manifest.json
|
| 21 |
+
-> payload-файлы, указанные в manifest
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Браузер не обращается к приватному bucket напрямую. Все запросы в bucket выполняет сервер
|
| 25 |
+
SvelteKit с помощью server-only переменной `HF_TOKEN`.
|
| 26 |
+
|
| 27 |
+
## Источник данных
|
| 28 |
+
|
| 29 |
+
Bucket настраивается runtime-переменными:
|
| 30 |
+
|
| 31 |
+
```text
|
| 32 |
+
HF_TOKEN=<private read token>
|
| 33 |
+
HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2
|
| 34 |
+
HF_BUCKET_PREFIX=latest
|
| 35 |
+
HF_BUCKET_REQUEST_TIMEOUT_MS=10000
|
| 36 |
+
HF_BUCKET_ENDPOINT=https://huggingface.co
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
`HF_BUCKET_ENDPOINT` и `HF_BUCKET_REQUEST_TIMEOUT_MS` опциональны. Выше указаны их значения по
|
| 40 |
+
умолчанию.
|
| 41 |
+
|
| 42 |
+
`HF_BUCKET_CACHE_TTL_MS` по-прежнему управляет обычным in-memory кешем manifest и прямым helper
|
| 43 |
+
Tools snapshot. Основной поток page data для Ranking, Details и Visualizations после промаха
|
| 44 |
+
браузерного кеша явно обновляет manifest, поэтому эта переменная не добавляет еще одну задержку
|
| 45 |
+
актуальности при обычных переходах по страницам.
|
| 46 |
+
|
| 47 |
+
## Manifest как указатель версии
|
| 48 |
+
|
| 49 |
+
`latest/manifest.json` является единственным указателем версии для frontend. Основные поля:
|
| 50 |
+
|
| 51 |
+
- `snapshot_id`: идентификатор опубликованного набора данных;
|
| 52 |
+
- `files`: пути ко всем payload-файлам;
|
| 53 |
+
- `hashes`: SHA-256 хеши payload-файлов;
|
| 54 |
+
- `schema_version`: версия контракта bucket;
|
| 55 |
+
- количества моделей, групп и датасетов для валидации.
|
| 56 |
+
|
| 57 |
+
Frontend определяет изменение payload по `snapshot_id`. Каждая новая публикация обязана иметь
|
| 58 |
+
новый уникальный `snapshot_id`.
|
| 59 |
+
|
| 60 |
+
Изменение payload-файлов или хешей без изменения `snapshot_id` не поддерживается. В таком случае
|
| 61 |
+
frontend считает snapshot прежним и может использовать старый in-memory payload до перезапуска
|
| 62 |
+
процесса Space.
|
| 63 |
+
|
| 64 |
+
## Маршруты и payload-файлы
|
| 65 |
+
|
| 66 |
+
| Маршрут | Server loader | Данные bucket |
|
| 67 |
+
| -------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
| 68 |
+
| `/` | `getHfBucketRankingState()` | catalog, leaderboard и details matrix |
|
| 69 |
+
| `/details` | `getHfBucketDetailsState()` | catalog, leaderboard и details matrix |
|
| 70 |
+
| `/tools` | `getHfBucketToolsState()` | catalog, leaderboard, drilldown index, radar, scatter, heatmap, grouped bars, Pareto, performance и robustness |
|
| 71 |
+
| `/methodology` | нет bucket page loader | payload из bucket не используется |
|
| 72 |
+
|
| 73 |
+
Ranking также использует details matrix, потому что статус модели `partial` вычисляется по
|
| 74 |
+
условию `metrics_evaluated_samples < sample_count`.
|
| 75 |
+
|
| 76 |
+
Корневой `+layout.server.ts` отдельно читает manifest и возвращает публичный статус bucket:
|
| 77 |
+
дату snapshot, количество моделей и другие общие поля. Этот статус использует обычный кеш
|
| 78 |
+
manifest. Он является метаданными layout и не управляет актуальностью page data.
|
| 79 |
+
|
| 80 |
+
## Клиентская навигация
|
| 81 |
+
|
| 82 |
+
SvelteKit перехватывает ��нутренние ссылки главного меню и запрашивает данные маршрутов через
|
| 83 |
+
endpoint вида:
|
| 84 |
+
|
| 85 |
+
```text
|
| 86 |
+
/__data.json
|
| 87 |
+
/details/__data.json
|
| 88 |
+
/tools/__data.json
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Локализованные варианты также распознаются, например `/ru/details/__data.json`.
|
| 92 |
+
|
| 93 |
+
Оптимизация response в `src/hooks.server.ts` применяется только к этим трем page-data
|
| 94 |
+
маршрутам. Она не применяется к Methodology, статическим файлам, ошибкам и произвольным API
|
| 95 |
+
response.
|
| 96 |
+
|
| 97 |
+
### Браузерный кеш
|
| 98 |
+
|
| 99 |
+
Успешный page-data response получает заголовок:
|
| 100 |
+
|
| 101 |
+
```http
|
| 102 |
+
Cache-Control: private, max-age=300
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
Это означает:
|
| 106 |
+
|
| 107 |
+
- браузер может повторно использовать response маршрута в течение пяти минут;
|
| 108 |
+
- response не попадает в общий CDN или shared proxy cache;
|
| 109 |
+
- окно `stale-while-revalidate` отсутствует;
|
| 110 |
+
- после пяти минут следующий переход обязан обратиться к серверу SvelteKit до отрисовки
|
| 111 |
+
маршрута.
|
| 112 |
+
|
| 113 |
+
Кеш привязан к URL response, включая внутренние query-параметры SvelteKit. Поэтому Ranking,
|
| 114 |
+
Details и Visualizations имеют отдельные записи браузерного кеша.
|
| 115 |
+
|
| 116 |
+
Уже открытая страница сама не обновляется. Новые данные применяются при следующем переходе или
|
| 117 |
+
полной перезагрузке страницы.
|
| 118 |
+
|
| 119 |
+
### Gzip-сжатие
|
| 120 |
+
|
| 121 |
+
Тот же hook сжимает page-data response, когда выполнены все условия:
|
| 122 |
+
|
| 123 |
+
- response успешный и содержит body;
|
| 124 |
+
- заявленный размер не меньше 1 024 bytes;
|
| 125 |
+
- клиент поддерживает `gzip`.
|
| 126 |
+
|
| 127 |
+
Сжатый response содержит:
|
| 128 |
+
|
| 129 |
+
```http
|
| 130 |
+
Content-Encoding: gzip
|
| 131 |
+
Vary: Accept-Encoding
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
`Content-Length` удаляется, потому что сжатый body передается потоком. Клиенты без поддержки
|
| 135 |
+
gzip получают исходный body с тем же приватным кешем на пять минут.
|
| 136 |
+
|
| 137 |
+
Примерные измеренные размеры для текущего набора данных:
|
| 138 |
+
|
| 139 |
+
| Данные маршрута | Без сжатия | Gzip |
|
| 140 |
+
| --------------- | ---------: | -----: |
|
| 141 |
+
| Ranking | 171 KB | 58 KB |
|
| 142 |
+
| Details | 643 KB | 158 KB |
|
| 143 |
+
| Visualizations | 709 KB | 122 KB |
|
| 144 |
+
|
| 145 |
+
Размеры зависят от содержимого bucket и будут меняться при добавлении моделей и датасетов.
|
| 146 |
+
|
| 147 |
+
## Поток server request
|
| 148 |
+
|
| 149 |
+
Если в браузере нет свежего page-data response, сервер выполняет следующие шаги:
|
| 150 |
+
|
| 151 |
+
1. Page loader вызывает соответствующую state-функцию Ranking, Details или Tools.
|
| 152 |
+
2. State-функция вызывает `fetchBucketRankingSnapshot({ refreshManifest: true })`.
|
| 153 |
+
3. `refreshManifest: true` обходит обычный TTL manifest и читает актуальный небольшой
|
| 154 |
+
`manifest.json` из Hugging Face.
|
| 155 |
+
4. Если полученный `snapshot_id` совпадает с in-memory ranking snapshot, catalog и leaderboard
|
| 156 |
+
используются повторно без скачивания payload.
|
| 157 |
+
5. Если `snapshot_id` изменился, catalog и leaderboard скачиваются параллельно, проверяются их
|
| 158 |
+
хеши и schema, после чего ranking snapshot заменяется.
|
| 159 |
+
6. Данные конкретного маршрута переиспользуются или пересобираются для нового `snapshot_id`.
|
| 160 |
+
7. SvelteKit сериализует адаптированные данные маршрута, а server hook добавляет приватный кеш и
|
| 161 |
+
gzip-сжатие.
|
| 162 |
+
|
| 163 |
+
Разница между параметрами snapshot:
|
| 164 |
+
|
| 165 |
+
- `refreshManifest: true` всегда проверяет manifest, но переиспользует payload при неизменном
|
| 166 |
+
`snapshot_id`;
|
| 167 |
+
- `forceRefresh: true` также отключает переиспользование payload и пересобирает ranking snapshot.
|
| 168 |
+
|
| 169 |
+
Обычные переходы по страницам используют `refreshManifest`, а не `forceRefresh`.
|
| 170 |
+
|
| 171 |
+
## In-memory кеши сервера
|
| 172 |
+
|
| 173 |
+
Все серверные кеши хранятся в памяти работающего Node process. Они принадлежат одному replica
|
| 174 |
+
Space и очищаются при перезапуске, засыпании контейнера или новом deploy.
|
| 175 |
+
|
| 176 |
+
### Кеш manifest
|
| 177 |
+
|
| 178 |
+
`src/lib/server/hf-bucket/cache.ts` хранит:
|
| 179 |
+
|
| 180 |
+
- распарсенный manifest;
|
| 181 |
+
- время его получения;
|
| 182 |
+
- время истечения кеша;
|
| 183 |
+
- один общий in-flight request manifest.
|
| 184 |
+
|
| 185 |
+
Обычные callers используют `HF_BUCKET_CACHE_TTL_MS`, по умолчанию пять минут. Page-data refresh
|
| 186 |
+
использует принудительное чтение manifest, описанное выше. Одновременные принудительные проверки
|
| 187 |
+
подключаются к одному in-flight request, если пересекаются по времени.
|
| 188 |
+
|
| 189 |
+
### Ranking snapshot
|
| 190 |
+
|
| 191 |
+
Ranking snapshot содержит manifest, catalog и leaderboard. Он кешируется по `snapshot_id`.
|
| 192 |
+
|
| 193 |
+
Одновременные загрузки одного snapshot используют общий promise. In-flight request также
|
| 194 |
+
привязан к `snapshot_id`, поэтому запрос нового опубликованного snapshot не использует по ошибке
|
| 195 |
+
загрузку предыдущего snapshot.
|
| 196 |
+
|
| 197 |
+
### Details snapshot
|
| 198 |
+
|
| 199 |
+
Details snapshot добавляет `details_matrix` к ranking snapshot и кешируется по `snapshot_id`.
|
| 200 |
+
|
| 201 |
+
Этот кеш общий для Ranking и Details. Ranking использует matrix для определения `partial`, поэтому
|
| 202 |
+
после открытия Ranking страница Details не скачивает и не валидирует `details_matrix` повторно.
|
| 203 |
+
Одновременные запросы одного details snapshot также используют общий promise.
|
| 204 |
+
|
| 205 |
+
### Адаптированные Ranking и Details
|
| 206 |
+
|
| 207 |
+
Готовые для UI объекты Ranking и Details кешируются по `snapshot_id`. Если manifest не изменился,
|
| 208 |
+
сервер возвращает уже адаптированный объект. Новый snapshot парсится, валидируется и адаптируется
|
| 209 |
+
один раз на Node process.
|
| 210 |
+
|
| 211 |
+
### Visualizations snapshot и адаптированный Tools dataset
|
| 212 |
+
|
| 213 |
+
Для нового snapshot файлы визуализаций скачиваются параллельно. Raw visualization snapshot и
|
| 214 |
+
адаптированный Tools dataset кешируются отдельно по `snapshot_id`.
|
| 215 |
+
|
| 216 |
+
Прямой helper `getHfBucketToolsSnapshot()` сохраняет свой быстрый путь на основе TTL. Страница
|
| 217 |
+
`/tools` использует state-путь, который обновляет manifest после промаха браузерного кеша.
|
| 218 |
+
|
| 219 |
+
## Валидация
|
| 220 |
+
|
| 221 |
+
Payload используется повторно только после проверки версии manifest. Новый payload проходит:
|
| 222 |
+
|
| 223 |
+
- JSON parsing;
|
| 224 |
+
- schema parsing;
|
| 225 |
+
- проверку SHA-256 при наличии хеша;
|
| 226 |
+
- сверку количеств из manifest;
|
| 227 |
+
- проверку ссылок на модели, группы и датасеты;
|
| 228 |
+
- проверку полноты matrix и дублирующихся пар;
|
| 229 |
+
- проверку ссылок в данных визуализаций.
|
| 230 |
+
|
| 231 |
+
Ошибка валидации обрабатывается так же, как другая ошибка обновления.
|
| 232 |
+
|
| 233 |
+
## Гарантия актуальности данных
|
| 234 |
+
|
| 235 |
+
Предположим, что корректный новый snapshot опубликован в момент `T`, а bucket доступен.
|
| 236 |
+
|
| 237 |
+
### Response маршрута уже находится в браузерном кеше
|
| 238 |
+
|
| 239 |
+
Старый response может использоваться до истечения его индивидуального `max-age` в пять минут.
|
| 240 |
+
Первый переход на этот маршрут после истечения кеша читает свежий manifest. Если `snapshot_id`
|
| 241 |
+
изменился, тот же переход ожидает загрузку нового payload и получает новые данные.
|
| 242 |
+
|
| 243 |
+
Ожидаемая гарантия:
|
| 244 |
+
|
| 245 |
+
> Не позднее первого перехода после истечения пятиминутного браузерного кеша маршрута
|
| 246 |
+
> пользователь получает новый snapshot.
|
| 247 |
+
|
| 248 |
+
Дополнительного окна выдачи stale response нет.
|
| 249 |
+
|
| 250 |
+
### Response маршрута отсутствует в браузерном кеше
|
| 251 |
+
|
| 252 |
+
Переход сразу обращается к серверу и проверяет manifest. Если новый payload уже находится в
|
| 253 |
+
памяти сервера, он используется повторно. Иначе переход ожидает скачивание и адаптацию нового
|
| 254 |
+
snapshot.
|
| 255 |
+
|
| 256 |
+
### Полная перезагрузка страницы
|
| 257 |
+
|
| 258 |
+
Пятиминутная политика применяется к SvelteKit navigation response `__data.json`. Обычный полный
|
| 259 |
+
HTML request не покрывается этим page-data кешем и снова запускает server page loader.
|
| 260 |
+
|
| 261 |
+
### Исключения
|
| 262 |
+
|
| 263 |
+
Пятиминутное ожидание неприменимо, если:
|
| 264 |
+
|
| 265 |
+
- Hugging Face или сеть недоступны;
|
| 266 |
+
- новый payload не проходит parsing, проверку хеша или валидацию;
|
| 267 |
+
- publisher повторно использовал старый `snapshot_id`;
|
| 268 |
+
- manifest опубликован раньше, чем стали доступны все указанные в нем файлы.
|
| 269 |
+
|
| 270 |
+
## Поведение при ошибках
|
| 271 |
+
|
| 272 |
+
Кеш также обеспечивает устойчивость:
|
| 273 |
+
|
| 274 |
+
- если refresh завершился ошибкой и готовые данные маршрута существуют, сервер возвращает кеш;
|
| 275 |
+
- если новый manifest доступен, но новый payload невалиден или недоступен, route state становится
|
| 276 |
+
`stale`, и UI может показать предупреждение;
|
| 277 |
+
- если пригодного кеша нет, маршрут возвращает состояние `unavailable` с пустым fallback dataset;
|
| 278 |
+
- одновременные запросы используют общую in-flight работу и не дублируют загрузку bucket.
|
| 279 |
+
|
| 280 |
+
Важная деталь текущей реализации: если принудительное чтение manifest завершилось ошибкой, но в
|
| 281 |
+
памяти есть предыдущий manifest, слой manifest может вернуть его как stale. После этого сервер
|
| 282 |
+
продолжит выдавать предыдущие route data. Текущий route state не во всех случаях пробрасывает
|
| 283 |
+
именно этот fallback manifest как видимое предупреждение `stale`.
|
| 284 |
+
|
| 285 |
+
## Публикация нового snapshot
|
| 286 |
+
|
| 287 |
+
Producer должен выполнять публикацию атомарно с точки зрения frontend:
|
| 288 |
+
|
| 289 |
+
1. Сгенерировать все payload-файлы.
|
| 290 |
+
2. По возможности использовать immutable или snapshot-specific пути.
|
| 291 |
+
3. Рассчитать и записать SHA-256 хеши.
|
| 292 |
+
4. Загрузить каждый payload-файл и проверить, что он доступен для чтения.
|
| 293 |
+
5. Создать manifest с новым уникальным `snapshot_id`, корректными путями, хешами, количествами и
|
| 294 |
+
версией schema.
|
| 295 |
+
6. Последним загрузить `latest/manifest.json`.
|
| 296 |
+
|
| 297 |
+
Публикация manifest последним не позволяет frontend увидеть новую версию, которая ссылается на
|
| 298 |
+
еще недоступные файлы.
|
| 299 |
+
|
| 300 |
+
## Cold start и replicas
|
| 301 |
+
|
| 302 |
+
После перезапуска или cold start Space in-memory snapshot отсутствует. Первый request должен
|
| 303 |
+
прочитать manifest и все payload-файлы, необходимые выбранному маршруту. Следующие запросы
|
| 304 |
+
используют подготовленные данные повторно.
|
| 305 |
+
|
| 306 |
+
Если Space работает с несколькими replicas, каждая имеет собственный кеш в памяти и прогревается
|
| 307 |
+
независимо. Браузерный кеш остается локальным для каждого пользователя.
|
| 308 |
+
|
| 309 |
+
## Операционная проверка
|
| 310 |
+
|
| 311 |
+
Для проверки заголовков page-data response нужен авторизованный запрос к Space:
|
| 312 |
+
|
| 313 |
+
```bash
|
| 314 |
+
curl -sS \
|
| 315 |
+
-H "Authorization: Bearer $HF_TOKEN" \
|
| 316 |
+
-H "Accept-Encoding: gzip" \
|
| 317 |
+
-D - \
|
| 318 |
+
-o /dev/null \
|
| 319 |
+
"https://<space-domain>/details/__data.json"
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
Ожидаемые заголовки:
|
| 323 |
+
|
| 324 |
+
```http
|
| 325 |
+
Cache-Control: private, max-age=300
|
| 326 |
+
Content-Encoding: gzip
|
| 327 |
+
Vary: Accept-Encoding
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
`curl` не воспроизводит navigation cache браузера без отдельной настройки собственного кеша.
|
| 331 |
+
Поэтому повторные curl-запросы доходят до сервера и могут повторно запускать проверку manifest.
|
| 332 |
+
|
| 333 |
+
При проверке новой публикации нужно убедиться, что:
|
| 334 |
+
|
| 335 |
+
- manifest содержит новый `snapshot_id`;
|
| 336 |
+
- каждый указанный файл существует;
|
| 337 |
+
- хеши соответствуют опубликованному содержимому;
|
| 338 |
+
- в логах Space нет ошибок валидации bucket;
|
| 339 |
+
- первый переход после истечения браузерного кеша показывает новый snapshot.
|
| 340 |
+
|
| 341 |
+
## Карта реализации
|
| 342 |
+
|
| 343 |
+
| Ответственность | Файл |
|
| 344 |
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
| 345 |
+
| HTTP client bucket и server-only token | `src/lib/server/hf-bucket/client.ts` |
|
| 346 |
+
| Обычный кеш manifest и in-flight request | `src/lib/server/hf-bucket/cache.ts` |
|
| 347 |
+
| Загрузка и валидация Ranking snapshot | `src/lib/server/hf-bucket/ranking-snapshot.ts` |
|
| 348 |
+
| Общий Details snapshot | `src/lib/server/hf-bucket/details-snapshot.ts` |
|
| 349 |
+
| Загрузка Visualization snapshot | `src/lib/server/hf-bucket/tools-snapshot.ts` |
|
| 350 |
+
| Кеш адаптированного Ranking | `src/lib/server/hf-bucket/ranking-cache.ts` |
|
| 351 |
+
| Кеш адаптированного Details | `src/lib/server/hf-bucket/details-cache.ts` |
|
| 352 |
+
| Raw и адаптированный кеш Tools | `src/lib/server/hf-bucket/tools-cache.ts` |
|
| 353 |
+
| Заголовки браузерного кеша и gzip | `src/hooks.server.ts` |
|
| 354 |
+
| Публичный статус bucket в root layout | `src/routes/+layout.server.ts` |
|
| 355 |
+
| Оркестрация маршрутов | `src/routes/+page.server.ts`, `src/routes/details/+page.server.ts`, `src/routes/tools/+page.server.ts` |
|
|
@@ -1,17 +1,82 @@
|
|
| 1 |
import type { Handle } from '@sveltejs/kit';
|
| 2 |
-
import { getTextDirection } from '$lib/paraglide/runtime';
|
| 3 |
import { paraglideMiddleware } from '$lib/paraglide/server';
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
const handleParaglide: Handle = ({ event, resolve }) =>
|
| 6 |
-
paraglideMiddleware(event.request, ({ request, locale }) => {
|
| 7 |
event.request = request;
|
| 8 |
|
| 9 |
-
|
| 10 |
transformPageChunk: ({ html }) =>
|
| 11 |
html
|
| 12 |
.replace('%paraglide.lang%', locale)
|
| 13 |
.replace('%paraglide.dir%', getTextDirection(locale))
|
| 14 |
});
|
|
|
|
|
|
|
| 15 |
});
|
| 16 |
|
| 17 |
export const handle: Handle = handleParaglide;
|
|
|
|
| 1 |
import type { Handle } from '@sveltejs/kit';
|
| 2 |
+
import { getTextDirection, isLocale } from '$lib/paraglide/runtime';
|
| 3 |
import { paraglideMiddleware } from '$lib/paraglide/server';
|
| 4 |
|
| 5 |
+
const DATA_CACHE_CONTROL = 'private, max-age=300';
|
| 6 |
+
const MIN_GZIP_SIZE_BYTES = 1024;
|
| 7 |
+
|
| 8 |
+
function isBucketDataRequest(request: Request) {
|
| 9 |
+
const segments = new URL(request.url).pathname.split('/').filter(Boolean);
|
| 10 |
+
if (segments.at(-1) !== '__data.json') return false;
|
| 11 |
+
|
| 12 |
+
segments.pop();
|
| 13 |
+
if (segments[0] && isLocale(segments[0])) segments.shift();
|
| 14 |
+
|
| 15 |
+
const route = segments.join('/');
|
| 16 |
+
return route === '' || route === 'details' || route === 'tools';
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function acceptsGzip(request: Request) {
|
| 20 |
+
return (request.headers.get('accept-encoding') ?? '').split(',').some((entry) => {
|
| 21 |
+
const [encoding, ...parameters] = entry.trim().toLowerCase().split(';');
|
| 22 |
+
if (encoding !== 'gzip' && encoding !== '*') return false;
|
| 23 |
+
|
| 24 |
+
const quality = parameters
|
| 25 |
+
.map((parameter) => parameter.trim())
|
| 26 |
+
.find((parameter) => parameter.startsWith('q='));
|
| 27 |
+
return quality ? Number(quality.slice(2)) > 0 : true;
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function appendVary(headers: Headers, value: string) {
|
| 32 |
+
const values = new Set(
|
| 33 |
+
(headers.get('vary') ?? '')
|
| 34 |
+
.split(',')
|
| 35 |
+
.map((item) => item.trim())
|
| 36 |
+
.filter(Boolean)
|
| 37 |
+
);
|
| 38 |
+
values.add(value);
|
| 39 |
+
headers.set('vary', [...values].join(', '));
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function optimizeBucketDataResponse(request: Request, response: Response) {
|
| 43 |
+
if (!response.ok || !response.body || !isBucketDataRequest(request)) return response;
|
| 44 |
+
|
| 45 |
+
const headers = new Headers(response.headers);
|
| 46 |
+
headers.set('cache-control', DATA_CACHE_CONTROL);
|
| 47 |
+
appendVary(headers, 'Accept-Encoding');
|
| 48 |
+
|
| 49 |
+
const contentLength = Number(headers.get('content-length') ?? 0);
|
| 50 |
+
if (request.method === 'HEAD' || contentLength < MIN_GZIP_SIZE_BYTES || !acceptsGzip(request)) {
|
| 51 |
+
return new Response(response.body, {
|
| 52 |
+
status: response.status,
|
| 53 |
+
statusText: response.statusText,
|
| 54 |
+
headers
|
| 55 |
+
});
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
headers.delete('content-length');
|
| 59 |
+
headers.set('content-encoding', 'gzip');
|
| 60 |
+
|
| 61 |
+
return new Response(response.body.pipeThrough(new CompressionStream('gzip')), {
|
| 62 |
+
status: response.status,
|
| 63 |
+
statusText: response.statusText,
|
| 64 |
+
headers
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
const handleParaglide: Handle = ({ event, resolve }) =>
|
| 69 |
+
paraglideMiddleware(event.request, async ({ request, locale }) => {
|
| 70 |
event.request = request;
|
| 71 |
|
| 72 |
+
const response = await resolve(event, {
|
| 73 |
transformPageChunk: ({ html }) =>
|
| 74 |
html
|
| 75 |
.replace('%paraglide.lang%', locale)
|
| 76 |
.replace('%paraglide.dir%', getTextDirection(locale))
|
| 77 |
});
|
| 78 |
+
|
| 79 |
+
return optimizeBucketDataResponse(request, response);
|
| 80 |
});
|
| 81 |
|
| 82 |
export const handle: Handle = handleParaglide;
|
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import { env } from '$env/dynamic/private';
|
| 2 |
import type { BucketDataState } from '$lib/types/bucket-state';
|
| 3 |
import type { DetailsDataset } from '$lib/types/details-data';
|
| 4 |
import { adaptBucketDetails } from './adapt-details';
|
|
@@ -7,28 +6,19 @@ import { emptyDetailsDataset } from './fallback-data';
|
|
| 7 |
import { toPublicBucketError } from './public-error';
|
| 8 |
import { fetchBucketRankingSnapshot } from './ranking-snapshot';
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
let cached: { value: DetailsDataset; snapshotId: string; expiresAt: number } | undefined;
|
| 13 |
let inFlight: Promise<DetailsDataset> | undefined;
|
| 14 |
|
| 15 |
-
function cacheTtlMs() {
|
| 16 |
-
const configured = Number(env.HF_BUCKET_CACHE_TTL_MS);
|
| 17 |
-
return Number.isInteger(configured) && configured > 0 ? configured : DEFAULT_CACHE_TTL_MS;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
async function refreshDetails() {
|
| 21 |
-
const rankingSnapshot = await fetchBucketRankingSnapshot();
|
| 22 |
if (cached?.snapshotId === rankingSnapshot.manifest.snapshot_id) {
|
| 23 |
-
cached.expiresAt = Date.now() + cacheTtlMs();
|
| 24 |
return cached.value;
|
| 25 |
}
|
| 26 |
|
| 27 |
const value = adaptBucketDetails(await fetchBucketDetailsSnapshot(rankingSnapshot));
|
| 28 |
cached = {
|
| 29 |
value,
|
| 30 |
-
snapshotId: rankingSnapshot.manifest.snapshot_id
|
| 31 |
-
expiresAt: Date.now() + cacheTtlMs()
|
| 32 |
};
|
| 33 |
return value;
|
| 34 |
}
|
|
@@ -40,14 +30,6 @@ export async function getHfBucketDetails() {
|
|
| 40 |
}
|
| 41 |
|
| 42 |
export async function getHfBucketDetailsState(): Promise<BucketDataState<DetailsDataset>> {
|
| 43 |
-
if (cached && cached.expiresAt > Date.now()) {
|
| 44 |
-
return {
|
| 45 |
-
status: 'ok',
|
| 46 |
-
source: 'cache',
|
| 47 |
-
data: cached.value
|
| 48 |
-
};
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
if (!inFlight) {
|
| 52 |
inFlight = refreshDetails().finally(() => {
|
| 53 |
inFlight = undefined;
|
|
|
|
|
|
|
| 1 |
import type { BucketDataState } from '$lib/types/bucket-state';
|
| 2 |
import type { DetailsDataset } from '$lib/types/details-data';
|
| 3 |
import { adaptBucketDetails } from './adapt-details';
|
|
|
|
| 6 |
import { toPublicBucketError } from './public-error';
|
| 7 |
import { fetchBucketRankingSnapshot } from './ranking-snapshot';
|
| 8 |
|
| 9 |
+
let cached: { value: DetailsDataset; snapshotId: string } | undefined;
|
|
|
|
|
|
|
| 10 |
let inFlight: Promise<DetailsDataset> | undefined;
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
async function refreshDetails() {
|
| 13 |
+
const rankingSnapshot = await fetchBucketRankingSnapshot({ refreshManifest: true });
|
| 14 |
if (cached?.snapshotId === rankingSnapshot.manifest.snapshot_id) {
|
|
|
|
| 15 |
return cached.value;
|
| 16 |
}
|
| 17 |
|
| 18 |
const value = adaptBucketDetails(await fetchBucketDetailsSnapshot(rankingSnapshot));
|
| 19 |
cached = {
|
| 20 |
value,
|
| 21 |
+
snapshotId: rankingSnapshot.manifest.snapshot_id
|
|
|
|
| 22 |
};
|
| 23 |
return value;
|
| 24 |
}
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
export async function getHfBucketDetailsState(): Promise<BucketDataState<DetailsDataset>> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
if (!inFlight) {
|
| 34 |
inFlight = refreshDetails().finally(() => {
|
| 35 |
inFlight = undefined;
|
|
@@ -3,6 +3,9 @@ import { parseBucketDetailsMatrix } from './details-payload';
|
|
| 3 |
import { fetchBucketRankingSnapshot, verifyBucketFileHash } from './ranking-snapshot';
|
| 4 |
import type { BucketDetailsSnapshot, BucketRankingSnapshot } from './types';
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
function parseJson(text: string, path: string) {
|
| 7 |
try {
|
| 8 |
return JSON.parse(text) as unknown;
|
|
@@ -72,6 +75,26 @@ export async function fetchBucketDetailsSnapshot(
|
|
| 72 |
rankingSnapshot?: BucketRankingSnapshot
|
| 73 |
): Promise<BucketDetailsSnapshot> {
|
| 74 |
rankingSnapshot ??= await fetchBucketRankingSnapshot();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
const path = rankingSnapshot.manifest.files.details_matrix;
|
| 76 |
const text = await fetchHfBucketText(path);
|
| 77 |
verifyBucketFileHash(rankingSnapshot.manifest, 'details_matrix', text);
|
|
|
|
| 3 |
import { fetchBucketRankingSnapshot, verifyBucketFileHash } from './ranking-snapshot';
|
| 4 |
import type { BucketDetailsSnapshot, BucketRankingSnapshot } from './types';
|
| 5 |
|
| 6 |
+
let cachedSnapshot: BucketDetailsSnapshot | undefined;
|
| 7 |
+
let inFlight: { snapshotId: string; promise: Promise<BucketDetailsSnapshot> } | undefined;
|
| 8 |
+
|
| 9 |
function parseJson(text: string, path: string) {
|
| 10 |
try {
|
| 11 |
return JSON.parse(text) as unknown;
|
|
|
|
| 75 |
rankingSnapshot?: BucketRankingSnapshot
|
| 76 |
): Promise<BucketDetailsSnapshot> {
|
| 77 |
rankingSnapshot ??= await fetchBucketRankingSnapshot();
|
| 78 |
+
const snapshotId = rankingSnapshot.manifest.snapshot_id;
|
| 79 |
+
|
| 80 |
+
if (cachedSnapshot?.manifest.snapshot_id === snapshotId) return cachedSnapshot;
|
| 81 |
+
if (inFlight?.snapshotId === snapshotId) return inFlight.promise;
|
| 82 |
+
|
| 83 |
+
const promise = fetchRemoteDetailsSnapshot(rankingSnapshot);
|
| 84 |
+
const currentRequest = { snapshotId, promise };
|
| 85 |
+
inFlight = currentRequest;
|
| 86 |
+
|
| 87 |
+
try {
|
| 88 |
+
cachedSnapshot = await promise;
|
| 89 |
+
return cachedSnapshot;
|
| 90 |
+
} finally {
|
| 91 |
+
if (inFlight === currentRequest) inFlight = undefined;
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
async function fetchRemoteDetailsSnapshot(
|
| 96 |
+
rankingSnapshot: BucketRankingSnapshot
|
| 97 |
+
): Promise<BucketDetailsSnapshot> {
|
| 98 |
const path = rankingSnapshot.manifest.files.details_matrix;
|
| 99 |
const text = await fetchHfBucketText(path);
|
| 100 |
verifyBucketFileHash(rankingSnapshot.manifest, 'details_matrix', text);
|
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import { env } from '$env/dynamic/private';
|
| 2 |
import type { BucketDataState } from '$lib/types/bucket-state';
|
| 3 |
import type { RankingDataset } from '$lib/types/ranking-data';
|
| 4 |
import { adaptBucketRanking } from './adapt-ranking';
|
|
@@ -7,28 +6,19 @@ import { emptyRankingDataset } from './fallback-data';
|
|
| 7 |
import { toPublicBucketError } from './public-error';
|
| 8 |
import { fetchBucketRankingSnapshot } from './ranking-snapshot';
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
let cached: { value: RankingDataset; snapshotId: string; expiresAt: number } | undefined;
|
| 13 |
let inFlight: Promise<RankingDataset> | undefined;
|
| 14 |
|
| 15 |
-
function cacheTtlMs() {
|
| 16 |
-
const configured = Number(env.HF_BUCKET_CACHE_TTL_MS);
|
| 17 |
-
return Number.isInteger(configured) && configured > 0 ? configured : DEFAULT_CACHE_TTL_MS;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
async function refreshRanking() {
|
| 21 |
-
const snapshot = await fetchBucketRankingSnapshot();
|
| 22 |
if (cached?.snapshotId === snapshot.manifest.snapshot_id) {
|
| 23 |
-
cached.expiresAt = Date.now() + cacheTtlMs();
|
| 24 |
return cached.value;
|
| 25 |
}
|
| 26 |
|
| 27 |
const value = adaptBucketRanking(await fetchBucketDetailsSnapshot(snapshot));
|
| 28 |
cached = {
|
| 29 |
value,
|
| 30 |
-
snapshotId: snapshot.manifest.snapshot_id
|
| 31 |
-
expiresAt: Date.now() + cacheTtlMs()
|
| 32 |
};
|
| 33 |
return value;
|
| 34 |
}
|
|
@@ -40,14 +30,6 @@ export async function getHfBucketRanking() {
|
|
| 40 |
}
|
| 41 |
|
| 42 |
export async function getHfBucketRankingState(): Promise<BucketDataState<RankingDataset>> {
|
| 43 |
-
if (cached && cached.expiresAt > Date.now()) {
|
| 44 |
-
return {
|
| 45 |
-
status: 'ok',
|
| 46 |
-
source: 'cache',
|
| 47 |
-
data: cached.value
|
| 48 |
-
};
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
if (!inFlight) {
|
| 52 |
inFlight = refreshRanking().finally(() => {
|
| 53 |
inFlight = undefined;
|
|
|
|
|
|
|
| 1 |
import type { BucketDataState } from '$lib/types/bucket-state';
|
| 2 |
import type { RankingDataset } from '$lib/types/ranking-data';
|
| 3 |
import { adaptBucketRanking } from './adapt-ranking';
|
|
|
|
| 6 |
import { toPublicBucketError } from './public-error';
|
| 7 |
import { fetchBucketRankingSnapshot } from './ranking-snapshot';
|
| 8 |
|
| 9 |
+
let cached: { value: RankingDataset; snapshotId: string } | undefined;
|
|
|
|
|
|
|
| 10 |
let inFlight: Promise<RankingDataset> | undefined;
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
async function refreshRanking() {
|
| 13 |
+
const snapshot = await fetchBucketRankingSnapshot({ refreshManifest: true });
|
| 14 |
if (cached?.snapshotId === snapshot.manifest.snapshot_id) {
|
|
|
|
| 15 |
return cached.value;
|
| 16 |
}
|
| 17 |
|
| 18 |
const value = adaptBucketRanking(await fetchBucketDetailsSnapshot(snapshot));
|
| 19 |
cached = {
|
| 20 |
value,
|
| 21 |
+
snapshotId: snapshot.manifest.snapshot_id
|
|
|
|
| 22 |
};
|
| 23 |
return value;
|
| 24 |
}
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
export async function getHfBucketRankingState(): Promise<BucketDataState<RankingDataset>> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
if (!inFlight) {
|
| 34 |
inFlight = refreshRanking().finally(() => {
|
| 35 |
inFlight = undefined;
|
|
@@ -90,7 +90,7 @@ function verifySnapshot(snapshot: BucketRankingSnapshot) {
|
|
| 90 |
}
|
| 91 |
|
| 92 |
let cachedSnapshot: BucketRankingSnapshot | undefined;
|
| 93 |
-
let inFlight: Promise<BucketRankingSnapshot> | undefined;
|
| 94 |
|
| 95 |
async function fetchRemoteRankingSnapshot(
|
| 96 |
manifest: HfBucketManifest
|
|
@@ -123,23 +123,26 @@ async function fetchRemoteRankingSnapshot(
|
|
| 123 |
|
| 124 |
export async function fetchBucketRankingSnapshot(options?: {
|
| 125 |
forceRefresh?: boolean;
|
|
|
|
| 126 |
}): Promise<BucketRankingSnapshot> {
|
| 127 |
-
const { manifest } = await getHfBucketManifest({
|
|
|
|
|
|
|
| 128 |
|
| 129 |
if (!options?.forceRefresh && cachedSnapshot?.manifest.snapshot_id === manifest.snapshot_id) {
|
| 130 |
return cachedSnapshot;
|
| 131 |
}
|
| 132 |
|
| 133 |
-
if (
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
})
|
| 139 |
-
.finally(() => {
|
| 140 |
-
inFlight = undefined;
|
| 141 |
-
});
|
| 142 |
-
}
|
| 143 |
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
let cachedSnapshot: BucketRankingSnapshot | undefined;
|
| 93 |
+
let inFlight: { snapshotId: string; promise: Promise<BucketRankingSnapshot> } | undefined;
|
| 94 |
|
| 95 |
async function fetchRemoteRankingSnapshot(
|
| 96 |
manifest: HfBucketManifest
|
|
|
|
| 123 |
|
| 124 |
export async function fetchBucketRankingSnapshot(options?: {
|
| 125 |
forceRefresh?: boolean;
|
| 126 |
+
refreshManifest?: boolean;
|
| 127 |
}): Promise<BucketRankingSnapshot> {
|
| 128 |
+
const { manifest } = await getHfBucketManifest({
|
| 129 |
+
forceRefresh: options?.forceRefresh || options?.refreshManifest
|
| 130 |
+
});
|
| 131 |
|
| 132 |
if (!options?.forceRefresh && cachedSnapshot?.manifest.snapshot_id === manifest.snapshot_id) {
|
| 133 |
return cachedSnapshot;
|
| 134 |
}
|
| 135 |
|
| 136 |
+
if (inFlight?.snapshotId === manifest.snapshot_id) return inFlight.promise;
|
| 137 |
+
|
| 138 |
+
const promise = fetchRemoteRankingSnapshot(manifest);
|
| 139 |
+
const currentRequest = { snapshotId: manifest.snapshot_id, promise };
|
| 140 |
+
inFlight = currentRequest;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
try {
|
| 143 |
+
cachedSnapshot = await promise;
|
| 144 |
+
return cachedSnapshot;
|
| 145 |
+
} finally {
|
| 146 |
+
if (inFlight === currentRequest) inFlight = undefined;
|
| 147 |
+
}
|
| 148 |
}
|
|
@@ -21,7 +21,7 @@ function cacheTtlMs() {
|
|
| 21 |
}
|
| 22 |
|
| 23 |
async function refreshToolsSnapshot() {
|
| 24 |
-
const rankingSnapshot = await fetchBucketRankingSnapshot();
|
| 25 |
if (cached?.snapshotId === rankingSnapshot.manifest.snapshot_id) {
|
| 26 |
cached.expiresAt = Date.now() + cacheTtlMs();
|
| 27 |
return cached.value;
|
|
@@ -62,14 +62,6 @@ export async function getHfBucketToolsSnapshot() {
|
|
| 62 |
async function getHfBucketToolsSnapshotState(): Promise<
|
| 63 |
BucketDataState<BucketVisualizationSnapshot>
|
| 64 |
> {
|
| 65 |
-
if (cached && cached.expiresAt > Date.now()) {
|
| 66 |
-
return {
|
| 67 |
-
status: 'ok',
|
| 68 |
-
source: 'cache',
|
| 69 |
-
data: cached.value
|
| 70 |
-
};
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
if (!inFlight) {
|
| 74 |
inFlight = refreshToolsSnapshot().finally(() => {
|
| 75 |
inFlight = undefined;
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
async function refreshToolsSnapshot() {
|
| 24 |
+
const rankingSnapshot = await fetchBucketRankingSnapshot({ refreshManifest: true });
|
| 25 |
if (cached?.snapshotId === rankingSnapshot.manifest.snapshot_id) {
|
| 26 |
cached.expiresAt = Date.now() + cacheTtlMs();
|
| 27 |
return cached.value;
|
|
|
|
| 62 |
async function getHfBucketToolsSnapshotState(): Promise<
|
| 63 |
BucketDataState<BucketVisualizationSnapshot>
|
| 64 |
> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
if (!inFlight) {
|
| 66 |
inFlight = refreshToolsSnapshot().finally(() => {
|
| 67 |
inFlight = undefined;
|