hansihe / live_data

https://hex.pm/packages/live_data
114 stars 1 forks source link

Reimplement escape analysis for keyed in new FlatAST #12

Open hansihe opened 3 years ago

hansihe commented 3 years ago

If we know the set of variables captured by a keyed, we can be smarter about when it is reevaluated.

As an example:

deft render(assigns) do
  for post <- assigns.posts do
    keyed post.id do
      %{
        id: post.id,
        name: post.name,
      }
    end
  end
end

If we know that the keyed post.id block only uses state from within post, and we know post has not changed since the last render, we can omit rendering that subtree entirely.

This is a simple example, so omitting rendering the subtree would not give us much here, but when scaled to deeper and more complicated data structures, being able to omit rendering subtrees like this could be a major advantage.

RodolfoSilva commented 9 months ago

@hansihe Have you tried using JSON Patch? Perhaps this could help with the serialization step, and it is already implemented in most languages.

Here is the implementation on Elixir:

https://hexdocs.pm/jsonpatch/readme.html

Dart:

https://pub.dev/packages/json_patch

JS:

https://www.npmjs.com/package/fast-json-patch https://www.npmjs.com/package/rfc6902

RodolfoSilva commented 9 months ago

To reduce the Payload you could do some compression like:

On elixir side:

  @json_patch_remove 0
  @json_patch_add 1
  @json_patch_replace 2
  @json_patch_test 3
  @json_patch_move 4
  @json_patch_copy 5

  defp compress_json_patch(patch) when is_list(patch) do
    Enum.flat_map(patch, &compress_json_patch/1)
  end

  defp compress_json_patch(%{op: "add", path: path, value: value}) do
    [@json_patch_add, path, value]
  end

  defp compress_json_patch(%{op: "replace", path: path, value: value}) do
    [@json_patch_replace, path, value]
  end

  defp compress_json_patch(%{op: "test", path: path, value: value}) do
    [@json_patch_test, path, value]
  end

  defp compress_json_patch(%{op: "move", path: path, from: from}) do
    [@json_patch_move, path, from]
  end

  defp compress_json_patch(%{op: "copy", path: path, from: from}) do
    [@json_patch_copy, path, from]
  end

  defp compress_json_patch(%{op: "remove", path: path}) do
    [@json_patch_remove, path]
  end

On the client side:

const OPERATIONS = new Map([
  [0, "remove"],
  [1, "add"],
  [2, "replace"],
  [3, "test"],
  [4, "move"],
  [5, "copy"],
]);

function decompress(diff) {
  const decoded = [];

  for (let i = 0; i < diff.length; i++) {
    const op = diff[i];
    const patch = {
      op: OPERATIONS.get(op),
      path: diff[++i],
    };

    if (["add", "replace", "test"].includes(patch.op)) {
      patch.value = diff[++i];
    } else if (patch.op !== "remove") {
      patch.from = diff[++i];
    }

    decoded.push(patch);
  }

  return decoded;
}
hansihe commented 9 months ago

Yep, I looked at using jsonpatch initially. Unfortunately it doesn't mesh very well with the fragment/template approach this library uses to minimize the data sent over the wire.

While jsonpatch having implementations in a lot of languages already is nice, I would argue: