npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

async-iterable-split

v3.0.1

Published

Split an async iterable (value is Uint8Array) into multiple "sub async iterable(iterator)"s by size, line or needle.

Downloads

54

Readme

async-iterable-split

English 中文文档


Split an async iterable (value is Uint8Array) into multiple "sub async iterable(iterator)"s by line, size or needle.

Installation

npm i async-iterable-split

Quick Start

1. Split by line

Suppose we have a foo.txt file with the following contents:

this is the first line
hello world
this is the last line
import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const iterable = createReadStream("./foo.txt"); // Node.js readable stream is async iterable (since v10.0.0)
const splitable = new Splitable(iterable); // Any object that deploys the [Symbol.asyncIterator] interface can be used as a parameter for instantiating Splitable

while (await splitable.hasValue()) {
  const subIterator = splitable.splitLine(); // Create a "sub async iterable(iterator)", all the data iterated from this object is one row of the original
  const chunks = [];
  for await (const chunk of subIterator) {
    chunks.push(chunk);
  }
  // When the "for await of" loop exits, all the data of a row will be iterated out
  console.dir(Buffer.concat(chunks).toString("utf-8"));
}

The output looks like this:

'this is the first line'
'hello world'
'this is the last line'

You can also use the readLine method to get a whole line at once:

import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));
while (await splitable.hasValue()) {
  const line = await splitable.readLine(); // Type of "line" is Uint8Array
  console.dir(Buffer.from(line).toString("utf-8"));
}

Line break can be either LF or CRLF.

2. Split by size

Suppose we have a foo.txt file with the following contents:

abcdefghijklmnopqrstuvwxyz
import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));

while (await splitable.hasValue()) {
  const subIterator = splitable.splitSize(10); // Create a "sub async iterable(iterator)", all the data iterated from this object is 10 bytes of the original
  const chunks = [];
  for await (const chunk of subIterator) {
    chunks.push(chunk);
  }
  console.dir(Buffer.concat(chunks).toString("utf-8"));
}

The output looks like this:

'abcdefghij'
'klmnopqrst'
'uvwxyz'

You can also use the readSize method to get fixed-size data at once:

import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));
while (await splitable.hasValue()) {
  const part = await splitable.readSize(10);
  console.dir(Buffer.from(part).toString("utf-8"));
}

3. Split by needle

Suppose we have a foo.txt file with the following contents:

foobarbaz
import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));

while (await splitable.hasValue()) {
  const subIterator = splitable.splitBeforeNeedle(Buffer.from("ba")); // Create a "sub async iterable(iterator)", all the data iterated from this object is data before "ba" of the original
  const chunks = [];
  for await (const chunk of subIterator) {
    chunks.push(chunk);
  }
  console.dir(Buffer.concat(chunks).toString("utf-8"));
}

The output looks like this:

'foo'
'r'
'z'

You can also use the readBeforeNeedle method to get all the data before needle at once:

import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));
while (await splitable.hasValue()) {
  const part = await splitable.readBeforeNeedle(Buffer.from("ba"));
  console.dir(Buffer.from(part).toString("utf-8"));
}

4. Cross-use

The above three splitting methods can be used alone or cross-used. The following is an example of parsing an HTTP request message, there are parts split by line and parts split by size:

Server:

import { createServer } from "node:net";
import { Splitable } from "async-iterable-split";

const server = createServer(async (socket) => {
  try {
    // Node.js TCP socket is also async iterable
    const splitable = new Splitable(socket);
    while (await splitable.hasValue()) {
      // httpReqParser will parses one HTTP request message at a time, one TCP connection may contain multiple HTTP messages
      console.log(await httpReqParser(splitable));
    }
  } catch (error) {
    console.log("got error", error);
    socket.destroy(error);
  }
}).listen(8888, "127.0.0.1", () => {
  console.log(server.address());
});

// This is just a simple HTTP request message parser, a production-ready parser needs more consideration.
async function httpReqParser(splitable) {
  const reqMsg = {};

  // Parse the request line, limiting the request line size to 65535 bytes
  const reqLine = Buffer.from(await splitable.readLine(65536)).toString("ascii");
  const reqInfos = reqLine.split(" ");
  reqMsg.method = reqInfos.shift();
  reqMsg.uri = reqInfos.shift();
  reqMsg.httpVersion = reqInfos.shift().replace("HTTP/", "");

  // Parse the request headers, limit the size of a single request header to 16384 bytes, and limit the count of request headers to 256
  reqMsg.headers = {};
  let headerCount = 0;
  while (await splitable.hasValue()) {
    if (headerCount > 256) {
      throw new Error("header count exceeded limit");
    }
    const header = Buffer.from(await splitable.readLine(16384)).toString("ascii");
    // If a blank line is encountered, it means the end of the request headers, jump out of the while loop
    if (header.length === 0) {
      break;
    }
    headerCount++;
    const [key, value] = header.split(":");
    reqMsg.headers[key.toLowerCase()] = value.trim();
  }

  // Parse the request body and limit the size of the request body to 1MB
  const bodySize = Number(reqMsg.headers["content-length"]) || 0;
  if (bodySize > 2 ** 20) {
    throw new Error("body size exceeded limit");
  }
  reqMsg.body = Buffer.from(await splitable.readSize(bodySize)).toString("utf-8");

  // return parsing result
  return reqMsg;
}

