feat: add SharedArrayBuffer (#1688)

diff --git a/doc/README.md b/doc/README.md
index bad1a5c..e5e24df 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -60,6 +60,7 @@
         - [ClassPropertyDescriptor](class_property_descriptor.md)
     - [Buffer](buffer.md)
     - [ArrayBuffer](array_buffer.md)
+    - [SharedArrayBuffer](shared_array_buffer.md)
     - [TypedArray](typed_array.md)
       - [TypedArrayOf](typed_array_of.md)
     - [DataView](dataview.md)
diff --git a/doc/shared_array_buffer.md b/doc/shared_array_buffer.md
new file mode 100644
index 0000000..872dbb4
--- /dev/null
+++ b/doc/shared_array_buffer.md
@@ -0,0 +1,65 @@
+# SharedArrayBuffer
+
+Class `Napi::SharedArrayBuffer` inherits from class [`Napi::Object`][].
+
+The `Napi::SharedArrayBuffer` class corresponds to the
+[JavaScript `SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)
+class.
+
+**NOTE**: The support for `Napi::SharedArrayBuffer` is only available when using
+`NAPI_EXPERIMENTAL` and building against Node.js headers that support this
+feature.
+
+## Methods
+
+### New
+
+Allocates a new `Napi::SharedArrayBuffer` instance with a given length.
+
+```cpp
+static Napi::SharedArrayBuffer Napi::SharedArrayBuffer::New(napi_env env, size_t byteLength);
+```
+
+- `[in] env`: The environment in which to create the `Napi::SharedArrayBuffer`
+  instance.
+- `[in] byteLength`: The length to be allocated, in bytes.
+
+Returns a new `Napi::SharedArrayBuffer` instance.
+
+### Constructor
+
+Initializes an empty instance of the `Napi::SharedArrayBuffer` class.
+
+```cpp
+Napi::SharedArrayBuffer::SharedArrayBuffer();
+```
+
+### Constructor
+
+Initializes a wrapper instance of an existing `Napi::SharedArrayBuffer` object.
+
+```cpp
+Napi::SharedArrayBuffer::SharedArrayBuffer(napi_env env, napi_value value);
+```
+
+- `[in] env`: The environment in which to create the `Napi::SharedArrayBuffer`
+  instance.
+- `[in] value`: The `Napi::SharedArrayBuffer` reference to wrap.
+
+### ByteLength
+
+```cpp
+size_t Napi::SharedArrayBuffer::ByteLength() const;
+```
+
+Returns the length of the wrapped data, in bytes.
+
+### Data
+
+```cpp
+void* Napi::SharedArrayBuffer::Data() const;
+```
+
+Returns a pointer the wrapped data.
+
+[`Napi::Object`]: ./object.md
diff --git a/doc/value.md b/doc/value.md
index f19532f..f61a36e 100644
--- a/doc/value.md
+++ b/doc/value.md
@@ -268,6 +268,19 @@
 Returns `true` if the underlying value is a JavaScript `Napi::Promise` or
 `false` otherwise.
 
+### IsSharedArrayBuffer
+
+```cpp
+bool Napi::Value::IsSharedArrayBuffer() const;
+```
+
+Returns `true` if the underlying value is a JavaScript
+`Napi::IsSharedArrayBuffer` or `false` otherwise.
+
+**NOTE**: The support for `Napi::SharedArrayBuffer` is only available when using
+`NAPI_EXPERIMENTAL` and building against Node.js headers that support this
+feature.
+
 ### IsString
 
 ```cpp
diff --git a/napi-inl.h b/napi-inl.h
index 54651c1..94574fc 100644
--- a/napi-inl.h
+++ b/napi-inl.h
@@ -934,6 +934,19 @@
   return Type() == napi_external;
 }
 
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+inline bool Value::IsSharedArrayBuffer() const {
+  if (IsEmpty()) {
+    return false;
+  }
+
+  bool result;
+  napi_status status = node_api_is_sharedarraybuffer(_env, _value, &result);
+  NAPI_THROW_IF_FAILED(_env, status, false);
+  return result;
+}
+#endif
+
 template <typename T>
 inline T Value::As() const {
 #ifdef NODE_ADDON_API_ENABLE_TYPE_CHECK_ON_AS
@@ -2068,6 +2081,55 @@
   return result;
 }
 
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+////////////////////////////////////////////////////////////////////////////////
+// SharedArrayBuffer class
+////////////////////////////////////////////////////////////////////////////////
+
+inline SharedArrayBuffer::SharedArrayBuffer() : Object() {}
+
+inline SharedArrayBuffer::SharedArrayBuffer(napi_env env, napi_value value)
+    : Object(env, value) {}
+
+inline void SharedArrayBuffer::CheckCast(napi_env env, napi_value value) {
+  NAPI_CHECK(value != nullptr, "SharedArrayBuffer::CheckCast", "empty value");
+
+  bool result;
+  napi_status status = node_api_is_sharedarraybuffer(env, value, &result);
+  NAPI_CHECK(status == napi_ok,
+             "SharedArrayBuffer::CheckCast",
+             "node_api_is_sharedarraybuffer failed");
+  NAPI_CHECK(
+      result, "SharedArrayBuffer::CheckCast", "value is not sharedarraybuffer");
+}
+
+inline SharedArrayBuffer SharedArrayBuffer::New(napi_env env,
+                                                size_t byteLength) {
+  napi_value value;
+  void* data;
+  napi_status status =
+      node_api_create_sharedarraybuffer(env, byteLength, &data, &value);
+  NAPI_THROW_IF_FAILED(env, status, SharedArrayBuffer());
+
+  return SharedArrayBuffer(env, value);
+}
+
+inline void* SharedArrayBuffer::Data() {
+  void* data;
+  napi_status status = napi_get_arraybuffer_info(_env, _value, &data, nullptr);
+  NAPI_THROW_IF_FAILED(_env, status, nullptr);
+  return data;
+}
+
+inline size_t SharedArrayBuffer::ByteLength() {
+  size_t length;
+  napi_status status =
+      napi_get_arraybuffer_info(_env, _value, nullptr, &length);
+  NAPI_THROW_IF_FAILED(_env, status, 0);
+  return length;
+}
+#endif  // NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+
 ////////////////////////////////////////////////////////////////////////////////
 // ArrayBuffer class
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/napi.h b/napi.h
index ba0e134..013a911 100644
--- a/napi.h
+++ b/napi.h
@@ -543,6 +543,9 @@
   bool IsDataView() const;    ///< Tests if a value is a JavaScript data view.
   bool IsBuffer() const;      ///< Tests if a value is a Node buffer.
   bool IsExternal() const;  ///< Tests if a value is a pointer to external data.
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+  bool IsSharedArrayBuffer() const;
+#endif
 
   /// Casts to another type of `Napi::Value`, when the actual type is known or
   /// assumed.
@@ -1202,6 +1205,21 @@
 };
 #endif  // NODE_ADDON_API_CPP_EXCEPTIONS
 
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+class SharedArrayBuffer : public Object {
+ public:
+  SharedArrayBuffer();
+  SharedArrayBuffer(napi_env env, napi_value value);
+
+  static SharedArrayBuffer New(napi_env env, size_t byteLength);
+
+  static void CheckCast(napi_env env, napi_value value);
+
+  void* Data();
+  size_t ByteLength();
+};
+#endif
+
 /// A JavaScript array buffer value.
 class ArrayBuffer : public Object {
  public:
diff --git a/test/binding.cc b/test/binding.cc
index 9e5aaaa..fa651cc 100644
--- a/test/binding.cc
+++ b/test/binding.cc
@@ -64,6 +64,7 @@
 Object InitTypedThreadSafeFunctionUnref(Env env);
 Object InitTypedThreadSafeFunction(Env env);
 #endif
+Object InitSharedArrayBuffer(Env env);
 Object InitSymbol(Env env);
 Object InitTypedArray(Env env);
 Object InitGlobalObject(Env env);
@@ -140,6 +141,7 @@
   exports.Set("promise", InitPromise(env));
   exports.Set("run_script", InitRunScript(env));
   exports.Set("symbol", InitSymbol(env));
+  exports.Set("sharedarraybuffer", InitSharedArrayBuffer(env));
 #if (NAPI_VERSION > 3)
   exports.Set("threadsafe_function_ctx", InitThreadSafeFunctionCtx(env));
   exports.Set("threadsafe_function_exception",
@@ -194,6 +196,12 @@
       "isExperimental",
       Napi::Boolean::New(env, NAPI_VERSION == NAPI_VERSION_EXPERIMENTAL));
 
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+  exports.Set("hasSharedArrayBuffer", Napi::Boolean::New(env, true));
+#else
+  exports.Set("hasSharedArrayBuffer", Napi::Boolean::New(env, false));
+#endif
+
   return exports;
 }
 
