• Stars
    star
    356
  • Rank 119,446 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 8 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Guide, recipes, and protocol specifications for Logux

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.

Next chapter

Sponsored by Evil Martians

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