From f525cf6bff0ead31f671268b64882d0a8eef465b Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 11:32:48 +1100
Subject: [PATCH 01/19] Add `index` property and `getPacketByIndex`

---
 nbsdecoder.d.ts | 27 ++++++++++++++++++++
 src/Decoder.cpp | 68 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/Decoder.hpp |  6 +++++
 src/Index.hpp   | 13 ++++++++++
 4 files changed, 114 insertions(+)

diff --git a/nbsdecoder.d.ts b/nbsdecoder.d.ts
index 4cc186c..0282a9d 100644
--- a/nbsdecoder.d.ts
+++ b/nbsdecoder.d.ts
@@ -61,6 +61,14 @@ export interface NbsTypeSubtypeBuffer extends NbsTypeSubtype {
   type: Buffer;
 }
 
+/**
+ * The valid timestamps for a type subtype
+ */
+export interface TypeIndex {
+  typeSubType: NbsTypeSubtype;
+  timestamps: NbsTimestamp[];
+}
+
 /**
  * A decoder that can be used to read packets from NBS files
  */
@@ -73,6 +81,13 @@ export declare class NbsDecoder {
    */
   public constructor(paths: string[]);
 
+  /**
+   * The index data of this decoder. 
+   * 
+   * Contains the a list of packet timestamps for each typeSubType in the loaded nbs files.
+   */
+  public readonly index: TypeIndex[];
+
   /** Get a list of all the types present in the loaded nbs files */
   public getAvailableTypes(): NbsTypeSubtypeBuffer[];
 
@@ -114,6 +129,18 @@ export declare class NbsDecoder {
     types: NbsTypeSubtype[]
   ): NbsPacket[];
 
+  /**
+   * Get the packet of the given type at the given index in the loaded nbs file.
+   * 
+   * Returns either the packet at the index with the given type, or `undefined` if the given
+   * index is outside the range of packets for the type. Use `.index` to check the number
+   * of packets of each type.
+   * 
+   * @param index The index of the requested packet
+   * @param type The type of the requested packet
+   */
+  public getPacketByIndex(index: number, type: NbsTypeSubtype): NbsPacket | undefined;
+
   /**
    * Get the timestamp to seek to such that all messages of the given types are stepped by (n) steps
    *
diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index b4b8704..c01d671 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -25,9 +25,12 @@ namespace nbs {
                     napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::GetPackets>("getPackets",
                                                      napi_property_attributes(napi_writable | napi_configurable)),
+                InstanceMethod<&Decoder::GetPacketByIndex>("getPacketByIndex",
+                                                     napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::NextTimestamp>("nextTimestamp",
                                                         napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::Close>("close", napi_property_attributes(napi_writable | napi_configurable)),
+                InstanceAccessor<&Decoder::GetIndices>("index"),
             });
 
         Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -106,6 +109,39 @@ namespace nbs {
         }
     }
 
+    Napi::Value Decoder::GetIndices(const Napi::CallbackInfo& info) {
+        Napi::Env env = info.Env();
+
+        auto type_iterators = this->index.getAllTypeIterators();
+        auto indices = Napi::Array::New(env, type_iterators.size());
+
+        uint index = 0;
+
+        for (const auto& [typeSubtype, iterators] : type_iterators) {
+
+            auto typeIndices = Napi::Object::New(env);
+
+            auto jsType = Napi::Object::New(env);
+            jsType.Set("type", hash::ToJsValue(typeSubtype.type, env));
+            jsType.Set("subtype", Napi::Number::New(env, typeSubtype.subtype));
+
+            auto timestamps = Napi::Array::New(env, std::distance(iterators.first, iterators.second));
+
+            uint timestampIndex = 0;
+            for (auto it = iterators.first; it != iterators.second; it++) {
+                auto index = *it;
+                timestamps[timestampIndex++] = timestamp::ToJsValue(index.item.timestamp, env);
+            }
+
+            typeIndices.Set("typeSubType", jsType);
+            typeIndices.Set("timestamps", timestamps);
+
+            indices[index++] = typeIndices;
+        }
+
+        return indices;
+    }
+
     Napi::Value Decoder::GetAvailableTypes(const Napi::CallbackInfo& info) {
         Napi::Env env = info.Env();
 
@@ -278,6 +314,38 @@ namespace nbs {
         return jsPackets;
     }
 
+    Napi::Value Decoder::GetPacketByIndex(const Napi::CallbackInfo& info) {
+        Napi::Env env = info.Env();
+
+        uint64_t index = 0;
+        auto jsIndex = info[0];
+        if (jsIndex.IsNumber()) {
+            index = jsIndex.As<Napi::Number>().Int64Value();
+        } else {
+            Napi::TypeError::New(env, "expected positive number for `index`");
+            return env.Undefined();
+        }
+
+        TypeSubtype type;
+        try {
+            type = this->TypeSubtypeFromJsValue(info[1], env);
+        } catch (const std::exception& ex) {
+            Napi::TypeError::New(env, "invalid type for `type`: " + std::string(ex.what()));
+            return env.Undefined();
+        }
+
+        auto typeIterator = this->index.getIteratorForType(type);
+        auto packetLocation = typeIterator.first + index;
+
+        // If the index is out of range return undefined
+        if (packetLocation > typeIterator.second) {
+            return env.Undefined();
+        }
+
+        auto packet = this->Read(*packetLocation);
+        return Packet::ToJsValue(packet, env);
+    }
+
     std::vector<Packet> Decoder::GetMatchingPackets(const uint64_t& timestamp, const std::vector<TypeSubtype>& types) {
         std::vector<Packet> packets;
 
diff --git a/src/Decoder.hpp b/src/Decoder.hpp
index 5232b5f..87f89f5 100755
--- a/src/Decoder.hpp
+++ b/src/Decoder.hpp
@@ -18,6 +18,9 @@ namespace nbs {
         /// Constructor: takes a list of file paths from JS and constructs a Decoder
         Decoder(const Napi::CallbackInfo& info);
 
+        /// Create the object representing the index file
+        Napi::Value GetIndices(const Napi::CallbackInfo& info);
+
         /// Get a list of the available types in the nbs files of this decoder
         /// Returns a JS array of type subtype objects
         Napi::Value GetAvailableTypes(const Napi::CallbackInfo& info);
@@ -30,6 +33,9 @@ namespace nbs {
         /// Returns a JS array of packet objects
         Napi::Value GetPackets(const Napi::CallbackInfo& info);
 
+        /// Get the packet at the given index of the given type subtype
+        Napi::Value GetPacketByIndex(const Napi::CallbackInfo& info);
+
         Napi::Value NextTimestamp(const Napi::CallbackInfo& info);
 
         /**
diff --git a/src/Index.hpp b/src/Index.hpp
index 7210232..612fb8a 100755
--- a/src/Index.hpp
+++ b/src/Index.hpp
@@ -11,6 +11,8 @@
 #include "zstr/zstr.hpp"
 namespace nbs {
 
+    using IndexIterator = std::vector<IndexItemFile>::iterator;
+
     class Index {
     public:
         /// Empty default constructor
@@ -78,6 +80,17 @@ namespace nbs {
             }
         }
 
+        /// Get all iterators for all type subtype pairs in the index
+        std::vector<std::pair<TypeSubtype, std::pair<IndexIterator, IndexIterator>>> getAllTypeIterators() {
+            std::vector<std::pair<TypeSubtype, std::pair<IndexIterator, IndexIterator>>> result;
+
+            for (const auto& pair : this->typeMap) {
+                result.push_back(pair);
+            }
+
+            return result;
+        }
+
         /// Get the iterator range (begin, end) for the given type and subtype in the index
         std::pair<std::vector<IndexItemFile>::iterator, std::vector<IndexItemFile>::iterator> getIteratorForType(
             const TypeSubtype& type) {

From cc2275669d56f1eb895dc37aa63c679297dc8726 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 14:54:29 +1100
Subject: [PATCH 02/19] Minor fixes + add tests

---
 src/Decoder.cpp       |  6 ++--
 tests/test_decoder.js | 69 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 72 insertions(+), 3 deletions(-)

diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index c01d671..9b97aff 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -322,7 +322,7 @@ namespace nbs {
         if (jsIndex.IsNumber()) {
             index = jsIndex.As<Napi::Number>().Int64Value();
         } else {
-            Napi::TypeError::New(env, "expected positive number for `index`");
+            Napi::TypeError::New(env, "invalid type for argument `index`: expected integer").ThrowAsJavaScriptException();
             return env.Undefined();
         }
 
@@ -330,7 +330,7 @@ namespace nbs {
         try {
             type = this->TypeSubtypeFromJsValue(info[1], env);
         } catch (const std::exception& ex) {
-            Napi::TypeError::New(env, "invalid type for `type`: " + std::string(ex.what()));
+            Napi::TypeError::New(env, "invalid type for argument `type`: " + std::string(ex.what())).ThrowAsJavaScriptException();
             return env.Undefined();
         }
 
@@ -338,7 +338,7 @@ namespace nbs {
         auto packetLocation = typeIterator.first + index;
 
         // If the index is out of range return undefined
-        if (packetLocation > typeIterator.second) {
+        if (packetLocation >= typeIterator.second) {
             return env.Undefined();
         }
 
diff --git a/tests/test_decoder.js b/tests/test_decoder.js
index 0157fd2..35fa3ce 100755
--- a/tests/test_decoder.js
+++ b/tests/test_decoder.js
@@ -438,6 +438,75 @@ test('NbsDecoder.getPackets() returns the last packet of each type when given a
   });
 });
 
+test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
+  assert.throws(
+    () => {
+      decoder.getPacketByIndex();
+    },
+    /invalid type for argument `index`: expected integer/,
+    'NbsDecoder.getPacketByIndex throws for missing `index` argument',
+  );
+
+  assert.throws(
+    () => {
+      decoder.getPacketByIndex(true);
+    },
+    /invalid type for argument `index`: expected integer/,
+    'NbsDecoder.getPacketByIndex throws for incorrect `index` argument type',
+  );
+
+  assert.throws(
+    () => {
+      decoder.getPacketByIndex(0);
+    },
+    /invalid type for argument `type`: expected object/,
+    'NbsDecoder.getPacketByIndex throws for missing `type` argument',
+  )
+
+  assert.throws(
+    () => {
+      decoder.getPacketByIndex(0, {});
+    },
+    /invalid type for argument `type`: expected object with `type` and `subtype` keys/,
+    'NbsDecoder.getPacketByIndex throws for invalid `type` argument',
+  )
+});
+
+test('NbsDecoder.getPacketByIndex() returns the correct packets in the valid range', () => {
+
+  const packet0 = decoder.getPacketByIndex(0, { type: pingType, subtype: 0});
+  const packet1 = decoder.getPacketByIndex(1, { type: pingType, subtype: 0});
+  const packetLast = decoder.getPacketByIndex(299, { type: pingType, subtype: 0});
+
+  assert.equal(packet0, {
+    timestamp: { seconds: 1000, nanos: 0 },
+    type: pingType,
+    subtype: 0,
+    payload: Buffer.from('ping.0', 'utf8'),
+  });
+
+  assert.equal(packet1, {
+    timestamp: { seconds: 1003, nanos: 0 },
+    type: pingType,
+    subtype: 0,
+    payload: Buffer.from('ping.1', 'utf8'),
+  });
+
+  assert.equal(packetLast, {
+    timestamp: { seconds: 1897, nanos: 0 },
+    type: pingType,
+    subtype: 0,
+    payload: Buffer.from('ping.699', 'utf8'),
+  });
+});
+
+test('NbsDecoder.getPacketByIndex() returns undefined for indexes greater than the valid range', () => {
+  assert.equal(decoder.getPacketByIndex(300, { type: pingType, subtype: 0}), undefined);
+  assert.equal(decoder.getPacketByIndex(300, { type: pongType, subtype: 0}), undefined);
+  assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 100}), undefined);
+  assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 200}), undefined);
+});
+
 const multiTypeNextTimestampArray = [
   { type: pongType, subtype: 0 },
   { type: pingType, subtype: 0 },

From 2eb5ac45b51ce8b63c99dd1f2656adfa53e77979 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:05:22 +1100
Subject: [PATCH 03/19] Add more tests + formatting

---
 nbsdecoder.d.ts       |  8 +++----
 tests/test_decoder.js | 55 ++++++++++++++++++++++++++++++++-----------
 2 files changed, 45 insertions(+), 18 deletions(-)

diff --git a/nbsdecoder.d.ts b/nbsdecoder.d.ts
index 0282a9d..62ec2c5 100644
--- a/nbsdecoder.d.ts
+++ b/nbsdecoder.d.ts
@@ -82,8 +82,8 @@ export declare class NbsDecoder {
   public constructor(paths: string[]);
 
   /**
-   * The index data of this decoder. 
-   * 
+   * The index data of this decoder.
+   *
    * Contains the a list of packet timestamps for each typeSubType in the loaded nbs files.
    */
   public readonly index: TypeIndex[];
@@ -131,11 +131,11 @@ export declare class NbsDecoder {
 
   /**
    * Get the packet of the given type at the given index in the loaded nbs file.
-   * 
+   *
    * Returns either the packet at the index with the given type, or `undefined` if the given
    * index is outside the range of packets for the type. Use `.index` to check the number
    * of packets of each type.
-   * 
+   *
    * @param index The index of the requested packet
    * @param type The type of the requested packet
    */
diff --git a/tests/test_decoder.js b/tests/test_decoder.js
index 35fa3ce..fa3ac70 100755
--- a/tests/test_decoder.js
+++ b/tests/test_decoder.js
@@ -79,6 +79,34 @@ test('NbsDecoder constructor throws for invalid arguments', () => {
   );
 });
 
+test('NbsDecoder.index contains the correct number of timestamps', () => {
+  const pingIndices = decoder.index.find((index) => pingType.equals(index.typeSubType.type));
+  assert.equal(pingIndices.timestamps.length, 300);
+
+  const pongIndices = decoder.index.find((index) => pongType.equals(index.typeSubType.type));
+  assert.equal(pongIndices.timestamps.length, 300);
+
+  const pang100Indices = decoder.index.find(
+    (index) => pangType.equals(index.typeSubType.type) && index.typeSubType.subtype === 100
+  );
+  assert.equal(pang100Indices.timestamps.length, 150);
+
+  const pang200Indices = decoder.index.find(
+    (index) => pangType.equals(index.typeSubType.type) && index.typeSubType.subtype === 200
+  );
+  assert.equal(pang200Indices.timestamps.length, 150);
+});
+
+test('NbsDecoder.index contains the correct packet timestamps', () => {
+  const pingIndices = decoder.index.find((index) => pingType.equals(index.typeSubType.type));
+
+  assert.equal(pingIndices.timestamps[0], { seconds: 1000, nanos: 0 });
+  assert.equal(pingIndices.timestamps[1], { seconds: 1003, nanos: 0 });
+
+  const lastIndex = pingIndices.timestamps.length - 1;
+  assert.equal(pingIndices.timestamps[lastIndex], { seconds: 1897, nanos: 0 });
+});
+
 test('NbsDecoder.getAvailableTypes() returns a list of all the types available in the nbs files', () => {
   const types = decoder.getAvailableTypes();
 
@@ -444,7 +472,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex();
     },
     /invalid type for argument `index`: expected integer/,
-    'NbsDecoder.getPacketByIndex throws for missing `index` argument',
+    'NbsDecoder.getPacketByIndex throws for missing `index` argument'
   );
 
   assert.throws(
@@ -452,7 +480,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex(true);
     },
     /invalid type for argument `index`: expected integer/,
-    'NbsDecoder.getPacketByIndex throws for incorrect `index` argument type',
+    'NbsDecoder.getPacketByIndex throws for incorrect `index` argument type'
   );
 
   assert.throws(
@@ -460,23 +488,22 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex(0);
     },
     /invalid type for argument `type`: expected object/,
