Repository: hildjj/cbor2
Pull request #71: Low-level sequence decoder
Author: login: novemberborn
Created: 2025-04-06T19:51:34Z | Merged: 2025-05-10T15:05:42Z

Allows low-level decoding of CBOR sequences, yielding tuples with major type, additional information & values.

Sequences can be iterated as is, but also supports peek() to look at the next value, and read() which consumes that value.

Fixes #70.

---

See usage in the `Decoder` in the very WIP Concordance rewrite. Current link: https://github.com/concordancejs/concordance/blob/b43859b9124fdbb15f657a8d9636183e83c59cf5/src/deserialize.ts#L52

This is very low-level, in fact I'm interpreting the major types etc by hand. I'm not dealing with indefinite length values either.

Mostly looking for feedback on whether this is worthwhile as a first approach.

Linked issue #70: Export decoder stream?
👋 Hi @hildjj! Excited to see a more nimble CBOR implementation here.

Would you be willing to export the decoder stream?

This should allow me to implement a lower-level application protocol based on CBOR Sequences, rather than shoehorning everything into lists and maps. Basically, my application would understand the expected nesting and know what value types to expect, so it could consume them one at a time from the decoder stream. (I'm looking at using CBOR in [`concordance`](https://github.com/concordancejs/concordance) rather than my own encoding logic.)

It looks like the encoder would already allow me to emit sequences.
Linked issue comments: user: login: hildjj
created_at: 2025-02-27T22:03:21Z
body: I'm open to it.  Let me think about the API I want to expose.
author_association: OWNER
user: login: novemberborn
created_at: 2025-02-28T13:40:44Z
body: I might not actually need the stream itself. A set of `read*()` methods that typecheck and return the next value; some way to `peek()` to allow different types at the same place in the sequence, a `skip()` so I can `peek()` the break stop code and then move past it, and an accessor for the current offset so I can restart in the future.

Tags, maps, lists, indefinite byte & text strings could behave the same way: `peek()` to identify the major type, then read the tag number or length, and then the next read gives the tag content or the next item.

Having a typed "not enough data left" would be useful to, because putting it all together you can then implement a streaming decoder outside of this library.
author_association: CONTRIBUTOR

Diff:
diff --git a/package.json b/package.json
index 51a08bd..ad1abd4 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,10 @@
       "name": "İrfan Bilaloğlu",
       "email": "irfanbilaloglu@gmail.com",
       "url": "https://github.com/irfan798"
+    },
+    {
+      "name": "Mark Wubben",
+      "url": "https://novemberborn.net"
     }
   ],
   "license": "MIT",
diff --git a/src/decodeStream.ts b/src/decodeStream.ts
index ad1a556..e12f028 100644
--- a/src/decodeStream.ts
+++ b/src/decodeStream.ts
@@ -80,6 +80,44 @@ export class DecodeStream implements Sliceable {
     }
   }
 
