Extends unknown.

Show error message to user on subscription errors in components deep in the tree.

import { ChannelErrors } from '@logux/client/react'

export const App: FC = () => {
  return <>
    <SideMenu />
    <ChannelErrors
      NotFound={NotFoundPage}
      AccessDenied={AccessDeniedPage}
      Error={ServerErrorPage}
    >
      <Layout />
    </ChannelErrors>
  <>
}

React.ClientContext

Context to send Logux Client or object space to components deep in the tree.

import { ClientContext, ChannelErrors } from '@logux/client/react'
import { CrossTabClient } from '@logux/client'

let client = new CrossTabClient(…)

render(
 <ClientContext.Provider value={client}>
   <ChannelErrors NotFound={Page404} AccessDenied={Page403}>
     <App />
   </ChannelErrors>
 </ClientContext.Provider>,
 document.body
)

Type: ReactContext.

React.ErrorsContext

Context to pass error handlers from ChannelErrors.

Type: ReactContext.

React.useAuth()

Hook to return user's current authentication state and ID.

import { useAuth } from '@logux/client/react'

export const UserPage = () => {
  let { isAuthenticated, userId } = useAuth()
  if (isAuthenticated) {
    return <User id={userId} />
  } else {
    return <Loader />
  }
}

Returns StoreValue.

React.useClient()

Hook to return Logux client, which you set by <ClientContext.Provider>.

import { useClient } from '@logux/client/react'

import { User } from '../stores/user'

export const NewUserForm = () => {
  let client = useClient()
  let onAdd = data => {
    User.create(client, data)
  }
}

Returns Client.

React.useFilter(Template, filter?, opts?)

The way to createFilter in React.

import { useFilter } from '@logux/client/react'

import { User } from '../stores/user'

export const Users = ({ projectId }) => {
  let users = useFilter(User, { projectId })
  return <div>
    {users.list.map(user => <User user={user} />)}
    {users.isLoading && <Loader />}
  </div>
}
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore template.
filter ?FilterKey-value filter for stores.
opts ?FilterOptionsFilter options.

Returns StoreValue. Filter store to use with map.

React.useSync(Template, id)

Create store by ID, subscribe and get store’s value.

import { useSync } from '@logux/client/react'

import { User } from '../stores/user'

export const UserPage: FC = ({ id }) => {
  let user = useSync(User, id)
  if (user.isLoading) {
    return <Loader />
  } else {
    return <h1>{user.name}</h1>
  }
}
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore builder.
idstringStore ID.
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore builder.
idstringStore ID.
argsArgsOther store arguments.

Returns SyncMapValue. Store value.


Vue.ChannelErrors

Show error message to user on subscription errors in components deep in the tree.

<template>
  <channel-errors v-slot="{ code, error }">
    <layout v-if="!error" />
    <error v-else-if="code === 500" />
    <error-not-found v-else-if="code === 404" />
    <error-access-denied v-else-if="code === 403" />
  </channel-errors>
</template>

<script>
import { ChannelErrors } from '@logux/client/vue'

export default {
  components: { ChannelErrors }
}
</script>

Type: Component.

Vue.ChannelErrorsSlotProps

PropertyType
codeRef<Type>
errorRef<Type>

Vue.ClientKey

Type: InjectionKey.

Vue.ErrorsKey

Type: InjectionKey.

Vue.loguxPlugin(app, client)

Plugin that injects Logux Client into all components within the application.

import { createApp } from 'vue'
import { loguxPlugin } from '@logux/client/vue'
import { CrossTabClient } from '@logux/client'

let client = new CrossTabClient(…)
let app = createApp(…)

app.use(loguxPlugin, client)
ArgumentType
appApp
clientClient

Vue.useAuth(client?)

Returns user's current authentication state and ID.

<template>
  <user v-if="isAuthenticated" :id="userId" />
  <sign-in v-else />
</template>

<script>
import { useAuth } from '@logux/client/vue'

export default () => {
  let { isAuthenticated, userId } = useAuth()
  return { isAuthenticated, userId }
}
</script>
ArgumentTypeDescription
client ?ClientLogux Client instance.

Returns { isAuthenticated: ComputedRef, userId: ComputedRef }.

Vue.useClient()

Returns the Logux Client instance.

<script>
import { useClient } from '@logux/client/vue'

import { User } from '../stores/user'

let client = useClient()
let onAdd = data => {
  User.create(client, data)
}
</script>

Returns Client.

Vue.useFilter(Template, filter?, opts?)

The way to createFilter in Vue.

<template>
  <loader v-if="users.isLoading" />
  <user v-else v-for="user in users" :user="user" />
</template>

<script>
import { useFilter } from '@logux/client/vue'

import { User } from '../stores/user'

export default {
  props: ['projectId'],
  setup (props) {
    let users = useFilter(User, { projectId: props.projectId })
    return { users }
  }
}
</script>
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore class.
filter ?anyKey-value filter for stores.
opts ?anyFilter options.

Returns Ref<Type>. Filter store to use with map.

Vue.useSync(Template, id)

Create store by ID, subscribe to store changes and get store’s value.

<template>
  <loader v-if="user.isLoading" />
  <h1 v-else>{{ user.name }}</h1>
</template>

<script>
import { useSync } from '@logux/client/vue'

import { User } from '../stores/user'

export default {
  props: ['id'],
  setup (props) {
    let user = useSync(User, props.id)
    return { user }
  }
}
</script>
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore template.
idanyStore ID.
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore template.
idanyStore ID.
argsArgsOther store arguments.

Returns Ref<Type>. Store value.


Extends unknown.

Show error message to user on subscription errors in components deep in the tree.

import { ChannelErrors } from '@logux/client/preact'

export const App: FC = () => {
  return <>
    <SideMenu />
    <ChannelErrors
      NotFound={NotFoundPage}
      AccessDenied={AccessDeniedPage}
      Error={ServerErrorPage}
    >
      <Layout />
    </ChannelErrors>
  <>
}

Preact.ChannelErrors#render()

Returns ComponentChild.

Preact.ClientContext

Context to send Logux Client or object space to components deep in the tree.

import { ClientContext, ChannelErrors } from '@logux/client/preact'
import { CrossTabClient } from '@logux/client'

let client = new CrossTabClient(…)

render(
 <ClientContext.Provider value={client}>
   <ChannelErrors NotFound={Page404} AccessDenied={Page403}>
     <App />
   </ChannelErrors>
 </ClientContext.Provider>,
 document.body
)

Type: PreactContext.

Preact.ErrorsContext

Context to pass error handlers from ChannelErrors.

Type: PreactContext.

Preact.useAuth()

Hook to return user's current authentication state and ID.

import { useAuth } from '@logux/client/preact'

export const UserPage = () => {
  let { isAuthenticated, userId } = useAuth()
  if (isAuthenticated) {
    return <User id={userId} />
  } else {
    return <Loader />
  }
}

Returns StoreValue.

Preact.useClient()

Hook to return Logux client, which you set by <ClientContext.Provider>.

import { useClient } from '@logux/client/preact'

import { User } from '../stores/user'

export const NewUserForm = () => {
  let client = useClient()
  let onAdd = data => {
    User.create(client, data)
  }
}

Returns Client.

Preact.useFilter(Template, filter?, opts?)

The way to createFilter in React.

import { useFilter } from '@logux/client/preact'

import { User } from '../stores/user'

export const Users = ({ projectId }) => {
  let users = useFilter(User, { projectId })
  return <div>
    {users.list.map(user => <User user={user} />)}
    {users.isLoading && <Loader />}
  </div>
}
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore class.
filter ?FilterKey-value filter for stores.
opts ?FilterOptionsFilter options.

Returns StoreValue. Filter store to use with map.

Preact.useSync(Template, id)

Create store by ID, subscribe and get store’s value.

import { useSync } from '@logux/client/preact'

import { User } from '../stores/user'

export const UserPage: FC = ({ id }) => {
  let user = useSync(User, id)
  if (user.isLoading) {
    return <Loader />
  } else {
    return <h1>{user.name}</h1>
  }
}
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore template.
idstringStore ID.
ArgumentTypeDescription
TemplateSyncMapTemplate | SyncMapTemplateLikeStore template.
idstringStore ID.
argsArgsOther store arguments.

Returns SyncMapValue. Store value.


Base class for browser API to be extended in CrossTabClient.

Because this class could have conflicts between different browser tab, you should use it only if you are really sure, that application will not be run in different tab (for instance, if you are developing a kiosk app).

import { Client } from '@logux/client'

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

const client = new Client({
  credentials: token,
  subprotocol: 1,
  server: 'wss://example.com:1337',
  userId: userId
})
client.start()
ParameterTypeDescription
optsClientOptionsClient options.

Client#clientId

Unique permanent client ID. Can be used to track this machine.

Type: string.

Client#connected

Is leader tab connected to server.

Type: boolean.

Client#log

Client events log.

client.log.add(action)

Type: ClientLog.

Client#node

Node instance to synchronize logs.

if (client.node.state === 'synchronized')

Type: ClientNode.

Client#nodeId

Unique Logux node ID.

console.log('Client ID: ', client.nodeId)

Type: string.

Client#options

Client options.

console.log('Connecting to ' + client.options.server)

Type: ClientOptions.

Client#state

Leader tab synchronization state. It can differs from client.node.state (because only the leader tab keeps connection).

client.on('state', () => {
  if (client.state === 'disconnected' && client.state === 'sending') {
    showCloseWarning()
  }
})

Type: ClientNode.

Client#tabId

Unique tab ID. Can be used to add an action to the specific tab.

Without localStorage (React Native) there are it is an empty string.

client.log.add(action, { tab: client.tabId })

Type: string.

Client#changeUser(userId, token?)

Disconnect from the server, update user, and connect again with new credentials.

onAuth(async (userId, token) => {
  showLoader()
  client.changeUser(userId, token)
  await client.node.waitFor('synchronized')
  hideLoader()
})

You need manually chang user ID in all browser tabs.

ArgumentTypeDescription
userIdstringThe new user ID.
token ?stringCredentials for new user.

Client#clean()

Clear stored data. Removes action log from IndexedDB if you used it.

Databases, subscribed to the cleaning event, are cleaned before the log, so the log can’t fill them back.

signout.addEventListener('click', async () => {
  await client.clean()
  location.reload()
})

Returns Promise. Promise when all data will be removed.

Client#destroy()

Disconnect and stop synchronization.

shutdown.addEventListener('click', () => {
  client.destroy()
})

Client#on(event, listener)

ArgumentType
event"user"
listener(userId: string) => void
ArgumentTypeDescription
event"cleaning"The event name.
listener() => void | PromiseThe listener function.
ArgumentTypeDescription
event"state"The event name.
listener() => voidThe listener function.
ArgumentType
event"add" | "clean" | "preadd"
listenerClientActionListener

Returns Unsubscribe.

Client#start(connect?)

Connect to server and reconnect on any connection problem.

client.start()
ArgumentTypeDescription
connect ?booleanStart connection immediately.

Client#sync(action, meta?)

Send action to the server (by setting meta.sync and adding to the log) and track server processing.

showLoader()
client.sync(
  { type: 'CHANGE_NAME', name }
).then(() => {
  hideLoader()
}).catch(error => {
  hideLoader()
  showError(error.action.reason)
})
ArgumentTypeDescription
actionSyncActionThe action
meta ?Partial<ClientMeta>Optional meta.

Returns Promise<ClientMeta>. Promise for server processing.

Client#type(type, listener, opts?)

Add listener for adding action with specific type. Works faster than on('add', cb) with if.

client.type('rename', (action, meta) => {
  name = action.name
})
ArgumentTypeDescription
typeTypeAction["type"]Action’s type.
listenerClientActionListener
opts ?{ event?: "add" | "clean" | "preadd", id?: string }
ArgumentTypeDescription
actionCreatorCreatorAction creator function.
listenerClientActionListener
opts ?{ event?: "add" | "clean" | "preadd", id?: string }

Returns Unsubscribe. Unbind listener from event.

Client#waitFor(state)

Wait for specific state of the leader tab.

await client.waitFor('synchronized')
hideLoader()
ArgumentTypeDescription
stateClientNodeState name

Returns Promise.

Extends Client.

Low-level browser API for Logux.

Instead of Client, this class prevents conflicts between Logux instances in different tabs on single browser.

import { CrossTabClient } from '@logux/client'

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

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

CrossTabClient#clientId

Unique permanent client ID. Can be used to track this machine.

Type: string.

CrossTabClient#connected

Is leader tab connected to server.

Type: boolean.

CrossTabClient#isLocalStorage

Cache for localStorage detection. Can be overridden to disable leader tab election in tests.

Type: boolean.

CrossTabClient#log

Client events log.

client.log.add(action)

Type: ClientLog.

CrossTabClient#node

Node instance to synchronize logs.

if (client.node.state === 'synchronized')

Type: ClientNode.

CrossTabClient#nodeId

Unique Logux node ID.

console.log('Client ID: ', client.nodeId)

Type: string.

CrossTabClient#options

Client options.

console.log('Connecting to ' + client.options.server)

Type: ClientOptions.

CrossTabClient#role

Current tab role. Only leader tab connects to server. followers just listen to events from leader.

client.on('role', () => {
  console.log('Tab role:', client.role)
})

Type: "follower" | "leader".

CrossTabClient#state

Leader tab synchronization state. It can differs from client.node.state (because only the leader tab keeps connection).

client.on('state', () => {
  if (client.state === 'disconnected' && client.state === 'sending') {
    showCloseWarning()
  }
})

Type: ClientNode.

CrossTabClient#tabId

Unique tab ID. Can be used to add an action to the specific tab.

Without localStorage (React Native) there are it is an empty string.

client.log.add(action, { tab: client.tabId })

Type: string.

CrossTabClient#changeUser(userId, token?)

Disconnect from the server, update user, and connect again with new credentials.

onAuth(async (userId, token) => {
  showLoader()
  client.changeUser(userId, token)
  await client.node.waitFor('synchronized')
  hideLoader()
})

You need manually chang user ID in all browser tabs.

ArgumentTypeDescription
userIdstringThe new user ID.
token ?stringCredentials for new user.

CrossTabClient#clean()

Clear stored data. Removes action log from IndexedDB if you used it.

Databases, subscribed to the cleaning event, are cleaned before the log, so the log can’t fill them back.

signout.addEventListener('click', async () => {
  await client.clean()
  location.reload()
})

Returns Promise. Promise when all data will be removed.

CrossTabClient#destroy()

Disconnect and stop synchronization.

shutdown.addEventListener('click', () => {
  client.destroy()
})

CrossTabClient#forceConnect()

