Packages

DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.

Current section

12 Versions

Jump to

Compare versions

7 files changed
+348 additions
-92 deletions
  @@ -369,16 +369,50 @@ def index(conn, params) do
369 369
370 370 ## Caching
371 371
372 - Carve includes request-scoped caching enabled by default, using [cachex](https://github.com/whitfin/cachex) under the hood. Each linked resource is stored in the cache for 100ms by default.
372 + ### Request-scoped cache
373 373
374 - To disable caching:
374 + Carve deduplicates linked resource lookups within a single request using [cachex](https://github.com/whitfin/cachex). Each request gets an isolated cache that's cleared when the response completes. TTL (default 500ms) acts as a safety net for slow requests.
375 375
376 - ```
377 376 ```elixir
378 377 config :carve, Carve.Config,
379 - enable_cache: false
378 + cache_ttl: 5_000, # milliseconds, default 500
379 + enable_cache: false # set to disable caching entirely
380 380 ```
381 381
382 + ### Shared computation between links and view
383 +
384 + When `links` and `view` both need the same expensive data, use `cache` to compute it once:
385 +
386 + ```elixir
387 + defmodule UserJSON do
388 + use Carve.View, :user
389 +
390 + get fn id -> Users.get!(id) end
391 +
392 + cache fn user ->
393 + %{
394 + plan: Plans.get_active_plan(user.id),
395 + team_plan: Plans.get_team_plan(user.id)
396 + }
397 + end
398 +
399 + links fn user, cached ->
400 + %{PlanJSON => cached.plan.id}
401 + end
402 +
403 + view fn user, cached ->
404 + %{
405 + id: hash(user.id),
406 + plan_id: PlanJSON.hash(cached.plan.id)
407 + }
408 + end
409 + end
410 + ```
411 +
412 + The `cache` function runs once per entity per request. If the same user is linked from multiple parents (e.g. 10 asset versions by the same user), it still runs only once.
413 +
414 + Modules without `cache` are unaffected — single-argument `links` and `view` work as before.
415 +
382 416 ## How does it work?
383 417
384 418 * Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })`
  @@ -31,4 +31,4 @@
31 31 {<<"optional">>,false},
32 32 {<<"repository">>,<<"hexpm">>},
33 33 {<<"requirement">>,<<"~> 3.6">>}]]}.
34 - {<<"version">>,<<"0.4.0">>}.
34 + {<<"version">>,<<"0.5.0">>}.
  @@ -2,16 +2,17 @@
