@nxtedition/rocksdb 15.5.0 → 16.0.0

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
@@ -551,10 +551,10 @@ class Iterator final : public BaseIterator {
551
551
  rocksdb::ColumnFamilyHandle* column = database->db->DefaultColumnFamily();
552
552
  NAPI_STATUS_THROWS(GetProperty(env, options, "column", column));
553
553
 
554
- Encoding keyEncoding;
554
+ Encoding keyEncoding = Encoding::Buffer;
555
555
  NAPI_STATUS_THROWS(GetProperty(env, options, "keyEncoding", keyEncoding));
556
556
 
557
- Encoding valueEncoding;
557
+ Encoding valueEncoding = Encoding::Buffer;
558
558
  NAPI_STATUS_THROWS(GetProperty(env, options, "valueEncoding", valueEncoding));
559
559
 
560
560
  rocksdb::ReadOptions readOptions;
@@ -601,6 +601,7 @@ class Iterator final : public BaseIterator {
601
601
  std::vector<rocksdb::PinnableSlice> keys;
602
602
  std::vector<rocksdb::PinnableSlice> values;
603
603
  size_t count = 0;
604
+ size_t bytes = 0;
604
605
  bool finished = false;
605
606
  bool limited = false;
606
607
  };
@@ -616,14 +617,16 @@ class Iterator final : public BaseIterator {
616
617
 
617
618
  const auto deadline = timeout ? database_->db->GetEnv()->NowMicros() + timeout * 1000 : 0;
618
619
 
619
- size_t bytesRead = 0;
620
620
  while (true) {
621
- if (state.count >= count || bytesRead > highWaterMarkBytes_) {
621
+ if (state.count >= count || state.bytes > highWaterMarkBytes_) {
622
+ // Batch cap (size/bytes) reached: more data may exist, so this is
623
+ // "limited", not "finished".
622
624
  state.limited = true;
623
625
  break;
624
626
  }
625
627
 
626
628
  if (deadline > 0 && database_->db->GetEnv()->NowMicros() > deadline) {
629
+ // Timed out: neither finished nor limited; the caller may retry.
627
630
  break;
628
631
  }
629
632
 
@@ -635,12 +638,19 @@ class Iterator final : public BaseIterator {
635
638
 
636
639
  ROCKS_STATUS_RETURN(Status());
637
640
 
638
- if (!Valid() || !Increment()) {
641
+ if (!Valid()) {
642
+ // Iterator naturally exhausted.
639
643
  state.finished = true;
640
644
  break;
641
645
  }
642
646
 
643
- bytesRead += CurrentKey().size() + CurrentValue().size();
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
+ }
644
654
 
645
655
  if (keyFilter_ && !re2::RE2::PartialMatch(CurrentKey().ToStringView(), *keyFilter_)) {
646
656
  continue;
@@ -653,18 +663,22 @@ class Iterator final : public BaseIterator {
653
663
  if (keys_ && values_) {
654
664
  rocksdb::PinnableSlice k;
655
665
  k.PinSelf(CurrentKey());
666
+ state.bytes += k.size();
656
667
  state.keys.push_back(std::move(k));
657
668
 
658
669
  rocksdb::PinnableSlice v;
659
670
  v.PinSelf(CurrentValue());
671
+ state.bytes += v.size();
660
672
  state.values.push_back(std::move(v));
661
673
  } else if (keys_) {
662
674
  rocksdb::PinnableSlice k;
663
675
  k.PinSelf(CurrentKey());
676
+ state.bytes += k.size();
664
677
  state.keys.push_back(std::move(k));
665
678
  } else if (values_) {
666
679
  rocksdb::PinnableSlice v;
667
680
  v.PinSelf(CurrentValue());
681
+ state.bytes += v.size();
668
682
  state.values.push_back(std::move(v));
669
683
  } else {
670
684
  assert(false);
@@ -729,14 +743,18 @@ class Iterator final : public BaseIterator {
729
743
  const auto deadline = timeout ? database_->db->GetEnv()->NowMicros() + timeout * 1000 : 0;
730
744
 
731
745
  size_t idx = 0;
732
- size_t bytesRead = 0;
746
+ size_t bytes = 0;
733
747
  while (true) {
734
- if (idx >= count * 2 || bytesRead > highWaterMarkBytes_) {
748
+ if (idx >= static_cast<size_t>(count) * 2 || bytes > highWaterMarkBytes_) {
749
+ // Batch cap (size/bytes) reached: more data may exist, so this is
750
+ // "limited", not "finished". (count is uint32_t; widen before *2 so
751
+ // query()'s UINT32_MAX count doesn't overflow to a small cap.)
735
752
  NAPI_STATUS_THROWS(napi_get_boolean(env, true, &limited));
736
753
  break;
737
754
  }
738
755
 
739
756
  if (deadline > 0 && database_->db->GetEnv()->NowMicros() > deadline) {
757
+ // Timed out: neither finished nor limited; the caller may retry.
740
758
  break;
741
759
  }
742
760
 
@@ -748,12 +766,19 @@ class Iterator final : public BaseIterator {
748
766
 
749
767
  ROCKS_STATUS_THROWS_NAPI(Status());
750
768
 
751
- if (!Valid() || !Increment()) {
769
+ if (!Valid()) {
770
+ // Iterator naturally exhausted.
752
771
  NAPI_STATUS_THROWS(napi_get_boolean(env, true, &finished));
753
772
  break;
754
773
  }
755
774
 
756
- bytesRead += CurrentKey().size() + CurrentValue().size();
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
+ }
757
782
 
758
783
  if (keyFilter_ && !re2::RE2::PartialMatch(CurrentKey().ToStringView(), *keyFilter_)) {
759
784
  continue;
@@ -767,12 +792,15 @@ class Iterator final : public BaseIterator {
767
792
  napi_value val;
768
793
 
769
794
  if (keys_ && values_) {
795
+ bytes += CurrentKey().size() + CurrentValue().size();
770
796
  NAPI_STATUS_THROWS(Convert(env, CurrentKey(), keyEncoding_, key, unsafe_));
771
797
  NAPI_STATUS_THROWS(Convert(env, CurrentValue(), valueEncoding_, val, unsafe_));
772
798
  } else if (keys_) {
799
+ bytes += CurrentKey().size();
773
800
  NAPI_STATUS_THROWS(Convert(env, CurrentKey(), keyEncoding_, key, unsafe_));
774
801
  NAPI_STATUS_THROWS(napi_get_undefined(env, &val));
775
802
  } else if (values_) {
803
+ bytes += CurrentValue().size();
776
804
  NAPI_STATUS_THROWS(napi_get_undefined(env, &key));
777
805
  NAPI_STATUS_THROWS(Convert(env, CurrentValue(), valueEncoding_, val, unsafe_));
778
806
  } else {
@@ -1233,6 +1261,11 @@ NAPI_METHOD(db_get_identity) {
1233
1261
  Database* database;
1234
1262
  NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast<void**>(&database)));
1235
1263
 
1264
+ if (!database->db) {
1265
+ napi_throw_error(env, "LEVEL_DATABASE_NOT_OPEN", "Database is not open");
1266
+ return NULL;
1267
+ }
1268
+
1236
1269
  std::string identity;
1237
1270
  ROCKS_STATUS_THROWS_NAPI(database->db->GetDbIdentity(identity));
1238
1271
 
@@ -1733,6 +1766,11 @@ NAPI_METHOD(db_get_latest_sequence) {
1733
1766
  Database* database;
1734
1767
  NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast<void**>(&database)));
1735
1768
 
1769
+ if (!database->db) {
1770
+ napi_throw_error(env, "LEVEL_DATABASE_NOT_OPEN", "Database is not open");
1771
+ return NULL;
1772
+ }
1773
+
1736
1774
  const auto seq = database->db->GetLatestSequenceNumber();
1737
1775
 
1738
1776
  napi_value result;
package/index.js CHANGED
@@ -52,6 +52,12 @@ class RocksLevel extends AbstractLevel {
52
52
  }
53
53
 
54
54
  get sequence () {
55
+ if (this.status !== 'open') {
56
+ throw new ModuleError('Database is not open', {
57
+ code: 'LEVEL_DATABASE_NOT_OPEN'
58
+ })
59
+ }
60
+
55
61
  return binding.db_get_latest_sequence(this[kContext])
56
62
  }
57
63
 
@@ -112,7 +118,14 @@ class RocksLevel extends AbstractLevel {
112
118
  [kUnref] () {
113
119
  this[kRefs]--
114
120
  if (this[kRefs] === 0 && this[kPendingClose]) {
115
- process.nextTick(this[kPendingClose])
121
+ // Perform the deferred native close now that all in-flight ops have
122
+ // drained. Note: kPendingClose holds the abstract-level _close callback,
123
+ // so we must call binding.db_close here (not just the callback) or the
124
+ // native DB and its directory lock would leak. nextTick avoids reentering
125
+ // the native layer from within the completing op's own callback.
126
+ const callback = this[kPendingClose]
127
+ this[kPendingClose] = null
128
+ process.nextTick(() => binding.db_close(this[kContext], callback))
116
129
  }
117
130
  }
118
131
 
@@ -243,10 +256,22 @@ class RocksLevel extends AbstractLevel {
243
256
  }
244
257
  }
245
258
 
246
- binding.batch_write(this[kContext], batch, options ?? {}, (err, val) => {
259
+ // Hold a db ref for the duration of the write so close() defers db_close
260
+ // (which frees the native db on a worker thread) until it completes. The
261
+ // array-form batch uses a transient WriteBatch that is not an abstract-level
262
+ // resource, so it is not otherwise tracked across close.
263
+ this[kRef]()
264
+ try {
265
+ binding.batch_write(this[kContext], batch, options ?? {}, (err, val) => {
266
+ this[kUnref]()
267
+ binding.batch_clear(batch)
268
+ callback(err, val)
269
+ })
270
+ } catch (err) {
271
+ this[kUnref]()
247
272
  binding.batch_clear(batch)
248
- callback(err, val)
249
- })
273
+ process.nextTick(callback, err)
274
+ }
250
275
 
251
276
  return callback[kPromise]
252
277
  }
@@ -256,6 +281,12 @@ class RocksLevel extends AbstractLevel {
256
281
  }
257
282
 
258
283
  get identity () {
284
+ if (this.status !== 'open') {
285
+ throw new ModuleError('Database is not open', {
286
+ code: 'LEVEL_DATABASE_NOT_OPEN'
287
+ })
288
+ }
289
+
259
290
  return binding.db_get_identity(this[kContext])
260
291
  }
261
292
 
@@ -305,10 +336,21 @@ class RocksLevel extends AbstractLevel {
305
336
 
306
337
  const handle = binding.updates_init(this[kContext], options)
307
338
  try {
308
- while (true) {
309
- const value = await new Promise((resolve, reject) => {
310
- binding.updates_next(handle, (err, val) => err ? reject(err) : resolve(val))
311
- })
339
+ // Stop if the db is closed between yields, so we never call updates_next
340
+ // on a freed db.
341
+ while (this.status === 'open') {
342
+ // Hold a db ref for the duration of each updates_next so close() defers
343
+ // db_close (and the Database::Close() that resets this log iterator on a
344
+ // worker thread) until the in-flight read completes.
345
+ this[kRef]()
346
+ let value
347
+ try {
348
+ value = await new Promise((resolve, reject) => {
349
+ binding.updates_next(handle, (err, val) => err ? reject(err) : resolve(val))
350
+ })
351
+ } finally {
352
+ this[kUnref]()
353
+ }
312
354
  if (!value) {
313
355
  break
314
356
  }
@@ -328,7 +370,16 @@ class RocksLevel extends AbstractLevel {
328
370
  })
329
371
  }
330
372
 
331
- binding.db_compact_range(this[kContext], options, callback)
373
+ this[kRef]()
374
+ try {
375
+ binding.db_compact_range(this[kContext], options, (err, val) => {
376
+ this[kUnref]()
377
+ callback(err, val)
378
+ })
379
+ } catch (err) {
380
+ this[kUnref]()
381
+ process.nextTick(callback, err)
382
+ }
332
383
 
333
384
  return callback[kPromise]
334
385
  }
@@ -342,7 +393,16 @@ class RocksLevel extends AbstractLevel {
342
393
  })
343
394
  }
344
395
 
345
- binding.db_flush_wal(this[kContext], options?.sync ?? false, callback)
396
+ this[kRef]()
397
+ try {
398
+ binding.db_flush_wal(this[kContext], options?.sync ?? false, (err, val) => {
399
+ this[kUnref]()
400
+ callback(err, val)
401
+ })
402
+ } catch (err) {
403
+ this[kUnref]()
404
+ process.nextTick(callback, err)
405
+ }
346
406
 
347
407
  return callback[kPromise]
348
408
  }
package/iterator.js CHANGED
@@ -15,6 +15,7 @@ const kFinished = Symbol('finished')
15
15
  const kFirst = Symbol('first')
16
16
  const kPosition = Symbol('position')
17
17
  const kBusy = Symbol('busy')
18
+ const kPendingClose = Symbol('pendingClose')
18
19
 
19
20
  const kEmpty = Object.freeze([])
20
21
 
@@ -30,6 +31,7 @@ class Iterator extends AbstractIterator {
30
31
  this[kPosition] = 0
31
32
  this[kDB] = db
32
33
  this[kBusy] = false
34
+ this[kPendingClose] = null
33
35
  }
34
36
 
35
37
  [Symbol.asyncDispose] () {
@@ -41,7 +43,23 @@ class Iterator extends AbstractIterator {
41
43
  }
42
44
 
43
45
  _close (callback) {
44
- return this._closeAsync(callback)
46
+ // If an async nextv/seek is in flight on a worker thread, defer the close
47
+ // until it completes so we never free the native rocksdb iterator while the
48
+ // worker is still reading it. The pending close is flushed from the async
49
+ // op's completion callback (see _flushPendingClose).
50
+ if (this[kBusy]) {
51
+ this[kPendingClose] = callback
52
+ } else {
53
+ this._closeAsync(callback)
54
+ }
55
+ }
56
+
57
+ _flushPendingClose () {
58
+ if (!this[kBusy] && this[kPendingClose]) {
59
+ const callback = this[kPendingClose]
60
+ this[kPendingClose] = null
61
+ this._closeAsync(callback)
62
+ }
45
63
  }
46
64
 
47
65
  _end (callback) {
@@ -147,6 +165,8 @@ class Iterator extends AbstractIterator {
147
165
  } else {
148
166
  callback(null)
149
167
  }
168
+
169
+ this._flushPendingClose()
150
170
  })
151
171
  } catch (err) {
152
172
  this[kBusy] = false
@@ -193,6 +213,8 @@ class Iterator extends AbstractIterator {
193
213
  this[kFinished] = result.finished
194
214
  callback(null, result)
195
215
  }
216
+
217
+ this._flushPendingClose()
196
218
  })
197
219
  }
198
220
  } catch (err) {
@@ -13,10 +13,14 @@ int compareRev(const rocksdb::Slice& a, const rocksdb::Slice& b) {
13
13
  return 1;
14
14
  }
15
15
 
16
- std::size_t indexA = 0;
17
- std::size_t indexB = 0;
18
- const std::size_t endA = std::min<std::size_t>(a[indexA++], a.size());
19
- const std::size_t endB = std::min<std::size_t>(b[indexB++], b.size());
16
+ // The first byte is a length prefix declaring the content length. Clamp it to
17
+ // the bytes actually available (size - 1) so malformed/truncated operands can
18
+ // never over-read, and cast through unsigned char so a prefix >= 0x80 is not
19
+ // sign-extended. endA/endB are exclusive end offsets: content is at [1, endX).
20
+ std::size_t indexA = 1;
21
+ std::size_t indexB = 1;
22
+ const std::size_t endA = 1 + std::min<std::size_t>(static_cast<unsigned char>(a[0]), a.size() - 1);
23
+ const std::size_t endB = 1 + std::min<std::size_t>(static_cast<unsigned char>(b[0]), b.size() - 1);
20
24
 
21
25
  // Compare the revision number
22
26
  auto result = 0;
@@ -52,7 +56,7 @@ int compareRev(const rocksdb::Slice& a, const rocksdb::Slice& b) {
52
56
  }
53
57
  }
54
58
 
55
- return endA - endB;
59
+ return static_cast<int>(endA) - static_cast<int>(endB);
56
60
  }
57
61
 
58
62
  class MaxRevOperator : public rocksdb::MergeOperator {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/rocksdb",
3
- "version": "15.5.0",
3
+ "version": "16.0.0",
4
4
  "description": "A low-level Node.js RocksDB binding",
5
5
  "license": "MIT",
6
6
  "main": "index.js",