@nxtedition/rocksdb 6.0.3 → 7.0.0-alpha.1

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
@@ -30,6 +30,8 @@ class NullLogger : public rocksdb::Logger {
30
30
 
31
31
  struct Database;
32
32
  struct Iterator;
33
+ struct Updates;
34
+ struct Snapshot;
33
35
 
34
36
  #define NAPI_STATUS_RETURN(call) \
35
37
  { \
@@ -47,6 +49,10 @@ struct Iterator;
47
49
  Iterator* iterator = nullptr; \
48
50
  NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&iterator));
49
51
 
52
+ #define NAPI_UPDATES_CONTEXT() \
53
+ Updates* updates = nullptr; \
54
+ NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&updates));
55
+
50
56
  #define NAPI_BATCH_CONTEXT() \
51
57
  rocksdb::WriteBatch* batch = nullptr; \
52
58
  NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&batch));
@@ -133,6 +139,18 @@ static std::optional<int> Int32Property(napi_env env, napi_value obj, const std:
133
139
  return {};
134
140
  }
135
141
 
142
+ static std::optional<int64_t> Int64Property(napi_env env, napi_value obj, const std::string_view& key) {
143
+ if (HasProperty(env, obj, key.data())) {
144
+ const auto value = GetProperty(env, obj, key.data());
145
+ int64_t result;
146
+ bool lossless;
147
+ napi_get_value_bigint_int64(env, value, &result, &lossless);
148
+ return result;
149
+ }
150
+
151
+ return {};
152
+ }
153
+
136
154
  static std::string ToString(napi_env env, napi_value from, const std::string& defaultValue = "") {
137
155
  if (IsString(env, from)) {
138
156
  size_t length = 0;
@@ -210,6 +228,18 @@ napi_status Convert(napi_env env, rocksdb::PinnableSlice&& s, bool asBuffer, nap
210
228
  }
211
229
  }
212
230
 
231
+ napi_status Convert(napi_env env, std::optional<std::string>&& s, bool asBuffer, napi_value& result) {
232
+ if (!s) {
233
+ return napi_get_null(env, &result);
234
+ } else if (asBuffer) {
235
+ auto ptr = new std::string(std::move(*s));
236
+ return napi_create_external_buffer(env, ptr->size(), const_cast<char*>(ptr->data()), Finalize<std::string>, ptr,
237
+ &result);
238
+ } else {
239
+ return napi_create_string_utf8(env, s->data(), s->size(), &result);
240
+ }
241
+ }
242
+
213
243
  struct NapiSlice : public rocksdb::Slice {
214
244
  std::unique_ptr<char[]> heap_;
215
245
  std::array<char, 128> stack_;
@@ -318,6 +348,26 @@ struct Database {
318
348
  DecrementPriorityWork(env);
319
349
  }
320
350
 
351
+ void AttachSnapshot(napi_env env, Snapshot* snapshot) {
352
+ snapshots_.insert(snapshot);
353
+ IncrementPriorityWork(env);
354
+ }
355
+
356
+ void DetachSnapshot(napi_env env, Snapshot* snapshot) {
357
+ snapshots_.erase(snapshot);
358
+ DecrementPriorityWork(env);
359
+ }
360
+
361
+ void AttachUpdates(napi_env env, Updates* updates) {
362
+ updates_.insert(updates);
363
+ IncrementPriorityWork(env);
364
+ }
365
+
366
+ void DetachUpdates(napi_env env, Updates* updates) {
367
+ updates_.erase(updates);
368
+ DecrementPriorityWork(env);
369
+ }
370
+
321
371
  void IncrementPriorityWork(napi_env env) { napi_reference_ref(env, priorityRef_, &priorityWork_); }
322
372
 
323
373
  void DecrementPriorityWork(napi_env env) {
@@ -334,12 +384,42 @@ struct Database {
334
384
  std::unique_ptr<rocksdb::DB> db_;
335
385
  Worker* pendingCloseWorker_;
336
386
  std::set<Iterator*> iterators_;
387
+ std::set<Snapshot*> snapshots_;
388
+ std::set<Updates*> updates_;
337
389
  napi_ref priorityRef_;
338
390
 
339
391
  private:
340
392
  uint32_t priorityWork_ = 0;
341
393
  };
342
394
 
395
+ struct Snapshot {
396
+ Snapshot(Database* database)
397
+ : snapshot_(database->db_->GetSnapshot(), [=](auto ptr) { database->db_->ReleaseSnapshot(ptr); }),
398
+ database_(database) {}
399
+
400
+ void Close() { snapshot_.reset(); }
401
+
402
+ void Attach(napi_env env, napi_value context) {
403
+ napi_create_reference(env, context, 1, &ref_);
404
+ database_->AttachSnapshot(env, this);
405
+ }
406
+
407
+ void Detach(napi_env env) {
408
+ database_->DetachSnapshot(env, this);
409
+ if (ref_) {
410
+ napi_delete_reference(env, ref_);
411
+ }
412
+ }
413
+
414
+ int64_t GetSequenceNumber() const { return snapshot_->GetSequenceNumber(); }
415
+
416
+ std::shared_ptr<const rocksdb::Snapshot> snapshot_;
417
+
418
+ private:
419
+ Database* database_;
420
+ napi_ref ref_ = nullptr;
421
+ };
422
+
343
423
  struct BaseIterator {
344
424
  BaseIterator(Database* database,
345
425
  const bool reverse,
@@ -348,13 +428,9 @@ struct BaseIterator {
348
428
  const std::optional<std::string>& gt,
349
429
  const std::optional<std::string>& gte,
350
430
  const int limit,
351
- const bool fillCache)
352
- : database_(database),
353
- snapshot_(database_->db_->GetSnapshot(),
354
- [this](const rocksdb::Snapshot* ptr) { database_->db_->ReleaseSnapshot(ptr); }),
355
- reverse_(reverse),
356
- limit_(limit),
357
- fillCache_(fillCache) {
431
+ const bool fillCache,
432
+ std::shared_ptr<const rocksdb::Snapshot> snapshot)
433
+ : database_(database), snapshot_(snapshot), reverse_(reverse), limit_(limit), fillCache_(fillCache) {
358
434
  if (lte) {
359
435
  upper_bound_ = rocksdb::PinnableSlice();
360
436
  *upper_bound_->GetSelf() = std::move(*lte) + '\0';
@@ -434,6 +510,7 @@ struct BaseIterator {
434
510
  rocksdb::Status Status() const { return iterator_->status(); }
435
511
 
436
512
  Database* database_;
513
+ std::shared_ptr<const rocksdb::Snapshot> snapshot_;
437
514
 
438
515
  private:
439
516
  void Init() {
@@ -453,7 +530,6 @@ struct BaseIterator {
453
530
 
454
531
  std::optional<rocksdb::PinnableSlice> lower_bound_;
455
532
  std::optional<rocksdb::PinnableSlice> upper_bound_;
456
- std::shared_ptr<const rocksdb::Snapshot> snapshot_;
457
533
  std::unique_ptr<rocksdb::Iterator> iterator_;
458
534
  const bool reverse_;
459
535
  const int limit_;
@@ -474,8 +550,9 @@ struct Iterator final : public BaseIterator {
474
550
  const bool fillCache,
475
551
  const bool keyAsBuffer,
476
552
  const bool valueAsBuffer,
477
- const uint32_t highWaterMarkBytes)
478
- : BaseIterator(database, reverse, lt, lte, gt, gte, limit, fillCache),
553
+ const uint32_t highWaterMarkBytes,
554
+ std::shared_ptr<const rocksdb::Snapshot> snapshot)
555
+ : BaseIterator(database, reverse, lt, lte, gt, gte, limit, fillCache, snapshot),
479
556
  keys_(keys),
480
557
  values_(values),
481
558
  keyAsBuffer_(keyAsBuffer),
@@ -505,6 +582,34 @@ struct Iterator final : public BaseIterator {
505
582
  napi_ref ref_ = nullptr;
506
583
  };
507
584
 
585
+ struct Updates {
586
+ Updates(Database* database, const bool keyAsBuffer, const bool valueAsBuffer, int64_t seqNumber)
587
+ : database_(database), keyAsBuffer_(keyAsBuffer), valueAsBuffer_(valueAsBuffer), seqNumber_(seqNumber) {}
588
+
589
+ void Close() { iterator_.reset(); }
590
+
591
+ void Attach(napi_env env, napi_value context) {
592
+ napi_create_reference(env, context, 1, &ref_);
593
+ database_->AttachUpdates(env, this);
594
+ }
595
+
596
+ void Detach(napi_env env) {
597
+ database_->DetachUpdates(env, this);
598
+ if (ref_) {
599
+ napi_delete_reference(env, ref_);
600
+ }
601
+ }
602
+
603
+ Database* database_;
604
+ const bool keyAsBuffer_;
605
+ const bool valueAsBuffer_;
606
+ int64_t seqNumber_;
607
+ std::unique_ptr<rocksdb::TransactionLogIterator> iterator_;
608
+
609
+ private:
610
+ napi_ref ref_ = nullptr;
611
+ };
612
+
508
613
  /**
509
614
  * Hook for when the environment exits. This hook will be called after
510
615
  * already-scheduled napi_async_work items have finished, which gives us
@@ -521,8 +626,18 @@ static void env_cleanup_hook(void* arg) {
521
626
  // following code must be a safe noop if called before db_open() or after
522
627
  // db_close().
523
628
  if (database && database->db_) {
524
- // TODO: does not do `napi_delete_reference(env, iterator->ref_)`. Problem?
525
629
  for (auto it : database->iterators_) {
630
+ // TODO: does not do `napi_delete_reference`. Problem?
631
+ it->Close();
632
+ }
633
+
634
+ for (auto it : database->snapshots_) {
635
+ // TODO: does not do `napi_delete_reference`. Problem?
636
+ it->Close();
637
+ }
638
+
639
+ for (auto it : database->updates_) {
640
+ // TODO: does not do `napi_delete_reference`. Problem?
526
641
  it->Close();
527
642
  }
528
643
 
@@ -587,11 +702,48 @@ NAPI_METHOD(db_open) {
587
702
  options.IncreaseParallelism(
588
703
  Uint32Property(env, argv[2], "parallelism").value_or(std::thread::hardware_concurrency() / 2));
589
704
 
705
+ options.avoid_unnecessary_blocking_io = true;
706
+
707
+ const auto memtable_memory_budget = Uint32Property(env, argv[2], "memtableMemoryBudget").value_or(256 * 1024 * 1024);
708
+
709
+ const auto compaction = StringProperty(env, argv[2], "compaction").value_or("level");
710
+
711
+ if (compaction == "universal") {
712
+ options.write_buffer_size = static_cast<size_t>(memtable_memory_budget / 4);
713
+ // merge two memtables when flushing to L0
714
+ options.min_write_buffer_number_to_merge = 2;
715
+ // this means we'll use 50% extra memory in the worst case, but will reduce
716
+ // write stalls.
717
+ options.max_write_buffer_number = 6;
718
+ // universal style compaction
719
+ options.compaction_style = rocksdb::kCompactionStyleUniversal;
720
+ options.compaction_options_universal.compression_size_percent = 80;
721
+ } else {
722
+ // merge two memtables when flushing to L0
723
+ options.min_write_buffer_number_to_merge = 2;
724
+ // this means we'll use 50% extra memory in the worst case, but will reduce
725
+ // write stalls.
726
+ options.max_write_buffer_number = 6;
727
+ // start flushing L0->L1 as soon as possible. each file on level0 is
728
+ // (memtable_memory_budget / 2). This will flush level 0 when it's bigger than
729
+ // memtable_memory_budget.
730
+ options.level0_file_num_compaction_trigger = 2;
731
+ // doesn't really matter much, but we don't want to create too many files
732
+ options.target_file_size_base = memtable_memory_budget / 8;
733
+ // make Level1 size equal to Level0 size, so that L0->L1 compactions are fast
734
+ options.max_bytes_for_level_base = memtable_memory_budget;
735
+
736
+ // level style compaction
737
+ options.compaction_style = rocksdb::kCompactionStyleLevel;
738
+
739
+ // TODO (perf): only compress levels >= 2
740
+ }
741
+
590
742
  const auto location = ToString(env, argv[1]);
591
743
  options.create_if_missing = BooleanProperty(env, argv[2], "createIfMissing").value_or(true);
592
744
  options.error_if_exists = BooleanProperty(env, argv[2], "errorIfExists").value_or(false);
593
- options.compression = BooleanProperty(env, argv[2], "compression").value_or((true)) ? rocksdb::kZSTD
594
- : rocksdb::kNoCompression;
745
+ options.compression =
746
+ BooleanProperty(env, argv[2], "compression").value_or((true)) ? rocksdb::kZSTD : rocksdb::kNoCompression;
595
747
  if (options.compression == rocksdb::kZSTD) {
596
748
  options.compression_opts.max_dict_bytes = 16 * 1024;
597
749
  options.compression_opts.zstd_max_train_bytes = 16 * 1024 * 100;
@@ -603,6 +755,9 @@ NAPI_METHOD(db_open) {
603
755
  options.max_background_jobs =
604
756
  Uint32Property(env, argv[2], "maxBackgroundJobs").value_or(std::thread::hardware_concurrency() / 4);
605
757
 
758
+ options.WAL_ttl_seconds = Uint32Property(env, argv[2], "walTTL").value_or(0) / 1e3;
759
+ options.WAL_size_limit_MB = Uint32Property(env, argv[2], "walSizeLimit").value_or(0) / 1e6;
760
+
606
761
  // TODO: Consider direct IO (https://github.com/facebook/rocksdb/wiki/Direct-IO) once
607
762
  // secondary compressed cache is stable.
608
763
 
@@ -647,9 +802,21 @@ NAPI_METHOD(db_open) {
647
802
  tableOptions.cache_index_and_filter_blocks = false;
648
803
  }
649
804
 
805
+ const auto optimize = StringProperty(env, argv[2], "optimize").value_or("");
806
+
807
+ if (optimize == "point-lookup") {
808
+ tableOptions.data_block_index_type = rocksdb::BlockBasedTableOptions::kDataBlockBinaryAndHash;
809
+ tableOptions.data_block_hash_table_util_ratio = 0.75;
810
+ tableOptions.filter_policy.reset(rocksdb::NewRibbonFilterPolicy(10));
811
+
812
+ options.memtable_prefix_bloom_size_ratio = 0.02;
813
+ options.memtable_whole_key_filtering = true;
814
+ } else {
815
+ tableOptions.filter_policy.reset(rocksdb::NewRibbonFilterPolicy(10));
816
+ }
817
+
650
818
  tableOptions.block_size = Uint32Property(env, argv[2], "blockSize").value_or(4096);
651
819
  tableOptions.block_restart_interval = Uint32Property(env, argv[2], "blockRestartInterval").value_or(16);
652
- tableOptions.filter_policy.reset(rocksdb::NewRibbonFilterPolicy(10));
653
820
  tableOptions.format_version = 5;
654
821
  tableOptions.checksum = rocksdb::kXXH3;
655
822
  tableOptions.optimize_filters_for_memory = BooleanProperty(env, argv[2], "optimizeFiltersForMemory").value_or(true);
@@ -692,6 +859,136 @@ NAPI_METHOD(db_close) {
692
859
  return 0;
693
860
  }
694
861
 
862
+ struct UpdateNextWorker final : public rocksdb::WriteBatch::Handler, public Worker {
863
+ UpdateNextWorker(napi_env env, Updates* updates, napi_value callback)
864
+ : Worker(env, updates->database_, callback, "rocks_level.db.get"), updates_(updates) {
865
+ database_->IncrementPriorityWork(env);
866
+ }
867
+
868
+ rocksdb::Status Execute(Database& database) override {
869
+ rocksdb::TransactionLogIterator::ReadOptions options;
870
+
871
+ if (!updates_->iterator_) {
872
+ const auto status = database_->db_->GetUpdatesSince(updates_->seqNumber_, &updates_->iterator_, options);
873
+ if (!status.ok()) {
874
+ return status;
875
+ }
876
+ }
877
+
878
+ if (!updates_->iterator_->Valid()) {
879
+ return rocksdb::Status::OK();
880
+ }
881
+
882
+ auto batch = updates_->iterator_->GetBatch();
883
+
884
+ updates_->seqNumber_ = batch.sequence;
885
+
886
+ const auto status = batch.writeBatchPtr->Iterate(this);
887
+ if (!status.ok()) {
888
+ return status;
889
+ }
890
+
891
+ updates_->iterator_->Next();
892
+
893
+ return rocksdb::Status::OK();
894
+ }
895
+
896
+ napi_status OnOk(napi_env env, napi_value callback) override {
897
+ const auto size = cache_.size();
898
+ napi_value result;
899
+
900
+ if (cache_.size()) {
901
+ NAPI_STATUS_RETURN(napi_create_array_with_length(env, size, &result));
902
+
903
+ for (size_t idx = 0; idx < cache_.size(); idx += 2) {
904
+ napi_value key;
905
+ napi_value val;
906
+
907
+ NAPI_STATUS_RETURN(Convert(env, std::move(cache_[idx + 0]), updates_->keyAsBuffer_, key));
908
+ NAPI_STATUS_RETURN(Convert(env, std::move(cache_[idx + 1]), updates_->valueAsBuffer_, val));
909
+
910
+ NAPI_STATUS_RETURN(napi_set_element(env, result, static_cast<int>(idx + 0), key));
911
+ NAPI_STATUS_RETURN(napi_set_element(env, result, static_cast<int>(idx + 1), val));
912
+ }
913
+
914
+ cache_.clear();
915
+ } else {
916
+ NAPI_STATUS_RETURN(napi_get_null(env, &result));
917
+ }
918
+
919
+ napi_value argv[3];
920
+ NAPI_STATUS_RETURN(napi_get_null(env, &argv[0]));
921
+ argv[1] = result;
922
+ NAPI_STATUS_RETURN(napi_create_bigint_int64(env, updates_->seqNumber_, &argv[2]));
923
+ return CallFunction(env, callback, 3, argv);
924
+ }
925
+
926
+ void Destroy(napi_env env) override {
927
+ database_->DecrementPriorityWork(env);
928
+ Worker::Destroy(env);
929
+ }
930
+
931
+ void Put(const rocksdb::Slice& key, const rocksdb::Slice& value) override {
932
+ cache_.emplace_back(key.ToString());
933
+ cache_.emplace_back(value.ToString());
934
+ }
935
+
936
+ void Delete(const rocksdb::Slice& key) override {
937
+ cache_.emplace_back(key.ToString());
938
+ cache_.emplace_back(std::nullopt);
939
+ }
940
+
941
+ bool Continue() override { return true; }
942
+
943
+ private:
944
+ std::vector<std::optional<std::string>> cache_;
945
+ Updates* updates_;
946
+ };
947
+
948
+ NAPI_METHOD(updates_init) {
949
+ NAPI_ARGV(2);
950
+ NAPI_DB_CONTEXT();
951
+
952
+ const auto options = argv[1];
953
+
954
+ const bool keyAsBuffer = EncodingIsBuffer(env, options, "keyEncoding");
955
+ const bool valueAsBuffer = EncodingIsBuffer(env, options, "valueEncoding");
956
+ const auto seqNumber = Int64Property(env, options, "since").value_or(database->db_->GetLatestSequenceNumber());
957
+
958
+ auto updates = std::make_unique<Updates>(database, keyAsBuffer, valueAsBuffer, seqNumber);
959
+
960
+ napi_value result;
961
+ NAPI_STATUS_THROWS(napi_create_external(env, updates.get(), Finalize<Updates>, updates.get(), &result));
962
+
963
+ // Prevent GC of JS object before the iterator is closed (explicitly or on
964
+ // db close) and keep track of non-closed iterators to end them on db close.
965
+ updates.release()->Attach(env, result);
966
+
967
+ return result;
968
+ }
969
+
970
+ NAPI_METHOD(updates_next) {
971
+ NAPI_ARGV(2);
972
+ NAPI_UPDATES_CONTEXT();
973
+
974
+ const auto callback = argv[1];
975
+
976
+ auto worker = new UpdateNextWorker(env, updates, callback);
977
+ worker->Queue(env);
978
+
979
+ return 0;
980
+ }
981
+
982
+ NAPI_METHOD(updates_close) {
983
+ NAPI_ARGV(1);
984
+ NAPI_UPDATES_CONTEXT();
985
+
986
+ updates->Detach(env);
987
+ updates->Close();
988
+
989
+ return 0;
990
+ }
991
+
695
992
  NAPI_METHOD(db_put) {
696
993
  NAPI_ARGV(4);
697
994
  NAPI_DB_CONTEXT();
@@ -782,8 +1079,7 @@ struct GetManyWorker final : public Worker {
782
1079
  keys_(std::move(keys)),
783
1080
  valueAsBuffer_(valueAsBuffer),
784
1081
  fillCache_(fillCache),
785
- snapshot_(database_->db_->GetSnapshot(),
786
- [this](const rocksdb::Snapshot* ptr) { database_->db_->ReleaseSnapshot(ptr); }) {
1082
+ snapshot_(database_->db_->GetSnapshot(), [=](const auto ptr) { database_->db_->ReleaseSnapshot(ptr); }) {
787
1083
  database_->IncrementPriorityWork(env);
788
1084
  }
789
1085
 
@@ -910,7 +1206,7 @@ NAPI_METHOD(db_clear) {
910
1206
  const auto lte = StringProperty(env, argv[1], "lte");
911
1207
  const auto gt = StringProperty(env, argv[1], "gt");
912
1208
  const auto gte = StringProperty(env, argv[1], "gte");
913
-
1209
+
914
1210
  if (!reverse && limit == -1 && (lte || lt)) {
915
1211
  rocksdb::Slice begin;
916
1212
  if (gte) {
@@ -938,7 +1234,8 @@ NAPI_METHOD(db_clear) {
938
1234
  } else {
939
1235
  // TODO (perf): Use DeleteRange.
940
1236
 
941
- BaseIterator it(database, reverse, lt, lte, gt, gte, limit, false);
1237
+ Snapshot snapshot(database);
1238
+ BaseIterator it(database, reverse, lt, lte, gt, gte, limit, false, snapshot.snapshot_);
942
1239
 
943
1240
  it.SeekToRange();
944
1241
 
@@ -994,6 +1291,18 @@ NAPI_METHOD(db_get_property) {
994
1291
  return result;
995
1292
  }
996
1293
 
1294
+ NAPI_METHOD(db_get_latest_sequence_number) {
1295
+ NAPI_ARGV(1);
1296
+ NAPI_DB_CONTEXT();
1297
+
1298
+ const auto seq = database->db_->GetLatestSequenceNumber();
1299
+
1300
+ napi_value result;
1301
+ NAPI_STATUS_THROWS(napi_create_bigint_int64(env, seq, &result));
1302
+
1303
+ return result;
1304
+ }
1305
+
997
1306
  NAPI_METHOD(iterator_init) {
998
1307
  NAPI_ARGV(2);
999
1308
  NAPI_DB_CONTEXT();
@@ -1013,8 +1322,20 @@ NAPI_METHOD(iterator_init) {
1013
1322
  const auto gt = StringProperty(env, options, "gt");
1014
1323
  const auto gte = StringProperty(env, options, "gte");
1015
1324
 
1325
+ std::shared_ptr<const rocksdb::Snapshot> snapshotRaw;
1326
+
1327
+ if (HasProperty(env, options, "snapshot")) {
1328
+ const auto property = GetProperty(env, options, "snapshot");
1329
+ Snapshot* snapshot;
1330
+ NAPI_STATUS_THROWS(napi_get_value_external(env, property, reinterpret_cast<void**>(&snapshot)));
1331
+ snapshotRaw = snapshot->snapshot_;
1332
+ } else {
1333
+ Snapshot snapshot(database);
1334
+ snapshotRaw = snapshot.snapshot_;
1335
+ }
1336
+
1016
1337
  auto iterator = std::make_unique<Iterator>(database, reverse, keys, values, limit, lt, lte, gt, gte, fillCache,
1017
- keyAsBuffer, valueAsBuffer, highWaterMarkBytes);
1338
+ keyAsBuffer, valueAsBuffer, highWaterMarkBytes, std::move(snapshotRaw));
1018
1339
 
1019
1340
  napi_value result;
1020
1341
  NAPI_STATUS_THROWS(napi_create_external(env, iterator.get(), Finalize<Iterator>, iterator.get(), &result));
@@ -1049,6 +1370,18 @@ NAPI_METHOD(iterator_close) {
1049
1370
  return 0;
1050
1371
  }
1051
1372
 
1373
+ NAPI_METHOD(iterator_get_sequence) {
1374
+ NAPI_ARGV(1);
1375
+ NAPI_ITERATOR_CONTEXT();
1376
+
1377
+ const auto seq = iterator->snapshot_->GetSequenceNumber();
1378
+
1379
+ napi_value result;
1380
+ NAPI_STATUS_THROWS(napi_create_bigint_int64(env, seq, &result));
1381
+
1382
+ return 0;
1383
+ }
1384
+
1052
1385
  struct NextWorker final : public Worker {
1053
1386
  NextWorker(napi_env env, Iterator* iterator, uint32_t size, napi_value callback)
1054
1387
  : Worker(env, iterator->database_, callback, "leveldown.iterator.next"), iterator_(iterator), size_(size) {}
@@ -1073,33 +1406,29 @@ struct NextWorker final : public Worker {
1073
1406
  if (iterator_->keys_ && iterator_->values_) {
1074
1407
  auto k = iterator_->CurrentKey();
1075
1408
  auto v = iterator_->CurrentValue();
1076
- cache_.push_back({});
1077
- cache_.back().PinSelf(k);
1078
- cache_.push_back({});
1079
- cache_.back().PinSelf(v);
1080
1409
  bytesRead += k.size() + v.size();
1410
+ cache_.push_back(k.ToString());
1411
+ cache_.push_back(v.ToString());
1081
1412
  } else if (iterator_->keys_) {
1082
1413
  auto k = iterator_->CurrentKey();
1083
- cache_.push_back({});
1084
- cache_.back().PinSelf(k);
1085
- cache_.push_back({});
1086
- // no value
1087
1414
  bytesRead += k.size();
1415
+ cache_.push_back(k.ToString());
1416
+ cache_.push_back(std::nullopt);
1088
1417
  } else if (iterator_->values_) {
1089
1418
  auto v = iterator_->CurrentValue();
1090
- cache_.push_back({});
1091
- // no key
1092
- cache_.push_back({});
1093
- cache_.back().PinSelf(v);
1094
1419
  bytesRead += v.size();
1420
+ cache_.push_back(std::nullopt);
1421
+ cache_.push_back(v.ToString());
1095
1422
  }
1096
1423
 
1097
1424
  if (bytesRead > iterator_->highWaterMarkBytes_ || cache_.size() / 2 >= size_) {
1098
- finished_ = true;
1425
+ finished_ = false;
1099
1426
  return rocksdb::Status::OK();
1100
1427
  }
1101
1428
  }
1102
1429
 
1430
+ finished_ = true;
1431
+
1103
1432
  return iterator_->Status();
1104
1433
  }
1105
1434
 
@@ -1124,15 +1453,15 @@ struct NextWorker final : public Worker {
1124
1453
  napi_value argv[3];
1125
1454
  NAPI_STATUS_RETURN(napi_get_null(env, &argv[0]));
1126
1455
  argv[1] = result;
1127
- NAPI_STATUS_RETURN(napi_get_boolean(env, !finished_, &argv[2]));
1456
+ NAPI_STATUS_RETURN(napi_get_boolean(env, finished_, &argv[2]));
1128
1457
  return CallFunction(env, callback, 3, argv);
1129
1458
  }
1130
1459
 
1131
1460
  private:
1132
- std::vector<rocksdb::PinnableSlice> cache_;
1461
+ std::vector<std::optional<std::string>> cache_;
1133
1462
  Iterator* iterator_ = nullptr;
1134
1463
  uint32_t size_ = 0;
1135
- bool finished_ = false;
1464
+ bool finished_ = true;
1136
1465
  };
1137
1466
 
1138
1467
  NAPI_METHOD(iterator_nextv) {
@@ -1243,10 +1572,54 @@ NAPI_METHOD(batch_write) {
1243
1572
  return ToError(env, database->db_->Write(options, batch));
1244
1573
  }
1245
1574
 
1575
+ NAPI_METHOD(snapshot_init) {
1576
+ NAPI_ARGV(1);
1577
+ NAPI_DB_CONTEXT();
1578
+
1579
+ auto snapshot = std::make_unique<Snapshot>(database);
1580
+
1581
+ napi_value result;
1582
+ NAPI_STATUS_THROWS(napi_create_external(env, snapshot.get(), Finalize<Snapshot>, snapshot.get(), &result));
1583
+
1584
+ // Prevent GC of JS object before the iterator is closed (explicitly or on
1585
+ // db close) and keep track of non-closed iterators to end them on db close.
1586
+ snapshot.release()->Attach(env, result);
1587
+
1588
+ return result;
1589
+ }
1590
+
1591
+ NAPI_METHOD(snapshot_get_sequence_number) {
1592
+ NAPI_ARGV(3);
1593
+ NAPI_DB_CONTEXT();
1594
+
1595
+ Snapshot* snapshot;
1596
+ NAPI_STATUS_THROWS(napi_get_value_external(env, argv[1], reinterpret_cast<void**>(&snapshot)));
1597
+
1598
+ const auto seq = snapshot->GetSequenceNumber();
1599
+
1600
+ napi_value result;
1601
+ NAPI_STATUS_THROWS(napi_create_bigint_int64(env, seq, &result));
1602
+
1603
+ return result;
1604
+ }
1605
+
1606
+ NAPI_METHOD(snapshot_close) {
1607
+ NAPI_ARGV(2);
1608
+ NAPI_DB_CONTEXT();
1609
+
1610
+ Snapshot* snapshot;
1611
+ NAPI_STATUS_THROWS(napi_get_value_external(env, argv[1], reinterpret_cast<void**>(&snapshot)));
1612
+
1613
+ snapshot->Close();
1614
+
1615
+ return 0;
1616
+ }
1617
+
1246
1618
  NAPI_INIT() {
1247
1619
  NAPI_EXPORT_FUNCTION(db_init);
1248
1620
  NAPI_EXPORT_FUNCTION(db_open);
1249
1621
  NAPI_EXPORT_FUNCTION(db_close);
1622
+ NAPI_EXPORT_FUNCTION(db_get_latest_sequence_number);
1250
1623
  NAPI_EXPORT_FUNCTION(db_put);
1251
1624
  NAPI_EXPORT_FUNCTION(db_get);
1252
1625
  NAPI_EXPORT_FUNCTION(db_get_many);
@@ -1258,6 +1631,11 @@ NAPI_INIT() {
1258
1631
  NAPI_EXPORT_FUNCTION(iterator_seek);
1259
1632
  NAPI_EXPORT_FUNCTION(iterator_close);
1260
1633
  NAPI_EXPORT_FUNCTION(iterator_nextv);
1634
+ NAPI_EXPORT_FUNCTION(iterator_get_sequence);
1635
+
1636
+ NAPI_EXPORT_FUNCTION(updates_init);
1637
+ NAPI_EXPORT_FUNCTION(updates_close);
1638
+ NAPI_EXPORT_FUNCTION(updates_next);
1261
1639
 
1262
1640
  NAPI_EXPORT_FUNCTION(batch_do);
1263
1641
  NAPI_EXPORT_FUNCTION(batch_init);
@@ -1265,4 +1643,8 @@ NAPI_INIT() {
1265
1643
  NAPI_EXPORT_FUNCTION(batch_del);
1266
1644
  NAPI_EXPORT_FUNCTION(batch_clear);
1267
1645
  NAPI_EXPORT_FUNCTION(batch_write);
1646
+
1647
+ NAPI_EXPORT_FUNCTION(snapshot_init);
1648
+ NAPI_EXPORT_FUNCTION(snapshot_get_sequence_number);
1649
+ NAPI_EXPORT_FUNCTION(snapshot_close);
1268
1650
  }
package/index.js CHANGED
@@ -6,6 +6,7 @@ const fs = require('fs')
6
6
  const binding = require('./binding')
7
7
  const { ChainedBatch } = require('./chained-batch')
8
8
  const { Iterator } = require('./iterator')
9
+ const { Snapshot } = require('./snapshot')
9
10
 
10
11
  const kContext = Symbol('context')
11
12
  const kLocation = Symbol('location')
@@ -30,7 +31,13 @@ class RocksLevel extends AbstractLevel {
30
31
  },
31
32
  seek: true,
32
33
  createIfMissing: true,
33
- errorIfExists: true
34
+ errorIfExists: true,
35
+ additionalMethods: {
36
+ getLatestSequenceNumber: true,
37
+ getSnapshot: true,
38
+ updates: true,
39
+ query: true
40
+ }
34
41
  }, options)
35
42
 
36
43
  this[kLocation] = location
@@ -84,6 +91,10 @@ class RocksLevel extends AbstractLevel {
84
91
  process.nextTick(callback, binding.batch_do(this[kContext], operations, options))
85
92
  }
86
93
 
94
+ _iterator (options) {
95
+ return new Iterator(this, this[kContext], options)
96
+ }
97
+
87
98
  getProperty (property) {
88
99
  if (typeof property !== 'string') {
89
100
  throw new TypeError("The first argument 'property' must be a string")
@@ -99,8 +110,181 @@ class RocksLevel extends AbstractLevel {
99
110
  return binding.db_get_property(this[kContext], property)
100
111
  }
101
112
 
102
- _iterator (options) {
103
- return new Iterator(this, this[kContext], options)
113
+ getLatestSequenceNumber () {
114
+ if (this.status !== 'open') {
115
+ throw new ModuleError('Database is not open', {
116
+ code: 'LEVEL_DATABASE_NOT_OPEN'
117
+ })
118
+ }
119
+
120
+ return binding.db_get_latest_sequence_number(this[kContext])
121
+ }
122
+
123
+ getSnapshot () {
124
+ if (this.status !== 'open') {
125
+ throw new ModuleError('Database is not open', {
126
+ code: 'LEVEL_DATABASE_NOT_OPEN'
127
+ })
128
+ }
129
+
130
+ return new Snapshot(this, this[kContext])
131
+ }
132
+
133
+ async * query (options) {
134
+ if (this.status !== 'open') {
135
+ throw new ModuleError('Database is not open', {
136
+ code: 'LEVEL_DATABASE_NOT_OPEN'
137
+ })
138
+ }
139
+
140
+ class Query {
141
+ constructor (db, options) {
142
+ this.context = binding.iterator_init(db[kContext], options)
143
+ this.closed = false
144
+ this.promise = null
145
+ this.sequence = binding.iterator_get_sequence(this.context)
146
+ this.db = db
147
+ this.db.attachResource(this)
148
+ }
149
+
150
+ async next () {
151
+ if (this.closed) {
152
+ return {}
153
+ }
154
+
155
+ this.promise = new Promise(resolve => binding.iterator_nextv(this.context, 1000, (err, rows, finished) => {
156
+ this.promise = null
157
+ if (err) {
158
+ resolve(Promise.reject(err))
159
+ } else {
160
+ resolve({
161
+ finished,
162
+ rows,
163
+ sequence: this.sequence
164
+ })
165
+ }
166
+ }))
167
+
168
+ return this.promise
169
+ }
170
+
171
+ async close (callback) {
172
+ try {
173
+ await this.promise
174
+ } catch {
175
+ // Do nothing...
176
+ }
177
+
178
+ try {
179
+ if (!this.closed) {
180
+ this.closed = true
181
+ binding.iterator_close(this.context)
182
+ }
183
+
184
+ if (callback) {
185
+ process.nextTick(callback)
186
+ }
187
+ } catch (err) {
188
+ if (callback) {
189
+ process.nextTick(callback, err)
190
+ } else {
191
+ throw err
192
+ }
193
+ } finally {
194
+ this.db.detachResource(this)
195
+ }
196
+ }
197
+ }
198
+
199
+ const query = new Query(this, options)
200
+ try {
201
+ while (true) {
202
+ const { finished, rows, sequence } = await query.next()
203
+
204
+ yield { rows, sequence }
205
+
206
+ if (finished) {
207
+ return
208
+ }
209
+ }
210
+ } finally {
211
+ await query.close()
212
+ }
213
+ }
214
+
215
+ async * updates (options) {
216
+ if (this.status !== 'open') {
217
+ throw new ModuleError('Database is not open', {
218
+ code: 'LEVEL_DATABASE_NOT_OPEN'
219
+ })
220
+ }
221
+
222
+ class Updates {
223
+ constructor (db, options) {
224
+ this.context = binding.updates_init(db[kContext], options)
225
+ this.closed = false
226
+ this.promise = null
227
+ this.db = db
228
+ this.db.attachResource(this)
229
+ }
230
+
231
+ async next () {
232
+ if (this.closed) {
233
+ return {}
234
+ }
235
+
236
+ this.promise = new Promise(resolve => binding.updates_next(this.context, (err, rows, sequence) => {
237
+ this.promise = null
238
+ if (err) {
239
+ resolve(Promise.reject(err))
240
+ } else {
241
+ resolve({ rows, sequence })
242
+ }
243
+ }))
244
+
245
+ return this.promise
246
+ }
247
+
248
+ async close (callback) {
249
+ try {
250
+ await this.promise
251
+ } catch {
252
+ // Do nothing...
253
+ }
254
+
255
+ try {
256
+ if (!this.closed) {
257
+ this.closed = true
258
+ binding.updates_close(this.context)
259
+ }
260
+
261
+ if (callback) {
262
+ process.nextTick(callback)
263
+ }
264
+ } catch (err) {
265
+ if (callback) {
266
+ process.nextTick(callback, err)
267
+ } else {
268
+ throw err
269
+ }
270
+ } finally {
271
+ this.db.detachResource(this)
272
+ }
273
+ }
274
+ }
275
+
276
+ const updates = new Updates(this, options)
277
+ try {
278
+ while (true) {
279
+ const { rows, sequence } = await updates.next()
280
+ if (!rows) {
281
+ return
282
+ }
283
+ yield { rows, sequence }
284
+ }
285
+ } finally {
286
+ await updates.close()
287
+ }
104
288
  }
105
289
  }
106
290
 
package/iterator.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { AbstractIterator } = require('abstract-level')
4
+
4
5
  const binding = require('./binding')
5
6
 
6
7
  const kContext = Symbol('context')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/rocksdb",
3
- "version": "6.0.3",
3
+ "version": "7.0.0-alpha.1",
4
4
  "description": "A low-level Node.js RocksDB binding",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
package/snapshot.js ADDED
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ const binding = require('./binding')
4
+
5
+ const kDbContext = Symbol('db')
6
+ const kContext = Symbol('context')
7
+
8
+ class Snapshot {
9
+ constructor (db, context) {
10
+ this[kDbContext] = context
11
+ this[kContext] = binding.snapshot_init(context)
12
+ }
13
+
14
+ get sequenceNumber () {
15
+ return binding.snapshot_get_sequence_number(this[kContext])
16
+ }
17
+
18
+ close () {
19
+ binding.snapshot_close(this[kContext])
20
+ }
21
+ }
22
+
23
+ exports.Snapshot = Snapshot
Binary file
Binary file