@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 CHANGED
@@ -54,7 +54,7 @@ class NullLogger : public rocksdb::Logger {
54
54
 
55
55
  struct Database;
56
56
  class Iterator;
57
- class Updates;
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
- int32_t highWaterMarkBytes = std::numeric_limits<int32_t>::max();
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
- if (!Increment()) {
648
- // Hit the user's `limit` option: terminal, and flag that it was a
649
- // limit rather than natural exhaustion.
650
- state.finished = true;
651
- state.limited = true;
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
- if (!Increment()) {
776
- // Hit the user's `limit` option: terminal, and flag that it was a limit
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
- // We should have an env_cleanup_hook for closing iterators...
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
- return Iterator::create(env, argv[0], argv[1])->nextv(env, std::numeric_limits<uint32_t>::max());
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
- uint32_t cacheSize = 8 << 20;
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
- uint32_t cacheSize = -1;
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
- uint32_t cacheSize = -1;
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
- uint32_t walTTL = 0;
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<uint32_t>(std::ceil(walTTL / 1e3));
1370
+ dbOptions.WAL_ttl_seconds = static_cast<uint64_t>(std::ceil(walTTL / 1e3));
1307
1371
 
1308
- uint32_t walSizeLimit = 0;
1372
+ uint64_t walSizeLimit = 0;
1309
1373
  NAPI_STATUS_THROWS(GetProperty(env, options, "walSizeLimit", walSizeLimit));
1310
- dbOptions.WAL_size_limit_MB = static_cast<uint32_t>(std::ceil(walSizeLimit / 1e6));
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 (auto n = 0; n < count; n++) {
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(2);
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
- docker build --iidfile prebuilds.iid .
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 (fix): Use batch + DeleteRange...
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])
@@ -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
- const auto end = std::min(endA, endB);
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 < end && indexB < end) {
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>(endA) - static_cast<int>(endB);
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.2",
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",
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
- auto s2 = new rocksdb::PinnableSlice(std::move(s));
443
- return napi_create_external_buffer(env, s2->size(), const_cast<char*>(s2->data()),
444
- Finalize<rocksdb::PinnableSlice>, s2, &result);
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
- }