2 2 defmodule Carve.Cache do
3 3 @moduledoc """
4 4 Request-scoped cache for Carve to prevent redundant data fetching within a single render.
5 - Each render operation gets its own isolated cache that expires after 100ms.
5 + Each render operation gets its own isolated cache keyed by a unique ref.
6 + TTL defaults to 500ms. Increase if requests involve slow queries.
7 + The cache is cleared at end of each render regardless of TTL.
6 8
7 - Can be disabled via config:
9 + Configuration:
8 10
9 11 config :carve, Carve.Config,
10 - enable_cache: false
12 + enable_cache: false,
13 + cache_ttl: 100
11 14 """
12 15
13 - @ttl 100
14 -
15 16 @doc """
16 17 Creates a new cache context for a render operation.
17 18 Returns a cache key that should be passed through the render pipeline.
  @@ -19,7 +20,7 @@ defmodule Carve.Cache do
19 20 def new_context do
20 21 if Carve.Config.caching_enabled?() do
21 22 cache_key = make_ref()
22 - Cachex.put(:carve_cache, cache_key, %{}, ttl: @ttl)
23 + Cachex.put(:carve_cache, cache_key, %{}, ttl: Carve.Config.cache_ttl())
23 24 cache_key
24 25 else
25 26 nil
  @@ -34,24 +35,31 @@ defmodule Carve.Cache do
34 35
35 36 def fetch(cache_key, key, fun) when is_function(fun, 0) do
36 37 if Carve.Config.caching_enabled?() do
37 - case Cachex.get(:carve_cache, cache_key) do
38 - {:ok, nil} ->
39 - fun.()
38 + cache_data =
39 + case Cachex.get(:carve_cache, cache_key) do
40 + {:ok, nil} -> %{}
41 + {:ok, data} -> data
42 + {:error, _} -> %{}
43 + end
40 44
41 - {:ok, cache_data} ->
42 - case Map.get(cache_data, key) do
43 - nil ->
44 - value = fun.()
45 - updated_cache = Map.put(cache_data, key, value)
46 - Cachex.put(:carve_cache, cache_key, updated_cache, ttl: @ttl)
47 - value
45 + case Map.get(cache_data, key) do
46 + nil ->
47 + value = fun.()
48 48
49 - cached_value ->
50 - cached_value
51 - end
49 + # Re-read cache to pick up writes from nested fetch calls
50 + current_cache =
51 + case Cachex.get(:carve_cache, cache_key) do
52 + {:ok, nil} -> %{}
53 + {:ok, data} -> data
54 + {:error, _} -> %{}
55 + end
52 56
53 - {:error, _} ->
54 - fun.()
57 + updated_cache = Map.put(current_cache, key, value)
58 + Cachex.put(:carve_cache, cache_key, updated_cache, ttl: Carve.Config.cache_ttl())
59 + value
60 +
61 + cached_value ->
62 + cached_value
55 63 end
56 64 else
57 65 fun.()
  @@ -25,4 +25,11 @@ defmodule Carve.Config do
25 25 def caching_enabled? do
26 26 get(:enable_cache, true)
27 27 end
28 +
29 + @doc """
30 + Returns the cache TTL in milliseconds. Defaults to 500.
31 + """
32 + def cache_ttl do
33 + get(:cache_ttl, 500)
34 + end
28 35 end
  @@ -1,93 +1,125 @@
1 1 defmodule Carve.Links do
2 - # Update all public functions to accept cache_key as last parameter
3 - def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil, cache_key \\ nil)
4 - def get_links_by_id(_module, nil, _visited, _whitelist, _cache_key), do: []
2 + # Public API — returns lists (backward compatible)
5 3
6 - def get_links_by_id(module, id, visited, whitelist, cache_key) when not is_list(id) do
4 + def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) do
5 + {links, _visited} = do_get_links_by_id(module, id, visited, whitelist, cache_key)
6 + links
7 + end
8 +
9 + def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) do
10 + {links, _visited} = do_get_links_by_data(module, data, visited, whitelist, cache_key)
11 + links
12 + end
13 +
14 + # Internal API — returns {links, visited} to accumulate visited across items
15 +
16 + defp do_get_links_by_id(_module, nil, visited, _whitelist, _cache_key), do: {[], visited}
17 +
18 + defp do_get_links_by_id(module, id, visited, whitelist, cache_key) when not is_list(id) do
7 19 cache_key = cache_key || Carve.Cache.get_or_create_context()
8 20
9 21 case Map.get(visited, {module, id}) do
10 22 nil ->
11 - # Use cache for get_by_id calls
12 23 data = Carve.Cache.fetch(cache_key, {module, :get, id}, fn ->
13 24 module.get_by_id(id)
14 25 end)
15 26
16 27 case data do
17 - nil -> []
18 - data -> get_links_by_data(module, data, visited, whitelist, cache_key) |> prepare_result(whitelist)
28 + nil ->
29 + {[], visited}
30 +
31 + data ->
32 + {links, visited} = do_get_links_by_data(module, data, visited, whitelist, cache_key)
33 + {prepare_result(links, whitelist), visited}
19 34 end
20 35
21 36 _ ->
22 - []
37 + {[], visited}
23 38 end
24 39 end
25 40
26 - def get_links_by_id(module, ids, visited, whitelist, cache_key) when is_list(ids) do
41 + defp do_get_links_by_id(module, ids, visited, whitelist, cache_key) when is_list(ids) do
27 42 cache_key = cache_key || Carve.Cache.get_or_create_context()
28 43
29 - Enum.flat_map(ids, &get_links_by_id(module, &1, visited, whitelist, cache_key))
30 - |> prepare_result(whitelist)
44 + {links, visited} =
45 + Enum.reduce(ids, {[], visited}, fn id, {acc, vis} ->
46 + {new_links, vis} = do_get_links_by_id(module, id, vis, whitelist, cache_key)
47 + {acc ++ new_links, vis}
48 + end)
49 +
50 + {prepare_result(links, whitelist), visited}
31 51 end
32 52
33 - def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil, cache_key \\ nil)
34 - def get_links_by_data(_module, nil, _visited, _whitelist, _cache_key), do: []
53 + defp do_get_links_by_data(_module, nil, visited, _whitelist, _cache_key), do: {[], visited}
35 54
36 - def get_links_by_data(module, data_list, visited, whitelist, cache_key) when is_list(data_list) do
55 + defp do_get_links_by_data(module, data_list, visited, whitelist, cache_key)
56 + when is_list(data_list) do
37 57 cache_key = cache_key || Carve.Cache.get_or_create_context()
38 58
39 - Enum.flat_map(data_list, &get_links_by_data(module, &1, visited, whitelist, cache_key))
40 - |> prepare_result(whitelist)
59 + {links, visited} =
60 + Enum.reduce(data_list, {[], visited}, fn item, {acc, vis} ->
61 + {new_links, vis} = do_get_links_by_data(module, item, vis, whitelist, cache_key)
62 + {acc ++ new_links, vis}
63 + end)
64 +
65 + {prepare_result(links, whitelist), visited}
41 66 end
42 67
43 - def get_links_by_data(_module, data, _visited, _whitelist, _cache_key) when not is_map(data), do: []
68 + defp do_get_links_by_data(_module, data, visited, _whitelist, _cache_key)
69 + when not is_map(data),
70 + do: {[], visited}
44 71
45 - def get_links_by_data(module, data, visited, whitelist, cache_key) when not is_list(data) do
72 + defp do_get_links_by_data(module, data, visited, whitelist, cache_key)
73 + when not is_list(data) do
46 74 cache_key = cache_key || Carve.Cache.get_or_create_context()
47 75
48 76 case fetch_id(data) do
49 77 {:ok, id} ->
50 78 if Map.get(visited, {module, id}) do
51 - []
79 + {[], visited}
52 80 else
53 81 visited = Map.put(visited, {module, id}, true)
54 82
83 + cached = fetch_cached(module, data, id, cache_key)
84 +
55 85 links =
56 - module.declare_links(data)
86 + module.declare_links(data, cached)
57 87 |> filter_and_evaluate_links(whitelist)
58 88
59 - links =
60 - links
61 - |> Enum.flat_map(fn {link_module, link_data_or_ids} ->
89 + {links, visited} =
90 + Enum.reduce(links, {[], visited}, fn {link_module, link_data_or_ids}, {acc, vis} ->
62 91 link_data_or_ids
63 92 |> normalize_link_ids()
64 - |> Enum.map(fn link_id_or_data ->
65 - link = process_single_link(link_module, link_id_or_data, visited, cache_key)
93 + |> Enum.reduce({acc, vis}, fn link_id_or_data, {acc, vis} ->
94 + link = process_single_link(link_module, link_id_or_data, vis, cache_key)
66 95
67 - children =
68 - if Map.get(visited, {link_module, extract_id(link_id_or_data)}) do
69 - []
96 + # let do_get_links_by_id/data mark visited internally
97 + {children, vis} =
98 + if is_map(link_id_or_data) do
99 + do_get_links_by_data(link_module, link_id_or_data, vis, whitelist, cache_key)
70 100 else
71 - if is_map(link_id_or_data) do
72 - get_links_by_data(link_module, link_id_or_data, visited, whitelist, cache_key)
73 - else
74 - get_links_by_id(link_module, link_id_or_data, visited, whitelist, cache_key)
75 - end
101 + do_get_links_by_id(link_module, link_id_or_data, vis, whitelist, cache_key)
76 102 end
77 103
78 - [link | children]
104 + {acc ++ [link | children], vis}
79 105 end)
80 106 end)
81 107
82 - prepare_result(links, whitelist)
108 + {prepare_result(links, whitelist), visited}
83 109 end
84 110
85 111 :error ->
86 - []
112 + {[], visited}
87 113 end
88 114 end
89 115
90 - # Update process_single_link to use cache
116 + # Fetch cached data for a module, using the Cachex cache
117 + defp fetch_cached(module, data, id, cache_key) do
118 + Carve.Cache.fetch(cache_key, {module, :cache, id}, fn ->
119 + module.__cache__(data)
120 + end)
121 + end
122 +
91 123 defp process_single_link(module, id, visited, cache_key) when is_number(id) or is_binary(id) do
92 124 case Map.get(visited, {module, id}) do
93 125 nil ->
  @@ -97,7 +129,9 @@ defmodule Carve.Links do
97 129
98 130 case data do
99 131 nil -> nil
100 - data -> module.prepare_for_view(data)
132 + data ->
133 + cached = fetch_cached(module, data, id, cache_key)
134 + module.prepare_for_view(data, cached)
101 135 end
102 136
103 137 _ ->
  @@ -105,12 +139,16 @@ defmodule Carve.Links do
105 139 end
106 140 end
107 141
108 - defp process_single_link(module, data, visited, _cache_key) do
142 + defp process_single_link(module, data, visited, cache_key) do
109 143 case fetch_id(data) do
110 144 {:ok, id} ->
111 145 case Map.get(visited, {module, id}) do
112 - nil -> module.prepare_for_view(data)
113 - _ -> nil
146 + nil ->
147 + cached = fetch_cached(module, data, id, cache_key)
148 + module.prepare_for_view(data, cached)
149 +
150 + _ ->
151 + nil
114 152 end
115 153
116 154 :error ->
  @@ -118,7 +156,6 @@ defmodule Carve.Links do
118 156 end
119 157 end
120 158
121 - # Keep all other private functions unchanged
122 159 defp filter_and_evaluate_links(links, nil) do
123 160 links
124 161 |> Enum.filter(fn {_, value} -> not is_function(value) end)
  @@ -150,7 +187,7 @@ defmodule Carve.Links do
150 187 |> filter_result(whitelist)
151 188 end
152 189
153 - defp filter_result(result, []), do: []
190 + defp filter_result(_result, []), do: []
154 191 defp filter_result(result, nil), do: result
155 192
156 193 defp filter_result(result, whitelist) do
Loading more files…