diff --git a/test/binding.gyp b/test/binding.gyp
index 8ee391f..9ff334b 100644
--- a/test/binding.gyp
+++ b/test/binding.gyp
@@ -54,6 +54,7 @@
         'object/subscript_operator.cc',
         'promise.cc',
         'run_script.cc',
+        'shared_array_buffer.cc',
         'symbol.cc',
         'threadsafe_function/threadsafe_function_ctx.cc',
         'threadsafe_function/threadsafe_function_exception.cc',
diff --git a/test/shared_array_buffer.cc b/test/shared_array_buffer.cc
new file mode 100644
index 0000000..57f6649
--- /dev/null
+++ b/test/shared_array_buffer.cc
@@ -0,0 +1,104 @@
+#include "napi.h"
+
+using namespace Napi;
+
+namespace {
+
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+Value TestIsSharedArrayBuffer(const CallbackInfo& info) {
+  if (info.Length() < 1) {
+    Error::New(info.Env(), "Wrong number of arguments")
+        .ThrowAsJavaScriptException();
+    return Value();
+  }
+
+  return Boolean::New(info.Env(), info[0].IsSharedArrayBuffer());
+}
+
+Value TestCreateSharedArrayBuffer(const CallbackInfo& info) {
+  if (info.Length() < 1) {
+    Error::New(info.Env(), "Wrong number of arguments")
+        .ThrowAsJavaScriptException();
+    return Value();
+  } else if (!info[0].IsNumber()) {
+    Error::New(info.Env(),
+               "Wrong type of arguments. Expects a number as first argument.")
+        .ThrowAsJavaScriptException();
+    return Value();
+  }
+
+  auto byte_length = info[0].As<Number>().Uint32Value();
+  if (byte_length == 0) {
+    Error::New(info.Env(),
+               "Invalid byte length. Expects a non-negative integer.")
+        .ThrowAsJavaScriptException();
+    return Value();
+  }
+
+  return SharedArrayBuffer::New(info.Env(), byte_length);
+}
+
+Value TestGetSharedArrayBufferInfo(const CallbackInfo& info) {
+  if (info.Length() < 1) {
+    Error::New(info.Env(), "Wrong number of arguments")
+        .ThrowAsJavaScriptException();
+    return Value();
+  } else if (!info[0].IsSharedArrayBuffer()) {
+    Error::New(info.Env(),
+               "Wrong type of arguments. Expects a SharedArrayBuffer as first "
+               "argument.")
+        .ThrowAsJavaScriptException();
+    return Value();
+  }
+
+  auto byte_length = info[0].As<SharedArrayBuffer>().ByteLength();
+
+  return Number::New(info.Env(), byte_length);
+}
+
+Value TestSharedArrayBufferData(const CallbackInfo& info) {
+  if (info.Length() < 1) {
+    Error::New(info.Env(), "Wrong number of arguments")
+        .ThrowAsJavaScriptException();
+    return Value();
+  } else if (!info[0].IsSharedArrayBuffer()) {
+    Error::New(info.Env(),
+               "Wrong type of arguments. Expects a SharedArrayBuffer as first "
+               "argument.")
+        .ThrowAsJavaScriptException();
+    return Value();
+  }
+
+  auto byte_length = info[0].As<SharedArrayBuffer>().ByteLength();
+  void* data = info[0].As<SharedArrayBuffer>().Data();
+
+  if (byte_length > 0 && data != nullptr) {
+    uint8_t* bytes = static_cast<uint8_t*>(data);
+    for (size_t i = 0; i < byte_length; i++) {
+      bytes[i] = i % 256;
+    }
+
+    return Boolean::New(info.Env(), true);
+  }
+
+  return Boolean::New(info.Env(), false);
+}
+#endif
+}  // end anonymous namespace
+
+Object InitSharedArrayBuffer(Env env) {
+  Object exports = Object::New(env);
+
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+  exports["testIsSharedArrayBuffer"] =
+      Function::New(env, TestIsSharedArrayBuffer);
+  exports["testCreateSharedArrayBuffer"] =
+      Function::New(env, TestCreateSharedArrayBuffer);
+  exports["testGetSharedArrayBufferInfo"] =
+      Function::New(env, TestGetSharedArrayBufferInfo);
+  exports["testSharedArrayBufferData"] =
+      Function::New(env, TestSharedArrayBufferData);
+#endif
+
+  return exports;
+}
diff --git a/test/shared_array_buffer.js b/test/shared_array_buffer.js
new file mode 100644
index 0000000..018021a
--- /dev/null
+++ b/test/shared_array_buffer.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const assert = require('assert');
+
+module.exports = require('./common').runTest(test);
+
+let skippedMessageShown = false;
+
+function test ({ hasSharedArrayBuffer, sharedarraybuffer }) {
+  if (!hasSharedArrayBuffer) {
+    if (!skippedMessageShown) {
+      console.log('   >Skipped (no SharedArrayBuffer support)');
+      skippedMessageShown = true;
+    }
+    return;
+  }
+
+  {
+    const sab = new SharedArrayBuffer(16);
+    const ab = new ArrayBuffer(16);
+    const obj = {};
+    const arr = [];
+
+    assert.strictEqual(sharedarraybuffer.testIsSharedArrayBuffer(sab), true);
+    assert.strictEqual(sharedarraybuffer.testIsSharedArrayBuffer(ab), false);
+    assert.strictEqual(sharedarraybuffer.testIsSharedArrayBuffer(obj), false);
+    assert.strictEqual(sharedarraybuffer.testIsSharedArrayBuffer(arr), false);
+    assert.strictEqual(sharedarraybuffer.testIsSharedArrayBuffer(null), false);
+    assert.strictEqual(sharedarraybuffer.testIsSharedArrayBuffer(undefined), false);
+  }
+
+  {
+    const sab = sharedarraybuffer.testCreateSharedArrayBuffer(16);
+    assert(sab instanceof SharedArrayBuffer);
+    assert.strictEqual(sab.byteLength, 16);
+  }
+
+  {
+    const sab = new SharedArrayBuffer(32);
+    const byteLength = sharedarraybuffer.testGetSharedArrayBufferInfo(sab);
+    assert.strictEqual(byteLength, 32);
+  }
+
+  {
+    const sab = new SharedArrayBuffer(8);
+    const result = sharedarraybuffer.testSharedArrayBufferData(sab);
+    assert.strictEqual(result, true);
+
+    // Check if data was written correctly
+    const view = new Uint8Array(sab);
+    for (let i = 0; i < 8; i++) {
+      assert.strictEqual(view[i], i % 256);
+    }
+  }
+}
diff --git a/test/value_type_cast.cc b/test/value_type_cast.cc
index 9a140d2..dfc03b3 100644
--- a/test/value_type_cast.cc
+++ b/test/value_type_cast.cc
@@ -27,6 +27,11 @@
 #define V(Type)                                                                \
   void TypeCast##Type(const CallbackInfo& info) { USE(info[0].As<Type>()); }
 TYPE_CAST_TYPES(V)
