@stomp/stompjs
Version:
STOMP client for Javascript and Typescript
255 lines (227 loc) • 6.38 kB
text/typescript
import { BYTE } from './byte.js';
import { IFrame } from './i-frame.js';
import { StompHeaders } from './stomp-headers.js';
import { IRawFrameType } from './types.js';
/**
* Frame class represents a STOMP frame.
*
* @internal
*/
export class FrameImpl implements IFrame {
/**
* STOMP Command
*/
public command: string;
/**
* Headers, key value pairs.
*/
public headers: StompHeaders;
/**
* Is this frame binary (based on whether body/binaryBody was passed when creating this frame).
*/
public isBinaryBody: boolean;
/**
* body of the frame
*/
get body(): string {
if (!this._body && this.isBinaryBody) {
this._body = new TextDecoder().decode(this._binaryBody);
}
return this._body || '';
}
private _body: string | undefined;
/**
* body as Uint8Array
*/
get binaryBody(): Uint8Array {
if (!this._binaryBody && !this.isBinaryBody) {
this._binaryBody = new TextEncoder().encode(this._body);
}
// At this stage it will definitely have a valid value
return this._binaryBody as Uint8Array;
}
private _binaryBody: Uint8Array | undefined;
private escapeHeaderValues: boolean;
private skipContentLengthHeader: boolean;
/**
* Frame constructor. `command`, `headers` and `body` are available as properties.
*
* @internal
*/
constructor(params: {
command: string;
headers?: StompHeaders;
body?: string;
binaryBody?: Uint8Array;
escapeHeaderValues?: boolean;
skipContentLengthHeader?: boolean;
}) {
const {
command,
headers,
body,
binaryBody,
escapeHeaderValues,
skipContentLengthHeader,
} = params;
this.command = command;
this.headers = (Object as any).assign({}, headers || {});
if (binaryBody) {
this._binaryBody = binaryBody;
this.isBinaryBody = true;
} else {
this._body = body || '';
this.isBinaryBody = false;
}
this.escapeHeaderValues = escapeHeaderValues || false;
this.skipContentLengthHeader = skipContentLengthHeader || false;
}
/**
* deserialize a STOMP Frame from raw data.
*
* @internal
*/
public static fromRawFrame(
rawFrame: IRawFrameType,
escapeHeaderValues: boolean
): FrameImpl {
const headers: StompHeaders = {};
const trim = (str: string): string => str.replace(/^\s+|\s+$/g, '');
// In case of repeated headers, as per standards, first value need to be used
for (const header of rawFrame.headers.reverse()) {
const idx = header.indexOf(':');
const key = trim(header[0]);
let value = trim(header[1]);
if (
escapeHeaderValues &&
rawFrame.command !== 'CONNECT' &&
rawFrame.command !== 'CONNECTED'
) {
value = FrameImpl.hdrValueUnEscape(value);
}
headers[key] = value;
}
return new FrameImpl({
command: rawFrame.command as string,
headers,
binaryBody: rawFrame.binaryBody,
escapeHeaderValues,
});
}
/**
* @internal
*/
public toString(): string {
return this.serializeCmdAndHeaders();
}
/**
* serialize this Frame in a format suitable to be passed to WebSocket.
* If the body is string the output will be string.
* If the body is binary (i.e. of type Unit8Array) it will be serialized to ArrayBuffer.
*
* @internal
*/
public serialize(): string | ArrayBuffer {
const cmdAndHeaders = this.serializeCmdAndHeaders();
if (this.isBinaryBody) {
return FrameImpl.toUnit8Array(
cmdAndHeaders,
this._binaryBody as Uint8Array
).buffer;
} else {
return cmdAndHeaders + this._body + BYTE.NULL;
}
}
private serializeCmdAndHeaders(): string {
const lines = [this.command];
if (this.skipContentLengthHeader) {
delete this.headers['content-length'];
}
for (const name of Object.keys(this.headers || {})) {
const value = this.headers[name];
if (
this.escapeHeaderValues &&
this.command !== 'CONNECT' &&
this.command !== 'CONNECTED'
) {
lines.push(`${name}:${FrameImpl.hdrValueEscape(`${value}`)}`);
} else {
lines.push(`${name}:${value}`);
}
}
if (
this.isBinaryBody ||
(!this.isBodyEmpty() && !this.skipContentLengthHeader)
) {
lines.push(`content-length:${this.bodyLength()}`);
}
return lines.join(BYTE.LF) + BYTE.LF + BYTE.LF;
}
private isBodyEmpty(): boolean {
return this.bodyLength() === 0;
}
private bodyLength(): number {
const binaryBody = this.binaryBody;
return binaryBody ? binaryBody.length : 0;
}
/**
* Compute the size of a UTF-8 string by counting its number of bytes
* (and not the number of characters composing the string)
*/
private static sizeOfUTF8(s: string): number {
return s ? new TextEncoder().encode(s).length : 0;
}
private static toUnit8Array(
cmdAndHeaders: string,
binaryBody: Uint8Array
): Uint8Array {
const uint8CmdAndHeaders = new TextEncoder().encode(cmdAndHeaders);
const nullTerminator = new Uint8Array([0]);
const uint8Frame = new Uint8Array(
uint8CmdAndHeaders.length + binaryBody.length + nullTerminator.length
);
uint8Frame.set(uint8CmdAndHeaders);
uint8Frame.set(binaryBody, uint8CmdAndHeaders.length);
uint8Frame.set(
nullTerminator,
uint8CmdAndHeaders.length + binaryBody.length
);
return uint8Frame;
}
/**
* Serialize a STOMP frame as per STOMP standards, suitable to be sent to the STOMP broker.
*
* @internal
*/
public static marshall(params: {
command: string;
headers?: StompHeaders;
body?: string;
binaryBody?: Uint8Array;
escapeHeaderValues?: boolean;
skipContentLengthHeader?: boolean;
}) {
const frame = new FrameImpl(params);
return frame.serialize();
}
/**
* Escape header values
*/
private static hdrValueEscape(str: string): string {
return str
.replace(/\\/g, '\\\\')
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/:/g, '\\c');
}
/**
* UnEscape header values
*/
private static hdrValueUnEscape(str: string): string {
return str
.replace(/\\r/g, '\r')
.replace(/\\n/g, '\n')
.replace(/\\c/g, ':')
.replace(/\\\\/g, '\\');
}
}