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()
Unique permanent client ID. Can be used to track this machine.
Type: string.
Is leader tab connected to server.
Type: boolean.
Client events log.
client.log.add(action)
Type: ClientLog.
Node instance to synchronize logs.
if (client.node.state === 'synchronized')
Type: ClientNode.
Unique Logux node ID.
console.log('Client ID: ', client.nodeId)
Type: string.
Client options.
console.log('Connecting to ' + client.options.server)
Type: ClientOptions.
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.
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.
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.
| Argument | Type | Description |
|---|
userId | string | The new user ID. |
token ? | string | Credentials for new user. |
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.
Disconnect and stop synchronization.
shutdown.addEventListener('click', () => {
client.destroy()
})
| Argument | Type |
|---|
event | "user" |
listener | (userId: string) => void |
| Argument | Type | Description |
|---|
event | "cleaning" | The event name. |
listener | () => void | Promise | The listener function. |
| Argument | Type | Description |
|---|
event | "state" | The event name. |
listener | () => void | The listener function. |
Returns Unsubscribe.
Connect to server and reconnect on any connection problem.
client.start()
| Argument | Type | Description |
|---|
connect ? | boolean | Start connection immediately. |
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)
})
| Argument | Type | Description |
|---|
action | SyncAction | The action |
meta ? | Partial<ClientMeta> | Optional meta. |
Returns Promise<ClientMeta>. Promise for server processing.
Add listener for adding action with specific type.
Works faster than on('add', cb) with if.
client.type('rename', (action, meta) => {
name = action.name
})
| Argument | Type | Description |
|---|
type | TypeAction["type"] | Action’s type. |
listener | ClientActionListener | |
opts ? | { event?: "add" | "clean" | "preadd", id?: string } | |
| Argument | Type | Description |
|---|
actionCreator | Creator | Action creator function. |
listener | ClientActionListener | |
opts ? | { event?: "add" | "clean" | "preadd", id?: string } | |
Returns Unsubscribe. Unbind listener from event.
Wait for specific state of the leader tab.
await client.waitFor('synchronized')
hideLoader()
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()
Unique permanent client ID. Can be used to track this machine.
Type: string.
Is leader tab connected to server.
Type: boolean.
Cache for localStorage detection. Can be overridden to disable leader tab
election in tests.
Type: boolean.
Client events log.
client.log.add(action)
Type: ClientLog.
Node instance to synchronize logs.
if (client.node.state === 'synchronized')
Type: ClientNode.
Unique Logux node ID.
console.log('Client ID: ', client.nodeId)
Type: string.
Client options.
console.log('Connecting to ' + client.options.server)
Type: ClientOptions.
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".
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.
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.
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.
| Argument | Type | Description |
|---|
userId | string | The new user ID. |
token ? | string | Credentials for new user. |
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.
Disconnect and stop synchronization.
shutdown.addEventListener('click', () => {
client.destroy()
})
Start web socket reconnection.
| Argument | Type | Description |
|---|
event | "role" | "state" | The event name. |
listener | () => void | The listener function. |
| Argument | Type | Description |
|---|
event | "user" | The event name. |
listener | (userId: string) => void | The listener function. |
| Argument | Type |
|---|
event | "cleaning" |
listener | () => void | Promise |
Returns Unsubscribe.
Connect to server and reconnect on any connection problem.
client.start()
| Argument | Type | Description |
|---|
connect ? | boolean | Start connection immediately. |
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)
})
| Argument | Type | Description |
|---|
action | SyncAction | The action |
meta ? | Partial<ClientMeta> | Optional meta. |
Returns Promise<ClientMeta>. Promise for server processing.
Add listener for adding action with specific type.
Works faster than on('add', cb) with if.
client.type('rename', (action, meta) => {
name = action.name
})
| Argument | Type | Description |
|---|
type | TypeAction["type"] | Action’s type. |
listener | ClientActionListener | |
opts ? | { event?: "add" | "clean" | "preadd", id?: string } | |
| Argument | Type | Description |
|---|
actionCreator | Creator | Action creator function. |
listener | ClientActionListener | |
opts ? | { event?: "add" | "clean" | "preadd", id?: string } | |
Returns Unsubscribe. Unbind listener from event.
Wait for specific state of the leader tab.
await client.waitFor('synchronized')
hideLoader()
Returns Promise.
Extends unknown.
IndexedDB store for Logux log.
import { IndexedStore } from '@logux/client'
const client = new CrossTabClient({
…,
store: new IndexedStore()
})
| Parameter | Type | Description |
|---|
name ? | string | Database name to run multiple Logux instances on same web page. |
Database name.
Type: string.
Highlight tabs on synchronization errors.
import { attention } from '@logux/client'
attention(client)
| Argument | Type | Description |
|---|
client | Client | Observed Client instance. |
Returns () => void. Unbind listener.
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'
})
| Argument | Type | Description |
|---|
client | Client | Observed Client instance. |
opts | BadgeOptions | Widget settings. |
Returns () => void. Unbind badge listener and remove widget from DOM.
Send create action and build store instance.
import { buildNewSyncMap } from '@logux/client'
let userStore = buildNewSyncMap(client, User, {
id: nanoid(),
login: 'test'
})
Returns Promise<any>. Promise with store instance.
Change keys in the store’s value.
import { changeSyncMap } from '@logux/client'
showLoader()
await changeSyncMap(userStore, { name: 'New name' })
hideLoader()
| Argument | Type | Description |
|---|
store | any | Store’s instance. |
diff | Partial<Omit<Value,"id">> | Store’s changes. |
| Argument | Type | Description |
|---|
store | any | Store’s instance. |
key | ValueKey | |
value | Value[ValueKey] | |
Returns Promise. Promise until server validation for remote classes
or saving action to the log of fully offline classes.
Change store without store instance just by store ID.
import { changeSyncMapById } from '@logux/client'
let userStore = changeSyncMapById(client, User, 'user:4hs2jd83mf', {
name: 'New name'
})
Returns Promise. Promise until server validation for remote classes
or saving action to the log of fully offline classes.
Show confirm popup, when user close tab with non-synchronized actions.
import { confirm } from '@logux/client'
confirm(client)
| Argument | Type | Description |
|---|
client | Client | Observed Client instance. |
Returns () => void. Unbind listener.
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())
Returns any.
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()
Returns Promise. Promise until server validation for remote classes
or saving action to the log of fully offline classes.
Delete store.
import { deleteSyncMap } from '@logux/client'
showLoader()
await deleteSyncMap(User)
| Argument | Type | Description |
|---|
store | any | Store’s instance. |
Returns Promise. Promise until server validation for remote classes
or saving action to the log of fully offline classes.
Delete store without store instance just by store ID.
import { deleteSyncMapById } from '@logux/client'
showLoader()
await deleteSyncMapById(client, User, 'user:4hs2jd83mf')
Returns Promise. Promise until server validation for remote classes
or saving action to the log of fully offline classes.
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']
})
| Argument | Type | Description |
|---|
client | Client | Observed Client instance. |
secret | string | CryptoKey | Password for encryption, or a CryptoKey AES key. |
opts ? | { clean?: boolean, ignore?: string[] } | Encryption options. |
Change favicon to show Logux synchronization status.
import { favicon } from '@logux/client'
favicon(client, {
normal: '/favicon.ico',
offline: '/offline.ico',
error: '/error.ico'
})
| Argument | Type | Description |
|---|
client | Client | Observed Client instance. |
links | FaviconLinks | Favicon links. |
Returns () => void. Unbind listener.
Display Logux events in browser console.
import { log } from '@logux/client'
log(client, { ignoreActions: ['user/add'] })
| Argument | Type | Description |
|---|
client | Client | Observed Client instance. |
messages ? | LogMessages | Disable specific message types. |
Returns () => void. Unbind listener.
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)
})
Returns Promise<SentAction>. Action of server response.
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')
| Argument | Type | Description |
|---|
plural | string | Plural 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.
Type: { id?: string } & CreateFields.
| Property | Type | Description |
|---|
type | string | Action type name. |
| Argument | Type |
|---|
args | CreatorArgs |
Returns CreatedAction.
Type: (action: Action) => action is CreatedAction.
| Argument | Type |
|---|
action | Action |
meta | LogMeta |
Returns void | boolean.
| Argument | Type |
|---|
action | ListenAction |
meta | any |
Returns void | Promise.
Packer of actions with binary parts to binary format to use it
in custom packers in SQL-based log stores.
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 }.
| Argument | Type |
|---|
nodeId | string |
token | string |
headers | object | Headers |
Returns Promise<boolean>.
Auth store. Use createAuth to create it.
| Property | Type | Description |
|---|
loading | Promise | While store is loading initial state. |
| Property | Type |
|---|
denied | string |
disconnected | string |
error | string |
protocolError | string |
sending | string |
syncError | string |
synchronized | string |
wait | string |
| Property | Type | Description |
|---|
duration ? | number | Synchronized state duration. Default is 3000. |
messages | BadgeMessages | Widget 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. |
styles | BadgeStyles | Inline styles for different states. |
| Property | Type |
|---|
base | object |
connecting | object |
disconnected | object |
error | object |
icon | { disconnected: string, error: string, protocolError: string, sending: string, synchronized: string, wait: string } |
protocolError | object |
sending | object |
synchronized | object |
text | object |
wait | object |
| Property | Type | Description |
|---|
noAutoReason ? | boolean | Disable setting timeTravel reason. |
sync ? | boolean | This action should be synchronized with other browser tabs and server. |
tab ? | string | Action 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)
| Parameter | Type | Description |
|---|
nodeId | string | Unique current machine name. |
log | NodeLog | Logux log instance to be synchronized. |
connection | Connection | Connection to remote node. |
options ? | NodeOptions | Synchronization options. |
Did we finish remote node authentication.
Type: boolean.
Is synchronization in process.
node.on('disconnect', () => {
node.connected
})
Type: boolean.
Connection used to communicate to remote node.
Type: Connection.
Promise for node data initial loadiging.
Type: Promise.
Latest remote node’s log added time, which was successfully
synchronized. It will be saves in log store.
Type: number.
Latest current log added time, which was successfully synchronized.
It will be saves in log store.
Type: number.
Unique current machine name.
console.log(node.localNodeId + ' is started')
Type: string.
Used Logux protocol.
if (tool.node.localProtocol !== 1) {
throw new Error('Unsupported Logux protocol')
}
Type: number.
Log for synchronization.
Type: NodeLog.
Minimum version of Logux protocol, which is supported.
console.log(`You need Logux protocol ${node.minProtocol} or higher`)
Type: number.
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.
Unique name of remote machine.
It is undefined until nodes handshake.
console.log('Connected to ' + node.remoteNodeId)
Type: string | undefined.
Remote node Logux protocol.
It is undefined until nodes handshake.
if (node.remoteProtocol >= 5) {
useNewAPI()
} else {
useOldAPI()
}
Type: number | undefined.
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.
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.
Time difference between nodes.
Type: number.
Disable throwing a error on error message and create error listener.
node.catch(error => {
console.error(error)
})
| Argument | Type | Description |
|---|
listener | (error: LoguxError) => void | The error listener. |
Returns Unsubscribe. Unbind listener from event.
Shut down the connection and unsubscribe from log events.
connection.on('disconnect', () => {
server.destroy()
})
| Argument | Type |
|---|
event | "headers" |
listener | (headers: Headers) => void |
| Argument | Type |
|---|
event | "synced" |
listener | (synced: number) => void |
| Argument | Type |
|---|
event | "clientError" | "error" |
listener | (error: LoguxError) => void |
| Argument | Type | Description |
|---|
event | "connect" | "debug" | "headers" | "state" | Event name. |
listener | () => void | The listener function. |
| Argument | Type |
|---|
event | "debug" |
listener | (type: "error", data: string) => void |
Returns Unsubscribe.
Set headers for current node.
if (navigator) {
node.setLocalHeaders({ language: navigator.language })
}
node.connection.connect()
| Argument | Type | Description |
|---|
headers | Headers | The data object will be set as headers for current node. |
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')
| Argument | Type | Description |
|---|
state | NodeState | The expected synchronization state value. |
Returns Promise. Promise until specific state.
| Property | Type | Description |
|---|
allowDangerousProtocol ? | boolean | Do not show warning when using ws:// in production. |
attempts ? | number | Maximum reconnection attempts. Default is Infinity. |
maxDelay ? | number | Maximum delay between reconnections. Default is 5000. |
minDelay ? | number | Minimum delay between reconnections. Default is 1000. |
ping ? | number | Milliseconds since last message to test connection by sending ping.
Default is 10000. |
prefix ? | string | Prefix for IndexedDB database to run multiple Logux instances
in the same browser. Default is logux. |
server | any | Server URL. |
store ? | any | Store to save log data. Default is MemoryStore. |
subprotocol | number | Client subprotocol version. |
time ? | any | Test time to test client. |
timeout ? | number | Timeout in milliseconds to break connection. Default is 70000. |
token ? | any | Client credentials for authentication. |
userId | string | User ID. |
| Property | Type |
|---|
decode | (str: string) => Value |
encode | (value: Value) => string |
| Property | Type | Description |
|---|
version ? | number | Version 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. |
Single field of a single row: [table, id, field].
Type: [table: string, id: string, field: string].
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).
| Property | Type | Description |
|---|
default ? | Type | () => Type | |
required | RequiredOnCreate | |
sql ? | string | { [key: string]: string } | |
type | "BIGINT" | "BOOLEAN" | "DOUBLE PRECISION" | "TEXT" | SQL column type used in CREATE TABLE. |
values ? | readonly string[] | |
| Property | Type | Description |
|---|
default ? | () => NoInfer | NoInfer | Default 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. |
Type: Column ? Type : never.
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.
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".
| Property | Type | Description |
|---|
ready | Promise | Promise resolved when the database was prepared and tables can be used. |
status | ReadableAtom | Database preparing status: |
tables | CrdtTables | Schemas 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 | |
| Property | Type | Description |
|---|
dialect ? | Dialect | SQL 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 ? | string | Storage 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 ? | PersistentStorage | Storage to keep the tables schema instead of localStorage
(for instance, for React Native or tests). |
sync ? | boolean | Should table actions be sent to the server. Default is true. |
timeout ? | number | Milliseconds to wait for the database to be prepared. |
repeat ? | () => [Action, MetaTime][] | Promise<[Action, MetaTime][]> | |
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[].
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.
| Property | Type | Description |
|---|
plural | string | Table 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. |
verb | CrdtVerb | Verb from the action type. |
| Property | Type | Description |
|---|
plural | string | Table name from the action type. |
verb | CrdtVerb | Verb from the action type. |
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 }.
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.
| Property | Type | Description |
|---|
driver | Driver | Database driver for raw queries to the table,
like in crdtTableToActions. |
plural | string | Table name. It is used as SQL table name and as prefix
of action types (user/created, user/changed, user/deleted). |
schema | Schema | Column 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 | |
Type: { fields: Partial<Fields>, id: string, type: string } | { fields: Partial<Fields>, ids: string[], type: string }.
Type: { fields: Fields, id: string, type: string } | { records: Fields & { id: string }[], type: string }.
Type: { id: string, type: string } | { ids: string[], type: string }.
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 }.
| Property | Type |
|---|
add | (task: () => void | Promise) => void |
destroy | () => void |
finish | () => Promise |
| Property | Type | Description |
|---|
onError ? | (error: unknown) => void | Called when the task throws. |
Type: "changed" | "created" | "deleted".
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
})
| Argument | Type | Description |
|---|
userIdStore | MapStore<UserId> | Store with object and userId key. |
builder | (value: UserId) => Client | Callback which return client |
| Argument | Type |
|---|
userIdStore | MapStore<UserId> |
builder | (value: UserId) => Client | undefined |
Returns Atom<Client>. Atom store with client
| Property | Type | Description |
|---|
exceptIndex ? | string | Do not change reasons for actions with this index in meta.indexes. |
id ? | string | Change reasons only for action with id. |
ids ? | string[] | Change reasons only for actions with these IDs. |
index ? | string | Change reasons only for actions with this index in meta.indexes. |
maxAdded ? | number | Change reasons only for actions with lower added. |
minAdded ? | number | Change reasons only for actions with bigger added. |
olderThan ? | ClientMeta | Change reasons only for actions older than specific action. |
youngerThan ? | ClientMeta | Change reasons only for actions younger than specific action. |
Type: "sqlite" | "pglite".
| Property | Type | Description |
|---|
error ? | string | Error favicon link. |
normal ? | string | Default favicon link. By default, it will be taken from current favicon. |
offline ? | string | Offline favicon link. |
Type: { [Key: keyof Value]?: Value[Key] }.
| Property | Type |
|---|
listChangesOnly ? | boolean |
| Property | Type | Description |
|---|
loading | Promise | While store is loading initial data from server or log. |
| Property | Type | Description |
|---|
index ? | string | Get entries with a custom index. |
order ? | "added" | "created" | Sort entries by created time or when they was added to current log. |
reason ? | string | Get only entries with this reason. |
Action unique ID across all nodes.
"OzcVoWD 380:R7BNGA:1"
Type: string.
| Property | Type |
|---|
clientId | string |
nodeId | string |
userId | string | undefined |
| Property | Type | Description |
|---|
received | number | The added value of latest received event. |
sent | number | The added value of latest sent event. |
Type: { id: string, isLoading: false } & Value.
Type: { isLoading: false } & Type.
Extends Connection.
Abstract interface for connection to synchronize logs over it.
For example, WebSocket or Loopback.
Is connection is enabled.
Type: boolean.
Disconnect and unbind all even listeners.
Type: () => void.
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.
Finish current connection.
| Argument | Type | Description |
|---|
reason ? | "destroy" | "error" | "timeout" | Disconnection reason. |
| Argument | Type |
|---|
event | "disconnect" |
listener | (reason: string) => void |
| Argument | Type |
|---|
event | "error" |
listener | (error: Error) => void |
| Argument | Type | Description |
|---|
event | "connect" | "connecting" | "disconnect" | Event name. |
listener | () => void | Event listener. |
| Argument | Type |
|---|
event | "message" |
listener | (msg: Message) => void |
Returns Unsubscribe.
Send message to connection.
| Argument | Type | Description |
|---|
message | Message | The 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)
| Parameter | Type | Description |
|---|
delay ? | number | Delay for connection and send events. Default is 1. |
Delay for connection and send events to emulate real connection latency.
Type: number.
First connection. Will be connected to right one after connect().
new ClientNode('client, log1, pair.left)
Type: LocalConnection.
Second connection. Will be connected to right one after connect().
new ServerNode('server, log2, pair.right)
Type: LocalConnection.
| Property | Type | Description |
|---|
add ? | boolean | Disable action added messages. |
clean ? | boolean | Disable action cleaned messages. |
error ? | boolean | Disable error messages. |
ignoreActions ? | string[] | Disable action messages with specific types. |
role ? | boolean | Disable tab role messages. |
state ? | boolean | Disable connection state messages. |
user ? | boolean | Disable user ID changing. |
| Property | Type | Description |
|---|
nodeId | string | Unique current machine name. |
store | Store | Store for log. |
Every Store class should provide 8 standard methods.
Add action to store. Action always will have type property.
Returns Promise<false | ClientMeta>. Promise with meta for new action or false if action with
same meta.id was already in store.
Add reasons to metadata of actions, which are already in the store.
Reasons, which action already has, should not be duplicated.
| Argument | Type | Description |
|---|
reasons | string[] | The reason names. |
criteria | Criteria | Criteria to select actions for reason adding. |
Returns Promise. Promise when adding will be finished.
Return action by action ID.
| Argument | Type | Description |
|---|
id | string | Action ID. |
Returns Promise<[null, null] | [Action, ClientMeta]>. Promise with array of action and metadata.
Change action metadata.
| Argument | Type | Description |
|---|
id | string | Action ID. |
diff | Partial<ClientMeta> | Object with values to change in action metadata. |
Returns Promise<boolean>. Promise with true if metadata was changed or false
on unknown ID.
Remove all data from the store.
Returns Promise. Promise when cleaning will be finished.
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.
| Argument | Type | Description |
|---|
opts ? | GetOptions | Query options. |
Returns Promise<LogPage>. Promise with first page.
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.
Get added values for latest synchronized received/sent events.
Returns Promise<LastSynced>. Promise with added values
Remove action from store.
| Argument | Type | Description |
|---|
id | string | Action ID. |
Returns Promise<false | [Action, ClientMeta]>. Promise with entry if action was in store.
Remove reasons from action’s metadata and remove actions without reasons.
| Argument | Type | Description |
|---|
reasons | string[] | The reason names. |
criteria | Criteria | Criteria to select actions for reason removing. |
callback | ReadonlyListener | Callback for every removed action. |
Returns Promise. Promise when cleaning will be finished.
Set added value for latest synchronized received or/and sent events.
| Argument | Type | Description |
|---|
values | Partial<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)
}
| Parameter | Type | Description |
|---|
type | ErrorType | The error code. |
options ? | LoguxErrorOptions[ErrorType] | The error option. |
received ? | boolean | Was error received from remote node. |
Return a error description by it code.
| Argument | Type | Description |
|---|
type | Type | The error code. |
options ? | LoguxErrorOptions[Type] | The errors options depends on error code. |
Returns string.
Human-readable error description.
console.log('Server throws: ' + error.description)
Type: string.
Full text of error to print in debug message.
Type: string.
Always equal to LoguxError. The best way to check error class.
if (error.name === 'LoguxError') {
Type: "LoguxError".
Error options depends on error type.
if (error.type === 'timeout') {
console.error('A timeout was reached (' + error.options + ' ms)')
}
Type: LoguxErrorOptions[ErrorType].
Was error received from remote client.
Type: boolean.
Calls which cause the error.
Type: string.
The error code.
if (error.type === 'timeout') {
fixNetwork()
}
Type: ErrorType.
| Property | Type |
|---|
bruteforce | void |
timeout | number |
unknown-message | string |
wrong-credentials | void |
wrong-format | string |
wrong-protocol | Versions |
wrong-subprotocol | Versions |
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()
},
…
})
Type: "LoguxNotFoundError".
| Property | Type |
|---|
id | string |
type | "logux/processed" |
| Property | Type |
|---|
channel | string |
creating ? | true |
filter ? | { } |
since ? | { id: string, time: number } |
type | "logux/subscribe" |
| Property | Type |
|---|
channel | string |
type | "logux/subscribed" |
| Property | Type |
|---|
action | RevertedAction |
id | string |
reason | Reason |
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')
}
}
| Parameter | Type |
|---|
action | RevertedAction |
Server logux/undo action. It has origin actions (which was undid)
in action.action.
console.log(error.action.action.type ' was undid')
Type: RevertedAction.
The better way to check error, than instanceof.
if (error.name === 'LoguxUndoError') {
Type: "LoguxUndoError".
| Property | Type |
|---|
channel | string |
filter ? | { } |
type | "logux/unsubscribe" |
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].
| Property | Type | Description |
|---|
added | number | Sequence number of action in current log. Log fills it. |
id | string | Action unique ID. Log sets it automatically. |
indexes ? | string[] | Indexes for action quick extraction. |
keepLast ? | string | Set value to reasons and this reason from old action. |
reasons | string[] | Why action should be kept in log. Action without reasons will be removed. |
subprotocol ? | number | Application subprotocol version. |
time | number | Action created time in current node time. Milliseconds since UNIX epoch. |
| Property | Type | Description |
|---|
auth ? | Authenticator | Function to check client credentials. |
fixTime ? | boolean | Detect difference between client and server and fix time
in synchronized actions. |
onReceive ? | ActionFilter | Function to filter or change actions coming from remote node’s
before put it to current log. |
onSend ? | ActionFilter | Function to filter or change actions before sending to remote node’s. |
ping ? | number | Milliseconds since last message to test connection by sending ping. |
subprotocol ? | number | Application subprotocol version. |
syncBatch ? | number | Maximum 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 ? | number | Timeout in milliseconds to wait answer before disconnect. |
token ? | string | TokenGenerator | Client credentials. For example, access token. |
Type: "connecting" | "disconnected" | "sending" | "synchronized".
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.
Type: { [key: string]: string | undefined }.
| Property | Type |
|---|
AccessDenied ? | any |
Error ? | any |
NotFound ? | any |
| Argument | Type |
|---|
action | ListenerAction |
meta | LogMeta |
| Property | Type |
|---|
AccessDenied ? | any |
Error ? | any |
NotFound ? | any |
| Argument | Type |
|---|
action | ListenerAction |
meta | LogMeta |
| Property | Type | Description |
|---|
attempts ? | number | Maximum reconnecting attempts. |
maxDelay ? | number | Maximum delay between re-connecting. |
minDelay ? | number | Minimum delay between re-connecting. |
| Property | Type | Description |
|---|
ready | Promise | Promise resolved when the data was prepared and all actions
from the log were reduced. |
status | ReadableAtom | |
destroy | () => void | |
type | (type: TypeAction["type"], listener: ActionListener) => void | |
| Property | Type | Description |
|---|
storage ? | PersistentStorage | Storage 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 | |
Type: "initializing" | "migrating" | "outdated" | "ready".
| Property | Type | Description |
|---|
allowDangerousProtocol ? | boolean | Do not show warning when using ws:// in production. |
attempts ? | number | Maximum reconnection attempts. Default is Infinity. |
maxDelay ? | number | Maximum delay between reconnections. Default is 5000. |
minDelay ? | number | Minimum delay between reconnections. Default is 1000. |
ping ? | number | Milliseconds since last message to test connection by sending ping.
Default is 10000. |
prefix ? | string | Prefix for IndexedDB database to run multiple Logux instances
in the same browser. Default is logux. |
server | any | Server URL. |
store ? | any | Store to save log data. Default is MemoryStore. |
subprotocol | number | Client subprotocol version. |
time ? | any | Test time to test client. |
timeout ? | number | Timeout in milliseconds to break connection. Default is 70000. |
token ? | any | Client credentials for authentication. |
userId ? | string | |
| Property | Type |
|---|
id | string |
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.
| Parameter | Type | Description |
|---|
db | Database | Database from @nanostores/sql openDb(). |
opts ? | SqlLogStoreOptions | Store options. |
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.
| Argument | Type | Description |
|---|
callback | (tx: Database, action: AnyAction, meta: ClientMeta) => void | Promise | undefined | Callback or undefined to remove the previous one. |
| Property | Type | Description |
|---|
packers ? | Packers | Packers to keep the binary parts of the actions in a separate column
instead of Base64 inside the JSON. |
| Property | Type | Description |
|---|
duration ? | number | Synchronized state duration. Default is 3000. |
Type: "connecting" | "connectingAfterWait" | "denied" | "disconnected" | "error" | "protocolError" | "sending" | "sendingAfterWait" | "syncError" | "synchronized" | "synchronizedAfterWait" | "wait" | "wrongCredentials".
| Argument | Type |
|---|
prevValue | Value |
action | ListenAction |
meta | any |
Returns Value | Promise<Value>.
| Property | Type | Description |
|---|
storage ? | PersistentStorage | Storage 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][]> | |
| Property | Type | Description |
|---|
ready | Promise | Promise resolved when the value was loaded and all actions
from the log were reduced. It is also resolved when the reducer
became outdated. |
status | ReadableAtom | |
value | ReadableAtom | The reduced value. |
destroy | () => void | |
type | (type: TypeAction["type"], listener: StorageActionListener) => void | |
| Property | Type |
|---|
fields | Partial<Omit<Value,"id">> |
id | string |
type | string |
| Property | Type |
|---|
fields | Partial<Omit<Value,"id">> |
id | string |
type | string |
| Property | Type |
|---|
fields | Omit<Value,"id"> |
id | string |
type | string |
| Property | Type |
|---|
fields | Omit<Value,"id"> |
id | string |
type | string |
| Property | Type |
|---|
id | string |
type | string |
| Property | Type |
|---|
id | string |
type | string |
| Property | Type | Description |
|---|
client | Client | Logux Client instance. |
createdAt ? | any | Meta from create action if the store was created locally. |
deleted ? | true | Mark that store was deleted. |
loading | Promise | While store is loading initial data from server or log. |
offline | boolean | Does store keep data in the log after store is destroyed. |
plural | string | Name of map class. |
remote | boolean | Does store use server to load and save data. |
Type: boolean | null | number | string | undefined.
| Property | Type |
|---|
id | string |
subprotocol ? | number |
time | number |
| Property | Type | Description |
|---|
nodeId ? | string | Unique log name. |
store ? | LogStore | Store for log. Will use MemoryStore by default. |
Returns string | Promise<string>.
| Property | Type |
|---|
supported | number |
used | number |
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)
| Parameter | Type | Description |
|---|
url | string | WebSocket server URL. |
Class ? | unknown | |
opts ? | unknown | Extra option for WebSocket constructor. |
Is connection is enabled.
Type: boolean.
Disconnect and unbind all even listeners.
Type: () => void.
Whether to use text JSON protocol instead of binary.
Always true for WsConnection, can change in WsBinaryConnection.
Type: boolean.
WebSocket instance.
Type: WS.
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.
Finish current connection.
| Argument | Type | Description |
|---|
reason ? | "destroy" | "error" | "timeout" | Disconnection reason. |
| Argument | Type |
|---|
event | "disconnect" |
listener | (reason: string) => void |
| Argument | Type |
|---|
event | "error" |
listener | (error: Error) => void |
| Argument | Type | Description |
|---|
event | "connect" | "connecting" | "disconnect" | Event name. |
listener | () => void | Event listener. |
| Argument | Type |
|---|
event | "message" |
listener | (msg: Message) => void |
Returns Unsubscribe.
Send message to connection.
| Argument | Type | Description |
|---|
message | Message | The 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)
| Parameter | Type | Description |
|---|
url | string | WebSocket server URL. |
Class ? | unknown | |
opts ? | unknown | Extra option for WebSocket constructor. |
Is connection is enabled.
Type: boolean.
Disconnect and unbind all even listeners.
Type: () => void.
Whether to use text JSON protocol instead of binary.
Always true for WsConnection, can change in WsBinaryConnection.
Type: boolean.
WebSocket instance.
Type: WS.
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.
Finish current connection.
| Argument | Type | Description |
|---|
reason ? | "destroy" | "error" | "timeout" | Disconnection reason. |
| Argument | Type |
|---|
event | "disconnect" |
listener | (reason: string) => void |
| Argument | Type |
|---|
event | "error" |
listener | (error: Error) => void |
| Argument | Type | Description |
|---|
event | "connect" | "connecting" | "disconnect" | Event name. |
listener | () => void | Event listener. |
| Argument | Type |
|---|
event | "message" |
listener | (msg: Message) => void |
Returns Unsubscribe.
Send message to connection.
| Argument | Type | Description |
|---|
message | Message | The message to be sent. |
Type: { type: "0/clean" } & { id: string } | { ids: string[] }.
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())
}
| Argument | Type | Description |
|---|
opts ? | string | Omit<CrdtColumnOptions,"default"> | Extra column definition SQL or column options. |
| Argument | Type | Description |
|---|
opts | { default: () => NoInfer | NoInfer } & CrdtColumnOptions | Extra column definition SQL or column options. |
Returns { type: "BIGINT" } & CrdtColumn.
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).
| Argument | Type | Description |
|---|
opts ? | string | Omit<CrdtColumnOptions,"default"> | Extra column definition SQL or column options. |
| Argument | Type | Description |
|---|
opts | { default: boolean | () => boolean } & CrdtColumnOptions | Extra column definition SQL or column options. |
Returns { type: "BOOLEAN" } & CrdtColumn.
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])
})
Returns Promise<[Action, MetaTime][]>. Actions with restored meta id and time, oldest first.
Create store with user’s authentication state.
import { createAuth } from '@logux/client'
let auth = createAuth(client)
await auth.loading
console.log(auth.get())
| Argument | Type | Description |
|---|
client | Client | Logux Client. |
Returns AuthStore.
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)
| Argument | Type | Description |
|---|
client | Client | Logux client. |
db | Database | SQL database from @nanostores/sql openDb(). |
opts ? | CrdtDatabaseOptions | Database options and the source of old actions. |
Returns CrdtDatabase.
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 }
)
})
})
Returns CrdtTasks. Task queue.
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 …`)
})
| Argument | Type | Description |
|---|
client | Client | Logux client. |
name | string | The name of the reducer to use in the storage version key. |
version | number | The current version to call migrations on new version. |
callbacks | ReducerInitCallbacks | The data migrations callbacks. |
Returns Reducer.
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)
Returns StorageReducer.
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)
Returns [ActionCreator, ActionCreator, ActionCreator].
Returns actions for CRDT Map.
import { defineSyncMapActions } from '@logux/actions'
const [
createUserAction,
changeUserAction,
deleteUserAction,
createdUserAction,
changedUserAction,
deletedUserAction
] = defineSyncMapActions('users')
Returns [ActionCreator, ActionCreator, ActionCreator].
Pass all common tests for Logux store to callback.
import { eachStoreCheck } from '@logux/core'
eachStoreCheck((desc, creator) => {
it(desc, creator(() => new CustomStore()))
})
| Argument | Type | Description |
|---|
test | (name: string, testCreator: (storeCreator: () => LogStore) => () => void) => void | Callback to create tests in your test framework. |
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' })
Returns LoadedSyncMapValue.
Decode number from -0-9A-Z_a-z alphabet.
fromCompat('OzcVoWD')
| Argument | Type | Description |
|---|
str | string | Encoded number. |
Returns number. Decoded number.
Decode meta.time from action ID.
idToTime('OzcVoWD client:1')
| Argument | Type | Description |
|---|
id | string | Action ID or its time part. |
Returns number. Milliseconds since UNIX epoch.
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)) {
}
| Argument | Type | Description |
|---|
id | string | Action or Node ID |
clientId | string | Client ID to compare with |
Returns boolean.
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)
| Argument | Type | Description |
|---|
store | Store | Store to load. |
| Argument | Type | Description |
|---|
store | Store | Store to load. |
Returns Promise<any>.
Returns logux/undo action.
| Argument | Type |
|---|
fields | { action: RevertedAction, id: string, reason: Reason } |
Returns LoguxUndoAction.
DOUBLE PRECISION column with number value.
| Argument | Type | Description |
|---|
opts ? | string | Omit<CrdtColumnOptions,"default"> | Extra column definition SQL or column options. |
| Argument | Type | Description |
|---|
opts | { default: NoInfer } & CrdtColumnOptions | Extra column definition SQL or column options. |
Returns { type: "DOUBLE PRECISION" } & CrdtColumn.
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'])
}
| Argument | Type | Description |
|---|
values | Values | Allowed string values. |
opts ? | string | Omit<CrdtColumnOptions,"default"> | Extra column definition SQL or column options. |
| Argument | Type | Description |
|---|
values | Values | Allowed string values. |
opts | { default: () => Values[number] | Values[number] } & CrdtColumnOptions | Extra column definition SQL or column options. |
Returns { type: "TEXT" } & CrdtColumn.
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())
}
| Argument | Type | Description |
|---|
column | Column | Column definition to wrap. |
Returns { type: Column["type"] } & CrdtColumn.
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 } })
Returns false | CrdtParsedAction. Parsed action or false if it is not an action of these tables.
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)
}
| Argument | Type | Description |
|---|
action | Action | Table action. |
Returns [id: string, fields: object][]. Row IDs with the fields, which the action writes to them.
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)
})
Returns false | CrdtParsedType. Table and verb or false if it is not an action of these tables.
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 })
}
})
| Argument | Type | Description |
|---|
client | Client | Logux Client. |
meta | ClientMeta | Meta of the original action. |
Returns Promise<ClientMeta>. Meta of the added shadow action.
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.
Convert string created by toSorted() back to metadata.
sortedToMeta('------Ec test ------Ec')
| Argument | Type | Description |
|---|
sorted | string | String created by toSorted(). |
Returns MetaTime. Action’s metadata with id and time keys.
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)
})
Returns () => void. Unbind listener.
TEXT column with string value.
import { string } from '@logux/client'
let schema = {
email: string('COLLATE NOCASE'),
name: string(),
theme: string<'dark' | 'light'>({ default: 'dark' })
}
| Argument | Type | Description |
|---|
opts ? | string | Omit<CrdtColumnOptions,"default"> | Extra column definition SQL or column options. |
| Argument | Type | Description |
|---|
opts | { default: NoInfer } & CrdtColumnOptions | Extra column definition SQL or column options. |
Returns { type: "TEXT" } & CrdtColumn.
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)
| Argument | Type | Description |
|---|
number | number | Number to encode. |
Returns string. Encoded number.
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) })
| Argument | Type | Description |
|---|
meta | MetaTime | Action’s metadata. |
Returns string. String to sort actions.
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')
})
})
| Argument | Type | Description |
|---|
client | any | Logux Client. |
id | ID | Action ID. |
Returns Promise. Promise when action was proccessed.
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' })
])
| Argument | Type | Description |
|---|
row | WithoutMeta | Row without updatedAt_field columns. |
Returns Value. Row with null in updatedAt_field column of every field.
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' }
])
Returns WithoutMeta[]. Rows without updatedAt_field columns.
Packer to 0 action to binary format to use in SQL stores.
Type: ActionPacker.
A Markdown version of this page is available at https://logux.org/web-api.md.