Client:

import { connect } from "node:net";

// Establish a TCP connection, and after successful establishment, send two consecutive HTTP request messages
const socket = connect({ host: "127.0.0.1", port: 8888 }, () => {
  socket.write(`GET / HTTP/1.1
Host: 127.0.0.1

`);

  socket.write(`POST /ping HTTP/1.1
Host: 127.0.0.1
Content-Type: text/plain; charset=utf-8
Content-Length: 8

👋ping`);
});

The server output looks like this:

{
  method: 'GET',
  uri: '/',
  httpVersion: '1.1',
  headers: { host: '127.0.0.1' },
  body: ''
}
{
  method: 'POST',
  uri: '/ping',
  httpVersion: '1.1',
  headers: {
    host: '127.0.0.1',
    'content-type': 'text/plain; charset=utf-8',
    'content-length': '8'
  },
  body: '👋ping'
}

Cautions

  1. The "sub async iterable(iterator)" created by splitXXX methods cannot be iterated at the same time. You must wait for the previous round of iteration to end before starting a new round of iteration.

  2. When calling readXXX methods, the corresponding "sub async iterable(iterator)" will be automatically created and iterated immediately. The iterated data will be temporarily stored in memory. When the iteration ends, All data chunks will be concated into one data chunk and resolve out. It is recommended to set a reasonable size limit to avoid memory leaks.

  3. splitable.hasValue() is used to check if any data can be iterated out,this method is asynchronous. It cannot be called again while it is in pending state, or a "sub async iterable(iterator)" is iterating.

  4. After a "sub async iterable(iterator)" is done, if you want to get the remaining original data, you can directly iterate the splitable, it's also an async iterable(iterator).


将一个“异步可迭代对象”(值为 Uint8Array)按行、大小或 needle 拆分成多个“子异步可迭代对象(迭代器)”。

安装

npm i async-iterable-split

快速开始

1. 按行拆分

假设我们有一个 foo.txt 文件,文件内容如下所示:

this is the first line
hello world
this is the last line
import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const iterable = createReadStream("./foo.txt"); // Node.js 的可读流是异步可迭代对象(从 v10.0.0 开始)
const splitable = new Splitable(iterable); // 任何部署了 [Symbol.asyncIterator] 接口的对象,都可以作为实例化 Splitable 的参数

while (await splitable.hasValue()) {
  const subIterator = splitable.splitLine(); // 创建一个“子异步可迭代对象(迭代器)”,该对象迭代出的全部数据是原始数据中的一行
  const chunks = [];
  for await (const chunk of subIterator) {
    chunks.push(chunk);
  }
  // 当 for await of 循环退出时,一行的数据就全部迭代出来了
  console.dir(Buffer.concat(chunks).toString("utf-8"));
}

输出如下所示:

'this is the first line'
'hello world'
'this is the last line'

也可以使用 readLine 方法一次性获取一整行的数据:

import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));
while (await splitable.hasValue()) {
  const line = await splitable.readLine(); // "line" 的类型是 Uint8Array
  console.dir(Buffer.from(line).toString("utf-8"));
}

换行符可以是 LF 也可以是 CRLF

2. 按大小拆分

假设我们有一个 foo.txt 文件,文件内容如下所示:

abcdefghijklmnopqrstuvwxyz
import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));

while (await splitable.hasValue()) {
  const subIterator = splitable.splitSize(10); // 创建一个子异步可迭代对象(迭代器),该对象迭代出的全部数据是原始数据中的 10 个字节
  const chunks = [];
  for await (const chunk of subIterator) {
    chunks.push(chunk);
  }
  console.dir(Buffer.concat(chunks).toString("utf-8"));
}

输出如下所示:

'abcdefghij'
'klmnopqrst'
'uvwxyz'

也可以使用 readSize 方法一次性获取固定大小的数据:

import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));
while (await splitable.hasValue()) {
  const part = await splitable.readSize(10);
  console.dir(Buffer.from(part).toString("utf-8"));
}

3. 按 needle 拆分

假设我们有一个 foo.txt 文件,文件内容如下所示:

foobarbaz
import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));

while (await splitable.hasValue()) {
  const subIterator = splitable.splitBeforeNeedle(Buffer.from("ba")); // 创建一个子异步可迭代对象(迭代器),该对象迭代出的数据是原始数据中 "ba" 前面的部分
  const chunks = [];
  for await (const chunk of subIterator) {
    chunks.push(chunk);
  }
  console.dir(Buffer.concat(chunks).toString("utf-8"));
}