+
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+V(SharedArrayBuffer)
+#endif
+
 #undef V
 
 void TypeCastBuffer(const CallbackInfo& info) {
@@ -47,6 +52,11 @@
 
 #define V(Type) exports["typeCast" #Type] = Function::New(env, TypeCast##Type);
   TYPE_CAST_TYPES(V)
+
+#ifdef NODE_API_EXPERIMENTAL_HAS_SHAREDARRAYBUFFER
+  V(SharedArrayBuffer)
+#endif
+
 #undef V
 
   exports["typeCastBuffer"] = Function::New(env, TypeCastBuffer);
diff --git a/test/value_type_cast.js b/test/value_type_cast.js
index cdebd4e..274dd27 100644
--- a/test/value_type_cast.js
+++ b/test/value_type_cast.js
@@ -42,7 +42,7 @@
     },
     typeCastArrayBuffer: {
       positiveValues: [new ArrayBuffer(0)],
-      negativeValues: [new Uint8Array(1), {}, [], null, undefined]
+      negativeValues: [new Uint8Array(1), new SharedArrayBuffer(0), {}, [], null, undefined]
     },
     typeCastTypedArray: {
       positiveValues: [new Uint8Array(0)],
@@ -77,6 +77,13 @@
     }
   };
 
+  if ('typeCastSharedArrayBuffer' in binding) {
+    testTable.typeCastSharedArrayBuffer = {
+      positiveValues: [new SharedArrayBuffer(0)],
+      negativeValues: [new Uint8Array(1), new ArrayBuffer(0), {}, [], null, undefined]
+    };
+  }
+
   if (process.argv[2] === 'child') {
     child(binding, testTable, process.argv[3], process.argv[4], parseInt(process.argv[5]));
     return;