Logux
Logux is an WebSocket client/server framework to make:
- Collaborative apps when multiple users work with the same document. Logux has features inspired by CRDT to resolve edit conflicts between users. Real-time updates to prevent conflicts. Time travel to keep actions order the same on every client. A distributed timer to detect the latest changes.
- Real-time to see changes by another user immediately. Logux combines WebSocket with modern reactive client architecture. It synchronizes Redux actions between clients and servers, and keeps the same order of actions.
- Optimistic UI to improve UI performance by updating UI without waiting for an answer from the server. Time travel will revert changes later if the server refuses them.
- Offline-first for the next billion users or New York City Subway. Logux saves Redux actions to IndexedDB and has a lot of features to merge changes from different users.
- Without vendor lock-in: works in any cloud with any database.
- Just extra 9 KB in client-side JS bundle.
Ask your questions at community chat or commercial support.
Client Example
Using Logux Client:
React client
import { syncMapTemplate } from '@logux/client'
export type TaskValue = {
finished: boolean
text: string
authorId: string
}
export const Task = syncMapTemplate<TaskValue>('tasks')
export const ToDo = ({ userId }) => {
const tasks = useFilter(Task, { authorId: userId })
if (tasks.isLoading) {
return <Loader />
} else {
return <ul>
{tasks.map(task => <li>{task.text}</li>)}
</ul>
}
}
export const TaskPage = ({ id }) => {
const client = useClient()
const task = useSync(Task, id)
if (task.isLoading) {
return <Loader />
} else {
return <form>
<input type="checkbox" checked={task.finished} onChange={e => {
changeSyncMapById(client, Task, id, { finished: e.target.checked })
}}>
<input type="text" value={task.text} onChange={e => {
changeSyncMapById(client, Task, id, { text: e.target.value })
}} />
</form>
}
}
Vue client
Using Logux Vuex:
<template>
<h1 v-if="isSubscribing">Loading</h1>
<div v-else>
<h1>{{ counter }}</h1>
<button @click="increment"></button>
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore, useSubscription } from '@logux/vuex'
export default {
setup () {
// Inject store into the component
let store = useStore()
// Retrieve counter state from store
let counter = computed(() => store.state.counter)
// Load current counter from server and subscribe to counter changes
let isSubscribing = useSubscription(['counter'])
function increment () {
// Send action to the server and all tabs in this browser
store.commit.sync({ type: 'INC' })
}
return {
counter,
increment,
isSubscribing
}
}
}
</script>
Pure JS client
You can use Logux Client API with any framework:
client.type('INC', (action, meta) => {
counter.innerHTML = parseInt(counter.innerHTML) + 1
})
increase.addEventListener('click', () => {
client.sync({ type: 'INC' })
})
loading.classList.add('is-show')
await client.sync({ type: 'logux/subscribe' channel: 'counter' })
loading.classList.remove('is-show')
Server Example
Using Logux Server:
addSyncMap(server, 'tasks', {
async access (ctx, id) {
const task = await Task.find(id)
return ctx.userId === task.authorId
},
async load (ctx, id, since) {
const task = await Task.find(id)
if (!task) throw new LoguxNotFoundError()
return {
id: task.id,
text: ChangedAt(task.text, task.textChanged),
finished: ChangedAt(task.finished, task.finishedChanged),
}
},
async create (ctx, id, fields, time) {
await Task.create({
id,
authorId: ctx.userId,
text: fields.text,
textChanged: time,
finished: fields.finished,
finishedChanged: time
})
},
async change (ctx, id, fields, time) {
const task = await Task.find(id)
if ('text' in fields) {
if (task.textChanged < time) {
await task.update({
text: fields.text,
textChanged: time
})
}
}
if ('finished' in fields) {
if (task.finishedChanged < time) {
await task.update({
finished: fields.finished,
finishedChanged: time
})
}
}
}
async delete (ctx, id) {
await Task.delete(id)
}
})
addSyncMapFilter(server, 'tasks', {
access (ctx, filter) {
return true
},
initial (ctx, filter, since) {
let tasks = await Tasks.where({ ...filter, authorId: ctx.userId })
return tasks.map(task => ({
id: task.id,
text: ChangedAt(task.text, task.textChanged),
finished: ChangedAt(task.finished, task.finishedChanged),
}))
},
actions (filterCtx, filter) {
return (actionCtx, action, meta) => {
return actionCtx.userId === filterCtx.userId
}
}
})
Talks
CRDT ideas in Logux
Youtube:c7t_YBNHkeo
CRDT ideas in Logux talk
Using Logux in Production
Youtube:DvHNOplQ-tY
Using Logux in Production talk