Skip to content

Commit dff5c6c

Browse files
Add an NbsEncoder (#5)
Co-authored-by: Josephus Paye II <j.paye96@gmail.com>
1 parent 16f7b45 commit dff5c6c

18 files changed

+830
-130
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ npm install nbsdecoder.js --save
1414

1515
## Usage
1616

17+
This package contains classes for reading and writing nbs files.
18+
1719
The following example shows a typical usage pattern of creating a decoder with nbs file paths, and reading types, timestamps, and data packets.
1820

1921
```js
@@ -40,6 +42,36 @@ const packets = decoder.getPackets(start, [firstType]);
4042
console.log({ types, firstType, start, end, packets });
4143
```
4244

45+
The following example shows a typical usage pattern of creating an encoder with an nbs file path and writing a packet to it.
46+
47+
```js
48+
const { NbsEncoder } = require('nbsdecoder.js');
49+
50+
// Create an encoder instance
51+
const encoder = new NbsEncoder('/path/to/new/file.nbs');
52+
53+
// Create a packet to write to the file
54+
const packet = {
55+
// Timestamp that the packet was emitted
56+
timestamp: { seconds: 2000, nanos: 0 },
57+
58+
// The nuclear hash of the message type name (In this example 'message.Ping')
59+
type: Buffer.from('8ce1582fa0eadc84', 'hex'),
60+
61+
// Optional subtype of the packet
62+
subtype: 0,
63+
64+
// Bytes of the payload
65+
payload: Buffer.from('Message', 'utf8'),
66+
};
67+
68+
// Write the packet to the file
69+
encoder.write(packet);
70+
71+
// Close the file reader
72+
encoder.close();
73+
```
74+
4375
## API
4476

4577
See [`nbsdecoder.d.ts`](./nbsdecoder.d.ts) for API and types.

binding.gyp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"sources": [
66
"src/binding.cpp",
77
"src/Decoder.cpp",
8+
"src/Encoder.cpp",
9+
"src/Hash.cpp",
10+
"src/Packet.cpp",
11+
"src/Timestamp.cpp",
812
"src/xxhash/xxhash.c",
913
],
1014
"cflags": [],

nbsdecoder.d.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ export interface NbsPacket {
2828
payload?: Buffer;
2929
}
3030

31+
/**
32+
* An NBS packet to write to an NBS file
33+
*/
34+
export interface NbsWritePacket {
35+
/** The NBS packet timestamp */
36+
timestamp: NbsTimestamp;
37+
38+
/** The XX64 hash of the packet type */
39+
type: Buffer;
40+
41+
/** The packet subtype */
42+
subtype?: number;
43+
44+
/** The packet data */
45+
payload: Buffer;
46+
}
47+
3148
/**
3249
* A (type, subtype) pair that uniquely identifies a specific type of message
3350
*/
@@ -110,4 +127,40 @@ export declare class NbsDecoder {
110127
type?: NbsTypeSubtype | NbsTypeSubtype[],
111128
steps?: number
112129
): NbsTimestamp;
130+
131+
/**
132+
* Close the readers for the NBS files.
133+
*/
134+
public close(): void;
135+
}
136+
137+
export declare class NbsEncoder {
138+
/**
139+
* Create a new NbsEncoder instance
140+
*
141+
* @param path Absolute path of the nbs file to write to.
142+
*/
143+
public constructor(path: string);
144+
145+
/**
146+
* Write a packet to the nbs file.
147+
*
148+
* @param packet Packet to write to the file
149+
*/
150+
public write(packet: NbsWritePacket): number;
151+
152+
/**
153+
* Get the total number of bytes written to the nbs file.
154+
*/
155+
public getBytesWritten(): BigInt;
156+
157+
/**
158+
* Close the writers for both the nbs file and its index file.
159+
*/
160+
public close(): void;
161+
162+
/**
163+
* Returns true if the file writer to the nbs file is open.
164+
*/
165+
public isOpen(): boolean;
113166
}

nbsdecoder.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ const binding = bindings({
55
});
66

