@nxtedition/rocksdb 16.0.2 → 16.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/binding.cc +130 -30
- package/build.sh +16 -2
- package/index.js +7 -3
- package/iterator.js +12 -0
- package/max_rev_operator.h +40 -4
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/@nxtedition+rocksdb.node +0 -0
- package/prebuilds/linux-x64/@nxtedition+rocksdb.node +0 -0
- package/release.sh +40 -0
- package/util.h +13 -3
- package/util.js +0 -70
- package/.claude/settings.local.json +0 -35
package/binding.cc
CHANGED
|
@@ -54,7 +54,7 @@ class NullLogger : public rocksdb::Logger {
|
|
|
54
54
|
|
|
55
55
|
struct Database;
|
|
56
56
|
class Iterator;
|
|
57
|
-
|
|
57
|
+
struct Updates;
|
|
58
58
|
|
|
59
59
|
struct ColumnFamily {
|
|
60
60
|
rocksdb::ColumnFamilyHandle* handle;
|
|
@@ -440,6 +440,15 @@ struct BaseIterator : public Closable {
|
|
|
440
440
|
return iterator_->status();
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
virtual rocksdb::Status Refresh() {
|
|
444
|
+
assert(iterator_);
|
|
445
|
+
// Refresh restarts iteration, so the user `limit` budget must restart too;
|
|
446
|
+
// otherwise an iterator that already yielded `limit` rows returns nothing
|
|
447
|
+
// after a refresh even though every other piece of state was reset.
|
|
448
|
+
count_ = 0;
|
|
449
|
+
return iterator_->Refresh();
|
|
450
|
+
}
|
|
451
|
+
|
|
443
452
|
Database* database_;
|
|
444
453
|
rocksdb::ColumnFamilyHandle* column_;
|
|
445
454
|
|
|
@@ -508,6 +517,11 @@ class Iterator final : public BaseIterator {
|
|
|
508
517
|
return BaseIterator::Seek(target);
|
|
509
518
|
}
|
|
510
519
|
|
|
520
|
+
rocksdb::Status Refresh() override {
|
|
521
|
+
first_ = true;
|
|
522
|
+
return BaseIterator::Refresh();
|
|
523
|
+
}
|
|
524
|
+
|
|
511
525
|
static std::unique_ptr<Iterator> create(napi_env env, napi_value db, napi_value options) {
|
|
512
526
|
Database* database;
|
|
513
527
|
NAPI_STATUS_THROWS(napi_get_value_external(env, db, reinterpret_cast<void**>(&database)));
|
|
@@ -527,7 +541,9 @@ class Iterator final : public BaseIterator {
|
|
|
527
541
|
int32_t limit = -1;
|
|
528
542
|
NAPI_STATUS_THROWS(GetProperty(env, options, "limit", limit));
|
|
529
543
|
|
|
530
|
-
|
|
544
|
+
// 64-bit: the value flows into a size_t cap, so parsing as int32 would wrap
|
|
545
|
+
// any value > 2 GiB to a garbage cap. Default stays ~2 GiB (effectively no cap).
|
|
546
|
+
int64_t highWaterMarkBytes = std::numeric_limits<int32_t>::max();
|
|
531
547
|
NAPI_STATUS_THROWS(GetProperty(env, options, "highWaterMarkBytes", highWaterMarkBytes));
|
|
532
548
|
|
|
533
549
|
std::optional<std::string> lt;
|
|
@@ -644,14 +660,11 @@ class Iterator final : public BaseIterator {
|
|
|
644
660
|
break;
|
|
645
661
|
}
|
|
646
662
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
|
|
663
|
+
// Apply the key/value filters BEFORE charging the user `limit`, so
|
|
664
|
+
// `limit` counts matched (emitted) rows, not rows merely scanned and
|
|
665
|
+
// then discarded. Otherwise a `{ limit, keyFilter }` query could
|
|
666
|
+
// exhaust its budget on non-matching rows and return fewer (or zero)
|
|
667
|
+
// matches than exist.
|
|
655
668
|
if (keyFilter_ && !re2::RE2::PartialMatch(CurrentKey().ToStringView(), *keyFilter_)) {
|
|
656
669
|
continue;
|
|
657
670
|
}
|
|
@@ -660,6 +673,14 @@ class Iterator final : public BaseIterator {
|
|
|
660
673
|
continue;
|
|
661
674
|
}
|
|
662
675
|
|
|
676
|
+
if (!Increment()) {
|
|
677
|
+
// Hit the user's `limit` option: terminal, and flag that it was a
|
|
678
|
+
// limit rather than natural exhaustion.
|
|
679
|
+
state.finished = true;
|
|
680
|
+
state.limited = true;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
|
|
663
684
|
if (keys_ && values_) {
|
|
664
685
|
rocksdb::PinnableSlice k;
|
|
665
686
|
k.PinSelf(CurrentKey());
|
|
@@ -772,14 +793,8 @@ class Iterator final : public BaseIterator {
|
|
|
772
793
|
break;
|
|
773
794
|
}
|
|
774
795
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
// rather than natural exhaustion.
|
|
778
|
-
NAPI_STATUS_THROWS(napi_get_boolean(env, true, &finished));
|
|
779
|
-
NAPI_STATUS_THROWS(napi_get_boolean(env, true, &limited));
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
|
|
796
|
+
// Apply the key/value filters BEFORE charging the user `limit`, so `limit`
|
|
797
|
+
// counts matched (emitted) rows, not rows merely scanned and discarded.
|
|
783
798
|
if (keyFilter_ && !re2::RE2::PartialMatch(CurrentKey().ToStringView(), *keyFilter_)) {
|
|
784
799
|
continue;
|
|
785
800
|
}
|
|
@@ -788,6 +803,14 @@ class Iterator final : public BaseIterator {
|
|
|
788
803
|
continue;
|
|
789
804
|
}
|
|
790
805
|
|
|
806
|
+
if (!Increment()) {
|
|
807
|
+
// Hit the user's `limit` option: terminal, and flag that it was a limit
|
|
808
|
+
// rather than natural exhaustion.
|
|
809
|
+
NAPI_STATUS_THROWS(napi_get_boolean(env, true, &finished));
|
|
810
|
+
NAPI_STATUS_THROWS(napi_get_boolean(env, true, &limited));
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
|
|
791
814
|
napi_value key;
|
|
792
815
|
napi_value val;
|
|
793
816
|
|
|
@@ -848,6 +871,11 @@ static void FinalizeDatabase(napi_env env, void* data, void* hint) {
|
|
|
848
871
|
database->resourceNamesRef = nullptr;
|
|
849
872
|
}
|
|
850
873
|
database->Close();
|
|
874
|
+
// This external owns the Database (the bigint-handle external in db_init is
|
|
875
|
+
// created with no finalizer, so it never reaches here). Close() already
|
|
876
|
+
// released the rocksdb::DB; free the heap object itself or it leaks for the
|
|
877
|
+
// lifetime of the process.
|
|
878
|
+
delete database;
|
|
851
879
|
}
|
|
852
880
|
}
|
|
853
881
|
|
|
@@ -880,7 +908,19 @@ NAPI_METHOD(db_init) {
|
|
|
880
908
|
database = reinterpret_cast<Database*>(value);
|
|
881
909
|
NAPI_STATUS_THROWS(napi_create_external(env, database, nullptr, nullptr, &result));
|
|
882
910
|
|
|
883
|
-
//
|
|
911
|
+
// TODO (critical, lifetime): sharing a Database* across V8 environments (e.g.
|
|
912
|
+
// worker_threads) via db_get_handle is unsafe. There is no cross-env
|
|
913
|
+
// reference count on the rocksdb::DB, so one env's db_close() runs
|
|
914
|
+
// Database::Close() (freeing the DB + column handles on a worker thread)
|
|
915
|
+
// while another env may still be running MultiGet / iterator / updates
|
|
916
|
+
// against it -> use-after-free / double-free. This branch also installs no
|
|
917
|
+
// env_cleanup_hook or finalizer, so a tearing-down secondary env never
|
|
918
|
+
// detaches its iterators, and GetResourceName() dereferences a napi_ref
|
|
919
|
+
// (resourceNamesRef) that belongs to the originating env (cross-env ref use
|
|
920
|
+
// is undefined behaviour). Fix: refcount the Database lifetime across all
|
|
921
|
+
// wrapping envs (run the real Close()/db.reset() only when the last
|
|
922
|
+
// reference drops), install a cleanup hook here, and make resource names
|
|
923
|
+
// per-env. Until then, close() on a shared handle must be app-coordinated.
|
|
884
924
|
} else {
|
|
885
925
|
NAPI_STATUS_THROWS(napi_invalid_arg);
|
|
886
926
|
}
|
|
@@ -916,7 +956,15 @@ NAPI_METHOD(db_query) {
|
|
|
916
956
|
NAPI_ARGV(2);
|
|
917
957
|
|
|
918
958
|
try {
|
|
919
|
-
|
|
959
|
+
auto iterator = Iterator::create(env, argv[0], argv[1]);
|
|
960
|
+
// Iterator::create uses NAPI_STATUS_THROWS internally, which on a N-API
|
|
961
|
+
// failure schedules a pending JS exception and `return NULL` — i.e. an empty
|
|
962
|
+
// unique_ptr. Dereferencing it (->nextv) would be a null deref / crash, so
|
|
963
|
+
// bail out and let the pending exception surface.
|
|
964
|
+
if (!iterator) {
|
|
965
|
+
return nullptr;
|
|
966
|
+
}
|
|
967
|
+
return iterator->nextv(env, std::numeric_limits<uint32_t>::max());
|
|
920
968
|
} catch (const std::exception& e) {
|
|
921
969
|
napi_throw_error(env, nullptr, e.what());
|
|
922
970
|
return nullptr;
|
|
@@ -983,6 +1031,10 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
|
|
|
983
1031
|
columnOptions.compression = rocksdb::kZSTD;
|
|
984
1032
|
columnOptions.compression_opts.max_dict_bytes = 16 * 1024;
|
|
985
1033
|
columnOptions.compression_opts.zstd_max_train_bytes = 16 * 1024 * 100;
|
|
1034
|
+
NAPI_STATUS_RETURN(GetProperty(env, options, "compressionLevel", columnOptions.compression_opts.level));
|
|
1035
|
+
NAPI_STATUS_RETURN(GetProperty(env, options, "maxDictBytes", columnOptions.compression_opts.max_dict_bytes));
|
|
1036
|
+
NAPI_STATUS_RETURN(
|
|
1037
|
+
GetProperty(env, options, "zstdMaxTrainBytes", columnOptions.compression_opts.zstd_max_train_bytes));
|
|
986
1038
|
// TODO (perf): compression_opts.parallel_threads
|
|
987
1039
|
} else {
|
|
988
1040
|
columnOptions.compression = rocksdb::kNoCompression;
|
|
@@ -1093,7 +1145,9 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
|
|
|
1093
1145
|
}
|
|
1094
1146
|
|
|
1095
1147
|
if (!cache) {
|
|
1096
|
-
|
|
1148
|
+
// size_t: RocksDB cache capacity is size_t; a 32-bit type silently wraps
|
|
1149
|
+
// requests >= 4 GiB (and 4 GiB exactly wraps to 0 -> cache disabled).
|
|
1150
|
+
uint64_t cacheSize = 8 << 20;
|
|
1097
1151
|
double compressedRatio = 0.0;
|
|
1098
1152
|
|
|
1099
1153
|
NAPI_STATUS_RETURN(GetProperty(env, options, "cacheSize", cacheSize));
|
|
@@ -1113,7 +1167,10 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
|
|
|
1113
1167
|
}
|
|
1114
1168
|
|
|
1115
1169
|
{
|
|
1116
|
-
|
|
1170
|
+
// int64: -1 means "unset" (inherit the shared cache); a 32-bit type both
|
|
1171
|
+
// wraps requests >= 4 GiB and collides the unset sentinel with a real
|
|
1172
|
+
// 4294967295-byte request.
|
|
1173
|
+
int64_t cacheSize = -1;
|
|
1117
1174
|
double compressedRatio = 0.0;
|
|
1118
1175
|
|
|
1119
1176
|
NAPI_STATUS_RETURN(GetProperty(env, options, "cachePrepopulate", tableOptions.prepopulate_block_cache));
|
|
@@ -1141,7 +1198,9 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
|
|
|
1141
1198
|
}
|
|
1142
1199
|
|
|
1143
1200
|
{
|
|
1144
|
-
|
|
1201
|
+
// int64: see the block-cache block above — -1 = unset, avoids 32-bit wrap
|
|
1202
|
+
// and the unset/4-GiB sentinel collision.
|
|
1203
|
+
int64_t cacheSize = -1;
|
|
1145
1204
|
double compressedRatio = 0.0;
|
|
1146
1205
|
|
|
1147
1206
|
NAPI_STATUS_RETURN(GetProperty(env, options, "cachePrepopulate", columnOptions.prepopulate_blob_cache));
|
|
@@ -1157,6 +1216,9 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
|
|
|
1157
1216
|
columnOptions.blob_cache = nullptr;
|
|
1158
1217
|
} else if (compressedRatio > 0.0) {
|
|
1159
1218
|
rocksdb::TieredCacheOptions options;
|
|
1219
|
+
// Match the block/main cache tiers: pin the primary tier to HyperClockCache
|
|
1220
|
+
// explicitly rather than letting it default to LRU.
|
|
1221
|
+
options.cache_type = rocksdb::PrimaryCacheType::kCacheTypeHCC;
|
|
1160
1222
|
options.total_capacity = cacheSize;
|
|
1161
1223
|
options.compressed_secondary_ratio = compressedRatio;
|
|
1162
1224
|
options.comp_cache_opts.compression_type = rocksdb::CompressionType::kZSTD;
|
|
@@ -1301,13 +1363,15 @@ NAPI_METHOD(db_open) {
|
|
|
1301
1363
|
|
|
1302
1364
|
NAPI_STATUS_THROWS(GetProperty(env, options, "walDir", dbOptions.wal_dir));
|
|
1303
1365
|
|
|
1304
|
-
|
|
1366
|
+
// 64-bit inputs: walTTL is in ms and walSizeLimit in bytes, so a 32-bit type
|
|
1367
|
+
// wraps a >= ~4.3 GB size limit (or a ~49-day TTL) before the unit conversion.
|
|
1368
|
+
uint64_t walTTL = 0;
|
|
1305
1369
|
NAPI_STATUS_THROWS(GetProperty(env, options, "walTTL", walTTL));
|
|
1306
|
-
dbOptions.WAL_ttl_seconds = static_cast<
|
|
1370
|
+
dbOptions.WAL_ttl_seconds = static_cast<uint64_t>(std::ceil(walTTL / 1e3));
|
|
1307
1371
|
|
|
1308
|
-
|
|
1372
|
+
uint64_t walSizeLimit = 0;
|
|
1309
1373
|
NAPI_STATUS_THROWS(GetProperty(env, options, "walSizeLimit", walSizeLimit));
|
|
1310
|
-
dbOptions.WAL_size_limit_MB = static_cast<
|
|
1374
|
+
dbOptions.WAL_size_limit_MB = static_cast<uint64_t>(std::ceil(walSizeLimit / 1e6));
|
|
1311
1375
|
|
|
1312
1376
|
NAPI_STATUS_THROWS(GetProperty(env, options, "maxTotalWalSize", dbOptions.max_total_wal_size));
|
|
1313
1377
|
|
|
@@ -1624,7 +1688,7 @@ NAPI_METHOD(db_get_many) {
|
|
|
1624
1688
|
[=](auto& state, napi_env env, napi_value* result) {
|
|
1625
1689
|
NAPI_STATUS_RETURN(napi_create_array_with_length(env, count, result));
|
|
1626
1690
|
|
|
1627
|
-
for (
|
|
1691
|
+
for (uint32_t n = 0; n < count; n++) {
|
|
1628
1692
|
napi_value row;
|
|
1629
1693
|
if (state.statuses[n].IsNotFound()) {
|
|
1630
1694
|
NAPI_STATUS_RETURN(napi_get_undefined(env, &row));
|
|
@@ -1688,6 +1752,10 @@ NAPI_METHOD(db_clear) {
|
|
|
1688
1752
|
*end.GetSelf() = std::move(*lt);
|
|
1689
1753
|
} else {
|
|
1690
1754
|
// HACK: Assume no key that starts with 0xFF is larger than 1MiB.
|
|
1755
|
+
// TODO (correctness): this synthetic upper bound silently leaves any key
|
|
1756
|
+
// >= a 1 MiB run of 0xFF bytes uncleared. Prefer DeleteRange over the full
|
|
1757
|
+
// keyspace (null end) or RangeBound::kInclusive on the max key instead of
|
|
1758
|
+
// assuming a bound.
|
|
1691
1759
|
end.GetSelf()->resize(1e6);
|
|
1692
1760
|
memset(end.GetSelf()->data(), 255, end.GetSelf()->size());
|
|
1693
1761
|
}
|
|
@@ -1743,16 +1811,26 @@ NAPI_METHOD(db_clear) {
|
|
|
1743
1811
|
}
|
|
1744
1812
|
|
|
1745
1813
|
NAPI_METHOD(db_get_property) {
|
|
1746
|
-
NAPI_ARGV(
|
|
1814
|
+
NAPI_ARGV(3);
|
|
1747
1815
|
|
|
1748
1816
|
Database* database;
|
|
1749
1817
|
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast<void**>(&database)));
|
|
1750
1818
|
|
|
1819
|
+
if (!database->db) {
|
|
1820
|
+
napi_throw_error(env, "LEVEL_DATABASE_NOT_OPEN", "Database is not open");
|
|
1821
|
+
return NULL;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1751
1824
|
rocksdb::PinnableSlice property;
|
|
1752
1825
|
NAPI_STATUS_THROWS(GetValue(env, argv[1], property));
|
|
1753
1826
|
|
|
1827
|
+
// Most rocksdb properties are column-family scoped; without an explicit
|
|
1828
|
+
// column they answer for the default CF only.
|
|
1829
|
+
rocksdb::ColumnFamilyHandle* column = database->db->DefaultColumnFamily();
|
|
1830
|
+
NAPI_STATUS_THROWS(GetProperty(env, argv[2], "column", column));
|
|
1831
|
+
|
|
1754
1832
|
std::string value;
|
|
1755
|
-
database->db->GetProperty(property, &value);
|
|
1833
|
+
database->db->GetProperty(column, property, &value);
|
|
1756
1834
|
|
|
1757
1835
|
napi_value result;
|
|
1758
1836
|
NAPI_STATUS_THROWS(napi_create_string_utf8(env, value.data(), value.size(), &result));
|
|
@@ -1804,6 +1882,11 @@ NAPI_METHOD(iterator_init_sync) {
|
|
|
1804
1882
|
napi_value result;
|
|
1805
1883
|
try {
|
|
1806
1884
|
auto iterator = Iterator::create(env, argv[0], argv[1]);
|
|
1885
|
+
// create() returns an empty unique_ptr (and a pending JS exception) on a
|
|
1886
|
+
// N-API failure; surface that instead of wrapping a null pointer.
|
|
1887
|
+
if (!iterator) {
|
|
1888
|
+
return nullptr;
|
|
1889
|
+
}
|
|
1807
1890
|
|
|
1808
1891
|
NAPI_STATUS_THROWS(napi_create_external(env, iterator.get(), Finalize<Iterator>, iterator.get(), &result));
|
|
1809
1892
|
iterator.release();
|
|
@@ -1815,6 +1898,22 @@ NAPI_METHOD(iterator_init_sync) {
|
|
|
1815
1898
|
return result;
|
|
1816
1899
|
}
|
|
1817
1900
|
|
|
1901
|
+
NAPI_METHOD(iterator_refresh_sync) {
|
|
1902
|
+
NAPI_ARGV(1);
|
|
1903
|
+
|
|
1904
|
+
try {
|
|
1905
|
+
Iterator* iterator;
|
|
1906
|
+
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast<void**>(&iterator)));
|
|
1907
|
+
|
|
1908
|
+
ROCKS_STATUS_THROWS_NAPI(iterator->Refresh());
|
|
1909
|
+
} catch (const std::exception& e) {
|
|
1910
|
+
napi_throw_error(env, nullptr, e.what());
|
|
1911
|
+
return nullptr;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
return 0;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1818
1917
|
NAPI_METHOD(iterator_seek) {
|
|
1819
1918
|
NAPI_ARGV(3);
|
|
1820
1919
|
|
|
@@ -2363,6 +2462,7 @@ NAPI_INIT() {
|
|
|
2363
2462
|
NAPI_EXPORT_FUNCTION(db_flush_wal);
|
|
2364
2463
|
|
|
2365
2464
|
NAPI_EXPORT_FUNCTION(iterator_init_sync);
|
|
2465
|
+
NAPI_EXPORT_FUNCTION(iterator_refresh_sync);
|
|
2366
2466
|
NAPI_EXPORT_FUNCTION(iterator_seek);
|
|
2367
2467
|
NAPI_EXPORT_FUNCTION(iterator_seek_sync);
|
|
2368
2468
|
NAPI_EXPORT_FUNCTION(iterator_close_sync);
|
package/build.sh
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
+
# The Dockerfile targets x86-64 explicitly (znver3 march flags, prebuildify
|
|
5
|
+
# --arch x64), so the image must be built for linux/amd64 even on arm64 hosts
|
|
6
|
+
# (e.g. Apple Silicon), where it runs under emulation. Without this the native
|
|
7
|
+
# arm64 gcc rejects -march=znver3 ("unknown value 'znver3'") and the build fails.
|
|
8
|
+
PLATFORM=linux/amd64
|
|
9
|
+
|
|
10
|
+
# Build on the remote x86-64 docker host by default (avoids emulation on
|
|
11
|
+
# Apple Silicon). Override with DOCKER_HOST=... ./build.sh, or
|
|
12
|
+
# DOCKER_HOST= ./build.sh to use the local docker daemon.
|
|
13
|
+
export DOCKER_HOST="${DOCKER_HOST-ssh://nxtop@hq-test-srv1.nxt.io}"
|
|
14
|
+
|
|
4
15
|
echo "Initializing submodules..."
|
|
5
16
|
git submodule update --init
|
|
6
17
|
|
|
7
18
|
echo "Building image..."
|
|
8
|
-
|
|
19
|
+
# JOBS caps build parallelism for the memory-heavy rocksdb compile (default 8,
|
|
20
|
+
# see Dockerfile). Lower it (e.g. JOBS=4 ./build.sh) if the build still OOMs on
|
|
21
|
+
# a memory-constrained Docker, or raise it on a large host.
|
|
22
|
+
docker build --platform "$PLATFORM" ${JOBS:+--build-arg JOBS="$JOBS"} --iidfile prebuilds.iid .
|
|
9
23
|
|
|
10
24
|
echo "Extracting prebuilds from image..."
|
|
11
25
|
IMG=$(cat prebuilds.iid)
|
|
12
|
-
ID=$(docker create $IMG)
|
|
26
|
+
ID=$(docker create --platform "$PLATFORM" $IMG)
|
|
13
27
|
docker cp "$ID:/rocks-level/prebuilds" ./
|
|
14
28
|
|
|
15
29
|
echo "Cleaning up..."
|
package/index.js
CHANGED
|
@@ -224,7 +224,11 @@ class RocksLevel extends AbstractLevel {
|
|
|
224
224
|
callback = fromCallback(callback, kPromise)
|
|
225
225
|
|
|
226
226
|
try {
|
|
227
|
-
// TODO (
|
|
227
|
+
// TODO (perf): db_clear is a synchronous native call that blocks the event
|
|
228
|
+
// loop. The whole-range (limit === -1) path is a single DeleteRange (cheap),
|
|
229
|
+
// but the limited path iterates + writes WriteBatches on the JS thread, and
|
|
230
|
+
// neither is ref-counted against close(). Move to an async binding
|
|
231
|
+
// (runAsync) that takes a kRef like the other ops.
|
|
228
232
|
binding.db_clear(this[kContext], options ?? kEmpty)
|
|
229
233
|
process.nextTick(callback, null)
|
|
230
234
|
} catch (err) {
|
|
@@ -290,7 +294,7 @@ class RocksLevel extends AbstractLevel {
|
|
|
290
294
|
return binding.db_get_identity(this[kContext])
|
|
291
295
|
}
|
|
292
296
|
|
|
293
|
-
getProperty (property) {
|
|
297
|
+
getProperty (property, options) {
|
|
294
298
|
if (typeof property !== 'string') {
|
|
295
299
|
throw new TypeError("The first argument 'property' must be a string")
|
|
296
300
|
}
|
|
@@ -302,7 +306,7 @@ class RocksLevel extends AbstractLevel {
|
|
|
302
306
|
})
|
|
303
307
|
}
|
|
304
308
|
|
|
305
|
-
return binding.db_get_property(this[kContext], property)
|
|
309
|
+
return binding.db_get_property(this[kContext], property, options ?? kEmpty)
|
|
306
310
|
}
|
|
307
311
|
|
|
308
312
|
query (options, callback) {
|
package/iterator.js
CHANGED
|
@@ -126,6 +126,18 @@ class Iterator extends AbstractIterator {
|
|
|
126
126
|
|
|
127
127
|
// nxt API
|
|
128
128
|
|
|
129
|
+
_refreshSync () {
|
|
130
|
+
assert(this[kContext])
|
|
131
|
+
assert(!this[kBusy])
|
|
132
|
+
|
|
133
|
+
this[kFirst] = true
|
|
134
|
+
this[kCache] = kEmpty
|
|
135
|
+
this[kFinished] = false
|
|
136
|
+
this[kPosition] = 0
|
|
137
|
+
|
|
138
|
+
binding.iterator_refresh_sync(this[kContext])
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
_seekSync (target) {
|
|
130
142
|
assert(this[kContext])
|
|
131
143
|
assert(!this[kBusy])
|
package/max_rev_operator.h
CHANGED
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
#include <iostream>
|
|
8
8
|
|
|
9
|
+
// Compares two length-prefixed revision operands and returns <0, 0, >0.
|
|
10
|
+
//
|
|
11
|
+
// This MUST stay byte-for-byte order-compatible with the in-memory JS comparator
|
|
12
|
+
// (@nxtedition/util compareRev, lib/packages/util/src/compare-rev.ts), because
|
|
13
|
+
// RocksDB selects the durable winner with this operator while the application
|
|
14
|
+
// compares the same revisions in memory with the JS one — if they disagree, the
|
|
15
|
+
// stored "max revision" diverges from what the app believes is the max. A 500k
|
|
16
|
+
// randomized fuzz (leading zeros, INF, length ties, missing dashes) confirms
|
|
17
|
+
// parity. Revisions are `<number>-<id>` (e.g. `12-7a00`, `INF-…`) compared as:
|
|
18
|
+
// 1. INF sentinel: a number beginning with 'I' is +infinity (largest).
|
|
19
|
+
// 2. leading zeros are skipped so `01-x` == `1-x` in magnitude.
|
|
20
|
+
// 3. the number is compared digit-by-digit, terminated by '-'; the side whose
|
|
21
|
+
// number ends first (fewer significant digits) is smaller.
|
|
22
|
+
// 4. then the id is compared bytewise; finally the zero-stripped length breaks
|
|
23
|
+
// ties (a longer number = larger revision).
|
|
9
24
|
int compareRev(const rocksdb::Slice& a, const rocksdb::Slice& b) {
|
|
10
25
|
if (a.empty()) {
|
|
11
26
|
return b.empty() ? 0 : -1;
|
|
@@ -22,14 +37,35 @@ int compareRev(const rocksdb::Slice& a, const rocksdb::Slice& b) {
|
|
|
22
37
|
const std::size_t endA = 1 + std::min<std::size_t>(static_cast<unsigned char>(a[0]), a.size() - 1);
|
|
23
38
|
const std::size_t endB = 1 + std::min<std::size_t>(static_cast<unsigned char>(b[0]), b.size() - 1);
|
|
24
39
|
|
|
40
|
+
// INF-XXXX sorts above every numeric revision. Mirror the JS comparator's
|
|
41
|
+
// explicit sentinel rather than relying on 'I' (0x49) happening to exceed the
|
|
42
|
+
// digit bytes.
|
|
43
|
+
const bool infA = indexA < endA && a[indexA] == 'I';
|
|
44
|
+
const bool infB = indexB < endB && b[indexB] == 'I';
|
|
45
|
+
if (infA != infB) {
|
|
46
|
+
return infA ? 1 : -1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Skip leading zeroes, tracking the zero-stripped content length for the final
|
|
50
|
+
// tiebreak, so `01-x` and `1-x` compare as the same magnitude.
|
|
51
|
+
std::size_t lenA = endA - indexA;
|
|
52
|
+
std::size_t lenB = endB - indexB;
|
|
53
|
+
while (indexA < endA && a[indexA] == '0') {
|
|
54
|
+
++indexA;
|
|
55
|
+
--lenA;
|
|
56
|
+
}
|
|
57
|
+
while (indexB < endB && b[indexB] == '0') {
|
|
58
|
+
++indexB;
|
|
59
|
+
--lenB;
|
|
60
|
+
}
|
|
61
|
+
|
|
25
62
|
// Compare the revision number. Compare bytes as unsigned char: rocksdb::Slice
|
|
26
63
|
// operator[] returns (signed-on-most-platforms) char, so a byte >= 0x80 would
|
|
27
64
|
// otherwise sort as negative and order opposite to the JS comparator, which
|
|
28
65
|
// reads bytes as unsigned (Buffer[i] in 0..255). Keeping both sides unsigned
|
|
29
66
|
// ensures the in-memory ordering and this durable maxRev merge agree.
|
|
30
67
|
auto result = 0;
|
|
31
|
-
|
|
32
|
-
while (indexA < end && indexB < end) {
|
|
68
|
+
while (indexA < endA && indexB < endB) {
|
|
33
69
|
const unsigned char ac = static_cast<unsigned char>(a[indexA++]);
|
|
34
70
|
const unsigned char bc = static_cast<unsigned char>(b[indexB++]);
|
|
35
71
|
|
|
@@ -52,7 +88,7 @@ int compareRev(const rocksdb::Slice& a, const rocksdb::Slice& b) {
|
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
// Compare the rest (unsigned, for the same reason as the loop above).
|
|
55
|
-
while (indexA <
|
|
91
|
+
while (indexA < endA && indexB < endB) {
|
|
56
92
|
const unsigned char ac = static_cast<unsigned char>(a[indexA++]);
|
|
57
93
|
const unsigned char bc = static_cast<unsigned char>(b[indexB++]);
|
|
58
94
|
if (ac != bc) {
|
|
@@ -60,7 +96,7 @@ int compareRev(const rocksdb::Slice& a, const rocksdb::Slice& b) {
|
|
|
60
96
|
}
|
|
61
97
|
}
|
|
62
98
|
|
|
63
|
-
return static_cast<int>(
|
|
99
|
+
return static_cast<int>(lenA) - static_cast<int>(lenB);
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
class MaxRevOperator : public rocksdb::MergeOperator {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/rocksdb",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.6",
|
|
4
4
|
"description": "A low-level Node.js RocksDB binding",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"test": "standard && (nyc -s tape test/*-test.js | faucet) && nyc report",
|
|
10
10
|
"test-prebuild": "cross-env PREBUILDS_ONLY=1 npm t",
|
|
11
11
|
"prebuildify": "JOBS=8 prebuildify --napi --strip",
|
|
12
|
-
"rebuild": "JOBS=8 npm run install --build-from-source"
|
|
12
|
+
"rebuild": "JOBS=8 npm run install --build-from-source",
|
|
13
|
+
"release": "./release.sh"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"abstract-level": "^1.0.2",
|
|
Binary file
|
|
Binary file
|
package/release.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
cd "$(dirname "$0")"
|
|
5
|
+
|
|
6
|
+
# Fail fast: npm version refuses a dirty tree, so check before the slow builds.
|
|
7
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
8
|
+
echo "Working tree is not clean, commit or stash changes first." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Keep the local arm64 build targeting the same node version as the Docker image.
|
|
13
|
+
NODE_TARGET=$(sed -n 's/^FROM node:\([0-9.]*\).*/\1/p' Dockerfile)
|
|
14
|
+
if [ -z "$NODE_TARGET" ]; then
|
|
15
|
+
echo "Could not determine node version from Dockerfile." >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "Building linux prebuilds (docker)..."
|
|
20
|
+
./build.sh
|
|
21
|
+
|
|
22
|
+
echo "Building darwin-arm64 prebuilds (node $NODE_TARGET)..."
|
|
23
|
+
JOBS=16 npx prebuildify -t "$NODE_TARGET" --napi --strip --arch arm64
|
|
24
|
+
|
|
25
|
+
read -r -p "Version bump (patch/minor/major): " BUMP
|
|
26
|
+
case "$BUMP" in
|
|
27
|
+
patch | minor | major) ;;
|
|
28
|
+
*)
|
|
29
|
+
echo "Invalid bump: '$BUMP' (expected patch, minor or major)" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
npm version "$BUMP"
|
|
35
|
+
npm publish
|
|
36
|
+
|
|
37
|
+
git push
|
|
38
|
+
git push --tags
|
|
39
|
+
|
|
40
|
+
echo "Published $(node -p "require('./package.json').version")."
|
package/util.h
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#include <rocksdb/status.h>
|
|
10
10
|
|
|
11
11
|
#include <array>
|
|
12
|
+
#include <memory>
|
|
12
13
|
#include <optional>
|
|
13
14
|
#include <string>
|
|
14
15
|
|
|
@@ -439,9 +440,18 @@ napi_status Convert(napi_env env,
|
|
|
439
440
|
bool unsafe = false) {
|
|
440
441
|
if (encoding == Encoding::Buffer) {
|
|
441
442
|
if (unsafe) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
// The heap PinnableSlice is owned by the finalizer, which N-API only
|
|
444
|
+
// registers when the external buffer is created successfully. Hold it in a
|
|
445
|
+
// unique_ptr and release ownership only on success, so a failed
|
|
446
|
+
// napi_create_external_buffer does not leak it (and the block/memtable
|
|
447
|
+
// region it pinned).
|
|
448
|
+
auto s2 = std::make_unique<rocksdb::PinnableSlice>(std::move(s));
|
|
449
|
+
const auto status = napi_create_external_buffer(env, s2->size(), const_cast<char*>(s2->data()),
|
|
450
|
+
Finalize<rocksdb::PinnableSlice>, s2.get(), &result);
|
|
451
|
+
if (status == napi_ok) {
|
|
452
|
+
s2.release();
|
|
453
|
+
}
|
|
454
|
+
return status;
|
|
445
455
|
} else {
|
|
446
456
|
return napi_create_buffer_copy(env, s.size(), s.data(), nullptr, &result);
|
|
447
457
|
}
|
package/util.js
CHANGED
|
@@ -2,73 +2,3 @@
|
|
|
2
2
|
|
|
3
3
|
exports.kRef = Symbol('ref')
|
|
4
4
|
exports.kUnref = Symbol('unref')
|
|
5
|
-
|
|
6
|
-
function handleMany (sizes, data, options) {
|
|
7
|
-
const { valueEncoding } = options ?? {}
|
|
8
|
-
|
|
9
|
-
data ??= Buffer.alloc(0)
|
|
10
|
-
sizes ??= Buffer.alloc(0)
|
|
11
|
-
|
|
12
|
-
const rows = []
|
|
13
|
-
let offset = 0
|
|
14
|
-
const sizes32 = new Int32Array(sizes.buffer, sizes.byteOffset, sizes.byteLength / 4)
|
|
15
|
-
for (let n = 0; n < sizes32.length; n++) {
|
|
16
|
-
const size = sizes32[n]
|
|
17
|
-
const encoding = valueEncoding
|
|
18
|
-
if (size < 0) {
|
|
19
|
-
rows.push(undefined)
|
|
20
|
-
} else {
|
|
21
|
-
if (!encoding || encoding === 'buffer') {
|
|
22
|
-
rows.push(data.subarray(offset, offset + size))
|
|
23
|
-
} else if (encoding === 'slice') {
|
|
24
|
-
rows.push({ buffer: data, byteOffset: offset, byteLength: size })
|
|
25
|
-
} else {
|
|
26
|
-
rows.push(data.toString(encoding, offset, offset + size))
|
|
27
|
-
}
|
|
28
|
-
offset += size
|
|
29
|
-
if (offset & 0x7) {
|
|
30
|
-
offset = (offset | 0x7) + 1
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return rows
|
|
36
|
-
}
|
|
37
|
-
function handleNextv (err, sizes, buffer, finished, options, callback) {
|
|
38
|
-
const { keyEncoding, valueEncoding } = options ?? {}
|
|
39
|
-
|
|
40
|
-
if (err) {
|
|
41
|
-
callback(err)
|
|
42
|
-
} else {
|
|
43
|
-
buffer ??= Buffer.alloc(0)
|
|
44
|
-
sizes ??= Buffer.alloc(0)
|
|
45
|
-
|
|
46
|
-
const rows = []
|
|
47
|
-
let offset = 0
|
|
48
|
-
const sizes32 = new Int32Array(sizes.buffer, sizes.byteOffset, sizes.byteLength / 4)
|
|
49
|
-
for (let n = 0; n < sizes32.length; n++) {
|
|
50
|
-
const size = sizes32[n]
|
|
51
|
-
const encoding = n & 1 ? valueEncoding : keyEncoding
|
|
52
|
-
if (size < 0) {
|
|
53
|
-
rows.push(undefined)
|
|
54
|
-
} else {
|
|
55
|
-
if (!encoding || encoding === 'buffer') {
|
|
56
|
-
rows.push(buffer.subarray(offset, offset + size))
|
|
57
|
-
} else if (encoding === 'slice') {
|
|
58
|
-
rows.push({ buffer, byteOffset: offset, byteLength: size })
|
|
59
|
-
} else {
|
|
60
|
-
rows.push(buffer.toString(encoding, offset, offset + size))
|
|
61
|
-
}
|
|
62
|
-
offset += size
|
|
63
|
-
if (offset & 0x7) {
|
|
64
|
-
offset = (offset | 0x7) + 1
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
callback(null, rows, finished)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
exports.handleMany = handleMany
|
|
74
|
-
exports.handleNextv = handleNextv
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm pack:*)",
|
|
5
|
-
"Bash(node:*)",
|
|
6
|
-
"Bash(npm view:*)",
|
|
7
|
-
"Bash(npm info:*)",
|
|
8
|
-
"Bash(nm:*)",
|
|
9
|
-
"Bash(grep:*)",
|
|
10
|
-
"Bash(while read f)",
|
|
11
|
-
"Bash(do basename \"$f\")",
|
|
12
|
-
"Bash(done)",
|
|
13
|
-
"Bash(ls:*)",
|
|
14
|
-
"Bash(git submodule update:*)",
|
|
15
|
-
"Bash(git submodule:*)",
|
|
16
|
-
"Bash(yarn build:*)",
|
|
17
|
-
"Bash(yarn rebuild:*)",
|
|
18
|
-
"Bash(npx tape:*)",
|
|
19
|
-
"Bash(echo \"=== In binding.gyp ===\" grep \"absl/base/internal\" binding.gyp echo \"\" echo \"=== Available \\(non-test\\) files ===\" ls deps/abseil-cpp/absl/base/internal/*.cc)",
|
|
20
|
-
"Bash(echo \"=== Checking for abseil source requirements ===\" echo \"\" echo \"profiling sources:\" ls /Users/ronagy/GitHub/nxtedition/rocks-level/deps/abseil-cpp/absl/profiling/internal/*.cc)",
|
|
21
|
-
"Bash(echo \"none found\" echo \"\" echo \"crc sources:\" ls /Users/ronagy/GitHub/nxtedition/rocks-level/deps/abseil-cpp/absl/crc/internal/*.cc)",
|
|
22
|
-
"Bash(echo \"NONE INCLUDED!\" echo \"\" echo \"=== Available log sources ===\" ls /Users/ronagy/GitHub/nxtedition/rocks-level/deps/abseil-cpp/absl/log/*.cc)",
|
|
23
|
-
"Bash(head -20 echo \"\" echo \"=== Log internal sources ===\" ls /Users/ronagy/GitHub/nxtedition/rocks-level/deps/abseil-cpp/absl/log/internal/*.cc)",
|
|
24
|
-
"Bash(echo \"=== Required log sources \\(non-test\\) ===\" ls /Users/ronagy/GitHub/nxtedition/rocks-level/deps/abseil-cpp/absl/log/*.cc)",
|
|
25
|
-
"Bash(CFLAGS=\"-g -O0\" CXXFLAGS=\"-g -O0\" npm run rebuild)",
|
|
26
|
-
"Bash(node-gyp rebuild:*)",
|
|
27
|
-
"Bash(npx node-gyp rebuild:*)",
|
|
28
|
-
"Bash(git rm:*)",
|
|
29
|
-
"WebFetch(domain:raw.githubusercontent.com)",
|
|
30
|
-
"Bash(git log:*)",
|
|
31
|
-
"WebFetch(domain:github.com)",
|
|
32
|
-
"Bash(npm run rebuild:*)"
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
}
|