fetch-event-source源码解析

1,678 阅读8分钟

讲一讲@microsoft/fetch-event-source源码是怎么处理流式数据的

问题驱动我们看源码的动力,在此,我们去看看改插件解决的最主要的两个问题,支持post获取的数据稳定。 主要的实现代码在 fetch.tsparse.ts,后者是工具函数的集成。

实质就是fetch发送了请求。

至此,最重要的三个函数出现了。

getBytes
export async function getBytes(
  stream: ReadableStream<Uint8Array>, // 可读取的二进制数据流
  onChunk: (arr: Uint8Array) => void
) {
  const reader = stream.getReader();
  let result: ReadableStreamDefaultReadResult<Uint8Array>;
  while (!(result = await reader.read()).done) { // 持续接收
    onChunk(result.value);
  }
}
getLines
export function getLines(
  onLine: (line: Uint8Array, fieldLength: number) => void
) {
  let buffer: Uint8Array | undefined;
  let position: number; // 当前读取位置
  let fieldLength: number; // 行中 `field` 部分的长度
  let discardTrailingNewline = false;

  // 返回一个可以处理每个传入字节块的函数:
  return function onChunk(arr: Uint8Array) {
    if (buffer === undefined) {
      buffer = arr;
      position = 0;
      fieldLength = -1;
    } else {
      // 我们仍在解析旧行。将新字节附加到缓冲区中:
      buffer = concat(buffer, arr);
    }

    const bufLength = buffer.length;
    let lineStart = 0; // 当前行的起始索引
    while (position < bufLength) {
      if (discardTrailingNewline) {
        if (buffer[position] === ControlChars.NewLine) {
          lineStart = ++position; // 跳过到下一个字符
        }

        discardTrailingNewline = false;
      }

      // 开始向前查找直到行尾:
      let lineEnd = -1; // \r 或 \n 字符的索引
      for (; position < bufLength && lineEnd === -1; ++position) {
        switch (buffer[position]) {
          case ControlChars.Colon:
            if (fieldLength === -1) {
              // 行中的第一个冒号
              fieldLength = position - lineStart;
            }
            break;
            // @ts-ignore:7029 \r 情况应当直接进入 \n:
          case ControlChars.CarriageReturn:
            discardTrailingNewline = true;
          case ControlChars.NewLine:
            lineEnd = position;
            break;
        }
      }

      if (lineEnd === -1) {
        // 我们到达了缓冲区的末尾,但行尚未结束。
        // 等待下一个 arr 然后继续解析:
        break;
      }

      // 我们已到达行尾,输出它:
      onLine(buffer.subarray(lineStart, lineEnd), fieldLength);
      lineStart = position; // 我们现在在下一行
      fieldLength = -1;
    }

    if (lineStart === bufLength) {
      buffer = undefined; // 我们已完成读取
    } else if (lineStart !== 0) {
      // 创建一个新的视图到缓冲区,从 lineStart 开始,
      // 这样当我们获得新 arr 时不需要复制之前的行:
      buffer = buffer.subarray(lineStart);
      position -= lineStart;
    }
  };
}
  • 这个函数的作用是逐个解析传入的字节块,以找到数据中的行结束符,并在每找到一个完整的行时调用 onLine 函数 。

