Skip to content

Commit 9a47548

Browse files
authored
Lazy load diffs (#105)
1 parent 9d63d2a commit 9a47548

36 files changed

+1792
-859
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ FROM debian:${DEBIAN_VERSION} AS app
4848

4949
RUN apt update && \
5050
apt upgrade -y && \
51-
apt install --no-install-recommends -y bash openssl git && \
51+
apt install --no-install-recommends -y bash openssl git ca-certificates && \
5252
apt clean -y && rm -rf /var/lib/apt/lists/*
5353

5454
RUN mkdir /app

assets/css/app.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,62 @@ table.package-list .button {
382382
margin-bottom: 0;
383383
}
384384

385+
.diff-stats-header {
386+
margin: 15px 0;
387+
padding: 10px 0;
388+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
389+
}
390+
391+
.diff-stats-header span {
392+
margin-right: 15px;
393+
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;
394+
font-size: 14px;
395+
}
396+
397+
.diff-stats-header .files-changed {
398+
color: #ddd;
399+
}
400+
401+
.diff-stats-header .additions {
402+
color: #4CAF50;
403+
font-weight: 600;
404+
}
405+
406+
.diff-stats-header .deletions {
407+
color: #f44336;
408+
font-weight: 600;
409+
}
410+
411+
.loading-spinner {
412+
height: 60px;
413+
width: 100%;
414+
margin: 30px 0;
415+
display: flex;
416+
align-items: center;
417+
justify-content: center;
418+
color: #888;
419+
font-size: 14px;
420+
opacity: 0.8;
421+
}
422+
423+
.loading-spinner::before {
424+
content: '';
425+
width: 20px;
426+
height: 20px;
427+
border: 2px solid #f3f3f3;
428+
border-top: 2px solid #888;
429+
border-radius: 50%;
430+
animation: spin 1s linear infinite;
431+
margin-right: 10px;
432+
}
433+
434+
@keyframes spin {
435+
0% { transform: rotate(0deg); }
436+
100% { transform: rotate(360deg); }
437+
}
438+
439+
440+
385441
@media only screen
386442
and (min-device-width: 320px)
387443
and (max-device-width: 480px) {

assets/js/app.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,53 @@ import "phoenix_html"
1414
import { Socket } from 'phoenix'
1515
import { LiveSocket } from "phoenix_live_view"
1616

17-
let liveSocket = new LiveSocket("/live", Socket, {})
17+
// Define hooks for LiveView
18+
window.Hooks = {}
19+
20+
window.Hooks.InfiniteScroll = {
21+
mounted() {
22+
this.pending = false
23+
24+
this.observer = new IntersectionObserver((entries) => {
25+
const target = entries[0]
26+
if (target.isIntersecting && !this.pending) {
27+
this.pending = true
28+
this.pushEvent("load-more", {})
29+
}
30+
}, {
31+
root: null,
32+
rootMargin: '100px',
33+
threshold: 0.1
34+
})
35+
36+
this.observer.observe(this.el)
37+
},
38+
39+
destroyed() {
40+
if (this.observer) {
41+
this.observer.disconnect()
42+
}
43+
},
44+
45+
updated() {
46+
this.pending = false
47+
48+
// Check if we're still at the bottom after loading content
49+
// Use requestAnimationFrame to ensure DOM has fully updated
50+
requestAnimationFrame(() => {
51+
const target = this.el
52+
const rect = target.getBoundingClientRect()
53+
const isIntersecting = rect.top <= (window.innerHeight || document.documentElement.clientHeight)
54+
55+
if (isIntersecting && !this.pending) {
56+
this.pending = true
57+
this.pushEvent("load-more", {})
58+
}
59+
})
60+
}
61+
}
62+
63+
let liveSocket = new LiveSocket("/live", Socket, { hooks: window.Hooks })
1864
liveSocket.connect()
1965

2066
/*

assets/yarn.lock

Lines changed: 332 additions & 325 deletions
Large diffs are not rendered by default.

config/test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ config :logger, level: :warning
1111

1212
config :diff,
1313
package_store_impl: Diff.Package.StoreMock,
14-
storage_impl: Diff.StorageMock
14+
storage_impl: Diff.StorageMock,
15+
hex_impl: Diff.HexMock

lib/diff/application.ex

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ defmodule Diff.Application do
1212
children = [
1313
goth_spec(),
1414
{Task.Supervisor, name: Diff.Tasks},
15-
# Start the PubSub system
1615
{Phoenix.PubSub, name: Diff.PubSub},
17-
# Start the endpoint when the application starts
18-
DiffWeb.Endpoint,
19-
# Starts a worker by calling: Diff.Worker.start_link(arg)
20-
# {Diff.Worker, arg},
21-
Diff.Package.Supervisor
16+
Diff.Package.Supervisor,
17+
DiffWeb.Endpoint
2218
]
2319

2420
# See https://hexdocs.pm/elixir/Supervisor.html

lib/diff/hex/behaviour.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
defmodule Diff.Hex.Behaviour do
2+
@callback diff(package :: String.t(), from :: String.t(), to :: String.t()) ::
3+
{:ok, Enumerable.t()} | :error
4+
end

lib/diff/hex/hex.ex

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule Diff.Hex do
2+
@behaviour Diff.Hex.Behaviour
3+
24
@config %{
35
:hex_core.default_config()
46
| http_adapter: {Diff.Hex.Adapter, %{}},
@@ -101,9 +103,12 @@ defmodule Diff.Hex do
101103

102104
with {_, true} <- {:file_size_old, file_size_check?(path_old)},
103105
{_, true} <- {:file_size_new, file_size_check?(path_new)},
104-
{_, {:ok, output}} <- {:git_diff, git_diff(path_old, path_new)},
105-
{_, {:ok, patches}} <- {:parse_patch, parse_patch(output, path_from, path_to)} do
106-
Enum.map(patches, &{:ok, &1})
106+
{_, {:ok, output}} <- {:git_diff, git_diff(path_old, path_new)} do
107+
if output do
108+
[{:ok, {output, path_from, path_to}}]
109+
else
110+
[]
111+
end
107112
else
108113
{:file_size_old, false} ->
109114
[{:too_large, Path.relative_to(path_old, path_from)}]
@@ -150,14 +155,6 @@ defmodule Diff.Hex do
150155
end
151156
end
152157

153-
defp parse_patch(_output = nil, _path_from, _path_to) do
154-
{:ok, []}
155-
end
156-
157-
defp parse_patch(output, path_from, path_to) do
158-
GitDiff.parse_patch(output, relative_from: path_from, relative_to: path_to)
159-
end
160-
161158
defp file_size_check?(path) do
162159
File.stat!(path).size <= @max_file_size
163160
end

lib/diff/storage/gcs.ex

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ defmodule Diff.Storage.GCS do
55

66
@gs_xml_url "https://storage.googleapis.com"
77

8-
def get(package, from_version, to_version) do
8+
def get_diff(package, from_version, to_version, diff_id) do
99
with {:ok, hash} <- combined_checksum(package, from_version, to_version),
10-
url = url(key(package, from_version, to_version, hash)),
11-
{:ok, 200, _headers, stream} <-
12-
Diff.HTTP.retry("gs", fn -> Diff.HTTP.get_stream(url, headers()) end) do
13-
{:ok, stream}
10+
url = url(diff_key(package, from_version, to_version, hash, diff_id)),
11+
{:ok, 200, _headers, body} <-
12+
Diff.HTTP.retry("gs", fn -> Diff.HTTP.get(url, headers()) end) do
13+
{:ok, body}
1414
else
1515
{:ok, 404, _headers, _body} ->
1616
{:error, :not_found}
@@ -25,23 +25,82 @@ defmodule Diff.Storage.GCS do
2525
end
2626
end
2727

28-
def put(package, from_version, to_version, stream) do
28+
def put_diff(package, from_version, to_version, diff_id, diff_data) do
2929
with {:ok, hash} <- combined_checksum(package, from_version, to_version),
30-
url = url(key(package, from_version, to_version, hash)),
30+
url = url(diff_key(package, from_version, to_version, hash, diff_id)),
3131
{:ok, 200, _headers, _body} <-
32-
Diff.HTTP.retry("gs", fn -> Diff.HTTP.put_stream(url, headers(), stream) end) do
32+
Diff.HTTP.retry("gs", fn -> Diff.HTTP.put(url, headers(), diff_data) end) do
3333
:ok
3434
else
3535
{:ok, status, _headers, _body} ->
3636
Logger.error("Failed to put diff to storage. Status #{status}")
37-
{:error, :not_found}
37+
{:error, :storage_error}
3838

3939
error ->
4040
Logger.error("Failed to put diff to storage. Reason #{inspect(error)}")
4141
error
4242
end
4343
end
4444

45+
def list_diffs(package, from_version, to_version) do
46+
case get_metadata(package, from_version, to_version) do
47+
{:ok, %{total_diffs: total_diffs}} ->
48+
diff_ids = 0..(total_diffs - 1) |> Enum.map(&"diff-#{&1}")
49+
{:ok, diff_ids}
50+
51+
{:error, :not_found} ->
52+
{:ok, []}
53+
54+
error ->
55+
error
56+
end
57+
end
58+
59+
def get_metadata(package, from_version, to_version) do
60+
with {:ok, hash} <- combined_checksum(package, from_version, to_version),
61+
url = url(metadata_key(package, from_version, to_version, hash)),
62+
{:ok, 200, _headers, body} <-
63+
Diff.HTTP.retry("gs", fn -> Diff.HTTP.get(url, headers()) end) do
64+
case Jason.decode(body, keys: :atoms) do
65+
{:ok, metadata} -> {:ok, metadata}
66+
{:error, _} -> {:error, :invalid_metadata}
67+
end
68+
else
69+
{:ok, 404, _headers, _body} ->
70+
{:error, :not_found}
71+
72+
{:ok, status, _headers, _body} ->
73+
Logger.error("Failed to get metadata from storage. Status #{status}")
74+
{:error, :not_found}
75+
76+
{:error, reason} ->
77+
Logger.error("Failed to get metadata from storage. Reason #{inspect(reason)}")
78+
{:error, :not_found}
79+
end
80+
end
81+
82+
def put_metadata(package, from_version, to_version, metadata) do
83+
with {:ok, hash} <- combined_checksum(package, from_version, to_version),
84+
url = url(metadata_key(package, from_version, to_version, hash)),
85+
{:ok, json} <- Jason.encode(metadata),
86+
{:ok, 200, _headers, _body} <-
87+
Diff.HTTP.retry("gs", fn -> Diff.HTTP.put(url, headers(), json) end) do
88+
:ok
89+
else
90+
{:ok, status, _headers, _body} ->
91+
Logger.error("Failed to put metadata to storage. Status #{status}")
92+
{:error, :storage_error}
93+
94+
{:error, %Jason.EncodeError{}} ->
95+
Logger.error("Failed to encode metadata as JSON")
96+
{:error, :invalid_metadata}
97+
98+
error ->
99+
Logger.error("Failed to put metadata to storage. Reason #{inspect(error)}")
100+
error
101+
end
102+
end
103+
45104
defp headers() do
46105
token = Goth.fetch!(Diff.Goth)
47106
[{"authorization", "#{token.type} #{token.token}"}]
@@ -53,8 +112,12 @@ defmodule Diff.Storage.GCS do
53112
end
54113
end
55114

56-
defp key(package, from_version, to_version, hash) do
57-
"diffs/#{package}-#{from_version}-#{to_version}-#{hash}.html"
115+
defp diff_key(package, from_version, to_version, hash, diff_id) do
116+
"diffs/#{package}-#{from_version}-#{to_version}-#{hash}-#{diff_id}.json"
117+
end
118+
119+
defp metadata_key(package, from_version, to_version, hash) do
120+
"metadata/#{package}-#{from_version}-#{to_version}-#{hash}.json"
58121
end
59122

60123
defp url(key) do

0 commit comments

Comments
 (0)