@atproto/common 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/streams.d.ts +10 -2
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +78 -19
- package/dist/streams.js.map +1 -1
- package/package.json +2 -2
- package/src/streams.ts +94 -15
- package/tests/streams.test.ts +29 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @atproto/common
|
|
2
2
|
|
|
3
|
+
## 0.4.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - add streamToNodeBuffer utility to convert Uint8Array (async) iterables to Buffer
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`eb20ff64a`](https://github.com/bluesky-social/atproto/commit/eb20ff64a2d8e3061c652e1e247bf9b0fe3c41a6)]:
|
|
10
|
+
- @atproto/common-web@0.3.1
|
|
11
|
+
|
|
12
|
+
## 0.4.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#2464](https://github.com/bluesky-social/atproto/pull/2464) [`98711a147`](https://github.com/bluesky-social/atproto/commit/98711a147a8674337f605c6368f39fc10c2fae93) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor optimization
|
|
17
|
+
|
|
3
18
|
## 0.4.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/streams.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import {
|
|
3
|
+
import { Duplex, Readable, Stream, Transform, TransformCallback } from 'node:stream';
|
|
4
4
|
export declare const forwardStreamErrors: (...streams: Stream[]) => void;
|
|
5
5
|
export declare const cloneStream: (stream: Readable) => Readable;
|
|
6
6
|
export declare const streamSize: (stream: Readable) => Promise<number>;
|
|
7
|
-
export declare const streamToBytes: (stream:
|
|
7
|
+
export declare const streamToBytes: (stream: AsyncIterable<Uint8Array>) => Promise<Uint8Array>;
|
|
8
|
+
export declare const streamToNodeBuffer: (stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>) => Promise<Buffer>;
|
|
8
9
|
export declare const byteIterableToStream: (iter: AsyncIterable<Uint8Array>) => Readable;
|
|
9
10
|
export declare const bytesToStream: (bytes: Uint8Array) => Readable;
|
|
10
11
|
export declare class MaxSizeChecker extends Transform {
|
|
@@ -14,4 +15,11 @@ export declare class MaxSizeChecker extends Transform {
|
|
|
14
15
|
constructor(maxSize: number, createError: () => Error);
|
|
15
16
|
_transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback): void;
|
|
16
17
|
}
|
|
18
|
+
export declare function decodeStream(stream: Readable, contentEncoding?: string): Readable;
|
|
19
|
+
export declare function decodeStream(stream: AsyncIterable<Uint8Array>, contentEncoding?: string): AsyncIterable<Uint8Array> | Readable;
|
|
20
|
+
/**
|
|
21
|
+
* Create a series of decoding streams based on the content-encoding header. The
|
|
22
|
+
* resulting streams should be piped together to decode the content.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createDecoders(contentEncoding?: string): Duplex[];
|
|
17
25
|
//# sourceMappingURL=streams.d.ts.map
|
package/dist/streams.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":";;AAAA,OAAO,EACL,MAAM,
|
|
1
|
+
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":";;AAAA,OAAO,EACL,MAAM,EAGN,QAAQ,EACR,MAAM,EACN,SAAS,EACT,iBAAiB,EAClB,MAAM,aAAa,CAAA;AAGpB,eAAO,MAAM,mBAAmB,eAAgB,MAAM,EAAE,SAOvD,CAAA;AAED,eAAO,MAAM,WAAW,WAAY,QAAQ,KAAG,QAI9C,CAAA;AAED,eAAO,MAAM,UAAU,WAAkB,QAAQ,KAAG,QAAQ,MAAM,CAMjE,CAAA;AAED,eAAO,MAAM,aAAa,WAAkB,cAAc,UAAU,CAAC,wBAInB,CAAA;AAGlD,eAAO,MAAM,kBAAkB,WACrB,SAAS,UAAU,CAAC,GAAG,cAAc,UAAU,CAAC,KACvD,QAAQ,MAAM,CAYhB,CAAA;AAED,eAAO,MAAM,oBAAoB,SACzB,cAAc,UAAU,CAAC,KAC9B,QAEF,CAAA;AAED,eAAO,MAAM,aAAa,UAAW,UAAU,KAAG,QAKjD,CAAA;AAED,qBAAa,cAAe,SAAQ,SAAS;IAGlC,OAAO,EAAE,MAAM;IACf,WAAW,EAAE,MAAM,KAAK;IAHjC,SAAS,SAAI;gBAEJ,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,KAAK;IAIjC,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,iBAAiB;CAQ1E;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,QAAQ,EAChB,eAAe,CAAC,EAAE,MAAM,GACvB,QAAQ,CAAA;AACX,wBAAgB,YAAY,CAC1B,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,EACjC,eAAe,CAAC,EAAE,MAAM,GACvB,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAA;AAUvC;;;GAGG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAcjE"}
|
package/dist/streams.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MaxSizeChecker = exports.bytesToStream = exports.byteIterableToStream = exports.streamToBytes = exports.streamSize = exports.cloneStream = exports.forwardStreamErrors = void 0;
|
|
4
|
-
const
|
|
3
|
+
exports.createDecoders = exports.decodeStream = exports.MaxSizeChecker = exports.bytesToStream = exports.byteIterableToStream = exports.streamToNodeBuffer = exports.streamToBytes = exports.streamSize = exports.cloneStream = exports.forwardStreamErrors = void 0;
|
|
4
|
+
const node_stream_1 = require("node:stream");
|
|
5
|
+
const node_zlib_1 = require("node:zlib");
|
|
5
6
|
const forwardStreamErrors = (...streams) => {
|
|
6
|
-
for (let i =
|
|
7
|
-
const
|
|
8
|
-
const next = streams[i
|
|
9
|
-
|
|
10
|
-
stream.once('error', (err) => next.emit('error', err));
|
|
11
|
-
}
|
|
7
|
+
for (let i = 1; i < streams.length; ++i) {
|
|
8
|
+
const prev = streams[i - 1];
|
|
9
|
+
const next = streams[i];
|
|
10
|
+
prev.once('error', (err) => next.emit('error', err));
|
|
12
11
|
}
|
|
13
12
|
};
|
|
14
13
|
exports.forwardStreamErrors = forwardStreamErrors;
|
|
15
14
|
const cloneStream = (stream) => {
|
|
16
|
-
const passthrough = new
|
|
15
|
+
const passthrough = new node_stream_1.PassThrough();
|
|
17
16
|
(0, exports.forwardStreamErrors)(stream, passthrough);
|
|
18
17
|
return stream.pipe(passthrough);
|
|
19
18
|
};
|
|
@@ -21,31 +20,45 @@ exports.cloneStream = cloneStream;
|
|
|
21
20
|
const streamSize = async (stream) => {
|
|
22
21
|
let size = 0;
|
|
23
22
|
for await (const chunk of stream) {
|
|
24
|
-
size += chunk
|
|
23
|
+
size += Buffer.byteLength(chunk);
|
|
25
24
|
}
|
|
26
25
|
return size;
|
|
27
26
|
};
|
|
28
27
|
exports.streamSize = streamSize;
|
|
29
|
-
const streamToBytes = async (stream) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const streamToBytes = async (stream) =>
|
|
29
|
+
// @NOTE Though Buffer is a sub-class of Uint8Array, we have observed
|
|
30
|
+
// inconsistencies when using a Buffer in place of Uint8Array. For this
|
|
31
|
+
// reason, we convert the Buffer to a Uint8Array.
|
|
32
|
+
new Uint8Array(await (0, exports.streamToNodeBuffer)(stream));
|
|
33
|
+
exports.streamToBytes = streamToBytes;
|
|
34
|
+
// streamToBuffer identifier name already taken by @atproto/common-web
|
|
35
|
+
const streamToNodeBuffer = async (stream) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let totalLength = 0; // keep track of total length for Buffer.concat
|
|
38
|
+
for await (const chunk of stream) {
|
|
39
|
+
if (chunk instanceof Uint8Array) {
|
|
40
|
+
chunks.push(chunk);
|
|
41
|
+
totalLength += Buffer.byteLength(chunk);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
throw new TypeError('expected Uint8Array');
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
|
-
return
|
|
47
|
+
return Buffer.concat(chunks, totalLength);
|
|
35
48
|
};
|
|
36
|
-
exports.
|
|
49
|
+
exports.streamToNodeBuffer = streamToNodeBuffer;
|
|
37
50
|
const byteIterableToStream = (iter) => {
|
|
38
|
-
return
|
|
51
|
+
return node_stream_1.Readable.from(iter, { objectMode: false });
|
|
39
52
|
};
|
|
40
53
|
exports.byteIterableToStream = byteIterableToStream;
|
|
41
54
|
const bytesToStream = (bytes) => {
|
|
42
|
-
const stream = new
|
|
55
|
+
const stream = new node_stream_1.Readable();
|
|
43
56
|
stream.push(bytes);
|
|
44
57
|
stream.push(null);
|
|
45
58
|
return stream;
|
|
46
59
|
};
|
|
47
60
|
exports.bytesToStream = bytesToStream;
|
|
48
|
-
class MaxSizeChecker extends
|
|
61
|
+
class MaxSizeChecker extends node_stream_1.Transform {
|
|
49
62
|
constructor(maxSize, createError) {
|
|
50
63
|
super();
|
|
51
64
|
Object.defineProperty(this, "maxSize", {
|
|
@@ -78,4 +91,50 @@ class MaxSizeChecker extends stream_1.Transform {
|
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
exports.MaxSizeChecker = MaxSizeChecker;
|
|
94
|
+
function decodeStream(stream, contentEncoding) {
|
|
95
|
+
const decoders = createDecoders(contentEncoding);
|
|
96
|
+
if (decoders.length === 0)
|
|
97
|
+
return stream;
|
|
98
|
+
return (0, node_stream_1.pipeline)([stream, ...decoders], () => { });
|
|
99
|
+
}
|
|
100
|
+
exports.decodeStream = decodeStream;
|
|
101
|
+
/**
|
|
102
|
+
* Create a series of decoding streams based on the content-encoding header. The
|
|
103
|
+
* resulting streams should be piped together to decode the content.
|
|
104
|
+
*/
|
|
105
|
+
function createDecoders(contentEncoding) {
|
|
106
|
+
const decoders = [];
|
|
107
|
+
if (contentEncoding) {
|
|
108
|
+
const encodings = contentEncoding.split(',');
|
|
109
|
+
for (const encoding of encodings) {
|
|
110
|
+
const normalizedEncoding = normalizeEncoding(encoding);
|
|
111
|
+
if (normalizedEncoding === 'identity')
|
|
112
|
+
continue;
|
|
113
|
+
decoders.push(createDecoder(normalizedEncoding));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return decoders.reverse();
|
|
117
|
+
}
|
|
118
|
+
exports.createDecoders = createDecoders;
|
|
119
|
+
function normalizeEncoding(encoding) {
|
|
120
|
+
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
|
|
121
|
+
// > All content-coding values are case-insensitive...
|
|
122
|
+
return encoding.trim().toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
function createDecoder(normalizedEncoding) {
|
|
125
|
+
switch (normalizedEncoding) {
|
|
126
|
+
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
|
|
127
|
+
case 'gzip':
|
|
128
|
+
case 'x-gzip':
|
|
129
|
+
return (0, node_zlib_1.createGunzip)();
|
|
130
|
+
case 'deflate':
|
|
131
|
+
return (0, node_zlib_1.createInflate)();
|
|
132
|
+
case 'br':
|
|
133
|
+
return (0, node_zlib_1.createBrotliDecompress)();
|
|
134
|
+
case 'identity':
|
|
135
|
+
return new node_stream_1.PassThrough();
|
|
136
|
+
default:
|
|
137
|
+
throw new TypeError(`Unsupported content-encoding: "${normalizedEncoding}"`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
81
140
|
//# sourceMappingURL=streams.js.map
|
package/dist/streams.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/common",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared web-platform-friendly code for atproto libraries",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"iso-datestring-validator": "^2.2.2",
|
|
21
21
|
"multiformats": "^9.9.0",
|
|
22
22
|
"pino": "^8.21.0",
|
|
23
|
-
"@atproto/common-web": "^0.3.
|
|
23
|
+
"@atproto/common-web": "^0.3.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"jest": "^28.1.2",
|
package/src/streams.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
Readable,
|
|
2
|
+
Duplex,
|
|
4
3
|
PassThrough,
|
|
4
|
+
pipeline,
|
|
5
|
+
Readable,
|
|
6
|
+
Stream,
|
|
5
7
|
Transform,
|
|
6
8
|
TransformCallback,
|
|
7
|
-
} from 'stream'
|
|
9
|
+
} from 'node:stream'
|
|
10
|
+
import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib'
|
|
8
11
|
|
|
9
12
|
export const forwardStreamErrors = (...streams: Stream[]) => {
|
|
10
|
-
for (let i =
|
|
11
|
-
const
|
|
12
|
-
const next = streams[i
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
13
|
+
for (let i = 1; i < streams.length; ++i) {
|
|
14
|
+
const prev = streams[i - 1]
|
|
15
|
+
const next = streams[i]
|
|
16
|
+
|
|
17
|
+
prev.once('error', (err) => next.emit('error', err))
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -25,17 +27,32 @@ export const cloneStream = (stream: Readable): Readable => {
|
|
|
25
27
|
export const streamSize = async (stream: Readable): Promise<number> => {
|
|
26
28
|
let size = 0
|
|
27
29
|
for await (const chunk of stream) {
|
|
28
|
-
size += chunk
|
|
30
|
+
size += Buffer.byteLength(chunk)
|
|
29
31
|
}
|
|
30
32
|
return size
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export const streamToBytes = async (stream:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
export const streamToBytes = async (stream: AsyncIterable<Uint8Array>) =>
|
|
36
|
+
// @NOTE Though Buffer is a sub-class of Uint8Array, we have observed
|
|
37
|
+
// inconsistencies when using a Buffer in place of Uint8Array. For this
|
|
38
|
+
// reason, we convert the Buffer to a Uint8Array.
|
|
39
|
+
new Uint8Array(await streamToNodeBuffer(stream))
|
|
40
|
+
|
|
41
|
+
// streamToBuffer identifier name already taken by @atproto/common-web
|
|
42
|
+
export const streamToNodeBuffer = async (
|
|
43
|
+
stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
44
|
+
): Promise<Buffer> => {
|
|
45
|
+
const chunks: Uint8Array[] = []
|
|
46
|
+
let totalLength = 0 // keep track of total length for Buffer.concat
|
|
47
|
+
for await (const chunk of stream) {
|
|
48
|
+
if (chunk instanceof Uint8Array) {
|
|
49
|
+
chunks.push(chunk)
|
|
50
|
+
totalLength += Buffer.byteLength(chunk)
|
|
51
|
+
} else {
|
|
52
|
+
throw new TypeError('expected Uint8Array')
|
|
53
|
+
}
|
|
37
54
|
}
|
|
38
|
-
return
|
|
55
|
+
return Buffer.concat(chunks, totalLength)
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
export const byteIterableToStream = (
|
|
@@ -68,3 +85,65 @@ export class MaxSizeChecker extends Transform {
|
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
}
|
|
88
|
+
|
|
89
|
+
export function decodeStream(
|
|
90
|
+
stream: Readable,
|
|
91
|
+
contentEncoding?: string,
|
|
92
|
+
): Readable
|
|
93
|
+
export function decodeStream(
|
|
94
|
+
stream: AsyncIterable<Uint8Array>,
|
|
95
|
+
contentEncoding?: string,
|
|
96
|
+
): AsyncIterable<Uint8Array> | Readable
|
|
97
|
+
export function decodeStream(
|
|
98
|
+
stream: Readable | AsyncIterable<Uint8Array>,
|
|
99
|
+
contentEncoding?: string,
|
|
100
|
+
): Readable | AsyncIterable<Uint8Array> {
|
|
101
|
+
const decoders = createDecoders(contentEncoding)
|
|
102
|
+
if (decoders.length === 0) return stream
|
|
103
|
+
return pipeline([stream as Readable, ...decoders], () => {}) as Duplex
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a series of decoding streams based on the content-encoding header. The
|
|
108
|
+
* resulting streams should be piped together to decode the content.
|
|
109
|
+
*/
|
|
110
|
+
export function createDecoders(contentEncoding?: string): Duplex[] {
|
|
111
|
+
const decoders: Duplex[] = []
|
|
112
|
+
|
|
113
|
+
if (contentEncoding) {
|
|
114
|
+
const encodings = contentEncoding.split(',')
|
|
115
|
+
for (const encoding of encodings) {
|
|
116
|
+
const normalizedEncoding = normalizeEncoding(encoding)
|
|
117
|
+
if (normalizedEncoding === 'identity') continue
|
|
118
|
+
|
|
119
|
+
decoders.push(createDecoder(normalizedEncoding))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return decoders.reverse()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeEncoding(encoding: string) {
|
|
127
|
+
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
|
|
128
|
+
// > All content-coding values are case-insensitive...
|
|
129
|
+
return encoding.trim().toLowerCase()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createDecoder(normalizedEncoding: string): Duplex {
|
|
133
|
+
switch (normalizedEncoding) {
|
|
134
|
+
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
|
|
135
|
+
case 'gzip':
|
|
136
|
+
case 'x-gzip':
|
|
137
|
+
return createGunzip()
|
|
138
|
+
case 'deflate':
|
|
139
|
+
return createInflate()
|
|
140
|
+
case 'br':
|
|
141
|
+
return createBrotliDecompress()
|
|
142
|
+
case 'identity':
|
|
143
|
+
return new PassThrough()
|
|
144
|
+
default:
|
|
145
|
+
throw new TypeError(
|
|
146
|
+
`Unsupported content-encoding: "${normalizedEncoding}"`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
package/tests/streams.test.ts
CHANGED
|
@@ -61,14 +61,41 @@ describe('streams', () => {
|
|
|
61
61
|
})
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
describe('
|
|
64
|
+
describe('streamToNodeBuffer', () => {
|
|
65
65
|
it('converts stream to byte array', async () => {
|
|
66
66
|
const stream = Readable.from(Buffer.from('foo'))
|
|
67
|
-
const bytes = await streams.
|
|
67
|
+
const bytes = await streams.streamToNodeBuffer(stream)
|
|
68
68
|
|
|
69
69
|
expect(bytes[0]).toBe('f'.charCodeAt(0))
|
|
70
70
|
expect(bytes[1]).toBe('o'.charCodeAt(0))
|
|
71
71
|
expect(bytes[2]).toBe('o'.charCodeAt(0))
|
|
72
|
+
expect(bytes.length).toBe(3)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('converts async iterable to byte array', async () => {
|
|
76
|
+
const iterable = (async function* () {
|
|
77
|
+
yield Buffer.from('b')
|
|
78
|
+
yield Buffer.from('a')
|
|
79
|
+
yield new Uint8Array(['r'.charCodeAt(0)])
|
|
80
|
+
})()
|
|
81
|
+
const bytes = await streams.streamToNodeBuffer(iterable)
|
|
82
|
+
|
|
83
|
+
expect(bytes[0]).toBe('b'.charCodeAt(0))
|
|
84
|
+
expect(bytes[1]).toBe('a'.charCodeAt(0))
|
|
85
|
+
expect(bytes[2]).toBe('r'.charCodeAt(0))
|
|
86
|
+
expect(bytes.length).toBe(3)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('throws error for non Uint8Array chunks', async () => {
|
|
90
|
+
const iterable: AsyncIterable<any> = (async function* () {
|
|
91
|
+
yield Buffer.from('b')
|
|
92
|
+
yield Buffer.from('a')
|
|
93
|
+
yield 'r'
|
|
94
|
+
})()
|
|
95
|
+
|
|
96
|
+
await expect(streams.streamToNodeBuffer(iterable)).rejects.toThrow(
|
|
97
|
+
'expected Uint8Array',
|
|
98
|
+
)
|
|
72
99
|
})
|
|
73
100
|
})
|
|
74
101
|
|