Start web socket reconnection.

CrossTabClient#on(event, listener)

ArgumentType
event"add" | "clean" | "preadd"
listenerClientActionListener
ArgumentTypeDescription
event"role" | "state"The event name.
listener() => voidThe listener function.
ArgumentTypeDescription
event"user"The event name.
listener(userId: string) => voidThe listener function.
ArgumentType
event"cleaning"
listener() => void | Promise

Returns Unsubscribe.

CrossTabClient#start(connect?)

Connect to server and reconnect on any connection problem.

client.start()
ArgumentTypeDescription
connect ?booleanStart connection immediately.

CrossTabClient#sync(action, meta?)

Send action to the server (by setting meta.sync and adding to the log) and track server processing.

showLoader()
client.sync(
  { type: 'CHANGE_NAME', name }
).then(() => {
  hideLoader()
}).catch(error => {
  hideLoader()
  showError(error.action.reason)
})
ArgumentTypeDescription
actionSyncActionThe action
meta ?Partial<ClientMeta>Optional meta.

Returns Promise<ClientMeta>. Promise for server processing.

CrossTabClient#type(type, listener, opts?)

Add listener for adding action with specific type. Works faster than on('add', cb) with if.

client.type('rename', (action, meta) => {
  name = action.name
})
ArgumentTypeDescription
typeTypeAction["type"]Action’s type.
listenerClientActionListener
opts ?{ event?: "add" | "clean" | "preadd", id?: string }
ArgumentTypeDescription
actionCreatorCreatorAction creator function.
listenerClientActionListener
opts ?{ event?: "add" | "clean" | "preadd", id?: string }

Returns Unsubscribe. Unbind listener from event.

CrossTabClient#waitFor(state)

Wait for specific state of the leader tab.

await client.waitFor('synchronized')
hideLoader()
ArgumentTypeDescription
stateClientNodeState name

Returns Promise.

Extends unknown.

IndexedDB store for Logux log.

import { IndexedStore } from '@logux/client'
const client = new CrossTabClient({
  …,
  store: new IndexedStore()
})
ParameterTypeDescription
name ?stringDatabase name to run multiple Logux instances on same web page.

IndexedStore#name

Database name.

Type: string.

attention(client)

Highlight tabs on synchronization errors.

import { attention } from '@logux/client'
attention(client)
ArgumentTypeDescription
clientClientObserved Client instance.

Returns () => void. Unbind listener.

badge(client, opts)

Display Logux widget in browser.

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

badge(client, {
 messages: badgeEn,
 styles: {
   ...badgeStyles,
   synchronized: { backgroundColor: 'green' }
 },
 position: 'top-left'
})
ArgumentTypeDescription
clientClientObserved Client instance.
optsBadgeOptionsWidget settings.

Returns () => void. Unbind badge listener and remove widget from DOM.

buildNewSyncMap(client, Template, value)

Send create action and build store instance.

import { buildNewSyncMap } from '@logux/client'

let userStore = buildNewSyncMap(client, User, {
  id: nanoid(),
  login: 'test'
})
ArgumentTypeDescription
clientClientLogux Client instance.
TemplateSyncMapTemplateStore template from syncMapTemplate.
value{ id: string } & ValueInitial value.

Returns Promise<any>. Promise with store instance.

changeSyncMap(store, diff)

Change keys in the store’s value.

import { changeSyncMap } from '@logux/client'

showLoader()
await changeSyncMap(userStore, { name: 'New name' })
hideLoader()
ArgumentTypeDescription
storeanyStore’s instance.
diffPartial<Omit<Value,"id">>Store’s changes.
ArgumentTypeDescription
storeanyStore’s instance.
keyValueKey
valueValue[ValueKey]

Returns Promise. Promise until server validation for remote classes or saving action to the log of fully offline classes.

changeSyncMapById(client, Template, id, diff)

Change store without store instance just by store ID.

import { changeSyncMapById } from '@logux/client'

let userStore = changeSyncMapById(client, User, 'user:4hs2jd83mf', {
  name: 'New name'
})
ArgumentTypeDescription
clientClientLogux Client instance.
TemplateSyncMapTemplateStore template from syncMapTemplate.
idstringStore’s ID.
diffPartial<Value>Store’s changes.
ArgumentTypeDescription
clientClientLogux Client instance.
TemplateSyncMapTemplateStore template from syncMapTemplate.
idstringStore’s ID.
keyValueKey
valueValue[ValueKey]

Returns Promise. Promise until server validation for remote classes or saving action to the log of fully offline classes.

confirm(client)

Show confirm popup, when user close tab with non-synchronized actions.

import { confirm } from '@logux/client'
confirm(client)
ArgumentTypeDescription
clientClientObserved Client instance.

Returns () => void. Unbind listener.

createFilter(client, Template, filter?, opts?)

Load list of SyncMap with simple key-value requirements.

It will look for stores in loaded cache, log (for offline maps) and will subscribe to list from server (for remote maps).

import { createFilter } from '@logux/client'

import { User } from '../store'

let usersInProject = createFilter(client, User, { projectId })
await usersInProject.loading
console.log(usersInProject.get())
ArgumentTypeDescription
clientClientLogux Client.
TemplateSyncMapTemplateStore template from syncMapTemplate.
filter ?FilterKey-value to filter stores.
opts ?FilterOptionsLoading options.

Returns any.

createSyncMap(client, Template, value)

Send create action to the server or to the log.

Server will create a row in database on this action. FilterStore will update the list.

import { createSyncMap } from '@logux/client'

showLoader()
await createSyncMap(client, User, {
  id: nanoid(),
  login: 'test'
})
hideLoader()
ArgumentTypeDescription
clientClientLogux Client instance.
TemplateSyncMapTemplateStore template from syncMapTemplate.
value{ id: string } & ValueInitial value.

Returns Promise. Promise until server validation for remote classes or saving action to the log of fully offline classes.

deleteSyncMap(store)

Delete store.

import { deleteSyncMap } from '@logux/client'

showLoader()
await deleteSyncMap(User)
ArgumentTypeDescription
storeanyStore’s instance.

Returns Promise. Promise until server validation for remote classes or saving action to the log of fully offline classes.

deleteSyncMapById(client, Template, id)

Delete store without store instance just by store ID.

import { deleteSyncMapById } from '@logux/client'

showLoader()
await deleteSyncMapById(client, User, 'user:4hs2jd83mf')
ArgumentTypeDescription
clientClientLogux Client instance.
TemplateSyncMapTemplateStore template from syncMapTemplate.
idstringStore’s ID.

Returns Promise. Promise until server validation for remote classes or saving action to the log of fully offline classes.

encryptActions(client, secret, opts?)

Encrypt actions before sending them to server.

Actions will be converted to { type: '0', d: encrypt(action) }

Client will be switched to binary protocol, which has a compact format for encrypted actions. Call it before client.start().

import { encryptActions } from '@logux/client'
encryptActions(client, localStorage.getItem('userPassword'), {
  ignore: ['server/public'] // action.type to not be encrypted
})
ArgumentTypeDescription
clientClientObserved Client instance.
secretstring | CryptoKeyPassword for encryption, or a CryptoKey AES key.
opts ?{ clean?: boolean, ignore?: string[] }Encryption options.

favicon(client, links)

Change favicon to show Logux synchronization status.

import { favicon } from '@logux/client'
favicon(client, {
  normal: '/favicon.ico',
  offline: '/offline.ico',
  error: '/error.ico'
})
ArgumentTypeDescription
clientClientObserved Client instance.
linksFaviconLinksFavicon links.

Returns () => void. Unbind listener.

log(client, messages?)

Display Logux events in browser console.

import { log } from '@logux/client'
log(client, { ignoreActions: ['user/add'] })
ArgumentTypeDescription
clientClientObserved Client instance.
messages ?LogMessagesDisable specific message types.

Returns () => void. Unbind listener.

request(action, opts)

Create temporary client instance, send an action, wait response action from the server and destroy client.

Useful for simple actions like signin or signup.

import { request } from '@logux/client'

let action = { type: 'signin', login, password }

request(action, {
  server: 'wss://example.com',
  subprotocol: 10
}).then(response => {
  saveToken(response.token)
}).catch(error => {
  showError(error.action.reason)
})
ArgumentTypeDescription
actionAnyActionAction which we need to send to the server.
optsRequestOptions

Returns Promise<SentAction>. Action of server response.

syncMapTemplate(plural, opts?)

CRDT LWW Map. It can use server validation or be fully offline.

The best option for classic case with server and many clients. Store will resolve client’s edit conflicts with last write wins strategy.

import { syncMapTemplate } from '@logux/client'

export const User = syncMapTemplate<{
  login: string,
  name?: string,
  isAdmin: boolean
}>('users')
ArgumentTypeDescription
pluralstringPlural store name. It will be used in action type and channel name.
opts ?{ offline?: boolean, remote?: boolean }Options to disable server validation or keep actions in log for offline support.

Returns SyncMapTemplate.


Extends Client.

Virtual client to test client-side code end store extnesions.

import { TestClient } from '@logux/client'

it('connects and sends actions', async () => {
  let client = new TestClient()
  let user = new UserStore(client, '10')

  client.server.onChannel('users/10', [
    { type: 'users/name', userId: 10, value: 'New name' }
  ])
  await client.connect()
  await delay(10)

  expect(user.name).toEqual('New name')
})
ParameterTypeDescription
userIdstringUser ID.
opts ?TestClientOptionsOther options.

TestClient#clientId

Unique permanent client ID. Can be used to track this machine.

Type: string.

TestClient#connected

Is leader tab connected to server.

Type: boolean.

TestClient#log

Client events log.

client.log.add(action)

Type: TestLog.

TestClient#node

Node instance to synchronize logs.

if (client.node.state === 'synchronized')

Type: ClientNode.

TestClient#nodeId

Unique Logux node ID.

console.log('Client ID: ', client.nodeId)

Type: string.

TestClient#options

Client options.

console.log('Connecting to ' + client.options.server)

Type: ClientOptions.

TestClient#pair

Connection between client and server.

Type: TestPair.

TestClient#server

Virtual server to test client.

expect(client.server.log.actions()).toEqual([
  { type: 'logux/subscribe', channel: 'users/10' }
])

Type: TestServer.

TestClient#state

Leader tab synchronization state. It can differs from client.node.state (because only the leader tab keeps connection).

client.on('state', () => {
  if (client.state === 'disconnected' && client.state === 'sending') {
    showCloseWarning()
  }
})

Type: ClientNode.

TestClient#tabId

Unique tab ID. Can be used to add an action to the specific tab.

Without localStorage (React Native) there are it is an empty string.

client.log.add(action, { tab: client.tabId })

Type: string.

TestClient#changeUser(userId, token?)

Disconnect from the server, update user, and connect again with new credentials.

onAuth(async (userId, token) => {
  showLoader()
  client.changeUser(userId, token)
  await client.node.waitFor('synchronized')
  hideLoader()
})

You need manually chang user ID in all browser tabs.

ArgumentTypeDescription
userIdstringThe new user ID.
token ?stringCredentials for new user.

TestClient#clean()

Clear stored data. Removes action log from IndexedDB if you used it.

Databases, subscribed to the cleaning event, are cleaned before the log, so the log can’t fill them back.

signout.addEventListener('click', async () => {
  await client.clean()
  location.reload()
})

Returns Promise. Promise when all data will be removed.

TestClient#connect()

Connect to virtual server.

await client.connect()

Returns Promise. Promise until connection will be established.

TestClient#destroy()

Disconnect and stop synchronization.

shutdown.addEventListener('click', () => {
  client.destroy()
})

TestClient#disconnect()

Disconnect from virtual server.

client.disconnect()

TestClient#on(event, listener)

ArgumentType
event"user"
listener(userId: string) => void
ArgumentTypeDescription
event"cleaning"The event name.
listener() => void | PromiseThe listener function.
ArgumentTypeDescription
event"state"The event name.
listener() => voidThe listener function.
ArgumentType
event"add" | "clean" | "preadd"
listenerClientActionListener

Returns Unsubscribe.

TestClient#sent(test)

Collect actions sent by client during the test call.

let answers = await client.sent(async () => {
  client.log.add({ type: 'local' })
})
expect(actions).toEqual([{ type: 'local' }])
ArgumentTypeDescription
test() => void | PromiseFunction, where do you expect action will be received

Returns Promise<Action[]>. Promise with all received actions

TestClient#start(connect?)

Connect to server and reconnect on any connection problem.

client.start()
ArgumentTypeDescription
connect ?booleanStart connection immediately.

TestClient#subscribed(channel)

Does client subscribed to specific channel.

let user = new UserStore(client, '10')
await delay(10)
expect(client.subscribed('users/10')).toBe(true)
ArgumentTypeDescription
channelstringChannel name.

Returns boolean. Does client has an active subscription.

TestClient#sync(action, meta?)

Send action to the server (by setting meta.sync and adding to the log) and track server processing.

showLoader()
client.sync(
  { type: 'CHANGE_NAME', name }
).then(() => {
  hideLoader()
}).catch(error => {
  hideLoader()
  showError(error.action.reason)
})
ArgumentTypeDescription
actionSyncActionThe action
meta ?Partial<ClientMeta>Optional meta.

Returns Promise<ClientMeta>. Promise for server processing.

TestClient#type(type, listener, opts?)

Add listener for adding action with specific type. Works faster than on('add', cb) with if.

client.type('rename', (action, meta) => {
  name = action.name
})
ArgumentTypeDescription
typeTypeAction["type"]Action’s type.
listenerClientActionListener
opts ?{ event?: "add" | "clean" | "preadd", id?: string }
ArgumentTypeDescription
actionCreatorCreatorAction creator function.
listenerClientActionListener
opts ?{ event?: "add" | "clean" | "preadd", id?: string }

Returns Unsubscribe. Unbind listener from event.

TestClient#waitFor(state)

Wait for specific state of the leader tab.

await client.waitFor('synchronized')
hideLoader()
ArgumentTypeDescription
stateClientNodeState name

Returns Promise.

Extends Log.

Log to be used in tests. It already has memory store, node ID, and special test timer.

Use TestTime to create test log.

import { TestTime } from '@logux/core'

it('tests log', () => {
  const log = TestTime.getLog()
})

it('tests 2 logs', () => {
  const time = new TestTime()
  const log1 = time.nextLog()
  const log2 = time.nextLog()
})

TestLog#nodeId

Unique node ID. It is used in action IDs.

Type: string.

TestLog#store

Log store.

Type: LogStore.

TestLog#actions()

Return all action (without metadata) inside log, sorted by created time.

This shortcut works only with MemoryStore.

expect(log.action).toEqual([
  { type: 'A' }
])