输出如下所示:

'foo'
'r'
'z'

也可以使用 readBeforeNeedle 方法一次性获取 needle 前面的所有数据:

import { createReadStream } from "node:fs";
import { Splitable } from "async-iterable-split";

const splitable = new Splitable(createReadStream("./foo.txt"));
while (await splitable.hasValue()) {
  const part = await splitable.readBeforeNeedle(Buffer.from("ba"));
  console.dir(Buffer.from(part).toString("utf-8"));
}

4. 交叉使用

以上三种拆分方式即可以单独使用,也可以交叉使用,下面是一个解析 HTTP 请求报文的示例,即有按行拆分的部分,也有按大小拆分的部分:

服务端

import { createServer } from "node:net";
import { Splitable } from "async-iterable-split";

const server = createServer(async (socket) => {
  try {
    // Node.js 中的 TCP socket 也是异步可迭代对象
    const splitable = new Splitable(socket);
    while (await splitable.hasValue()) {
      // httpReqParser 每次解析一个 HTTP 请求报文,一个 TCP 连接可能包含多个 HTTP 报文
      console.log(await httpReqParser(splitable));
    }
  } catch (error) {
    console.log("got error", error);
    socket.destroy(error);
  }
}).listen(8888, "127.0.0.1", () => {
  console.log(server.address());
});

// 这只是一个简单的 HTTP 请求报文解析器,一个可用于生产环境的解析器要考虑更多。
async function httpReqParser(splitable) {
  const reqMsg = {};

  // 解析请求行,将请求行大小限制在 65535 个字节以内
  const reqLine = Buffer.from(await splitable.readLine(65536)).toString("ascii");
  const reqInfos = reqLine.split(" ");
  reqMsg.method = reqInfos.shift();
  reqMsg.uri = reqInfos.shift();
  reqMsg.httpVersion = reqInfos.shift().replace("HTTP/", "");

  // 解析请求头,将单个请求头大小限制在 16384 个字节以内,请求头个数限制在 256 个以内
  reqMsg.headers = {};
  let headerCount = 0;
  while (await splitable.hasValue()) {
    if (headerCount > 256) {
      throw new Error("header count exceeded limit");
    }
    const header = Buffer.from(await splitable.readLine(16384)).toString("ascii");
    // 如果遇到了空行代表请求头部分结束了,跳出 while 循环
    if (header.length === 0) {
      break;
    }
    headerCount++;
    const [key, value] = header.split(":");
    reqMsg.headers[key.toLowerCase()] = value.trim();
  }

  // 解析请求体,将请求体大小限制在 1MB 以内
  const bodySize = Number(reqMsg.headers["content-length"]) || 0;
  if (bodySize > 2 ** 20) {
    throw new Error("body size exceeded limit");
  }
  reqMsg.body = Buffer.from(await splitable.readSize(bodySize)).toString("utf-8");

  // 返回解析结果
  return reqMsg;
}

客户端

import { connect } from "node:net";

// 建立 TCP 连接,并在建立成功后,连续发送两条 HTTP 请求报文
const socket = connect({ host: "127.0.0.1", port: 8888 }, () => {
  socket.write(`GET / HTTP/1.1
Host: 127.0.0.1

`);

  socket.write(`POST /ping HTTP/1.1
Host: 127.0.0.1
Content-Type: text/plain; charset=utf-8
Content-Length: 8

👋ping`);
});

服务端的输出如下所示:

{
  method: 'GET',
  uri: '/',
  httpVersion: '1.1',
  headers: { host: '127.0.0.1' },
  body: ''
}
{
  method: 'POST',
  uri: '/ping',
  httpVersion: '1.1',
  headers: {
    host: '127.0.0.1',
    'content-type': 'text/plain; charset=utf-8',
    'content-length': '8'
  },
  body: '👋ping'
}

注意事项

  1. 通过 splitXXX 方法创建的“子异步可迭代对象(迭代器)”不能被同时迭代,必须等前一轮迭代结束了,才能开启新的一轮迭代。

  2. 调用 readXXX 方法时会自动创建对应的“子异步可迭代对象(迭代器)”并立即对其进行迭代,迭代出的数据会暂存在内存中,当迭代结束时,会将所有数据块合并成一个完整的数据块 resolve 出来。建议使用这些方法时设置一个合理的大小限制,避免内存泄漏。

  3. splitable.hasValue() 用于判断是否还可以迭代出数据,该方法是异步的。当它处于 pending 状态时不可再次调用,亦不可迭代“子异步可迭代对象(迭代器)”。

  4. 当迭代完某个“子异步可迭代对象(迭代器)”后,如果想获取剩余的原始数据,可以直接迭代 splitable,它也是一个异步可迭代对象(迭代器)。