Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 43 additions & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Credo runs in strict mode, so `mix credo` locally reports exactly what CI
# reports. The settings below are the ones this repository pins on purpose;
# everything else is Credo's own default.
#
# Waived checks carry the reason and the number of sites they fired on when the
# waiver was written. The list is allowed to shrink and not to grow.
%{
configs: [
%{
name: "default",
strict: true,
files: %{
included: ["lib/", "src/", "test/", "config/", "mix.exs"],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
checks: %{
extra: [
# Classic McCabe cyclomatic complexity, at the ceiling shared by every
# project in this family, in Elixir and in JavaScript alike.
{Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 9},
# Credo's strict default, or `line_length` from .formatter.exs where
# that is larger, so the formatter and the linter cannot disagree
# about a line the formatter itself produced.
{Credo.Check.Readability.MaxLineLength, max_length: 150}
],
disabled: [
# TODO and FIXME notes are tracked in the issue tracker. Failing a
# build on one only encourages deleting the note.
{Credo.Check.Design.TagTODO, []},
{Credo.Check.Design.TagFIXME, []},
# 109 sites. Whether a call is written out or aliased is a question of
# naming, not of complexity, and a sweep would touch most files in the
# tree at once. Worth doing as a change of its own.
{Credo.Check.Design.AliasUsage, []},
# 1 site: `Beacon.Config`, at 42 fields. That struct is the site
# configuration; every field is one documented option, and splitting
# it would change the public API rather than reduce anything.
{Credo.Check.Warning.StructFieldAmount, []}
]
}
}
]
}
33 changes: 33 additions & 0 deletions .github/workflows/javascript.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: JavaScript

on:
push:
branches:
- main
paths:
- 'assets/**'
- '.github/workflows/javascript.yml'
pull_request:
paths:
- 'assets/**'
- '.github/workflows/javascript.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'

# Run straight from the registry: oxlint is a single binary, and nothing
# else in `assets/` is needed to lint it.
- run: npx --yes oxlint@1.80.0
working-directory: assets
2 changes: 2 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ jobs:
- run: mix compile --warnings-as-errors

- run: mix deps.unlock --check-unused

- run: mix credo --strict
15 changes: 0 additions & 15 deletions assets/.eslintrc.js

This file was deleted.

27 changes: 27 additions & 0 deletions assets/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
// Every JavaScript file this repository authors, at oxlint's strict
// categories. `mix assets.lint` and the `javascript.yml` workflow both run
// oxlint from this directory, so a local run and the CI run report the same
// thing.
//
// Rules below are either configured (the rule stays on, with the option that
// matches how this repository writes) or waived, with the reason and the
// number of sites it fired on when it was written down. The waivers shrink.
"plugins": ["import", "node", "oxc", "promise", "unicorn"],
"categories": {
"correctness": "error",
"pedantic": "error",
"perf": "error",
"suspicious": "error"
},
"ignorePatterns": ["node_modules/**", "vendor/**"],
"rules": {
// Classic McCabe complexity, at the ceiling `.credo.exs` enforces on the
// Elixir in this repository and every sibling project enforces on both.
"eslint/complexity": ["error", { "max": 9, "variant": "classic" }],
// A line count is not a complexity measure; `complexity` above is.
"eslint/max-lines": "off",
"eslint/max-lines-per-function": "off",
"eslint/no-inline-comments": "off"
}
}
10 changes: 5 additions & 5 deletions assets/js/beacon.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
// 2. phoenix js loaded from the host application

window.addEventListener("phx:beacon:css-ready", (e) => {
let link = document.getElementById("beacon-runtime-stylesheet")
let link = document.querySelector("#beacon-runtime-stylesheet")
if (link) {
link.href = e.detail.href
}
})