Returns Action[].

TestLog#add(action, meta?)

Add action to log.

It will set id, time (if they was missed) and added property to meta and call all listeners.

removeButton.addEventListener('click', () => {
  log.add({ type: 'users:remove', user: id })
})
ArgumentTypeDescription
actionNewActionThe new action.
meta ?Partial<LogMeta>Open structure for action metadata.
ArgumentType
entries[AnyAction, ?][]

Returns Promise<false | LogMeta>. Promise with meta if action was added to log or false if action was already in log.

TestLog#addReason(reasons, criteria?)

Add reason tags to metadata of actions, which are already in the log. Reasons, which action already has, will not be duplicated.

// The action still owns these cells, so it should keep their reasons
log.addReason('last-value', { id: meta.id })
ArgumentTypeDescription
reasonsstring | string[]The reason name or names.
criteria ?CriteriaCriteria to select actions for reason adding.

Returns Promise. Promise when adding will be finished.

TestLog#byId(id)

Does log already has action with this ID.

if (action.type === 'logux/undo') {
  const [undidAction, undidMeta] = await log.byId(action.id)
  log.changeMeta(meta.id, { reasons: undidMeta.reasons })
}
ArgumentTypeDescription
idstringAction ID.

Returns Promise<[null, null] | [Action, LogMeta]>. Promise with array of action and metadata.

TestLog#changeMeta(id, diff)

Change action metadata. You will remove action by setting reasons: [].

await process(action)
log.changeMeta(action, { status: 'processed' })
ArgumentTypeDescription
idstringAction ID.
diffPartial<LogMeta>Object with values to change in action metadata.

Returns Promise<boolean>. Promise with true if metadata was changed or false on unknown ID.

TestLog#each(opts, callback)

ArgumentTypeDescription
optsGetOptionsIterator options.
callbackActionIteratorFunction will be executed on every action.
ArgumentTypeDescription
callbackActionIteratorFunction will be executed on every action.
ArgumentTypeDescription
callbackActionIteratorFunction will be executed on every action.

Returns Promise.

TestLog#entries()

Return all entries (with metadata) inside log, sorted by created time.

This shortcut works only with MemoryStore.

expect(log.action).toEqual([
  [{ type: 'A' }, { id: '1 test1 0', time: 1, added: 1, reasons: ['t'] }]
])

Returns [Action, ClientMeta][].

TestLog#generateId()

Generate next unique action ID.

const id = log.generateId()

Returns string. Unique ID for action.

TestLog#keepActions()

Keep actions without meta.reasons in the log by setting test reason during adding to the log.

log.keepActions()
log.add({ type: 'test' })
log.actions() //=> [{ type: 'test' }]

TestLog#now()

Current time of this node. Log uses it for meta.time and action ID.

Redefine it to use a custom clock, for instance, in tests.

log.now = () => fakeTime

Returns number. Milliseconds since UNIX epoch.

TestLog#on(event, listener)

Subscribe for log events. It implements nanoevents API. Supported events:

  • preadd: when somebody try to add action to log. It fires before ID check. The best place to add reason.
  • add: when new action was added to log.
  • clean: when action was cleaned from store.
  • batch: when actions from a single Log#add() call were added.

Note, that Log#type() will work faster than on event with if.

log.on('preadd', (action, meta) => {
  if (action.type === 'beep') {
    meta.reasons.push('test')
  }
})
ArgumentTypeDescription
event"add" | "clean"The event name.
listenerReadonlyListenerThe listener function.
ArgumentTypeDescription
event"preadd"The event name.
listenerPreaddListenerThe listener function.
ArgumentTypeDescription
event"batch"The event name.
listener(entries: [Action, LogMeta][]) => voidThe listener function.

Returns Unsubscribe. Unbind listener from event.

TestLog#removeReason(reasons, criteria?)

Remove reason tags from actions’ metadata and remove actions without reasons from log.

onSync(lastSent) {
  log.removeReason('unsynchronized', { maxAdded: lastSent })
}
ArgumentTypeDescription
reasonsstring | string[]The reason name or names.
criteria ?CriteriaCriteria to select actions for reason removing.

Returns Promise. Promise when cleaning will be finished.

TestLog#type(type, listener, opts?)

Add listener for adding action with specific type. Works faster than on('add', cb) with if.

Setting opts.id will filter events ponly from actions with specific action.id.

const unbind = log.type('beep', (action, meta) => {
  beep()
})
function disableBeeps () {
  unbind()
}
ArgumentTypeDescription
typeNewAction["type"]Action’s type.
listenerReadonlyListenerThe listener function.
opts ?{ event?: "add" | "clean", id?: string }
ArgumentTypeDescription
typeNewAction["type"]Action’s type.
listenerPreaddListenerThe listener function.
opts{ event: "preadd", id?: string }

Returns Unsubscribe. Unbind listener from event.

Extends LocalPair.

Two paired loopback connections with events tracking to be used in Logux tests.

import { TestPair } from '@logux/core'
it('tracks events', async () => {
  const pair = new TestPair()
  const client = new ClientNode(pair.right)
  await pair.left.connect()
  expect(pair.leftEvents).toEqual('connect')
  await pair.left.send(msg)
  expect(pair.leftSent).toEqual([msg])
})
ParameterTypeDescription
delay ?numberDelay for connection and send events. Default is 1.

TestPair#delay

Delay for connection and send events to emulate real connection latency.

Type: number.

TestPair#left

First connection. Will be connected to right one after connect().

