LiveData: LiveView for JSON endpoints
Hans Elias B. Josephsen
- Original creator of Rustler
- Worked on Lumen
You write the HTML
LiveView takes care of sending minimal updates when things change
You write the data structure
LiveData takes care of sending minimal updates when things change
Diffing engine
Demo #1
Simple LiveData + Svelte
defmodule PdvBackendWeb.SimpleData do
use LiveData
def mount(_params, socket) do
{:ok, _tref} = :timer.send_after(1000, :tick)
socket = assign(socket, :counter, 0)
{:ok, socket}
end
def handle_info(:tick, socket) do
{:ok, _tref} = :timer.send_after(1000, :tick)
socket = assign(socket, :counter, socket.assigns.counter + 1)
{:ok, socket}
end
deft render(assigns) do
%{
"counter" => assigns.counter,
}
end
end
<script>
import LiveDataSocket from './live_data/LiveDataSocket.svelte';
import LiveData from './live_data/LiveData.svelte';
export let dataViewUrl;
</script>
<main>
<LiveDataSocket url={dataViewUrl}>
<LiveData path="/simple_data" let:data={data}>
The counter is: {data.counter}
</LiveData>
</LiveDataSocket>
</main>
Objective:
Send the least amount of data to the client
Avoid duplication!
Avoid duplication
<div>
<h1>Hello <%= @name %></h1>
<p>Status: <%= @status %></p>
</div>
[
"<div><h1>Hello ",
"!</h1><p>Status: ",
"</p></div>"
]
[name, status]
+
Avoid duplication
{
"id": 33421,
"name": "Adam Johnson",
"status": "Relaxing at home"
}
{
"id": <slot1>,
"name": <slot2>,
"status": <slot3>
}
[id, name, status]
+
Objective:
Send the least amount of data to the client
-
Avoid duplication
- Send static data only once
{
"id": <slot1>,
"name": <slot2>,
"status": <slot3>
}
[id, name, status]
+
[
{"id": 1, "name": "Paul"},
{"id": 2, "name": "Andrew"},
{"id": 3, "name": "Helly"},
{"id": 4, "name": "Stian"},
{"id": 5, "name": "Kristine"}
]
deft render(assigns) do
for user <- assigns[:users] do
%{
id: user.id,
name: user.id
}
end
end
[
{"id": 1, "name": "Paul"},
{"id": 2, "name": "Andrew"},
{"id": 4, "name": "Stian"},
{"id": 3, "name": "Helly"},
{"id": 5, "name": "Kristine"}
]
deft render(assigns) do
for user <- assigns[:users] do
%{
id: user.id,
name: user.id
}
end
end
State of the art in general tree diffing algorithms is O(n^3)
Keys!
[
{"id": 1, "name": "Paul"},
{"id": 2, "name": "Andrew"},
{"id": 4, "name": "Stian"},
{"id": 3, "name": "Helly"},
{"id": 5, "name": "Kristine"}
]
deft render(assigns) do
for user <- assigns[:users] do
%{
id: user.id,
name: user.name
}
end
end
[
{"id": 1, "name": "Paul"},
{"id": 2, "name": "Andrew"},
{"id": 4, "name": "Stian"},
{"id": 3, "name": "Helly"},
{"id": 5, "name": "Kristine"}
]
deft render(assigns) do
for user <- assigns[:users] do
keyed user.id, %{
id: user.id,
name: user.name
}
end
end
1
2
3
4
5
Objective:
Send the least amount of data to the client
-
Avoid duplication
- Send static data only once
-
Give subtrees identity
- Ability to diff dynamic parts
deft render(assigns) do
for user <- assigns[:users] do
track(render_user(user))
end
end
deft render_user(user) do
keyed user.id, %{
id: user.id,
name: user.id
}
end
deft render(assigns) do
...
end
deft -> define tracked
deft render(assigns) do
...
end
def render(assigns) do
...
end
def __tracked_meta__render__1__(...) do
...
end
def __tracked__render__(assigns) do
...
end
def __tracked_meta__render__1__(:statics) do
%{
{:expr, 8} =>
{:make_map, nil,
[
{
{:literal, "counter"},
%{__struct__: LiveData.Tracked.Tree.Slot, num: 0}
}
]}
}
end
deft render(assigns) do
%{
"counter" => assigns.counter,
}
end
def __tracked__render__(assigns) do
gen_var_13 = assigns.counter
%LiveData.Tracked.RenderTree.Static{
id: {{PdvBackendWeb.SimpleData, :render, 1}, {:expr, 8}},
slots: [gen_var_13]
}
end
deft render(assigns) do
%{
"counter" => assigns.counter,
}
end
Downside:
deft is not magic
deft
- The deft compiler needs to be able to introspect the AST of the function
- Higher order functions need to be explicitly supported
- Can still be called, less efficient on changes
make map
return
"counter"
.counter
arg: assigns
Ask me afterwards if you are curious about the deft compiler, I think it's quite neat!
Demo #2
LiveData + DSL + Native Mobile App (Flutter)
defmodule PdvBackendWeb.FlutterViewDemoData do
use LiveData
use FlutterView
alias PdvBackendWeb.LoginChangeset
def mount(_params, socket) do
changeset = LoginChangeset.changeset(%LoginChangeset{})
socket = assign(socket, :changeset, changeset)
{:ok, socket}
end
def handle_event(%{"e" => "do_validate"} = event, socket) do
data = event["data"]
changeset = LoginChangeset.changeset(%LoginChangeset{}, data)
socket = assign(socket, :changeset, changeset)
{:ok, socket}
end
deft render(assigns) do
scaffold(
app_bar: app_bar(
title: text("My Form")
),
body: form(assigns.changeset, fn f ->
column(
children: [
form_text_input(f, :name, hint: "Name"),
form_text_input(f, :email, hint: "Email"),
form_text_input(f, :password, hint: "Password")
]
)
end)
)
end
end
defmodule PdvBackendWeb.FlutterViewDemoData do
use LiveData
use FlutterView
alias PdvBackendWeb.LoginChangeset
def mount(_params, socket) do
changeset = LoginChangeset.changeset(%LoginChangeset{})
socket = assign(socket, :changeset, changeset)
{:ok, socket}
end
def handle_event(%{"e" => "do_validate"} = event, socket) do
data = event["data"]
changeset = LoginChangeset.changeset(%LoginChangeset{}, data)
socket = assign(socket, :changeset, changeset)
{:ok, socket}
end
deft render(assigns) do
scaffold(
app_bar: app_bar(
title: text("My Form")
),
body: form(assigns.changeset, fn f ->
column(
children: [
form_text_input(f, :name, hint: "Name"),
form_text_input(f, :email, hint: "Email"),
form_text_input(f, :password, hint: "Password")
]
)
end)
)
end
end
defmodule PdvBackendWeb.LoginChangeset do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :name, :string
field :email, :string
field :password, :string
end
def changeset(%__MODULE__{} = struct, params \\ %{}) do
struct
|> cast(params, [:name, :email, :password])
|> validate_required([:name, :email, :password])
end
end
import 'package:flutter/material.dart';
import 'package:live_data_client/live_data_flutter.dart';
WidgetRegistry makeWidgetRegistry() {
var registry = new WidgetRegistry();
registerBasicWidgets(registry);
registerMaterialWidgets(registry);
return registry;
}
WidgetRegistry widgetRegistry = makeWidgetRegistry();
void main() {
var service = DataViewSocketService(
"ws://localhost:4000/data/websocket");
runApp(DataViewSocket(service: service, child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.light(),
home: LiveNativeMount(widgetRegistry, "/flutter_view_demo"),
);
}
}
Wire protocol
Two tables:
- Table of Templates
- Table of Fragments
Templates
{
"id": ["$s",0],
"name": ["$s",1],
"links": {
"profile": ["$s",2]
}
}
- Templates have an arbitrary amount of slots
- Can be any JSON
Fragments
["$t", 1, ["$f", 5]]
- Can instantiate templates
- Can embed other fragments
- Can contain parts of data structure too
Each message from server -> client consists of a series of OPs
- SetFragment
- PatchFragment
- SetTemplate
- ...
- Render
Each message from server -> client consists of a series of OPs
Render:
Takes ID of root fragment, renders it out to JSON
- LiveView-like development of mobile apps
- Easy integration with Javascript apps
- Redux pattern over the network
Future work
- Getting it production ready
- Data dependency tracking
- Support binary/string construction
- Better diffing
- Other "data structures"
- Other clients
LiveData
By Hans Elias Bukholm Josephsen
LiveData
- 189