+  /**
+   * Get a stream of events describing all CBOR items in the input.  Yields
+   * Value tuples.
+   *
+   * Note that this includes items indicating the start of an array or map, and
+   * the end of an indefinite-length item, and tag numbers separate from the tag
+   * content. Does not guarantee that the input is valid.
+   *
+   * Will attempt to read all items in an array or map, even if indefinite.
+   * Throws when there is insufficient data to do so. The same applies when
+   * reading tagged items, byte strings and text strings.
+   *
+   * @throws On insufficient data.
+   * @example
+   * ```js
+   * const s = new DecodeStream(buffer);
+   * for (const [majorType, additionalInfo, value] of s.seq()) {
+   *  ...
+   * }
+   * ```
+   */
+  public *seq(): ValueGenerator {
+    // Repeatedly read the next value from the input, until the offset of the
+    // next possible value is past the input length.
+    //
+    // Throws if their is insufficient data to read the next value. Otherwise,
+    // within #nextVal the offset will be incremented to where it expects the
+    // next item to be.
+    //
+    // Note that since we're producting each item, we don't need to track depth.
+    //
+    // Note that #nextVal takes care of reading array and map items and tag
+    // content, which means this can still throw if there is insufficient data.
+    while (this.#offset < this.#src.length) {
+      yield *this.#nextVal(0);
+    }
+  }
+
   /**
    * Get the next CBOR value from the input stream.  Yields Value tuples.
    *
diff --git a/src/decoder.ts b/src/decoder.ts
index c084efb..a802ab6 100644
--- a/src/decoder.ts
+++ b/src/decoder.ts
@@ -1,21 +1,11 @@
-import type {DecodeOptions, Parent} from './options.js';
+import type {DecodeOptions, MtAiValue, Parent} from './options.js';
+import {DecodeStream, type ValueGenerator} from './decodeStream.js';
 import {CBORcontainer} from './container.js';
-import {DecodeStream} from './decodeStream.js';
 import {SYMS} from './constants.js';
 
-/**
- * Decode CBOR bytes to a JS value.
- *
- * @param src CBOR bytes to decode.
- * @param options Options for decoding.
- * @returns JS value decoded from cbor.
- * @throws {Error} No value found, decoding errors.
- */
-// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-export function decode<T = unknown>(
-  src: Uint8Array | string,
-  options: DecodeOptions = {}
-): T {
+function normalizeOptions(
+  options: DecodeOptions
+): Required<DecodeOptions> {
   const opts = {...CBORcontainer.defaultDecodeOptions};
   if (options.dcbor) {
     Object.assign(opts, CBORcontainer.dcborDecodeOptions);
@@ -31,6 +21,24 @@ export function decode<T = unknown>(
   if (opts.boxed) {
     opts.saveOriginal = true;
   }
+
+  return opts;
+}
+
+/**
+ * Decode CBOR bytes to a JS value.
+ *
+ * @param src CBOR bytes to decode.
+ * @param options Options for decoding.
+ * @returns JS value decoded from cbor.
+ * @throws {Error} No value found, decoding errors.
+ */
+// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+export function decode<T = unknown>(
+  src: Uint8Array | string,
+  options: DecodeOptions = {}
+): T {
+  const opts = normalizeOptions(options);
   const stream = new DecodeStream(src, opts);
   let parent: Parent | undefined = undefined;
   let ret: unknown = undefined;
@@ -64,3 +72,73 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode (a sequence of) CBOR bytes to
+ * major-type/additional-information/value tuples.
+ *
+  * Note that this includes items indicating the start of an array or map, and
+  * the end of an indefinite-length item, and tag numbers separate from the tag
+  * content. Does not guarantee that the input is valid.
+  *
+  * Will attempt to read all items in an array or map, even if indefinite.
+  * Throws when there is insufficient data to do so. The same applies when
+  * reading tagged items, byte strings and text strings.
+ *
+ * @param src CBOR bytes to decode.
+ * @param options Options for decoding.
+ * @throws {Error} On insufficient data.
+ * @example
+ * ```js
+ * const s = new Sequence(buffer);
+ * for (const [majorType, additionalInfo, value] of s.seq()) {
+ *  ...
+ * }
+ * ```
+ */
+export class Sequence {
+  #seq: ValueGenerator;
+  #peeked: MtAiValue | undefined;
+
+  public constructor(src: Uint8Array | string, options: DecodeOptions = {}) {
+    const stream = new DecodeStream(src, normalizeOptions(options));
+    this.#seq = stream.seq();
+  }
+
+  /** Peek at the next tuple, allowing for later reads. */
+  public peek(): MtAiValue | undefined {
+    if (!this.#peeked) {
+      this.#peeked = this.#next();
+    }
+    return this.#peeked;
+  }
+
+  /** Read the next tuple. */
+  public read(): MtAiValue | undefined {
+    const mav = this.#peeked ?? this.#next();
+    this.#peeked = undefined;
+    return mav;
+  }
+
+  /** Iterate over all tuples. */
+  public *[Symbol.iterator](): Generator<MtAiValue, void, undefined> {
+    while (true) {
+      const tuple = this.read();
+
+      if (!tuple) {
+        return;
+      }
+
+      yield tuple;
+    }
+  }
+
+  #next(): MtAiValue | undefined {
+    const {value: tuple, done} = this.#seq.next();
+    if (done) {
+      return undefined;
+    }
+
+    return tuple;
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index 9b8f60b..bc31e4b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -32,7 +32,7 @@ export type {
   WriterOptions,
 } from './options.js';
 export {DiagnosticSizes} from './options.js';
-export {decode} from './decoder.js';
+export {decode, Sequence} from './decoder.js';
 export {diagnose} from './diagnostic.js';
 export {comment} from './comment.js';
 export {
diff --git a/test/sequence.test.js b/test/sequence.test.js
new file mode 100644
index 0000000..771ffdb
--- /dev/null
+++ b/test/sequence.test.js
@@ -0,0 +1,356 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration with Uint8Array input', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(new Uint8Array([1, 2, 3]));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence iteration with hex input', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete fixed length array', () => {
+  // Array claiming length 3 but only 2 elements provided
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   (missing third element)
+  const hex = '830102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when reading array with insufficient elements
+  assert.throws(() => [...seq], {
+    name: 'RangeError',
+  }, 'Should throw when reading incomplete array');
+});
+
+test('Incomplete fixed length map', () => {
+  // Map claiming 2 pairs but only 1.5 pairs provided
+  // A2       -- Map (Length: 2 pairs)
+  //   01     --   [0] Key: 1
+  //   02     --   [0] Value: 2
+  //   03     --   [1] Key: 3
+  //   (missing second value)
+  const hex = 'a2010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when reading map with insufficient key-value pairs
+  assert.throws(() => [...seq], {
+    name: 'RangeError',
+  }, 'Should throw when reading incomplete map');
+});
+
+test('Incomplete tagged value', () => {
+  // Tag 0 without its content
+  // C0       -- Tag #0
+  //   (missing tagged content)
+  const hex = 'c0';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when attempting to read tag without content
+  assert.throws(() => [...seq], {
+    name: 'RangeError',
+  }, 'Should throw when reading tag without content');
+});
+
+test('Incomplete byte string', () => {
+  // Byte string claiming length 10 but only 3 bytes provided
+  // 4A          -- Byte String (Length: 10)
+  //   010203    -- Only 3 bytes provided
+  const hex = '4a010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read byte string with insufficient data
+  assert.throws(() => [...seq], {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete byte string');
+});
+
+test('Incomplete text string', () => {
+  // Text string claiming length 10 but only 3 bytes provided
+  // 6A          -- Text String (Length: 10)
+  //   616263    -- Only 3 bytes provided ("abc")
+  const hex = '6a616263';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read text string with insufficient data
+  assert.throws(() => [...seq], {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete text string');
+});
+
+test('Incomplete indefinite length array', () => {
+  // Indefinite length array without break code
+  // 9F          -- Array (Length: Indefinite)
+  //   01        --   [0] Unsigned: 1
+  //   02        --   [1] Unsigned: 2
+  //   (missing break code)
+  const hex = '9f0102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Partially reading should work
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[0], 4, 'First tuple should be array type');
+  assert.equal(firstTuple[1], 31, 'Additional info should be 31 for indef length');
+
+  const secondTuple = seq.read();
+  assert.equal(secondTuple[2], 1, 'Second tuple should be 1');
+
+  const thirdTuple = seq.read();
+  assert.equal(thirdTuple[2], 2, 'Third tuple should be 2');
+
+  // But when trying to read past available bytes, it should throw
+  assert.throws(() => seq.read(), {
+    name: 'RangeError',
+  }, 'Should throw when reading indefinite array without break code');
+});
+
+test('Incomplete nested structure', () => {
+  // Nested array with incomplete inner array
+  // 81          -- Array (Length: 1)
+  //   83        --   [0] Array (Length: 3)
+  //     01      --     [0] Unsigned: 1
+  //     02      --     [1] Unsigned: 2
+  //     (missing third element)
+  const hex = '81830102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read the incomplete nested structure
+  assert.throws(() => [...seq], {
+    name: 'RangeError',
+  }, 'Should throw when reading incomplete nested structure');
+});


Reviews:
user: login: hildjj
body: Just a couple of nits.  Really close here.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-09T18:05:10Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:27:32Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:28:19Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:45:15Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:49:29Z
user: login: hildjj
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T18:50:32Z
user: login: hildjj
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T18:50:58Z
user: login: hildjj
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T18:54:20Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T19:36:11Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T19:46:12Z
user: login: hildjj
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T19:46:43Z
user: login: hildjj
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T19:50:09Z
user: login: novemberborn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T20:13:38Z
user: login: hildjj
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T20:21:59Z
user: login: hildjj
body: LGTM
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-10T15:04:49Z

Review comments:
diff_hunk: @@ -80,6 +80,12 @@ export class DecodeStream implements Sliceable {
     }
   }
 
+  public *seq(): ValueGenerator {
path: src/decodeStream.ts
user: login: hildjj
body: Needs TSdoc.  I know not all of the methods here have it, but I'll fix those.
created_at: 2025-05-09T17:44:33Z
author_association: OWNER
diff_hunk: @@ -80,6 +80,12 @@ export class DecodeStream implements Sliceable {
     }
   }
 
+  public *seq(): ValueGenerator {
+    while (this.#offset < this.#src.length) {
path: src/decodeStream.ts
user: login: hildjj
body: This feels like it might have a chance at an infinite loop, but I can't think of a way to make it happen.  Please put a comment here that assures me you've thought of that, so the next time I see it I don't have to think about it again. :)
created_at: 2025-05-09T17:46:19Z
author_association: OWNER
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
path: src/decoder.ts
user: login: hildjj
body: This is documented, but not enough for someone to really use it.  Constructing an object from MtAiValue is delicate, particularly when streaming is supported.  I wonder if there should be a high-level version of this that uses the logic from `decode` to generate a sequence of object results?  I can add that after this merges, just interested in your thoughts.
created_at: 2025-05-09T17:54:10Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
path: test/sequence.test.js
user: login: hildjj
body: prefer `assert.deepEqual(seq2, [...])` here.
created_at: 2025-05-09T17:57:28Z
author_association: OWNER
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
+ */
+export class Sequence {
+  #seq: ValueGenerator;
+  #peeked: MtAiValue | undefined;
+
+  public constructor(src: Uint8Array | string, options: DecodeOptions = {}) {
+    const stream = new DecodeStream(src, normalizeOptions(options));
+    this.#seq = stream.seq();
+  }
+
+  /** Peek at the next tuple, allowing for later reads. */
+  public peek(): MtAiValue | undefined {
+    if (!this.#peeked) {
+      this.#peeked = this.#next();
+    }
+    return this.#peeked;
+  }
+
+  /** Read the next tuple. */
+  public read(): MtAiValue | undefined {
+    const mav = this.#peeked ?? this.#next();
+    this.#peeked = undefined;
+    return mav;
+  }
+
+  /** Iterate over all tuples. */
+  public *[Symbol.iterator](): Generator<MtAiValue, void, undefined> {
+    while (true) {
+      const tuple = this.read();
+
+      if (!tuple) {
+        return;
+      }
+
+      yield tuple;
+    }
+  }
+
+  #next(): MtAiValue | undefined {
+    const {value: tuple, done} = this.#seq.next();
+    if (done) {
+      return undefined;
+    }
+
+    return tuple;
path: src/decoder.ts
user: login: hildjj
body: I wonder if we should call `Object.freeze` on these, so that you can't peek, modify the result, and cause the next read to be different than expected.
created_at: 2025-05-09T18:01:08Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: hildjj
body: Add another test for "8201", it gets a different error.
created_at: 2025-05-09T18:04:47Z
author_association: OWNER
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
path: src/decoder.ts
user: login: novemberborn
body: That would be useful, yes. Under [RFC 8742](https://www.rfc-editor.org/rfc/rfc8742.html) a CBOR sequence is merely one data item *followed* by another. I don't think you can nest sequences, so a higher-level implementation could consume multiple MtAiValues to yield an object.

If I recall I couldn't immediately figure out how to do this and opted for a lower-level implementation instead. The parsing code I wrote on top of this is necessarily low-level.

On the other hand, if you're implementing a CBOR Sequence application protocol, you may want that low-level information to validate the data stream.
created_at: 2025-05-09T18:27:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
+ */
+export class Sequence {
+  #seq: ValueGenerator;
+  #peeked: MtAiValue | undefined;
+
+  public constructor(src: Uint8Array | string, options: DecodeOptions = {}) {
+    const stream = new DecodeStream(src, normalizeOptions(options));
+    this.#seq = stream.seq();
+  }
+
+  /** Peek at the next tuple, allowing for later reads. */
+  public peek(): MtAiValue | undefined {
+    if (!this.#peeked) {
+      this.#peeked = this.#next();
+    }
+    return this.#peeked;
+  }
+
+  /** Read the next tuple. */
+  public read(): MtAiValue | undefined {
+    const mav = this.#peeked ?? this.#next();
+    this.#peeked = undefined;
+    return mav;
+  }
+
+  /** Iterate over all tuples. */
+  public *[Symbol.iterator](): Generator<MtAiValue, void, undefined> {
+    while (true) {
+      const tuple = this.read();
+
+      if (!tuple) {
+        return;
+      }
+
+      yield tuple;
+    }
+  }
+
+  #next(): MtAiValue | undefined {
+    const {value: tuple, done} = this.#seq.next();
+    if (done) {
+      return undefined;
+    }
+
+    return tuple;
path: src/decoder.ts
user: login: novemberborn
body: If you're peeking and then modifying the value you're really shooting yourself in the foot.
created_at: 2025-05-09T18:28:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
path: test/sequence.test.js
user: login: novemberborn
body: That makes them the same as the sequence iteration test, so I'll make two versions of that for the different inputs.
created_at: 2025-05-09T18:45:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: novemberborn
body: `8201` meaning "array with 2 items"? That won't throw an error, instead we'll see "here comes an array with 2 items", "item one is `1`"… and the generator returns. The consumer needs to validate that it received the correct number of items.
created_at: 2025-05-09T18:49:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
path: src/decoder.ts
user: login: hildjj
body: It's a little worse; I would expect many CBOR Sequence applications to need to stream data in a little at a time.  I"m *not* proposing supporting that at this time.  The high-level API will just decode a stream where you have the entire contents at once.  We can add support for good errors and saving the prefix when an item is truncated, I suppose.
created_at: 2025-05-09T18:50:32Z
author_association: OWNER
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
+ */
+export class Sequence {
+  #seq: ValueGenerator;
+  #peeked: MtAiValue | undefined;
+
+  public constructor(src: Uint8Array | string, options: DecodeOptions = {}) {
+    const stream = new DecodeStream(src, normalizeOptions(options));
+    this.#seq = stream.seq();
+  }
+
+  /** Peek at the next tuple, allowing for later reads. */
+  public peek(): MtAiValue | undefined {
+    if (!this.#peeked) {
+      this.#peeked = this.#next();
+    }
+    return this.#peeked;
+  }
+
+  /** Read the next tuple. */
+  public read(): MtAiValue | undefined {
+    const mav = this.#peeked ?? this.#next();
+    this.#peeked = undefined;
+    return mav;
+  }
+
+  /** Iterate over all tuples. */
+  public *[Symbol.iterator](): Generator<MtAiValue, void, undefined> {
+    while (true) {
+      const tuple = this.read();
+
+      if (!tuple) {
+        return;
+      }
+
+      yield tuple;
+    }
+  }
+
+  #next(): MtAiValue | undefined {
+    const {value: tuple, done} = this.#seq.next();
+    if (done) {
+      return undefined;
+    }
+
+    return tuple;
path: src/decoder.ts
user: login: hildjj
body: I guess I'm wondering how you're going to use this.  Is it possible this is a security issue?
created_at: 2025-05-09T18:50:58Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: hildjj
body: With this code:

```js
const s = new Sequence('8201');
[...s]
```

I get:
```
Uncaught RangeError: Offset is outside the bounds of the DataView
    at DataView.prototype.getUint8 (<anonymous>)
...
```
created_at: 2025-05-09T18:54:20Z
author_association: OWNER
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
+ */
+export class Sequence {
+  #seq: ValueGenerator;
+  #peeked: MtAiValue | undefined;
+
+  public constructor(src: Uint8Array | string, options: DecodeOptions = {}) {
+    const stream = new DecodeStream(src, normalizeOptions(options));
+    this.#seq = stream.seq();
+  }
+
+  /** Peek at the next tuple, allowing for later reads. */
+  public peek(): MtAiValue | undefined {
+    if (!this.#peeked) {
+      this.#peeked = this.#next();
+    }
+    return this.#peeked;
+  }
+
+  /** Read the next tuple. */
+  public read(): MtAiValue | undefined {
+    const mav = this.#peeked ?? this.#next();
+    this.#peeked = undefined;
+    return mav;
+  }
+
+  /** Iterate over all tuples. */
+  public *[Symbol.iterator](): Generator<MtAiValue, void, undefined> {
+    while (true) {
+      const tuple = this.read();
+
+      if (!tuple) {
+        return;
+      }
+
+      yield tuple;
+    }
+  }
+
+  #next(): MtAiValue | undefined {
+    const {value: tuple, done} = this.#seq.next();
+    if (done) {
+      return undefined;
+    }
+
+    return tuple;
path: src/decoder.ts
user: login: novemberborn
body: In Concordance, I'm encoding aspects of an object as a sequence. There's a marker item for each type of aspect (named property, symbol property, iterator value, etc). So when decoding named properties, if I see the symbol property marker I know to stop decoding named properties. This is easiest if I can peek at the next item.
created_at: 2025-05-09T19:36:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: novemberborn
body: Oh I see, it's because the test wasn't reading all items.

This is a problem, actually. `*#nextVal()` tries to read all array items so it throws when it's out of data. IMHO it'd make more sense if it only returned the next item. Same for maps and tags.
created_at: 2025-05-09T19:46:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,3 +72,53 @@ export function decode<T = unknown>(
   }
   return ret as T;
 }
+
+/**
+ * Decode CBOR Sequence bytes to major-type/additional-information/value tuples.
+ */
+export class Sequence {
+  #seq: ValueGenerator;
+  #peeked: MtAiValue | undefined;
+
+  public constructor(src: Uint8Array | string, options: DecodeOptions = {}) {
+    const stream = new DecodeStream(src, normalizeOptions(options));
+    this.#seq = stream.seq();
+  }
+
+  /** Peek at the next tuple, allowing for later reads. */
+  public peek(): MtAiValue | undefined {
+    if (!this.#peeked) {
+      this.#peeked = this.#next();
+    }
+    return this.#peeked;
+  }
+
+  /** Read the next tuple. */
+  public read(): MtAiValue | undefined {
+    const mav = this.#peeked ?? this.#next();
+    this.#peeked = undefined;
+    return mav;
+  }
+
+  /** Iterate over all tuples. */
+  public *[Symbol.iterator](): Generator<MtAiValue, void, undefined> {
+    while (true) {
+      const tuple = this.read();
+
+      if (!tuple) {
+        return;
+      }
+
+      yield tuple;
+    }
+  }
+
+  #next(): MtAiValue | undefined {
+    const {value: tuple, done} = this.#seq.next();
+    if (done) {
+      return undefined;
+    }
+
+    return tuple;
path: src/decoder.ts
user: login: hildjj
body: Close enough.  We'll keep an eye on how this is used, and can always add a freeze if desired later.
created_at: 2025-05-09T19:46:43Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: hildjj
body: Also the same for integers expecting 8 more bytes, that only get 6, for example.  If we could add more data, would good things happen?  At that point, we have most of a TransformStream<Uint8Array, any>.
created_at: 2025-05-09T19:50:09Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: novemberborn
body: "Add more data" as in when it becomes available? I think we'll have already consumed the array header and preceding items, so we could resume from the last offset and take the next item.

At which point we're decoding one item at a time, since we'll no longer know we're in an array. So that should work.

May want a better error object though which includes offset details. Although it's also available in the last tuple.

If we don't change the behavior I think the `Sequence` documentation could do a better job at reflecting it. Ultimately "all data is expected to be present" would still hold, like it does for `decode()`.
created_at: 2025-05-09T20:13:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,249 @@
+import '../lib/types.js';
+import {Sequence} from '../lib/decoder.js';
+import assert from 'node:assert/strict';
+import {hexToU8} from '../lib/utils.js';
+import test from 'node:test';
+
+test('Sequence constructor', () => {
+  // Test with Uint8Array input
+  const seq1 = new Sequence(hexToU8('0102')); // Two integers: 1, 2
+  assert.ok(seq1, 'Should create Sequence from Uint8Array');
+
+  // Test with hex string and explicit encoding
+  const seq2 = new Sequence('0102', {encoding: 'hex'}); // Two integers: 1, 2
+  assert.ok(seq2, 'Should create Sequence from hex string');
+});
+
+test('Sequence read and peek', () => {
+  // Simple sequence of two integers: 1, 2
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  const seq = new Sequence(hexToU8('0102'));
+
+  // Peek first tuple
+  const firstPeek = seq.peek();
+  assert.ok(firstPeek, 'Peek should return a tuple');
+  assert.equal(firstPeek[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(firstPeek[2], 1, 'Value should be 1');
+
+  // Peek again should return the same tuple
+  const secondPeek = seq.peek();
+  assert.strictEqual(secondPeek, firstPeek, 'Multiple peeks should return same tuple');
+
+  // Read should return the same tuple and advance
+  const firstRead = seq.read();
+  assert.strictEqual(firstRead, firstPeek, 'Read should return same tuple as peek');
+
+  // Next read should get the second tuple
+  const secondRead = seq.read();
+  assert.ok(secondRead, 'Should read second tuple');
+  assert.equal(secondRead[0], 0, 'Major type should be 0 (positive integer)');
+  assert.equal(secondRead[2], 2, 'Value should be 2');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'Should return undefined when no more tuples');
+});
+
+test('Sequence iteration', () => {
+  // Sequence of three integers: 1, 2, 3
+  // 01 -- Unsigned: 1
+  // 02 -- Unsigned: 2
+  // 03 -- Unsigned: 3
+  const seq = new Sequence(hexToU8('010203'));
+
+  const values = [];
+  for (const tuple of seq) {
+    values.push(tuple[2]);
+  }
+
+  assert.deepEqual(values, [1, 2, 3], 'Should iterate through all values');
+
+  // Sequence should be exhausted
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined after iteration');
+});
+
+test('Sequence with complex CBOR', () => {
+  // Sequence containing: 1, "hello", [1, 2, 3], 3
+  // 01       -- Unsigned: 1
+  // 65       -- UTF8 (Length: 5): "hello"
+  //   68656c6c6f
+  // 83       -- Array (Length: 3 items)
+  //   01     --   [0] Unsigned: 1
+  //   02     --   [1] Unsigned: 2
+  //   03     --   [2] Unsigned: 3
+  // 03       -- Unsigned: 3
+  const hex = '016568656c6c6f8301020303';
+  const seq = new Sequence(hex, {encoding: 'hex'});
+
+  const tuples = Array.from(seq);
+
+  // We should get 7 tuples in total
+  assert.equal(tuples.length, 7, 'Should have 7 tuples for this sequence');
+
+  // First tuple: 1
+  assert.equal(tuples[0][0], 0, 'First tuple should be unsigned integer type (0)');
+  assert.equal(tuples[0][2], 1, 'First value should be 1');
+
+  // Second tuple: string "hello"
+  assert.equal(tuples[1][0], 3, 'Second major type should be 3 (text string)');
+  assert.equal(tuples[1][2], 'hello', 'String value should be "hello"');
+
+  // Third tuple: array header with length 3
+  assert.equal(tuples[2][0], 4, 'Third major type should be 4 (array)');
+  assert.equal(tuples[2][2], 3, 'Array length should be 3');
+
+  // Array elements
+  assert.equal(tuples[3][2], 1, 'First array element should be 1');
+  assert.equal(tuples[4][2], 2, 'Second array element should be 2');
+  assert.equal(tuples[5][2], 3, 'Third array element should be 3');
+
+  // Final value: 3
+  assert.equal(tuples[6][2], 3, 'Last value should be 3');
+});
+
+test('Sequence with break code', () => {
+  // Integer 1 followed by BREAK code
+  // 01 -- Unsigned: 1
+  // FF -- BREAK
+  const hex = '01FF';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First item should be readable
+  const firstTuple = seq.read();
+  assert.equal(firstTuple[2], 1, 'First item should be readable');
+
+  // Break code should be readable as a tuple
+  const breakTuple = seq.read();
+  assert.ok(breakTuple, 'Break code should be readable');
+  assert.equal(breakTuple[0], 7, 'Major type should be 7 for special values');
+  assert.equal(breakTuple[1], 31, 'Additional info should be 31 for break code');
+
+  // No more tuples
+  assert.equal(seq.read(), undefined, 'No more tuples after break code');
+});
+
+test('Incomplete array in sequence', () => {
+  // Array claiming length 2 but followed by a separate item
+  // 82    -- Array (Length: 2 items)
+  //   01  --   [0] Unsigned: 1
+  // 02    -- Unsigned: 2 (appears outside array)
+  const hex = '820102';
+  const seq = new Sequence(hexToU8(hex));
+
+  // First tuple should be the array header
+  const arrayHeader = seq.read();
+  assert.equal(arrayHeader[0], 4, 'First tuple should be array type');
+  assert.equal(arrayHeader[2], 2, 'Array length should be 2');
+
+  // Second tuple should be the first array item
+  const firstItem = seq.read();
+  assert.equal(firstItem[2], 1, 'First array item should be 1');
+
+  // Next tuple should be what looks like a second array item
+  // but is actually a separate sequence item
+  const nextItem = seq.read();
+  assert.equal(nextItem[2], 2, 'Next item should be 2');
+
+  // No more items
+  assert.equal(seq.read(), undefined, 'No more items after reading all bytes');
+});
+
+test('Empty sequence', () => {
+  // Empty byte sequence
+  const seq = new Sequence(hexToU8(''));
+  assert.equal(seq.peek(), undefined, 'Peek should return undefined for empty sequence');
+  assert.equal(seq.read(), undefined, 'Read should return undefined for empty sequence');
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 0, 'Should have no tuples for empty sequence');
+});
+
+test('Indefinite length items in sequence', () => {
+  // Indefinite length array with 2 items followed by break
+  // 9F    -- Array (Length: Indefinite)
+  //   01  --   [0] Unsigned: 1
+  //   02  --   [1] Unsigned: 2
+  //   FF  --   [2] BREAK
+  const hex = '9f0102ff';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 4, 'Should have 4 tuples for indef array sequence');
+
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][1], 31, 'Additional info should be 31 for indef length');
+
+  assert.equal(tuples[1][2], 1, 'First array item should be 1');
+  assert.equal(tuples[2][2], 2, 'Second array item should be 2');
+
+  assert.equal(tuples[3][0], 7, 'Fourth tuple should be special type');
+  assert.equal(tuples[3][1], 31, 'Additional info should be 31 for break');
+});
+
+test('Nested structures in sequence', () => {
+  // Nested array [[[1]]] followed by 2, 3
+  // 81      -- Array (Length: 1)
+  //   81    --   [0] Array (Length: 1)
+  //     81  --     [0] Array (Length: 1)
+  //       01 --       [0] Unsigned: 1
+  // 02      -- Unsigned: 2
+  // 03      -- Unsigned: 3
+  const hex = '818181010203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+  assert.equal(tuples.length, 6, 'Should have 6 tuples including nested structures');
+
+  // Check the nesting structure
+  assert.equal(tuples[0][0], 4, 'First tuple should be array type');
+  assert.equal(tuples[0][2], 1, 'Outer array length should be 1');
+
+  assert.equal(tuples[1][0], 4, 'Second tuple should be array type');
+  assert.equal(tuples[1][2], 1, 'Middle array length should be 1');
+
+  assert.equal(tuples[2][0], 4, 'Third tuple should be array type');
+  assert.equal(tuples[2][2], 1, 'Inner array length should be 1');
+
+  assert.equal(tuples[3][2], 1, 'Inner value should be 1');
+
+  // Then come the individual integers from the sequence
+  assert.equal(tuples[4][2], 2, 'Next sequence item should be 2');
+  assert.equal(tuples[5][2], 3, 'Last sequence item should be 3');
+});
+
+test('Tagged values in sequence', () => {
+  // Tag 0 (date/time string) with "2023-01-01", followed by 2, 3
+  // C0          -- Tag #0: (String Date)
+  //   6A        --   UTF8 (Length: 10): "2023-01-01"
+  //     323032332d30312d3031
+  // 02          -- Unsigned: 2
+  // 03          -- Unsigned: 3
+  const hex = 'c06a323032332d30312d30310203';
+  const seq = new Sequence(hexToU8(hex));
+
+  const tuples = Array.from(seq);
+
+  // First tuple should be tag 0
+  assert.equal(tuples[0][0], 6, 'First tuple should be tag type');
+  assert.equal(tuples[0][2], 0, 'Tag value should be 0 (date/time)');
+
+  // Second tuple should be the string
+  assert.equal(tuples[1][0], 3, 'Second tuple should be string type');
+  assert.equal(tuples[1][2], '2023-01-01', 'String value should be "2023-01-01"');
+
+  // The remaining sequence items
+  assert.equal(tuples[2][2], 2, 'Third item should be 2');
+  assert.equal(tuples[3][2], 3, 'Fourth item should be 3');
+});
+
+test('Incomplete CBOR data', () => {
+  // String claiming length 10 but only 5 bytes provided
+  // 6A          -- UTF8 (Length: 10)
+  //   48656c6c6f -- "Hello" (only 5 bytes)
+  const hex = '6a48656c6c6f';
+  const seq = new Sequence(hexToU8(hex));
+
+  // Should throw when trying to read past available bytes
+  assert.throws(() => seq.read(), {
+    message: /Unexpected end of stream/,
+  }, 'Should throw when reading incomplete string');
path: test/sequence.test.js
user: login: hildjj
body: Let's just do the doc change for now, add a test or two for the current expected behavior, land this PR, and decide if we have a use case we care about for real streaming.
created_at: 2025-05-09T20:21:59Z
author_association: OWNER

PR comments:
user: login: codecov-commenter
created_at: 2025-04-19T14:13:42Z
body: ## [Codecov](https://app.codecov.io/gh/hildjj/cbor2/pull/71?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joe+Hildebrand) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`8710694`)](https://app.codecov.io/gh/hildjj/cbor2/commit/871069490f522ae56af4335a6a21dcb3f9dad4f2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joe+Hildebrand) to head [(`64bcfe8`)](https://app.codecov.io/gh/hildjj/cbor2/commit/64bcfe8fca6e646ef9a7155bc7b9bd6036ca5f0d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joe+Hildebrand).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              main       #71    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           17        17            
  Lines         4237      4353   +116     
  Branches       613       621     +8     
==========================================
+ Hits          4237      4353   +116     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/hildjj/cbor2/pull/71?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joe+Hildebrand).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joe+Hildebrand).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: hildjj
created_at: 2025-04-19T14:35:46Z
body: This seems to work.  Can you please:

- [x] add Sequence to index.ts
- [x] add tests for all of the new code
- [x] ensure every public method has adequate TSDoc comments
- [x] add yourself to the contributors section of package.json
author_association: OWNER
user: login: hildjj
created_at: 2025-05-07T19:21:19Z
body: @novemberborn can you address the issues above?  I'm going to do a major release (to bump the required version of node to 20+), and would like to get this in.
author_association: OWNER
user: login: novemberborn
created_at: 2025-05-08T21:11:45Z
body: @hildjj this should do the trick.

Full disclosure, I worked with Copilot to put together the tests. Couple rounds of interaction and improvement, seems like it clarifies behavior in a bunch of edge cases and definitely covers the code changes. I haven't looked too closely at how the existing tests were put together, this PR may be too verbose for your liking.
author_association: CONTRIBUTOR
user: login: hildjj
created_at: 2025-05-08T21:44:21Z
body: Not going to lie, the AI involvement is a little bit of an issue for me, and I appreciate your telling me about it.  I'll think about it some more while you take a look at the CI errors.
author_association: OWNER
user: login: novemberborn
created_at: 2025-05-09T06:11:23Z
body: With tests, the value for me is in generating the inputs and the various edge cases that warrant evaluating. Tests themselves have very little creativity in them.

The models have a good understanding of CBOR (which you'd expect for a standard) so it was a great help. Though the output still needed a good amount of tweaking to make it work.

Re CI, looks like a build error with the exports, not sure why that didn't show in my IDE (or maybe I overlooked it). Will fix.
author_association: CONTRIBUTOR
user: login: hildjj
created_at: 2025-05-09T07:24:46Z
body: Agreed, I'm not terribly worried about the tests, as long as they do the job.
author_association: OWNER
user: login: novemberborn
created_at: 2025-05-09T11:33:22Z
body: > Re CI, looks like a build error with the exports, not sure why that didn't show in my IDE (or maybe I overlooked it). Will fix.

Ah, it's because the error was due to a type export that wasn't explicitly marked as such.
author_association: CONTRIBUTOR
user: login: hildjj
created_at: 2025-05-10T15:05:35Z
body: Thanks for doing all of this!
I'll take it from here and add the high-level support.  Release in the next few days, I hope.
author_association: OWNER
user: login: hildjj
created_at: 2025-05-10T19:36:19Z
body: See #78 for pending name change.
author_association: OWNER