• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

camatcode / basenji / 1715aa574214f945b9f0cd088379d9f17301f106

21 Jul 2025 02:48PM UTC coverage: 74.417%. Remained the same
1715aa574214f945b9f0cd088379d9f17301f106

push

github

web-flow
Merge pull request #60 from camatcode/work/error-investigation

fix: cbz/cbr when weird file names

11 of 15 new or added lines in 6 files covered. (73.33%)

1085 of 1458 relevant lines covered (74.42%)

398.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.96
/lib/basenji_web/controllers/api/predictive_cache.ex
1
defmodule BasenjiWeb.PredictiveCache do
2
  @moduledoc false
3

4
  use GenServer
5

6
  alias __MODULE__, as: PredictiveCache
7
  alias Basenji.Comic
8
  alias Basenji.Comics
9
  alias Basenji.ImageProcessor
10

11
  require Logger
12

13
  @prefetch_count 4
14
  @prefetch_behind 2
15
  @max_concurrent_prefetches 3
16
  @prefetch_delay 100
17

18
  def start_link(state) do
19
    GenServer.start_link(PredictiveCache, state, name: PredictiveCache)
1✔
20
  end
21

22
  def init(_opts) do
1✔
23
    {:ok,
24
     %{
25
       active_prefetches: %{},
26
       completed_prefetch_count: 0
27
     }}
28
  end
29

30
  def get_state do
31
    GenServer.call(PredictiveCache, :state)
2✔
32
  end
33

34
  def get_comic_page_from_cache(%Comic{} = comic, page_num, opts \\ []) do
35
    result = fetch_page_from_cache(comic, page_num, opts)
20✔
36

37
    prefetch_next_pages(comic, page_num, opts)
20✔
38

39
    result
20✔
40
  end
41

42
  def fetch_page_from_cache(%Comic{id: comic_id} = comic, page_num, opts) do
43
    Cachex.fetch(
44
      :basenji_cache,
45
      page_cache_key(comic_id, page_num, opts),
46
      fn _key ->
NEW
47
        with {:ok, page, _mime} <- Comics.get_page(comic, page_num) do
×
48
          ImageProcessor.resize_image(page, opts)
29✔
49
        end
50
        |> case do
29✔
51
          {:ok, page} ->
52
            {:commit, {page, "image/jpeg"}, [ttl: to_timeout(minute: 1)]}
29✔
53

NEW
54
          {_, resp} ->
×
55
            {:ignore, {:error, resp}}
56
        end
57
      end
58
    )
59
    |> case do
44✔
NEW
60
      {:ignore, {:error, error}} ->
×
61
        {:error, error}
62

63
      {_, {page, mime}} ->
64
        {:ok, page, mime}
44✔
65

66
      response ->
NEW
67
        response
×
68
    end
69
  end
70

71
  # for testing / probing
72
  def handle_call(:state, _, state) do
73
    {:reply, state, state}
2✔
74
  end
75

76
  def handle_cast({:prefetch, comic, current_page, opts}, state) do
77
    Process.send_after(self(), {:do_prefetch, comic, current_page, opts}, @prefetch_delay)
20✔
78
    {:noreply, state}
79
  end
80

81
  def handle_cast({:prefetch_complete, prefetch_key}, state) do
82
    prefetches = Map.delete(state.active_prefetches, prefetch_key)
18✔
83
    count = state.completed_prefetch_count + 1
18✔
84
    {:noreply, %{active_prefetches: prefetches, completed_prefetch_count: count}}
85
  end
86

87
  def handle_info({:do_prefetch, comic, current_page, opts}, state) do
88
    prefetch_key = "#{comic.id}_#{current_page}_#{inspect(opts)}"
20✔
89

90
    if Map.has_key?(state.active_prefetches, prefetch_key) do
20✔
91
      {:noreply, state}
92
    else
93
      task =
18✔
94
        Task.start(fn ->
95
          prefetch_pages(comic, current_page, opts)
18✔
96
          GenServer.cast(PredictiveCache, {:prefetch_complete, prefetch_key})
18✔
97
        end)
98

99
      new_state = Map.put(state, :active_prefetches, Map.put(state.active_prefetches, prefetch_key, task))
18✔
100
      {:noreply, new_state}
101
    end
102
  end
103

104
  defp prefetch_pages(comic, current_page, opts) do
105
    max_page = comic.page_count
18✔
106

107
    forward_pages = calculate_forward_pages(current_page, max_page)
18✔
108
    backward_pages = calculate_backward_pages(current_page)
18✔
109

110
    all_pages = forward_pages ++ backward_pages
18✔
111

112
    Logger.debug("Prefetching pages #{inspect(all_pages)} for comic #{comic.id} with #{inspect(opts)}")
18✔
113

114
    all_pages
115
    |> Task.async_stream(
116
      fn page_num -> prefetch_single_page(comic, page_num, opts) end,
50✔
117
      max_concurrency: @max_concurrent_prefetches,
118
      timeout: 30_000
119
    )
120
    |> Stream.run()
18✔
121
  end
122

123
  defp calculate_forward_pages(current_page, max_page) do
124
    (current_page + 1)..(current_page + @prefetch_count)
125
    |> Enum.to_list()
126
    |> Enum.filter(&(&1 <= max_page))
18✔
127
  end
128

129
  defp calculate_backward_pages(current_page) do
130
    start_page = max(1, current_page - @prefetch_behind)
18✔
131
    end_page = current_page - 1
18✔
132

133
    if end_page >= start_page do
18✔
134
      start_page..end_page
135
      |> Enum.to_list()
136
      |> Enum.reverse()
11✔
137
    else
138
      []
139
    end
140
  end
141

142
  defp prefetch_single_page(comic, page_num, opts) do
143
    cache_key = page_cache_key(comic.id, page_num, opts)
50✔
144

145
    case Cachex.exists?(:basenji_cache, cache_key) do
50✔
146
      {:ok, false} ->
147
        case fetch_page_from_cache(comic, page_num, opts) do
23✔
148
          {:ok, _data, _mime} ->
149
            Logger.debug("Prefetched page #{page_num} for comic #{comic.id}")
23✔
150
            :ok
151

152
          _ ->
×
153
            :error
154
        end
155

156
      {:ok, true} ->
27✔
157
        :cached
158

159
      error ->
160
        Logger.warning("Cache check failed for page #{page_num}: #{inspect(error)}")
×
161
        :error
162
    end
163
  end
164

165
  defp page_cache_key(comic_id, page_num, opts) do
166
    %{comic_id: comic_id, page_num: page_num, optimized: true, opts: opts}
94✔
167
  end
168

169
  defp prefetch_next_pages(%Comic{} = comic, current_page, opts) do
170
    GenServer.cast(PredictiveCache, {:prefetch, comic, current_page, opts})
20✔
171
  end
172
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc