Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/hexdocs.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ fn update(model: Model, msg: Msg) {
msg.ApiReturnedPackages(response) -> api_returned_packages(model, response)
msg.ApiReturnedTypesenseSearch(response) ->
api_returned_typesense_search(model, response)
msg.ApiReturnedInitialLatestPackages(versions) ->
api_returned_initial_latest_packages(model, versions)

msg.DocumentChangedLocation(location:) ->
model.update_route(model, location)
Expand Down Expand Up @@ -161,6 +163,18 @@ fn api_returned_typesense_search(model: Model, response: Loss(decode.Dynamic)) {
|> result.unwrap(#(model, effect.none()))
}

fn api_returned_initial_latest_packages(
model: Model,
versions: Loss(List(hexpm.Package)),
) -> #(Model, Effect(a)) {
case versions {
Error(_) -> #(model, toast.error("Server error. Retry later."))
Copy link
Member

@josevalim josevalim Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to mark that this package has no version in the error case. So we can go and mark it as not found later on. See comment below.

Ok(versions) ->
model.add_packages_versions(model, versions)
|> model.replace_search_packages
}
}

fn document_registered_event_listener(model: Model, unsubscriber: fn() -> Nil) {
let dom_click_unsubscriber = Some(unsubscriber)
Model(..model, dom_click_unsubscriber:)
Expand Down
32 changes: 31 additions & 1 deletion src/hexdocs/data/model.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,44 @@ pub fn update_route(model: Model, route: uri.Uri) {
case string.is_empty(q) {
True -> #(set_search_results(model, #(-1, [])), effect.none())
False -> {
let latest = list.filter(packages, fn(p) { p.1 == "latest" })
Model(..model, search_input: q, search_packages_filters: packages)
|> pair.new(effects.typesense_search(q, packages))
|> pair.new({
case latest {
[] -> effects.typesense_search(q, packages)
latest -> {
latest
|> list.map(pair.first)
|> effects.initial_latest_packages
}
}
})
}
}
}
}
}

pub fn replace_search_packages(model: Model) {
let model =
Model(..model, search_packages_filters: {
use #(package, version) <- list.map(model.search_packages_filters)
use <- bool.guard(when: version != "latest", return: #(package, version))
case dict.get(model.packages_versions, package) {
Error(_) -> #(package, version)
Ok(versions) -> {
case versions.releases {
[] -> #(package, version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should mark it as "not found" or something? Otherwise the version will be "latest" forever, which means the search never runs.

Suggested change
[] -> #(package, version)
[] -> #(package, "not found")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think this branch is not possible. All packages will have at least one version... but I posted a comment above to add packages with zero versions, so we can mark them as "not found" (or alternatively as "0.0.0"). Otherwise the search never runs if one package fails.

[release, ..] -> #(package, release.version)
}
}
}
})
route.Search(q: model.search_input, packages: model.search_packages_filters)
|> route.replace
|> pair.new(model, _)
}

pub fn select_autocomplete_option(model: Model, package: String) {
case model.autocomplete, model.route {
None, _ -> model
Expand Down
5 changes: 5 additions & 0 deletions src/hexdocs/data/model/route.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ pub fn push(route: Route) {
modem.push(route.path, route.query, route.fragment)
}

pub fn replace(route: Route) {
let route = to_uri(route)
modem.replace(route.path, route.query, route.fragment)
}

fn create_query(
query: List(#(String, String)),
packages: List(#(String, String)),
Expand Down
4 changes: 2 additions & 2 deletions src/hexdocs/data/model/version.gleam
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import gleam/option.{type Option, None, Some}
import gleam/regexp

const version_regexp = "^#([a-zA-Z_0-9]+)(:(([0-9]+|\\.){1,5}))?"
const version_regexp = "^#([a-zA-Z_0-9]+)(:((([0-9]+|\\.){1,5})|latest))?"

pub fn match_package(word: String) -> Result(#(String, Option(String)), Nil) {
let regexp = version_search()
case regexp.scan(regexp, word) {
case regexp.scan(regexp, word) |> echo {
[regexp.Match(content: _, submatches:)] -> {
case submatches {
[Some(package), _, Some(version), ..] -> Ok(#(package, Some(version)))
Expand Down
1 change: 1 addition & 0 deletions src/hexdocs/data/msg.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub type Msg {
ApiReturnedPackages(Loss(String))
ApiReturnedTypesenseSearch(Loss(Dynamic))
ApiReturnedPackagesVersions(packages: Loss(List(hexpm.Package)))
ApiReturnedInitialLatestPackages(versions: Loss(List(hexpm.Package)))

// Application messages.
DocumentChangedLocation(location: uri.Uri)
Expand Down
23 changes: 23 additions & 0 deletions src/hexdocs/effects.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import browser/document
import gleam/function
import gleam/http/response.{type Response}
import gleam/javascript/promise
import gleam/list
import gleam/result
import hexdocs/data/msg
import hexdocs/loss.{type Loss}
import hexdocs/services/hex
Expand All @@ -24,6 +26,27 @@ pub fn package_versions(package: String) {
dispatch(msg.ApiReturnedPackageVersions(response:))
}

pub fn initial_latest_packages(packages: List(String)) {
use dispatch <- effect.from()
use _ <- function.tap(Nil)
use response <- promise.map({
promise.await_list({
use package <- list.map(packages)
hex.package_versions(package)
})
})
let versions = case result.all(response) {
Error(error) -> Error(error)
Ok(response) -> {
case list.any(response, fn(r) { r.status != 200 }) {
True -> Error(loss.HttpError)
False -> Ok(list.map(response, fn(r) { r.body }))
}
}
}
dispatch(msg.ApiReturnedInitialLatestPackages(versions:))
}

pub fn subscribe_blurred_search() {
use dispatch <- effect.from()
document.add_listener(fn() { dispatch(msg.UserBlurredSearch) })
Expand Down
6 changes: 5 additions & 1 deletion src/hexdocs/view/search.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ pub fn search(model: Model) {
"self-stretch justify-start text-slate-700 dark:text-slate-400 text-sm font-normal leading-none",
),
],
[html.text(version)],
// You can add any loader you want here.
case version {
"latest" -> [html.text("Loading…")]
version -> [html.text(version)]
},
),
],
),
Expand Down