@fanoutio/grip
Version:
GRIP Interface Library
169 lines (168 loc) • 5.73 kB
JavaScript
import { jspack } from 'jspack';
import { createWebSocketControlMessage, concatUint8Arrays } from '../../utilities/index.js';
import { WebSocketEvent } from './WebSocketEvent.js';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export class WebSocketContext {
id;
_inEvents;
opening = false;
accepted = false;
closeCode = null;
closed = false;
outCloseCode = null;
outEvents = [];
origMeta;
meta;
prefix;
constructor(id, meta, inEvents, prefix = '') {
this.id = id;
this.meta = JSON.parse(JSON.stringify(meta));
this.origMeta = meta;
this._inEvents = inEvents;
if (inEvents[0]?.type === 'OPEN') {
this.opening = true;
}
this.prefix = prefix;
}
isOpening() {
return this.opening;
}
accept() {
this.accepted = true;
}
close(code = 0) {
this.closed = true;
this.outCloseCode = code;
}
handleUpcomingPingsAndPongs() {
while (true) {
const e = this._inEvents.at(0);
if (e == null ||
!['PING', 'PONG'].includes(e.type)) {
return;
}
this._inEvents.shift();
if (e.type === 'PING') {
this.outEvents.push(new WebSocketEvent('PONG', e.content));
}
}
}
canRecv() {
this.handleUpcomingPingsAndPongs();
return this._inEvents.some(event => ['TEXT', 'BINARY', 'CLOSE', 'DISCONNECT'].includes(event.type));
}
disconnect() {
this.outEvents.push(new WebSocketEvent('DISCONNECT'));
}
recvRaw() {
this.handleUpcomingPingsAndPongs();
let e = undefined;
while (true) {
e = this._inEvents.shift();
if (e == null) {
throw new Error('Read from empty buffer.');
}
if (['TEXT', 'BINARY', 'CLOSE', 'DISCONNECT'].includes(e.type)) {
break;
}
}
const { type } = e;
if (type === 'TEXT') {
if (e.content == null) {
return '';
}
if (typeof e.content === 'string') {
return e.content;
}
return textDecoder.decode(e.content);
}
if (type === 'BINARY') {
if (e.content == null) {
return new Uint8Array();
}
if (typeof e.content === 'string') {
return textEncoder.encode(e.content);
}
return e.content;
}
if (type === 'CLOSE') {
const { content } = e;
if (content instanceof Uint8Array && content.length === 2) {
this.closeCode = jspack.Unpack('>H', [...content])[0];
}
return null;
}
else {
throw new Error('Client disconnected unexpectedly.');
}
}
recv() {
const result = this.recvRaw();
if (result instanceof Uint8Array) {
return textDecoder.decode(result);
}
return result;
}
send(message) {
this.outEvents.push(new WebSocketEvent('TEXT', concatUint8Arrays(textEncoder.encode('m:'), message instanceof Uint8Array ? message : textEncoder.encode(message))));
}
sendBinary(message) {
this.outEvents.push(new WebSocketEvent('BINARY', concatUint8Arrays(textEncoder.encode('m:'), message instanceof Uint8Array ? message : textEncoder.encode(message))));
}
sendControl(message) {
this.outEvents.push(new WebSocketEvent('TEXT', concatUint8Arrays(textEncoder.encode('c:'), message instanceof Uint8Array ? message : textEncoder.encode(message))));
}
subscribe(channel) {
this.sendControl(createWebSocketControlMessage('subscribe', { channel: this.prefix + channel }));
}
unsubscribe(channel) {
this.sendControl(createWebSocketControlMessage('unsubscribe', { channel: this.prefix + channel }));
}
detach() {
this.sendControl(createWebSocketControlMessage('detach'));
}
getOutgoingEvents() {
const events = [];
if (this.accepted) {
events.push(new WebSocketEvent('OPEN'));
}
for (const event of this.outEvents) {
events.push(event);
}
if (this.closed) {
const octets = jspack.Pack('>H', [this.outCloseCode ?? 0]);
if (octets) {
events.push(new WebSocketEvent('CLOSE', new Uint8Array(octets)));
}
}
return events;
}
toHeaders() {
// Find all keys of wsContext.origMeta that don't have the same key
// in wsContext.meta
const metaToRemove = Object.keys(this.origMeta).filter((k) => Object.keys(this.meta).every((nk) => nk.toLowerCase() !== k));
// Find all items in wsContext.meta whose keys and values don't match
// any in wsContext.origMeta
const metaToSet = Object.entries(this.meta).reduce((acc, [nk, nv]) => {
const lname = nk.toLowerCase();
if (Object.entries(this.origMeta).every(([k, v]) => lname !== k || nv !== v)) {
acc[lname] = nv;
}
return acc;
}, {});
const headers = {
'Content-Type': 'application/websocket-events',
};
if (this.accepted) {
headers['Sec-WebSocket-Extensions'] = 'grip';
}
for (const k of metaToRemove) {
headers['Set-Meta-' + k] = '';
}
for (const [k, v] of Object.entries(metaToSet)) {
headers['Set-Meta-' + k] = String(v);
}
return headers;
}
}