@atproto/common 0.7.6 → 0.8.0

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @atproto/common
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `fanOut`, a pipeline-destination counterpart to `Tee` that owns its output sinks: an output that errors or ends early is dropped while the others (and the input) keep flowing, and the input is only torn down once every sink has died. Also harden `Tee` so a branch that dies mid-write (e.g. one created with `autoDestroy: false`, which errors without emitting `close`) can no longer stall the main stream.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate `forwardStreamErrors`
12
+
13
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Deprecate `cloneStream`
14
+
15
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `createError` argument of `MaxSizeChecker` optional
16
+
17
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Migrate test runner to vitest
18
+
19
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `Tee` stream utility
20
+
21
+ - [#5382](https://github.com/bluesky-social/atproto/pull/5382) [`898c469`](https://github.com/bluesky-social/atproto/commit/898c469cf568b941f38f9c3371997a513ac05cfa) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `HashPassThrough` utility
22
+
3
23
  ## 0.7.6
4
24
 
5
25
  ### Patch Changes
package/dist/streams.d.ts CHANGED
@@ -1,6 +1,70 @@
1
- import { type Duplex, Readable, type Stream, Transform, type TransformCallback } from 'node:stream';
1
+ import { type HashOptions } from 'node:crypto';
2
+ import { type Duplex, Readable, type Stream, Transform, type TransformCallback, Writable } from 'node:stream';
3
+ /**
4
+ * @deprecated use native stream piping instead.
5
+ */
2
6
  export declare const forwardStreamErrors: (...streams: Stream[]) => void;
7
+ /**
8
+ * @deprecated This function has several flaws:
9
+ * - It only works if all the stream are setup during the same tick
10
+ * - It does not apply any backpressure to the source stream, which can lead to
11
+ * memory issues
12
+ * Use {@link Tee} instead, which is a more robust implementation of the same
13
+ * concept.
14
+ */
3
15
  export declare const cloneStream: (stream: Readable) => Readable;
16
+ /**
17
+ * A {@link Transform} that forwards every chunk downstream while mirroring a
18
+ * copy into a "branch" {@link Writable} (exposed through {@link Tee.branch}, e.g.
19
+ * to be cached). The tee is paced by the slower of its two consumers, bounding
20
+ * how much data gets buffered. The branch is completed when the source ends,
21
+ * and torn down if the tee errors or is destroyed early (e.g. the client
22
+ * disconnected).
23
+ *
24
+ * Consuming the branch is best-effort: its failures are swallowed here and must
25
+ * never break the main stream.
26
+ *
27
+ * Because the tee is a {@link Transform}, its *main* output is a downstream
28
+ * pipeline stage — so `pipeline([readable, tee, main])` tears the tee (and the
29
+ * source) down as soon as `main` fails. If you need the branch to keep going
30
+ * after the main consumer disappears (or vice-versa), use {@link fanOut}, which
31
+ * owns both outputs instead of exposing one as a pipeline stage.
32
+ */
33
+ export declare class Tee extends Transform {
34
+ #private;
35
+ readonly branch: Writable;
36
+ constructor(branch: Writable | ((readable: Readable) => void));
37
+ get branchWritable(): boolean;
38
+ _transform(chunk: unknown, _enc: BufferEncoding, cb: TransformCallback): void;
39
+ _flush(cb: TransformCallback): void;
40
+ _destroy(err: Error | null, cb: (err?: Error | null) => void): void;
41
+ }
42
+ /**
43
+ * A {@link Writable} that mirrors everything written to it into several
44
+ * independent "sinks", pacing the input by the slowest sink that is still
45
+ * alive. Unlike {@link Tee}, the sinks are *owned* by the fan-out rather than
46
+ * being downstream pipeline stages, which lets the two error domains be kept
47
+ * apart:
48
+ *
49
+ * - An *output* (sink) that errors or ends early is simply dropped; the other
50
+ * sinks — and the input — keep going. Its error is swallowed (best-effort),
51
+ * exactly like a {@link Tee} branch.
52
+ * - The *input* (writable side) is only torn down once *every* sink has died,
53
+ * or if the input itself fails — in which case the failure is propagated to
54
+ * every surviving sink.
55
+ *
56
+ * Designed to be the destination of a pipeline:
57
+ *
58
+ * ```
59
+ * pipeline([readable, ...transforms, fanOut(main, branch)])
60
+ * ```
61
+ *
62
+ * If `main` disconnects (e.g. an HTTP response whose client went away), `branch`
63
+ * still receives the full stream (e.g. to finish populating a cache), and
64
+ * vice-versa. A sink may also be provided as a function receiving a
65
+ * {@link Readable} to consume, mirroring {@link Tee}'s constructor.
66
+ */
67
+ export declare function fanOut(...outputs: ReadonlyArray<Writable | ((readable: Readable) => void)>): Writable;
4
68
  export declare const streamSize: (stream: Readable) => Promise<number>;
5
69
  export declare const streamToBytes: (stream: AsyncIterable<Uint8Array>) => Promise<Uint8Array<ArrayBuffer>>;
6
70
  export declare const streamToNodeBuffer: (stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>) => Promise<Buffer>;
@@ -17,8 +81,16 @@ export declare class MaxSizeChecker extends Transform {
17
81
  maxSize: number;
18
82
  createError: () => Error;
19
83
  totalSize: number;
20
- constructor(maxSize: number, createError: () => Error);
84
+ constructor(maxSize: number, createError?: () => Error);
85
+ _transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback): void;
86
+ }
87
+ export declare class HashPassThrough extends Transform {
88
+ #private;
89
+ private readonly hash;
90
+ constructor(algorithm: string, options?: HashOptions);
21
91
  _transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback): void;
