๐Ÿ“ฆ socketio / engine.io-parser

๐Ÿ“„ index.ts ยท 157 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157import { encodePacket, encodePacketToBinary } from "./encodePacket.js";
import { decodePacket } from "./decodePacket.js";
import {
  Packet,
  PacketType,
  RawData,
  BinaryType,
  ERROR_PACKET
} from "./commons.js";

const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text

const encodePayload = (
  packets: Packet[],
  callback: (encodedPayload: string) => void
) => {
  // some packets may be added to the array while encoding, so the initial length must be saved
  const length = packets.length;
  const encodedPackets = new Array(length);
  let count = 0;

  packets.forEach((packet, i) => {
    // force base64 encoding for binary packets
    encodePacket(packet, false, encodedPacket => {
      encodedPackets[i] = encodedPacket;
      if (++count === length) {
        callback(encodedPackets.join(SEPARATOR));
      }
    });
  });
};

const decodePayload = (
  encodedPayload: string,
  binaryType?: BinaryType
): Packet[] => {
  const encodedPackets = encodedPayload.split(SEPARATOR);
  const packets = [];
  for (let i = 0; i < encodedPackets.length; i++) {
    const decodedPacket = decodePacket(encodedPackets[i], binaryType);
    packets.push(decodedPacket);
    if (decodedPacket.type === "error") {
      break;
    }
  }
  return packets;
};

const HEADER_LENGTH = 4;

export function createPacketEncoderStream() {
  return new TransformStream({
    transform(packet: Packet, controller) {
      encodePacketToBinary(packet, encodedPacket => {
        const header = new Uint8Array(HEADER_LENGTH);
        // last 31 bits indicate the length of the payload
        new DataView(header.buffer).setUint32(0, encodedPacket.length);
        // first bit indicates whether the payload is plain text (0) or binary (1)
        if (packet.data && typeof packet.data !== "string") {
          header[0] |= 0x80;
        }
        controller.enqueue(header);
        controller.enqueue(encodedPacket);
      });
    }
  });
}

let TEXT_DECODER;

function totalLength(chunks: Uint8Array[]) {
  return chunks.reduce((acc, chunk) => acc + chunk.length, 0);
}

function concatChunks(chunks: Uint8Array[], size: number) {
  if (chunks[0].length === size) {
    return chunks.shift();
  }
  const buffer = new Uint8Array(size);
  let j = 0;
  for (let i = 0; i < size; i++) {
    buffer[i] = chunks[0][j++];
    if (j === chunks[0].length) {
      chunks.shift();
      j = 0;
    }
  }
  if (chunks.length && j < chunks[0].length) {
    chunks[0] = chunks[0].slice(j);
  }
  return buffer;
}

export function createPacketDecoderStream(
  maxPayload: number,
  binaryType: BinaryType
) {
  if (!TEXT_DECODER) {
    TEXT_DECODER = new TextDecoder();
  }
  const chunks: Uint8Array[] = [];
  let expectedSize = -1;
  let isBinary = false;

  return new TransformStream({
    transform(chunk: Uint8Array, controller) {
      chunks.push(chunk);
      while (true) {
        const expectHeader = expectedSize === -1;
        if (expectHeader) {
          if (totalLength(chunks) < HEADER_LENGTH) {
            break;
          }
          const headerArray = concatChunks(chunks, HEADER_LENGTH);
          const header = new DataView(
            headerArray.buffer,
            headerArray.byteOffset,
            headerArray.length
          ).getUint32(0);

          isBinary = header >> 31 === -1;
          expectedSize = header & 0x7fffffff;

          if (expectedSize === 0 || expectedSize > maxPayload) {
            controller.enqueue(ERROR_PACKET);
            break;
          }
        } else {
          if (totalLength(chunks) < expectedSize) {
            break;
          }
          const data = concatChunks(chunks, expectedSize);
          controller.enqueue(
            decodePacket(
              isBinary ? data : TEXT_DECODER.decode(data),
              binaryType
            )
          );
          expectedSize = -1;
        }
      }
    }
  });
}

export const protocol = 4;
export {
  encodePacket,
  encodePayload,
  decodePacket,
  decodePayload,
  Packet,
  PacketType,
  RawData,
  BinaryType
};