window.addEventListener("phx:beacon:page-updated", (e) => {
if (e.detail.hasOwnProperty("runtime_css_path")) {
document.getElementById("beacon-runtime-stylesheet").href = e.detail.runtime_css_path
if (Object.prototype.hasOwnProperty.call(e.detail, "runtime_css_path")) {
document.querySelector("#beacon-runtime-stylesheet").href = e.detail.runtime_css_path
}

if (e.detail.hasOwnProperty("meta_tags")) {
if (Object.prototype.hasOwnProperty.call(e.detail, "meta_tags")) {
// remove current tags, except csrf-token
document.querySelectorAll("meta:not([name='csrf-token'])").forEach((el) => el.remove())

Expand All @@ -28,7 +28,7 @@ window.addEventListener("phx:beacon:page-updated", (e) => {
newMetaTag.setAttribute(key, metaTag[key])
})

document.getElementsByTagName("head")[0].appendChild(newMetaTag)
document.head.append(newMetaTag)
})
}
})
Expand Down
4 changes: 3 additions & 1 deletion assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"repository": {},
"scripts": {
"format": "prettier --write .",
"format-check": "prettier --check ."
"format-check": "prettier --check .",
"lint": "oxlint"
},
"devDependencies": {
"oxlint": "^1.80",
"prettier": "^3.2"
},
"dependencies": {
Expand Down
28 changes: 14 additions & 14 deletions lib/beacon/actions/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -306,20 +306,7 @@ defmodule Beacon.Actions.Interpreter do
end

defp evaluate_test(%{"path" => path, "op" => op, "value" => expected}, ctx) do
actual = resolve_value("$" <> path, ctx)

case op do
"eq" -> actual == expected
"neq" -> actual != expected
"gt" -> is_number(actual) and actual > expected
"lt" -> is_number(actual) and actual < expected
"gte" -> is_number(actual) and actual >= expected
"lte" -> is_number(actual) and actual <= expected
"contains" -> is_binary(actual) and String.contains?(actual, expected)
"exists" -> actual != nil
"not_exists" -> actual == nil
_ -> false
end
compare(op, resolve_value("$" <> path, ctx), expected)
end

defp evaluate_test(%{"field" => field, "op" => op, "value" => expected}, ctx) do
Expand All @@ -328,10 +315,23 @@ defmodule Beacon.Actions.Interpreter do

defp evaluate_test(_, _ctx), do: false

defp compare("eq", actual, expected), do: actual == expected
defp compare("neq", actual, expected), do: actual != expected
defp compare("gt", actual, expected), do: is_number(actual) and actual > expected
defp compare("lt", actual, expected), do: is_number(actual) and actual < expected
defp compare("gte", actual, expected), do: is_number(actual) and actual >= expected
defp compare("lte", actual, expected), do: is_number(actual) and actual <= expected
defp compare("contains", actual, expected), do: is_binary(actual) and String.contains?(actual, expected)
defp compare("exists", actual, _expected), do: actual != nil
defp compare("not_exists", actual, _expected), do: actual == nil
defp compare(_op, _actual, _expected), do: false

defp get_nested(nil, _), do: nil
defp get_nested(value, []), do: value

defp get_nested(value, [key | rest]) when is_map(value) do
get_nested(Map.get(value, key) || Map.get(value, String.to_atom(key)), rest)
end

defp get_nested(_, _), do: nil
end
58 changes: 31 additions & 27 deletions lib/beacon/beacon.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,42 +106,46 @@ defmodule Beacon do
[]
end

site_children =
Enum.reduce(sites, [], fn opts, acc ->
config = Beacon.Config.new(opts)
site_children = Enum.reduce(sites, [], &maybe_start_site/2)

if Beacon.Config.env_test?() do
[site_child_spec(config) | acc]
else
# we only care about starting sites that are valid and reachable
case Beacon.Router.reachable(config) do
{:ok, _} ->
[site_child_spec(config) | acc]
Supervisor.init(finch_children ++ vault_children ++ site_children, strategy: :one_for_one)
end

{:error, {endpoint, host}} ->
Logger.warning("""
site #{config.site} is not reachable on host #{host} and will not be started
# We only care about starting sites that are valid and reachable.
defp maybe_start_site(opts, acc) do
config = Beacon.Config.new(opts)

Check both the Router and #{inspect(endpoint)} configuratation
if Beacon.Config.env_test?() do
[site_child_spec(config) | acc]
else
case Beacon.Router.reachable(config) do
{:ok, _} -> [site_child_spec(config) | acc]
{:error, {endpoint, host}} -> warn_unreachable(config, endpoint, host, acc)
:error -> warn_invalid(config, acc)
end
end
end

defp warn_unreachable(config, endpoint, host, acc) do
Logger.warning("""
site #{config.site} is not reachable on host #{host} and will not be started

See https://hexdocs.pm/beacon/troubleshooting.html for more info.
""")
Check both the Router and #{inspect(endpoint)} configuratation

acc
See https://hexdocs.pm/beacon/troubleshooting.html for more info.
""")

:error ->
Logger.warning("""
site #{config.site} is not reachable or is invalid, it will not be started
acc
end

See https://hexdocs.pm/beacon/troubleshooting.html for more info.
""")
defp warn_invalid(config, acc) do
Logger.warning("""
site #{config.site} is not reachable or is invalid, it will not be started

acc
end
end
end)
See https://hexdocs.pm/beacon/troubleshooting.html for more info.
""")

Supervisor.init(finch_children ++ vault_children ++ site_children, strategy: :one_for_one)
acc
end

defp site_child_spec(%Beacon.Config{} = config) do
Expand Down
26 changes: 14 additions & 12 deletions lib/beacon/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,15 @@ defmodule Beacon.Cache do

:ets.foldl(
fn
{_key, {:__loading__, _, _}}, acc -> acc
{_key, {:__loading__, _, _}}, acc ->
acc

{key, {_value, inserted_at}}, acc when inserted_at < cutoff ->
:ets.delete(table, key)
acc + 1
_, acc -> acc

_, acc ->
acc
end,
0,
table
Expand All @@ -90,16 +94,14 @@ defmodule Beacon.Cache do
end

defp run_load(table, key, ref, load_fun) do
try do
value = load_fun.()
:ets.insert(table, {key, {value, System.monotonic_time(:second)}})
value
catch
kind, reason ->
# Clean up only OUR sentinel
:ets.match_delete(table, {key, {:__loading__, ref, self()}})
:erlang.raise(kind, reason, __STACKTRACE__)
end
value = load_fun.()
:ets.insert(table, {key, {value, System.monotonic_time(:second)}})
value
catch
kind, reason ->
# Clean up only OUR sentinel
:ets.match_delete(table, {key, {:__loading__, ref, self()}})
:erlang.raise(kind, reason, __STACKTRACE__)
end

defp await_result(table, key, ref, loader_pid, load_fun, ttl) do
Expand Down
23 changes: 12 additions & 11 deletions lib/beacon/circuit_breaker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@ defmodule Beacon.CircuitBreaker do
:ok
else
case :ets.lookup(@table, {site, path}) do
[{_, tripped_at, ttl}] ->
elapsed = System.monotonic_time(:second) - tripped_at
[{_, tripped_at, ttl}] -> check_elapsed(site, path, tripped_at, ttl)
[] -> :ok
end
end
end

if elapsed < ttl do
{:tripped, ttl - elapsed}
else
:ets.delete(@table, {site, path})
:ok
end
defp check_elapsed(site, path, tripped_at, ttl) do
elapsed = System.monotonic_time(:second) - tripped_at

[] ->
:ok
end
if elapsed < ttl do
{:tripped, ttl - elapsed}
else
:ets.delete(@table, {site, path})
:ok
end
end

Expand Down
Loading
Loading