92
+ _flush(cb: TransformCallback): void;
93
+ get digest(): Buffer<ArrayBuffer>;
22
94
  }
23
95
  export declare function decodeStream(stream: Readable, contentEncoding?: string | string[]): Readable;
24
96
  export declare function decodeStream(stream: AsyncIterable<Uint8Array>, contentEncoding?: string | string[]): AsyncIterable<Uint8Array> | Readable;
@@ -1 +1 @@
1
- {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,QAAQ,EACR,KAAK,MAAM,EACX,SAAS,EACT,KAAK,iBAAiB,EAEvB,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,OAAO,CAAC,MAAM,CAMjE,CAAA;AAED,eAAO,MAAM,aAAa,WAAkB,aAAa,CAAC,UAAU,CAAC,qCAInB,CAAA;AAGlD,eAAO,MAAM,kBAAkB,WACrB,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,KACvD,OAAO,CAAC,MAAM,CAYhB,CAAA;AAED,eAAO,MAAM,oBAAoB,SACzB,aAAa,CAAC,UAAU,CAAC,KAC9B,QAEF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,WACrB,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,gBAC1C,MAAM,KACnB,QAkEF,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;IACb,YACS,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,KAAK,EAGhC;IACD,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,iBAAiB,QAOxE;CACF;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,QAAQ,EAChB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAClC,QAAQ,CAAA;AACX,wBAAgB,YAAY,CAC1B,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,EACjC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAClC,aAAa,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAA;AAUvC;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAqB5E"}
1
+ {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"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"}
package/dist/streams.js CHANGED
@@ -1,5 +1,9 @@
1
- import { PassThrough, Readable, Transform, pipeline, } from 'node:stream';
1
+ import { createHash } from 'node:crypto';
2
+ import { PassThrough, Readable, Transform, Writable, pipeline, } from 'node:stream';
2
3
  import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
4
+ /**
5
+ * @deprecated use native stream piping instead.
6
+ */
3
7
  export const forwardStreamErrors = (...streams) => {
4
8
  for (let i = 1; i < streams.length; ++i) {
5
9
  const prev = streams[i - 1];
@@ -7,11 +11,194 @@ export const forwardStreamErrors = (...streams) => {
7
11
  prev.once('error', (err) => next.emit('error', err));
8
12
  }
9
13
  };
14
+ /**
15
+ * @deprecated This function has several flaws:
16
+ * - It only works if all the stream are setup during the same tick
17
+ * - It does not apply any backpressure to the source stream, which can lead to
18
+ * memory issues
19
+ * Use {@link Tee} instead, which is a more robust implementation of the same
20
+ * concept.
21
+ */
10
22
  export const cloneStream = (stream) => {
11
23
  const passthrough = new PassThrough();
12
24
  forwardStreamErrors(stream, passthrough);
13
25
  return stream.pipe(passthrough);
14
26
  };
27
+ /**
28
+ * A {@link Transform} that forwards every chunk downstream while mirroring a
29
+ * copy into a "branch" {@link Writable} (exposed through {@link Tee.branch}, e.g.
30
+ * to be cached). The tee is paced by the slower of its two consumers, bounding
31
+ * how much data gets buffered. The branch is completed when the source ends,
32
+ * and torn down if the tee errors or is destroyed early (e.g. the client
33
+ * disconnected).
34
+ *
35
+ * Consuming the branch is best-effort: its failures are swallowed here and must
36
+ * never break the main stream.
37
+ *
38
+ * Because the tee is a {@link Transform}, its *main* output is a downstream
39
+ * pipeline stage — so `pipeline([readable, tee, main])` tears the tee (and the
40
+ * source) down as soon as `main` fails. If you need the branch to keep going
41
+ * after the main consumer disappears (or vice-versa), use {@link fanOut}, which
42
+ * owns both outputs instead of exposing one as a pipeline stage.
43
+ */
44
+ export class Tee extends Transform {
45
+ #branchFailed = false;
46
+ constructor(branch) {
47
+ super();
48
+ if (typeof branch === 'function') {
49
+ const passthrough = new PassThrough({ autoDestroy: true });
50
+ branch(passthrough);
51
+ this.branch = passthrough;
52
+ }
53
+ else {
54
+ this.branch = branch;
55
+ }
56
+ // The branch is best-effort: its failures must never break or stall the
57
+ // main stream.
58
+ this.branch.on('error', () => {
59
+ this.#branchFailed = true;
60
+ });
61
+ }
62
+ get branchWritable() {
63
+ return !this.#branchFailed && this.branch.writable;
64
+ }
65
+ _transform(chunk, _enc, cb) {
66
+ // Forward downstream, applying backpressure if either branch is slower.
67
+ if (!this.branchWritable || this.branch.write(chunk)) {
68
+ cb(null, chunk);
69
+ }
70
+ else {
71
+ const done = () => {
72
+ this.branch.off('drain', done);
73
+ this.branch.off('close', done);
74
+ this.branch.off('error', done);
75
+ cb(null, chunk);
76
+ };
77
+ // Resume on 'drain' (branch caught up) but also on 'close'/'error' so the
78
+ // tee is never stuck if the branch dies mid-write — e.g. a branch created
79
+ // with `autoDestroy: false`, which errors without emitting 'close'.
80
+ this.branch.once('drain', done);
81
+ this.branch.once('close', done);
82
+ this.branch.once('error', done);
83
+ }
84
+ }
85
+ _flush(cb) {
86
+ if (this.branchWritable)
87
+ this.branch.end();
88
+ cb();
89
+ }
90
+ _destroy(err, cb) {
91
+ // A failed branch has already torn itself down (or given up); leave it be
92
+ // rather than emitting a second, spurious error on it.
93
+ if (this.branchWritable) {
94
+ this.branch.destroy(err ?? new Error('Tee destroyed'));
95
+ }
96
+ cb(err);
97
+ }
98
+ }
99
+ /**
100
+ * A {@link Writable} that mirrors everything written to it into several
101
+ * independent "sinks", pacing the input by the slowest sink that is still
102
+ * alive. Unlike {@link Tee}, the sinks are *owned* by the fan-out rather than
103
+ * being downstream pipeline stages, which lets the two error domains be kept
104
+ * apart:
105
+ *
106
+ * - An *output* (sink) that errors or ends early is simply dropped; the other
107
+ * sinks — and the input — keep going. Its error is swallowed (best-effort),
108
+ * exactly like a {@link Tee} branch.
109
+ * - The *input* (writable side) is only torn down once *every* sink has died,
110
+ * or if the input itself fails — in which case the failure is propagated to
111
+ * every surviving sink.
112
+ *
113
+ * Designed to be the destination of a pipeline:
114
+ *
115
+ * ```
116
+ * pipeline([readable, ...transforms, fanOut(main, branch)])
117
+ * ```
118
+ *
119
+ * If `main` disconnects (e.g. an HTTP response whose client went away), `branch`
120
+ * still receives the full stream (e.g. to finish populating a cache), and
121
+ * vice-versa. A sink may also be provided as a function receiving a
122
+ * {@link Readable} to consume, mirroring {@link Tee}'s constructor.
123
+ */
124
+ export function fanOut(...outputs) {
125
+ const sinks = outputs.map((output) => {
126
+ if (typeof output !== 'function')
127
+ return output;
128
+ const passthrough = new PassThrough();
129
+ output(passthrough);
130
+ return passthrough;
131
+ });
132
+ // Sinks still able to receive data. A sink that errors or closes is dropped
133
+ // and never written to again; its 'error' is swallowed here so a dead sink can
134
+ // neither crash the process nor disturb the surviving sinks (other listeners,
135
+ // e.g. a reader consuming it, still observe the error).
136
+ const alive = new Set(sinks);
137
+ const drop = (sink) => alive.delete(sink);
138
+ for (const sink of sinks) {
139
+ sink.on('error', () => drop(sink));
140
+ sink.on('close', () => drop(sink));
141
+ }
142
+ // Whether the input ended cleanly, so we can distinguish a normal completion
143
+ // (let the sinks finish the data we handed them) from a teardown (destroy
144
+ // them).
145
+ let ended = false;
146
+ return new Writable({
147
+ write(chunk, _enc, cb) {
148
+ // Write to every live sink, then invoke `cb` once they have all either
149
+ // accepted the chunk, drained, or died — pacing the input by the slowest
150
+ // surviving sink.
151
+ let waiting = 1; // guard so `cb` can't fire before the loop completes
152
+ const settle = () => {
153
+ if (--waiting > 0)
154
+ return;
155
+ // If every sink died while handling this chunk, tear the input down.
156
+ cb(alive.size === 0 ? new Error('fanOut: all sinks ended') : null);
157
+ };
158
+ for (const sink of alive) {
159
+ if (!sink.writable) {
160
+ drop(sink);
161
+ continue;
162
+ }
163
+ if (sink.write(chunk))
164
+ continue;
165
+ waiting++;
166
+ const done = () => {
167
+ sink.off('drain', done);
168
+ sink.off('close', done);
169
+ sink.off('error', done);
170
+ settle();
171
+ };
172
+ // Resume on 'drain', but also on 'close'/'error' so a sink that dies
173
+ // mid-write (e.g. one created with autoDestroy:false, which errors
174
+ // without emitting 'close') never leaves the fan-out stuck.
175
+ sink.once('drain', done);
176
+ sink.once('close', done);
177
+ sink.once('error', done);
178
+ }
179
+ settle();
180
+ },
181
+ final(cb) {
182
+ ended = true;
183
+ for (const sink of alive) {
184
+ if (sink.writable)
185
+ sink.end();
186
+ }
187
+ cb();
188
+ },
189
+ destroy(err, cb) {
190
+ // A clean end (`_final` already ran) leaves the sinks to flush what they
191
+ // were handed; any other teardown propagates to the sinks we own.
192
+ if (!ended || err) {
193
+ for (const sink of alive) {
194
+ if (sink.writable)
195
+ sink.destroy(err ?? undefined);
196
+ }
197
+ }
198
+ cb(err);
199
+ },
200
+ });
201
+ }
15
202
  export const streamSize = async (stream) => {
16
203
  let size = 0;
17
204
  for await (const chunk of stream) {
@@ -116,7 +303,7 @@ export const bytesToStream = (bytes) => {
116
303
  return stream;
117
304
  };
118
305
  export class MaxSizeChecker extends Transform {
119
- constructor(maxSize, createError) {
306
+ constructor(maxSize, createError = () => new Error(`Max size of ${maxSize} bytes exceeded`)) {
120
307
  super();
121
308
  this.maxSize = maxSize;
122
309
  this.createError = createError;
@@ -132,6 +319,28 @@ export class MaxSizeChecker extends Transform {
132
319
  }
133
320
  }
134
321
  }
322
+ export class HashPassThrough extends Transform {
323
+ #digest;
324
+ constructor(algorithm, options) {
325
+ super();
326
+ this.hash = createHash(algorithm, options);
327
+ }
328
+ _transform(chunk, _enc, cb) {
329
+ this.hash.update(chunk);
330
+ cb(null, chunk);
331
+ }
332
+ _flush(cb) {
333
+ const digest = this.hash.digest();
334
+ this.#digest = digest;
335
+ this.emit('hash', digest);
336
+ cb();
337
+ }
338
+ get digest() {
339
+ if (this.#digest)
340
+ return this.#digest;
341
+ throw new Error('Hash not yet computed. Wait for the stream to finish.');
342
+ }
343
+ }
135
344
  export function decodeStream(stream, contentEncoding) {
136
345
  const decoders = createDecoders(contentEncoding);
137
346
  if (decoders.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"streams.js","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Duplex,\n PassThrough,\n Readable,\n type Stream,\n Transform,\n type TransformCallback,\n pipeline,\n} from 'node:stream'\nimport { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib'\n\nexport const forwardStreamErrors = (...streams: Stream[]) => {\n for (let i = 1; i < streams.length; ++i) {\n const prev = streams[i - 1]\n const next = streams[i]\n\n prev.once('error', (err) => next.emit('error', err))\n }\n}\n\nexport const cloneStream = (stream: Readable): Readable => {\n const passthrough = new PassThrough()\n forwardStreamErrors(stream, passthrough)\n return stream.pipe(passthrough)\n}\n\nexport const streamSize = async (stream: Readable): Promise<number> => {\n let size = 0\n for await (const chunk of stream) {\n size += Buffer.byteLength(chunk)\n }\n return size\n}\n\nexport const streamToBytes = async (stream: AsyncIterable<Uint8Array>) =>\n // @NOTE Though Buffer is a sub-class of Uint8Array, we have observed\n // inconsistencies when using a Buffer in place of Uint8Array. For this\n // reason, we convert the Buffer to a Uint8Array.\n new Uint8Array(await streamToNodeBuffer(stream))\n\n// streamToBuffer identifier name already taken by @atproto/common-web\nexport const streamToNodeBuffer = async (\n stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n): Promise<Buffer> => {\n const chunks: Uint8Array[] = []\n let totalLength = 0 // keep track of total length for Buffer.concat\n for await (const chunk of stream) {\n if (chunk instanceof Uint8Array) {\n chunks.push(chunk)\n totalLength += Buffer.byteLength(chunk)\n } else {\n throw new TypeError('expected Uint8Array')\n }\n }\n return Buffer.concat(chunks, totalLength)\n}\n\nexport const byteIterableToStream = (\n iter: AsyncIterable<Uint8Array>,\n): Readable => {\n return Readable.from(iter, { objectMode: false })\n}\n\n/**\n * Coalesce a stream of Uint8Array chunks into larger chunks of at least the\n * specified size ({@link minChunkSize}). This is useful for optimizing\n * downstream processing that benefits from larger chunk sizes, such as\n * compression or hashing.\n */\nexport const coalesceByteStream = (\n stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n minChunkSize: number,\n): Readable => {\n if (!Number.isInteger(minChunkSize) || minChunkSize < 1) {\n throw new TypeError('minChunkSize must be a positive integer')\n }\n\n // @NOTE On Node 22, this *does* return a PassThrough (\"@types/node\"\n // incorrectly types it as Writable).\n return pipeline(stream, coalesce, (_err) => {\n // Errors are expected to be handled through the stream\n }) as PassThrough\n\n // @NOTE This implementation is not NodeJS specific and could be exported as\n // utility (from \"@atproto/common-web\") if needed. We don't do it now to avoid\n // increasing the API surface of our packages.\n async function* coalesce(\n iter: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n ): AsyncGenerator<Uint8Array> {\n // @NOTE This implementation avoids as many un-necessary copies as possible\n // and only buffers incoming chunks when they are smaller than the\n // coalescing buffer.\n\n let buffer = new Uint8Array(minChunkSize)\n let offset = 0\n\n for await (const chunk of iter) {\n const freeSpace = buffer.length - offset\n if (freeSpace > chunk.length) {\n // If the incoming chunk is smaller than the free space, we copy the\n // entire chunk into the coalescing buffer and continue to the next\n // chunk\n buffer.set(chunk, offset)\n offset += chunk.length\n } else if (offset === 0 && chunk.length >= minChunkSize) {\n // If the coalescing buffer is empty and the incoming chunk is larger\n // than the coalescing buffer, we can skip any copying and yield the\n // incoming chunk directly\n yield chunk\n } else {\n // Otherwise, we need to copy as much of the incoming chunk as we can\n // into the coalescing buffer and yield the full coalescing buffer.\n buffer.set(chunk.subarray(0, freeSpace), offset)\n yield buffer\n\n // We create a new coalescing buffer (for future use)\n buffer = new Uint8Array(minChunkSize)\n offset = 0\n\n const remainingBytes = chunk.subarray(freeSpace)\n if (remainingBytes.length > minChunkSize) {\n // If the remaining of chunk is still too big to fit in the\n // coalescing buffer, we yield it directly without copying it\n yield remainingBytes\n } else if (remainingBytes.length > 0) {\n // Otherwise, we copy the remaining bytes into the coalescing buffer\n // and continue to the next chunk\n buffer.set(remainingBytes, offset)\n offset += remainingBytes.length\n }\n }\n }\n\n // Yield any remaining bytes in the coalescing buffer\n if (offset > 0) {\n yield buffer.subarray(0, offset)\n }\n }\n}\n\nexport const bytesToStream = (bytes: Uint8Array): Readable => {\n const stream = new Readable()\n stream.push(bytes)\n stream.push(null)\n return stream\n}\n\nexport class MaxSizeChecker extends Transform {\n totalSize = 0\n constructor(\n public maxSize: number,\n public createError: () => Error,\n ) {\n super()\n }\n _transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback) {\n this.totalSize += chunk.length\n if (this.totalSize > this.maxSize) {\n cb(this.createError())\n } else {\n cb(null, chunk)\n }\n }\n}\n\nexport function decodeStream(\n stream: Readable,\n contentEncoding?: string | string[],\n): Readable\nexport function decodeStream(\n stream: AsyncIterable<Uint8Array>,\n contentEncoding?: string | string[],\n): AsyncIterable<Uint8Array> | Readable\nexport function decodeStream(\n stream: Readable | AsyncIterable<Uint8Array>,\n contentEncoding?: string | string[],\n): Readable | AsyncIterable<Uint8Array> {\n const decoders = createDecoders(contentEncoding)\n if (decoders.length === 0) return stream\n return pipeline([stream as Readable, ...decoders], () => {}) as Duplex\n}\n\n/**\n * Create a series of decoding streams based on the content-encoding header. The\n * resulting streams should be piped together to decode the content.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9110#section-8.4.1}\n */\nexport function createDecoders(contentEncoding?: string | string[]): Duplex[] {\n const decoders: Duplex[] = []\n\n if (contentEncoding?.length) {\n const encodings: string[] = Array.isArray(contentEncoding)\n ? contentEncoding.flatMap(commaSplit)\n : contentEncoding.split(',')\n for (const encoding of encodings) {\n const normalizedEncoding = normalizeEncoding(encoding)\n\n // @NOTE\n // > The default (identity) encoding [...] is used only in the\n // > Accept-Encoding header, and SHOULD NOT be used in the\n // > Content-Encoding header.\n if (normalizedEncoding === 'identity') continue\n\n decoders.push(createDecoder(normalizedEncoding))\n }\n }\n\n return decoders.reverse()\n}\n\nfunction commaSplit(header: string): string[] {\n return header.split(',')\n}\n\nfunction normalizeEncoding(encoding: string) {\n // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1\n // > All content-coding values are case-insensitive...\n return encoding.trim().toLowerCase()\n}\n\nfunction createDecoder(normalizedEncoding: string): Duplex {\n switch (normalizedEncoding) {\n // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2\n case 'gzip':\n case 'x-gzip':\n return createGunzip()\n case 'deflate':\n return createInflate()\n case 'br':\n return createBrotliDecompress()\n case 'identity':\n return new PassThrough()\n default:\n throw new TypeError(\n `Unsupported content-encoding: \"${normalizedEncoding}\"`,\n )\n }\n}\n"]}
1
+ {"version":3,"file":"streams.js","sourceRoot":"","sources":["../src/streams.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Hash, type HashOptions, createHash } from 'node:crypto'\nimport {\n type Duplex,\n PassThrough,\n Readable,\n type Stream,\n Transform,\n type TransformCallback,\n Writable,\n pipeline,\n} from 'node:stream'\nimport { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib'\n\n/**\n * @deprecated use native stream piping instead.\n */\nexport const forwardStreamErrors = (...streams: Stream[]) => {\n for (let i = 1; i < streams.length; ++i) {\n const prev = streams[i - 1]\n const next = streams[i]\n\n prev.once('error', (err) => next.emit('error', err))\n }\n}\n\n/**\n * @deprecated This function has several flaws:\n * - It only works if all the stream are setup during the same tick\n * - It does not apply any backpressure to the source stream, which can lead to\n * memory issues\n * Use {@link Tee} instead, which is a more robust implementation of the same\n * concept.\n */\nexport const cloneStream = (stream: Readable): Readable => {\n const passthrough = new PassThrough()\n forwardStreamErrors(stream, passthrough)\n return stream.pipe(passthrough)\n}\n\n/**\n * A {@link Transform} that forwards every chunk downstream while mirroring a\n * copy into a \"branch\" {@link Writable} (exposed through {@link Tee.branch}, e.g.\n * to be cached). The tee is paced by the slower of its two consumers, bounding\n * how much data gets buffered. The branch is completed when the source ends,\n * and torn down if the tee errors or is destroyed early (e.g. the client\n * disconnected).\n *\n * Consuming the branch is best-effort: its failures are swallowed here and must\n * never break the main stream.\n *\n * Because the tee is a {@link Transform}, its *main* output is a downstream\n * pipeline stage — so `pipeline([readable, tee, main])` tears the tee (and the\n * source) down as soon as `main` fails. If you need the branch to keep going\n * after the main consumer disappears (or vice-versa), use {@link fanOut}, which\n * owns both outputs instead of exposing one as a pipeline stage.\n */\nexport class Tee extends Transform {\n readonly branch: Writable\n #branchFailed = false\n\n constructor(branch: Writable | ((readable: Readable) => void)) {\n super()\n if (typeof branch === 'function') {\n const passthrough = new PassThrough({ autoDestroy: true })\n branch(passthrough)\n this.branch = passthrough\n } else {\n this.branch = branch\n }\n\n // The branch is best-effort: its failures must never break or stall the\n // main stream.\n this.branch.on('error', () => {\n this.#branchFailed = true\n })\n }\n\n get branchWritable() {\n return !this.#branchFailed && this.branch.writable\n }\n\n _transform(chunk: unknown, _enc: BufferEncoding, cb: TransformCallback) {\n // Forward downstream, applying backpressure if either branch is slower.\n if (!this.branchWritable || this.branch.write(chunk)) {\n cb(null, chunk)\n } else {\n const done = () => {\n this.branch.off('drain', done)\n this.branch.off('close', done)\n this.branch.off('error', done)\n cb(null, chunk)\n }\n // Resume on 'drain' (branch caught up) but also on 'close'/'error' so the\n // tee is never stuck if the branch dies mid-write — e.g. a branch created\n // with `autoDestroy: false`, which errors without emitting 'close'.\n this.branch.once('drain', done)\n this.branch.once('close', done)\n this.branch.once('error', done)\n }\n }\n\n _flush(cb: TransformCallback) {\n if (this.branchWritable) this.branch.end()\n cb()\n }\n\n _destroy(err: Error | null, cb: (err?: Error | null) => void) {\n // A failed branch has already torn itself down (or given up); leave it be\n // rather than emitting a second, spurious error on it.\n if (this.branchWritable) {\n this.branch.destroy(err ?? new Error('Tee destroyed'))\n }\n\n cb(err)\n }\n}\n\n/**\n * A {@link Writable} that mirrors everything written to it into several\n * independent \"sinks\", pacing the input by the slowest sink that is still\n * alive. Unlike {@link Tee}, the sinks are *owned* by the fan-out rather than\n * being downstream pipeline stages, which lets the two error domains be kept\n * apart:\n *\n * - An *output* (sink) that errors or ends early is simply dropped; the other\n * sinks — and the input — keep going. Its error is swallowed (best-effort),\n * exactly like a {@link Tee} branch.\n * - The *input* (writable side) is only torn down once *every* sink has died,\n * or if the input itself fails — in which case the failure is propagated to\n * every surviving sink.\n *\n * Designed to be the destination of a pipeline:\n *\n * ```\n * pipeline([readable, ...transforms, fanOut(main, branch)])\n * ```\n *\n * If `main` disconnects (e.g. an HTTP response whose client went away), `branch`\n * still receives the full stream (e.g. to finish populating a cache), and\n * vice-versa. A sink may also be provided as a function receiving a\n * {@link Readable} to consume, mirroring {@link Tee}'s constructor.\n */\nexport function fanOut(\n ...outputs: ReadonlyArray<Writable | ((readable: Readable) => void)>\n): Writable {\n const sinks = outputs.map((output) => {\n if (typeof output !== 'function') return output\n const passthrough = new PassThrough()\n output(passthrough)\n return passthrough\n })\n\n // Sinks still able to receive data. A sink that errors or closes is dropped\n // and never written to again; its 'error' is swallowed here so a dead sink can\n // neither crash the process nor disturb the surviving sinks (other listeners,\n // e.g. a reader consuming it, still observe the error).\n const alive = new Set<Writable>(sinks)\n const drop = (sink: Writable) => alive.delete(sink)\n for (const sink of sinks) {\n sink.on('error', () => drop(sink))\n sink.on('close', () => drop(sink))\n }\n\n // Whether the input ended cleanly, so we can distinguish a normal completion\n // (let the sinks finish the data we handed them) from a teardown (destroy\n // them).\n let ended = false\n\n return new Writable({\n write(chunk, _enc, cb) {\n // Write to every live sink, then invoke `cb` once they have all either\n // accepted the chunk, drained, or died — pacing the input by the slowest\n // surviving sink.\n let waiting = 1 // guard so `cb` can't fire before the loop completes\n const settle = () => {\n if (--waiting > 0) return\n // If every sink died while handling this chunk, tear the input down.\n cb(alive.size === 0 ? new Error('fanOut: all sinks ended') : null)\n }\n\n for (const sink of alive) {\n if (!sink.writable) {\n drop(sink)\n continue\n }\n if (sink.write(chunk)) continue\n\n waiting++\n const done = () => {\n sink.off('drain', done)\n sink.off('close', done)\n sink.off('error', done)\n settle()\n }\n // Resume on 'drain', but also on 'close'/'error' so a sink that dies\n // mid-write (e.g. one created with autoDestroy:false, which errors\n // without emitting 'close') never leaves the fan-out stuck.\n sink.once('drain', done)\n sink.once('close', done)\n sink.once('error', done)\n }\n\n settle()\n },\n final(cb) {\n ended = true\n for (const sink of alive) {\n if (sink.writable) sink.end()\n }\n cb()\n },\n destroy(err, cb) {\n // A clean end (`_final` already ran) leaves the sinks to flush what they\n // were handed; any other teardown propagates to the sinks we own.\n if (!ended || err) {\n for (const sink of alive) {\n if (sink.writable) sink.destroy(err ?? undefined)\n }\n }\n cb(err)\n },\n })\n}\n\nexport const streamSize = async (stream: Readable): Promise<number> => {\n let size = 0\n for await (const chunk of stream) {\n size += Buffer.byteLength(chunk)\n }\n return size\n}\n\nexport const streamToBytes = async (stream: AsyncIterable<Uint8Array>) =>\n // @NOTE Though Buffer is a sub-class of Uint8Array, we have observed\n // inconsistencies when using a Buffer in place of Uint8Array. For this\n // reason, we convert the Buffer to a Uint8Array.\n new Uint8Array(await streamToNodeBuffer(stream))\n\n// streamToBuffer identifier name already taken by @atproto/common-web\nexport const streamToNodeBuffer = async (\n stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n): Promise<Buffer> => {\n const chunks: Uint8Array[] = []\n let totalLength = 0 // keep track of total length for Buffer.concat\n for await (const chunk of stream) {\n if (chunk instanceof Uint8Array) {\n chunks.push(chunk)\n totalLength += Buffer.byteLength(chunk)\n } else {\n throw new TypeError('expected Uint8Array')\n }\n }\n return Buffer.concat(chunks, totalLength)\n}\n\nexport const byteIterableToStream = (\n iter: AsyncIterable<Uint8Array>,\n): Readable => {\n return Readable.from(iter, { objectMode: false })\n}\n\n/**\n * Coalesce a stream of Uint8Array chunks into larger chunks of at least the\n * specified size ({@link minChunkSize}). This is useful for optimizing\n * downstream processing that benefits from larger chunk sizes, such as\n * compression or hashing.\n */\nexport const coalesceByteStream = (\n stream: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n minChunkSize: number,\n): Readable => {\n if (!Number.isInteger(minChunkSize) || minChunkSize < 1) {\n throw new TypeError('minChunkSize must be a positive integer')\n }\n\n // @NOTE On Node 22, this *does* return a PassThrough (\"@types/node\"\n // incorrectly types it as Writable).\n return pipeline(stream, coalesce, (_err) => {\n // Errors are expected to be handled through the stream\n }) as PassThrough\n\n // @NOTE This implementation is not NodeJS specific and could be exported as\n // utility (from \"@atproto/common-web\") if needed. We don't do it now to avoid\n // increasing the API surface of our packages.\n async function* coalesce(\n iter: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,\n ): AsyncGenerator<Uint8Array> {\n // @NOTE This implementation avoids as many un-necessary copies as possible\n // and only buffers incoming chunks when they are smaller than the\n // coalescing buffer.\n\n let buffer = new Uint8Array(minChunkSize)\n let offset = 0\n\n for await (const chunk of iter) {\n const freeSpace = buffer.length - offset\n if (freeSpace > chunk.length) {\n // If the incoming chunk is smaller than the free space, we copy the\n // entire chunk into the coalescing buffer and continue to the next\n // chunk\n buffer.set(chunk, offset)\n offset += chunk.length\n } else if (offset === 0 && chunk.length >= minChunkSize) {\n // If the coalescing buffer is empty and the incoming chunk is larger\n // than the coalescing buffer, we can skip any copying and yield the\n // incoming chunk directly\n yield chunk\n } else {\n // Otherwise, we need to copy as much of the incoming chunk as we can\n // into the coalescing buffer and yield the full coalescing buffer.\n buffer.set(chunk.subarray(0, freeSpace), offset)\n yield buffer\n\n // We create a new coalescing buffer (for future use)\n buffer = new Uint8Array(minChunkSize)\n offset = 0\n\n const remainingBytes = chunk.subarray(freeSpace)\n if (remainingBytes.length > minChunkSize) {\n // If the remaining of chunk is still too big to fit in the\n // coalescing buffer, we yield it directly without copying it\n yield remainingBytes\n } else if (remainingBytes.length > 0) {\n // Otherwise, we copy the remaining bytes into the coalescing buffer\n // and continue to the next chunk\n buffer.set(remainingBytes, offset)\n offset += remainingBytes.length\n }\n }\n }\n\n // Yield any remaining bytes in the coalescing buffer\n if (offset > 0) {\n yield buffer.subarray(0, offset)\n }\n }\n}\n\nexport const bytesToStream = (bytes: Uint8Array): Readable => {\n const stream = new Readable()\n stream.push(bytes)\n stream.push(null)\n return stream\n}\n\nexport class MaxSizeChecker extends Transform {\n totalSize = 0\n constructor(\n public maxSize: number,\n public createError: () => Error = () =>\n new Error(`Max size of ${maxSize} bytes exceeded`),\n ) {\n super()\n }\n _transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback) {\n this.totalSize += chunk.length\n if (this.totalSize > this.maxSize) {\n cb(this.createError())\n } else {\n cb(null, chunk)\n }\n }\n}\n\nexport class HashPassThrough extends Transform {\n private readonly hash: Hash\n #digest?: Buffer<ArrayBuffer>\n\n constructor(algorithm: string, options?: HashOptions) {\n super()\n this.hash = createHash(algorithm, options)\n }\n\n _transform(chunk: Uint8Array, _enc: BufferEncoding, cb: TransformCallback) {\n this.hash.update(chunk)\n cb(null, chunk)\n }\n\n _flush(cb: TransformCallback) {\n const digest = this.hash.digest()\n this.#digest = digest\n this.emit('hash', digest)\n cb()\n }\n\n get digest(): Buffer<ArrayBuffer> {\n if (this.#digest) return this.#digest\n throw new Error('Hash not yet computed. Wait for the stream to finish.')\n }\n}\n\nexport function decodeStream(\n stream: Readable,\n contentEncoding?: string | string[],\n): Readable\nexport function decodeStream(\n stream: AsyncIterable<Uint8Array>,\n contentEncoding?: string | string[],\n): AsyncIterable<Uint8Array> | Readable\nexport function decodeStream(\n stream: Readable | AsyncIterable<Uint8Array>,\n contentEncoding?: string | string[],\n): Readable | AsyncIterable<Uint8Array> {\n const decoders = createDecoders(contentEncoding)\n if (decoders.length === 0) return stream\n return pipeline([stream as Readable, ...decoders], () => {}) as Duplex\n}\n\n/**\n * Create a series of decoding streams based on the content-encoding header. The\n * resulting streams should be piped together to decode the content.\n *\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9110#section-8.4.1}\n */\nexport function createDecoders(contentEncoding?: string | string[]): Duplex[] {\n const decoders: Duplex[] = []\n\n if (contentEncoding?.length) {\n const encodings: string[] = Array.isArray(contentEncoding)\n ? contentEncoding.flatMap(commaSplit)\n : contentEncoding.split(',')\n for (const encoding of encodings) {\n const normalizedEncoding = normalizeEncoding(encoding)\n\n // @NOTE\n // > The default (identity) encoding [...] is used only in the\n // > Accept-Encoding header, and SHOULD NOT be used in the\n // > Content-Encoding header.\n if (normalizedEncoding === 'identity') continue\n\n decoders.push(createDecoder(normalizedEncoding))\n }\n }\n\n return decoders.reverse()\n}\n\nfunction commaSplit(header: string): string[] {\n return header.split(',')\n}\n\nfunction normalizeEncoding(encoding: string) {\n // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1\n // > All content-coding values are case-insensitive...\n return encoding.trim().toLowerCase()\n}\n\nfunction createDecoder(normalizedEncoding: string): Duplex {\n switch (normalizedEncoding) {\n // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2\n case 'gzip':\n case 'x-gzip':\n return createGunzip()\n case 'deflate':\n return createInflate()\n case 'br':\n return createBrotliDecompress()\n case 'identity':\n return new PassThrough()\n default:\n throw new TypeError(\n `Unsupported content-encoding: \"${normalizedEncoding}\"`,\n )\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/common",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "description": "Shared web-platform-friendly code for atproto libraries",
6
6
  "keywords": [
@@ -30,16 +30,16 @@
30
30
  "dependencies": {
31
31
  "multiformats": "^13.0.0",
32
32
  "pino": "^10.3.1",
33
- "@atproto/lex-cbor": "^0.1.6",
34
33
  "@atproto/common-web": "^0.5.9",
34
+ "@atproto/lex-cbor": "^0.1.6",
35
35
  "@atproto/lex-data": "^0.1.7"
36
36
  },
37
37
  "devDependencies": {
38
- "jest": "^30.0.0",
39
- "uint8arrays": "^5.0.0"
38
+ "uint8arrays": "^5.0.0",
39
+ "vitest": "^4.1.10"
40
40
  },
41
41
  "scripts": {
42
- "test": "NODE_OPTIONS=--experimental-vm-modules jest",
42
+ "test": "vitest run",
43
43
  "build": "tsc --build tsconfig.build.json"
44
44
  }
45
45
  }