Emitting
Once you have successfully built a registration flow for your users (see the Registering guides if you have not), you are able to emit to those registered users anywhere in your application (yes, including within background jobs!).
PsychicAppWebsocketsWs.emit() boots a PsychicAppWebsockets instance on its first call, via PsychicAppWebsockets.getOrFail(). Each Node process has its own module cache — the websocket app instance is not shared across processes. If any process skips PsychicAppWebsockets.init() (e.g. via a service-role guard that is too narrow), the call throws:
must call `cachePsychicAppWebsockets` before loading cached psychic application websockets
BullMQ marks the job failed and retries — the error looks like a framework cache problem rather than an application configuration problem. By default the generated initializer runs in all processes; if a role guard was added that excludes 'worker', that's the usual cause. See Config for details and the optional role guard.
The initializer isn't the problem here, and neither is the client transport. The likely cause is cross-process delivery: an emit from a web or worker process only reaches a socket held by the websocket-server process through the Redis adapter. A process emitting on the in-process adapter (the test default, or a misconfigured non-test process) fans out only within its own process, so the browser's socket never receives it. See Adapter in Config.
To establish a new Ws instance, we need to provide it with a set of routes that the application allows, like so:
const wsRoutes = ['/users/ping', 'users/alert', 'users/info'] as const
ws = new Ws(wsRoutes)
For your convenience, we recommend that you set up a simple helper function in your application to wrap this, as well as to establish a singleton, so as to prevent multiple instances from needing to establish independent connections:
// app/helpers/ws.ts
import { Ws } from '@rvoh/psychic-websockets'
export const WS_ROUTES = ['/ops/connection-success'] as const
const ws = new Ws(WS_ROUTES)
export default ws
Once this has been established, you can now emit to your users with any of the provided routes:
await ws.emit(user, '/users/ping', { hello: 'world' })
Emitting from a background job
Since Ws.emit() works from any process that initializes PsychicAppWebsockets, a common pattern is a backgrounded service that sends a websocket notification once a job completes, then triggers it from a model lifecycle hook:
export class NotificationService extends ApplicationBackgroundedService {
public static async placeBooked(placeId: string, guestId: string) {
await this.background('_placeBooked', placeId, guestId)
}
public static async _placeBooked(placeId: string, guestId: string) {
const place = await Place.findOrFail(placeId)
const hosts = await place.associationQuery('hosts').all()
const ws = new Ws(['/notifications/booking'] as const)
for (const host of hosts) {
await ws.emit(host.userId, '/notifications/booking', {
placeName: place.name,
guestId,
})
}
}
}
// Triggered from a model hook
@deco.AfterCreateCommit()
public async notifyBooking(this: Booking) {
await NotificationService.placeBooked(this.placeId, this.guestId)
}