diff --git a/binding.gyp b/binding.gyp index e904562..d80576f 100644 --- a/binding.gyp +++ b/binding.gyp @@ -8,14 +8,14 @@ ['AR', '<(module_root_dir)/mason_packages/.link/bin/llvm-ar'], ['NM', '<(module_root_dir)/mason_packages/.link/bin/llvm-nm'] ], - 'includes': [ 'common.gypi' ], # brings in a default set of options that are inherited from gyp + 'includes': [ 'common.gypi'], # brings in a default set of options that are inherited from gyp 'variables': { # custom variables we use specific to this file 'error_on_warnings%':'true', # can be overriden by a command line variable because of the % sign using "WERROR" (defined in Makefile) - # Use this variable to silence warnings from mason dependencies and from NAN + # Use this variable to silence warnings from mason dependencies # It's a variable to make easy to pass to # cflags (linux) and xcode (mac) 'system_includes': [ - "-isystem <(module_root_dir)/ +// std +#include +#include +#include + +namespace deflate { + +inline bool is_zlib(char const* data) noexcept +{ + return (static_cast(data[0]) == 0x78 && + (static_cast(data[1]) == 0x9C || + static_cast(data[1]) == 0x01 || + static_cast(data[1]) == 0xDA || + static_cast(data[1]) == 0x5E)); +} + +inline bool is_gzip(char const* data) noexcept +{ + return (static_cast(data[0]) == 0x1F && static_cast(data[1]) == 0x8B); +} + +inline bool is_compressed(char const* data, std::size_t size) noexcept +{ + return size > 2 && (is_gzip(data) || is_zlib(data)); +} + +class Compressor +{ + struct cleanup_compressor + { + void operator()(libdeflate_compressor* ptr) const + { + if (ptr) libdeflate_free_compressor(ptr); + } + }; + std::size_t max_; + int level_; + std::unique_ptr compressor_; + // make noncopyable + Compressor(Compressor const&) = delete; + Compressor& operator=(Compressor const&) = delete; + + public: + Compressor(int level = 6, + std::size_t max_bytes = 2000000000) // by default refuse operation if uncompressed data is > 2GB + : max_{max_bytes}, + level_{level}, + compressor_{libdeflate_alloc_compressor(level_)} + { + if (!compressor_) + { + throw std::runtime_error("libdeflate_alloc_compressor failed"); + } + } + + template + void operator()(OutputType& output, + char const* data, + std::size_t size) const + { + if (size > max_) + { + throw std::runtime_error("size may use more memory than intended when decompressing"); + } + + std::size_t max_compressed_size = libdeflate_gzip_compress_bound(compressor_.get(), size); + // TODO: sanity check this before allocating + if (max_compressed_size > output.size()) + { + output.resize(max_compressed_size); + } + + std::size_t actual_compressed_size = libdeflate_gzip_compress(compressor_.get(), + data, + size, + const_cast(output.data()), + max_compressed_size); + if (actual_compressed_size == 0) + { + throw std::runtime_error("actual_compressed_size 0"); + } + output.resize(actual_compressed_size); + } +}; + +class Decompressor +{ + struct cleanup_decompressor + { + void operator()(libdeflate_decompressor* ptr) const + { + if (ptr) libdeflate_free_decompressor(ptr); + } + }; + + std::size_t const max_; + std::unique_ptr decompressor_; + // noncopyable + Decompressor(Decompressor const&) = delete; + Decompressor& operator=(Decompressor const&) = delete; + + public: + Decompressor(std::size_t max_bytes = 2147483648u) // by default refuse operation if required uutput buffer is > 2GB + : max_{max_bytes}, + decompressor_{libdeflate_alloc_decompressor(), cleanup_decompressor()} + { + if (!decompressor_) + { + throw std::runtime_error("libdeflate_alloc_decompressor failed"); + } + } + + template + void operator()(OutputType& output, + char const* data, + std::size_t size) const + { + if (is_gzip(data)) + apply(output, libdeflate_gzip_decompress, data, size); + else if (is_zlib(data)) + apply(output, libdeflate_zlib_decompress, data, size); + } + + template + void apply(OutputType& output, Fun fun, + char const* data, + std::size_t size) const + { + std::size_t actual_size; + std::size_t uncompressed_size_guess = std::min(size * 4, max_); + output.resize(uncompressed_size_guess); + libdeflate_result result; + for (;;) + { + result = fun(decompressor_.get(), + data, + size, + const_cast(output.data()), + output.size(), &actual_size); + if (result == LIBDEFLATE_SUCCESS) + { + output.resize(actual_size); + break; + } + else if (result == LIBDEFLATE_INSUFFICIENT_SPACE) + { + if (output.size() == max_) + { + throw std::runtime_error("request to resize output buffer can't exceed maximum limit"); + } + std::size_t new_size = std::min(output.size() << 1, max_); + output.resize(new_size); + } + else //LIBDEFLATE_BAD_DATA + { + throw std::runtime_error("bad data: did not succeed"); + } + } + } +}; + +} // namespace deflate diff --git a/src/module.cpp b/src/module.cpp index 9d189d0..72b9e37 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -1,9 +1,10 @@ #include "vtcomposite.hpp" -#include +#include -NAN_MODULE_INIT(init) +Napi::Object init(Napi::Env env, Napi::Object exports) { - Nan::SetMethod(target, "composite", vtile::composite); + exports.Set(Napi::String::New(env, "composite"), Napi::Function::New(env, vtile::composite)); + return exports; } -NODE_MODULE(module, init) // NOLINT +NODE_API_MODULE(module, init) // NOLINT diff --git a/src/module_utils.hpp b/src/module_utils.hpp index 87a9691..d0a8f73 100644 --- a/src/module_utils.hpp +++ b/src/module_utils.hpp @@ -1,28 +1,13 @@ #pragma once -#include +#include namespace utils { -/* -* This is an internal function used to return callback error messages instead of -* throwing errors. -* Usage: -* -* v8::Local callback; -* return CallbackError("error message", callback); // "return" is important to -* prevent duplicate callbacks from being fired! -* -* -* "inline" is important here as well. See for more contex: -* - https://github.com/mapbox/cpp/blob/master/glossary.md#inline-keyword -* - https://github.com/mapbox/node-cpp-skel/pull/52#discussion_r126847394 for -* context -* -*/ -inline void CallbackError(std::string message, v8::Local func) +inline Napi::Value CallbackError(std::string const& message, Napi::CallbackInfo const& info) { - Nan::Callback cb(func); - v8::Local argv[1] = {Nan::Error(message.c_str())}; - Nan::Call(cb, 1, argv); + Napi::Object obj = Napi::Object::New(info.Env()); + obj.Set("message", message); + auto func = info[info.Length() - 1].As(); + return func.Call({obj}); } -} // namespace utils \ No newline at end of file +} // namespace utils diff --git a/src/vtcomposite.cpp b/src/vtcomposite.cpp index 1eeae05..c849995 100644 --- a/src/vtcomposite.cpp +++ b/src/vtcomposite.cpp @@ -1,19 +1,16 @@ // vtcomposite #include "vtcomposite.hpp" +#include "deflate.hpp" +#include "feature_builder.hpp" #include "module_utils.hpp" #include "zxy_math.hpp" -#include "feature_builder.hpp" -// gzip-hpp -#include -#include -#include // vtzero #include #include // geometry.hpp +#include #include #include -#include // stl #include @@ -26,14 +23,13 @@ struct TileObject TileObject(int z0, int x0, int y0, - v8::Local& buffer) + Napi::Buffer const& buffer) : z{z0}, x{x0}, y{y0}, - data{node::Buffer::Data(buffer), node::Buffer::Length(buffer)}, - buffer_ref{} + data{buffer.Data(), buffer.Length()}, + buffer_ref{Napi::Persistent(buffer)} { - buffer_ref.Reset(buffer.As()); } ~TileObject() @@ -45,15 +41,11 @@ struct TileObject TileObject(TileObject const&) = delete; TileObject& operator=(TileObject const&) = delete; - // non-movable - TileObject(TileObject&&) = delete; - TileObject& operator=(TileObject&&) = delete; - int z; int x; int y; vtzero::data_view data; - Nan::Persistent buffer_ref; + Napi::Reference> buffer_ref; }; struct BatonType @@ -67,10 +59,6 @@ struct BatonType BatonType(BatonType const&) = delete; BatonType& operator=(BatonType const&) = delete; - // non-movable - BatonType(BatonType&&) = delete; - BatonType& operator=(BatonType&&) = delete; - // members std::vector> tiles{}; int z{}; @@ -85,7 +73,7 @@ namespace { template struct build_feature_from_v1 { - build_feature_from_v1(FeatureBuilder& builder) + explicit build_feature_from_v1(FeatureBuilder& builder) : builder_(builder) {} bool operator()(vtzero::feature const& feature) @@ -106,7 +94,7 @@ struct build_feature_from_v1 template struct build_feature_from_v2 { - build_feature_from_v2(FeatureBuilder& builder) + explicit build_feature_from_v2(FeatureBuilder& builder) : builder_(builder) {} bool operator()(vtzero::feature const& feature) @@ -119,12 +107,12 @@ struct build_feature_from_v2 } // namespace -struct CompositeWorker : Nan::AsyncWorker +struct CompositeWorker : Napi::AsyncWorker { - using Base = Nan::AsyncWorker; + using Base = Napi::AsyncWorker; - CompositeWorker(std::unique_ptr&& baton_data, Nan::Callback* cb) - : Base(cb, "skel:standalone-async-worker"), + CompositeWorker(std::unique_ptr&& baton_data, Napi::Function& cb) + : Base(cb), baton_data_{std::move(baton_data)}, output_buffer_{std::make_unique()} {} @@ -140,19 +128,20 @@ struct CompositeWorker : Nan::AsyncWorker int const target_x = baton_data_->x; int const target_y = baton_data_->y; - std::vector>> buffer_cache; + std::vector> buffer_cache; + deflate::Decompressor decompressor; + deflate::Compressor compressor; for (auto const& tile_obj : baton_data_->tiles) { if (vtile::within_target(*tile_obj, target_z, target_x, target_y)) { vtzero::data_view tile_view{}; - if (gzip::is_compressed(tile_obj->data.data(), tile_obj->data.size())) + if (deflate::is_compressed(tile_obj->data.data(), tile_obj->data.size())) { - buffer_cache.push_back(std::make_unique>()); - gzip::Decompressor decompressor; - decompressor.decompress(*buffer_cache.back(), tile_obj->data.data(), tile_obj->data.size()); - tile_view = protozero::data_view{buffer_cache.back()->data(), buffer_cache.back()->size()}; + buffer_cache.emplace_back(); + decompressor(buffer_cache.back(), tile_obj->data.data(), tile_obj->data.size()); + tile_view = protozero::data_view{buffer_cache.back().data(), buffer_cache.back().size()}; } else { @@ -206,12 +195,12 @@ struct CompositeWorker : Nan::AsyncWorker throw std::invalid_argument(os.str()); } } - std::string& tile_buffer = *output_buffer_.get(); + std::string& tile_buffer = *output_buffer_; if (baton_data_->compress) { std::string temp; builder.serialize(temp); - tile_buffer = gzip::compress(temp.data(), temp.size()); + compressor(tile_buffer, temp.data(), temp.size()); } else { @@ -221,232 +210,234 @@ struct CompositeWorker : Nan::AsyncWorker // LCOV_EXCL_START catch (std::exception const& e) { - SetErrorMessage(e.what()); + SetError(e.what()); } // LCOV_EXCL_STOP } - - void HandleOKCallback() override + void OnOK() override { - std::string& tile_buffer = *output_buffer_.get(); - Nan::HandleScope scope; - const auto argc = 2u; - v8::Local argv[argc] = { - Nan::Null(), - Nan::NewBuffer(&tile_buffer[0], - static_cast(tile_buffer.size()), - [](char*, void* hint) { - delete reinterpret_cast(hint); - }, - output_buffer_.release()) - .ToLocalChecked()}; - - // Static cast done here to avoid 'cppcoreguidelines-pro-bounds-array-to-pointer-decay' warning with clang-tidy - callback->Call(argc, static_cast*>(argv), async_resource); + std::string& tile_buffer = *output_buffer_; + Napi::HandleScope scope(Env()); + Napi::Value argv = Napi::Buffer::New(Env(), + const_cast(tile_buffer.data()), + tile_buffer.size(), + [](Napi::Env, char*, std::string* s) { + delete s; + }, + output_buffer_.release()); + + Callback().Call({Env().Null(), argv}); } std::unique_ptr const baton_data_; std::unique_ptr output_buffer_; }; -NAN_METHOD(composite) +Napi::Value composite(Napi::CallbackInfo const& info) { // validate callback function - v8::Local callback_val = info[info.Length() - 1]; - if (!callback_val->IsFunction()) + std::size_t length = info.Length(); + if (length == 0) { - Nan::ThrowError("last argument must be a callback function"); - return; + Napi::Error::New(info.Env(), "last argument must be a callback function").ThrowAsJavaScriptException(); + return info.Env().Null(); + } + Napi::Value callback_val = info[length - 1]; + if (!callback_val.IsFunction()) + { + Napi::Error::New(info.Env(), "last argument must be a callback function").ThrowAsJavaScriptException(); + return info.Env().Null(); } - v8::Local callback = callback_val.As(); + Napi::Function callback = callback_val.As(); // validate tiles - if (!info[0]->IsArray()) + if (!info[0].IsArray()) { - return utils::CallbackError("first arg 'tiles' must be an array of tile objects", callback); + return utils::CallbackError("first arg 'tiles' must be an array of tile objects", info); } - v8::Local tiles = info[0].As(); - unsigned num_tiles = tiles->Length(); + Napi::Array tiles = info[0].As(); + unsigned num_tiles = tiles.Length(); if (num_tiles <= 0) { - return utils::CallbackError("'tiles' array must be of length greater than 0", callback); + return utils::CallbackError("'tiles' array must be of length greater than 0", info); } std::unique_ptr baton_data = std::make_unique(num_tiles); for (unsigned t = 0; t < num_tiles; ++t) { - v8::Local tile_val = tiles->Get(t); - if (!tile_val->IsObject()) + Napi::Value tile_val = tiles.Get(t); + if (!tile_val.IsObject()) { - return utils::CallbackError("items in 'tiles' array must be objects", callback); + return utils::CallbackError("items in 'tiles' array must be objects", info); } - v8::Local tile_obj = tile_val->ToObject(); + Napi::Object tile_obj = tile_val.As(); // check buffer value - if (!tile_obj->Has(Nan::New("buffer").ToLocalChecked())) + if (!tile_obj.Has(Napi::String::New(info.Env(), "buffer"))) { - return utils::CallbackError("item in 'tiles' array does not include a buffer value", callback); + return utils::CallbackError("item in 'tiles' array does not include a buffer value", info); } - v8::Local buf_val = tile_obj->Get(Nan::New("buffer").ToLocalChecked()); - if (buf_val->IsNull() || buf_val->IsUndefined()) + Napi::Value buf_val = tile_obj.Get(Napi::String::New(info.Env(), "buffer")); + if (buf_val.IsNull() || buf_val.IsUndefined()) { - return utils::CallbackError("buffer value in 'tiles' array item is null or undefined", callback); + return utils::CallbackError("buffer value in 'tiles' array item is null or undefined", info); } - v8::Local buffer = buf_val->ToObject(); - if (!node::Buffer::HasInstance(buffer)) + Napi::Object buffer_obj = buf_val.As(); + if (!buffer_obj.IsBuffer()) { - return utils::CallbackError("buffer value in 'tiles' array item is not a true buffer", callback); + return utils::CallbackError("buffer value in 'tiles' array item is not a true buffer", info); } + Napi::Buffer buffer = buffer_obj.As>(); // z value - if (!tile_obj->Has(Nan::New("z").ToLocalChecked())) + if (!tile_obj.Has(Napi::String::New(info.Env(), "z"))) { - return utils::CallbackError("item in 'tiles' array does not include a 'z' value", callback); + return utils::CallbackError("item in 'tiles' array does not include a 'z' value", info); } - v8::Local z_val = tile_obj->Get(Nan::New("z").ToLocalChecked()); - if (!z_val->IsInt32()) + Napi::Value z_val = tile_obj.Get(Napi::String::New(info.Env(), "z")); + if (!z_val.IsNumber()) { - return utils::CallbackError("'z' value in 'tiles' array item is not an int32", callback); + return utils::CallbackError("'z' value in 'tiles' array item is not an int32", info); } - int z = z_val->Int32Value(); + int z = z_val.As().Int32Value(); if (z < 0) { - return utils::CallbackError("'z' value must not be less than zero", callback); + return utils::CallbackError("'z' value must not be less than zero", info); } // x value - if (!tile_obj->Has(Nan::New("x").ToLocalChecked())) + if (!tile_obj.Has(Napi::String::New(info.Env(), "x"))) { - return utils::CallbackError("item in 'tiles' array does not include a 'x' value", callback); + return utils::CallbackError("item in 'tiles' array does not include a 'x' value", info); } - v8::Local x_val = tile_obj->Get(Nan::New("x").ToLocalChecked()); - if (!x_val->IsInt32()) + Napi::Value x_val = tile_obj.Get(Napi::String::New(info.Env(), "x")); + if (!x_val.IsNumber()) { - return utils::CallbackError("'x' value in 'tiles' array item is not an int32", callback); + return utils::CallbackError("'x' value in 'tiles' array item is not an int32", info); } - int x = x_val->Int32Value(); + + int x = x_val.As().Int32Value(); if (x < 0) { - return utils::CallbackError("'x' value must not be less than zero", callback); + return utils::CallbackError("'x' value must not be less than zero", info); } // y value - if (!tile_obj->Has(Nan::New("y").ToLocalChecked())) + if (!tile_obj.Has(Napi::String::New(info.Env(), "y"))) { - return utils::CallbackError("item in 'tiles' array does not include a 'y' value", callback); + return utils::CallbackError("item in 'tiles' array does not include a 'y' value", info); } - v8::Local y_val = tile_obj->Get(Nan::New("y").ToLocalChecked()); - if (!y_val->IsInt32()) + Napi::Value y_val = tile_obj.Get(Napi::String::New(info.Env(), "y")); + if (!y_val.IsNumber()) { - return utils::CallbackError("'y' value in 'tiles' array item is not an int32", callback); + return utils::CallbackError("'y' value in 'tiles' array item is not an int32", info); } - int y = y_val->Int32Value(); + int y = y_val.As().Int32Value(); if (y < 0) { - return utils::CallbackError("'y' value must not be less than zero", callback); + return utils::CallbackError("'y' value must not be less than zero", info); } + baton_data->tiles.push_back(std::make_unique(z, x, y, buffer)); } //validate zxy maprequest object - if (!info[1]->IsObject()) + if (!info[1].IsObject()) { - return utils::CallbackError("'zxy_maprequest' must be an object", callback); + return utils::CallbackError("'zxy_maprequest' must be an object", info); } - v8::Local zxy_maprequest = v8::Local::Cast(info[1]); + Napi::Object zxy_maprequest = info[1].As(); // z value of map request object - if (!zxy_maprequest->Has(Nan::New("z").ToLocalChecked())) + if (!zxy_maprequest.Has(Napi::String::New(info.Env(), "z"))) { - return utils::CallbackError("item in 'tiles' array does not include a 'z' value", callback); + return utils::CallbackError("item in 'tiles' array does not include a 'z' value", info); } - v8::Local z_val_maprequest = zxy_maprequest->Get(Nan::New("z").ToLocalChecked()); - if (!z_val_maprequest->IsInt32()) + Napi::Value z_val_maprequest = zxy_maprequest.Get(Napi::String::New(info.Env(), "z")); + if (!z_val_maprequest.IsNumber()) { - return utils::CallbackError("'z' value in 'tiles' array item is not an int32", callback); + return utils::CallbackError("'z' value in 'tiles' array item is not an int32", info); } - int z_maprequest = z_val_maprequest->Int32Value(); + int z_maprequest = z_val_maprequest.As().Int32Value(); if (z_maprequest < 0) { - return utils::CallbackError("'z' value must not be less than zero", callback); + return utils::CallbackError("'z' value must not be less than zero", info); } baton_data->z = z_maprequest; // x value of map request object - if (!zxy_maprequest->Has(Nan::New("x").ToLocalChecked())) + if (!zxy_maprequest.Has(Napi::String::New(info.Env(), "x"))) { - return utils::CallbackError("item in 'tiles' array does not include a 'x' value", callback); + return utils::CallbackError("item in 'tiles' array does not include a 'x' value", info); } - v8::Local x_val_maprequest = zxy_maprequest->Get(Nan::New("x").ToLocalChecked()); - if (!x_val_maprequest->IsInt32()) + Napi::Value x_val_maprequest = zxy_maprequest.Get(Napi::String::New(info.Env(), "x")); + if (!x_val_maprequest.IsNumber()) { - return utils::CallbackError("'x' value in 'tiles' array item is not an int32", callback); + return utils::CallbackError("'x' value in 'tiles' array item is not an int32", info); } - int x_maprequest = x_val_maprequest->Int32Value(); + int x_maprequest = x_val_maprequest.As().Int32Value(); if (x_maprequest < 0) { - return utils::CallbackError("'x' value must not be less than zero", callback); + return utils::CallbackError("'x' value must not be less than zero", info); } baton_data->x = x_maprequest; // y value of maprequest object - if (!zxy_maprequest->Has(Nan::New("y").ToLocalChecked())) + if (!zxy_maprequest.Has(Napi::String::New(info.Env(), "y"))) { - return utils::CallbackError("item in 'tiles' array does not include a 'y' value", callback); + return utils::CallbackError("item in 'tiles' array does not include a 'y' value", info); } - v8::Local y_val_maprequest = zxy_maprequest->Get(Nan::New("y").ToLocalChecked()); - if (!y_val_maprequest->IsInt32()) + Napi::Value y_val_maprequest = zxy_maprequest.Get(Napi::String::New(info.Env(), "y")); + if (!y_val_maprequest.IsNumber()) { - return utils::CallbackError("'y' value in 'tiles' array item is not an int32", callback); + return utils::CallbackError("'y' value in 'tiles' array item is not an int32", info); } - int y_maprequest = y_val_maprequest->Int32Value(); + int y_maprequest = y_val_maprequest.As().Int32Value(); if (y_maprequest < 0) { - return utils::CallbackError("'y' value must not be less than zero", callback); + return utils::CallbackError("'y' value must not be less than zero", info); } baton_data->y = y_maprequest; if (info.Length() > 3) // options { - if (!info[2]->IsObject()) + if (!info[2].IsObject()) { - return utils::CallbackError("'options' arg must be an object", callback); + return utils::CallbackError("'options' arg must be an object", info); } - v8::Local options = info[2]->ToObject(); - if (options->Has(Nan::New("buffer_size").ToLocalChecked())) + Napi::Object options = info[2].As(); + if (options.Has(Napi::String::New(info.Env(), "buffer_size"))) { - v8::Local bs_value = options->Get(Nan::New("buffer_size").ToLocalChecked()); - if (!bs_value->IsInt32()) + Napi::Value bs_value = options.Get(Napi::String::New(info.Env(), "buffer_size")); + if (!bs_value.IsNumber()) { - return utils::CallbackError("'buffer_size' must be an int32", callback); + return utils::CallbackError("'buffer_size' must be an int32", info); } - - int buffer_size = bs_value->Int32Value(); + int buffer_size = bs_value.As().Int32Value(); if (buffer_size < 0) { - return utils::CallbackError("'buffer_size' must be a positive int32", callback); + return utils::CallbackError("'buffer_size' must be a positive int32", info); } baton_data->buffer_size = buffer_size; } - if (options->Has(Nan::New("compress").ToLocalChecked())) + if (options.Has(Napi::String::New(info.Env(), "compress"))) { - v8::Local comp_value = options->Get(Nan::New("compress").ToLocalChecked()); - if (!comp_value->IsBoolean()) + Napi::Value comp_value = options.Get(Napi::String::New(info.Env(), "compress")); + if (!comp_value.IsBoolean()) { - return utils::CallbackError("'compress' must be a boolean", callback); + return utils::CallbackError("'compress' must be a boolean", info); } - baton_data->compress = comp_value->BooleanValue(); + baton_data->compress = comp_value.As().Value(); } } - // enter the threadpool, then done in the callback function call the threadpool - auto* worker = new CompositeWorker{std::move(baton_data), new Nan::Callback{callback}}; - Nan::AsyncQueueWorker(worker); + auto* worker = new CompositeWorker{std::move(baton_data), callback}; + worker->Queue(); + return info.Env().Undefined(); } - } // namespace vtile diff --git a/src/vtcomposite.hpp b/src/vtcomposite.hpp index a767be0..683b2c7 100644 --- a/src/vtcomposite.hpp +++ b/src/vtcomposite.hpp @@ -1,8 +1,8 @@ #pragma once -#include +#include namespace vtile { -NAN_METHOD(composite); +Napi::Value composite(Napi::CallbackInfo const& info); } // namespace vtile diff --git a/test/vtcomposite-param-validation.test.js b/test/vtcomposite-param-validation.test.js index 841cf4e..9cdefea 100644 --- a/test/vtcomposite-param-validation.test.js +++ b/test/vtcomposite-param-validation.test.js @@ -7,8 +7,7 @@ var mvtFixtures = require('@mapbox/mvt-fixtures'); test('failure: fails without callback function', assert => { try { - -composite(); + composite(); } catch(err) { assert.ok(/last argument must be a callback function/.test(err.message), 'expected error message'); assert.end();