A new way of client-server conversation
Andrey Sitnik, Evil Martians
We Work with
The creator of
Autoprefixer
PostCSS
Browserslist
Slides will be in Twitter
Part 1. The problem.
Image: Invasion Base on the Moon (Apr, 1948)
The 36-Hour War: Life Magazine, 1945
My family lives in China
Chinese Internet
40% packet loss
Websites on Chinese Internet
Waiting for the next page
No stable Internet
in New York subway
Image: Mr. Robot
No stable Internet
on dotConfs in Paris
Image: Nicolas Ravelli
Wireless is always unstable
Image: John Stanmeyer
Long offline
Everyday offline
Long ping
Unstable Internet
vs.
Image: Nikita Prokopov
Connection in development
Image: Nikita Prokopov
Real connection
Common problem #1
Infinite loader
on AJAX errors
Universal solution
Have you tried to press “Reload” button?
Common problem #2
Continue to work
Save
Wait
Wait
Request statistics
Success
Failure
Common problem #3
Images: Nikita Prokopov
name: A price: A
name: A price: B
name: B price: A
name: B price: A
You always have at least 2 users
1
2
Part 2. The dream.
Image: O'Neill cylinder
Client
Server
Client
Server
Connection
Client
Server
Opportunity
All regular web apps…
… will have real-time updates …
new comment
update comments list
… will sync changes in background …
Image author: Denys Mishunov
… will have offline support …
Read-only offline
Edit in offline
… will not need a lot of code …
… and will use existing ecosystems
Replace DB
Next billion
Good UX
Part 3. The analysis.
Complexity curve with AJAX
Complexity
Features
Replace AJAX
Image: Deadpool movie
Current approach
AJAX request
The things
we care about
Don’t care
Distributed systems
Sync
Distributed computing is not easy
Image: J. R. Wharton Eyerman
Frameworks reduce complexity
Framework is the idea
Ruby on Rails: Getting Real
React: retained rendering of components
Redux: newState = reducer(state, action)
Image: Robert McCall
Part 4. The idea.
Saint Petersburg has best bars in Russia
Conflict
Images: Nikita Prokopov
{
title: '伏特加',
likes: 50
}
{
title: '白兰地',
likes: 51
}
?
Solution
“Bad programmers worry about the code.
Good programmers worry about
data structures”
— Linus Torvalds
Source: https://lwn.net/Articles/193245/
Event sourcing
Images: Nikita Prokopov
['post:title', '伏特加']
['likes:set', 51]
['likes:remove', 1]
['post:title', '伏特加']
['likes:set', 51]
['post:title', '白兰地']
CRDT , 2009
Conflict-free Replicated Data Types
CRDT in JS
Swarm.js
@gritzko
Logs everywhere
Redux actions
GraphQL mutations
CRDT
log
Undo/redo
log
Single log to rule them all
Redux
Sync with back-end
CRDT
Undo/redo
Log
Actions log on client
{
type: 'CHANGE_NAME',
user: 13,
name: 'New name'
}
Images: Nikita Prokopov
Actions log on server
Images: Nikita Prokopov
Send new actions to server
WebSocket
Images: Nikita Prokopov
Send new actions after going offline
1
2
3
4
5
— Give me new actions since “4”
Images: Nikita Prokopov
Server could create actions too
Real-time update out of box
Cross-tab sync too
Put action to the log
and forget about it
AJAX shows loader
Rename to New
Name: New
Save to DB
Optimistic UI out of box
Rename to New
Name: New
Save to DB
Common UI to show sync status
Any client action can be undone on error
Name: New
Rename to New
Name: Old
Error
Undo rename
Part 5. Details.
Image: Davis Paul Meltzer
How to write open source
Step 1 Create an idea
Step2 Implement it
Problem 1: order of actions
A
A
B
B
State: B
State: A
Images: Nikita Prokopov
Sort by time?
[
[action, { time: Date.now() }]
].sortBy(i => i[1].time)
Distributed time in complicated 1
const time1 = Date.now()
const time2 = Date.now()
time2 !== time1
Distributed time in complicated 2
const time1 = Date.now()
const time2 = Date.now()
time2 >= time1
Distributed time in complicated 3
const client = await client.askTime()
const server = Date.now()
diff(client, server) < HOUR
Vector clock?
const prevTime = 0
time: [
prevTime++,
nodeId
]
Vector clock problems
1. Doesn’t work good in offline
2. Doesn’t connected to actual time (no month, weekday)
Logux time
id: [1489328408677, '10:bfj58s0w', 0]
id: [1489328408677, '10:bfj58s0w', 1]
id: [1489328408678, '10:bfj58s0w', 0]
Milliseconds
Unique Node ID
User ID
Random string
Increase on same millisecond
Server sends current time
[
'connected',
[0, 0], // Protocol version
'server:h4vjdl', // Server unique ID
[
1475316481050, // "connect" message was received
1475316482879 // "connected" message was sent
]
]
Client calculates and applies time difference
{ // Received
id,
time: id[0] + timeFix
}
{ // Sent
id,
time: id[0] - timeFix
}
const roundTrip = now - startTime - authTime
const timeFix = startTime -
receivedTime +
roundTrip / 2
Logux time benefits
Problem 2: tabs conflict
Images author: Sebastien Gabriel & Nikita Prokopov
Duplicate
Action
Saved
I hate programming
I hate programming
I hate programming
It works!
I love programming
Only the leader tab keeps WS
leader
follower
Images author: Sebastien Gabriel & Nikita Prokopov
Synchronized state between tabs
Part 6. The reality.
Image: NASA
API
“Talk is cheap. Show me the code.”
— Linus Torvalds
Source: lkml.org/lkml/2000/8/25/132
We care about client JS size
logux + redux ≈
12.6 KB
Thanks to
Size Limit
Redux-compatible API
3 ways to dispatch
dispatch
dispatch.crossTab
dispatch.sync
Cross-tab actions
dispatch.crossTab({
type: 'READ_NOTIFICATION',
id: notification.id
})
Sync Redux actions with server
dispatch.sync({
type: 'CHANGE_NAME',
userId: user.id,
name: newName
})
Instead of AJAX, sagas, etc
showLoader()
fetch('/profile', {
credentials: 'include',
method: 'POST',
form
}).then(r => {
hideLoadaer()
}).catch(e => {
…
})
dispatch.sync({
type: 'CHANGE_NAME',
userId: user.id,
name: newName
})
Node.js framework for server
import { Server } from 'logux-server'
const app = new Server(
subprotocol: '1.0.0',
supports: '1.x',
root: __dirname
})
Handling actions on server
app.type('CHANGE_NAME', {
access(action, meta, userId) {
return userId === action.user
}
process(action, meta) {
…
}
})
Use any DB or send any requests
app.type('CHANGE_NAME', {
…
process (action, meta) {
return db.user.id(action.user).then(u => {
if (isFirstOlder(u.lastChange, meta)) {
return u.changeName(action.name)
}
})
}
Wrap legacy back-end (PHP, Ruby)
app.type('CHANGE_NAME', {
…
process(action) {
return send('POST', 'user/rename.php', {
name: action.name,
id: action.user
})
}
Send actions to client at any time
app.log.add({
type: 'CHANGE_NAME',
name: 'Looser',
user: user.id
})
Server could undo any action on clients
{
type: 'logux/undo',
id: [1489328408677, '10:bfj58s0w', 0],
reason: 'error'
}
Decorator for subscribing
const subscribe = require('react-logux/subscribe')
@subscribe(props => `users/${ props.id }`)
class User extends React.Component {
…
}
Subscription on the server
app.channel('user/:id', (params, action, meta, creator) => {
if (hasAccess(creator.userId, params.id)) return false
return getUser(params.id).then(user => {
app.log.add(userStateAction(user), {
nodeIds: [creator.nodeId]
})
})
})
Part 7. Trade-offs.
Image: NASA
VODKA
What Logux is not
Not a CRDT
Not a DB
Framework to implement both
Problem 1: Log cleaning
store.log.on('preadd', (action, meta) => {
if (action.type === 'READ_NOTIFICATION') {
meta.reasons.push('notification:' + action.notificationId)
}
})
onNotificationRemove (id) {
store.log.removeReason('notification:' + id)
}
Problem 2: Complexity
Complexity
App
Net
Logux
Cleaning
App
Right case for Logux
Complexity
App
Net
Logux
Cleaning
App
Problem 3: not for large production sites
Version: 0.2
Amplifr already uses it in production
Brave managers of Amplifr
30+ contributors
Image: Paolo Nespoli
Part 8. The result.
What is Logux?
Replaces AJAX and RESTful
Synchronizes logs (Redux actions)
between client and server
Where to use Logux
Any webapp
where users edit data a lot
Why Logux?
Questions?