77
module.exports.NbsDecoder = binding.Decoder;
8+
module.exports.NbsEncoder = binding.Encoder;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
],
1616
"scripts": {
1717
"build": "node-gyp configure && node-gyp build",
18-
"test": "node tests/test.js",
18+
"test": "uvu tests",
1919
"format": "prettier --write \"*.{js,ts,json,md}\" \".github/**/*.{js,yml}\" \"tests/*.js\"",
2020
"format:check": "prettier --check \"*.{js,ts,json,md}\" \".github/**/*.{js,yml}\" \"tests/*.js\""
2121
},

src/Decoder.cpp

Lines changed: 31 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,31 @@
44
#include <napi.h>
55
#include <string>
66

7+
#include "Hash.hpp"
78
#include "IndexItem.hpp"
89
#include "Packet.hpp"
10+
#include "Timestamp.hpp"
911
#include "TypeSubtype.hpp"
10-
#include "xxhash/xxhash.h"
12+
1113
namespace nbs {
1214

1315
Napi::Object Decoder::Init(Napi::Env& env, Napi::Object& exports) {
14-
Napi::Function func =
15-
DefineClass(env,
16-
"Decoder",
17-
{
18-
InstanceMethod<&Decoder::GetAvailableTypes>(
19-
"getAvailableTypes",
20-
static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
21-
InstanceMethod<&Decoder::GetTimestampRange>(
22-
"getTimestampRange",
23-
static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
24-
InstanceMethod<&Decoder::GetPackets>(
25-
"getPackets",
26-
static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
27-
InstanceMethod<&Decoder::NextTimestamp>(
28-
"nextTimestamp",
29-
static_cast<napi_property_attributes>(napi_writable | napi_configurable)),
30-
});
16+
Napi::Function func = DefineClass(
17+
env,
18+
"Decoder",
19+
{
20+
InstanceMethod<&Decoder::GetAvailableTypes>(
21+
"getAvailableTypes",
22+
napi_property_attributes(napi_writable | napi_configurable)),
23+
InstanceMethod<&Decoder::GetTimestampRange>(
24+
"getTimestampRange",
25+
napi_property_attributes(napi_writable | napi_configurable)),
26+
InstanceMethod<&Decoder::GetPackets>("getPackets",
27+
napi_property_attributes(napi_writable | napi_configurable)),
28+
InstanceMethod<&Decoder::NextTimestamp>("nextTimestamp",
29+
napi_property_attributes(napi_writable | napi_configurable)),
30+
InstanceMethod<&Decoder::Close>("close", napi_property_attributes(napi_writable | napi_configurable)),
31+
});
3132

3233
Napi::FunctionReference* constructor = new Napi::FunctionReference();
3334

@@ -115,7 +116,7 @@ namespace nbs {
115116
for (size_t i = 0; i < availableTypes.size(); i++) {
116117
auto jsType = Napi::Object::New(env);
117118

118-
jsType.Set("type", this->HashToJsValue(availableTypes[i].type, env));
119+
jsType.Set("type", hash::ToJsValue(availableTypes[i].type, env));
119120
jsType.Set("subtype", Napi::Number::New(env, availableTypes[i].subtype));
120121

121122
jsTypes[i] = jsType;
@@ -147,8 +148,8 @@ namespace nbs {
147148
auto jsRange = Napi::Array::New(env, 2);
148149

149150
size_t i = 0;
150-
jsRange[i + 0] = this->TimestampToJsValue(range.first, env);
151-
jsRange[i + 1] = this->TimestampToJsValue(range.second, env);
151+
jsRange[i + 0] = timestamp::ToJsValue(range.first, env);
152+
jsRange[i + 1] = timestamp::ToJsValue(range.second, env);
152153

153154
return jsRange;
154155
}
@@ -158,7 +159,7 @@ namespace nbs {
158159

159160
uint64_t timestamp = 0;
160161
try {
161-
timestamp = this->TimestampFromJsValue(info[0], env);
162+
timestamp = timestamp::FromJsValue(info[0], env);
162163
}
163164
catch (const std::exception& ex) {
164165
Napi::TypeError::New(env, std::string("invalid type for argument `timestamp`: ") + ex.what())
@@ -220,7 +221,7 @@ namespace nbs {
220221
auto index_timestamp = this->index.nextTimestamp(timestamp, types, steps);
221222

222223
// Convert timestamp back to Napi format and return.
223-
auto new_timestamp = this->TimestampToJsValue(index_timestamp, env).As<Napi::Number>();
224+
auto new_timestamp = timestamp::ToJsValue(index_timestamp, env).As<Napi::Number>();
224225
return new_timestamp;
225226
}
226227

@@ -230,7 +231,7 @@ namespace nbs {
230231
uint64_t timestamp = 0;
231232

232233
try {
233-
timestamp = this->TimestampFromJsValue(info[0], env);
234+
timestamp = timestamp::FromJsValue(info[0], env);
234235
}
235236
catch (const std::exception& ex) {
236237
Napi::TypeError::New(env, std::string("invalid type for argument `timestamp`: ") + ex.what())
@@ -267,25 +268,11 @@ namespace nbs {
267268
return env.Undefined();
268269
}
269270

270-
auto packets = this->GetMatchingPackets(timestamp, types);
271-
271+
auto packets = this->GetMatchingPackets(timestamp, types);
272272
auto jsPackets = Napi::Array::New(env, packets.size());
273273

274274
for (size_t i = 0; i < packets.size(); i++) {
275-
auto jsPacket = Napi::Object::New(env);
276-
277-
jsPacket.Set("timestamp", this->TimestampToJsValue(packets[i].timestamp, env));
278-
jsPacket.Set("type", HashToJsValue(packets[i].type, env));
279-
jsPacket.Set("subtype", Napi::Number::New(env, packets[i].subtype));
280-
281-
if (packets[i].payload == nullptr) {
282-
jsPacket.Set("payload", env.Undefined());
283-
}
284-
else {
285-
jsPacket.Set("payload", Napi::Buffer<uint8_t>::Copy(env, packets[i].payload, packets[i].length));
286-
}
287-
288-
jsPackets[i] = jsPacket;
275+
jsPackets[i] = Packet::ToJsValue(packets[i], env);
289276
}
290277

291278
return jsPackets;
@@ -348,42 +335,6 @@ namespace nbs {
348335
return packet;
349336
}
350337

351-
uint64_t Decoder::HashFromJsValue(const Napi::Value& jsHash, const Napi::Env& env) {
352-
uint64_t hash = 0;
353-
354-
// If we have a string, apply XXHash to get the hash
355-
if (jsHash.IsString()) {
356-
std::string s = jsHash.As<Napi::String>().Utf8Value();
357-
hash = XXH64(s.c_str(), s.size(), 0x4e55436c);
358-
}
359-
// Otherwise try to interpret it as a buffer that contains the hash
360-
else if (jsHash.IsTypedArray()) {
361-
Napi::TypedArray typedArray = jsHash.As<Napi::TypedArray>();
362-
Napi::ArrayBuffer buffer = typedArray.ArrayBuffer();
363-
364-
uint8_t* data = reinterpret_cast<uint8_t*>(buffer.Data());
365-
uint8_t* start = data + typedArray.ByteOffset();
366-
uint8_t* end = start + typedArray.ByteLength();
367-
368-
if (std::distance(start, end) == 8) {
369-
std::memcpy(&hash, start, 8);
370-
}
371-
else {
372-
throw std::runtime_error("provided Buffer length is not 8");
373-
}
374-
}
375-
else {
376-
throw std::runtime_error("expected a string or Buffer");
377-
}
378-
379-
return hash;
380-
}
381-
382-
Napi::Value Decoder::HashToJsValue(const uint64_t& hash, const Napi::Env& env) {
383-
return Napi::Buffer<uint8_t>::Copy(env, reinterpret_cast<const uint8_t*>(&hash), sizeof(uint64_t))
384-
.As<Napi::Value>();
385-
}
386-
387338
TypeSubtype Decoder::TypeSubtypeFromJsValue(const Napi::Value& jsTypeSubtype, const Napi::Env& env) {
388339
if (!jsTypeSubtype.IsObject()) {
389340
throw std::runtime_error("expected object");
@@ -398,7 +349,7 @@ namespace nbs {
398349
uint64_t type = 0;
399350

400351
try {
401-
type = this->HashFromJsValue(typeSubtype.Get("type"), env);
352+
type = hash::FromJsValue(typeSubtype.Get("type"), env);
402353
}
403354
catch (const std::exception& ex) {
404355
throw std::runtime_error("invalid `.type`: " + std::string(ex.what()));
@@ -416,44 +367,10 @@ namespace nbs {
416367
return {type, subtype};
417368
}
418369

419-
uint64_t Decoder::TimestampFromJsValue(const Napi::Value& jsTimestamp, const Napi::Env& env) {
420-
uint64_t timestamp = 0;
421-
422-
if (jsTimestamp.IsNumber()) {
423-
timestamp = jsTimestamp.As<Napi::Number>().Int64Value();
424-
}
425-
else if (jsTimestamp.IsBigInt()) {
426-
bool lossless = true;
427-
timestamp = jsTimestamp.As<Napi::BigInt>().Uint64Value(&lossless);
370+
void Decoder::Close(const Napi::CallbackInfo& info) {
371+
for (auto& map : memoryMaps) {
372+
map.unmap();
428373
}
429-
else if (jsTimestamp.IsObject()) {
430-
auto ts = jsTimestamp.As<Napi::Object>();
431-
432-
if (!ts.Has("seconds") || !ts.Has("nanos")) {
433-
throw std::runtime_error("expected object with `seconds` and `nanos` keys");
434-
}
435-
436-
if (!ts.Get("seconds").IsNumber() || !ts.Get("nanos").IsNumber()) {
437-
throw std::runtime_error("`seconds` and `nanos` must be numbers");
438-
}
439-
440-
uint64_t seconds = ts.Get("seconds").As<Napi::Number>().Int64Value();
441-
uint64_t nanos = ts.Get("nanos").As<Napi::Number>().Int64Value();
442-
443-
timestamp = seconds * 1e9 + nanos;
444-
}
445-
else {
446-
throw std::runtime_error("expected positive number or BigInt or timestamp object");
447-
}
448-
449-
return timestamp;
450-
}
451-
452-
Napi::Value Decoder::TimestampToJsValue(const uint64_t& timestamp, const Napi::Env& env) {
453-
Napi::Object jsTimestamp = Napi::Object::New(env);
454-
jsTimestamp.Set("seconds", Napi::Number::New(env, timestamp / 1000000000L));
455-
jsTimestamp.Set("nanos", Napi::Number::New(env, timestamp % 1000000000L));
456-
return jsTimestamp;
457374
}
458375

459376
} // namespace nbs

src/Decoder.hpp

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ namespace nbs {
3232

3333
Napi::Value NextTimestamp(const Napi::CallbackInfo& info);
3434

35+
/**
36+
* Close the readers to this decoder's nbs files.
37+
*
38+
* @param info JS request. Does not require any arguments.
39+
*/
40+
void Close(const Napi::CallbackInfo& info);
41+
3542
private:
3643
/// Holds the index for the nbs files loaded in this decoder
3744
Index index;
@@ -46,20 +53,6 @@ namespace nbs {
4653
/// Read the packet for the given index item
4754
Packet Read(const IndexItemFile& item);
4855

49-
/// Create a XX64 hash from the given JS value, which could be a string or a buffer.
50-
/// String values will be hashed, and buffers will be interpreted as a XX64 hash.
51-
uint64_t HashFromJsValue(const Napi::Value& jsHash, const Napi::Env& env);
52-
53-
/// Convert the given XX64 hash to a JS Buffer value
54-
Napi::Value HashToJsValue(const uint64_t& hash, const Napi::Env& env);
55-
56-
/// Convert the given JS value to a timestamp in nanoseconds.
57-
/// The JS value can be a number, BigInt, or an object with `seconds` and `nanos` properties.
58-
uint64_t TimestampFromJsValue(const Napi::Value& jsTimestamp, const Napi::Env& env);
59-
60-
/// Convert the given timestamp to a JS object with `seconds` and `nanos` properties
61-
Napi::Value TimestampToJsValue(const uint64_t& timestamp, const Napi::Env& env);
62-
6356
/// Convert the given JS object with `type` and `subtype` properties to a TypeSubtype struct
6457
TypeSubtype TypeSubtypeFromJsValue(const Napi::Value& jsTypeSubtype, const Napi::Env& env);
6558
};

0 commit comments

Comments
 (0)