Extends Context.
Subscription context.
server.channel('user/:id', {
access (ctx, action, meta) {
return ctx.params.id === ctx.userId
}
})
Unique persistence client ID.
server.clientIds.get(node.clientId)
Type: string.
Open structure to save some data between different steps of processing.
server.type('RENAME', {
access (ctx, action, meta) {
ctx.data.user = findUser(ctx.userId)
return ctx.data.user.hasAccess(action.projectId)
}
process (ctx, action, meta) {
return ctx.data.user.rename(action.projectId, action.name)
}
})
Type: Data.
Client’s headers.
ctx.sendBack({
type: 'error',
message: I18n[ctx.headers.locale || 'en'].error
})
Type: Headers.
Was action created by Logux server.
access: (ctx, action, meta) => ctx.isServer
Type: boolean.
Unique node ID.
server.nodeIds.get(node.nodeId)
Type: string.
Parsed variable parts of channel pattern.
server.channel('user/:id', {
access (ctx, action, meta) {
action.channel
ctx.params
}
})
server.channel(/post/(\d+)/, {
access (ctx, action, meta) {
action.channel
ctx.params
}
})
Type: ChannelParams.
Action creator application subprotocol version.
Type: number.
User ID taken node ID.
async access (ctx, action, meta) {
const user = await db.getUser(ctx.userId)
return user.admin
}
Type: string.
Wait until the client will confirm all actions, which were sent to it.
Use it to send a long history without loading it all into the memory:
the client’s speed will limit how fast you read the database.
while (await ctx.drain()) {
let page = await cursor.next(100)
if (!page.length) break
ctx.sendBack(page.map(i => i.action))
}
Returns Promise<boolean>. Promise with false if the client was disconnected.
Send action back to the client.
ctx.sendBack({ type: 'login/success', token })
An array of actions will be sent in a single message. Use it to send
a big history page by page instead of a message per action.
ctx.sendBack(page.map(i => i.action))
Every action in the array can have own meta as [action, meta].
Action will not be processed by server’s callbacks from Server#type.
| Argument | Type | Description |
|---|
action | TypeAction | TypeAction | [TypeAction, Partial<ServerMeta>][] | The action or the array of actions. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise. Promise until action was added to the server log.
Extends ConnectContext.
Action context.
Unique persistence client ID.
server.clientIds.get(node.clientId)
Type: string.
Open structure to save some data between different steps of processing.
server.type('RENAME', {
access (ctx, action, meta) {
ctx.data.user = findUser(ctx.userId)
return ctx.data.user.hasAccess(action.projectId)
}
process (ctx, action, meta) {
return ctx.data.user.rename(action.projectId, action.name)
}
})
Type: Data.
Client’s headers.
ctx.sendBack({
type: 'error',
message: I18n[ctx.headers.locale || 'en'].error
})
Type: Headers.
Was action created by Logux server.
access: (ctx, action, meta) => ctx.isServer
Type: boolean.
Unique node ID.
server.nodeIds.get(node.nodeId)
Type: string.
Action creator application subprotocol version.
Type: number.
User ID taken node ID.
async access (ctx, action, meta) {
const user = await db.getUser(ctx.userId)
return user.admin
}
Type: string.
Wait until the client will confirm all actions, which were sent to it.
Use it to send a long history without loading it all into the memory:
the client’s speed will limit how fast you read the database.
while (await ctx.drain()) {
let page = await cursor.next(100)
if (!page.length) break
ctx.sendBack(page.map(i => i.action))
}
Returns Promise<boolean>. Promise with false if the client was disconnected.
Send action back to the client.
ctx.sendBack({ type: 'login/success', token })
An array of actions will be sent in a single message. Use it to send
a big history page by page instead of a message per action.
ctx.sendBack(page.map(i => i.action))
Every action in the array can have own meta as [action, meta].
Action will not be processed by server’s callbacks from Server#type.
| Argument | Type | Description |
|---|
action | TypeAction | TypeAction | [TypeAction, Partial<ServerMeta>][] | The action or the array of actions. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise. Promise until action was added to the server log.
Extends Error.
Throwing this error in accessAndProcess or accessAndLoad
will deny the action.
| Parameter | Type |
|---|
statusCode | number |
url | string |
Extends BaseServer.
End-user API to create Logux server.
import { Server } from '@logux/server'
const env = process.env.NODE_ENV || 'development'
const envOptions = {}
if (env === 'production') {
envOptions.cert = 'cert.pem'
envOptions.key = 'key.pem'
}
const server = new Server(Object.assign({
subprotocol: 1,
minSubprotocol: 1,
root: import.meta.dirname
}, envOptions))
server.listen()
Load options from command-line arguments and/or environment.
const server = new Server(Server.loadOptions(process, {
minSubprotocol: 1,
subprotocol: 1,
root: import.meta.dirname,
port: 31337
}))
| Argument | Type | Description |
|---|
process | Process | Current process object. |
defaults | ServerOptions | Default server options. Arguments and environment
variables will override them. |
Returns ServerOptions. Parsed options object.
Connected client by client ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient>.
Connected clients.
for (let client of server.connected.values()) {
console.log(client.remoteAddress)
}
Type: Map<string,ServerClient>.
Production or development mode.
if (server.env === 'development') {
logDebugData()
}
Type: "development" | "production".
Server actions log.
server.log.each(finder)
Type: Log.
Console for custom log records. It uses pino API.
server.on('connected', client => {
server.logger.info(
{ domain: client.httpHeaders.domain },
'Client domain'
)
})
Type: { debug: LogFn, error: LogFn, fatal: LogFn, info: LogFn, warn: LogFn }.
Server unique ID.
console.log('Error was raised on ' + server.nodeId)
Type: string.
Connected client by node ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient>.
Server options.
console.log('Server options', server.options.subprotocol)
Type: ServerOptions.
Clients subscribed to some channel.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: { }.
Connected client by user ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient[]>.
Add new client for server. You should call this method manually
mostly for test purposes.
server.addClient(test.right)
Returns number. Client ID.
Set authenticate function. It will receive client credentials
and node ID. It should return a Promise with true or false.
server.auth(async ({ userId, cookie }) => {
const user = await findUserByToken(cookie.token)
return !!user && userId === user.id
})
Load module creators and apply to the server. By default, it will load
files from modules/*.
await server.autoloadModules()
| Argument | Type | Description |
|---|
files ? | string | string[] | Pattern for module files. |
Returns Promise.
Define the channel.
server.channel('user/:id', {
access (ctx, action, meta) {
return ctx.params.id === ctx.userId
}
filter (ctx, action, meta) {
return (otherCtx, otherAction, otherMeta) => {
return !action.hidden
}
}
async load (ctx, action, meta) {
const user = await db.loadUser(ctx.params.id)
ctx.sendBack({ type: 'USER_NAME', name: user.name })
}
})
| Argument | Type | Description |
|---|
pattern | string | Pattern for channel name. |
callbacks | ChannelCallbacks | Callback during subscription process. |
options ? | ChannelOptions | Additional options |
| Argument | Type | Description |
|---|
pattern | RegExp | Regular expression for channel name. |
callbacks | ChannelCallbacks | Callback during subscription process. |
options ? | ChannelOptions | Additional options |
Send runtime error stacktrace to all clients.
process.on('uncaughtException', e => {
server.debugError(e)
})
| Argument | Type | Description |
|---|
error | Error | Runtime error instance. |
Stop server and unbind all listeners.
afterEach(() => {
testServer.destroy()
})
Returns Promise. Promise when all listeners will be removed.
Wait until the client will confirm all actions, which were sent to it.
Use it to send a long history without loading it all into the memory:
the client’s speed will limit how fast you read the database.
while (await server.drain(clientId)) {
let page = await cursor.next(100)
if (!page.length) break
server.log.add(page.map(i => [i.action, { clients: [clientId] }]))
}
| Argument | Type | Description |
|---|
clientId | string | Client ID. |
Returns Promise<boolean>. Promise with false if the client was disconnected.
Handle WebSocket connection explicitly
This is a low-level method allowing to integrate Logux server with an existing server
fastify.get('/', { websocket: true }, (socket, req) => {
loguxServer.handleClient(socket, req)
})
Add non-WebSocket HTTP request processor.
server.http('GET', '/auth', (req, res) => {
let token = signIn(req)
if (token) {
res.setHeader('Set-Cookie', `token=${token}; Secure; HttpOnly`)
res.end()
} else {
res.statusCode = 400
res.end('Wrong user or password')
}
})
Start WebSocket server and listen for clients.
Returns Promise. When the server has been bound.
| Argument | Type | Description |
|---|
event | "subscriptionCancelled" | The event name. |
listener | () => void | Event listener. |
| Argument | Type | Description |
|---|
event | "processed" | The event name. |
listener | (action: Action, meta: ServerMeta, latencyMilliseconds: number) => void | Processing listener. |
| Argument | Type | Description |
|---|
event | "add" | "clean" | The event name. |
listener | (action: Action, meta: ServerMeta) => void | Action listener. |
| Argument | Type | Description |
|---|
event | "connected" | "disconnected" | The event name. |
listener | (client: ServerClient) => void | Client listener. |
| Argument | Type | Description |
|---|
event | "clientError" | "fatal" | The event name. |
listener | (err: Error) => void | The listener function. |
| Argument | Type | Description |
|---|
event | "error" | The event name. |
listener | (err: Error, action: Action, meta: ServerMeta) => void | Error listener. |
| Argument | Type | Description |
|---|
event | "authenticated" | "unauthenticated" | The event name. |
listener | (client: ServerClient, latencyMilliseconds: number) => void | Client listener. |
| Argument | Type | Description |
|---|
event | "preadd" | The event name. |
listener | (action: Action, meta: ServerMeta) => void | Action listener. |
| Argument | Type | Description |
|---|
event | "subscribed" | The event name. |
listener | (action: LoguxSubscribeAction, meta: ServerMeta, latencyMilliseconds: number) => void | Subscription listener. |
| Argument | Type | Description |
|---|
event | "unsubscribed" | The event name. |
listener | (action: LoguxUnsubscribeAction, meta: ServerMeta, clientNodeId: string) => void | Subscription listener. |
| Argument | Type | Description |
|---|
event | "report" | The event name. |
listener | Reporter | Report listener. |
Returns Unsubscribe.
Set callbacks for unknown channel subscription.
server.otherChannel({
async access (ctx, action, meta) {
const res = await phpBackend.checkChannel(ctx.params[0], ctx.userId)
if (res.code === 404) {
this.wrongChannel(action, meta)
return false
} else {
return response.body === 'granted'
}
}
})
| Argument | Type | Description |
|---|
callbacks | ChannelCallbacks | Callback during subscription process. |
Define callbacks for actions, which type was not defined
by any Server#type. Useful for proxy or some hacks.
Without this settings, server will call Server#unknownType
on unknown type.
server.otherType(
async access (ctx, action, meta) {
const response = await phpBackend.checkByHTTP(action, meta)
if (response.code === 404) {
this.unknownType(action, meta)
return false
} else {
return response.body === 'granted'
}
}
async process (ctx, action, meta) {
return await phpBackend.sendHTTP(action, meta)
}
})
| Argument | Type | Description |
|---|
callbacks | ActionCallbacks | Callbacks for actions with this type. |
Add new action to the server and return the Promise until it will be
resend to clients and processed.
| Argument | Type | Description |
|---|
action | TypeAction | New action to resend and process. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise<ServerMeta>. Promise until new action will be resend to clients and processed.
Send action, received by other server, to all clients of current server.
This method is for multi-server configuration only.
server.on('add', (action, meta) => {
if (meta.server === server.nodeId) {
sendToOtherServers(action, meta)
}
})
onReceivingFromOtherServer((action, meta) => {
server.sendAction(action, meta)
})
Returns void | Promise.
Change a way how server loads actions history for the client.
server.sendOnConnect(async (ctx, lastSynced) => {
return db.loadActions({ user: ctx.userId, after: lastSynced })
})
Actions should be returned from the newest one to the oldest one.
The list will be split into messages by the syncBatch option, but it
is loaded into the memory as a whole. For a big history send it by
pages with Context#sendBack() and Context#drain() instead.
Set meta.added to let the client ask only for newer actions after
the reconnect: the biggest added will be sent as the sync position.
| Argument | Type | Description |
|---|
loader | ConnectLoader | Callback which loads list of actions and meta. |
Send logux/subscribed if client was not already subscribed.
server.subscribe(ctx.nodeId, `users/${loaded}`)
| Argument | Type | Description |
|---|
nodeId | string | Node ID. |
channel | string | Channel name. |
| Argument | Type | Description |
|---|
actionCreator | Creator | Action creator function. |
callbacks | ActionCallbacks | Callbacks for action created by creator. |
options ? | TypeOptions | Additional options |
| Argument | Type | Description |
|---|
name | RegExp | TypeAction["type"] | The action’s type or action’s type matching rule as RegExp.. |
callbacks | ActionCallbacks | Callbacks for actions with this type. |
options ? | TypeOptions | Additional options |
Undo action from client.
if (couldNotFixConflict(action, meta)) {
server.undo(action, meta)
}
| Argument | Type | Description |
|---|
action | Action | The original action to undo. |
meta | ServerMeta | The action’s metadata. |
reason ? | string | Optional code for reason. Default is 'error'. |
extra ? | object | Extra fields to logux/undo action. |
Returns Promise. When action was saved to the log.
If you receive action with unknown type, this method will mark this action
with error status and undo it on the clients.
If you didn’t set Server#otherType,
Logux will call it automatically.
server.otherType({
access (ctx, action, meta) {
if (action.type.startsWith('myapp/')) {
return proxy.access(action, meta)
} else {
server.unknownType(action, meta)
}
}
})
| Argument | Type | Description |
|---|
action | Action | The action with unknown type. |
meta | ServerMeta | Action’s metadata. |
Report that client try to subscribe for unknown channel.
Logux call it automatically,
if you will not set Server#otherChannel.
server.otherChannel({
async access (ctx, action, meta) {
const res = phpBackend.checkChannel(params[0], ctx.userId)
if (res.code === 404) {
this.wrongChannel(action, meta)
return false
} else {
return response.body === 'granted'
}
}
})
Logux client connected to server.
const client = server.connected.get(0)
Unique persistence machine ID.
It will be undefined before correct authentication.
Type: string.
The Logux wrapper to WebSocket connection.
console.log(client.connection.ws.upgradeReq.headers)
Type: ServerConnection.
Open structure to save data for the whole connection.
server.auth(async ({ client, userId, token }) => {
let session = await findSession(token)
if (!session) return false
client.data.sessionId = session.id
return true
})
server.on('disconnected', client => {
touchSession(client.data.sessionId)
})
Type: ClientData.
HTTP headers of WS connection.
client.httpHeaders['User-Agent']
Type: { }.
Client number used as app.connected key.
function stillConnected (client) {
return app.connected.has(client.key)
}
Type: string.
Node instance to synchronize logs.
if (client.node.state === 'synchronized')
Type: ServerNode.
Unique node ID.
It will be undefined before correct authentication.
Type: string.
Does server process some action from client.
console.log('Clients in processing:', clients.map(i => i.processing))
Type: boolean.
Client IP address.
const clientCity = detectLocation(client.remoteAddress)
Type: string.
User ID. It will be filled from client’s node ID.
It will be undefined before correct authentication.
Type: string.
Wait until the client will confirm all sent actions.
Returns Promise<boolean>. Promise with false if the client was disconnected.
Add callbacks for client’s SyncMap.
import { addSyncMap, isFirstTimeOlder, ChangedAt } from '@logux/server'
import { LoguxNotFoundError } from '@logux/actions'
addSyncMap(server, 'tasks', {
async access (ctx, id) {
const task = await Task.find(id)
return ctx.userId === task.authorId
},
async load (ctx, id, since) {
const task = await Task.find(id)
if (!task) throw new LoguxNotFoundError()
return {
id: task.id,
text: ChangedAt(task.text, task.textChanged),
finished: ChangedAt(task.finished, task.finishedChanged),
}
},
async create (ctx, id, fields, time) {
await Task.create({
id,
text: fields.text,
finished: fields.finished,
authorId: ctx.userId,
textChanged: time,
finishedChanged: time
})
},
async change (ctx, id, fields, time) {
const task = await Task.find(id)
if ('text' in fields) {
if (task.textChanged < time) {
await task.update({
text: fields.text,
textChanged: time
})
}
}
if ('finished' in fields) {
if (task.finishedChanged < time) {
await task.update({
finished: fields.finished,
finishedChanged: time
})
}
}
}
async delete (ctx, id) {
await Task.delete(id)
}
})
| Argument | Type | Description |
|---|
server | BaseServer | Server instance. |
plural | string | Prefix for channel names and action types. |
operations | SyncMapOperations | Callbacks. |
Add callbacks for client’s useFilter.
import { addSyncMapFilter, ChangedAt } from '@logux/server'
addSyncMapFilter(server, 'tasks', {
access (ctx, filter) {
return true
},
initial (ctx, filter, since) {
let tasks = await Tasks.where({ ...filter, authorId: ctx.userId })
return tasks.map(task => ({
id: task.id,
text: ChangedAt(task.text, task.textChanged),
finished: ChangedAt(task.finished, task.finishedChanged),
}))
},
actions (filterCtx, filter) {
return (actionCtx, action, meta) => {
return actionCtx.userId === filterCtx.userId
}
}
})
Client to test server.
import { TestServer } from '@logux/server'
import postsModule from './posts.js'
import authModule from './auth.js'
let destroyable
afterEach(() => {
if (destroyable) destroyable.destroy()
})
function createServer () {
destroyable = new TestServer()
return destroyable
}
it('check auth', () => {
let server = createServer()
authModule(server)
await server.connect('1', { token: 'good' })
expect(() => {
await server.connect('2', { token: 'bad' })
}).rejects.toEqual({
error: 'Wrong credentials'
})
})
it('creates and loads posts', () => {
let server = createServer()
postsModule(server)
let client1 = await server.connect('1')
await client1.process({ type: 'posts/add', post })
let client1 = await server.connect('2')
expect(await client2.subscribe('posts')).toEqual([
{ type: 'posts/add', post }
])
})
Client’s ID.
let client = new TestClient(server, '10')
client.clientId
Type: string.
Client’s log with extra methods to check actions inside.
console.log(client.log.entries())
Type: TestLog.
Client’s node ID.
let client = new TestClient(server, '10')
client.nodeId
Type: string.
Connection channel between client and server to track sent messages.
console.log(client.pair.leftSent)
Type: TestPair.
User ID.
let client = new TestClient(server, '10')
client.userId
Type: string.
Collect actions added by server and other clients during the test call.
let answers = await client.collect(async () => {
client.log.add({ type: 'pay' })
await delay(10)
})
expect(actions).toEqual([{ type: 'paid' }])
| Argument | Type | Description |
|---|
test | () => Promise<unknown> | Function, where do you expect action will be received |
Returns Promise<Action[]>. Promise with all received actions
Connect to test server.
let client = new TestClient(server, '10')
await client.connect()
| Argument | Type |
|---|
opts ? | { token: string } |
Returns Promise. Promise until the authorization.
Disconnect from test server.
await client.disconnect()
Returns Promise. Promise until connection close.
Send action to the sever and collect all response actions.
await client.process({ type: 'posts/add', post })
let posts = await client.subscribe('posts')
expect(posts).toHaveLength(1)
| Argument | Type | Description |
|---|
action | TypeAction | New action. |
meta ? | Partial<ServerMeta> | Optional action’s meta. |
Returns Promise<Action[]>. Promise until logux/processed answer.
Collect actions received from server during the test call.
let answers = await client1.received(async () => {
await client2.process({ type: 'resend' })
})
expect(actions).toEqual([{ type: 'local' }])
| Argument | Type | Description |
|---|
test | () => unknown | Function, where do you expect action will be received |
Returns Promise<Action[]>. Promise with all received actions
Subscribe to the channel and collect all actions during the subscription.
let posts = await client.subscribe('posts')
expect(posts).toEqual([
{ type: 'posts/add', post }
])
| Argument | Type | Description |
|---|
channel | any | Channel name or logux/subscribe action. |
filter ? | object | Optional filter for subscription. |
since ? | { id: string, time: number } | Optional time from last data. |
Returns Promise<Action[]>. Promise with all actions from the server.
Unsubscribe client from the channel.
await client.unsubscribe('posts')
| Argument | Type | Description |
|---|
channel | any | Channel name or logux/subscribe action. |
filter ? | object | Optional filter for subscription. |
Returns Promise<Action[]>. Promise until server will remove client from subscribers.
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()
})
Unique node ID. It is used in action IDs.
Type: string.
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[].
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 })
})
| Argument | Type | Description |
|---|
action | NewAction | The new action. |
meta ? | Partial<LogMeta> | Open structure for action metadata. |
Returns Promise<false | LogMeta>. Promise with meta if action was added to log or false
if action was already in log.
Add reason tags to metadata of actions, which are already in the log. Reasons, which action already has, will not be duplicated.
log.addReason('last-value', { id: meta.id })
| Argument | Type | Description |
|---|
reasons | string | string[] | The reason name or names. |
criteria ? | Criteria | Criteria to select actions for reason adding. |
Returns Promise. Promise when adding will be finished.
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 })
}
| Argument | Type | Description |
|---|
id | string | Action ID. |
Returns Promise<[null, null] | [Action, LogMeta]>. Promise with array of action and metadata.
Change action metadata. You will remove action by setting reasons: [].
await process(action)
log.changeMeta(action, { status: 'processed' })
| Argument | Type | Description |
|---|
id | string | Action ID. |
diff | Partial<LogMeta> | Object with values to change in action metadata. |
Returns Promise<boolean>. Promise with true if metadata was changed or false
on unknown ID.
| Argument | Type | Description |
|---|
callback | ActionIterator | Function will be executed on every action. |
| Argument | Type | Description |
|---|
callback | ActionIterator | Function will be executed on every action. |
Returns Promise.
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, ServerMeta][].
Generate next unique action ID.
const id = log.generateId()
Returns string. Unique ID for action.
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()
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.
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')
}
})
| Argument | Type | Description |
|---|
event | "add" | "clean" | The event name. |
listener | ReadonlyListener | The listener function. |
| Argument | Type | Description |
|---|
event | "preadd" | The event name. |
listener | PreaddListener | The listener function. |
| Argument | Type | Description |
|---|
event | "batch" | The event name. |
listener | (entries: [Action, LogMeta][]) => void | The listener function. |
Returns Unsubscribe. Unbind listener from event.
Remove reason tags from actions’ metadata and remove actions without
reasons from log.
onSync(lastSent) {
log.removeReason('unsynchronized', { maxAdded: lastSent })
}
| Argument | Type | Description |
|---|
reasons | string | string[] | The reason name or names. |
criteria ? | Criteria | Criteria to select actions for reason removing. |
Returns Promise. Promise when cleaning will be finished.
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()
}
| Argument | Type | Description |
|---|
type | NewAction["type"] | Action’s type. |
listener | ReadonlyListener | The listener function. |
opts ? | { event?: "add" | "clean", id?: string } | |
| Argument | Type | Description |
|---|
type | NewAction["type"] | Action’s type. |
listener | PreaddListener | The 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])
})
| 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.
Emitted events from left connection.
await pair.left.connect()
pair.leftEvents
Type: string[][].
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.
Sent messages from left connection.
await pair.left.send(msg)
pair.leftSent
Type: Message[].
Second connection. Will be connected to right one after connect().
new ServerNode('server, log2, pair.right)
Type: LocalConnection.
Emitted events from right connection.
await pair.right.connect()
pair.rightEvents
Type: string[][].
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.
Sent messages from right connection.
await pair.right.send(msg)
pair.rightSent
Type: Message[].
Clear all connections events and messages to test only last events.
await client.connection.connect()
pair.clear()
await client.log.add({ type: 'a' })
expect(pair.leftSent).toEqual([
['sync', …]
])
Return Promise until next event.
pair.left.send(['test'])
await pair.wait('left')
pair.leftSend
| Argument | Type | Description |
|---|
receiver ? | "left" | "right" | Wait for specific receiver event. |
Returns Promise<TestPair>. Promise until next event.
Extends BaseServer.
Server to be used in test.
import { TestServer } from '@logux/server'
import usersModule from './users.js'
let server
afterEach(() => {
if (server) server.destroy()
})
it('connects to the server', () => {
server = new TestServer()
usersModule(server)
let client = await server.connect('10')
})
Connected client by client ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient>.
Connected clients.
for (let client of server.connected.values()) {
console.log(client.remoteAddress)
}
Type: Map<string,ServerClient>.
Production or development mode.
if (server.env === 'development') {
logDebugData()
}
Type: "development" | "production".
fetch() compatible API to test HTTP endpoints.
server.http('GET', '/version', (req, res) => {
res.end('1.0.0')
})
let res = await server.fetch()
expect(await res.text()).toEqual('1.0.0')
Type: (input: URL | RequestInfo, init?: RequestInit) => Promise<Response>.
Server actions log, with methods to check actions inside.
server.log.actions()
Type: TestLog.
Console for custom log records. It uses pino API.
server.on('connected', client => {
server.logger.info(
{ domain: client.httpHeaders.domain },
'Client domain'
)
})
Type: { debug: LogFn, error: LogFn, fatal: LogFn, info: LogFn, warn: LogFn }.
Server unique ID.
console.log('Error was raised on ' + server.nodeId)
Type: string.
Connected client by node ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient>.
Server options.
console.log('Server options', server.options.subprotocol)
Type: BaseServerOptions.
Clients subscribed to some channel.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: { }.
Time replacement without variable parts like current timestamp.
Type: TestTime.
Connected client by user ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient[]>.
Add new client for server. You should call this method manually
mostly for test purposes.
server.addClient(test.right)
Returns number. Client ID.
Set authenticate function. It will receive client credentials
and node ID. It should return a Promise with true or false.
server.auth(async ({ userId, cookie }) => {
const user = await findUserByToken(cookie.token)
return !!user && userId === user.id
})
Define the channel.
server.channel('user/:id', {
access (ctx, action, meta) {
return ctx.params.id === ctx.userId
}
filter (ctx, action, meta) {
return (otherCtx, otherAction, otherMeta) => {
return !action.hidden
}
}
async load (ctx, action, meta) {
const user = await db.loadUser(ctx.params.id)
ctx.sendBack({ type: 'USER_NAME', name: user.name })
}
})
| Argument | Type | Description |
|---|
pattern | string | Pattern for channel name. |
callbacks | ChannelCallbacks | Callback during subscription process. |
options ? | ChannelOptions | Additional options |
| Argument | Type | Description |
|---|
pattern | RegExp | Regular expression for channel name. |
callbacks | ChannelCallbacks | Callback during subscription process. |
options ? | ChannelOptions | Additional options |
Create and connect client.
server = new TestServer()
let client = await server.connect('10')
Returns Promise<TestClient>. Promise with new client.
Send runtime error stacktrace to all clients.
process.on('uncaughtException', e => {
server.debugError(e)
})
| Argument | Type | Description |
|---|
error | Error | Runtime error instance. |
Stop server and unbind all listeners.
afterEach(() => {
testServer.destroy()
})
Returns Promise. Promise when all listeners will be removed.
Wait until the client will confirm all actions, which were sent to it.
Use it to send a long history without loading it all into the memory:
the client’s speed will limit how fast you read the database.
while (await server.drain(clientId)) {
let page = await cursor.next(100)
if (!page.length) break
server.log.add(page.map(i => [i.action, { clients: [clientId] }]))
}
| Argument | Type | Description |
|---|
clientId | string | Client ID. |
Returns Promise<boolean>. Promise with false if the client was disconnected.
Call callback and throw an error if there was no Action was denied
during callback.
await server.expectDenied(async () => {
client.subscribe('secrets')
})
| Argument | Type | Description |
|---|
test | () => unknown | Callback with subscripting or action sending. |
Returns Promise.
Call callback and throw an error if there was no error during
server processing.
| Argument | Type | Description |
|---|
text | string | RegExp | RegExp or string of error message. |
test | () => unknown | Callback with subscripting or action sending. |
Returns Promise.
Call callback and throw an error if there was no logux/undo in return
with specific reason.
await server.expectUndo('notFound', async () => {
client.subscribe('projects/nothing')
})
| Argument | Type | Description |
|---|
reason | string | The reason in undo action. |
test | () => unknown | Callback with subscripting or action sending. |
Returns Promise.
Try to connect client and throw an error is client didn’t received
Wrong Cregentials message from the server.
server = new TestServer()
await server.expectWrongCredentials('10')
Returns Promise. Promise until check.
Handle WebSocket connection explicitly
This is a low-level method allowing to integrate Logux server with an existing server
fastify.get('/', { websocket: true }, (socket, req) => {
loguxServer.handleClient(socket, req)
})
Add non-WebSocket HTTP request processor.
server.http('GET', '/auth', (req, res) => {
let token = signIn(req)
if (token) {
res.setHeader('Set-Cookie', `token=${token}; Secure; HttpOnly`)
res.end()
} else {
res.statusCode = 400
res.end('Wrong user or password')
}
})
Start WebSocket server and listen for clients.
Returns Promise. When the server has been bound.
| Argument | Type | Description |
|---|
event | "subscriptionCancelled" | The event name. |
listener | () => void | Event listener. |
| Argument | Type | Description |
|---|
event | "processed" | The event name. |
listener | (action: Action, meta: ServerMeta, latencyMilliseconds: number) => void | Processing listener. |
| Argument | Type | Description |
|---|
event | "add" | "clean" | The event name. |
listener | (action: Action, meta: ServerMeta) => void | Action listener. |
| Argument | Type | Description |
|---|
event | "connected" | "disconnected" | The event name. |
listener | (client: ServerClient) => void | Client listener. |
| Argument | Type | Description |
|---|
event | "clientError" | "fatal" | The event name. |
listener | (err: Error) => void | The listener function. |
| Argument | Type | Description |
|---|
event | "error" | The event name. |
listener | (err: Error, action: Action, meta: ServerMeta) => void | Error listener. |
| Argument | Type | Description |
|---|
event | "authenticated" | "unauthenticated" | The event name. |
listener | (client: ServerClient, latencyMilliseconds: number) => void | Client listener. |
| Argument | Type | Description |
|---|
event | "preadd" | The event name. |
listener | (action: Action, meta: ServerMeta) => void | Action listener. |
| Argument | Type | Description |
|---|
event | "subscribed" | The event name. |
listener | (action: LoguxSubscribeAction, meta: ServerMeta, latencyMilliseconds: number) => void | Subscription listener. |
| Argument | Type | Description |
|---|
event | "unsubscribed" | The event name. |
listener | (action: LoguxUnsubscribeAction, meta: ServerMeta, clientNodeId: string) => void | Subscription listener. |
| Argument | Type | Description |
|---|
event | "report" | The event name. |
listener | Reporter | Report listener. |
Returns Unsubscribe.
Set callbacks for unknown channel subscription.
server.otherChannel({
async access (ctx, action, meta) {
const res = await phpBackend.checkChannel(ctx.params[0], ctx.userId)
if (res.code === 404) {
this.wrongChannel(action, meta)
return false
} else {
return response.body === 'granted'
}
}
})
| Argument | Type | Description |
|---|
callbacks | ChannelCallbacks | Callback during subscription process. |
Define callbacks for actions, which type was not defined
by any Server#type. Useful for proxy or some hacks.
Without this settings, server will call Server#unknownType
on unknown type.
server.otherType(
async access (ctx, action, meta) {
const response = await phpBackend.checkByHTTP(action, meta)
if (response.code === 404) {
this.unknownType(action, meta)
return false
} else {
return response.body === 'granted'
}
}
async process (ctx, action, meta) {
return await phpBackend.sendHTTP(action, meta)
}
})
| Argument | Type | Description |
|---|
callbacks | ActionCallbacks | Callbacks for actions with this type. |
Add new action to the server and return the Promise until it will be
resend to clients and processed.
| Argument | Type | Description |
|---|
action | TypeAction | New action to resend and process. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise<ServerMeta>. Promise until new action will be resend to clients and processed.
Send action, received by other server, to all clients of current server.
This method is for multi-server configuration only.
server.on('add', (action, meta) => {
if (meta.server === server.nodeId) {
sendToOtherServers(action, meta)
}
})
onReceivingFromOtherServer((action, meta) => {
server.sendAction(action, meta)
})
Returns void | Promise.
Change a way how server loads actions history for the client.
server.sendOnConnect(async (ctx, lastSynced) => {
return db.loadActions({ user: ctx.userId, after: lastSynced })
})
Actions should be returned from the newest one to the oldest one.
The list will be split into messages by the syncBatch option, but it
is loaded into the memory as a whole. For a big history send it by
pages with Context#sendBack() and Context#drain() instead.
Set meta.added to let the client ask only for newer actions after
the reconnect: the biggest added will be sent as the sync position.
| Argument | Type | Description |
|---|
loader | ConnectLoader | Callback which loads list of actions and meta. |
Send logux/subscribed if client was not already subscribed.
server.subscribe(ctx.nodeId, `users/${loaded}`)
| Argument | Type | Description |
|---|
nodeId | string | Node ID. |
channel | string | Channel name. |
| Argument | Type | Description |
|---|
actionCreator | Creator | Action creator function. |
callbacks | ActionCallbacks | Callbacks for action created by creator. |
options ? | TypeOptions | Additional options |
| Argument | Type | Description |
|---|
name | RegExp | TypeAction["type"] | The action’s type or action’s type matching rule as RegExp.. |
callbacks | ActionCallbacks | Callbacks for actions with this type. |
options ? | TypeOptions | Additional options |
Undo action from client.
if (couldNotFixConflict(action, meta)) {
server.undo(action, meta)
}
| Argument | Type | Description |
|---|
action | Action | The original action to undo. |
meta | ServerMeta | The action’s metadata. |
reason ? | string | Optional code for reason. Default is 'error'. |
extra ? | object | Extra fields to logux/undo action. |
Returns Promise. When action was saved to the log.
If you receive action with unknown type, this method will mark this action
with error status and undo it on the clients.
If you didn’t set Server#otherType,
Logux will call it automatically.
server.otherType({
access (ctx, action, meta) {
if (action.type.startsWith('myapp/')) {
return proxy.access(action, meta)
} else {
server.unknownType(action, meta)
}
}
})
| Argument | Type | Description |
|---|
action | Action | The action with unknown type. |
meta | ServerMeta | Action’s metadata. |
Report that client try to subscribe for unknown channel.
Logux call it automatically,
if you will not set Server#otherChannel.
server.otherChannel({
async access (ctx, action, meta) {
const res = phpBackend.checkChannel(params[0], ctx.userId)
if (res.code === 404) {
this.wrongChannel(action, meta)
return false
} else {
return response.body === 'granted'
}
}
})
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()
})
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()
})
Returns TestLog.
Last letd number in log’s nodeId.
Type: number.
Return next test log in same time.
it('tests 2 logs', () => {
const time = new TestTime()
const log1 = time.nextLog()
const log2 = time.nextLog()
})
Returns TestLog.
Base methods for synchronization nodes. Client and server nodes
are based on this module.
| 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.
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. |
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' })
| Parameter | Type | Description |
|---|
opts | LogOptions | Log options. |
Unique node ID. It is used in action IDs.
Type: string.
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 })
})
| Argument | Type | Description |
|---|
action | NewAction | The new action. |
meta ? | Partial<LogMeta> | Open structure for action metadata. |
Returns Promise<false | LogMeta>. Promise with meta if action was added to log or false
if action was already in log.
Add reason tags to metadata of actions, which are already in the log. Reasons, which action already has, will not be duplicated.
log.addReason('last-value', { id: meta.id })
| Argument | Type | Description |
|---|
reasons | string | string[] | The reason name or names. |
criteria ? | Criteria | Criteria to select actions for reason adding. |
Returns Promise. Promise when adding will be finished.
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 })
}
| Argument | Type | Description |
|---|
id | string | Action ID. |
Returns Promise<[null, null] | [Action, LogMeta]>. Promise with array of action and metadata.
Change action metadata. You will remove action by setting reasons: [].
await process(action)
log.changeMeta(action, { status: 'processed' })
| Argument | Type | Description |
|---|
id | string | Action ID. |
diff | Partial<LogMeta> | Object with values to change in action metadata. |
Returns Promise<boolean>. Promise with true if metadata was changed or false
on unknown ID.
| Argument | Type | Description |
|---|
callback | ActionIterator | Function will be executed on every action. |
| Argument | Type | Description |
|---|
callback | ActionIterator | Function will be executed on every action. |
Returns Promise.
Generate next unique action ID.
const id = log.generateId()
Returns string. Unique ID for action.
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.
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')
}
})
| Argument | Type | Description |
|---|
event | "add" | "clean" | The event name. |
listener | ReadonlyListener | The listener function. |
| Argument | Type | Description |
|---|
event | "preadd" | The event name. |
listener | PreaddListener | The listener function. |
| Argument | Type | Description |
|---|
event | "batch" | The event name. |
listener | (entries: [Action, LogMeta][]) => void | The listener function. |
Returns Unsubscribe. Unbind listener from event.
Remove reason tags from actions’ metadata and remove actions without
reasons from log.
onSync(lastSent) {
log.removeReason('unsynchronized', { maxAdded: lastSent })
}
| Argument | Type | Description |
|---|
reasons | string | string[] | The reason name or names. |
criteria ? | Criteria | Criteria to select actions for reason removing. |
Returns Promise. Promise when cleaning will be finished.
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()
}
| Argument | Type | Description |
|---|
type | NewAction["type"] | Action’s type. |
listener | ReadonlyListener | The listener function. |
opts ? | { event?: "add" | "clean", id?: string } | |
| Argument | Type | Description |
|---|
type | NewAction["type"] | Action’s type. |
listener | PreaddListener | The 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()
})
Add action to store. Action always will have type property.
Returns Promise<false | ServerMeta>. 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, ServerMeta]>. Promise with array of action and metadata.
Change action metadata.
| Argument | Type | Description |
|---|
id | string | Action ID. |
diff | Partial<ServerMeta> | 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, ServerMeta]>. 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 WsBinaryConnection.
Logux connection for server WebSocket.
Automatically handles both binary and text protocol clients.
When a text-based client connects, it falls back to JSON encoding.
import { ServerConnection } from '@logux/core'
import { Server } from 'ws'
wss.on('connection', function connection(ws) {
const connection = new ServerConnection(ws)
const node = new ServerNode('server', log, connection, opts)
})
| Parameter | Type | Description |
|---|
ws | WebSocket | WebSocket connection instance |
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 connection instance
Type: WebSocket.
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. |
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
}
| Argument | Type | Description |
|---|
firstMeta | string | ServerMeta | undefined | Some action’s metadata. |
secondMeta | string | ServerMeta | undefined | Other action’s metadata. |
Returns boolean.
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)
| Argument | Type | Description |
|---|
id | string | Action or Node ID |
Returns IDComponents.
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 | Description |
|---|
ctx | Context | Information about node, who create this action. |
action | TypeAction | The action data. |
meta | ServerMeta | The action metadata. |
| Argument | Type |
|---|
action | Action |
meta | LogMeta |
Returns void | boolean.
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 }.
List of meta keys permitted for clients.
import { ALLOWED_META } from '@logux/server'
async function onSend (action, meta) {
const filtered = { }
for (const i in meta) {
if (ALLOWED_META.includes(i)) {
filtered[i] = meta[i]
}
}
return [action, filtered]
}
Type: string[].
| Property | Type |
|---|
connectionId | string |
nodeId | string |
subprotocol | string |
| Argument | Type |
|---|
nodeId | string |
token | string |
headers | object | Headers |
Returns Promise<boolean>.
| Property | Type |
|---|
client | ServerClient |
cookie | { [key: string]: string } |
token | string |
userId | string |
| Argument | Type | Description |
|---|
ctx | Context | Information about node, who create this action. |
action | TypeAction | The action data. |
meta | ServerMeta | The action metadata. |
Returns boolean | Promise<boolean>.
Base server class to extend.
Connected client by client ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient>.
Connected clients.
for (let client of server.connected.values()) {
console.log(client.remoteAddress)
}
Type: Map<string,ServerClient>.
Production or development mode.
if (server.env === 'development') {
logDebugData()
}
Type: "development" | "production".
Server actions log.
server.log.each(finder)
Type: ServerLog.
Console for custom log records. It uses pino API.
server.on('connected', client => {
server.logger.info(
{ domain: client.httpHeaders.domain },
'Client domain'
)
})
Type: { debug: LogFn, error: LogFn, fatal: LogFn, info: LogFn, warn: LogFn }.
Server unique ID.
console.log('Error was raised on ' + server.nodeId)
Type: string.
Connected client by node ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient>.
Server options.
console.log('Server options', server.options.subprotocol)
Type: BaseServerOptions.
Clients subscribed to some channel.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: { }.
Connected client by user ID.
Do not rely on this data, when you have multiple Logux servers.
Each server will have a different list.
Type: Map<string,ServerClient[]>.
Add new client for server. You should call this method manually
mostly for test purposes.
server.addClient(test.right)
Returns number. Client ID.
Set authenticate function. It will receive client credentials
and node ID. It should return a Promise with true or false.
server.auth(async ({ userId, cookie }) => {
const user = await findUserByToken(cookie.token)
return !!user && userId === user.id
})
Define the channel.
server.channel('user/:id', {
access (ctx, action, meta) {
return ctx.params.id === ctx.userId
}
filter (ctx, action, meta) {
return (otherCtx, otherAction, otherMeta) => {
return !action.hidden
}
}
async load (ctx, action, meta) {
const user = await db.loadUser(ctx.params.id)
ctx.sendBack({ type: 'USER_NAME', name: user.name })
}
})
| Argument | Type | Description |
|---|
pattern | string | Pattern for channel name. |
callbacks | ChannelCallbacks | Callback during subscription process. |
options ? | ChannelOptions | Additional options |
| Argument | Type | Description |
|---|
pattern | RegExp | Regular expression for channel name. |
callbacks | ChannelCallbacks | Callback during subscription process. |
options ? | ChannelOptions | Additional options |
Send runtime error stacktrace to all clients.
process.on('uncaughtException', e => {
server.debugError(e)
})
| Argument | Type | Description |
|---|
error | Error | Runtime error instance. |
Stop server and unbind all listeners.
afterEach(() => {
testServer.destroy()
})
Returns Promise. Promise when all listeners will be removed.
Wait until the client will confirm all actions, which were sent to it.
Use it to send a long history without loading it all into the memory:
the client’s speed will limit how fast you read the database.
while (await server.drain(clientId)) {
let page = await cursor.next(100)
if (!page.length) break
server.log.add(page.map(i => [i.action, { clients: [clientId] }]))
}
| Argument | Type | Description |
|---|
clientId | string | Client ID. |
Returns Promise<boolean>. Promise with false if the client was disconnected.
Handle WebSocket connection explicitly
This is a low-level method allowing to integrate Logux server with an existing server
fastify.get('/', { websocket: true }, (socket, req) => {
loguxServer.handleClient(socket, req)
})
Add non-WebSocket HTTP request processor.
server.http('GET', '/auth', (req, res) => {
let token = signIn(req)
if (token) {
res.setHeader('Set-Cookie', `token=${token}; Secure; HttpOnly`)
res.end()
} else {
res.statusCode = 400
res.end('Wrong user or password')
}
})
Start WebSocket server and listen for clients.
Returns Promise. When the server has been bound.
| Argument | Type | Description |
|---|
event | "subscriptionCancelled" | The event name. |
listener | () => void | Event listener. |
| Argument | Type | Description |
|---|
event | "processed" | The event name. |
listener | (action: Action, meta: ServerMeta, latencyMilliseconds: number) => void | Processing listener. |
| Argument | Type | Description |
|---|
event | "add" | "clean" | The event name. |
listener | (action: Action, meta: ServerMeta) => void | Action listener. |
| Argument | Type | Description |
|---|
event | "connected" | "disconnected" | The event name. |
listener | (client: ServerClient) => void | Client listener. |
| Argument | Type | Description |
|---|
event | "clientError" | "fatal" | The event name. |
listener | (err: Error) => void | The listener function. |
| Argument | Type | Description |
|---|
event | "error" | The event name. |
listener | (err: Error, action: Action, meta: ServerMeta) => void | Error listener. |
| Argument | Type | Description |
|---|
event | "authenticated" | "unauthenticated" | The event name. |
listener | (client: ServerClient, latencyMilliseconds: number) => void | Client listener. |
| Argument | Type | Description |
|---|
event | "preadd" | The event name. |
listener | (action: Action, meta: ServerMeta) => void | Action listener. |
| Argument | Type | Description |
|---|
event | "subscribed" | The event name. |
listener | (action: LoguxSubscribeAction, meta: ServerMeta, latencyMilliseconds: number) => void | Subscription listener. |
| Argument | Type | Description |
|---|
event | "unsubscribed" | The event name. |
listener | (action: LoguxUnsubscribeAction, meta: ServerMeta, clientNodeId: string) => void | Subscription listener. |
| Argument | Type | Description |
|---|
event | "report" | The event name. |
listener | Reporter | Report listener. |
Returns Unsubscribe.
Set callbacks for unknown channel subscription.
server.otherChannel({
async access (ctx, action, meta) {
const res = await phpBackend.checkChannel(ctx.params[0], ctx.userId)
if (res.code === 404) {
this.wrongChannel(action, meta)
return false
} else {
return response.body === 'granted'
}
}
})
| Argument | Type | Description |
|---|
callbacks | ChannelCallbacks | Callback during subscription process. |
Define callbacks for actions, which type was not defined
by any Server#type. Useful for proxy or some hacks.
Without this settings, server will call Server#unknownType
on unknown type.
server.otherType(
async access (ctx, action, meta) {
const response = await phpBackend.checkByHTTP(action, meta)
if (response.code === 404) {
this.unknownType(action, meta)
return false
} else {
return response.body === 'granted'
}
}
async process (ctx, action, meta) {
return await phpBackend.sendHTTP(action, meta)
}
})
| Argument | Type | Description |
|---|
callbacks | ActionCallbacks | Callbacks for actions with this type. |
Add new action to the server and return the Promise until it will be
resend to clients and processed.
| Argument | Type | Description |
|---|
action | TypeAction | New action to resend and process. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise<ServerMeta>. Promise until new action will be resend to clients and processed.
Send action, received by other server, to all clients of current server.
This method is for multi-server configuration only.
server.on('add', (action, meta) => {
if (meta.server === server.nodeId) {
sendToOtherServers(action, meta)
}
})
onReceivingFromOtherServer((action, meta) => {
server.sendAction(action, meta)
})
Returns void | Promise.
Change a way how server loads actions history for the client.
server.sendOnConnect(async (ctx, lastSynced) => {
return db.loadActions({ user: ctx.userId, after: lastSynced })
})
Actions should be returned from the newest one to the oldest one.
The list will be split into messages by the syncBatch option, but it
is loaded into the memory as a whole. For a big history send it by
pages with Context#sendBack() and Context#drain() instead.
Set meta.added to let the client ask only for newer actions after
the reconnect: the biggest added will be sent as the sync position.
| Argument | Type | Description |
|---|
loader | ConnectLoader | Callback which loads list of actions and meta. |
Send logux/subscribed if client was not already subscribed.
server.subscribe(ctx.nodeId, `users/${loaded}`)
| Argument | Type | Description |
|---|
nodeId | string | Node ID. |
channel | string | Channel name. |
| Argument | Type | Description |
|---|
actionCreator | Creator | Action creator function. |
callbacks | ActionCallbacks | Callbacks for action created by creator. |
options ? | TypeOptions | Additional options |
| Argument | Type | Description |
|---|
name | RegExp | TypeAction["type"] | The action’s type or action’s type matching rule as RegExp.. |
callbacks | ActionCallbacks | Callbacks for actions with this type. |
options ? | TypeOptions | Additional options |
Undo action from client.
if (couldNotFixConflict(action, meta)) {
server.undo(action, meta)
}
| Argument | Type | Description |
|---|
action | Action | The original action to undo. |
meta | ServerMeta | The action’s metadata. |
reason ? | string | Optional code for reason. Default is 'error'. |
extra ? | object | Extra fields to logux/undo action. |
Returns Promise. When action was saved to the log.
If you receive action with unknown type, this method will mark this action
with error status and undo it on the clients.
If you didn’t set Server#otherType,
Logux will call it automatically.
server.otherType({
access (ctx, action, meta) {
if (action.type.startsWith('myapp/')) {
return proxy.access(action, meta)
} else {
server.unknownType(action, meta)
}
}
})
| Argument | Type | Description |
|---|
action | Action | The action with unknown type. |
meta | ServerMeta | Action’s metadata. |
Report that client try to subscribe for unknown channel.
Logux call it automatically,
if you will not set Server#otherChannel.
server.otherChannel({
async access (ctx, action, meta) {
const res = phpBackend.checkChannel(params[0], ctx.userId)
if (res.code === 404) {
this.wrongChannel(action, meta)
return false
} else {
return response.body === 'granted'
}
}
})
| Property | Type | Description |
|---|
cert ? | string | SSL certificate or path to it. Path could be relative from server
root. It is required in production mode, because WSS is highly
recommended. |
cleanFromLog ? | RegExp | Regular expression which should be cleaned from error message and stack. |
disableHttpServer ? | boolean | Disable health check endpoint, Server#http. |
env ? | "development" | "production" | Development or production server mode. By default,
it will be taken from NODE_ENV environment variable.
On empty NODE_ENV it will be 'development'. |
fileUrl ? | string | URL of main JS file in the root dir for the cases where you can’t use
import.meta.dirname. |
host ? | string | IP-address to bind server. Default is 127.0.0.1. |
id ? | string | Custom random ID to be used in node ID. |
key ? | string | { pem: string } | SSL key or path to it. Path could be relative from server root.
It is required in production mode, because WSS is highly recommended. |
minSubprotocol ? | number | The version requirements for client subprotocol version. |
Node ? | ServerNodeConstructor | Replace class for ServerNode. |
pid ? | number | Process ID, to display in logs. |
ping ? | number | Milliseconds since last message to test connection by sending ping.
Default is 20000. |
port ? | string | number | Port to bind server. It will create HTTP server manually to connect
WebSocket server to it. Default is 31337. |
redis ? | string | URL to Redis for Logux Server Pro scaling. |
root ? | string | Application root to load files and show errors.
Default is process.cwd(). |
server ? | any | HTTP server to serve Logux’s WebSocket and HTTP requests. |
store ? | any | Store to save log. Will be {@link @logux/core:MemoryStore}, by default. |
subprotocol ? | number | Server current application subprotocol version. |
syncBatch ? | number | How many actions could be sent in a single message. 100 by default. |
time ? | any | Test time to test server. |
timeout ? | number | Timeout in milliseconds to disconnect connection.
Default is 70000. |
Add last changed time to value to use in conflict resolution.
If you do not know the time, use NoConflictResolution.
| Argument | Type | Description |
|---|
value | Value | The value. |
time | number | UNIX milliseconds. |
Returns WithTime. Wrapper.
| Argument | Type | Description |
|---|
ctx | ChannelContext | Information about node, who create this action. |
action | SubscribeAction | The action data. |
meta | ServerMeta | The action metadata. |
Returns boolean | Promise<boolean>.
| Argument | Type | Description |
|---|
ctx | Context | Information about node, who create this action. |
action | Action | The action data. |
meta | ServerMeta | The action metadata. |
Returns boolean | Promise<boolean>.
| Argument | Type | Description |
|---|
ctx | ChannelContext | Information about node, who create this action. |
action | SubscribeAction | The action data. |
meta | ServerMeta | The action metadata. |
| Argument | Type | Description |
|---|
ctx | ChannelContext | Information about node, who create this action. |
action | SubscribeAction | The action data. |
meta | ServerMeta | The action metadata. |
Returns any.
| Property | Type | Description |
|---|
queue ? | string | Name of the queue that will be used to process channels
with the specified name pattern. Default is 'main' |
Unique persistence client ID.
server.clientIds.get(node.clientId)
Type: string.
Client’s headers.
ctx.sendBack({
type: 'error',
message: I18n[ctx.headers.locale || 'en'].error
})
Type: Headers.
Unique node ID.
server.nodeIds.get(node.nodeId)
Type: string.
Action creator application subprotocol version.
Type: number.
User ID taken node ID.
async access (ctx, action, meta) {
const user = await db.getUser(ctx.userId)
return user.admin
}
Type: string.
Wait until the client will confirm all actions, which were sent to it.
Use it to send a long history without loading it all into the memory:
the client’s speed will limit how fast you read the database.
while (await ctx.drain()) {
let page = await cursor.next(100)
if (!page.length) break
ctx.sendBack(page.map(i => i.action))
}
Returns Promise<boolean>. Promise with false if the client was disconnected.
Send action back to the client.
ctx.sendBack({ type: 'login/success', token })
An array of actions will be sent in a single message. Use it to send
a big history page by page instead of a message per action.
ctx.sendBack(page.map(i => i.action))
Every action in the array can have own meta as [action, meta].
Action will not be processed by server’s callbacks from Server#type.
| Argument | Type | Description |
|---|
action | TypeAction | TypeAction | [TypeAction, Partial<ServerMeta>][] | The action or the array of actions. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise. Promise until action was added to the server log.
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 }.
| 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 ? | ServerMeta | Change reasons only for actions older than specific action. |
youngerThan ? | ServerMeta | Change reasons only for actions younger than specific action. |
| 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. |
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.
| Argument | Type |
|---|
objs | unknown[] |
| Property | Type |
|---|
debug | (details: object, message: string) => void |
error | (details: object, message: string) => void |
fatal | (details: object, message: string) => void |
info | (details: object, message: string) => void |
warn | (details: object, message: string) => void |
| Property | Type | Description |
|---|
color ? | boolean | Use color for human output. |
stream ? | LogStream | Stream to be used by logger to write log. |
type ? | "human" | "json" | Logger message format. |
| 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 | ServerMeta>. 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, ServerMeta]>. Promise with array of action and metadata.
Change action metadata.
| Argument | Type | Description |
|---|
id | string | Action ID. |
diff | Partial<ServerMeta> | 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, ServerMeta]>. 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.
| Property | Type |
|---|
flushSync ? | () => void |
write | (str: string) => void |
Extends Error.
| Parameter | Type |
|---|
message ? | string |
| Parameter | Type |
|---|
message ? | string |
options ? | ErrorOptions |
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" |
| 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. |
Mark that the value has no last changed date and conflict resolution
can’t be applied.
| Argument | Type | Description |
|---|
value | Value | The value. |
Returns WithTime. Wrapper.
| 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.
Database driver, which the store can use without any wrapper:
pg’s Pool or Client, PGlite, or postgres.
The parameters are never[], so that the driver’s own stricter types
for them still match this type.
Type: { begin?: (body: (tx: PostgresDriver) => Promise<unknown>) => Promise<unknown>, unsafe: (sql: string, params: never[]) => Promise<unknown> } | { connect?: () => Promise<unknown>, query: (sql: string, params: never[]) => Promise<unknown>, transaction?: (body: (tx: PostgresDriver) => Promise<unknown>) => Promise<unknown> }.
| Argument | Type |
|---|
sql | string |
params | unknown[] |
Returns Promise<PostgresRows>.
Run the callback on a single connection inside a transaction.
Without it init() can not lock the migrations, so two servers
starting at the same moment can apply them twice.
Type: (body: (query: PostgresQuery) => Promise) => Promise.
Rows of the query. pg and PGlite keep them in the result object,
postgres returns them as an array.
Type: { rows: { [key: string]: unknown }[] } | { [key: string]: unknown }[].
Log store, which keeps actions in PostgreSQL.
It takes the database of pg, postgres, or PGlite. For any other driver,
pass the function to send the query, see PostgresQuery.
import { PostgresStore, Server } from '@logux/server'
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const store = new PostgresStore(pool)
await store.init()
const server = new Server(Server.loadOptions(process, {
minSubprotocol: 1,
subprotocol: 1,
root: import.meta.dirname,
store
}))
| Argument | Type |
|---|
reasons | string[] |
criteria | Criteria |
Returns Promise.
Returns Promise<boolean>.
Bring the log tables to the latest version, creating them if they were
not created by the application’s migration.
Returns Promise.
| Property | Type | Description |
|---|
packers ? | Packers | Packers to keep the binary parts of the action in the blob column
instead of Base64 inside JSON. The key must be the type of the action. |
pageSize ? | number | How many entries to load by a single query in get(). |
| Argument | Type |
|---|
action | ListenerAction |
meta | LogMeta |
| Argument | Type | Description |
|---|
ctx | Context | Information about node, who create this action. |
action | TypeAction | The action data. |
meta | ServerMeta | The action metadata. |
Returns void | Promise.
| 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 |
|---|
add | ActionReporter |
addClean | ActionReporter |
authenticated | AuthenticationReporter |
clean | CleanReporter |
clientError | { connectionId?: string, err: Error, nodeId?: string } |
connect | { connectionId: string, ipAddress: string } |
denied | CleanReporter |
destroy | void |
disconnect | { connectionId?: string, nodeId?: string } |
error | { actionId?: any, connectionId?: string, err: Error, fatal?: true, nodeId?: string } |
listen | { cert: boolean, environment: "development" | "production", host: string, loguxServer: string, minSubprotocol: number, nodeId: string, notes: object, port: string, redis: string, server: boolean, subprotocol: number } |
processed | { actionId: ID, latency: number } |
subscribed | SubscriptionReporter |
unauthenticated | AuthenticationReporter |
unknownType | { actionId: ID, type: string } |
unsubscribed | SubscriptionReporter |
useless | ActionReporter |
wrongChannel | SubscriptionReporter |
zombie | { nodeId: string } |
Type: { channel?: string, channels?: string[], client?: string, clients?: string[], excludeClients?: string[], node?: string, nodes?: string[], user?: string, users?: string[] } | string | string[].
| Argument | Type | Description |
|---|
ctx | Context | Information about node, who create this action. |
action | TypeAction | The action data. |
meta | ServerMeta | The action metadata. |
Returns Promise<Resend> | Resend.
Returns boolean | Promise<boolean>.
| Property | Type | Description |
|---|
channel ? | string | All nodes subscribed to channel will receive the action. |
channels ? | string[] | All nodes subscribed to listed channels will receive the action. |
client ? | string | All nodes with listed client ID will receive the action. |
clients ? | string[] | All nodes with listed client IDs will receive the action. |
excludeClients ? | string[] | Client IDs, which will not receive the action. |
node ? | string | Node with listed node ID will receive the action. |
nodes ? | string[] | All nodes with listed node IDs will receive the action. |
server | string | Node ID of the server received the action. |
status ? | "error" | "processed" | "waiting" | Action processing status |
user ? | string | All nodes with listed user ID will receive the action. |
users ? | string[] | All nodes with listed user IDs will receive the action. |
Extends BaseNode.
Server node in synchronization pair.
Instead of client node, it doesn’t initialize synchronization
and destroy itself on disconnect.
import { ServerNode } from '@logux/core'
startServer(ws => {
const connection = new ServerConnection(ws)
const node = new ServerNode('server' + id, 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 |
|---|
cert ? | string | SSL certificate or path to it. Path could be relative from server
root. It is required in production mode, because WSS is highly
recommended. |
cleanFromLog ? | RegExp | Regular expression which should be cleaned from error message and stack. |
disableHttpServer ? | boolean | Disable health check endpoint, Server#http. |
env ? | "development" | "production" | Development or production server mode. By default,
it will be taken from NODE_ENV environment variable.
On empty NODE_ENV it will be 'development'. |
fileUrl ? | string | URL of main JS file in the root dir for the cases where you can’t use
import.meta.dirname. |
host ? | string | IP-address to bind server. Default is 127.0.0.1. |
id ? | string | Custom random ID to be used in node ID. |
key ? | string | { pem: string } | SSL key or path to it. Path could be relative from server root.
It is required in production mode, because WSS is highly recommended. |
logger ? | Logger | LoggerOptions | Logger with custom settings. |
minSubprotocol ? | number | The version requirements for client subprotocol version. |
Node ? | ServerNodeConstructor | Replace class for ServerNode. |
pid ? | number | Process ID, to display in logs. |
ping ? | number | Milliseconds since last message to test connection by sending ping.
Default is 20000. |
port ? | string | number | Port to bind server. It will create HTTP server manually to connect
WebSocket server to it. Default is 31337. |
redis ? | string | URL to Redis for Logux Server Pro scaling. |
root ? | string | Application root to load files and show errors.
Default is process.cwd(). |
server ? | any | HTTP server to serve Logux’s WebSocket and HTTP requests. |
store ? | any | Store to save log. Will be {@link @logux/core:MemoryStore}, by default. |
subprotocol ? | number | Server current application subprotocol version. |
syncBatch ? | number | How many actions could be sent in a single message. 100 by default. |
time ? | any | Test time to test server. |
timeout ? | number | Timeout in milliseconds to disconnect connection.
Default is 70000. |
| Property | Type |
|---|
id | string |
type | "shadow" |
| Property | Type |
|---|
actionId | ID |
channel | string |
Returns boolean | Promise<boolean>.
| 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 |
|---|
access ? | (ctx: Context, filter: Partial<Value> | undefined, action: LoguxSubscribeAction, meta: ServerMeta) => boolean | Promise<boolean> |
actions ? | (ctx: Context, filter: Partial<Value> | undefined, action: LoguxSubscribeAction, meta: ServerMeta) => void | Promise<SyncMapActionFilter> | SyncMapActionFilter |
initial | (ctx: Context, filter: Partial<Value> | undefined, since: number | undefined, action: LoguxSubscribeAction, meta: ServerMeta) => SyncMapData[] | Promise<SyncMapData[]> |
| Property | Type |
|---|
access | (ctx: Context, id: string, action: any, meta: ServerMeta) => boolean | Promise<boolean> |
change ? | (ctx: Context, id: string, fields: Partial<Value>, time: number, action: SyncMapChangeAction, meta: ServerMeta) => void | boolean | Promise<void | boolean> |
create ? | (ctx: Context, id: string, fields: Value, time: number, action: SyncMapCreateAction, meta: ServerMeta) => void | boolean | Promise<void | boolean> |
delete ? | (ctx: Context, id: string, action: SyncMapDeleteAction, meta: ServerMeta) => void | boolean | Promise<void | boolean> |
load ? | (ctx: Context, id: string, since: number | undefined, action: LoguxSubscribeAction, meta: ServerMeta) => false | Promise<false | SyncMapData> | SyncMapData |
Type: boolean | null | number | string | undefined.
| Property | Type |
|---|
id | string |
subprotocol ? | number |
time | number |
| Property | Type |
|---|
cookie ? | object |
subprotocol ? | number |
token ? | string |
| Property | Type | Description |
|---|
nodeId ? | string | Unique log name. |
store ? | LogStore | Store for log. Will use MemoryStore by default. |
| Property | Type | Description |
|---|
auth ? | false | Disable built-in auth. |
cert ? | string | SSL certificate or path to it. Path could be relative from server
root. It is required in production mode, because WSS is highly
recommended. |
cleanFromLog ? | RegExp | Regular expression which should be cleaned from error message and stack. |
disableHttpServer ? | boolean | Disable health check endpoint, Server#http. |
env ? | "development" | "production" | Development or production server mode. By default,
it will be taken from NODE_ENV environment variable.
On empty NODE_ENV it will be 'development'. |
fileUrl ? | string | URL of main JS file in the root dir for the cases where you can’t use
import.meta.dirname. |
host ? | string | IP-address to bind server. Default is 127.0.0.1. |
id ? | string | Custom random ID to be used in node ID. |
key ? | string | { pem: string } | SSL key or path to it. Path could be relative from server root.
It is required in production mode, because WSS is highly recommended. |
logger ? | Logger | LoggerOptions | Logger with custom settings. |
minSubprotocol ? | number | |
Node ? | ServerNodeConstructor | Replace class for ServerNode. |
pid ? | number | Process ID, to display in logs. |
ping ? | number | Milliseconds since last message to test connection by sending ping.
Default is 20000. |
port ? | string | number | Port to bind server. It will create HTTP server manually to connect
WebSocket server to it. Default is 31337. |
redis ? | string | URL to Redis for Logux Server Pro scaling. |
root ? | string | Application root to load files and show errors.
Default is process.cwd(). |
server ? | any | HTTP server to serve Logux’s WebSocket and HTTP requests. |
store ? | any | Store to save log. Will be {@link @logux/core:MemoryStore}, by default. |
subprotocol ? | number | |
syncBatch ? | number | How many actions could be sent in a single message. 100 by default. |
time ? | any | Test time to test server. |
timeout ? | number | Timeout in milliseconds to disconnect connection.
Default is 70000. |
Returns string | Promise<string>.
| Property | Type | Description |
|---|
queue ? | string | Name of the queue that will be used to process actions
of the specified type. Default is 'main' |
| Property | Type |
|---|
supported | number |
used | number |
| Property | Type |
|---|
[WITH_TIME] | false |
time | undefined |
value | Value |
| Property | Type |
|---|
[WITH_TIME] | true |
time | number |
value | Value |
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. |
Type: { type: "0/clean" } & { id: string } | { ids: string[] }.
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. |
Remove all non-allowed keys from meta.
| Argument | Type | Description |
|---|
meta | ServerMeta | Meta to remove keys. |
Returns ServerMeta. Meta with removed keys.
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.
Returns logux/undo action.
| Argument | Type |
|---|
fields | { action: RevertedAction, id: string, reason: Reason } |
Returns LoguxUndoAction.
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.
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.
Return false if cb() got response error with 403.
import { wasNot403 } from '@logux/server'
server.auth(({ userId, token }) => {
return wasNot403(async () => {
get(`/checkUser/${userId}/${token}`)
})
})
| Argument | Type | Description |
|---|
cb | () => Promise | Callback with request calls. |
Returns Promise<boolean>.
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/node-api.md.