Skip to content
/ docs Public

Guide, recipes, and protocol specifications for Logux

License

Notifications You must be signed in to change notification settings

logux/docs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

3a53dde · Nov 25, 2024
Aug 5, 2024
Sep 19, 2024
Nov 25, 2024
Aug 25, 2024
Aug 25, 2024
Nov 21, 2021
Jan 7, 2020
Dec 23, 2021
Nov 4, 2019
Sep 8, 2024
Sep 19, 2024
Aug 5, 2024
Aug 5, 2024

Repository files navigation

Logux

Logux is a flexible JS framework to make local-first sync engine with real-time updates, offline-first, CRDT, an optimistic UI.

  • Instead of other local-first solutions, it is not a database, but a framework to build sync engines with specific needs of your project.
  • No vendor lock-in. It works with any database and in any cloud.
  • Great TypeScript support with end-to-end type checking from client to server.
  • We thought about many production-ready problems like monitoring, scaling, outdated clients, authentication, rich test API.
  • Optional end-to-end encryption.
  • Just extra 7 KB in client-side JS bundle.

Ask your questions at community 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<TaskValue>(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<TaskValue>(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
    }
  }
})