// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.

/** Options for {@linkcode LimitedBytesTransformStream}. */
export interface LimitedBytesTransformStreamOptions {
  /**
   * If true, a {@linkcode RangeError} is thrown when queueing the current chunk
   * would exceed the specified size limit.
   *
   * @default {false}
   */
  error?: boolean;
}

/**
 * A {@linkcode TransformStream} that will only read & enqueue chunks until the
 * total amount of enqueued data exceeds `size`. The last chunk that would
 * exceed the limit will NOT be enqueued, in which case a {@linkcode RangeError}
 * is thrown when `options.error` is set to true, otherwise the stream is just
 * terminated.
 *
 * @example `size` is equal to the total byte length of the chunks
 * ```ts
 * import { LimitedBytesTransformStream } from "@std/streams/limited-bytes-transform-stream";
 * import { assertEquals } from "@std/assert/assert-equals";
 *
 * const stream = ReadableStream.from(["1234", "5678"]);
 * const transformed = stream.pipeThrough(new TextEncoderStream()).pipeThrough(
 *   new LimitedBytesTransformStream(8),
 * ).pipeThrough(new TextDecoderStream());
 *
 * assertEquals(
 *   await Array.fromAsync(transformed),
 *   ["1234", "5678"],
 * );
 * ```
 *
 * @example `size` is less than the total byte length of the chunks, and at the
 * boundary of the chunks
 * ```ts
 * import { LimitedBytesTransformStream } from "@std/streams/limited-bytes-transform-stream";
 * import { assertEquals } from "@std/assert/assert-equals";
 *
 * const stream = ReadableStream.from(["1234", "5678"]);
 * const transformed = stream.pipeThrough(new TextEncoderStream()).pipeThrough(
 *   // `4` is the boundary of the chunks
 *   new LimitedBytesTransformStream(4),
 * ).pipeThrough(new TextDecoderStream());
 *
 * assertEquals(
 *   await Array.fromAsync(transformed),
 *   // The first chunk was read, but the second chunk was not
 *   ["1234"],
 * );
 * ```
 *
 * @example `size` is less than the total byte length of the chunks, and not at
 * the boundary of the chunks
 * ```ts
 * import { LimitedBytesTransformStream } from "@std/streams/limited-bytes-transform-stream";
 * import { assertEquals } from "@std/assert/assert-equals";
 *
 * const stream = ReadableStream.from(["1234", "5678"]);
 * const transformed = stream.pipeThrough(new TextEncoderStream()).pipeThrough(
 *   // `5` is not the boundary of the chunks
 *   new LimitedBytesTransformStream(5),
 * ).pipeThrough(new TextDecoderStream());
 *
 * assertEquals(
 *   await Array.fromAsync(transformed),
 *   // The second chunk was not read because it would exceed the specified size
 *   ["1234"],
 * );
 * ```
 *
 * @example error: true
 * ```ts
 * import { LimitedBytesTransformStream } from "@std/streams/limited-bytes-transform-stream";
 * import { assertRejects } from "@std/assert/assert-rejects";
 *
 * const stream = ReadableStream.from(["1234", "5678"]);
 * const transformed = stream.pipeThrough(new TextEncoderStream()).pipeThrough(
 *   new LimitedBytesTransformStream(5, { error: true }),
 * ).pipeThrough(new TextDecoderStream());
 *
 * await assertRejects(async () => {
 *   await Array.fromAsync(transformed);
 * }, RangeError);
 * ```
 */
export class LimitedBytesTransformStream
  extends TransformStream<Uint8Array, Uint8Array> {
  #read = 0;

  /**
   * Constructs a new instance.
   *
   * @param size A size limit in bytes.
   * @param options Options for the stream.
   *
   * @example size = 42
   * ```ts no-assert
   * import { LimitedBytesTransformStream } from "@std/streams/limited-bytes-transform-stream";
   *
   * const limitedBytesTransformStream = new LimitedBytesTransformStream(42);
   * ```
   *
   * @example size = 42, error = true
   * ```ts no-assert
   * import { LimitedBytesTransformStream } from "@std/streams/limited-bytes-transform-stream";
   *
   * const limitedBytesTransformStream = new LimitedBytesTransformStream(42, { error: true });
   * ```
   */
  constructor(
    size: number,
    options: LimitedBytesTransformStreamOptions = { error: false },
  ) {
    super({
      transform: (chunk, controller) => {
        if ((this.#read + chunk.byteLength) > size) {
          if (options.error) {
            throw new RangeError(`Exceeded byte size limit of '${size}'`);
          } else {
            controller.terminate();
          }
        } else {
          this.#read += chunk.byteLength;
          controller.enqueue(chunk);
        }
      },
    });
  }
}

// denoCacheMetadata={"headers":{"age":"112265","cf-ray":"a223e657caaa7db6-LAX","access-control-expose-headers":"*","cross-origin-resource-policy":"cross-origin","set-cookie":"__cf_bm=j9EG2UbFDlVpS7sY0dzt.awuEQk2CLDTaYmwXBKkrK4-1785241875.1765518-1.0.1.1-rKDGUIc70lZ0q1xN17FrdhtlxoL.TARdB6VHXvuwXpycssp_iT3pgYu5by2SRRFEdFCQPveQ2HgOxce4SvTSnKOlbbOxlRDkYBOnFAG8J5ZoaJRwILY8zrqmVaG1jXgD; HttpOnly; SameSite=None; Secure; Path=/; Domain=jsr.io; Expires=Tue, 28 Jul 2026 13:01:15 GMT","cache-control":"public, max-age=31536000, immutable","etag":"\"df68e55c12f84695752000203f5c4cf5\"","date":"Tue, 28 Jul 2026 12:31:15 GMT","access-control-allow-origin":"*","alt-svc":"h3=\":443\"; ma=86400","x-robots-tag":"noindex","nel":"{\"report_to\":\"cf-nel\",\"success_fraction\":0.0,\"max_age\":604800}","content-type":"text/typescript","server":"cloudflare","report-to":"{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=U3R7%2BPV%2FX0jlUn9BBNP44L23CSlChXU1%2F8EAEdFJzAQaGSwQAt1ppmrb9INr9kqB1owXY5jmUI%2Bu3ppxPzPFuVv9R713QAuUWjOT8pXIqS7oJWIwrbLAyZs%3D\"}]}","accept-ranges":"bytes","content-length":"4520","vary":"Origin","x-content-type-options":"nosniff","content-security-policy":"default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'; font-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; frame-ancestors 'none'; sandbox; form-action 'none';","x-jsr-backend":"modules","cf-cache-status":"HIT"},"url":"https://jsr.io/@std/streams/0.224.5/limited_bytes_transform_stream.ts","time":1785241875}