text-line-transform-stream.ts 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
  2. // This module is browser compatible.
  3. // Modified by Sukka (https://skk.moe) to increase compatibility and performance with Bun.
  4. import { TransformStream } from 'node:stream/web';
  5. interface TextLineStreamOptions {
  6. /** Allow splitting by solo \r */
  7. allowCR?: boolean
  8. }
  9. /** Transform a stream into a stream where each chunk is divided by a newline,
  10. * be it `\n` or `\r\n`. `\r` can be enabled via the `allowCR` option.
  11. *
  12. * ```ts
  13. * const res = await fetch('https://example.com');
  14. * const lines = res.body!
  15. * .pipeThrough(new TextDecoderStream())
  16. * .pipeThrough(new TextLineStream());
  17. * ```
  18. */
  19. export class TextLineStream extends TransformStream<string, string> {
  20. // private __buf = '';
  21. constructor({
  22. allowCR = false
  23. }: TextLineStreamOptions = {}) {
  24. let __buf = '';
  25. let chunkIndex = 0;
  26. super({
  27. transform(chunk, controller) {
  28. chunk = __buf + chunk;
  29. chunkIndex = 0;
  30. for (; ;) {
  31. const lfIndex = chunk.indexOf('\n', chunkIndex);
  32. if (allowCR) {
  33. const crIndex = chunk.indexOf('\r', chunkIndex);
  34. if (
  35. crIndex !== -1 && crIndex !== (chunk.length - 1)
  36. && (lfIndex === -1 || (lfIndex - 1) > crIndex)
  37. ) {
  38. controller.enqueue(chunk.slice(chunkIndex, crIndex));
  39. chunkIndex = crIndex + 1;
  40. continue;
  41. }
  42. }
  43. if (lfIndex === -1) {
  44. // we can no longer find a line break in the chunk, break the current loop
  45. break;
  46. }
  47. // enqueue current line, and loop again to find next line
  48. let crOrLfIndex = lfIndex;
  49. if (chunk[lfIndex - 1] === '\r') {
  50. crOrLfIndex--;
  51. }
  52. controller.enqueue(chunk.slice(chunkIndex, crOrLfIndex));
  53. chunkIndex = lfIndex + 1;
  54. continue;
  55. }
  56. __buf = chunk.slice(chunkIndex);
  57. },
  58. flush(controller) {
  59. if (__buf.length > 0) {
  60. // eslint-disable-next-line sukka/string/prefer-string-starts-ends-with -- performance
  61. if (allowCR && __buf[__buf.length - 1] === '\r') {
  62. controller.enqueue(__buf.slice(0, -1));
  63. } else {
  64. controller.enqueue(__buf);
  65. }
  66. }
  67. }
  68. });
  69. }
  70. }