-    'NbsDecoder.getPacketByIndex throws for missing `type` argument',
-  )
+    'NbsDecoder.getPacketByIndex throws for missing `type` argument'
+  );
 
   assert.throws(
     () => {
       decoder.getPacketByIndex(0, {});
     },
     /invalid type for argument `type`: expected object with `type` and `subtype` keys/,
-    'NbsDecoder.getPacketByIndex throws for invalid `type` argument',
-  )
+    'NbsDecoder.getPacketByIndex throws for invalid `type` argument'
+  );
 });
 
 test('NbsDecoder.getPacketByIndex() returns the correct packets in the valid range', () => {
-
-  const packet0 = decoder.getPacketByIndex(0, { type: pingType, subtype: 0});
-  const packet1 = decoder.getPacketByIndex(1, { type: pingType, subtype: 0});
-  const packetLast = decoder.getPacketByIndex(299, { type: pingType, subtype: 0});
+  const packet0 = decoder.getPacketByIndex(0, { type: pingType, subtype: 0 });
+  const packet1 = decoder.getPacketByIndex(1, { type: pingType, subtype: 0 });
+  const packetLast = decoder.getPacketByIndex(299, { type: pingType, subtype: 0 });
 
   assert.equal(packet0, {
     timestamp: { seconds: 1000, nanos: 0 },
@@ -501,10 +528,10 @@ test('NbsDecoder.getPacketByIndex() returns the correct packets in the valid ran
 });
 
 test('NbsDecoder.getPacketByIndex() returns undefined for indexes greater than the valid range', () => {
-  assert.equal(decoder.getPacketByIndex(300, { type: pingType, subtype: 0}), undefined);
-  assert.equal(decoder.getPacketByIndex(300, { type: pongType, subtype: 0}), undefined);
-  assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 100}), undefined);
-  assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 200}), undefined);
+  assert.equal(decoder.getPacketByIndex(300, { type: pingType, subtype: 0 }), undefined);
+  assert.equal(decoder.getPacketByIndex(300, { type: pongType, subtype: 0 }), undefined);
+  assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 100 }), undefined);
+  assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 200 }), undefined);
 });
 
 const multiTypeNextTimestampArray = [

From 4095b6b26d2e6e24cccb60e30cf41302353b2962 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:12:19 +1100
Subject: [PATCH 04/19] Formatting

---
 src/Decoder.cpp | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index 9b97aff..8a21fb6 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -26,7 +26,7 @@ namespace nbs {
                 InstanceMethod<&Decoder::GetPackets>("getPackets",
                                                      napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::GetPacketByIndex>("getPacketByIndex",
-                                                     napi_property_attributes(napi_writable | napi_configurable)),
+                                                           napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::NextTimestamp>("nextTimestamp",
                                                         napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::Close>("close", napi_property_attributes(napi_writable | napi_configurable)),
@@ -113,7 +113,7 @@ namespace nbs {
         Napi::Env env = info.Env();
 
         auto type_iterators = this->index.getAllTypeIterators();
-        auto indices = Napi::Array::New(env, type_iterators.size());
+        auto indices        = Napi::Array::New(env, type_iterators.size());
 
         uint index = 0;
 
@@ -129,7 +129,7 @@ namespace nbs {
 
             uint timestampIndex = 0;
             for (auto it = iterators.first; it != iterators.second; it++) {
-                auto index = *it;
+                auto index                   = *it;
                 timestamps[timestampIndex++] = timestamp::ToJsValue(index.item.timestamp, env);
             }
 
@@ -318,23 +318,27 @@ namespace nbs {
         Napi::Env env = info.Env();
 
         uint64_t index = 0;
-        auto jsIndex = info[0];
+        auto jsIndex   = info[0];
         if (jsIndex.IsNumber()) {
             index = jsIndex.As<Napi::Number>().Int64Value();
-        } else {
-            Napi::TypeError::New(env, "invalid type for argument `index`: expected integer").ThrowAsJavaScriptException();
+        }
+        else {
+            Napi::TypeError::New(env, "invalid type for argument `index`: expected integer")
+                .ThrowAsJavaScriptException();
             return env.Undefined();
         }
 
         TypeSubtype type;
         try {
             type = this->TypeSubtypeFromJsValue(info[1], env);
-        } catch (const std::exception& ex) {
-            Napi::TypeError::New(env, "invalid type for argument `type`: " + std::string(ex.what())).ThrowAsJavaScriptException();
+        }
+        catch (const std::exception& ex) {
+            Napi::TypeError::New(env, "invalid type for argument `type`: " + std::string(ex.what()))
+                .ThrowAsJavaScriptException();
             return env.Undefined();
         }
 
-        auto typeIterator = this->index.getIteratorForType(type);
+        auto typeIterator   = this->index.getIteratorForType(type);
         auto packetLocation = typeIterator.first + index;
 
         // If the index is out of range return undefined

From 7652ef8fc115f1a568ebf16d7ea1614abcdfbfa4 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:25:42 +1100
Subject: [PATCH 05/19] Update workflows

---
 .github/workflows/node.js.yml | 33 ++++++++++++++++++++++++++-------
 1 file changed, 26 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 0512fda..1d6d77a 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -9,24 +9,43 @@ on:
   pull_request:
     branches: [main]
 
+# Ensure that only one instance of the workflow is run at a time for each branch/tag.
+# Jobs currently running on the branch/tag will be cancelled when new commits are pushed.
+# See https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency.
+concurrency:
+  # `github.workflow` is the workflow name, `github.ref` is the current branch/tag identifier
+  group: ${{ format('{0}:{1}', github.workflow, github.ref) }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
 jobs:
-  build-and-test:
+  build:
     runs-on: ${{ matrix.os }}
+    continue-on-error: true
 
     strategy:
       matrix:
-        os: [ubuntu-latest, windows-2019, macos-latest]
+        os: [ubuntu-latest, windows-latest, macos-latest]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
-        node-version: [14.x, 16.x, 18.x]
+        node-version: [12.x, 14.x, 16.x, 18.x, 20.x]
 
     steps:
-      - uses: actions/checkout@v3
+      - name: Checkout Code
+        uses: actions/checkout@v3
 
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
 
-      - run: yarn install
-      - run: yarn test
+      - name: Install node-gyp
+        run: npm install -g node-gyp
+
+      - name: Install dependencies
+        run: yarn ci
+
+      - name: Build
+        run: yarn build
+
+      - name: Run tests
+        run: yarn test

From c697ca652a6fdbe4b1470cbd477d2d303bcede1b Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:27:15 +1100
Subject: [PATCH 06/19] oop

---
 .github/workflows/node.js.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 1d6d77a..87ba280 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -42,7 +42,7 @@ jobs:
         run: npm install -g node-gyp
 
       - name: Install dependencies
-        run: yarn ci
+        run: yarn
 
       - name: Build
         run: yarn build

From 88b184e9a7f04d262c194b9a9aa983ff85d99bf9 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:31:57 +1100
Subject: [PATCH 07/19] Remove old node

---
 .github/workflows/node.js.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 87ba280..56222f8 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -26,7 +26,7 @@ jobs:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
-        node-version: [12.x, 14.x, 16.x, 18.x, 20.x]
+        node-version: [14.x, 16.x, 18.x, 20.x]
 
     steps:
       - name: Checkout Code

From 1b7b781bf2bf9be019736d4721e7523615f689e9 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:45:59 +1100
Subject: [PATCH 08/19] Maybe fix

---
 .github/workflows/node.js.yml | 4 ++--
 package.json                  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 56222f8..64b6a16 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -26,7 +26,7 @@ jobs:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
-        node-version: [14.x, 16.x, 18.x, 20.x]
+        node-version: [18.x, 20.x, 21.x]
 
     steps:
       - name: Checkout Code
@@ -39,7 +39,7 @@ jobs:
           cache: 'yarn'
 
       - name: Install node-gyp
-        run: npm install -g node-gyp
+        run: npm install -g node-gyp@10.0.1
 
       - name: Install dependencies
         run: yarn
diff --git a/package.json b/package.json
index 6c50e74..82c14f7 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "main": "nbsdecoder.js",
   "types": "nbsdecoder.d.ts",
   "engines": {
-    "node": ">=14.14.0"
+    "node": ">=18.17.0"
   },
   "files": [
     "nbsdecoder.js",

From 87ca2fc1b08844b3598db909a03bb35988e86b96 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 15:52:20 +1100
Subject: [PATCH 09/19] More build fixes

---
 .github/workflows/formatting.yml |  4 ++--
 src/Decoder.cpp                  | 12 +++++++-----
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml
index 7ec141d..de4be4c 100644
--- a/.github/workflows/formatting.yml
+++ b/.github/workflows/formatting.yml
@@ -15,10 +15,10 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
-      - name: Use Node.js 16
+      - name: Use Node.js 21
         uses: actions/setup-node@v3
         with:
-          node-version: '16.x'
+          node-version: '21.x'
           cache: 'yarn'
 
       - run: yarn install
diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index 8a21fb6..21c3105 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -115,19 +115,21 @@ namespace nbs {
         auto type_iterators = this->index.getAllTypeIterators();
         auto indices        = Napi::Array::New(env, type_iterators.size());
 
-        uint index = 0;
+        uint64_t index = 0;
 
-        for (const auto& [typeSubtype, iterators] : type_iterators) {
+        for (const auto& typeIterator : type_iterators) {
+            auto typeSubType = typeIterator.first;
+            auto iterators   = typeIterator.second;
 
             auto typeIndices = Napi::Object::New(env);
 
             auto jsType = Napi::Object::New(env);
-            jsType.Set("type", hash::ToJsValue(typeSubtype.type, env));
-            jsType.Set("subtype", Napi::Number::New(env, typeSubtype.subtype));
+            jsType.Set("type", hash::ToJsValue(typeSubType.type, env));
+            jsType.Set("subtype", Napi::Number::New(env, typeSubType.subtype));
 
             auto timestamps = Napi::Array::New(env, std::distance(iterators.first, iterators.second));
 
-            uint timestampIndex = 0;
+            uint64_t timestampIndex = 0;
             for (auto it = iterators.first; it != iterators.second; it++) {
                 auto index                   = *it;
                 timestamps[timestampIndex++] = timestamp::ToJsValue(index.item.timestamp, env);

From 911f6c02c1dbd3f3807d17ad5f35c0f68783313e Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 12 Feb 2024 16:05:44 +1100
Subject: [PATCH 10/19] Update node versions and workflows

---
 .github/workflows/formatting.yml |  4 ++--
 .github/workflows/node.js.yml    | 33 +++++++++++++++++++++++++-------
 package.json                     |  2 +-
 3 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml
index 7ec141d..de4be4c 100644
--- a/.github/workflows/formatting.yml
+++ b/.github/workflows/formatting.yml
@@ -15,10 +15,10 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
-      - name: Use Node.js 16
+      - name: Use Node.js 21
         uses: actions/setup-node@v3
         with:
-          node-version: '16.x'
+          node-version: '21.x'
           cache: 'yarn'
 
       - run: yarn install
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 0512fda..64b6a16 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -9,24 +9,43 @@ on:
   pull_request:
     branches: [main]
 
+# Ensure that only one instance of the workflow is run at a time for each branch/tag.
+# Jobs currently running on the branch/tag will be cancelled when new commits are pushed.
+# See https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency.
+concurrency:
+  # `github.workflow` is the workflow name, `github.ref` is the current branch/tag identifier
+  group: ${{ format('{0}:{1}', github.workflow, github.ref) }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
 jobs:
-  build-and-test:
+  build:
     runs-on: ${{ matrix.os }}
+    continue-on-error: true
 
     strategy:
       matrix:
-        os: [ubuntu-latest, windows-2019, macos-latest]
+        os: [ubuntu-latest, windows-latest, macos-latest]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
-        node-version: [14.x, 16.x, 18.x]
+        node-version: [18.x, 20.x, 21.x]
 
     steps:
-      - uses: actions/checkout@v3
+      - name: Checkout Code
+        uses: actions/checkout@v3
 
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: ${{ matrix.node-version }}
           cache: 'yarn'
 
-      - run: yarn install
-      - run: yarn test
+      - name: Install node-gyp
+        run: npm install -g node-gyp@10.0.1
+
+      - name: Install dependencies
+        run: yarn
+
+      - name: Build
+        run: yarn build
+
+      - name: Run tests
+        run: yarn test
diff --git a/package.json b/package.json
index 6c50e74..82c14f7 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "main": "nbsdecoder.js",
   "types": "nbsdecoder.d.ts",
   "engines": {
-    "node": ">=14.14.0"
+    "node": ">=18.17.0"
   },
   "files": [
     "nbsdecoder.js",

From dc00c944ab28e1751477843dbeb5bbb1387cfd06 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Thu, 15 Feb 2024 10:33:55 +1100
Subject: [PATCH 11/19] Change to node 20 for formatting

---
 .github/workflows/formatting.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml
index de4be4c..1f89dbb 100644
--- a/.github/workflows/formatting.yml
+++ b/.github/workflows/formatting.yml
@@ -15,10 +15,10 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
-      - name: Use Node.js 21
+      - name: Install Node.js
         uses: actions/setup-node@v3
         with:
-          node-version: '21.x'
+          node-version: '20.x'
           cache: 'yarn'
 
       - run: yarn install

From d1266c93b56f10413fe2293349da04f1d008b275 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Thu, 15 Feb 2024 10:37:28 +1100
Subject: [PATCH 12/19] Update npm publish node version

---
 .github/workflows/publish-to-npm.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/publish-to-npm.yml b/.github/workflows/publish-to-npm.yml
index 8a81fe7..a2e8279 100644
--- a/.github/workflows/publish-to-npm.yml
+++ b/.github/workflows/publish-to-npm.yml
@@ -15,7 +15,7 @@ jobs:
       # Installs Node and sets up up the .npmrc file to publish to npm
       - uses: actions/setup-node@v3
         with:
-          node-version: '16.x'
+          node-version: '20.x'
           registry-url: 'https://registry.npmjs.org'
           cache: 'yarn'
 

From 65cc0ba52ea120cd3638971d7cb4cdfd52111c28 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Thu, 15 Feb 2024 14:29:07 +1100
Subject: [PATCH 13/19] Apply suggestions to get index

---
 src/Decoder.cpp | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index 21c3105..25472cb 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -340,15 +340,15 @@ namespace nbs {
             return env.Undefined();
         }
 
-        auto typeIterator   = this->index.getIteratorForType(type);
-        auto packetLocation = typeIterator.first + index;
+        auto typeIterator = this->index.getIteratorForType(type);
 
         // If the index is out of range return undefined
-        if (packetLocation >= typeIterator.second) {
+        if (std::distance(typeIterator.first, typeIterator.second) < index) {
             return env.Undefined();
         }
 
-        auto packet = this->Read(*packetLocation);
+        auto packetLocation = std::next(typeIterator.first, index);
+        auto packet         = this->Read(*packetLocation);
         return Packet::ToJsValue(packet, env);
     }
 

From 3cbb935e7e2fa3e954981b14016eeb1be38ab44f Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Thu, 15 Feb 2024 16:25:06 +1100
Subject: [PATCH 14/19] Replace `.index` property with `getTypeIndex`

---
 nbsdecoder.d.ts       |  6 +++---
 src/Decoder.cpp       | 48 +++++++++++++++++--------------------------
 src/Decoder.hpp       |  6 +++---
 src/Index.hpp         |  8 +++++---
 tests/test_decoder.js | 39 ++++++++++++++++++-----------------
 5 files changed, 50 insertions(+), 57 deletions(-)

diff --git a/nbsdecoder.d.ts b/nbsdecoder.d.ts
index 62ec2c5..3e1c269 100644
--- a/nbsdecoder.d.ts
+++ b/nbsdecoder.d.ts
@@ -82,11 +82,11 @@ export declare class NbsDecoder {
   public constructor(paths: string[]);
 
   /**
-   * The index data of this decoder.
+   * Get all the timestamps of a specified message type subtype.
    *
-   * Contains the a list of packet timestamps for each typeSubType in the loaded nbs files.
+   * @param type A type subtype object to get the timestamps for
    */
-  public readonly index: TypeIndex[];
+  public getTypeIndex(type: NbsTypeSubtype): NbsTimestamp[];
 
   /** Get a list of all the types present in the loaded nbs files */
   public getAvailableTypes(): NbsTypeSubtypeBuffer[];
diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index 25472cb..a3f3f1f 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -23,6 +23,8 @@ namespace nbs {
                 InstanceMethod<&Decoder::GetTimestampRange>(
                     "getTimestampRange",
                     napi_property_attributes(napi_writable | napi_configurable)),
+                InstanceMethod<&Decoder::GetTypeIndex>("getTypeIndex",
+                                                       napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::GetPackets>("getPackets",
                                                      napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::GetPacketByIndex>("getPacketByIndex",
@@ -30,7 +32,6 @@ namespace nbs {
                 InstanceMethod<&Decoder::NextTimestamp>("nextTimestamp",
                                                         napi_property_attributes(napi_writable | napi_configurable)),
                 InstanceMethod<&Decoder::Close>("close", napi_property_attributes(napi_writable | napi_configurable)),
-                InstanceAccessor<&Decoder::GetIndices>("index"),
             });
 
         Napi::FunctionReference* constructor = new Napi::FunctionReference();
@@ -109,39 +110,28 @@ namespace nbs {
         }
     }
 
-    Napi::Value Decoder::GetIndices(const Napi::CallbackInfo& info) {
+    Napi::Value Decoder::GetTypeIndex(const Napi::CallbackInfo& info) {
         Napi::Env env = info.Env();
 
-        auto type_iterators = this->index.getAllTypeIterators();
-        auto indices        = Napi::Array::New(env, type_iterators.size());
-
-        uint64_t index = 0;
-
-        for (const auto& typeIterator : type_iterators) {
-            auto typeSubType = typeIterator.first;
-            auto iterators   = typeIterator.second;
-
-            auto typeIndices = Napi::Object::New(env);
-
-            auto jsType = Napi::Object::New(env);
-            jsType.Set("type", hash::ToJsValue(typeSubType.type, env));
-            jsType.Set("subtype", Napi::Number::New(env, typeSubType.subtype));
-
-            auto timestamps = Napi::Array::New(env, std::distance(iterators.first, iterators.second));
-
-            uint64_t timestampIndex = 0;
-            for (auto it = iterators.first; it != iterators.second; it++) {
-                auto index                   = *it;
-                timestamps[timestampIndex++] = timestamp::ToJsValue(index.item.timestamp, env);
-            }
+        TypeSubtype type;
+        try {
+            type = this->TypeSubtypeFromJsValue(info[0], env);
+        }
+        catch (const std::exception& ex) {
+            Napi::TypeError::New(env, "invalid type for argument `type`: " + std::string(ex.what()))
+                .ThrowAsJavaScriptException();
+            return env.Undefined();
+        }
 
-            typeIndices.Set("typeSubType", jsType);
-            typeIndices.Set("timestamps", timestamps);
+        auto typeIterator = this->index.getIteratorForType(type);
+        auto timestamps   = Napi::Array::New(env, std::distance(typeIterator.first, typeIterator.second));
 
-            indices[index++] = typeIndices;
+        uint64_t idx = 0;
+        for (auto it = typeIterator.first; it != typeIterator.second; it++) {
+            timestamps[idx++] = timestamp::ToJsValue(it->item.timestamp, env);
         }
 
-        return indices;
+        return timestamps;
     }
 
     Napi::Value Decoder::GetAvailableTypes(const Napi::CallbackInfo& info) {
@@ -343,7 +333,7 @@ namespace nbs {
         auto typeIterator = this->index.getIteratorForType(type);
 
         // If the index is out of range return undefined
-        if (std::distance(typeIterator.first, typeIterator.second) < index) {
+        if (std::distance(typeIterator.first, typeIterator.second) <= index) {
             return env.Undefined();
         }
 
diff --git a/src/Decoder.hpp b/src/Decoder.hpp
index 87f89f5..a89694b 100755
--- a/src/Decoder.hpp
+++ b/src/Decoder.hpp
@@ -18,13 +18,13 @@ namespace nbs {
         /// Constructor: takes a list of file paths from JS and constructs a Decoder
         Decoder(const Napi::CallbackInfo& info);
 
-        /// Create the object representing the index file
-        Napi::Value GetIndices(const Napi::CallbackInfo& info);
-
         /// Get a list of the available types in the nbs files of this decoder
         /// Returns a JS array of type subtype objects
         Napi::Value GetAvailableTypes(const Napi::CallbackInfo& info);
 
+        /// Get the timestamps for a specified type subtype
+        Napi::Value GetTypeIndex(const Napi::CallbackInfo& info);
+
         /// Get a list of the available types in the nbs files of this decoder
         /// Returns a JS array with two elements: the start timestamp object and the end timestamp object
         Napi::Value GetTimestampRange(const Napi::CallbackInfo& info);
diff --git a/src/Index.hpp b/src/Index.hpp
index 612fb8a..91ab712 100755
--- a/src/Index.hpp
+++ b/src/Index.hpp
@@ -92,9 +92,11 @@ namespace nbs {
         }
 
         /// Get the iterator range (begin, end) for the given type and subtype in the index
-        std::pair<std::vector<IndexItemFile>::iterator, std::vector<IndexItemFile>::iterator> getIteratorForType(
-            const TypeSubtype& type) {
-            return this->typeMap[type];
+        std::pair<nbs::IndexIterator, nbs::IndexIterator> getIteratorForType(const TypeSubtype& type) {
+            // Check that the type actually exists, otherwise the type map will add the given type
+            // with an empty iterator
+            auto typeExists = (this->typeMap.count(type) > 0);
+            return typeExists ? this->typeMap[type] : std::make_pair((nbs::IndexIterator) 0, (nbs::IndexIterator) 0);
         }
 
         /// Get a list of iterator ranges (begin, end) for the given list of type and subtype in the index
diff --git a/tests/test_decoder.js b/tests/test_decoder.js
index fa3ac70..0cf4419 100755
--- a/tests/test_decoder.js
+++ b/tests/test_decoder.js
@@ -79,32 +79,33 @@ test('NbsDecoder constructor throws for invalid arguments', () => {
   );
 });
 
-test('NbsDecoder.index contains the correct number of timestamps', () => {
-  const pingIndices = decoder.index.find((index) => pingType.equals(index.typeSubType.type));
-  assert.equal(pingIndices.timestamps.length, 300);
+test('NbsDecoder.getTypeIndex returns the correct number of timestamps', () => {
+  const pingIndices = decoder.getTypeIndex({ type: pingType, subtype: 0 });
+  assert.equal(pingIndices.length, 300);
 
-  const pongIndices = decoder.index.find((index) => pongType.equals(index.typeSubType.type));
-  assert.equal(pongIndices.timestamps.length, 300);
+  const pongIndices = decoder.getTypeIndex({ type: pongType, subtype: 0 });
+  assert.equal(pongIndices.length, 300);
 
-  const pang100Indices = decoder.index.find(
-    (index) => pangType.equals(index.typeSubType.type) && index.typeSubType.subtype === 100
-  );
-  assert.equal(pang100Indices.timestamps.length, 150);
+  const pang100Indices = decoder.getTypeIndex({ type: pangType, subtype: 100 });
+  assert.equal(pang100Indices.length, 150);
 
-  const pang200Indices = decoder.index.find(
-    (index) => pangType.equals(index.typeSubType.type) && index.typeSubType.subtype === 200
-  );
-  assert.equal(pang200Indices.timestamps.length, 150);
+  const pang200Indices = decoder.getTypeIndex({ type: pangType, subtype: 200 });
+  assert.equal(pang200Indices.length, 150);
+});
+
+test('NbsDecoder.getTypeIndex returns an empty array for types not in the decoder', () => {
+  const fakePacketIndices = decoder.getTypeIndex({ type: 'fakeIndex', subtype: 0 });
+  assert.equal(fakePacketIndices.length, 0);
 });
 
-test('NbsDecoder.index contains the correct packet timestamps', () => {
-  const pingIndices = decoder.index.find((index) => pingType.equals(index.typeSubType.type));
+test('NbsDecoder.getTypeIndex the correct packet timestamps', () => {
+  const pingIndices = decoder.getTypeIndex({ type: pingType, subtype: 0 });
 
-  assert.equal(pingIndices.timestamps[0], { seconds: 1000, nanos: 0 });
-  assert.equal(pingIndices.timestamps[1], { seconds: 1003, nanos: 0 });
+  assert.equal(pingIndices[0], { seconds: 1000, nanos: 0 });
+  assert.equal(pingIndices[1], { seconds: 1003, nanos: 0 });
 
-  const lastIndex = pingIndices.timestamps.length - 1;
-  assert.equal(pingIndices.timestamps[lastIndex], { seconds: 1897, nanos: 0 });
+  const lastIndex = pingIndices.length - 1;
+  assert.equal(pingIndices[lastIndex], { seconds: 1897, nanos: 0 });
 });
 
 test('NbsDecoder.getAvailableTypes() returns a list of all the types available in the nbs files', () => {

From e8be2eda01acef79bff23cc63a0e153b91b9f804 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 19 Feb 2024 14:39:25 +1100
Subject: [PATCH 15/19] Apply review suggestions

---
 nbsdecoder.d.ts       | 21 +++++++++------------
 src/Decoder.cpp       | 24 ++++++++++++++----------
 src/Decoder.hpp       |  2 +-
 src/Index.hpp         | 16 ++++------------
 tests/test_decoder.js | 22 +++++++++++++---------
 5 files changed, 41 insertions(+), 44 deletions(-)

diff --git a/nbsdecoder.d.ts b/nbsdecoder.d.ts
index 3e1c269..f945247 100644
--- a/nbsdecoder.d.ts
+++ b/nbsdecoder.d.ts
@@ -61,9 +61,7 @@ export interface NbsTypeSubtypeBuffer extends NbsTypeSubtype {
   type: Buffer;
 }
 
-/**
- * The valid timestamps for a type subtype
- */
+/** The timestamps for a type subtype */
 export interface TypeIndex {
   typeSubType: NbsTypeSubtype;
   timestamps: NbsTimestamp[];
@@ -84,9 +82,9 @@ export declare class NbsDecoder {
   /**
    * Get all the timestamps of a specified message type subtype.
    *
-   * @param type A type subtype object to get the timestamps for
+   * @param typeSubtype A type subtype object to get the timestamps for
    */
-  public getTypeIndex(type: NbsTypeSubtype): NbsTimestamp[];
+  public getTypeIndex(typeSubtype: NbsTypeSubtype): NbsTimestamp[];
 
   /** Get a list of all the types present in the loaded nbs files */
   public getAvailableTypes(): NbsTypeSubtypeBuffer[];
@@ -97,9 +95,9 @@ export declare class NbsDecoder {
   /**
    * Get the timestamp range (start, end) for all packets of the given type in the loaded nbs files
    *
-   * @param type A type subtype object to get the timestamp range for
+   * @param typeSubtype A type subtype object to get the timestamp range for
    */
-  public getTimestampRange(type: NbsTypeSubtype): [NbsTimestamp, NbsTimestamp];
+  public getTimestampRange(typeSubtype: NbsTypeSubtype): [NbsTimestamp, NbsTimestamp];
 
   /**
    * Get the packets at or before the given timestamp for all types in the loaded nbs files
@@ -133,13 +131,12 @@ export declare class NbsDecoder {
    * Get the packet of the given type at the given index in the loaded nbs file.
    *
    * Returns either the packet at the index with the given type, or `undefined` if the given
-   * index is outside the range of packets for the type. Use `.index` to check the number
-   * of packets of each type.
+   * index is outside the range of packets for the type.
    *
-   * @param index The index of the requested packet
-   * @param type The type of the requested packet
+   * @param index       The index of the requested packet
+   * @param typeSubtype The type of the requested packet
    */
-  public getPacketByIndex(index: number, type: NbsTypeSubtype): NbsPacket | undefined;
+  public getPacketByIndex(index: number, typeSubtype: NbsTypeSubtype): NbsPacket | undefined;
 
   /**
    * Get the timestamp to seek to such that all messages of the given types are stepped by (n) steps
diff --git a/src/Decoder.cpp b/src/Decoder.cpp
index a3f3f1f..d2eebda 100755
--- a/src/Decoder.cpp
+++ b/src/Decoder.cpp
@@ -113,17 +113,17 @@ namespace nbs {
     Napi::Value Decoder::GetTypeIndex(const Napi::CallbackInfo& info) {
         Napi::Env env = info.Env();
 
-        TypeSubtype type;
+        TypeSubtype typeSubtype;
         try {
-            type = this->TypeSubtypeFromJsValue(info[0], env);
+            typeSubtype = this->TypeSubtypeFromJsValue(info[0], env);
         }
         catch (const std::exception& ex) {
-            Napi::TypeError::New(env, "invalid type for argument `type`: " + std::string(ex.what()))
+            Napi::TypeError::New(env, "invalid type for argument `typeSubtype`: " + std::string(ex.what()))
                 .ThrowAsJavaScriptException();
             return env.Undefined();
         }
 
-        auto typeIterator = this->index.getIteratorForType(type);
+        auto typeIterator = this->index.getIteratorForType(typeSubtype);
         auto timestamps   = Napi::Array::New(env, std::distance(typeIterator.first, typeIterator.second));
 
         uint64_t idx = 0;
@@ -309,8 +309,8 @@ namespace nbs {
     Napi::Value Decoder::GetPacketByIndex(const Napi::CallbackInfo& info) {
         Napi::Env env = info.Env();
 
-        uint64_t index = 0;
-        auto jsIndex   = info[0];
+        int64_t index = 0;
+        auto jsIndex  = info[0];
         if (jsIndex.IsNumber()) {
             index = jsIndex.As<Napi::Number>().Int64Value();
         }
@@ -320,17 +320,21 @@ namespace nbs {
             return env.Undefined();
         }
 
-        TypeSubtype type;
+        if (index < 0) {
+            return env.Undefined();
+        }
+
+        TypeSubtype typeSubtype;
         try {
-            type = this->TypeSubtypeFromJsValue(info[1], env);
+            typeSubtype = this->TypeSubtypeFromJsValue(info[1], env);
         }
         catch (const std::exception& ex) {
-            Napi::TypeError::New(env, "invalid type for argument `type`: " + std::string(ex.what()))
+            Napi::TypeError::New(env, "invalid type for argument `typeSubtype`: " + std::string(ex.what()))
                 .ThrowAsJavaScriptException();
             return env.Undefined();
         }
 
-        auto typeIterator = this->index.getIteratorForType(type);
+        auto typeIterator = this->index.getIteratorForType(typeSubtype);
 
         // If the index is out of range return undefined
         if (std::distance(typeIterator.first, typeIterator.second) <= index) {
diff --git a/src/Decoder.hpp b/src/Decoder.hpp
index a89694b..fd0abe4 100755
--- a/src/Decoder.hpp
+++ b/src/Decoder.hpp
@@ -22,7 +22,7 @@ namespace nbs {
         /// Returns a JS array of type subtype objects
         Napi::Value GetAvailableTypes(const Napi::CallbackInfo& info);
 
-        /// Get the timestamps for a specified type subtype
+        /// Get all the timestamps of a specified message type subtype from the index
         Napi::Value GetTypeIndex(const Napi::CallbackInfo& info);
 
         /// Get a list of the available types in the nbs files of this decoder
diff --git a/src/Index.hpp b/src/Index.hpp
index 91ab712..c2e1fef 100755
--- a/src/Index.hpp
+++ b/src/Index.hpp
@@ -3,6 +3,7 @@
 
 #include <iostream>
 #include <map>
+#include <optional>
 #include <sys/stat.h>
 #include <vector>
 
@@ -80,23 +81,14 @@ namespace nbs {
             }
         }
 
-        /// Get all iterators for all type subtype pairs in the index
-        std::vector<std::pair<TypeSubtype, std::pair<IndexIterator, IndexIterator>>> getAllTypeIterators() {
-            std::vector<std::pair<TypeSubtype, std::pair<IndexIterator, IndexIterator>>> result;
-
-            for (const auto& pair : this->typeMap) {
-                result.push_back(pair);
-            }
-
-            return result;
-        }
-
         /// Get the iterator range (begin, end) for the given type and subtype in the index
         std::pair<nbs::IndexIterator, nbs::IndexIterator> getIteratorForType(const TypeSubtype& type) {
             // Check that the type actually exists, otherwise the type map will add the given type
             // with an empty iterator
             auto typeExists = (this->typeMap.count(type) > 0);
-            return typeExists ? this->typeMap[type] : std::make_pair((nbs::IndexIterator) 0, (nbs::IndexIterator) 0);
+            if (typeExists) {
+                return this->typeMap[type];
+            }
         }
 
         /// Get a list of iterator ranges (begin, end) for the given list of type and subtype in the index
diff --git a/tests/test_decoder.js b/tests/test_decoder.js
index 0cf4419..d1a87b3 100755
--- a/tests/test_decoder.js
+++ b/tests/test_decoder.js
@@ -79,7 +79,7 @@ test('NbsDecoder constructor throws for invalid arguments', () => {
   );
 });
 
-test('NbsDecoder.getTypeIndex returns the correct number of timestamps', () => {
+test('NbsDecoder.getTypeIndex() returns the correct index for the given type subtype', () => {
   const pingIndices = decoder.getTypeIndex({ type: pingType, subtype: 0 });
   assert.equal(pingIndices.length, 300);
 
@@ -93,12 +93,12 @@ test('NbsDecoder.getTypeIndex returns the correct number of timestamps', () => {
   assert.equal(pang200Indices.length, 150);
 });
 
-test('NbsDecoder.getTypeIndex returns an empty array for types not in the decoder', () => {
+test('NbsDecoder.getTypeIndex() returns an empty array for types not in the decoder', () => {
   const fakePacketIndices = decoder.getTypeIndex({ type: 'fakeIndex', subtype: 0 });
   assert.equal(fakePacketIndices.length, 0);
 });
 
-test('NbsDecoder.getTypeIndex the correct packet timestamps', () => {
+test('NbsDecoder.getTypeIndex() returns the correct index with the correct packet timestamps', () => {
   const pingIndices = decoder.getTypeIndex({ type: pingType, subtype: 0 });
 
   assert.equal(pingIndices[0], { seconds: 1000, nanos: 0 });
@@ -473,7 +473,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex();
     },
     /invalid type for argument `index`: expected integer/,
-    'NbsDecoder.getPacketByIndex throws for missing `index` argument'
+    'NbsDecoder.getPacketByIndex() throws for missing `index` argument'
   );
 
   assert.throws(
@@ -481,7 +481,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex(true);
     },
     /invalid type for argument `index`: expected integer/,
-    'NbsDecoder.getPacketByIndex throws for incorrect `index` argument type'
+    'NbsDecoder.getPacketByIndex() throws for incorrect `index` argument type'
   );
 
   assert.throws(
@@ -489,7 +489,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex(0);
     },
     /invalid type for argument `type`: expected object/,
-    'NbsDecoder.getPacketByIndex throws for missing `type` argument'
+    'NbsDecoder.getPacketByIndex() throws for missing `type` argument'
   );
 
   assert.throws(
@@ -497,11 +497,11 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
       decoder.getPacketByIndex(0, {});
     },
     /invalid type for argument `type`: expected object with `type` and `subtype` keys/,
-    'NbsDecoder.getPacketByIndex throws for invalid `type` argument'
+    'NbsDecoder.getPacketByIndex() throws for invalid `type` argument'
   );
 });
 
-test('NbsDecoder.getPacketByIndex() returns the correct packets in the valid range', () => {
+test('NbsDecoder.getPacketByIndex() returns the correct packet for a valid index', () => {
   const packet0 = decoder.getPacketByIndex(0, { type: pingType, subtype: 0 });
   const packet1 = decoder.getPacketByIndex(1, { type: pingType, subtype: 0 });
   const packetLast = decoder.getPacketByIndex(299, { type: pingType, subtype: 0 });
@@ -528,7 +528,11 @@ test('NbsDecoder.getPacketByIndex() returns the correct packets in the valid ran
   });
 });
 
-test('NbsDecoder.getPacketByIndex() returns undefined for indexes greater than the valid range', () => {
+test('NbsDecoder.getPacketByIndex() returns undefined for indices outside the valid range', () => {
+  // Negative indices
+  assert.equal(decoder.getPacketByIndex(-1, { type: pingType, subtype: 0 }), undefined);
+
+  // Indices one above the max
   assert.equal(decoder.getPacketByIndex(300, { type: pingType, subtype: 0 }), undefined);
   assert.equal(decoder.getPacketByIndex(300, { type: pongType, subtype: 0 }), undefined);
   assert.equal(decoder.getPacketByIndex(150, { type: pangType, subtype: 100 }), undefined);

From e06b497f7ec96f27ef1a353fcb14805c87dd2674 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 19 Feb 2024 14:39:36 +1100
Subject: [PATCH 16/19] Maybe fix builds

---
 src/Index.hpp | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Index.hpp b/src/Index.hpp
index c2e1fef..2dc8737 100755
--- a/src/Index.hpp
+++ b/src/Index.hpp
@@ -85,10 +85,11 @@ namespace nbs {
         std::pair<nbs::IndexIterator, nbs::IndexIterator> getIteratorForType(const TypeSubtype& type) {
             // Check that the type actually exists, otherwise the type map will add the given type
             // with an empty iterator
-            auto typeExists = (this->typeMap.count(type) > 0);
-            if (typeExists) {
+            auto typeExists = (this->typeMap.find(type) != this->typeMap.end());
+            if (typeExists == true) {
                 return this->typeMap[type];
             }
+            return std::make_pair((std::vector<IndexItemFile>::iterator) 0, (std::vector<IndexItemFile>::iterator) 0);
         }
 
         /// Get a list of iterator ranges (begin, end) for the given list of type and subtype in the index

From ef499e3b1920bfd8728f3f083dca7631bfad555b Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 19 Feb 2024 14:41:07 +1100
Subject: [PATCH 17/19] Fix test error messages

---
 tests/test_decoder.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/test_decoder.js b/tests/test_decoder.js
index d1a87b3..df940a1 100755
--- a/tests/test_decoder.js
+++ b/tests/test_decoder.js
@@ -488,7 +488,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
     () => {
       decoder.getPacketByIndex(0);
     },
-    /invalid type for argument `type`: expected object/,
+    /invalid type for argument `typeSubtype`: expected object/,
     'NbsDecoder.getPacketByIndex() throws for missing `type` argument'
   );
 
@@ -496,7 +496,7 @@ test('NbsDecoder.getPacketByIndex() throws for invalid arguments', () => {
     () => {
       decoder.getPacketByIndex(0, {});
     },
-    /invalid type for argument `type`: expected object with `type` and `subtype` keys/,
+    /invalid type for argument `typeSubtype`: expected object with `type` and `subtype` keys/,
     'NbsDecoder.getPacketByIndex() throws for invalid `type` argument'
   );
 });

From aad6e9ff4d1354028877991204dbb269ff66817a Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Mon, 19 Feb 2024 14:50:17 +1100
Subject: [PATCH 18/19] Maybe fix again

---
 src/Index.hpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Index.hpp b/src/Index.hpp
index 2dc8737..dffa7cb 100755
--- a/src/Index.hpp
+++ b/src/Index.hpp
@@ -89,7 +89,7 @@ namespace nbs {
             if (typeExists == true) {
                 return this->typeMap[type];
             }
-            return std::make_pair((std::vector<IndexItemFile>::iterator) 0, (std::vector<IndexItemFile>::iterator) 0);
+            return {};
         }
 
         /// Get a list of iterator ranges (begin, end) for the given list of type and subtype in the index

From 056a9060c287d9f5c058ce27fd977e8f517b3b97 Mon Sep 17 00:00:00 2001
From: Adrian Wann <awann@4tel.com.au>
Date: Tue, 20 Feb 2024 10:07:18 +1100
Subject: [PATCH 19/19] Remove unused import

---
 src/Index.hpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Index.hpp b/src/Index.hpp
index dffa7cb..19b6400 100755
--- a/src/Index.hpp
+++ b/src/Index.hpp
@@ -3,7 +3,6 @@
 
 #include <iostream>
 #include <map>
-#include <optional>
 #include <sys/stat.h>
 #include <vector>
 