new ClientNode('client, log1, pair.left)

Type: LocalConnection.

TestPair#leftEvents

Emitted events from left connection.

await pair.left.connect()
pair.leftEvents //=> [['connect']]

Type: string[][].

TestPair#leftNode

Node instance used in this test, connected with left.

function createTest () {
  test = new TestPair()
  test.leftNode = ClientNode('client', log, test.left)
  return test
}

Type: BaseNode.

TestPair#leftSent

Sent messages from left connection.

await pair.left.send(msg)
pair.leftSent //=> [msg]

Type: Message[].

TestPair#right

Second connection. Will be connected to right one after connect().

new ServerNode('server, log2, pair.right)

Type: LocalConnection.

TestPair#rightEvents

Emitted events from right connection.

await pair.right.connect()
pair.rightEvents //=> [['connect']]

Type: string[][].

TestPair#rightNode

Node instance used in this test, connected with right.

function createTest () {
  test = new TestPair()
  test.rightNode = ServerNode('client', log, test.right)
  return test
}

Type: BaseNode.

TestPair#rightSent

Sent messages from right connection.

await pair.right.send(msg)
pair.rightSent //=> [msg]

Type: Message[].

TestPair#clear()

Clear all connections events and messages to test only last events.

await client.connection.connect()
pair.clear() // Remove all connecting messages
await client.log.add({ type: 'a' })
expect(pair.leftSent).toEqual([
  ['sync', …]
])

TestPair#wait(receiver?)

Return Promise until next event.

pair.left.send(['test'])
await pair.wait('left')
pair.leftSend //=> [['test']]
ArgumentTypeDescription
receiver ?"left" | "right"Wait for specific receiver event.

Returns Promise<TestPair>. Promise until next event.

Virtual server to test client.

let client = new TestClient()
client.server //=> TestServer

TestServer#log

All actions received from the client.

expect(client.server.log.actions()).toEqual([
  { type: 'logux/subscribe', channel: 'users/10' }
])

Type: TestLog.

TestServer#freezeProcessing(test)

Stop to response with logux/processed on all new action and send logux/processed for all received actions when test callback will be finished.

await client.server.freezeProcessing(() => {
  user.rename('Another name')
  expect(user.nameIsSaving).toBe(true)
})
await delay(10)
expect(user.nameIsSaving).toBe(false)
ArgumentTypeDescription
test() => PromiseFunction, where server will not send logux/processed.

Returns Promise. Promise until test will be finished.

TestServer#onChannel(channel, response)

Define server’s responses for specific channel.

Second call with the same channel name will override previous data.

  client.server.onChannel('users/10', [
    { type: 'users/name', userId: 10, value: 'New name' }
  ])
  let user = new UserStore(client, '10')
  await delay(10)
  expect(user.name).toEqual('New name')
ArgumentTypeDescription
channelstringThe channel name.
responseanyActions to send back on subscription.

TestServer#resend(type, resend)

Set channels for client’s actions.

ArgumentTypeDescription
typeResentAction["type"]Action type.
resend(action: ResentAction, meta: ClientMeta) => string | string[]Callback returns channel name.

TestServer#sendAll(action, meta?)

Send action to all connected clients.

client.server.sendAll(action)
ArgumentTypeDescription
actionSentActionAction.
meta ?anyAction‘s meta.

Returns Promise.

TestServer#undoAction(action, reason?, extra?)

Response with logux/undo instead of logux/process on receiving specific action.

client.server.undoAction(
  { type: 'rename', userId: '10', value: 'Bad name' }
)
user.rename('Good name')
user.rename('Bad name')
await delay(10)
expect(user.name).toEqual('Good name')
ArgumentTypeDescription
actionRevertedActionAction to be undone on receiving
reason ?stringOptional code for reason. Default is 'error'.
extra ?objectExtra fields to logux/undo action.

TestServer#undoNext(reason?, extra?)

Response with logux/undo instead of logux/process on next action from the client.

client.server.undoNext()
user.rename('Another name')
await delay(10)
expect(user.name).toEqual('Old name')
ArgumentTypeDescription
reason ?stringOptional code for reason. Default is 'error'.
extra ?objectExtra fields to logux/undo action.

Creates special logs for test purposes.

Real logs use real time in actions ID, so log content will be different on every test execution.

To fix it Logux has special logs for tests with simple sequence timer. All logs from one test should share same time. This is why you should use log creator to share time between all logs in one test.

import { TestTime } from '@logux/core'

it('tests log', () => {
  const log = TestTime.getLog()
})

it('tests 2 logs', () => {
  const time = new TestTime()
  const log1 = time.nextLog()
  const log2 = time.nextLog()
})

TestTime.getLog(opts?)

Shortcut to create time and generate single log. Use it only if you need one log in test.

it('tests log', () => {
  const log = TestTime.getLog()
})
ArgumentTypeDescription
opts ?TestLogOptionsLog options.

Returns TestLog.

TestTime#lastId

Last letd number in log’s nodeId.

Type: number.

TestTime#nextLog(opts?)

Return next test log in same time.

it('tests 2 logs', () => {
  const time = new TestTime()
  const log1 = time.nextLog()
  const log2 = time.nextLog()
})
ArgumentTypeDescription
opts ?TestLogOptionsLog options.

Returns TestLog.

emptyInTest(Template)

Disable loader for filter for this builder.

import { emptyInTest, cleanStores } from '@logux/client'

beforeEach(() => {
  prepareForTest(client, User, { name: 'Test user 1' })
  prepareForTest(client, User, { name: 'Test user 2' })
})

afterEach(() => {
  cleanStores(User)
})
ArgumentTypeDescription
TemplateSyncMapTemplateStore builder.

prepareForTest

Create and load stores to builder’s cache to use them in tests or storybook.

import { prepareForTest, cleanStores, TestClient } from '@logux/client'

import { User } from '../store'

let client = new TestClient('10')

beforeEach(() => {
  prepareForTest(client, User, { name: 'Test user 1' })
  prepareForTest(client, User, { name: 'Test user 2' })
})

afterEach(() => {
  cleanStores(User)
})

Type: PrepareForTest.


Base methods for synchronization nodes. Client and server nodes are based on this module.

ParameterTypeDescription
nodeIdstringUnique current machine name.
logNodeLogLogux log instance to be synchronized.
connectionConnectionConnection to remote node.
options ?NodeOptionsSynchronization options.

BaseNode#authenticated

Did we finish remote node authentication.

Type: boolean.

BaseNode#connected

Is synchronization in process.

node.on('disconnect', () => {
  node.connected //=> false
})

Type: boolean.

BaseNode#connection

Connection used to communicate to remote node.

Type: Connection.

BaseNode#initializing

Promise for node data initial loadiging.

Type: Promise.

BaseNode#lastReceived

Latest remote node’s log added time, which was successfully synchronized. It will be saves in log store.

Type: number.

BaseNode#lastSent

Latest current log added time, which was successfully synchronized. It will be saves in log store.

Type: number.

BaseNode#localNodeId

Unique current machine name.

console.log(node.localNodeId + ' is started')

Type: string.

BaseNode#localProtocol

Used Logux protocol.

if (tool.node.localProtocol !== 1) {
  throw new Error('Unsupported Logux protocol')
}

Type: number.

BaseNode#log

Log for synchronization.

Type: NodeLog.

BaseNode#minProtocol

Minimum version of Logux protocol, which is supported.

console.log(`You need Logux protocol ${node.minProtocol} or higher`)

Type: number.

BaseNode#options

Synchronization options.

Type: NodeOptions.

BaseNode#remoteHeaders

Headers set by remote node. By default, it is an empty object.

let message = I18N_ERRORS[node.remoteHeaders.language || 'en']
node.log.add({ type: 'error', message })

Type: Headers | EmptyHeaders.

BaseNode#remoteNodeId

Unique name of remote machine. It is undefined until nodes handshake.

console.log('Connected to ' + node.remoteNodeId)

Type: string | undefined.

BaseNode#remoteProtocol

Remote node Logux protocol. It is undefined until nodes handshake.

if (node.remoteProtocol >= 5) {
  useNewAPI()
} else {
  useOldAPI()
}

Type: number | undefined.

BaseNode#remoteSubprotocol

Remote node’s application subprotocol version.

It is undefined until nodes handshake. If remote node will not send on handshake its subprotocol, it will be set to 0.

if (node.remoteSubprotocol > 9) {
  useNewAPI()
} else {
  useOldAPI()
}

Type: number | undefined.

BaseNode#state

Current synchronization state.

  • disconnected: no connection.
  • connecting: connection was started and we wait for node answer.
  • sending: new actions was sent, waiting for answer.
  • synchronized: all actions was synchronized and we keep connection.
node.on('state', () => {
  if (node.state === 'sending') {
    console.log('Do not close browser')
  }
})

Type: NodeState.

BaseNode#timeFix

Time difference between nodes.

Type: number.

BaseNode#catch(listener)

Disable throwing a error on error message and create error listener.

node.catch(error => {
  console.error(error)
})
ArgumentTypeDescription
listener(error: LoguxError) => voidThe error listener.

Returns Unsubscribe. Unbind listener from event.

BaseNode#destroy()

Shut down the connection and unsubscribe from log events.

connection.on('disconnect', () => {
  server.destroy()
})

BaseNode#on(event, listener)

ArgumentType
event"headers"
listener(headers: Headers) => void
ArgumentType
event"synced"
listener(synced: number) => void
ArgumentType
event"clientError" | "error"
listener(error: LoguxError) => void
ArgumentTypeDescription
event"connect" | "debug" | "headers" | "state"Event name.
listener() => voidThe listener function.
ArgumentType
event"debug"
listener(type: "error", data: string) => void

Returns Unsubscribe.

BaseNode#setLocalHeaders(headers)

Set headers for current node.

if (navigator) {
  node.setLocalHeaders({ language: navigator.language })
}
node.connection.connect()
ArgumentTypeDescription
headersHeadersThe data object will be set as headers for current node.

BaseNode#waitFor(state)

Return Promise until sync will have specific state.

If current state is correct, method will return resolved Promise.

await node.waitFor('synchronized')
console.log('Everything is synchronized')
ArgumentTypeDescription
stateNodeStateThe expected synchronization state value.

Returns Promise. Promise until specific state.

Abstract interface for connection to synchronize logs over it. For example, WebSocket or Loopback.

Connection#connected

Is connection is enabled.

Type: boolean.

Connection#destroy

Disconnect and unbind all even listeners.

Type: () => void.

Connection#connect()

Start connection. Connection should be in disconnected state from the beginning and start connection only on this method call.

This method could be called again if connection moved to disconnected state.

Returns Promise. Promise until connection will be established.

Connection#disconnect(reason?)

Finish current connection.

ArgumentTypeDescription
reason ?"destroy" | "error" | "timeout"Disconnection reason.

Connection#on(event, listener)

ArgumentType
event"disconnect"
listener(reason: string) => void
ArgumentType
event"error"
listener(error: Error) => void
ArgumentTypeDescription
event"connect" | "connecting" | "disconnect"Event name.
listener() => voidEvent listener.
ArgumentType
event"message"
listener(msg: Message) => void

Returns Unsubscribe.

Connection#send(message)

Send message to connection.

ArgumentTypeDescription
messageMessageThe message to be sent.

Stores actions with time marks. Log is main idea in Logux. In most end-user tools you will work with log and should know log API.

import Log from '@logux/core'
const log = new Log({
  store: new MemoryStore(),
  nodeId: 'client:134'
})

log.on('add', beeper)
log.add({ type: 'beep' })
ParameterTypeDescription
optsLogOptionsLog options.

Log#nodeId

Unique node ID. It is used in action IDs.

Type: string.

Log#store

Log store.

Type: Store.

Log#add(action, meta?)

Add action to log.

It will set id, time (if they was missed) and added property to meta and call all listeners.

removeButton.addEventListener('click', () => {
  log.add({ type: 'users:remove', user: id })
})
ArgumentTypeDescription
actionNewActionThe new action.
meta ?Partial<LogMeta>Open structure for action metadata.
ArgumentType
entries[AnyAction, ?][]

Returns Promise<false | LogMeta>. Promise with meta if action was added to log or false if action was already in log.

Log#addReason(reasons, criteria?)

Add reason tags to metadata of actions, which are already in the log. Reasons, which action already has, will not be duplicated.

// The action still owns these cells, so it should keep their reasons
log.addReason('last-value', { id: meta.id })
ArgumentTypeDescription
reasonsstring | string[]The reason name or names.
criteria ?CriteriaCriteria to select actions for reason adding.

Returns Promise. Promise when adding will be finished.

Log#byId(id)

Does log already has action with this ID.

if (action.type === 'logux/undo') {
  const [undidAction, undidMeta] = await log.byId(action.id)
  log.changeMeta(meta.id, { reasons: undidMeta.reasons })
}
ArgumentTypeDescription
idstringAction ID.

Returns Promise<[null, null] | [Action, LogMeta]>. Promise with array of action and metadata.

Log#changeMeta(id, diff)

Change action metadata. You will remove action by setting reasons: [].

await process(action)
log.changeMeta(action, { status: 'processed' })
ArgumentTypeDescription
idstringAction ID.
diffPartial<LogMeta>Object with values to change in action metadata.

Returns Promise<boolean>. Promise with true if metadata was changed or false on unknown ID.

Log#each(opts, callback)

ArgumentTypeDescription
optsGetOptionsIterator options.
callbackActionIteratorFunction will be executed on every action.
ArgumentTypeDescription
callbackActionIteratorFunction will be executed on every action.
ArgumentTypeDescription
callbackActionIteratorFunction will be executed on every action.

Returns Promise.

Log#generateId()

Generate next unique action ID.

const id = log.generateId()

Returns string. Unique ID for action.

Log#now()

Current time of this node. Log uses it for meta.time and action ID.

Redefine it to use a custom clock, for instance, in tests.

log.now = () => fakeTime

Returns number. Milliseconds since UNIX epoch.

Log#on(event, listener)

Subscribe for log events. It implements nanoevents API. Supported events:

  • preadd: when somebody try to add action to log. It fires before ID check. The best place to add reason.
  • add: when new action was added to log.
  • clean: when action was cleaned from store.
  • batch: when actions from a single Log#add() call were added.

Note, that Log#type() will work faster than on event with if.

log.on('preadd', (action, meta) => {
  if (action.type === 'beep') {
    meta.reasons.push('test')
  }
})
ArgumentTypeDescription
event"add" | "clean"The event name.
listenerReadonlyListenerThe listener function.
ArgumentTypeDescription
event"preadd"The event name.
listenerPreaddListenerThe listener function.
ArgumentTypeDescription
event"batch"The event name.
listener(entries: [Action, LogMeta][]) => voidThe listener function.

Returns Unsubscribe. Unbind listener from event.

Log#removeReason(reasons, criteria?)

Remove reason tags from actions’ metadata and remove actions without reasons from log.

onSync(lastSent) {
  log.removeReason('unsynchronized', { maxAdded: lastSent })
}
ArgumentTypeDescription
reasonsstring | string[]The reason name or names.
criteria ?CriteriaCriteria to select actions for reason removing.

Returns Promise. Promise when cleaning will be finished.

Log#type(type, listener, opts?)

Add listener for adding action with specific type. Works faster than on('add', cb) with if.

Setting opts.id will filter events ponly from actions with specific action.id.

const unbind = log.type('beep', (action, meta) => {
  beep()
})
function disableBeeps () {
  unbind()
}
ArgumentTypeDescription
typeNewAction["type"]Action’s type.
listenerReadonlyListenerThe listener function.
opts ?{ event?: "add" | "clean", id?: string }
ArgumentTypeDescription
typeNewAction["type"]Action’s type.
listenerPreaddListenerThe listener function.
opts{ event: "preadd", id?: string }

Returns Unsubscribe. Unbind listener from event.

Extends LogStore.

Simple memory-based log store.

It is good for tests, but not for server or client usage, because it store all data in memory and will lose log on exit.

import { MemoryStore } from '@logux/core'

var log = new Log({
  nodeId: 'server',
  store: new MemoryStore()
})

MemoryStore#entries

Actions in the store.

Type: [Action, ClientMeta][].

MemoryStore#add(action, meta)

Add action to store. Action always will have type property.

ArgumentTypeDescription
actionAnyActionThe action to add.
metaClientMetaAction’s metadata.

Returns Promise<false | ClientMeta>. Promise with meta for new action or false if action with same meta.id was already in store.

MemoryStore#addReason(reasons, criteria)

Add reasons to metadata of actions, which are already in the store.

Reasons, which action already has, should not be duplicated.

ArgumentTypeDescription
reasonsstring[]The reason names.
criteriaCriteriaCriteria to select actions for reason adding.

Returns Promise. Promise when adding will be finished.

MemoryStore#byId(id)

Return action by action ID.

ArgumentTypeDescription
idstringAction ID.

Returns Promise<[null, null] | [Action, ClientMeta]>. Promise with array of action and metadata.

MemoryStore#changeMeta(id, diff)

Change action metadata.

ArgumentTypeDescription
idstringAction ID.
diffPartial<ClientMeta>Object with values to change in action metadata.

Returns Promise<boolean>. Promise with true if metadata was changed or false on unknown ID.

MemoryStore#clean()

Remove all data from the store.

Returns Promise. Promise when cleaning will be finished.

MemoryStore#get(opts?)

Return a Promise with first page. Page object has entries property with part of actions and next property with function to load next page. If it was a last page, next property should be empty.

This tricky API is used, because log could be very big. So we need pagination to keep them in memory.

ArgumentTypeDescription
opts ?GetOptionsQuery options.

Returns Promise<LogPage>. Promise with first page.

MemoryStore#getLastAdded()

Return biggest added number in store. All actions in this log have less or same added time.

Returns Promise<number>. Promise with biggest added number.

MemoryStore#getLastSynced()

Get added values for latest synchronized received/sent events.

Returns Promise<LastSynced>. Promise with added values

MemoryStore#remove(id)

Remove action from store.

ArgumentTypeDescription
idstringAction ID.

Returns Promise<false | [Action, ClientMeta]>. Promise with entry if action was in store.

MemoryStore#removeReason(reasons, criteria, callback)

Remove reasons from action’s metadata and remove actions without reasons.

ArgumentTypeDescription
reasonsstring[]The reason names.
criteriaCriteriaCriteria to select actions for reason removing.
callbackReadonlyListenerCallback for every removed action.

Returns Promise. Promise when cleaning will be finished.

MemoryStore#setLastSynced(values)

Set added value for latest synchronized received or/and sent events.

ArgumentTypeDescription
valuesPartial<LastSynced>Object with latest sent or received values.

Returns Promise. Promise when values will be saved to store.

Extends Connection.

Wrapper for Connection for re-connecting it on every disconnect.

import { ClientNode, Reconnect } from '@logux/core'
const recon = new Reconnect(connection)
new ClientNode(nodeId, log, recon, options)
ParameterTypeDescription
connectionConnectionThe connection to be re-connectable.
options ?ReconnectOptionsRe-connection options.

Reconnect#attempts

Fails attempts since the last connected state.

Type: number.

Reconnect#connected

Is connection is enabled.

Type: boolean.

Reconnect#connecting

Are we in the middle of connecting.

Type: boolean.

Reconnect#connection

Wrapped connection.

Type: Connection.

Reconnect#destroy

Unbind all listeners and disconnect. Use it if you will not need this class anymore.

Type: () => void.

Reconnect#options

Re-connection options.

Type: ReconnectOptions.

Reconnect#reconnecting

Should we re-connect connection on next connection break. Next connect call will set to true.

function lastTry () {
  recon.reconnecting = false
}

Type: boolean.

Reconnect#connect()

Start connection. Connection should be in disconnected state from the beginning and start connection only on this method call.

This method could be called again if connection moved to disconnected state.

Returns Promise. Promise until connection will be established.

Reconnect#disconnect(reason?)

Finish current connection.

ArgumentTypeDescription
reason ?"destroy" | "error" | "timeout"Disconnection reason.

Reconnect#on(event, listener)

ArgumentType
event"disconnect"
listener(reason: string) => void
ArgumentType
event"error"
listener(error: Error) => void
ArgumentTypeDescription
event"connect" | "connecting" | "disconnect"Event name.
listener() => voidEvent listener.
ArgumentType
event"message"
listener(msg: Message) => void

Returns Unsubscribe.

Reconnect#send(message)

Send message to connection.

ArgumentTypeDescription
messageMessageThe message to be sent.

defineAction

Type: DefineAction.

isFirstOlder(firstMeta, secondMeta)

Compare time, when log entries were created.

It uses meta.time and meta.id to detect entries order.

import { isFirstOlder } from '@logux/core'
if (isFirstOlder(lastBeep, meta) {
  beep(action)
  lastBeep = meta
}
ArgumentTypeDescription
firstMetastring | ClientMeta | undefinedSome action’s metadata.
secondMetastring | ClientMeta | undefinedOther action’s metadata.

Returns boolean.

parseId(id)

Parse meta.id or Node ID into component: user ID, client ID, node ID.

import { parseId } from '@logux/core'
const { userId, clientId } = parseId(meta.id)
ArgumentTypeDescription
idstringAction or Node ID

Returns IDComponents.


AbstractActionCreator(args)

ArgumentType
argsany[]

Returns CreatedAction.

AbstractActionCreator#type

Type: string.

AbstractCrdtTable

PropertyType
pluralstring
create(fields: AbstractNewCrdtRow[] | AbstractNewCrdtRow) => Promise<string | string[]>
update(id: string, diff: Partial<RowFields>) => Promise

AbstractNewCrdtRow

Type: { id?: string } & CreateFields.

Action

PropertyTypeDescription
typestringAction type name.

ActionCreator(args)

ArgumentType
argsCreatorArgs

Returns CreatedAction.

ActionCreator#match

Type: (action: Action) => action is CreatedAction.

ActionCreator#type

Type: string.

ActionFilter(action, meta)

ArgumentType
actionAction
metaClientMeta

Returns false | Promise<false | [Action, ClientMeta]> | [Action, ClientMeta].

ActionIterator(action, meta)

ArgumentType
actionAction
metaLogMeta

Returns void | boolean.

ActionListener(action, meta)

ArgumentType
actionListenAction
metaany

Returns void | Promise.

ActionPacker

Packer of actions with binary parts to binary format to use it in custom packers in SQL-based log stores.

PropertyType
pack(action: FullAction) => PackedAction | undefined
unpack(action: PackedAction) => FullAction

ActionPackerMap

Checks that every key of the packers map is equal to type of the action, which packer packs.

function createStore<Packers extends ActionPackerMap<Packers>>(
  packers: Packers
): Store

createStore({ '0': zeroPacker })

Type: { [Type: keyof Packers]: Type ? ActionPacker : never }.

AnyAction

PropertyType
typestring

Authenticator(nodeId, token, headers)

ArgumentType
nodeIdstring
tokenstring
headersobject | Headers

Returns Promise<boolean>.

AuthStore

Auth store. Use createAuth to create it.

PropertyTypeDescription
loadingPromiseWhile store is loading initial state.

BadgeMessages

PropertyType
deniedstring
disconnectedstring
errorstring
protocolErrorstring
sendingstring
syncErrorstring
synchronizedstring
waitstring

BadgeOptions

PropertyTypeDescription
duration ?numberSynchronized state duration. Default is 3000.
messagesBadgeMessagesWidget text for different states.
position ?"bottom-center" | "bottom-left" | "bottom-right" | "middle-center" | "middle-left" | "middle-right" | "top-center" | "top-left" | "top-right"Widget position. Default is bottom-right.
stylesBadgeStylesInline styles for different states.

BadgeStyles

PropertyType
baseobject
connectingobject
disconnectedobject
errorobject
icon{ disconnected: string, error: string, protocolError: string, sending: string, synchronized: string, wait: string }
protocolErrorobject
sendingobject
synchronizedobject
textobject
waitobject

ChannelDeniedError

Type: LoguxUndoError.

ChannelError

Type: ChannelDeniedError | ChannelNotFoundError | ChannelServerError.

ChannelNotFoundError

Type: LoguxUndoError.

ChannelServerError

Type: LoguxUndoError.

ClientActionListener(action, meta)

ArgumentType
actionListenAction
metaClientMeta

ClientMeta

PropertyTypeDescription
noAutoReason ?booleanDisable setting timeTravel reason.
sync ?booleanThis action should be synchronized with other browser tabs and server.
tab ?stringAction should be visible only for browser tab with the same client.tabId.

Extends BaseNode.

Client node in synchronization pair.

Instead of server node, it initializes synchronization and sends connect message.

import { ClientNode } from '@logux/core'
const connection = new BrowserConnection(url)
const node = new ClientNode(nodeId, log, connection)
ParameterTypeDescription
nodeIdstringUnique current machine name.
logNodeLogLogux log instance to be synchronized.
connectionConnectionConnection to remote node.
options ?NodeOptionsSynchronization options.

ClientNode#authenticated

Did we finish remote node authentication.

Type: boolean.

ClientNode#connected

Is synchronization in process.

node.on('disconnect', () => {
  node.connected //=> false
})

Type: boolean.

ClientNode#connection

Connection used to communicate to remote node.

Type: Connection.

ClientNode#initializing

Promise for node data initial loadiging.

Type: Promise.

ClientNode#lastReceived

Latest remote node’s log added time, which was successfully synchronized. It will be saves in log store.

Type: number.

ClientNode#lastSent

Latest current log added time, which was successfully synchronized. It will be saves in log store.

Type: number.

ClientNode#localNodeId

Unique current machine name.

console.log(node.localNodeId + ' is started')

Type: string.

ClientNode#localProtocol

Used Logux protocol.

if (tool.node.localProtocol !== 1) {
  throw new Error('Unsupported Logux protocol')
}

Type: number.

ClientNode#log

Log for synchronization.

Type: NodeLog.

ClientNode#minProtocol

Minimum version of Logux protocol, which is supported.

console.log(`You need Logux protocol ${node.minProtocol} or higher`)

Type: number.

ClientNode#options

Synchronization options.

Type: NodeOptions.

ClientNode#remoteHeaders

Headers set by remote node. By default, it is an empty object.

let message = I18N_ERRORS[node.remoteHeaders.language || 'en']
node.log.add({ type: 'error', message })

Type: Headers | EmptyHeaders.

ClientNode#remoteNodeId

Unique name of remote machine. It is undefined until nodes handshake.

console.log('Connected to ' + node.remoteNodeId)

Type: string | undefined.

ClientNode#remoteProtocol

Remote node Logux protocol. It is undefined until nodes handshake.

if (node.remoteProtocol >= 5) {
  useNewAPI()
} else {
  useOldAPI()
}

Type: number | undefined.

ClientNode#remoteSubprotocol

Remote node’s application subprotocol version.

It is undefined until nodes handshake. If remote node will not send on handshake its subprotocol, it will be set to 0.

if (node.remoteSubprotocol > 9) {
  useNewAPI()
} else {
  useOldAPI()
}

Type: number | undefined.

ClientNode#state

Current synchronization state.

  • disconnected: no connection.
  • connecting: connection was started and we wait for node answer.
  • sending: new actions was sent, waiting for answer.
  • synchronized: all actions was synchronized and we keep connection.
node.on('state', () => {
  if (node.state === 'sending') {
    console.log('Do not close browser')
  }
})

Type: NodeState.

ClientNode#timeFix

Time difference between nodes.

Type: number.

ClientNode#catch(listener)

Disable throwing a error on error message and create error listener.

node.catch(error => {
  console.error(error)
})
ArgumentTypeDescription
listener(error: LoguxError) => voidThe error listener.

Returns Unsubscribe. Unbind listener from event.

ClientNode#destroy()

Shut down the connection and unsubscribe from log events.

connection.on('disconnect', () => {
  server.destroy()
})

ClientNode#on(event, listener)

ArgumentType
event"headers"
listener(headers: Headers) => void
ArgumentType
event"synced"
listener(synced: number) => void
ArgumentType
event"clientError" | "error"
listener(error: LoguxError) => void
ArgumentTypeDescription
event"connect" | "debug" | "headers" | "state"Event name.
listener() => voidThe listener function.
ArgumentType
event"debug"
listener(type: "error", data: string) => void

Returns Unsubscribe.

ClientNode#setLocalHeaders(headers)

Set headers for current node.

if (navigator) {
  node.setLocalHeaders({ language: navigator.language })
}
node.connection.connect()
ArgumentTypeDescription
headersHeadersThe data object will be set as headers for current node.

ClientNode#waitFor(state)

Return Promise until sync will have specific state.

If current state is correct, method will return resolved Promise.

await node.waitFor('synchronized')
console.log('Everything is synchronized')
ArgumentTypeDescription
stateNodeStateThe expected synchronization state value.

Returns Promise. Promise until specific state.

ClientOptions

PropertyTypeDescription
allowDangerousProtocol ?booleanDo not show warning when using ws:// in production.
attempts ?numberMaximum reconnection attempts. Default is Infinity.
maxDelay ?numberMaximum delay between reconnections. Default is 5000.
minDelay ?numberMinimum delay between reconnections. Default is 1000.
ping ?numberMilliseconds since last message to test connection by sending ping. Default is 10000.
prefix ?stringPrefix for IndexedDB database to run multiple Logux instances in the same browser. Default is logux.
serveranyServer URL.
store ?anyStore to save log data. Default is MemoryStore.
subprotocolnumberClient subprotocol version.
time ?anyTest time to test client.
timeout ?numberTimeout in milliseconds to break connection. Default is 70000.
token ?anyClient credentials for authentication.
userIdstringUser ID.

Convertor

PropertyType
decode(str: string) => Value
encode(value: Value) => string

CrdtActionOptions

PropertyTypeDescription
version ?numberVersion of the callback’s logic. Adding or removing an action re-creates the database from the log automatically, but a change inside the callback is not visible to the library. Increase this number to re-create the database after such change.

CrdtAppliedListener

Listener of the applied event.

Type: (tx: Database, action: Action, meta: ClientMeta | MetaTime, won: CrdtCell[], touched: CrdtCell[]) => Promise | void.

CrdtCell

Single field of a single row: [table, id, field].

Type: [table: string, id: string, field: string].

CrdtColumn

Column definition created by string, number, bigint, boolean, oneOf and optional builders.

Type is the JS type of the column value in rows. RequiredOnCreate marks whether CrdtTable#create requires the field (columns wrapped in optional() or having default don’t).

PropertyTypeDescription
default ?Type | () => Type
requiredRequiredOnCreate
sql ?string | { [key: string]: string }
type"BIGINT" | "BOOLEAN" | "DOUBLE PRECISION" | "TEXT"SQL column type used in CREATE TABLE.
values ? readonly string[]

CrdtColumnOptions

PropertyTypeDescription
default ?() => NoInfer | NoInferDefault value or function to get it. Column with default becomes optional in CrdtTable#create. The default is resolved when the create action is added and is stored inside the action, so replaying the log is deterministic.
sql ?string | { [key: Dialects]: string }Extra SQL appended to the column definition, like 'UNIQUE COLLATE NOCASE'. Pass a string to use it for every database, or an object with CrdtDatabaseOptions#dialect names as keys to set SQL per database dialect.

CrdtColumnType

Type: Column ? Type : never.

CrdtColumnValue

JS types of column values. Only JSON types are supported, because all values are stored in Logux actions and passed to the database driver as-is. Store dates as a number of milliseconds in bigint columns.

Type: SyncMapTypes.

CrdtCorruption

Reason why the local database can not be trusted anymore:

  • error: the database threw the error and can not be prepared.
  • interrupted-migration: the tab was closed during the schema migration, between the drop of the tables and the end of the replay.
  • lost-database: the data is gone. The client did not find the database it was using before: the file was lost or replaced by an empty one.
  • timeout: the database did not answer in CrdtDatabaseOptions#timeout.

Type: "error" | "interrupted-migration" | "lost-database" | "timeout".

CrdtCreateFields

Fields accepted by CrdtTable#create. Columns wrapped in optional or having default can be omitted.

Type: { [Column: keyof Schema]?: undefined ? Exclude<CrdtColumnType,undefined> | null : CrdtColumnType } & { [Column: keyof Schema]: CrdtColumnType }.

CrdtDatabase

PropertyTypeDescription
readyPromisePromise resolved when the database was prepared and tables can be used.
statusReadableAtomDatabase preparing status:
tablesCrdtTablesSchemas of all tables of CrdtDatabase#table by their plural.
action(creator: Creator, apply: (tx: Database, action: ReturnType<Creator>, meta: ClientMeta) => void | Promise, opts?: CrdtActionOptions) => (args: Parameters) => Promise
clean() => Promise
destroy() => void
empty() => Promise
on(event: "applied", listener: CrdtAppliedListener) => Unsubscribe
table(plural: string, schema: Schema, indexes?: CrdtIndex[]) => CrdtTable

CrdtDatabaseOptions

PropertyTypeDescription
dialect ?DialectSQL dialect of the database: 'sqlite' (default), 'pglite' or any other name for your own dialect. The dialect selects per-dialect extra column SQL in CrdtColumnOptions#sql and prohibits boolean columns in SQLite (values are passed to the database driver without conversion and SQLite has no boolean type).
key ?stringStorage key to store the schema version (also used as the prefix of the leader tab lock name). Change it when the database is used in a third-party widget to avoid conflicts with the website’s own Logux database. Default is logux:db.
storage ?PersistentStorageStorage to keep the tables schema instead of localStorage (for instance, for React Native or tests).
sync ?booleanShould table actions be sent to the server. Default is true.
timeout ?numberMilliseconds to wait for the database to be prepared.
repeat ?() => [Action, MetaTime][] | Promise<[Action, MetaTime][]>

CrdtIndex

Index definition for CrdtDatabase#table. It can be:

  • a column name for a single-column index;
  • an array of columns for a multi-column index;
  • an object with columns and unique;
  • an object with the whole CREATE INDEX statement in sql for partial indexes, expressions and dialect-specific features.
let user = crdt.table('user', schema, [
  'email',
  ['teamId', 'name'],
  { columns: ['email'], unique: true },
  {
    sql: `CREATE INDEX IF NOT EXISTS "user_active" ON "user" ("name")` +
      ` WHERE "role" = 'admin'`
  }
])

Type: { columns: CrdtIndexColumn[], unique?: boolean } | { sql: string } | CrdtIndexColumn | CrdtIndexColumn[].

CrdtIndexColumn

Column in CrdtIndex: a column name from the table schema, id, or an updatedAt_field column.

Everything after the column name is passed to the database as is, so it can contain DESC, COLLATE or an operator class:

let post = crdt.table('post', schema, [
  'publishedAt DESC',
  'title COLLATE NOCASE'
])

Type: `${CrdtIndexName} ${string}` | CrdtIndexName.

CrdtParsedAction

PropertyTypeDescription
pluralstringTable name from the action type.
rows[id: string, fields: string[]][]Rows of the action with the names of the fields it writes to them. A plural/deleted action writes no fields.
verbCrdtVerbVerb from the action type.

CrdtParsedType

PropertyTypeDescription
pluralstringTable name from the action type.
verbCrdtVerbVerb from the action type.

CrdtRowFields

Row fields (without id and fields meta) inferred from table schema. Optional columns take null to clear the value (undefined fields are not changed, like in JSON).

Type: { [Column: keyof Schema]?: Exclude<CrdtColumnType,undefined> | null } & { [Column: keyof Schema]: CrdtColumnType }.

CrdtSqlParam

Values for SQL template parameters of CrdtTable#select. Parameters are passed to the database driver as-is, without any conversion. Booleans are allowed only in dialects with boolean columns (not in 'sqlite').

Type: Dialect ? never : boolean | number | string.

CrdtTable

PropertyTypeDescription
driverDriverDatabase driver for raw queries to the table, like in crdtTableToActions.
pluralstringTable name. It is used as SQL table name and as prefix of action types (user/created, user/changed, user/deleted).
schemaSchemaColumn definitions of the table.
change(tx: Database, id: string | string[], fields: Partial<CrdtRowFields>, meta: ClientMeta) => Promise<CrdtCell[]>
create(rows: NewCrdtRow[]) => Promise<string[]>
delete(id: string | string[]) => Promise
select(sql?: TemplateStringsArray, params: CrdtSqlParam[]) => SqlStore
update(id: string | string[], diff: Partial<CrdtRowFields>) => Promise

CrdtTableChangedAction

Type: { fields: Partial<Fields>, id: string, type: string } | { fields: Partial<Fields>, ids: string[], type: string }.

CrdtTableCreatedAction

Type: { fields: Fields, id: string, type: string } | { records: Fields & { id: string }[], type: string }.

CrdtTableDeletedAction

Type: { id: string, type: string } | { ids: string[], type: string }.

CrdtTableRow

Table row returned by CrdtTable#select. Rows contain data as the database driver returns it, without any conversion. Missing optional columns are null.

Every field has an extra updatedAt_field column with Logux Meta ID of the last action which changed it (null if the field was never set). They are used to resolve conflicts with per-field last write wins, and can be used in SQL, for instance, to sort by the last change:

let $recent = user.select`ORDER BY "updatedAt_name" DESC`

Type: { id: string } & { [Column: keyof Schema]: undefined ? Exclude<CrdtColumnType,undefined> | null : CrdtColumnType } & { [Column: keyof Schema]: null | string }.

CrdtTables

Schemas of the tables by their plural, like CrdtDatabase#tables.

Type: { [key: string]: CrdtTableSchema }.

CrdtTableSchema

CrdtTasks

PropertyType
add(task: () => void | Promise) => void
destroy() => void
finish() => Promise

CrdtTasksOptions

PropertyTypeDescription
onError ?(error: unknown) => voidCalled when the task throws.

CrdtVerb

Type: "changed" | "created" | "deleted".

CreateClientStore(userIdStore, builder)

Create stores to keep client instance and update it on user ID changes.

import { createClientStore, Client, log } from '@logux/client'
import { persistentMap } from '@nanostores/persistent'

let sessionStore = persistentMap<{ userId: string }>('session:', {
  userId: 'anonymous'
})

export const clientStore = createClientStore(sessionStore, session => {
  let client new Client({
    subprotocol: SUBPROTOCOL,
    server: 'ws://example.com',
    userId: session.userId
  })
  log(client)
  return client
})
ArgumentTypeDescription
userIdStoreMapStore<UserId>Store with object and userId key.
builder(value: UserId) => ClientCallback which return client
ArgumentType
userIdStoreMapStore<UserId>
builder(value: UserId) => Client | undefined

Returns Atom<Client>. Atom store with client

Criteria

PropertyTypeDescription
exceptIndex ?stringDo not change reasons for actions with this index in meta.indexes.
id ?stringChange reasons only for action with id.
ids ?string[]Change reasons only for actions with these IDs.
index ?stringChange reasons only for actions with this index in meta.indexes.
maxAdded ?numberChange reasons only for actions with lower added.
minAdded ?numberChange reasons only for actions with bigger added.
olderThan ?ClientMetaChange reasons only for actions older than specific action.
youngerThan ?ClientMetaChange reasons only for actions younger than specific action.

Dialects

Type: "sqlite" | "pglite".

EmptyHeaders

PropertyTypeDescription

Fields

Filter

Type: { [Key: keyof Value]?: Value[Key] }.

FilterOptions

PropertyType
listChangesOnly ?boolean

FilterStore

Type: FilterStoreExt & MapStore<FilterValue>.

FilterStoreExt

PropertyTypeDescription
loadingPromiseWhile store is loading initial data from server or log.

FilterValue

Type: { isLoading: true } | LoadedFilterValue.

GetOptions

PropertyTypeDescription
index ?stringGet entries with a custom index.
order ?"added" | "created"Sort entries by created time or when they was added to current log.
reason ?stringGet only entries with this reason.

ID

Action unique ID across all nodes.

"OzcVoWD 380:R7BNGA:1"

Type: string.

IDComponents

PropertyType
clientIdstring
nodeIdstring
userIdstring | undefined

LastSynced

PropertyTypeDescription
receivednumberThe added value of latest received event.
sentnumberThe added value of latest sent event.

LoadableStore

Type: { loading: Promise<unknown> } & ReadableAtom.

LoadedFilter

Type: FilterStoreExt & MapStore<LoadedFilterValue>.

LoadedFilterValue

PropertyType
isEmptyboolean
isLoadingfalse
listLoadedSyncMapValue[]
storesMap<string,SyncMapStore>

LoadedSyncMap

Type: MapStore<LoadedSyncMapValue> & SyncMapStoreExt.

LoadedSyncMapValue

Type: { id: string, isLoading: false } & Value.

LoadedValue

Type: { isLoading: false } & Type.

Extends Connection.

Abstract interface for connection to synchronize logs over it. For example, WebSocket or Loopback.

LocalConnection#connected

Is connection is enabled.

Type: boolean.

LocalConnection#destroy

Disconnect and unbind all even listeners.

Type: () => void.

LocalConnection#connect()

Start connection. Connection should be in disconnected state from the beginning and start connection only on this method call.

This method could be called again if connection moved to disconnected state.

Returns Promise. Promise until connection will be established.

LocalConnection#disconnect(reason?)

Finish current connection.

ArgumentTypeDescription
reason ?"destroy" | "error" | "timeout"Disconnection reason.

LocalConnection#on(event, listener)

ArgumentType
event"disconnect"
listener(reason: string) => void
ArgumentType
event"error"
listener(error: Error) => void
ArgumentTypeDescription
event"connect" | "connecting" | "disconnect"Event name.
listener() => voidEvent listener.
ArgumentType
event"message"
listener(msg: Message) => void

Returns Unsubscribe.

LocalConnection#other()

Returns LocalConnection.

LocalConnection#send(message)

Send message to connection.

ArgumentTypeDescription
messageMessageThe message to be sent.

Two paired loopback connections.

import { LocalPair, ClientNode, ServerNode } from '@logux/core'
const pair = new LocalPair()
const client = new ClientNode('client', log1, pair.left)
const server = new ServerNode('server', log2, pair.right)
ParameterTypeDescription
delay ?numberDelay for connection and send events. Default is 1.

LocalPair#delay

Delay for connection and send events to emulate real connection latency.

Type: number.

LocalPair#left

First connection. Will be connected to right one after connect().

new ClientNode('client, log1, pair.left)

Type: LocalConnection.

LocalPair#right

Second connection. Will be connected to right one after connect().

new ServerNode('server, log2, pair.right)

Type: LocalConnection.

LogMessages

PropertyTypeDescription
add ?booleanDisable action added messages.
clean ?booleanDisable action cleaned messages.
error ?booleanDisable error messages.
ignoreActions ?string[]Disable action messages with specific types.
role ?booleanDisable tab role messages.
state ?booleanDisable connection state messages.
user ?booleanDisable user ID changing.

LogOptions

PropertyTypeDescription
nodeIdstringUnique current machine name.
storeStoreStore for log.

LogPage

PropertyTypeDescription
entries[Action, ClientMeta][]Pagination page.
next ?() => Promise<LogPage>

Every Store class should provide 8 standard methods.

LogStore#add(action, meta)

Add action to store. Action always will have type property.

ArgumentTypeDescription
actionAnyActionThe action to add.
metaClientMetaAction’s metadata.

Returns Promise<false | ClientMeta>. Promise with meta for new action or false if action with same meta.id was already in store.

LogStore#addReason(reasons, criteria)

Add reasons to metadata of actions, which are already in the store.

Reasons, which action already has, should not be duplicated.

ArgumentTypeDescription
reasonsstring[]The reason names.
criteriaCriteriaCriteria to select actions for reason adding.

Returns Promise. Promise when adding will be finished.

LogStore#byId(id)

Return action by action ID.

ArgumentTypeDescription
idstringAction ID.

Returns Promise<[null, null] | [Action, ClientMeta]>. Promise with array of action and metadata.

LogStore#changeMeta(id, diff)

Change action metadata.

ArgumentTypeDescription
idstringAction ID.
diffPartial<ClientMeta>Object with values to change in action metadata.

Returns Promise<boolean>. Promise with true if metadata was changed or false on unknown ID.

LogStore#clean()

Remove all data from the store.

Returns Promise. Promise when cleaning will be finished.

LogStore#get(opts?)

Return a Promise with first page. Page object has entries property with part of actions and next property with function to load next page. If it was a last page, next property should be empty.

This tricky API is used, because log could be very big. So we need pagination to keep them in memory.

ArgumentTypeDescription
opts ?GetOptionsQuery options.

Returns Promise<LogPage>. Promise with first page.

LogStore#getLastAdded()

Return biggest added number in store. All actions in this log have less or same added time.

Returns Promise<number>. Promise with biggest added number.

LogStore#getLastSynced()

Get added values for latest synchronized received/sent events.

Returns Promise<LastSynced>. Promise with added values

LogStore#remove(id)

Remove action from store.

ArgumentTypeDescription
idstringAction ID.

Returns Promise<false | [Action, ClientMeta]>. Promise with entry if action was in store.

LogStore#removeReason(reasons, criteria, callback)

Remove reasons from action’s metadata and remove actions without reasons.

ArgumentTypeDescription
reasonsstring[]The reason names.
criteriaCriteriaCriteria to select actions for reason removing.
callbackReadonlyListenerCallback for every removed action.

Returns Promise. Promise when cleaning will be finished.

LogStore#setLastSynced(values)

Set added value for latest synchronized received or/and sent events.

ArgumentTypeDescription
valuesPartial<LastSynced>Object with latest sent or received values.

Returns Promise. Promise when values will be saved to store.

Extends Error.

Logux error in logs synchronization.

if (error.name === 'LoguxError') {
  console.log('Server throws: ' + error.description)
}
ParameterTypeDescription
typeErrorTypeThe error code.
options ?LoguxErrorOptions[ErrorType]The error option.
received ?booleanWas error received from remote node.

LoguxError.description(type, options?)

Return a error description by it code.

ArgumentTypeDescription
typeTypeThe error code.
options ?LoguxErrorOptions[Type]The errors options depends on error code.

Returns string.

LoguxError#description

Human-readable error description.

console.log('Server throws: ' + error.description)

Type: string.

LoguxError#message

Full text of error to print in debug message.

Type: string.

LoguxError#name

Always equal to LoguxError. The best way to check error class.

if (error.name === 'LoguxError') {

Type: "LoguxError".

LoguxError#options

Error options depends on error type.

if (error.type === 'timeout') {
  console.error('A timeout was reached (' + error.options + ' ms)')
}

Type: LoguxErrorOptions[ErrorType].

LoguxError#received

Was error received from remote client.

Type: boolean.

LoguxError#stack

Calls which cause the error.

Type: string.

LoguxError#type

The error code.

if (error.type === 'timeout') {
  fixNetwork()
}

Type: ErrorType.

LoguxErrorOptions

PropertyType
bruteforcevoid
timeoutnumber
unknown-messagestring
wrong-credentialsvoid
wrong-formatstring
wrong-protocolVersions
wrong-subprotocolVersions

Extends Error.

An error for load() callback to return logux/undo with 404.

import { LoguxNotFoundError } from '@logux/actions'

server.channel('posts/:id', {
  load () {
    throw new LoguxNotFoundError()
  },
  …
})

LoguxNotFoundError#name

Type: "LoguxNotFoundError".

LoguxProcessedAction

PropertyType
idstring
type"logux/processed"

LoguxSubscribeAction

PropertyType
channelstring
creating ?true
filter ?{ }
since ?{ id: string, time: number }
type"logux/subscribe"

LoguxSubscribedAction

PropertyType
channelstring
type"logux/subscribed"

LoguxUndoAction

PropertyType
actionRevertedAction
idstring
reasonReason
type"logux/undo"

Extends Error.

Error on logux/undo action from the server.

try {
  client.sync(action)
} catch (e) {
  if (e.name === 'LoguxUndoError') {
    console.log(e.action.action.type ' was undid')
  }
}
ParameterType
actionRevertedAction

LoguxUndoError#action

Server logux/undo action. It has origin actions (which was undid) in action.action.

console.log(error.action.action.type ' was undid')

Type: RevertedAction.

LoguxUndoError#name

The better way to check error, than instanceof.

if (error.name === 'LoguxUndoError') {

Type: "LoguxUndoError".

LoguxUnsubscribeAction

PropertyType
channelstring
filter ?{ }
type"logux/unsubscribe"

Message

Type: ["connect", number, string, number, ?] | ["connected", number, string, [number, number], ?] | ["debug", "error", string] | ["error", keyof LoguxErrorOptions, ?] | ["headers", object] | ["ping", number] | ["pong", number] | ["sync", number, ...AnyAction | SyncMeta[]] | ["synced", number].

Meta

PropertyTypeDescription
addednumberSequence number of action in current log. Log fills it.
idstringAction unique ID. Log sets it automatically.
indexes ?string[]Indexes for action quick extraction.
keepLast ?stringSet value to reasons and this reason from old action.
reasonsstring[]Why action should be kept in log. Action without reasons will be removed.
subprotocol ?numberApplication subprotocol version.
timenumberAction created time in current node time. Milliseconds since UNIX epoch.

MetaTime

Type: Pick<ClientMeta,"time" | "id">.

NewCrdtRow

Row accepted by CrdtTable#create: CrdtCreateFields with optional id, which will be generated if omitted.

Type: { id?: string } & CrdtCreateFields.

NodeOptions

PropertyTypeDescription
auth ?AuthenticatorFunction to check client credentials.
fixTime ?booleanDetect difference between client and server and fix time in synchronized actions.
onReceive ?ActionFilterFunction to filter or change actions coming from remote node’s before put it to current log.
onSend ?ActionFilterFunction to filter or change actions before sending to remote node’s.
ping ?numberMilliseconds since last message to test connection by sending ping.
subprotocol ?numberApplication subprotocol version.
syncBatch ?numberMaximum actions in a single sync message. 100 by default. Node will split a bigger batch into a few messages, so the remote node will be able to apply them by parts.
timeout ?numberTimeout in milliseconds to wait answer before disconnect.
token ?string | TokenGeneratorClient credentials. For example, access token.

NodeState

Type: "connecting" | "disconnected" | "sending" | "synchronized".

OmitFromUnion

Omit which is applied to each member of the union separately. It is necessary for actions with different shapes, like batch actions.

Type: Type ? Omit<Type,Keys> : never.

PackedAction

PropertyType
actionReducedAction
blobUint8Array

PersistentStorage

Type: { [key: string]: string | undefined }.

PreactErrorHandlers

PropertyType
AccessDenied ?any
Error ?any
NotFound ?any

PreaddListener(action, meta)

ArgumentType
actionListenerAction
metaLogMeta

PrepareForTest(client, Template, value)

ArgumentType
clientClient
TemplateSyncMapTemplateLike
value{ id?: string } & Omit<Value,"id">
ArgumentType
clientClient
TemplateSyncMapTemplate
value{ id?: string } & Omit<Value,"id">

Returns MapStore<Value>.

ReactErrorHandlers

PropertyType
AccessDenied ?any
Error ?any
NotFound ?any

ReadonlyListener(action, meta)

ArgumentType
actionListenerAction
metaLogMeta

ReconnectOptions

PropertyTypeDescription
attempts ?numberMaximum reconnecting attempts.
maxDelay ?numberMaximum delay between re-connecting.
minDelay ?numberMinimum delay between re-connecting.

Reducer

PropertyTypeDescription
readyPromisePromise resolved when the data was prepared and all actions from the log were reduced.
statusReadableAtom
destroy() => void
type(type: TypeAction["type"], listener: ActionListener) => void

ReducerInitCallbacks

PropertyTypeDescription
storage ?PersistentStorageStorage to keep the reducer’s version instead of localStorage (for instance, for React Native or tests).
clean(oldVersion: number) => void | [Action, MetaTime][] | Promise | Promise<[Action, MetaTime][]>
init ?() => void | Promise
migrating ?(done: Promise) => void
stop ?() => void

ReducerMigrationStatus

Type: "initializing" | "migrating" | "outdated" | "ready".

RequestOptions

PropertyTypeDescription
allowDangerousProtocol ?booleanDo not show warning when using ws:// in production.
attempts ?numberMaximum reconnection attempts. Default is Infinity.
maxDelay ?numberMaximum delay between reconnections. Default is 5000.
minDelay ?numberMinimum delay between reconnections. Default is 1000.
ping ?numberMilliseconds since last message to test connection by sending ping. Default is 10000.
prefix ?stringPrefix for IndexedDB database to run multiple Logux instances in the same browser. Default is logux.
serveranyServer URL.
store ?anyStore to save log data. Default is MemoryStore.
subprotocolnumberClient subprotocol version.
time ?anyTest time to test client.
timeout ?numberTimeout in milliseconds to break connection. Default is 70000.
token ?anyClient credentials for authentication.
userId ?string

ShadowAction

PropertyType
idstring
type"shadow"

Extends unknown.

SQL store for Logux log on top of @nanostores/sql database.

It works with any driver of @nanostores/sql: SQLite in the browser by SQLocal, PGlite, Expo SQLite in React Native, or Node.js SQLite in tests.

import { CrossTabClient } from '@logux/client'
import { SqlLogStore } from '@logux/client/db'
import { openDb } from '@nanostores/sql'
import { sqlocalDriver } from '@nanostores/sql/sqlocal'

const db = openDb(sqlocalDriver('app.sqlite'))
const client = new CrossTabClient({
  …,
  store: new SqlLogStore(db)
})

The store keeps actions in logux_log, logux_reason, logux_index, and logux_extra tables. They will be created on the first query.

Version of the tables format is kept in logux_version table. If the database was created by a newer version of the client, all methods will throw an error.

ParameterTypeDescription
dbDatabaseDatabase from @nanostores/sql openDb().
opts ?SqlLogStoreOptionsStore options.

SqlLogStore#onTransactionAdd(callback)

Set the callback, which will be called inside the transaction writing the action to the log. It allows to apply the action to the tables of the same database atomically: the action and its result will be committed together, and await of log.add() will mean that the tables were already changed.

createCrdtDatabase sets it automatically, if the log and the CRDT tables are in the same database.

An error in the callback rolls back the whole transaction, so the action will not be added to the log.

ArgumentTypeDescription
callback(tx: Database, action: AnyAction, meta: ClientMeta) => void | Promise | undefinedCallback or undefined to remove the previous one.

SqlLogStoreOptions

PropertyTypeDescription
packers ?PackersPackers to keep the binary parts of the actions in a separate column instead of Base64 inside the JSON.

StatusListener(current, details)

ArgumentType
currentStatusValue
details{ action: LoguxUndoAction, meta: ClientMeta } | { error: Error } | undefined

StatusOptions

PropertyTypeDescription
duration ?numberSynchronized state duration. Default is 3000.

StatusValue

Type: "connecting" | "connectingAfterWait" | "denied" | "disconnected" | "error" | "protocolError" | "sending" | "sendingAfterWait" | "syncError" | "synchronized" | "synchronizedAfterWait" | "wait" | "wrongCredentials".

StorageActionListener(prevValue, action, meta)

ArgumentType
prevValueValue
actionListenAction
metaany

Returns Value | Promise<Value>.

StorageCallbacks

PropertyTypeDescription
storage ?PersistentStorageStorage to keep the value and the reducer’s version instead of localStorage (for instance, for React Native or tests).
migrating ?(done: Promise) => void
repeat() => [Action, MetaTime][] | Promise<[Action, MetaTime][]>

StorageReducer

PropertyTypeDescription
readyPromisePromise resolved when the value was loaded and all actions from the log were reduced. It is also resolved when the reducer became outdated.
statusReadableAtom
valueReadableAtomThe reduced value.
destroy() => void
type(type: TypeAction["type"], listener: StorageActionListener) => void

SyncMapChangeAction

PropertyType
fieldsPartial<Omit<Value,"id">>
idstring
typestring

SyncMapChangedAction

PropertyType
fieldsPartial<Omit<Value,"id">>
idstring
typestring

SyncMapCreateAction

PropertyType
fieldsOmit<Value,"id">
idstring
typestring

SyncMapCreatedAction

PropertyType
fieldsOmit<Value,"id">
idstring
typestring

SyncMapDeleteAction

PropertyType
idstring
typestring

SyncMapDeletedAction

PropertyType
idstring
typestring

SyncMapStore

Type: MapStore<SyncMapValue> & SyncMapStoreExt.

SyncMapStoreExt

PropertyTypeDescription
clientClientLogux Client instance.
createdAt ?anyMeta from create action if the store was created locally.
deleted ?trueMark that store was deleted.
loadingPromiseWhile store is loading initial data from server or log.
offlinebooleanDoes store keep data in the log after store is destroyed.
pluralstringName of map class.
remotebooleanDoes store use server to load and save data.

SyncMapTemplate(id, client, args)

ArgumentType
idstring
clientClient
args[] | [Action, ClientMeta, boolean | undefined]

Returns any.

SyncMapTemplate#cache

Type: { }.

SyncMapTemplate#offline

Type: boolean.

SyncMapTemplate#plural

Type: string.

SyncMapTemplate#remote

Type: boolean.

SyncMapTemplateLike(id, client, args)

ArgumentType
idstring
clientClient
argsArgs

Returns MapStore<Value>.

SyncMapTypes

Type: boolean | null | number | string | undefined.

SyncMapValue

Type: { id: string, isLoading: true } | LoadedSyncMapValue.

SyncMapValues

SyncMeta

PropertyType
idstring
subprotocol ?number
timenumber

TabID

Type: string.

TestClientOptions

PropertyType
headers ?Headers
server ?TestServer
subprotocol ?number

TestLogOptions

PropertyTypeDescription
nodeId ?stringUnique log name.
store ?LogStoreStore for log. Will use MemoryStore by default.

TokenGenerator()

Returns string | Promise<string>.

Versions

PropertyType
supportednumber
usednumber

WithoutMeta

Row without conflict resolution data of every field.

Type: { [Key: keyof Value]: Value[Key] }.

Extends WsConnection.

Logux connection for WebSocket using binary protocol.

Automatically detects text-based peers and falls back to JSON encoding, so it can be used in ServerConnection to handle both binary and text clients.

import { WsBinaryConnection } from '@logux/core'

const connection = new WsBinaryConnection('wss://logux.example.com/')
const node = new ClientNode(nodeId, log, connection, opts)
ParameterTypeDescription
urlstringWebSocket server URL.
Class ?unknown
opts ?unknownExtra option for WebSocket constructor.

WsBinaryConnection#connected

Is connection is enabled.

Type: boolean.

WsBinaryConnection#destroy

Disconnect and unbind all even listeners.

Type: () => void.

WsBinaryConnection#textMode

Whether to use text JSON protocol instead of binary. Always true for WsConnection, can change in WsBinaryConnection.

Type: boolean.

WsBinaryConnection#ws

WebSocket instance.

Type: WS.

WsBinaryConnection#connect()

Start connection. Connection should be in disconnected state from the beginning and start connection only on this method call.

This method could be called again if connection moved to disconnected state.

Returns Promise. Promise until connection will be established.

WsBinaryConnection#disconnect(reason?)

Finish current connection.

ArgumentTypeDescription
reason ?"destroy" | "error" | "timeout"Disconnection reason.

WsBinaryConnection#on(event, listener)

ArgumentType
event"disconnect"
listener(reason: string) => void
ArgumentType
event"error"
listener(error: Error) => void
ArgumentTypeDescription
event"connect" | "connecting" | "disconnect"Event name.
listener() => voidEvent listener.
ArgumentType
event"message"
listener(msg: Message) => void

Returns Unsubscribe.

WsBinaryConnection#send(message)

Send message to connection.

ArgumentTypeDescription
messageMessageThe message to be sent.

Extends Connection.

Logux connection for WebSocket.

import { WsConnection } from '@logux/core'

const connection = new WsConnection('wss://logux.example.com/')
const node = new ClientNode(nodeId, log, connection, opts)
ParameterTypeDescription
urlstringWebSocket server URL.
Class ?unknown
opts ?unknownExtra option for WebSocket constructor.

WsConnection#connected

Is connection is enabled.

Type: boolean.

WsConnection#destroy

Disconnect and unbind all even listeners.

Type: () => void.

WsConnection#textMode

Whether to use text JSON protocol instead of binary. Always true for WsConnection, can change in WsBinaryConnection.

Type: boolean.

WsConnection#ws

WebSocket instance.

Type: WS.

WsConnection#connect()

Start connection. Connection should be in disconnected state from the beginning and start connection only on this method call.

This method could be called again if connection moved to disconnected state.

Returns Promise. Promise until connection will be established.

WsConnection#disconnect(reason?)

Finish current connection.

ArgumentTypeDescription
reason ?"destroy" | "error" | "timeout"Disconnection reason.

WsConnection#on(event, listener)

ArgumentType
event"disconnect"
listener(reason: string) => void
ArgumentType
event"error"
listener(error: Error) => void
ArgumentTypeDescription
event"connect" | "connecting" | "disconnect"Event name.
listener() => voidEvent listener.
ArgumentType
event"message"
listener(msg: Message) => void

Returns Unsubscribe.

WsConnection#send(message)

Send message to connection.

ArgumentTypeDescription
messageMessageThe message to be sent.

ZeroAction

PropertyType
compressedboolean
dUint8Array
ivUint8Array
type"0"

ZeroCleanAction

Type: { type: "0/clean" } & { id: string } | { ids: string[] }.

actionEvents(emitter, event, action, meta)

ArgumentType
emitterEmitter
event"add" | "clean" | "preadd"
actionAction
metaClientMeta

badgeEn

English translation for widget.

Type: BadgeMessages.

badgeRu

Russian translation for widget.

Type: BadgeMessages.

badgeStyles

Type: BadgeStyles.

bigint(opts?)

BIGINT column with number value. Use it for timestamps as a number of milliseconds — the same format as dates in Logux actions:

import { bigint } from '@logux/client/db'

let schema = {
  createdAt: bigint({ default: () => Date.now() }),
  publishedAt: optional(bigint())
}
ArgumentTypeDescription
opts ?string | Omit<CrdtColumnOptions,"default">Extra column definition SQL or column options.
ArgumentTypeDescription
opts{ default: () => NoInfer | NoInfer } & CrdtColumnOptionsExtra column definition SQL or column options.

Returns { type: "BIGINT" } & CrdtColumn.

boolean(opts?)

BOOLEAN column for databases with native boolean support like PGlite. SQLite has no boolean type, so with 'sqlite' dialect use number column with 1/0 instead (using this builder there is a type error and throws in CrdtDatabase#table).

ArgumentTypeDescription
opts ?string | Omit<CrdtColumnOptions,"default">Extra column definition SQL or column options.
ArgumentTypeDescription
opts{ default: boolean | () => boolean } & CrdtColumnOptionsExtra column definition SQL or column options.

Returns { type: "BOOLEAN" } & CrdtColumn.

crdtTableToActions(tables)

Restore actions from the tables.

The oldest restored action of a row is plural/created, the rest are plural/changed. Rows changed by one batch action are restored as a single batch action.

let crdt = createCrdtDatabase(client, db, {
  repeat: () => crdtTableToActions([user, post])
})
ArgumentTypeDescription
tablesPick<CrdtTable,"driver" | "plural" | "schema">[]Tables of CrdtDatabase#table to read.

Returns Promise<[Action, MetaTime][]>. Actions with restored meta id and time, oldest first.

createAuth(client)

Create store with user’s authentication state.

import { createAuth } from '@logux/client'

let auth = createAuth(client)
await auth.loading
console.log(auth.get())
ArgumentTypeDescription
clientClientLogux Client.

Returns AuthStore.

createClientStore

Type: CreateClientStore.

createCrdtDatabase(client, db, opts?)

Create CRDT LWW Map tables on top of SQL database filled from Logux log.

Tables are filled by reducing actions from the log (like createReducer). CrdtTable#create, CrdtTable#update and CrdtTable#delete add actions to the log, and the reducer applies them to the database. Their promises are resolved only after the tables were changed, so the next CrdtTable#select will already see the change. They are rejected if applying failed or if the database was stopped before it.

Until the action is applied, it is kept in the log by the applying-to-db reason. Any tab can take the key:apply lock, apply the actions in batches, and remove the reason. Actions of the tab, which was closed in the middle of the work, will be applied on the next start.

Actions are the same as in syncMapTemplate (user/created, user/changed, user/deleted from @logux/actions), so tables are compatible with existing Logux servers and can be mixed with syncMapTemplate stores on other clients. Arrays in CrdtTable#create, CrdtTable#update and CrdtTable#delete produce batch actions (with records or ids instead of id), which are applied to the database in a single query.

Each table has an extra id column and an updatedAt_field column for every field with the Logux Meta ID of its last change, to resolve edit conflicts with per-field last write wins strategy.

The schema version — serialized schemas and indexes of all tables — is kept in the logux_crdt table of the database itself and is copied to localStorage (or to CrdtDatabaseOptions#storage) to tell other tabs about the change. On any schema change all tables (including tables removed from the schema) are dropped and refilled by replaying actions from the log and from the repeat() callback.

While there are actions waiting to be applied to the database, the tab asks the user to confirm closing. It only saves the user from waiting for the next start: it guarantees nothing, since the browser can close the tab without asking, and the actions are not lost anyway.

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

let db = openDb(sqlocalDriver('app.sqlite'))
let crdt = createCrdtDatabase(client, db, {
  async repeat() {
    return await fetchActionsSnapshot()
  }
})

crdt.on('migrating', done => {
  showLoader('Migrating database', done)
})
crdt.on('stop', () => {
  updateAppWarning.show()
})

showLoader('Loading data', crdt.ready)

let user = crdt.table(
  'user',
  {
    age: optional(number()),
    createdAt: bigint({ default: () => Date.now() }),
    email: string('COLLATE NOCASE'),
    isAdmin: number({ default: 0 }),
    name: string(),
    theme: oneOf(['dark', 'light'], { default: 'dark' })
  },
  [{ columns: ['email'], unique: true }, ['isAdmin', 'name']]
)

let id = await user.create({ email: 'ann@example.com', name: 'Ann' })
await user.update(id, { age: 30 })
let $admins = user.select`WHERE "isAdmin" = ${1} ORDER BY "name"`
await user.delete(id)
ArgumentTypeDescription
clientClientLogux client.
dbDatabaseSQL database from @nanostores/sql openDb().
opts ?CrdtDatabaseOptionsDatabase options and the source of old actions.

Returns CrdtDatabase.

createCrdtTasks(crdt, opts?)

Queue for the log writes of the database bookkeeping.

The applied listener runs inside the applying transaction, so it can not await a log write: the store will put the write in its own transaction, which waits for the applying one to commit. The migration replay opens its transactions the same way.

The queue solves both: the tasks are not awaited by the applier, they are serialized between themselves, so they do not overwrite each other’s changes, and they start only after CrdtDatabase#ready.

import { createCrdtTasks } from '@logux/client/db'

let tasks = createCrdtTasks(crdt)

crdt.on('applied', (tx, action, meta, won) => {
  tasks.add(async () => {
    await client.log.removeReason(
      won.map(cell => cell.join('/')),
      { olderThan: meta }
    )
  })
})
ArgumentTypeDescription
crdtPick<CrdtDatabase,"ready">CRDT database from createCrdtDatabase.
opts ?CrdtTasksOptionsQueue options.

Returns CrdtTasks. Task queue.

createReducer(client, name, version, callbacks)

Create long-term persistent Logux actions reducer. It can be used to store Logux data in WASM sqlite or localStorage.

Only one browser tab will reduce the log.

The reducer’s version is removed on Client#clean, since the value is not re-created from the log on the next start. Reducers with their own storage should subscribe to the cleaning event to remove the data itself.

import { createReducer } from '@logux/client'

let db = sqlite.openDatabase('database.sqlite')
createReducer(client, 'db', 10, {
  async clean() {
    await db.close()
    await sqlite.removeFile('database.sqlite')
  },
  async init() {
    db.query('CREATE TABLE users …')
  },
  migrating(done) {
    showLoader('Migrating data', done)
  },
  stop() {
    db.close()
    updateAppWarning.show()
  }
})
  .type('users/create', async action => {
    await db.query(`INSERT INTO users …`)
  })
ArgumentTypeDescription
clientClientLogux client.
namestringThe name of the reducer to use in the storage version key.
versionnumberThe current version to call migrations on new version.
callbacksReducerInitCallbacksThe data migrations callbacks.

Returns Reducer.

createStorageReducer(client, name, version, initialValue, callbacks)

Create a reducer that reduces actions into a single value stored in localStorage (or in StorageCallbacks#storage). The value is loaded on first run and kept in sync across tabs via storage events.

The value is removed on Client#clean together with the log.

import { createStorageReducer } from '@logux/client'

let counter = createStorageReducer(client, 'counter', 1, 0, {
  decode: s => parseInt(s, 10),
  encode: v => String(v),
  repeat() {
    return client.log.each(action => action.type === 'inc')
  }
})
counter.type<{ type: 'inc' }>('inc', prev => prev + 1)
ArgumentType
clientClient
namestring
versionnumber
initialValueNoInfer
callbacksStorageCallbacks
ArgumentType
clientClient
namestring
versionnumber
initialValueValue
callbacksStorageCallbacks & Convertor

Returns StorageReducer.

defineChangedCrdtTable(table)

ArgumentType
tableAbstractCrdtTable

Returns ActionCreator.

defineChangedSyncMap(plural)

ArgumentType
pluralstring

Returns ActionCreator.

defineChangeSyncMap(plural)

ArgumentType
pluralstring

Returns ActionCreator.

defineCrdtTableActions(table)

Returns created/changed/deleted action creators for a CRDT table. Column types are extracted from the table and put into the action fields types.

import { defineCrdtTableActions } from '@logux/actions'

const user = crdt.table('user', { name: string(), age: optional(number()) })

const [
  createdUserAction,
  changedUserAction,
  deletedUserAction
] = defineCrdtTableActions(user)
ArgumentType
tableAbstractCrdtTable

Returns [ActionCreator, ActionCreator, ActionCreator].

defineCreatedCrdtTable(table)

ArgumentType
tableAbstractCrdtTable

Returns ActionCreator.

defineCreatedSyncMap(plural)

ArgumentType
pluralstring

Returns ActionCreator.

defineCreateSyncMap(plural)

ArgumentType
pluralstring

Returns ActionCreator.

defineDeletedCrdtTable(table)

ArgumentType
tableAbstractCrdtTable

Returns ActionCreator.

defineDeletedSyncMap(plural)

ArgumentType
pluralstring

Returns ActionCreator.

defineDeleteSyncMap(plural)

ArgumentType
pluralstring

Returns ActionCreator.

defineSyncMapActions(plural)

Returns actions for CRDT Map.

import { defineSyncMapActions } from '@logux/actions'

const [
  createUserAction,
  changeUserAction,
  deleteUserAction,
  createdUserAction,
  changedUserAction,
  deletedUserAction
] = defineSyncMapActions('users')
ArgumentType
pluralstring

Returns [ActionCreator, ActionCreator, ActionCreator].

eachStoreCheck(test)

Pass all common tests for Logux store to callback.

import { eachStoreCheck } from '@logux/core'

eachStoreCheck((desc, creator) => {
  it(desc, creator(() => new CustomStore()))
})
ArgumentTypeDescription
test(name: string, testCreator: (storeCreator: () => LogStore) => () => void) => voidCallback to create tests in your test framework.

ensureLoaded(value)

Change store’s value type to value with isLoaded: false.

If store is still loading, this function will trow an error.

Use it for tests written on TypeScript.

import { ensureLoaded } from '@logux/client'

expect(ensureLoaded($currentUser)).toEqual({ id: 1, name: 'User' })
ArgumentTypeDescription
valueSyncMapValueStore’s value.
ArgumentTypeDescription
valueFilterValueStore’s value.

Returns LoadedSyncMapValue.

ensureLoadedStore(store)

ArgumentType
storeStore

Returns any.

fake-indexeddb

fake-indexeddb/lib/FDBKeyRange

fromCompat(str)

Decode number from -0-9A-Z_a-z alphabet.

fromCompat('OzcVoWD') //=> 1786312345678
ArgumentTypeDescription
strstringEncoded number.

Returns number. Decoded number.

getRandomSpaces()

Returns string.

idToTime(id)

Decode meta.time from action ID.

idToTime('OzcVoWD client:1') //=> 1786312345678
ArgumentTypeDescription
idstringAction ID or its time part.

Returns number. Milliseconds since UNIX epoch.

isSameClient(id, clientId)

Faster alternative for parseId(meta.id).clientId === clientId check.

It doesn’t create any object or string during the check.

import { isSameClient } from '@logux/core'
if (isSameClient(meta.id, ctx.clientId)) {
  // Action was created by this client
}
ArgumentTypeDescription
idstringAction or Node ID
clientIdstringClient ID to compare with

Returns boolean.

loadValue(store)

Return store’s value if store is loaded or wait until store will be loaded and return its value.

Returns undefined on 404.

import { loadValue } from '@logux/client'

let user = loadValue($currentUser)
ArgumentTypeDescription
storeStoreStore to load.
ArgumentTypeDescription
storeStoreStore to load.

Returns Promise<any>.

loguxProcessed

Returns logux/processed action.

Type: ActionCreator.

loguxSubscribe

Returns logux/subscribe action.

Type: ActionCreator.

loguxSubscribed

Returns logux/subscribed action.

Type: ActionCreator.

loguxUndo(fields)

Returns logux/undo action.

ArgumentType
fields{ action: RevertedAction, id: string, reason: Reason }

Returns LoguxUndoAction.

loguxUnsubscribe

Returns logux/unsubscribe action.

Type: ActionCreator.

number(opts?)

DOUBLE PRECISION column with number value.

ArgumentTypeDescription
opts ?string | Omit<CrdtColumnOptions,"default">Extra column definition SQL or column options.
ArgumentTypeDescription
opts{ default: NoInfer } & CrdtColumnOptionsExtra column definition SQL or column options.

Returns { type: "DOUBLE PRECISION" } & CrdtColumn.

oneOf(values, opts?)

Enum column with union of string values. Stored as TEXT with CHECK constraint.

import { oneOf } from '@logux/client/db'

let schema = {
  role: oneOf(['admin', 'guest', 'user'], { default: 'user' }),
  theme: oneOf(['dark', 'light'])
}
ArgumentTypeDescription
valuesValuesAllowed string values.
opts ?string | Omit<CrdtColumnOptions,"default">Extra column definition SQL or column options.
ArgumentTypeDescription
valuesValuesAllowed string values.
opts{ default: () => Values[number] | Values[number] } & CrdtColumnOptionsExtra column definition SQL or column options.

Returns { type: "TEXT" } & CrdtColumn.

optional(column)

Mark column as optional. The field can be omitted or set to null in CrdtTable#create, can be set to null in CrdtTable#update to clear the value, and is null (SQL NULL) in rows when missing.

import { number, optional } from '@logux/client'

let schema = {
  age: optional(number())
}
ArgumentTypeDescription
columnColumnColumn definition to wrap.

Returns { type: Column["type"] } & CrdtColumn.

parseCrdtAction(action, crdt)

Read the rows and fields of a table action, whatever batch shape it uses (records, ids or a single id).

Policies on the applied event and on the log events need it to know, which rows the action is about, before it was applied.

import { parseCrdtAction } from '@logux/client/db'

client.on('preadd', (action, meta) => {
  let parsed = parseCrdtAction(action, crdt)
  if (!parsed) return
  for (let [id, fields] of parsed.rows) {
    for (let field of fields) {
      meta.reasons.push(`${parsed.plural}/${id}/${field}`)
    }
  }
})

The schemas can be written by hand, so actions can be parsed without the database (in tests or on the server):

parseCrdtAction(action, { tables: { feeds: feedsSchema } })
ArgumentTypeDescription
actionActionAny action from the log.
crdtPick<CrdtDatabase,"tables">Database from createCrdtDatabase or any object with CrdtDatabase#tables to parse actions without the database.

Returns false | CrdtParsedAction. Parsed action or false if it is not an action of these tables.

parseCrdtRows(action)

Split a table action into rows, whatever batch shape it uses (records, ids or a single id). The single place, which knows the shapes: re-implementing them in the app will break on the next shape added here.

The fields are the action’s own values without any check, so the caller must filter them by the table schema: the fields of a records row also contain id, and a plural/deleted action has no fields at all. Use parseCrdtAction to get the field names of the schema instead.

The action type is not checked, so call it only for actions of CrdtDatabase#table.

import { parseCrdtRows } from '@logux/client/db'

for (let [id, fields] of parseCrdtRows(action)) {
  await copyToBackup(id, fields)
}
ArgumentTypeDescription
actionActionTable action.

Returns [id: string, fields: object][]. Row IDs with the fields, which the action writes to them.

parseCrdtType(type, crdt)

Read the table and the verb of the action type, if the action belongs to one of the tables.

Use it when the rows are not necessary, for instance, to find the actions of deleted rows in the log.

import { parseCrdtType } from '@logux/client/db'

client.log.on('clean', action => {
  let parsed = parseCrdtType(action.type, crdt)
  if (parsed?.verb === 'deleted') cleaned.add(parsed.plural)
})
ArgumentTypeDescription
typestringType of any action from the log.
crdtPick<CrdtDatabase,"tables">Database from createCrdtDatabase or any object with CrdtDatabase#tables to parse actions without the database.

Returns false | CrdtParsedType. Table and verb or false if it is not an action of these tables.

replaceWithShadow(client, meta)

Add shadow action to the log. It is useful for client to clean server from encrypted zero actions.

It replaces materialized action in the log, keeping its ID, reasons and indexes, but dropping the body.

By tracking shadow reasons you can detect when you can ask server to remove original action.

import { shadow, zeroClean } from '@logux/actions'
import { replaceWithShadow } from '@logux/client'

client.log.type('logux/processed', async processed => {
  let [action, meta] = await client.log.byId(processed.id)
  if (action) await replaceWithShadow(client, meta)
})

client.log.on('clean', action => {
  if (shadow.match(action)) {
    client.log.add(zeroClean({ ids: [action.id] }), { sync: true })
  }
})
ArgumentTypeDescription
clientClientLogux Client.
metaClientMetaMeta of the original action.

Returns Promise<ClientMeta>. Meta of the added shadow action.

shadow

Returns shadow action. It is useful for client to clean server from encrypted zero actions.

It replaces materialized action in the log, keeping its ID, reasons and indexes, but dropping the body.

By tracking shadow reasons you can detect when you can ask server to remove original action.

Type: ActionCreator.

sortedToMeta(sorted)

Convert string created by toSorted() back to metadata.

sortedToMeta('------Ec test ------Ec') //=> { id: 'Ec test', time: 1000 }
ArgumentTypeDescription
sortedstringString created by toSorted().

Returns MetaTime. Action’s metadata with id and time keys.

status(client, callback, options?)

Low-level function to show Logux synchronization status with your custom UI. It is used in badge widget.

import { status } from '@logux/client'
status(client, current => {
  updateUI(current)
})
ArgumentTypeDescription
clientClientObserved Client instance.
callbackStatusListener
options ?StatusOptions

Returns () => void. Unbind listener.

string(opts?)

TEXT column with string value.

import { string } from '@logux/client'

let schema = {
  email: string('COLLATE NOCASE'),
  name: string(),
  theme: string<'dark' | 'light'>({ default: 'dark' })
}
ArgumentTypeDescription
opts ?string | Omit<CrdtColumnOptions,"default">Extra column definition SQL or column options.
ArgumentTypeDescription
opts{ default: NoInfer } & CrdtColumnOptionsExtra column definition SQL or column options.

Returns { type: "TEXT" } & CrdtColumn.

toCompat(number)

Encode number to -0-9A-Z_a-z alphabet.

Chars are in ASCII order, so strings of the same length have the same order as encoded numbers.

toCompat(64) //=> "0-"
ArgumentTypeDescription
numbernumberNumber to encode.

Returns string. Encoded number.

toSorted(meta)

Convert metadata to a string with the same order as isFirstOlder().

Numbers are padded, so simple string sorting (for instance, in a database column) will return actions in the log order.

db.insert({ action, sorted: toSorted(meta) })
// SELECT * FROM actions ORDER BY sorted
ArgumentTypeDescription
metaMetaTimeAction’s metadata.

Returns string. String to sort actions.

track(client, id)

Track for logux/processed or logux/undo answer from server for the cases when Client#sync can’t be used.

client.type('pay', (action, meta) => {
  track(client, id).then(() => {
    console.log('paid')
  }).catch(() => {
    console.log('unpaid')
  })
})
ArgumentTypeDescription
clientanyLogux Client.
idIDAction ID.

Returns Promise. Promise when action was proccessed.

withMeta(row)

Add empty conflict resolution data to a row built in tests, so it could be compared with rows from CrdtTable#select.

expect(await loadList(user.select())).toEqual([
  withMeta<UserValue>({ id: 'U1', name: 'Ann' })
])
ArgumentTypeDescription
rowWithoutMetaRow without updatedAt_field columns.

Returns Value. Row with null in updatedAt_field column of every field.

withoutMeta(rows)

Remove conflict resolution data, which is local and should not be in the backup or in tests expectations.

expect(withoutMeta(await loadList(user.select()))).toEqual([
  { id: 'U1', name: 'Ann' }
])
ArgumentTypeDescription
rowsValue[]Rows from CrdtTable#select.

Returns WithoutMeta[]. Rows without updatedAt_field columns.

zero

Returns 0 action.

Type: ActionCreator.

zeroClean

Returns 0/clean action.

Type: ActionCreator.

zeroPacker

Packer to 0 action to binary format to use in SQL stores.

Type: ActionPacker.