The server is working.
The user just cannot tell.
“Are we done yet?”
Every two seconds. Forever.
That is why we keep using it.
It is also easy to overuse.
Queues can also interact with other real-time options like WebSockets
https://slides.com/elpete/itb-2023-cbq
Server pushes events to the client.
Server pushes events to the client.
Both sides talk over one connection.
One response, sent in chunks.
Section 1
Large responses without waiting for the end.
A normal HTTP response sent in chunks.
BoxLang producer streams audit rows.
GET /api/audit/exportOne JSON object per line.
{"id":1,"action":"LOGIN","user":"eric@example.com"}
{"id":2,"action":"EXPORT","user":"jane@example.com"}
{"id":3,"action":"DELETE","user":"admin@example.com"}event.noRender();
event.setHTTPHeader(
name = "Content-Type", value = "application/x-ndjson; charset=utf-8"
);
event.setHTTPHeader(
name = "Cache-Control", value = "no-cache, no-store, must-revalidate"
);
event.setHTTPHeader(
name = "X-Accel-Buffering", value = "no"
);
getBoxContext().clearBuffer();while ( emitted < maxRows ) {
var page = auditLogStore.getPageAfter(
lastId = lastId,
limit = min( batchSize, maxRows - emitted )
);
if ( !page.len() ) {
break;
}
for ( var row in page ) {
writeOutput( JSONSerialize( row ) & char( 10 ) );
lastId = row.id;
emitted++;
if ( emitted % flushEvery == 0 ) {
bx:flush;
if ( delayMs > 0 ) {
sleep( delayMs );
}
}
}
}
bx:flush;Consume the stream with HTTP callbacks.
GET /api/audit/import
http( exportURL )
.get()
.charset( "utf-8" )
.onChunk( ( chunkNumber, chunk, totalBytes, httpResult, httpClient, response ) => {
var chunkText = extractChunkText( chunk );
consumer.consumeLine( chunkText );
return true;
} )
.send();const response = await fetch( buildURL(), { signal : controller.signal } );
if ( !response.ok || !response.body ) {
throw new Error( `Stream failed with HTTP ${ response.status }` );
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while ( true ) {
const { value, done } = await reader.read();
if ( done ) {
break;
}
buffer += decoder.decode( value, { stream : true } );
const lines = buffer.split( /\r?\n/ );
buffer = lines.pop() || "";
lines.forEach( consumeLine );
}
buffer += decoder.decode();
consumeLine( buffer );Use fetch() and ReadableStream.
Section 2
One-way server push, built for events.
A persistent HTTP connection where the server pushes events.
event: progress
data: {"percent":45}
id: 9BoxLang producer emits job events.
GET /api/jobs/:id/events
SSE(
callback : ( emitter ) => {
for ( var sseEvent in jobEvents ) {
if ( emitter.isClosed() ) {
break;
}
emitter.send( sseEvent.data, sseEvent.event, sseEvent.id );
if ( delayMs > 0 && sseEvent.event != "complete" ) {
sleep( delayMs );
}
}
emitter.close();
},
retry : 3000
);source = new EventSource( buildURL() );source.addEventListener( "open", () => {
connectionCount++;
connectionsTarget.textContent = connectionCount.toLocaleString();
if ( connectionCount > 1 ) {
completed = false;
completionTarget.textContent = "Browser reconnected and started another job stream.";
addEventLog( "reconnected", "", {
jobId : cleanJobId(),
message : "EventSource reopened after the previous stream closed.",
percent : Number.parseInt( percentTarget.textContent, 10 ) || 0
} );
}
setStatus( "Connected" );
} );[ "started", "progress", "warning", "complete" ]
.forEach( ( eventName ) => {
source.addEventListener(
eventName,
( event ) => handleNamedEvent( eventName, event )
);
} );Useful when another service emits SSE.
http( eventsURL )
.get()
.header( "Accept", "text/event-stream" )
.charset( "utf-8" )
.sse( true )
.onChunk( ( sseEvent, lastEventId, httpResult, httpClient, response ) => {
var processedEvent = consumer.consumeEvent( sseEvent, lastEventId );
return true;
} )
.send();Real-time, two-way communication.
Section 3
WebSocket Listener library to be used with CommandBox Websocket and BoxLang WebSocket server.
It's a chat app
...but with global notifications!
// server.json
"websocket": {
"enable": true,
"uri": "/ws",
"listener": "/WebSocket.bx"
}
// public/WebSocket.bx
class extends="modules.socketbox.models.WebSocketCore" {
function onConnect( channel ) {
// called when ws connection is first established
}
function onClose( channel ) {
var member = removeMember( arguments.channel );
if ( member.keyExists( "room" ) ) {
broadcastToRoom(
room = member.room,
payload = {
"type" : "chat-presence",
"action" : "left",
"room" : member.room,
"displayName" : member.displayName,
"message" : "#member.displayName# left #member.room#.",
"timestamp" : now()
}
);
}
}
}class extends="modules.socketbox.models.WebSocketCore" {
function onMessage( required string message, required channel ){
var payload = {};
try {
payload = JSONDeserialize( arguments.message );
} catch ( any e ) {
sendChatError( arguments.channel, "Messages must be JSON." );
return;
}
switch ( payload.type ?: "" ) {
case "chat-join":
handleJoin( arguments.channel, payload );
break;
case "chat-message":
handleChatMessage( arguments.channel, payload );
break;
default:
sendChatError( arguments.channel, "Unknown chat message type." );
}
}
}private void function broadcastToRoom( room, payload ) {
ensureChatState();
if ( !application.socketChatRooms.keyExists( arguments.room ) ) {
return;
}
var message = JSONSerialize( arguments.payload );
for ( var memberId in application.socketChatRooms[ arguments.room ] ) {
if ( application.socketChatMembers.keyExists( memberId ) ) {
sendMessage(
channel = application.socketChatMembers[ memberId ].channel,
message = message
);
}
}
}// SocketNotificationBroadcaster.bx
class extends="modules.socketbox.models.WebSocketCore" {
public struct function broadcast( required string kind ) {
var payload = buildNotification( arguments.kind );
var connections = getAllConnections();
broadcastMessage(
message = JSONSerialize( payload ),
rebroadcast = false
);
return {
"sent" : true,
"connectionCount" : connections.len(),
"notification" : payload
};
}
}
if ( socket ) {
return;
}
socket = new WebSocket( buildURL() );
socket.addEventListener( "open", () => {
setStatus( "Connected" );
} );
socket.addEventListener( "message", ( event ) => {
handleMessage( event.data );
} );
socket.addEventListener( "close", () => {
socket = null;
setStatus( "Closed" );
} );
socket.addEventListener( "error", () => {
setStatus( "Error" );
} );Compare and Contrast