Skip to content

Latest commit

 

History

1,220 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Logux Client

Logux is a new way to connect client and server. Instead of sending HTTP requests (e.g., AJAX and GraphQL) it synchronizes log of operations between client, server, and other clients.

This repository contains Logux base components to build web client:

  • CrossTabClient and Client to create web client for Logux.
  • IndexedStore to store Logux log in IndexedDB.
  • SqlLogStore to store Logux log in SQLite or PGlite.
  • badge() widget to show Logux synchronization status in UI.
  • status() to write own UI to show Logux synchronization status in UI.
  • attention(), confirm(), favicon() to improve UX in Logux web app.
  • log() to print Logux synchronization status to browser DevTools.

Check demo page for widget UI.


  Logux Client is built by Evil Martians, an American design and engineering consultancy for developer tools, AI, and cybersecurity startups.


Install

npm install @logux/core @logux/client nanostores

Usage

See documentation for Logux API.

import { CrossTabClient, badge, badgeEn, log } from '@logux/client'
import { badgeStyles } from '@logux/client/badge/styles'

let userId = document.querySelector('meta[name=user]').content
let token = document.querySelector('meta[name=token]').content

const client = new CrossTabClient({
  subprotocol: 1,
  server: 'wss://example.com:1337',
  userId,
  token
})

badge(client, { messages: badgeEn, styles: badgeStyles })
log(client)

client.start()

SQL Tables

@logux/client/db keeps CRDT tables in a local SQL database (like SQLite in the browser) filled from Logux log. All changes are synchronized as Logux actions, and edit conflicts are resolved with per-field last write wins.

It needs Nano Stores SQL database:

npm install @nanostores/sql
import { openDb } from '@nanostores/sql'
import { sqlocalDriver } from '@nanostores/sql/sqlocal'
import {
  bigint,
  createCrdtDatabase,
  number,
  optional,
  string
} from '@logux/client/db'

let db = openDb(sqlocalDriver('app.sqlite'))
let crdt = createCrdtDatabase(client, db, {
  migrating(done) {
    // Show “Migrating database” loader until done promise
  },
  async repeat() {
    // Ask server to the full client log
    // Remove if you store the whole lo locally
  }
})

// The third argument defines indexes: a column, an array of columns,
// `{ columns, unique }`, or the whole `CREATE INDEX` statement in `{ sql }`
let user = crdt.table(
  'user',
  {
    age: optional(number()),
    createdAt: bigint({ default: () => Date.now() }),
    name: string()
  },
  ['name', ['age', 'createdAt DESC']]
)

await crdt.ready

let id = await user.create({ name: 'Ann' })
await user.update(id, { age: 30 })

let $adults = user.select`WHERE "age" >= ${18} ORDER BY "name"`

// Arrays create batch actions, applied in a single SQL query
let ids = await user.create([{ name: 'Ben' }, { name: 'Cat' }])
await user.update(ids, { age: 20 })
await user.delete(ids)

Custom actions are applied to the database by your callback. Action types in the callback come from defineAction():

import { defineAction } from '@logux/actions'

let userRenamed = defineAction<{
  id: string
  name: string
  type: 'user/renamed'
}>('user/renamed')

let renameUser = crdt.action(userRenamed, async (tx, action, meta) => {
  // change() keeps per-field last write wins, unlike your own UPDATE
  await user.change(tx, action.id, { name: action.name }, meta)
})

await renameUser({ id, name: 'New' })

The same database can keep the Logux log itself:

import { CrossTabClient } from '@logux/client'
import { SqlLogStore } from '@logux/client/db'

const client = new CrossTabClient({,
  store: new SqlLogStore(db)
})

End-to-End Encryption

encryptActions() encrypts actions before sending them to the server, so the server can’t read users’ data. Pass a password or an AES CryptoKey and list action types to be kept unencrypted.

import { encryptActions } from '@logux/client'

encryptActions(client, localStorage.getItem('userPassword'), {
  ignore: ['server/public']
})

About

Logux base components to build web client

Resources

Stars

745 stars

Watchers

10 watching

Forks

Releases

Used by

Contributors

Languages