From 927e1c944764069272287340a3e5a373538929a8 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 13:51:39 +0200 Subject: [PATCH 1/9] add ets mock node --- dev/ets_demo_node.exs | 177 ++++++++++++++++++++++++++++-------------- dev/mock_nodes/ets.sh | 2 + 2 files changed, 120 insertions(+), 59 deletions(-) create mode 100755 dev/mock_nodes/ets.sh diff --git a/dev/ets_demo_node.exs b/dev/ets_demo_node.exs index 2de2ee4a..b8e20fba 100644 --- a/dev/ets_demo_node.exs +++ b/dev/ets_demo_node.exs @@ -1,81 +1,142 @@ # Seeds a node with ETS tables covering every case the Voyager ETS UI handles. # # Run: -# elixir --name demo@127.0.0.1 --cookie demo ets_demo_node.exs +# dev/mock_nodes/ets.sh # -# Then in Voyager connect to demo@127.0.0.1 with cookie "demo". - -# --- Truncated-binary-key repro ------------------------------------------- -# In :truncated_key_bug, one key is an 8000-byte binary. -# 1. Open the table, set "Budget per record" to 100, Fetch records. -# 2. The long key renders as a silent ~99-byte prefix; Lookup stays enabled. -# 3. Click Lookup -> "No record with this key. It may have been deleted." -# ...but the record exists (this script prints proof on request, see below). -:ets.new(:truncated_key_bug, [:set, :named_table, :public]) -:ets.insert(:truncated_key_bug, {:binary.copy("K", 8000), :value_a}) -:ets.insert(:truncated_key_bug, {"short-key", :value_b}) - -# --- General zoo ----------------------------------------------------------- -:ets.new(:kv_set, [:set, :named_table, :public]) - -for i <- 1..137 do - :ets.insert(:kv_set, {i, %{name: "item_#{i}", tags: [:a, :b], score: i * 1.5}}) +# Then in Voyager connect to ets@127.0.0.1 with cookie "demo". + +defmodule Demo do + # Protected and private tables only accept writes from their owner, so every call runs there. + def run(fun) do + Agent.get(__MODULE__, fn _ -> + try do + {:ok, fun.()} + rescue + e -> {:error, e} + end + end) + end + + def new(name, opts \\ [:set, :named_table, :public]), do: run(fn -> :ets.new(name, opts) end) + def put(tab, records), do: run(fn -> :ets.insert(tab, records) end) + def delete(tab, key), do: run(fn -> :ets.delete(tab, key) end) + def clear(tab), do: run(fn -> :ets.delete_all_objects(tab) end) + def drop(tab), do: run(fn -> :ets.delete(tab) end) + + def give_away(tab), do: run(fn -> :ets.give_away(tab, sleeper(), nil) end) + def heir(tab), do: run(fn -> :ets.setopts(tab, {:heir, sleeper(), nil}) end) + def heir(tab, :none), do: run(fn -> :ets.setopts(tab, {:heir, :none}) end) + + # Type, protection, keypos, named_table, compressed and concurrency are fixed at :ets.new. + def recreate(tab, changes) do + run(fn -> + info = Keyword.merge(:ets.info(tab), changes) + rows = :ets.tab2list(tab) + :ets.delete(tab) + new_tab = :ets.new(info[:name], opts(info)) + :ets.insert(new_tab, rows) + new_tab + end) + end + + defp opts(info) do + flags = for flag <- [:named_table, :compressed], info[flag], do: flag + heir = if is_pid(info[:heir]), do: [{:heir, info[:heir], nil}], else: [] + + [ + info[:type], + info[:protection], + keypos: info[:keypos], + read_concurrency: info[:read_concurrency], + write_concurrency: info[:write_concurrency], + decentralized_counters: info[:decentralized_counters] + ] ++ flags ++ heir + end + + defp sleeper, do: spawn(fn -> Process.sleep(:infinity) end) end -:ets.new(:users_ordered, [:ordered_set, :named_table, :public]) +{:ok, _} = Agent.start(fn -> nil end, name: Demo) -for i <- 1..23 do - :ets.insert(:users_ordered, {"user-#{String.pad_leading(to_string(i), 3, "0")}", i, :active}) -end +{:ok, _} = + Demo.run(fn -> + # --- Truncated-binary-key repro ------------------------------------------- + # In :truncated_key_bug, one key is an 8000-byte binary. + # 1. Open the table, set "Budget per record" to 100, Fetch records. + # 2. The long key renders as a silent ~99-byte prefix; Lookup stays enabled. + # 3. Click Lookup -> "No record with this key. It may have been deleted." + # ...but the record exists (this script prints proof on request, see below). + :ets.new(:truncated_key_bug, [:set, :named_table, :public]) + :ets.insert(:truncated_key_bug, {:binary.copy("K", 8000), :value_a}) + :ets.insert(:truncated_key_bug, {"short-key", :value_b}) + + # --- General zoo ----------------------------------------------------------- + :ets.new(:kv_set, [:set, :named_table, :public]) + + for i <- 1..137 do + :ets.insert(:kv_set, {i, %{name: "item_#{i}", tags: [:a, :b], score: i * 1.5}}) + end -:ets.new(:by_second_key, [:set, :named_table, :public, {:keypos, 2}]) -:ets.insert(:by_second_key, {%{payload: 1}, :alpha, "extra"}) -:ets.insert(:by_second_key, {%{payload: 2}, :beta, "extra2"}) + :ets.new(:users_ordered, [:ordered_set, :named_table, :public]) -:ets.new(:bag_events, [:bag, :named_table, :public]) -:ets.insert(:bag_events, for(i <- 1..60, do: {:login, "user-#{i}", i})) -:ets.insert(:bag_events, {:logout, "alice", 61}) + for i <- 1..23 do + :ets.insert( + :users_ordered, + {"user-#{String.pad_leading(to_string(i), 3, "0")}", i, :active} + ) + end -:ets.new(:dup_bag, [:duplicate_bag, :named_table, :public]) -:ets.insert(:dup_bag, List.duplicate({:x, 1}, 25)) -:ets.insert(:dup_bag, {:y, 2}) + :ets.new(:by_second_key, [:set, :named_table, :public, {:keypos, 2}]) + :ets.insert(:by_second_key, {%{payload: 1}, :alpha, "extra"}) + :ets.insert(:by_second_key, {%{payload: 2}, :beta, "extra2"}) -:ets.new(:private_tab, [:set, :named_table, :private]) -:ets.insert(:private_tab, {:secret, 42}) + :ets.new(:bag_events, [:bag, :named_table, :public]) + :ets.insert(:bag_events, for(i <- 1..60, do: {:login, "user-#{i}", i})) + :ets.insert(:bag_events, {:logout, "alice", 61}) -unnamed = :ets.new(:anon_data, [:set, :public]) -for i <- 1..12, do: :ets.insert(unnamed, {i, {:anon, i}}) + :ets.new(:dup_bag, [:duplicate_bag, :named_table, :public]) + :ets.insert(:dup_bag, List.duplicate({:x, 1}, 25)) + :ets.insert(:dup_bag, {:y, 2}) -:ets.new(:huge_records, [:set, :named_table, :public]) -:ets.insert(:huge_records, {:big_binary, :crypto.strong_rand_bytes(5_000_000)}) + :ets.new(:private_tab, [:set, :named_table, :private]) + :ets.insert(:private_tab, {:secret, 42}) -deep = - Enum.reduce(1..30, :leaf, fn i, acc -> %{level: i, child: acc, pad: List.duplicate(i, 50)} end) + unnamed = :ets.new(:anon_data, [:set, :public]) + for i <- 1..12, do: :ets.insert(unnamed, {i, {:anon, i}}) -:ets.insert(:huge_records, {:deep_term, deep}) -:ets.insert(:huge_records, {:long_list, Enum.to_list(1..100_000)}) -:ets.insert(:huge_records, {:small, :ok}) + :ets.new(:huge_records, [:set, :named_table, :public]) + :ets.insert(:huge_records, {:big_binary, :crypto.strong_rand_bytes(5_000_000)}) -:ets.new(:charlist_keys, [:set, :named_table, :public]) -:ets.insert(:charlist_keys, {~c"compiler", ~c"/lib/compiler", []}) + deep = + Enum.reduce(1..30, :leaf, fn i, acc -> + %{level: i, child: acc, pad: List.duplicate(i, 50)} + end) -:ets.new(:tuple_keys, [:set, :named_table, :public]) -:ets.insert(:tuple_keys, {{:user, 1}, "alice"}) -:ets.insert(:tuple_keys, {{:user, 2}, "bob"}) + :ets.insert(:huge_records, {:deep_term, deep}) + :ets.insert(:huge_records, {:long_list, Enum.to_list(1..100_000)}) + :ets.insert(:huge_records, {:small, :ok}) -:ets.new(:binary_keys, [:set, :named_table, :public]) -:ets.insert(:binary_keys, {"session-abc", %{ttl: 300}}) + :ets.new(:charlist_keys, [:set, :named_table, :public]) + :ets.insert(:charlist_keys, {~c"compiler", ~c"/lib/compiler", []}) -:ets.new(:truncated_struct_key, [:set, :named_table, :public]) + :ets.new(:tuple_keys, [:set, :named_table, :public]) + :ets.insert(:tuple_keys, {{:user, 1}, "alice"}) + :ets.insert(:tuple_keys, {{:user, 2}, "bob"}) -:ets.insert( - :truncated_struct_key, - {%Version{major: 1, minor: 0, patch: 0, pre: [:binary.copy("P", 500)]}, :value_a} -) + :ets.new(:binary_keys, [:set, :named_table, :public]) + :ets.insert(:binary_keys, {"session-abc", %{ttl: 300}}) -:ets.insert(:truncated_struct_key, {%Version{major: 2, minor: 0, patch: 0}, :value_b}) + :ets.new(:truncated_struct_key, [:set, :named_table, :public]) -:ets.new(:empty_set, [:set, :named_table, :public]) + :ets.insert( + :truncated_struct_key, + {%Version{major: 1, minor: 0, patch: 0, pre: [:binary.copy("P", 500)]}, :value_a} + ) + + :ets.insert(:truncated_struct_key, {%Version{major: 2, minor: 0, patch: 0}, :value_b}) + + :ets.new(:empty_set, [:set, :named_table, :public]) + end) IO.puts(""" node ready: #{node()} (cookie: demo) @@ -83,10 +144,8 @@ node ready: #{node()} (cookie: demo) repro check from another shell (proves the record the UI calls deleted exists): elixir --name probe@127.0.0.1 --cookie demo -e ' - n = :"demo@127.0.0.1"; true = Node.connect(n) + n = :"#{node()}"; true = Node.connect(n) key = :binary.copy("K", 8000) IO.inspect(full_key: :erpc.call(n, :ets, :lookup, [:truncated_key_bug, key]) != [], prefix_99: :erpc.call(n, :ets, :lookup, [:truncated_key_bug, :binary.part(key, 0, 99)]))' """) - -Process.sleep(:infinity) diff --git a/dev/mock_nodes/ets.sh b/dev/mock_nodes/ets.sh new file mode 100755 index 00000000..923a0a4d --- /dev/null +++ b/dev/mock_nodes/ets.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +iex --name ets@127.0.0.1 --cookie demo "$(dirname "$0")/../ets_demo_node.exs" From 103378f2c2ef8cabf8f27f7c8b179fcc87ff6acd Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 13:52:06 +0200 Subject: [PATCH 2/9] fetch table info on reload --- .../components/ets_peek_components.ex | 8 ++++++-- lib/voyager_web/live/ets_table_live.ex | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/voyager_web/components/ets_peek_components.ex b/lib/voyager_web/components/ets_peek_components.ex index dd279849..5e4dd423 100644 --- a/lib/voyager_web/components/ets_peek_components.ex +++ b/lib/voyager_web/components/ets_peek_components.ex @@ -102,8 +102,12 @@ defmodule VoyagerWeb.Components.EtsPeekComponents do <.info_item id="ets-info-keypos" label="Key position" help={:keypos}>{@info.keypos} - <.info_item label="Records" help={:size}>{Formatters.format_integer(@info.size)} - <.info_item label="Memory" help={:memory}>{Formatters.format_bytes(@info.memory)} + <.info_item id="ets-info-size" label="Records" help={:size}> + {Formatters.format_integer(@info.size)} + + <.info_item id="ets-info-memory" label="Memory" help={:memory}> + {Formatters.format_bytes(@info.memory)} + <.info_item id="ets-info-owner" label="Owner" help={:owner}> diff --git a/lib/voyager_web/live/ets_table_live.ex b/lib/voyager_web/live/ets_table_live.ex index 319a4882..e03f309f 100644 --- a/lib/voyager_web/live/ets_table_live.ex +++ b/lib/voyager_web/live/ets_table_live.ex @@ -223,6 +223,7 @@ defmodule VoyagerWeb.EtsTableLive do socket |> assign(:conts, [nil]) |> assign(:page_size, socket.assigns.controls.chunk_size) + |> refresh_info() |> fetch_page(0) |> noreply() end @@ -360,12 +361,14 @@ defmodule VoyagerWeb.EtsTableLive do def handle_async(:info, {:ok, {:error, reason}}, socket) do socket + |> flash_info_failure(reason) |> assign(:info, AsyncResult.failed(socket.assigns.info, reason)) |> noreply() end def handle_async(:info, {:exit, reason}, socket) do socket + |> flash_info_failure(reason) |> assign(:info, AsyncResult.failed(socket.assigns.info, reason)) |> noreply() end @@ -444,6 +447,19 @@ defmodule VoyagerWeb.EtsTableLive do end) end + defp refresh_info(socket) do + node = socket.assigns.session.node + table = socket.assigns.table_id + timeout = socket.assigns.controls.timeout + + start_async(socket, :info, fn -> Remote.info(node, table, timeout) end) + end + + defp flash_info_failure(%{assigns: %{info: %AsyncResult{ok?: true}}} = socket, reason), + do: put_flash(socket, :error, format_error(reason)) + + defp flash_info_failure(socket, _reason), do: socket + # A reference cannot be reconstructed from its inspect string, so it is # matched against the node's live table handles instead. defp table_ids(node, "#Ref" <> _param, timeout) do From 45a258f380935e9b233371d0767ea6c905377b99 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 13:52:11 +0200 Subject: [PATCH 3/9] add tests --- test/voyager_web/live/ets_table_live_test.exs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/voyager_web/live/ets_table_live_test.exs b/test/voyager_web/live/ets_table_live_test.exs index ee48b8be..cf355e70 100644 --- a/test/voyager_web/live/ets_table_live_test.exs +++ b/test/voyager_web/live/ets_table_live_test.exs @@ -22,6 +22,37 @@ defmodule VoyagerWeb.EtsTableLiveTest do :ok end + test "reloading the snapshot re-reads the table info", %{conn: conn} do + name = named_table(:set) + :ets.insert(name, for(i <- 1..60, do: {i, i})) + + view = fetch_records(conn, name) + + assert info_size(view) == "60" + assert has_element?(view, "#ets-pager", "of 60") + + :ets.insert(name, for(i <- 61..80, do: {i, i})) + view |> element("#ets-peek-fetch") |> render_click() + render_async(view, 2_000) + + assert info_size(view) == "80" + assert has_element?(view, "#ets-pager", "of 80") + end + + test "a failed info reload flashes the error and keeps the last info", %{conn: conn} do + name = named_table(:set) + :ets.insert(name, {:k, 1}) + + view = fetch_records(conn, name) + :ets.delete(name) + + view |> element("#ets-peek-fetch") |> render_click() + render_async(view, 2_000) + + assert has_element?(view, "#flash-error") + assert has_element?(view, "#ets-table-info") + end + test "a set row can open the lookup sidebar", %{conn: conn} do name = named_table(:set) :ets.insert(name, {:k, 1}) @@ -192,6 +223,8 @@ defmodule VoyagerWeb.EtsTableLiveTest do view |> render() |> LazyHTML.from_fragment() |> LazyHTML.query(selector) |> LazyHTML.text() end + defp info_size(view), do: view |> text("#ets-info-size dd") |> String.trim() + defp open_lookup(conn, name) do view = fetch_records(conn, name) From d3d92926b1fe57d343eb9adcb326eeafaba7a145 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 15:40:59 +0200 Subject: [PATCH 4/9] add refetch info to porvate tables --- .../components/ets_peek_components.ex | 13 +++++++------ lib/voyager_web/live/ets_table_live.ex | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/voyager_web/components/ets_peek_components.ex b/lib/voyager_web/components/ets_peek_components.ex index 5e4dd423..c3dd4b77 100644 --- a/lib/voyager_web/components/ets_peek_components.ex +++ b/lib/voyager_web/components/ets_peek_components.ex @@ -166,10 +166,7 @@ defmodule VoyagerWeb.Components.EtsPeekComponents do ~H""" <.form for={@form} id="ets-peek-controls" phx-change="validate" class="flex flex-col gap-1"> -
+
<.field_label field={@form[:budget]} label="Record budget" help={@budget_help} /> <.field_label field={@form[:timeout]} label="Timeout (ms)" help={@records_timeout_help} /> @@ -186,11 +183,11 @@ defmodule VoyagerWeb.Components.EtsPeekComponents do id="ets-peek-fetch" type="button" phx-click="fetch" - disabled={@loading? or not @readable?} + disabled={@loading?} class="btn btn-primary btn-sm gap-2" > - {if @fetched?, do: "Reload snapshot", else: "Fetch records"} + {fetch_label(@readable?, @fetched?)} <.field_error field={@form[:budget]} /> @@ -518,6 +515,10 @@ defmodule VoyagerWeb.Components.EtsPeekComponents do """ end + defp fetch_label(false = _readable?, _fetched?), do: "Refetch info" + defp fetch_label(_readable?, true = _fetched?), do: "Refetch records" + defp fetch_label(_readable?, _fetched?), do: "Fetch records" + defp row_open?(open_rows, index), do: MapSet.member?(open_rows, index) defp truncated_record?(@truncated), do: true diff --git a/lib/voyager_web/live/ets_table_live.ex b/lib/voyager_web/live/ets_table_live.ex index e03f309f..81ea20a9 100644 --- a/lib/voyager_web/live/ets_table_live.ex +++ b/lib/voyager_web/live/ets_table_live.ex @@ -220,12 +220,18 @@ defmodule VoyagerWeb.EtsTableLive do # A new snapshot starts a fresh select: the old continuations belong to a # walk that is no longer on screen. def handle_event("fetch", _params, socket) do - socket - |> assign(:conts, [nil]) - |> assign(:page_size, socket.assigns.controls.chunk_size) - |> refresh_info() - |> fetch_page(0) - |> noreply() + if readable?(socket.assigns.info) do + socket + |> assign(:conts, [nil]) + |> assign(:page_size, socket.assigns.controls.chunk_size) + |> refresh_info() + |> fetch_page(0) + |> noreply() + else + socket + |> refresh_info() + |> noreply() + end end # The pager is 1-based; a page is reachable only while its continuation is From 6081339f8be9a1f31929735926e6d8dd6992de88 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 15:41:04 +0200 Subject: [PATCH 5/9] add tests --- test/voyager_web/live/ets_table_live_test.exs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/voyager_web/live/ets_table_live_test.exs b/test/voyager_web/live/ets_table_live_test.exs index cf355e70..fb7e4d29 100644 --- a/test/voyager_web/live/ets_table_live_test.exs +++ b/test/voyager_web/live/ets_table_live_test.exs @@ -53,6 +53,27 @@ defmodule VoyagerWeb.EtsTableLiveTest do assert has_element?(view, "#ets-table-info") end + test "refetching info on a private table re-reads the table info", %{conn: conn} do + name = EtsTable.unique_name() + :ets.new(name, [:named_table, :private, :set]) + + {:ok, view, _html} = live(conn, ~p"/node/#{@node_name}/ets-tables/#{inspect(name)}") + render_async(view, 2_000) + + assert has_element?(view, "#ets-private-notice") + assert has_element?(view, "#ets-peek-fetch:not([disabled])", "Refetch info") + + :ets.delete(name) + :ets.new(name, [:named_table, :public, :set]) + :ets.insert(name, {:k, 1}) + + view |> element("#ets-peek-fetch") |> render_click() + render_async(view, 2_000) + + refute has_element?(view, "#ets-private-notice") + refute has_element?(view, "#ets-records-count") + end + test "a set row can open the lookup sidebar", %{conn: conn} do name = named_table(:set) :ets.insert(name, {:k, 1}) From 85efd960a3be64032470482c6e5495f5168791d2 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 16:55:17 +0200 Subject: [PATCH 6/9] remove private records on table visibility change --- lib/voyager_web/live/ets_table_live.ex | 14 ++++++++++++++ test/voyager_web/live/ets_table_live_test.exs | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/voyager_web/live/ets_table_live.ex b/lib/voyager_web/live/ets_table_live.ex index 81ea20a9..d1235d41 100644 --- a/lib/voyager_web/live/ets_table_live.ex +++ b/lib/voyager_web/live/ets_table_live.ex @@ -362,6 +362,7 @@ defmodule VoyagerWeb.EtsTableLive do socket |> assign(:table_id, info.id) |> assign(:info, AsyncResult.ok(socket.assigns.info, info)) + |> drop_unreadable_records() |> noreply() end @@ -399,6 +400,7 @@ defmodule VoyagerWeb.EtsTableLive do def handle_async(:chunk, {:ok, {:error, reason}}, socket) do socket |> assign(:chunk, AsyncResult.failed(socket.assigns.chunk, reason)) + |> drop_unreadable_records() |> noreply() end @@ -466,6 +468,18 @@ defmodule VoyagerWeb.EtsTableLive do defp flash_info_failure(socket, _reason), do: socket + defp drop_unreadable_records(socket) do + if readable?(socket.assigns.info) do + socket + else + socket + |> assign(:chunk, %AsyncResult{}) + |> assign(:records, []) + |> assign(:truncated?, false) + |> assign(:fetched?, false) + end + end + # A reference cannot be reconstructed from its inspect string, so it is # matched against the node's live table handles instead. defp table_ids(node, "#Ref" <> _param, timeout) do diff --git a/test/voyager_web/live/ets_table_live_test.exs b/test/voyager_web/live/ets_table_live_test.exs index fb7e4d29..79351638 100644 --- a/test/voyager_web/live/ets_table_live_test.exs +++ b/test/voyager_web/live/ets_table_live_test.exs @@ -74,6 +74,25 @@ defmodule VoyagerWeb.EtsTableLiveTest do refute has_element?(view, "#ets-records-count") end + test "refetching records on a table that became private hides the records", %{conn: conn} do + name = named_table(:set) + :ets.insert(name, {:k, 1}) + + view = fetch_records(conn, name) + + :ets.delete(name) + :ets.new(name, [:named_table, :private, :set]) + :ets.insert(name, {:k, 1}) + + view |> element("#ets-peek-fetch") |> render_click() + render_async(view, 2_000) + + assert has_element?(view, "#ets-private-notice") + assert has_element?(view, "#ets-peek-fetch", "Refetch info") + refute has_element?(view, "#ets-records-0") + refute has_element?(view, "#ets-peek-error") + end + test "a set row can open the lookup sidebar", %{conn: conn} do name = named_table(:set) :ets.insert(name, {:k, 1}) From 0c885a589741352c6e152cc6cdae2ef551242898 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Thu, 8 Oct 2026 16:57:48 +0200 Subject: [PATCH 7/9] cr suggestions --- dev/ets_demo_node.exs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dev/ets_demo_node.exs b/dev/ets_demo_node.exs index b8e20fba..d34c5d39 100644 --- a/dev/ets_demo_node.exs +++ b/dev/ets_demo_node.exs @@ -32,8 +32,17 @@ defmodule Demo do run(fn -> info = Keyword.merge(:ets.info(tab), changes) rows = :ets.tab2list(tab) + options = opts(info) + temporary = :ets.new(info[:name], List.delete(options, :named_table)) + + try do + :ets.insert(temporary, rows) + after + :ets.delete(temporary) + end + :ets.delete(tab) - new_tab = :ets.new(info[:name], opts(info)) + new_tab = :ets.new(info[:name], options) :ets.insert(new_tab, rows) new_tab end) From 3b82a3a5526faa5c88c20b0d2af48705627857d8 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Fri, 9 Oct 2026 11:38:12 +0200 Subject: [PATCH 8/9] fix credo --- test/voyager_web/live/ets_table_live_test.exs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/voyager_web/live/ets_table_live_test.exs b/test/voyager_web/live/ets_table_live_test.exs index 32284a2d..8c3f6e3d 100644 --- a/test/voyager_web/live/ets_table_live_test.exs +++ b/test/voyager_web/live/ets_table_live_test.exs @@ -5,6 +5,7 @@ defmodule VoyagerWeb.EtsTableLiveTest do import Phoenix.LiveViewTest + alias Voyager.Erpc alias Voyager.Fakes alias Voyager.Test.EtsTable alias Voyager.Test.VoyagerAgentFixture @@ -96,11 +97,11 @@ defmodule VoyagerWeb.EtsTableLiveTest do send(test_pid, {:info_waiting, self()}) receive do - :go -> Voyager.Erpc.Impl.call(node, :ets, :info, args, timeout) + :go -> Erpc.Impl.call(node, :ets, :info, args, timeout) end node, mod, :ets_select_chunk, args, timeout -> - result = Voyager.Erpc.Impl.call(node, mod, :ets_select_chunk, args, timeout) + result = Erpc.Impl.call(node, mod, :ets_select_chunk, args, timeout) send(test_pid, {:chunk_read, self()}) receive do @@ -108,7 +109,7 @@ defmodule VoyagerWeb.EtsTableLiveTest do end node, mod, fun, args, timeout -> - Voyager.Erpc.Impl.call(node, mod, fun, args, timeout) + Erpc.Impl.call(node, mod, fun, args, timeout) end) view |> element("#ets-peek-fetch") |> render_click() From 031a54cc741be5ac1991bb41267c178bee8d1338 Mon Sep 17 00:00:00 2001 From: srzeszut Date: Fri, 9 Oct 2026 14:48:54 +0200 Subject: [PATCH 9/9] cr suggestions --- dev/ets_demo_node.exs | 5 +++++ lib/voyager_web/live/ets_table_live.ex | 1 + 2 files changed, 6 insertions(+) diff --git a/dev/ets_demo_node.exs b/dev/ets_demo_node.exs index d34c5d39..5e137b66 100644 --- a/dev/ets_demo_node.exs +++ b/dev/ets_demo_node.exs @@ -33,6 +33,11 @@ defmodule Demo do info = Keyword.merge(:ets.info(tab), changes) rows = :ets.tab2list(tab) options = opts(info) + registered = :ets.whereis(info[:name]) + + if info[:named_table] and registered not in [:undefined, :ets.info(tab, :id)], + do: raise(ArgumentError, "#{inspect(info[:name])} is already a named table") + temporary = :ets.new(info[:name], List.delete(options, :named_table)) try do diff --git a/lib/voyager_web/live/ets_table_live.ex b/lib/voyager_web/live/ets_table_live.ex index 07b26a97..fa2768e8 100644 --- a/lib/voyager_web/live/ets_table_live.ex +++ b/lib/voyager_web/live/ets_table_live.ex @@ -475,6 +475,7 @@ defmodule VoyagerWeb.EtsTableLive do socket else socket + |> cancel_async(:chunk, {:shutdown, :cancel}) |> assign(:chunk, %AsyncResult{}) |> assign(:records, []) |> assign(:truncated?, false)