|
|
@@ -11,35 +11,156 @@ export interface SSEOptions {
|
|
|
type SSEOpenHandler = ((event?: Event) => void) | null;
|
|
|
type SSEMessageHandler = ((event: MessageEvent<string>) => void) | null;
|
|
|
type SSEErrorHandler = ((error: unknown) => void) | null;
|
|
|
+type ByteArray = Uint8Array<ArrayBufferLike>;
|
|
|
|
|
|
-function createTextDecoder() {
|
|
|
- if (typeof TextDecoder !== "undefined") {
|
|
|
- return new TextDecoder("utf-8");
|
|
|
+type ChunkDecoder = {
|
|
|
+ decode: (data: unknown) => string;
|
|
|
+ flush: () => string;
|
|
|
+};
|
|
|
+
|
|
|
+function toUint8Array(data: unknown): ByteArray | null {
|
|
|
+ if (data instanceof Uint8Array) {
|
|
|
+ return data as ByteArray;
|
|
|
+ }
|
|
|
+ if (data instanceof ArrayBuffer) {
|
|
|
+ return new Uint8Array(data) as ByteArray;
|
|
|
+ }
|
|
|
+ if (ArrayBuffer.isView(data)) {
|
|
|
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) as ByteArray;
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
-function decodeBufferChunk(data: unknown, decoder: TextDecoder | null): string {
|
|
|
- if (typeof data === "string") {
|
|
|
- return data;
|
|
|
- }
|
|
|
+function decodeUtf8Bytes(bytes: ByteArray, previousTail: ByteArray): { text: string; tail: ByteArray } {
|
|
|
+ const merged = previousTail.length
|
|
|
+ ? (() => {
|
|
|
+ const tmp = new Uint8Array(previousTail.length + bytes.length);
|
|
|
+ tmp.set(previousTail, 0);
|
|
|
+ tmp.set(bytes, previousTail.length);
|
|
|
+ return tmp as ByteArray;
|
|
|
+ })()
|
|
|
+ : bytes;
|
|
|
+
|
|
|
+ let text = "";
|
|
|
+ let i = 0;
|
|
|
+
|
|
|
+ while (i < merged.length) {
|
|
|
+ const b1 = merged[i];
|
|
|
+
|
|
|
+ if (b1 <= 0x7f) {
|
|
|
+ text += String.fromCharCode(b1);
|
|
|
+ i += 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- if (data instanceof ArrayBuffer) {
|
|
|
- if (decoder) {
|
|
|
- return decoder.decode(new Uint8Array(data), { stream: true });
|
|
|
+ let needed = 0;
|
|
|
+ if (b1 >= 0xc2 && b1 <= 0xdf) needed = 2;
|
|
|
+ else if (b1 >= 0xe0 && b1 <= 0xef) needed = 3;
|
|
|
+ else if (b1 >= 0xf0 && b1 <= 0xf4) needed = 4;
|
|
|
+ else {
|
|
|
+ text += "\ufffd";
|
|
|
+ i += 1;
|
|
|
+ continue;
|
|
|
}
|
|
|
- return String.fromCharCode(...new Uint8Array(data));
|
|
|
- }
|
|
|
|
|
|
- if (ArrayBuffer.isView(data)) {
|
|
|
- const typedArray = data as Uint8Array;
|
|
|
- if (decoder) {
|
|
|
- return decoder.decode(typedArray, { stream: true });
|
|
|
+ if (i + needed > merged.length) {
|
|
|
+ break;
|
|
|
}
|
|
|
- return String.fromCharCode(...typedArray);
|
|
|
+
|
|
|
+ const b2 = merged[i + 1];
|
|
|
+ if ((b2 & 0xc0) !== 0x80) {
|
|
|
+ text += "\ufffd";
|
|
|
+ i += 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needed === 2) {
|
|
|
+ const codePoint = ((b1 & 0x1f) << 6) | (b2 & 0x3f);
|
|
|
+ text += String.fromCharCode(codePoint);
|
|
|
+ i += 2;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const b3 = merged[i + 2];
|
|
|
+ if ((b3 & 0xc0) !== 0x80) {
|
|
|
+ text += "\ufffd";
|
|
|
+ i += 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needed === 3) {
|
|
|
+ const codePoint = ((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
|
|
|
+ text += String.fromCharCode(codePoint);
|
|
|
+ i += 3;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const b4 = merged[i + 3];
|
|
|
+ if ((b4 & 0xc0) !== 0x80) {
|
|
|
+ text += "\ufffd";
|
|
|
+ i += 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const codePoint =
|
|
|
+ ((b1 & 0x07) << 18) |
|
|
|
+ ((b2 & 0x3f) << 12) |
|
|
|
+ ((b3 & 0x3f) << 6) |
|
|
|
+ (b4 & 0x3f);
|
|
|
+ const cp = codePoint - 0x10000;
|
|
|
+ text += String.fromCharCode((cp >> 10) + 0xd800, (cp & 0x3ff) + 0xdc00);
|
|
|
+ i += 4;
|
|
|
}
|
|
|
|
|
|
- return "";
|
|
|
+ return {
|
|
|
+ text,
|
|
|
+ tail: i < merged.length ? (merged.slice(i) as ByteArray) : (new Uint8Array(0) as ByteArray),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function createChunkDecoder(): ChunkDecoder {
|
|
|
+ if (typeof TextDecoder !== "undefined") {
|
|
|
+ const decoder = new TextDecoder("utf-8");
|
|
|
+ return {
|
|
|
+ decode(data: unknown) {
|
|
|
+ if (typeof data === "string") {
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ const bytes = toUint8Array(data);
|
|
|
+ if (!bytes) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ return decoder.decode(bytes, { stream: true });
|
|
|
+ },
|
|
|
+ flush() {
|
|
|
+ return decoder.decode();
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ let tail: ByteArray = new Uint8Array(0) as ByteArray;
|
|
|
+ return {
|
|
|
+ decode(data: unknown) {
|
|
|
+ if (typeof data === "string") {
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ const bytes = toUint8Array(data);
|
|
|
+ if (!bytes) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ const decoded = decodeUtf8Bytes(bytes, tail);
|
|
|
+ tail = decoded.tail;
|
|
|
+ return decoded.text;
|
|
|
+ },
|
|
|
+ flush() {
|
|
|
+ if (!tail.length) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ const decoded = decodeUtf8Bytes(new Uint8Array(0) as ByteArray, tail);
|
|
|
+ tail = new Uint8Array(0) as ByteArray;
|
|
|
+ return decoded.text;
|
|
|
+ },
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
function parsePayload(payload?: string): string | Record<string, unknown> | ArrayBuffer | undefined {
|
|
|
@@ -182,9 +303,10 @@ export class SSE {
|
|
|
}
|
|
|
|
|
|
private startWithUniRequest() {
|
|
|
- const decoder = createTextDecoder();
|
|
|
+ const chunkDecoder = createChunkDecoder();
|
|
|
const method = this.options.method || "GET";
|
|
|
const payloadData = parsePayload(this.options.payload);
|
|
|
+ let hasReceivedChunk = false;
|
|
|
|
|
|
const requestTask = uni.request({
|
|
|
url: this.url,
|
|
|
@@ -204,8 +326,14 @@ export class SSE {
|
|
|
if (this.closed) {
|
|
|
return;
|
|
|
}
|
|
|
- const text = decodeBufferChunk(res.data, decoder);
|
|
|
- this.consumeSseChunk(text);
|
|
|
+ if (!hasReceivedChunk) {
|
|
|
+ const text = chunkDecoder.decode(res.data);
|
|
|
+ this.consumeSseChunk(text);
|
|
|
+ }
|
|
|
+ const tail = chunkDecoder.flush();
|
|
|
+ if (tail) {
|
|
|
+ this.consumeSseChunk(tail);
|
|
|
+ }
|
|
|
this.flushRemainder();
|
|
|
},
|
|
|
fail: (error) => {
|
|
|
@@ -220,10 +348,11 @@ export class SSE {
|
|
|
};
|
|
|
if (typeof chunkableTask.onChunkReceived === "function") {
|
|
|
chunkableTask.onChunkReceived((result: { data: ArrayBuffer }) => {
|
|
|
+ hasReceivedChunk = true;
|
|
|
if (this.closed) {
|
|
|
return;
|
|
|
}
|
|
|
- const text = decodeBufferChunk(result.data, decoder);
|
|
|
+ const text = chunkDecoder.decode(result.data);
|
|
|
this.consumeSseChunk(text);
|
|
|
});
|
|
|
}
|
|
|
@@ -265,22 +394,20 @@ export class SSE {
|
|
|
}
|
|
|
|
|
|
const reader = response.body.getReader();
|
|
|
- const decoder = createTextDecoder();
|
|
|
+ const chunkDecoder = createChunkDecoder();
|
|
|
|
|
|
while (!this.closed) {
|
|
|
const result = await reader.read();
|
|
|
if (result.done) {
|
|
|
break;
|
|
|
}
|
|
|
- const chunkText = decodeBufferChunk(result.value, decoder);
|
|
|
+ const chunkText = chunkDecoder.decode(result.value);
|
|
|
this.consumeSseChunk(chunkText);
|
|
|
}
|
|
|
|
|
|
- if (decoder) {
|
|
|
- const tail = decoder.decode();
|
|
|
- if (tail) {
|
|
|
- this.consumeSseChunk(tail);
|
|
|
- }
|
|
|
+ const tail = chunkDecoder.flush();
|
|
|
+ if (tail) {
|
|
|
+ this.consumeSseChunk(tail);
|
|
|
}
|
|
|
this.flushRemainder();
|
|
|
} catch (error) {
|