Extends Context.
Subscription context.
server.channel('user/:id', {
access (ctx, action, meta) {
return ctx.params.id === ctx.userId
}
})
ChannelContext#clientId
Unique persistence client ID.
server.clientIds.get(node.clientId)
Type: string
.
ChannelContext#data
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: object
.
ChannelContext#headers
Client’s headers.
ctx.sendBack({
type: 'error',
message: I18n[ctx.headers.locale || 'en'].error
})
Type: object
.
ChannelContext#isServer
Was action created by Logux server.
access: (ctx, action, meta) => ctx.isServer
Type: boolean
.
ChannelContext#nodeId
Unique node ID.
server.nodeIds.get(node.nodeId)
Type: string
.
ChannelContext#params
Parsed variable parts of channel pattern.
server.channel('user/:id', {
access (ctx, action, meta) {
action.channel //=> user/10
ctx.params //=> { id: '10' }
}
})
server.channel(/post\/(\d+)/, {
access (ctx, action, meta) {
action.channel //=> post/10
ctx.params //=> ['post/10', '10']
}
})
Type: object | string[]
.
ChannelContext#server
Logux server
Type: Server
.
ChannelContext#subprotocol
Action creator application subprotocol version in SemVer format.
Use Context#isSubprotocol
to check it.
Type: string
.
ChannelContext#userId
User ID taken node ID.
async access (ctx, action, meta) {
const user = await db.getUser(ctx.userId)
return user.admin
}
Type: string
.
ChannelContext#isSubprotocol(range)
Check creator subprotocol version. It uses semver
npm package
to parse requirements.
if (ctx.isSubprotocol('2.x')) {
useOldAPI()
}
Argument | Type | Description |
---|---|---|
range | string | npm’s version requirements. |
Returns boolean
. Is version satisfies requirements.
ChannelContext#sendBack(action, meta?)
Send action back to the client.
ctx.sendBack({ type: 'login/success', token })
Action will not be processed by server’s callbacks from Server#type
.
Argument | Type | Description |
---|---|---|
action | AnyAction | The action. |
meta ? | Partial<ServerMeta> | Action’s meta. |
Returns Promise
. Promise until action was added to the server log.
Action context.
server.type('FOO', {
access (ctx, action, meta) {
return ctx.isSubprotocol('3.x') ? check3(action) : check4(action)
}
})
Context#clientId
Unique persistence client ID.
server.clientIds.get(node.clientId)
Type: string
.
Context#data
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: object
.
Context#headers
Client’s headers.
ctx.sendBack({
type: 'error',
message: I18n[ctx.headers.locale || 'en'].error
})
Type: object
.
Context#isServer
Was action created by Logux server.
access: (ctx, action, meta) => ctx.isServer
Type: boolean
.
Context#nodeId
Unique node ID.
server.nodeIds.get(node.nodeId)
Type: string
.
Context#server
Logux server
Type: Server
.
Context#subprotocol
Action creator application subprotocol version in SemVer format.
Use Context#isSubprotocol
to check it.
Type: string
.
Context#userId
User ID taken node ID.
async access (ctx, action, meta) {
const user = await db.getUser(ctx.userId)
return user.admin
}
Type: string
.
Context#isSubprotocol(range)
Check creator subprotocol version. It uses semver
npm package
to parse requirements.
if (ctx.isSubprotocol('2.x')) {
useOldAPI()
}
Argument | Type | Description |
---|---|---|
range | string | npm’s version requirements. |
Returns boolean
. Is version satisfies requirements.
Context#sendBack(action, meta?)
Send action back to the client.
ctx.sendBack({ type: 'login/success', token })
Action will not be processed by server’s callbacks from Server#type
.
Argument | Type | Description |
---|---|---|
action | AnyAction | The action. |
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 |
opts | RequestInit |
response | string |
ResponseError#name
Type: "ResponseError"
.
ResponseError#statusCode
Type: number
.
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.0.0',
supports: '1.x || 0.x',
fileUrl: import.meta.url
}, envOptions))
server.listen()
Parameter | Type | Description |
---|---|---|
opts | ServerOptions | Server options. |
Server.loadOptions(process, defaults)
Load options from command-line arguments and/or environment.
const server = new Server(Server.loadOptions(process, {
subprotocol: '1.0.0',
supports: '1.x',
fileUrl: import.meta.url,
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.
Server#clientIds
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>
.
Server#connected
Connected clients.
for (let client of server.connected.values()) {
console.log(client.remoteAddress)
}
Type: Map<string,ServerClient>
.
Server#controls
Add callback to internal HTTP server.
Type: { [path: string]: GetProcessor | PostProcessor }
.
Server#env
Production or development mode.
if (server.env === 'development') {
logDebugData()
}
Type: "development" | "production"
.
Server#log
Server actions log.
server.log.each(finder)
Type: Log
.
Server#logger
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#nodeId
Server unique ID.
console.log('Error was raised on ' + server.nodeId)
Type: string
.
Server#nodeIds
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
Server options.
console.log('Server options', server.options.subprotocol)
Type: ServerOptions
.
Server#subscribers
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: { [channel: string]: { [nodeId: string]: { filters: { [key: string]: ChannelFilter | true }, unsubscribe?: (action: LoguxUnsubscribeAction, meta: ServerMeta) => void } } }
.
Server#userIds
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[]>
.
Server#addClient(connection)
Add new client for server. You should call this method manually mostly for test purposes.
server.addClient(test.right)
Argument | Type | Description |
---|---|---|
connection | ServerConnection | Logux connection to client. |
Returns number
. Client ID.
Server#auth(authenticator)
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
})
Argument | Type | Description |
---|---|---|
authenticator | ServerAuthenticator | The authentication callback. |
Server#autoloadModules(files?)
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
.
Server#channel(pattern, callbacks, options?)
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 |
Server#debugError(error)
Send runtime error stacktrace to all clients.
process.on('uncaughtException', e => {
server.debugError(e)
})
Argument | Type | Description |
---|---|---|
error | Error | Runtime error instance. |
Server#destroy()
Stop server and unbind all listeners.
afterEach(() => {
testServer.destroy()
})
Returns Promise
. Promise when all listeners will be removed.
Server#http(listener)
Add non-WebSocket HTTP request processor.
server.http((req, res) => {
if (req.url === '/auth') {
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')
}
}
})
Argument | Type |
---|---|
listener | (req: IncomingMessage, res: ServerResponse) => void |
Server#listen()
Start WebSocket server and listen for clients.
Returns Promise
. When the server has been bound.
Server#on(event, listener)
Argument | Type | Description |
---|---|---|
event | "subscriptionCancelled" | The event name. |
listener | () => void | Event listener. |
Argument | Type | Description |
---|---|---|
event | "subscribing" | The event name. |
listener | (action: LoguxSubscribeAction, meta: ServerMeta) => void | Subscription listener. |
Argument | Type | Description |
---|---|---|
event | "processed" | "backendGranted" | "backendProcessed" | The event name. |
listener | (action: Action, meta: ServerMeta, latencyMilliseconds: number) => void | Processing listener. |
Argument | Type | Description |
---|---|---|
event | "add" | "clean" | "backendSent" | 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
.
Server#otherChannel(callbacks)
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. |
Server#otherType(callbacks)
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. |
Server#process(action, meta?)
Add new action to the server and return the Promise until it will be resend to clients and processed.
Argument | Type | Description |
---|---|---|
action | AnyAction | 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.
Server#sendAction(action, meta)
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)
})
Argument | Type | Description |
---|---|---|
action | Action | New action. |
meta | ServerMeta | Action’s metadata. |
Returns void | Promise
.
Server#subscribe(nodeId, channel)
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. |
Server#type(actionCreator, callbacks, options?)
Define action type’s callbacks.
server.type('CHANGE_NAME', {
access (ctx, action, meta) {
return action.user === ctx.userId
},
resend (ctx, action) {
return `user/${ action.user }`
}
process (ctx, action, meta) {
if (isFirstOlder(lastNameChange(action.user), meta)) {
return db.changeUserName({ id: action.user, name: action.name })
}
}
})
Argument | Type | Description |
---|---|---|
actionCreator | AbstractActionCreator | Action creator function. |
callbacks | ActionCallbacks | Callbacks for action created by creator. |
options ? | TypeOptions | Additional options |
Argument | Type | Description |
---|---|---|
name | RegExp | Action["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 |
Server#undo(action, meta, reason?, extra?)
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.
Server#unknownType(action, meta)
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. |
Server#wrongChannel(action, meta)
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'
}
}
})
Argument | Type | Description |
---|---|---|
action | LoguxSubscribeAction | The subscribe action. |
meta | ServerMeta | Action’s metadata. |
Logux client connected to server.
const client = server.connected.get(0)
ServerClient#app
Server, which received client.
Type: BaseServer
.
ServerClient#clientId
Unique persistence machine ID. It will be undefined before correct authentication.
Type: string
.
ServerClient#connection
The Logux wrapper to WebSocket connection.
console.log(client.connection.ws.upgradeReq.headers)
Type: ServerConnection
.
ServerClient#httpHeaders
HTTP headers of WS connection.
client.httpHeaders['User-Agent']
Type: { [name: string]: string }
.
ServerClient#key
Client number used as app.connected
key.
function stillConnected (client) {
return app.connected.has(client.key)
}
Type: string
.
ServerClient#node
Node instance to synchronize logs.
if (client.node.state === 'synchronized')
Type: ServerNode
.
ServerClient#nodeId
Unique node ID. It will be undefined before correct authentication.
Type: string
.
ServerClient#processing
Does server process some action from client.
console.log('Clients in processing:', clients.map(i => i.processing))
Type: boolean
.
ServerClient#remoteAddress
Client IP address.
const clientCity = detectLocation(client.remoteAddress)
Type: string
.
ServerClient#userId
User ID. It will be filled from client’s node ID. It will be undefined before correct authentication.
Type: string
.
ServerClient#destroy()
Disconnect client.
ServerClient#isSubprotocol(range)
Check client subprotocol version. It uses semver
npm package
to parse requirements.
if (client.isSubprotocol('4.x')) {
useOldAPI()
}
Argument | Type | Description |
---|---|---|
range | string | npm’s version requirements. |
Returns boolean
. Is version satisfies requirements.
addSyncMap(server, plural, operations)
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. |
addSyncMapFilter(server, plural, operations)
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 })
// You can return only data changed after `since`
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
}
}
})
Argument | Type | Description |
---|---|---|
server | BaseServer | Server instance. |
plural | string | Prefix for channel names and action types. |
operations | SyncMapFilterOperations | Callbacks. |
del(url, opts?, fetcher?)
Syntax sugar for DELETE
requests by node-fetch
, throwing
ResponseError
on non-2xx response and parsing JSON response.
Argument | Type | Description |
---|---|---|
url | string | Resource URL. |
opts ? | Omit<RequestInit,"method"> | fetch() options. |
fetcher ? | (url: URL | RequestInfo, init?: RequestInit) => Promise<Response> | A way to replace fetch() for tests. |
Returns any
.
get(url, opts?, fetcher?)
Syntax sugar for GET
requests by node-fetch
, throwing
ResponseError
on non-2xx response and parsing JSON response.
import { get } from '@logux/server'
let user = get(url)
Argument | Type | Description |
---|---|---|
url | string | Resource URL. |
opts ? | Omit<RequestInit,"method"> | fetch() options. |
fetcher ? | (url: URL | RequestInfo, init?: RequestInit) => Promise<Response> | A way to replace fetch() for tests. |
Returns any
.
patch(url, opts?, fetcher?)
Syntax sugar for PATCH
requests by node-fetch
, throwing
ResponseError
on non-2xx response and parsing JSON response.
Argument | Type | Description |
---|---|---|
url | string | Resource URL. |
opts ? | Omit<RequestInit,"method"> | fetch() options. |
fetcher ? | (url: URL | RequestInfo, init?: RequestInit) => Promise<Response> | A way to replace fetch() for tests. |
Returns any
.
post(url, opts?, fetcher?)
Syntax sugar for POST
requests by node-fetch
, throwing
ResponseError
on non-2xx response.
Argument | Type | Description |
---|---|---|
url | string | Resource URL. |
opts ? | Omit<RequestInit,"method"> | fetch() options. |
fetcher ? | (url: URL | RequestInfo, init?: RequestInit) => Promise<Response> | A way to replace fetch() for tests. |
Returns any
.
put(url, opts?, fetcher?)
Syntax sugar for PUT
requests by node-fetch
, throwing
ResponseError
on non-2xx response and parsing JSON response.
Argument | Type | Description |
---|---|---|
url | string | Resource URL. |
opts ? | Omit<RequestInit,"method"> | fetch() options. |
fetcher ? | (url: URL | RequestInfo, init?: RequestInit) => Promise<Response> | A way to replace fetch() for tests. |
Returns any
.
request(url, opts?, fetcher?)
Syntax sugar around node-fetch
, which throws ResponseError
on non-2xx response.
Argument | Type | Description |
---|---|---|
url | string | Resource URL. |
opts ? | RequestInit | fetch() options. |
fetcher ? | (url: URL | RequestInfo, init?: RequestInit) => Promise<Response> | A way to replace fetch() for tests. |
Returns any
.