Skip to content

Commit dc8b293

Browse files
committed
Surface documented crates on docs.forc.pub and warm docs loads
1 parent b266e88 commit dc8b293

File tree

5 files changed

+131
-17
lines changed

5 files changed

+131
-17
lines changed

app/src/features/detail/components/PackageSidebar.tsx

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useEffect, useRef } from "react";
44
import { useRouter } from "next/navigation";
55
import {
66
Box,
@@ -32,6 +32,7 @@ interface PackageSidebarProps {
3232
}
3333

3434
const PackageSidebar = ({ data, loading, error }: PackageSidebarProps) => {
35+
const warmedDocsRef = useRef<Set<string>>(new Set());
3536
const router = useRouter();
3637
const docsRelativeUrl =
3738
data?.docsIpfsUrl && data.name && data.version
@@ -49,6 +50,74 @@ const PackageSidebar = ({ data, loading, error }: PackageSidebarProps) => {
4950
: docsRelativeUrl
5051
: "";
5152

53+
useEffect(() => {
54+
const docKey =
55+
data?.docsIpfsUrl && data?.name && data?.version
56+
? `${data.name}@${data.version}`
57+
: null;
58+
59+
if (!docKey || !docsRelativeUrl || warmedDocsRef.current.has(docKey)) {
60+
return;
61+
}
62+
63+
const controller = new AbortController();
64+
let isMounted = true;
65+
const baseDocsUrl = docsRelativeUrl.endsWith("/")
66+
? docsRelativeUrl.slice(0, -1)
67+
: docsRelativeUrl;
68+
69+
const warmDocs = async () => {
70+
try {
71+
const response = await fetch(baseDocsUrl, {
72+
signal: controller.signal,
73+
credentials: "same-origin",
74+
cache: "force-cache",
75+
headers: {
76+
"x-docs-prefetch": "1",
77+
},
78+
});
79+
80+
if (!response.ok) {
81+
throw new Error(`Docs prefetch failed with status ${response.status}`);
82+
}
83+
84+
// Warm the search index in the background; ignore any errors.
85+
fetch(`${baseDocsUrl}/search.js`, {
86+
signal: controller.signal,
87+
credentials: "same-origin",
88+
cache: "force-cache",
89+
headers: {
90+
"x-docs-prefetch": "1",
91+
},
92+
}).catch(() => undefined);
93+
94+
if (isMounted) {
95+
warmedDocsRef.current.add(docKey);
96+
if (process.env.NODE_ENV !== "production") {
97+
console.debug(`Prefetched docs for ${docKey}`);
98+
}
99+
}
100+
} catch (prefetchError) {
101+
if (
102+
controller.signal.aborted ||
103+
(prefetchError as Error).name === "AbortError"
104+
) {
105+
return;
106+
}
107+
if (process.env.NODE_ENV !== "production") {
108+
console.debug("Docs prefetch failed", prefetchError);
109+
}
110+
}
111+
};
112+
113+
warmDocs();
114+
115+
return () => {
116+
isMounted = false;
117+
controller.abort();
118+
};
119+
}, [data?.docsIpfsUrl, data?.name, data?.version, docsRelativeUrl]);
120+
52121
if (loading) {
53122
return (
54123
<Box

src/api/search.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,17 @@ use url::Url;
88
#[derive(Serialize, Debug)]
99
#[serde(rename_all = "camelCase")]
1010
pub struct RecentPackagesResponse {
11-
pub recently_created: Vec<PackagePreview>,
12-
pub recently_updated: Vec<PackagePreview>,
11+
pub recently_created: Vec<RecentPackage>,
12+
pub recently_updated: Vec<RecentPackage>,
13+
}
14+
15+
#[derive(Serialize, Debug, Clone)]
16+
#[serde(rename_all = "camelCase")]
17+
pub struct RecentPackage {
18+
#[serde(flatten)]
19+
pub package: PackagePreview,
20+
#[serde(skip_serializing_if = "Option::is_none")]
21+
pub docs_ipfs_url: Option<String>,
1322
}
1423

1524
#[derive(Serialize, Debug)]

src/db/package_version.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::api::pagination::{PaginatedResponse, Pagination};
44
use crate::handlers::publish::PublishInfo;
55
use crate::models::{
66
ApiToken, AuthorInfo, CountResult, FullPackage, FullPackageWithCategories, PackagePreview,
7-
PackagePreviewWithCategories, PackageVersionInfo,
7+
PackagePreviewWithCategories, PackagePreviewWithDocsHash, PackageVersionInfo,
88
};
99
use chrono::{DateTime, Utc};
1010
use diesel::prelude::*;
@@ -136,7 +136,9 @@ impl DbConn<'_> {
136136
}
137137

138138
/// Fetch the most recently updated packages.
139-
pub fn get_recently_updated(&mut self) -> Result<Vec<PackagePreview>, DatabaseError> {
139+
pub fn get_recently_updated(
140+
&mut self,
141+
) -> Result<Vec<PackagePreviewWithDocsHash>, DatabaseError> {
140142
let packages = diesel::sql_query(
141143
r#"WITH ranked_versions AS (
142144
SELECT
@@ -146,30 +148,35 @@ impl DbConn<'_> {
146148
pv.package_description AS description,
147149
p.created_at AS created_at,
148150
pv.created_at AS updated_at,
151+
u.docs_ipfs_hash AS docs_ipfs_hash,
149152
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pv.created_at DESC) AS rank
150153
FROM package_versions pv
151154
JOIN packages p ON pv.package_id = p.id
155+
JOIN uploads u ON pv.upload_id = u.id
152156
)
153157
SELECT
154158
name,
155159
version,
156160
description,
157161
created_at,
158-
updated_at
162+
updated_at,
163+
docs_ipfs_hash
159164
FROM ranked_versions
160165
WHERE rank = 1
161166
ORDER BY updated_at DESC
162167
LIMIT 10;
163168
"#,
164169
)
165-
.load::<PackagePreview>(self.inner())
170+
.load::<PackagePreviewWithDocsHash>(self.inner())
166171
.map_err(|err| DatabaseError::QueryFailed("recently updated".to_string(), err))?;
167172

168173
Ok(packages)
169174
}
170175

171176
/// Fetch the [PackagePreview]s of the most recently created packages.
172-
pub fn get_recently_created(&mut self) -> Result<Vec<PackagePreview>, DatabaseError> {
177+
pub fn get_recently_created(
178+
&mut self,
179+
) -> Result<Vec<PackagePreviewWithDocsHash>, DatabaseError> {
173180
let packages = diesel::sql_query(
174181
r#"WITH ranked_versions AS (
175182
SELECT
@@ -179,23 +186,26 @@ impl DbConn<'_> {
179186
pv.package_description AS description,
180187
p.created_at AS created_at,
181188
pv.created_at AS updated_at,
189+
u.docs_ipfs_hash AS docs_ipfs_hash,
182190
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pv.created_at DESC) AS rank
183191
FROM package_versions pv
184192
JOIN packages p ON pv.package_id = p.id
193+
JOIN uploads u ON pv.upload_id = u.id
185194
)
186195
SELECT
187196
name,
188197
version,
189198
description,
190199
created_at,
191-
updated_at
200+
updated_at,
201+
docs_ipfs_hash
192202
FROM ranked_versions
193203
WHERE rank = 1
194204
ORDER BY created_at DESC
195205
LIMIT 10;
196206
"#,
197207
)
198-
.load::<PackagePreview>(self.inner())
208+
.load::<PackagePreviewWithDocsHash>(self.inner())
199209
.map_err(|err| DatabaseError::QueryFailed("recently created".to_string(), err))?;
200210

201211
Ok(packages)

src/main.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use chrono::{DateTime, Utc};
77
use forc_pub::api::api_token::{CreateTokenRequest, CreateTokenResponse, Token, TokensResponse};
88
use forc_pub::api::pagination::{PaginatedResponse, Pagination};
99
use forc_pub::api::publish::{PublishRequest, PublishResponse, UploadResponse};
10-
use forc_pub::api::search::{DownloadLinksResponse, FullPackage, RecentPackagesResponse};
10+
use forc_pub::api::search::{
11+
DownloadLinksResponse, FullPackage, RecentPackage, RecentPackagesResponse,
12+
};
1113
use forc_pub::api::ApiError;
1214
use forc_pub::api::{
1315
auth::{LoginRequest, LoginResponse, UserResponse},
@@ -17,7 +19,7 @@ use forc_pub::db::error::DatabaseError;
1719
use forc_pub::db::Database;
1820
use forc_pub::file_uploader::s3::{ipfs_hash_to_s3_url, S3Client, S3ClientImpl};
1921
use forc_pub::file_uploader::{
20-
pinata::{PinataClient, PinataClientImpl},
22+
pinata::{ipfs_hash_to_docs_url, PinataClient, PinataClientImpl},
2123
FileUploader,
2224
};
2325
use forc_pub::github::handle_login;
@@ -27,7 +29,8 @@ use forc_pub::middleware::cors::Cors;
2729
use forc_pub::middleware::session_auth::{SessionAuth, SESSION_COOKIE_NAME};
2830
use forc_pub::middleware::token_auth::TokenAuth;
2931
use forc_pub::models::{
30-
FullPackageWithCategories, PackagePreviewWithCategories, PackageVersionInfo,
32+
FullPackageWithCategories, PackagePreviewWithCategories, PackagePreviewWithDocsHash,
33+
PackageVersionInfo,
3134
};
3235
use forc_pub::util::{load_env, validate_or_format_semver};
3336
use rocket::http::Status;
@@ -400,11 +403,28 @@ fn recent_packages(db: &State<Database>) -> ApiResult<RecentPackagesResponse> {
400403
Ok::<_, DatabaseError>((recently_created, recently_updated))
401404
})?;
402405
Ok(Json(RecentPackagesResponse {
403-
recently_created,
404-
recently_updated,
406+
recently_created: map_recent_packages(recently_created),
407+
recently_updated: map_recent_packages(recently_updated),
405408
}))
406409
}
407410

411+
fn map_recent_packages(packages: Vec<PackagePreviewWithDocsHash>) -> Vec<RecentPackage> {
412+
packages
413+
.into_iter()
414+
.map(|pkg| {
415+
let docs_ipfs_url = pkg
416+
.docs_ipfs_hash
417+
.filter(|hash| !hash.is_empty())
418+
.map(|hash| ipfs_hash_to_docs_url(&hash));
419+
420+
RecentPackage {
421+
package: pkg.package,
422+
docs_ipfs_url,
423+
}
424+
})
425+
.collect()
426+
}
427+
408428
/// Catches all OPTION requests in order to get the CORS related Fairing triggered.
409429
#[options("/<_..>")]
410430
fn all_options() {
@@ -484,8 +504,6 @@ async fn get_package_docs(
484504
name: String,
485505
version: String,
486506
) -> Result<Redirect, Status> {
487-
use forc_pub::file_uploader::pinata::ipfs_hash_to_docs_url;
488-
489507
let package_result =
490508
db.transaction(|conn| conn.get_full_package_with_categories(name.clone(), version.clone()));
491509

src/models.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ pub struct PackagePreview {
218218
pub updated_at: DateTime<Utc>,
219219
}
220220

221+
#[derive(QueryableByName, Debug, Clone)]
222+
pub struct PackagePreviewWithDocsHash {
223+
#[diesel(embed)]
224+
pub package: PackagePreview,
225+
#[diesel(sql_type = Nullable<Text>)]
226+
pub docs_ipfs_hash: Option<String>,
227+
}
228+
221229
#[derive(Serialize, Debug, Clone)]
222230
#[serde(rename_all = "camelCase")]
223231
pub struct PackagePreviewWithCategories {

0 commit comments

Comments
 (0)