测试用例可观全景:

    describe('getLines', () => {
        it('single line', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual('id: abc');
                expect(fieldLength).toEqual(2);
            });

            // act:
            next(encoder.encode('id: abc\n'));
            
            // assert:
            expect(lineNum).toBe(1);
        });

        it('multiple lines', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
                expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
            });
            
            // act:
            next(encoder.encode('id: abc\n'));
            next(encoder.encode('data: def\n'));
            
            // assert:
            expect(lineNum).toBe(2);
        });

        it('single line split across multiple arrays', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual('id: abc');
                expect(fieldLength).toEqual(2);
            });

            // act:
            next(encoder.encode('id: a'));
            next(encoder.encode('bc\n'));
            
            // assert:
            expect(lineNum).toBe(1);
        });

        it('multiple lines split across multiple arrays', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
                expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
            });
            
            // act:
            next(encoder.encode('id: ab'));
            next(encoder.encode('c\nda'));
            next(encoder.encode('ta: def\n'));
            
            // assert:
            expect(lineNum).toBe(2);
        });

        it('new line', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual('');
                expect(fieldLength).toEqual(-1);
            });

            // act:
            next(encoder.encode('\n'));
            
            // assert:
            expect(lineNum).toBe(1);
        });

        it('comment line', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual(': this is a comment');
                expect(fieldLength).toEqual(0);
            });

            // act:
            next(encoder.encode(': this is a comment\n'));
            
            // assert:
            expect(lineNum).toBe(1);
        });

        it('line with no field', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual('this is an invalid line');
                expect(fieldLength).toEqual(-1);
            });

            // act:
            next(encoder.encode('this is an invalid line\n'));
            
            // assert:
            expect(lineNum).toBe(1);
        });

        it('line with multiple colons', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual('id: abc: def');
                expect(fieldLength).toEqual(2);
            });

            // act:
            next(encoder.encode('id: abc: def\n'));
            
            // assert:
            expect(lineNum).toBe(1);
        });

        it('single byte array with multiple lines separated by \\n', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
                expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
            });

            // act:
            next(encoder.encode('id: abc\ndata: def\n'));
            
            // assert:
            expect(lineNum).toBe(2);
        });

        it('single byte array with multiple lines separated by \\r', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
                expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
            });

            // act:
            next(encoder.encode('id: abc\rdata: def\r'));
            
            // assert:
            expect(lineNum).toBe(2);
        });

        it('single byte array with multiple lines separated by \\r\\n', () => {
            // arrange:
            let lineNum = 0;
            const next = parse.getLines((line, fieldLength) => {
                ++lineNum;
                expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
                expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
            });

            // act:
            next(encoder.encode('id: abc\r\ndata: def\r\n'));
            
            // assert:
            expect(lineNum).toBe(2);
        });
    });
  1. single line :
    • 测试最基本的单行数据处理
    • 验证能正确解析 "id: abc" 格式的数据
    • 确认字段长度(fieldLength)计算正确
  2. multiple lines :
    • 测试多行数据的处理
    • 验证能正确处理连续的两行不同数据
    • 确认每行的字段长度计算正确
  3. single line split across multiple arrays :
    • 测试跨数据块的单行处理
    • 验证当一行数据被分割成多个数据块时能正确组合
    • 例如:"id: a" 和 "bc\n" 能被正确组合成 "id: abc"
  4. multiple lines split across multiple arrays :
    • 测试跨数据块的多行处理
    • 验证复杂场景:数据既跨行又跨数据块
    • 确保行的拼接和分割都正确
  5. new line :
    • 测试空行处理
    • 验证对单个换行符的处理
    • 确认空行的 fieldLength 为 -1
  6. comment line :
    • 测试注释行处理(以冒号开头的行)
    • 验证注释行的 fieldLength 为 0
    • 确保注释内容被正确保留
  7. line with no field :
    • 测试没有字段分隔符(冒号)的行
    • 验证这种情况下 fieldLength 为 -1
    • 确保原始内容被保留
  8. line with multiple colons :
    • 测试包含多个冒号的行
    • 验证只有第一个冒号被用作字段分隔符
    • 确认 fieldLength 计算正确
  9. 换行符处理系列测试 :
    • 测试不同换行符的处理:
      • \n (Unix 风格)
      • \r (旧 Mac 风格)
      • \r\n (Windows 风格)
    • 确保所有换行符格式都能正确分割行

这些测试用例全面覆盖了 getLines 函数的各种使用场景,包括:

  • 基本功能
  • 边界情况
  • 错误处理
  • 跨平台兼容性
  • 数据分块处理
getMessages
export function getMessages(
  onId: (id: string) => void,
  onRetry: (retry: number) => void,
  onMessage?: (msg: EventSourceMessage) => void
) {
  let message = newMessage();
  const decoder = new TextDecoder();

  // 返回一个可以处理每个传入行缓冲区的函数:
  return function onLine(line: Uint8Array, fieldLength: number) {
    if (line.length === 0) {
      // 空行表示消息的结束。触发回调并开始新消息:
      onMessage?.(message);
      message = newMessage();
    } else if (fieldLength > 0) {
      // 排除注释和没有值的行
      // 行的格式为 "<field>:<value>" 或 "<field>: <value>"
      // 参考:https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
      const field = decoder.decode(line.subarray(0, fieldLength));
      const valueOffset =
        fieldLength + (line[fieldLength + 1] === ControlChars.Space ? 2 : 1);
      const value = decoder.decode(line.subarray(valueOffset));

      switch (field) {
        case "data":
          // 如果此消息已经有数据,则将新值附加到旧数据后面。
          // 否则,只需设置为新值:
          message.data = message.data ? message.data + "\n" + value : value;
          break;
        case "event":
          message.event = value;
          break;
        case "id":
          onId((message.id = value));
          break;
        case "retry":
          const retry = parseInt(value, 10);
          if (!isNaN(retry)) {
            // 根据规范,忽略非整数
            onRetry((message.retry = retry));
          }
          break;
      }
    }
  };
}
  • 通过处理传入的行数据,解析出消息的 data、event、id retry 字段,并通过回调函数处理这些字段。当遇到空行时,表示一条消息结束,并调用onMessage回调。

