Client guide
Installation
npm install @jcbuisson/express-x-client socket.io-clientExample
import { io } from 'socket.io-client'
import { createClient } from '@jcbuisson/express-x-client'
const socket = io('http://localhost:8000', { transports: ['websocket'] })
const app = createClient(socket)
const user = await app.service('user').findUnique({ where: { id: 22 } })
app.service('user').on('create', (user) => {
console.log('CREATE USER event', user)
})
...Configuration
createClient(socket, options)
Options passed to createClient:
| Option | Type | Default | Description |
|---|---|---|---|
debug | boolean | false | Log socket events and service requests to the console |
Socket.io connection options (path, reconnection delays, extra headers, etc.) are passed directly to io():
const socket = io('http://localhost:8000', {
path: '/myapp-socket.io/',
transports: ['websocket'],
reconnectionDelay: 1000,
reconnectionDelayMax: 10000,
extraHeaders: {
'bearer-token': 'mytoken'
},
})Service options
Options can be passed per-service to app.service(name, options):
timeout (integer)
An exception is thrown if a service request doesn't get an answer after timeout ms. Default: 20000
app.service('mail', { timeout: 10000 }).send(...)volatile (boolean)
When volatile is set, a service request is not buffered if the connection is down — it will simply not be emitted. Default: false
Connection lifecycle
// called when socket connects or reconnects
app.addConnectListener((socket) => { ... })
app.removeConnectListener(func)
// called when socket disconnects
app.addDisconnectListener((socket) => { ... })
app.removeDisconnectListener(func)
// called on connection error
app.addErrorListener((socket) => { ... })
app.removeErrorListener(func)Handling exceptions
When an exception occurs during a service method execution on the server, the error is serialized and sent back to the client so that it can be caught:
try {
const { userId } = await app.service('auth').login(email, password)
...
} catch(err) {
if (err.code === 'wrong-credentials') {
...
}
}Use EXError on the server to attach a structured code to errors.
Publish-subscribe
Service events callbacks
When a service event is sent by the server, the client can set a callback. In the following example, the callback is executed when the client receives a 'create' event from the 'user' service. Its only argument is the data returned by the method called — here the created user.
app.service('user').on('create', (createdUser) => {
...
})Application event callbacks
When an application event is sent by the server, the client can set a callback with app.on(). In the following example, the callback is executed whenever the application-wide event 'expireAt' is sent to the client:
app.on('expireAt', (date) => {
...
})Reload plugin
reloadPlugin uses a short-lived, one-time transfer token to preserve approved socket rooms and transferable socket.data across page reloads. On reconnect, it emits a cnx-transfer message to the server to copy the previous socket's state onto the new connection.
The server-side reloadPlugin must also be configured.
import { createClient, reloadPlugin } from '@jcbuisson/express-x-client'
const socket = io('http://localhost:8000', { transports: ['websocket'] })
const app = createClient(socket)
await reloadPlugin(app)reloadPlugin uses sessionStorage internally to track the previous socket ID and transfer token. Token listeners and acknowledgement listeners are bounded and cleaned up across reconnects.
Room restoration is denied by default on the server. See the server-side reloadPlugin for authorization configuration.
Offline plugin
offlinePlugin enables offline-first CRUD: reads and writes work even with no network, and data is automatically synchronised with the server on reconnect.
See the offline guide for full setup and usage.
import { createClient, offlinePlugin } from '@jcbuisson/express-x-client'
const socket = io('http://localhost:8000', { transports: ['websocket'] })
const app = createClient(socket)
offlinePlugin(app, {
// Stable, non-secret identity boundary for IndexedDB.
cacheNamespace: `tenant:${tenantId}:user:${userId}`,
})
const post = app.createOfflineModel('post', ['title', 'content'])
// create a record (written locally immediately, synced to server when online)
await post.create({ title: 'Hello', content: 'World' })
// reactive observable — emits on every local change
post.getObservable({ userId: 42 }).subscribe(posts => {
console.log('posts', posts)
})cacheNamespace is required. Never share a namespace between authenticated users or tenants, and do not use an access token as the namespace. Filters passed to findWhere, getObservable, or synchronization methods must contain JSON-safe values.
app.service(name).call(action, ...args) is available for remote method names that collide with local proxy methods, such as toString or constructor.