UNPKG

6.38 kBPlain TextView Raw
1import { BYTE } from './byte.js';
2import { IFrame } from './i-frame.js';
3import { StompHeaders } from './stomp-headers.js';
4import { IRawFrameType } from './types.js';
5
6/**
7 * Frame class represents a STOMP frame.
8 *
9 * @internal
10 */
11export class FrameImpl implements IFrame {
12 /**
13 * STOMP Command
14 */
15 public command: string;
16
17 /**
18 * Headers, key value pairs.
19 */
20 public headers: StompHeaders;
21
22 /**
23 * Is this frame binary (based on whether body/binaryBody was passed when creating this frame).
24 */
25 public isBinaryBody: boolean;
26
27 /**
28 * body of the frame
29 */
30 get body(): string {
31 if (!this._body && this.isBinaryBody) {
32 this._body = new TextDecoder().decode(this._binaryBody);
33 }
34 return this._body || '';
35 }
36 private _body: string | undefined;
37
38 /**
39 * body as Uint8Array
40 */
41 get binaryBody(): Uint8Array {
42 if (!this._binaryBody && !this.isBinaryBody) {
43 this._binaryBody = new TextEncoder().encode(this._body);
44 }
45 // At this stage it will definitely have a valid value
46 return this._binaryBody as Uint8Array;
47 }
48 private _binaryBody: Uint8Array | undefined;
49
50 private escapeHeaderValues: boolean;
51 private skipContentLengthHeader: boolean;
52
53 /**
54 * Frame constructor. `command`, `headers` and `body` are available as properties.
55 *
56 * @internal
57 */
58 constructor(params: {
59 command: string;
60 headers?: StompHeaders;
61 body?: string;
62 binaryBody?: Uint8Array;
63 escapeHeaderValues?: boolean;
64 skipContentLengthHeader?: boolean;
65 }) {
66 const {
67 command,
68 headers,
69 body,
70 binaryBody,
71 escapeHeaderValues,
72 skipContentLengthHeader,
73 } = params;
74 this.command = command;
75 this.headers = (Object as any).assign({}, headers || {});
76
77 if (binaryBody) {
78 this._binaryBody = binaryBody;
79 this.isBinaryBody = true;
80 } else {
81 this._body = body || '';
82 this.isBinaryBody = false;
83 }
84 this.escapeHeaderValues = escapeHeaderValues || false;
85 this.skipContentLengthHeader = skipContentLengthHeader || false;
86 }
87
88 /**
89 * deserialize a STOMP Frame from raw data.
90 *
91 * @internal
92 */
93 public static fromRawFrame(
94 rawFrame: IRawFrameType,
95 escapeHeaderValues: boolean
96 ): FrameImpl {
97 const headers: StompHeaders = {};
98 const trim = (str: string): string => str.replace(/^\s+|\s+$/g, '');
99
100 // In case of repeated headers, as per standards, first value need to be used
101 for (const header of rawFrame.headers.reverse()) {
102 const idx = header.indexOf(':');
103
104 const key = trim(header[0]);
105 let value = trim(header[1]);
106
107 if (
108 escapeHeaderValues &&
109 rawFrame.command !== 'CONNECT' &&
110 rawFrame.command !== 'CONNECTED'
111 ) {
112 value = FrameImpl.hdrValueUnEscape(value);
113 }
114
115 headers[key] = value;
116 }
117
118 return new FrameImpl({
119 command: rawFrame.command as string,
120 headers,
121 binaryBody: rawFrame.binaryBody,
122 escapeHeaderValues,
123 });
124 }
125
126 /**
127 * @internal
128 */
129 public toString(): string {
130 return this.serializeCmdAndHeaders();
131 }
132
133 /**
134 * serialize this Frame in a format suitable to be passed to WebSocket.
135 * If the body is string the output will be string.
136 * If the body is binary (i.e. of type Unit8Array) it will be serialized to ArrayBuffer.
137 *
138 * @internal
139 */
140 public serialize(): string | ArrayBuffer {
141 const cmdAndHeaders = this.serializeCmdAndHeaders();
142
143 if (this.isBinaryBody) {
144 return FrameImpl.toUnit8Array(
145 cmdAndHeaders,
146 this._binaryBody as Uint8Array
147 ).buffer;
148 } else {
149 return cmdAndHeaders + this._body + BYTE.NULL;
150 }
151 }
152
153 private serializeCmdAndHeaders(): string {
154 const lines = [this.command];
155 if (this.skipContentLengthHeader) {
156 delete this.headers['content-length'];
157 }
158
159 for (const name of Object.keys(this.headers || {})) {
160 const value = this.headers[name];
161 if (
162 this.escapeHeaderValues &&
163 this.command !== 'CONNECT' &&
164 this.command !== 'CONNECTED'
165 ) {
166 lines.push(`${name}:${FrameImpl.hdrValueEscape(`${value}`)}`);
167 } else {
168 lines.push(`${name}:${value}`);
169 }
170 }
171 if (
172 this.isBinaryBody ||
173 (!this.isBodyEmpty() && !this.skipContentLengthHeader)
174 ) {
175 lines.push(`content-length:${this.bodyLength()}`);
176 }
177 return lines.join(BYTE.LF) + BYTE.LF + BYTE.LF;
178 }
179
180 private isBodyEmpty(): boolean {
181 return this.bodyLength() === 0;
182 }
183
184 private bodyLength(): number {
185 const binaryBody = this.binaryBody;
186 return binaryBody ? binaryBody.length : 0;
187 }
188
189 /**
190 * Compute the size of a UTF-8 string by counting its number of bytes
191 * (and not the number of characters composing the string)
192 */
193 private static sizeOfUTF8(s: string): number {
194 return s ? new TextEncoder().encode(s).length : 0;
195 }
196
197 private static toUnit8Array(
198 cmdAndHeaders: string,
199 binaryBody: Uint8Array
200 ): Uint8Array {
201 const uint8CmdAndHeaders = new TextEncoder().encode(cmdAndHeaders);
202 const nullTerminator = new Uint8Array([0]);
203 const uint8Frame = new Uint8Array(
204 uint8CmdAndHeaders.length + binaryBody.length + nullTerminator.length
205 );
206
207 uint8Frame.set(uint8CmdAndHeaders);
208 uint8Frame.set(binaryBody, uint8CmdAndHeaders.length);
209 uint8Frame.set(
210 nullTerminator,
211 uint8CmdAndHeaders.length + binaryBody.length
212 );
213
214 return uint8Frame;
215 }
216 /**
217 * Serialize a STOMP frame as per STOMP standards, suitable to be sent to the STOMP broker.
218 *
219 * @internal
220 */
221 public static marshall(params: {
222 command: string;
223 headers?: StompHeaders;
224 body?: string;
225 binaryBody?: Uint8Array;
226 escapeHeaderValues?: boolean;
227 skipContentLengthHeader?: boolean;
228 }) {
229 const frame = new FrameImpl(params);
230 return frame.serialize();
231 }
232
233 /**
234 * Escape header values
235 */
236 private static hdrValueEscape(str: string): string {
237 return str
238 .replace(/\\/g, '\\\\')
239 .replace(/\r/g, '\\r')
240 .replace(/\n/g, '\\n')
241 .replace(/:/g, '\\c');
242 }
243
244 /**
245 * UnEscape header values
246 */
247 private static hdrValueUnEscape(str: string): string {
248 return str
249 .replace(/\\r/g, '\r')
250 .replace(/\\n/g, '\n')
251 .replace(/\\c/g, ':')
252 .replace(/\\\\/g, '\\');
253 }
254}
255
\No newline at end of file