测试用例:

    describe('getMessages', () => {
        it('happy path', () => {
            // arrange:
            let msgNum = 0;
            const next = parse.getMessages(id => {
                expect(id).toEqual('abc');
            }, retry => {
                expect(retry).toEqual(42);
            }, msg => {
                ++msgNum;
                expect(msg).toEqual({
                    retry: 42,
                    id: 'abc',
                    event: 'def',
                    data: 'ghi'
                });
            });

            // act:
            next(encoder.encode('retry: 42'), 5);
            next(encoder.encode('id: abc'), 2);
            next(encoder.encode('event:def'), 5);
            next(encoder.encode('data:ghi'), 4);
            next(encoder.encode(''), -1);

            // assert:
            expect(msgNum).toBe(1);
        });

        it('skip unknown fields', () => {
            let msgNum = 0;
            const next = parse.getMessages(id => {
                expect(id).toEqual('abc');
            }, _retry => {
                fail('retry should not be called');
            }, msg => {
                ++msgNum;
                expect(msg).toEqual({
                    id: 'abc',
                    data: '',
                    event: '',
                    retry: undefined,
                });
            });

            // act:
            next(encoder.encode('id: abc'), 2);
            next(encoder.encode('foo: null'), 3);
            next(encoder.encode(''), -1);

            // assert:
            expect(msgNum).toBe(1);
        });
        
        it('ignore non-integer retry', () => {
            let msgNum = 0;
            const next = parse.getMessages(_id => {
                fail('id should not be called');
            }, _retry => {
                fail('retry should not be called');
            }, msg => {
                ++msgNum;
                expect(msg).toEqual({
                    id: '',
                    data: '',
                    event: '',
                    retry: undefined,
                });
            });

            // act:
            next(encoder.encode('retry: def'), 5);
            next(encoder.encode(''), -1);

            // assert:
            expect(msgNum).toBe(1);
        });

        it('skip comment-only messages', () => {
            // arrange:
            let msgNum = 0;
            const next = parse.getMessages(id => {
                expect(id).toEqual('123');
            }, _retry => {
                fail('retry should not be called');
            }, msg => {
                ++msgNum;
                expect(msg).toEqual({
                    retry: undefined,
                    id: '123',
                    event: 'foo ',
                    data: '',
                });
            });

            // act:
            next(encoder.encode('id:123'), 2);
            next(encoder.encode(':'), 0);
            next(encoder.encode(':    '), 0);
            next(encoder.encode('event: foo '), 5);
            next(encoder.encode(''), -1);

            // assert:
            expect(msgNum).toBe(1);
        });

        it('should append data split across multiple lines', () => {
            // arrange:
            let msgNum = 0;
            const next = parse.getMessages(_id => {
                fail('id should not be called');
            }, _retry => {
                fail('retry should not be called');
            }, msg => {
                ++msgNum;
                expect(msg).toEqual({
                    data: 'YHOO\n+2\n\n10',
                    id: '',
                    event: '',
                    retry: undefined,
                });
            });

            // act:
            next(encoder.encode('data:YHOO'), 4);
            next(encoder.encode('data: +2'), 4);
            next(encoder.encode('data'), 4);
            next(encoder.encode('data: 10'), 4);
            next(encoder.encode(''), -1);

            // assert:
            expect(msgNum).toBe(1);
        });

        it('should reset id if sent multiple times', () => {
            // arrange:
            const expectedIds = ['foo', ''];
            let idsIdx = 0;
            let msgNum = 0;
            const next = parse.getMessages(id => {
                expect(id).toEqual(expectedIds[idsIdx]);
                ++idsIdx;
            }, _retry => {
                fail('retry should not be called');
            }, msg => {
                ++msgNum;
                expect(msg).toEqual({
                    data: '',
                    id: '',
                    event: '',
                    retry: undefined,
                });
            });

            // act:
            next(encoder.encode('id: foo'), 2);
            next(encoder.encode('id'), 2);
            next(encoder.encode(''), -1);

            // assert:
            expect(idsIdx).toBe(2);
            expect(msgNum).toBe(1);
        });
    });
  1. happy path :
    • 测试正常流程
    • 验证完整的消息处理:包含 retry、id、event 和 data 字段
    • 确保所有回调都被正确调用
    • 验证最终消息对象的完整性
  2. skip unknown fields :
    • 测试处理未知字段的情况
    • 验证未知字段(如 'foo')被正确忽略
    • 确保只有已知字段被处理
    • 确保 retry 回调不会被错误触发
  3. ignore non-integer retry :
    • 测试非数字的 retry 值处理
    • 验证非整数 retry 值被忽略
    • 确保相关回调不会被触发
    • 验证消息对象保持默认状态
  4. skip comment-only messages :
    • 测试注释行的处理
    • 验证注释行(以冒号开头)被正确跳过
    • 确保其他有效字段正常处理
    • 验证最终消息的正确性
  5. append data split across multiple lines :
    • 测试多行数据的拼接
    • 验证多个 data 字段被正确合并
    • 确保换行符被正确处理
    • 验证最终数据的格式正确性
  6. reset id if sent multiple times :
    • 测试多次发送 id 的情况
    • 验证 id 的重置机制
    • 确保 id 回调被正确触发
    • 验证最终消息状态的正确性

这些测试用例覆盖了 getMessages 的主要功能点:

  • 基本消息解析
  • 字段验证和处理
  • 错误处理
  • 特殊情况处理
  • 数据拼接
  • 状态重置

简单总结:

  • 这三个函数实际上是实现了基于fetch封装的处理 SSE(Server-Send Events),处理流式的数据,进行接收分段、解析、组装