@nxtedition/rocksdb 16.0.6 → 16.0.9

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
@@ -17,6 +17,7 @@
17
17
  #include <rocksdb/status.h>
18
18
  #include <rocksdb/table.h>
19
19
  #include <rocksdb/write_batch.h>
20
+ #include <rocksdb/write_buffer_manager.h>
20
21
 
21
22
  #include <re2/re2.h>
22
23
 
@@ -29,6 +30,19 @@
29
30
  #include <thread>
30
31
  #include <vector>
31
32
 
33
+ #ifdef __linux__
34
+ #include <sys/syscall.h>
35
+ #include <unistd.h>
36
+
37
+ #include <cerrno>
38
+
39
+ // Older libc headers may lack the SYS_ alias for io_uring_setup even though
40
+ // the kernel number (__NR_) is available — keep the Linux probe a boolean.
41
+ #if !defined(SYS_io_uring_setup) && defined(__NR_io_uring_setup)
42
+ #define SYS_io_uring_setup __NR_io_uring_setup
43
+ #endif
44
+ #endif
45
+
32
46
  #include "max_rev_operator.h"
33
47
  #include "util.h"
34
48
 
@@ -334,6 +348,12 @@ struct BaseIterator : public Closable {
334
348
  const int limit,
335
349
  rocksdb::ReadOptions readOptions = {})
336
350
  : database_(database), column_(column), reverse_(reverse), limit_(limit) {
351
+ // TODO (correctness): the +'\0' byte-successor trick below converts
352
+ // inclusive/exclusive bounds correctly only under bytewise ordering. With a
353
+ // custom CF comparator (InitOptions "comparator", e.g.
354
+ // rocksdb.ReverseBytewiseComparator) RocksDB applies these bounds with that
355
+ // comparator, silently inverting the lte/gt boundary semantics. Seek()'s
356
+ // manual bound check below uses raw bytewise Slice::compare as well.
337
357
  if (lte) {
338
358
  upper_bound_ = rocksdb::PinnableSlice();
339
359
  *upper_bound_->GetSelf() = std::move(*lte) + '\0';
@@ -446,7 +466,17 @@ struct BaseIterator : public Closable {
446
466
  // otherwise an iterator that already yielded `limit` rows returns nothing
447
467
  // after a refresh even though every other piece of state was reset.
448
468
  count_ = 0;
449
- return iterator_->Refresh();
469
+ ROCKS_STATUS_RETURN(iterator_->Refresh());
470
+ // rocksdb::Iterator::Refresh invalidates the iterator (a Seek* is required
471
+ // before use), so re-establish the starting position like the constructor
472
+ // does — otherwise the next read sees Valid()==false and reports an empty
473
+ // database.
474
+ if (reverse_) {
475
+ iterator_->SeekToLast();
476
+ } else {
477
+ iterator_->SeekToFirst();
478
+ }
479
+ return iterator_->status();
450
480
  }
451
481
 
452
482
  Database* database_;
@@ -701,9 +731,9 @@ class Iterator final : public BaseIterator {
701
731
  v.PinSelf(CurrentValue());
702
732
  state.bytes += v.size();
703
733
  state.values.push_back(std::move(v));
704
- } else {
705
- assert(false);
706
734
  }
735
+ // keys:false + values:false is valid per abstract-level: rows still
736
+ // count, each entry surfaces as [undefined, undefined].
707
737
  state.count += 1;
708
738
  }
709
739
 
@@ -733,7 +763,8 @@ class Iterator final : public BaseIterator {
733
763
  NAPI_STATUS_RETURN(napi_get_undefined(env, &key));
734
764
  NAPI_STATUS_RETURN(Convert(env, std::move(state.values[n]), valueEncoding_, val, unsafe_));
735
765
  } else {
736
- assert(false);
766
+ NAPI_STATUS_RETURN(napi_get_undefined(env, &key));
767
+ NAPI_STATUS_RETURN(napi_get_undefined(env, &val));
737
768
  }
738
769
 
739
770
  NAPI_STATUS_RETURN(napi_set_element(env, rows, n * 2 + 0, key));
@@ -827,7 +858,8 @@ class Iterator final : public BaseIterator {
827
858
  NAPI_STATUS_THROWS(napi_get_undefined(env, &key));
828
859
  NAPI_STATUS_THROWS(Convert(env, CurrentValue(), valueEncoding_, val, unsafe_));
829
860
  } else {
830
- assert(false);
861
+ NAPI_STATUS_THROWS(napi_get_undefined(env, &key));
862
+ NAPI_STATUS_THROWS(napi_get_undefined(env, &val));
831
863
  }
832
864
 
833
865
  NAPI_STATUS_THROWS(napi_set_element(env, rows, idx++, key));
@@ -904,6 +936,10 @@ NAPI_METHOD(db_init) {
904
936
  int64_t value;
905
937
  bool lossless;
906
938
  NAPI_STATUS_THROWS(napi_get_value_bigint_int64(env, argv[0], &value, &lossless));
939
+ if (!lossless) {
940
+ napi_throw_error(env, nullptr, "invalid database handle");
941
+ return NULL;
942
+ }
907
943
 
908
944
  database = reinterpret_cast<Database*>(value);
909
945
  NAPI_STATUS_THROWS(napi_create_external(env, database, nullptr, nullptr, &result));
@@ -1092,6 +1128,10 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
1092
1128
 
1093
1129
  NAPI_STATUS_RETURN(GetProperty(env, options, "optimizeFiltersForHits", columnOptions.optimize_filters_for_hits));
1094
1130
  NAPI_STATUS_RETURN(GetProperty(env, options, "periodicCompactionSeconds", columnOptions.periodic_compaction_seconds));
1131
+ // memtable_huge_page_size is a column-family option: when the DB is opened
1132
+ // with explicit column descriptors the copy read into dbOptions in db_open is
1133
+ // sliced away, so it must be settable per column to take effect at all.
1134
+ NAPI_STATUS_RETURN(GetProperty(env, options, "memTableHugePageSize", columnOptions.memtable_huge_page_size));
1095
1135
 
1096
1136
  // Compat
1097
1137
  NAPI_STATUS_RETURN(GetProperty(env, options, "enableBlobFiles", columnOptions.enable_blob_files));
@@ -1131,12 +1171,18 @@ napi_status InitOptions(napi_env env, T& columnOptions, const U& options) {
1131
1171
  bool lossless;
1132
1172
  int64_t ptr;
1133
1173
  NAPI_STATUS_RETURN(napi_get_value_bigint_int64(env, handleValue, &ptr, &lossless));
1174
+ if (!lossless) {
1175
+ return napi_invalid_arg;
1176
+ }
1134
1177
 
1135
1178
  cache = *reinterpret_cast<std::shared_ptr<rocksdb::Cache>*>(ptr);
1136
1179
  } else if (cacheType == napi_bigint) {
1137
1180
  bool lossless;
1138
1181
  int64_t ptr;
1139
1182
  NAPI_STATUS_RETURN(napi_get_value_bigint_int64(env, cacheValue, &ptr, &lossless));
1183
+ if (!lossless) {
1184
+ return napi_invalid_arg;
1185
+ }
1140
1186
 
1141
1187
  cache = *reinterpret_cast<std::shared_ptr<rocksdb::Cache>*>(ptr);
1142
1188
  } else if (cacheType != napi_undefined && cacheType != napi_null) {
@@ -1361,6 +1407,14 @@ NAPI_METHOD(db_open) {
1361
1407
  NAPI_STATUS_THROWS(GetProperty(env, options, "parallelism", parallelism));
1362
1408
  dbOptions.IncreaseParallelism(parallelism);
1363
1409
 
1410
+ // IncreaseParallelism sizes the (process-wide) Env LOW pool to `parallelism`
1411
+ // but pins the HIGH pool — where every flush of every DB sharing the default
1412
+ // Env runs — at a single thread, so flushes across DBs serialize behind one
1413
+ // thread. Both pools are process-wide: the last opened DB's value wins.
1414
+ int flushParallelism = std::max(1, parallelism / 4);
1415
+ NAPI_STATUS_THROWS(GetProperty(env, options, "flushParallelism", flushParallelism));
1416
+ dbOptions.env->SetBackgroundThreads(std::max(1, flushParallelism), rocksdb::Env::HIGH);
1417
+
1364
1418
  NAPI_STATUS_THROWS(GetProperty(env, options, "walDir", dbOptions.wal_dir));
1365
1419
 
1366
1420
  // 64-bit inputs: walTTL is in ms and walSizeLimit in bytes, so a 32-bit type
@@ -1427,6 +1481,34 @@ NAPI_METHOD(db_open) {
1427
1481
 
1428
1482
  NAPI_STATUS_THROWS(GetProperty(env, options, "writeBufferSize", dbOptions.db_write_buffer_size));
1429
1483
 
1484
+ {
1485
+ napi_value wbmValue;
1486
+ NAPI_STATUS_THROWS(napi_get_named_property(env, options, "writeBufferManager", &wbmValue));
1487
+
1488
+ napi_valuetype wbmType;
1489
+ NAPI_STATUS_THROWS(napi_typeof(env, wbmValue, &wbmType));
1490
+
1491
+ if (wbmType == napi_object || wbmType == napi_bigint) {
1492
+ napi_value handleValue = wbmValue;
1493
+ if (wbmType == napi_object) {
1494
+ NAPI_STATUS_THROWS(napi_get_named_property(env, wbmValue, "handle", &handleValue));
1495
+ }
1496
+
1497
+ bool lossless;
1498
+ int64_t ptr;
1499
+ NAPI_STATUS_THROWS(napi_get_value_bigint_int64(env, handleValue, &ptr, &lossless));
1500
+ if (!lossless) {
1501
+ napi_throw_error(env, nullptr, "invalid writeBufferManager handle");
1502
+ return NULL;
1503
+ }
1504
+
1505
+ dbOptions.write_buffer_manager = *reinterpret_cast<std::shared_ptr<rocksdb::WriteBufferManager>*>(ptr);
1506
+ } else if (wbmType != napi_undefined && wbmType != napi_null) {
1507
+ napi_throw_error(env, nullptr, "invalid writeBufferManager");
1508
+ return NULL;
1509
+ }
1510
+ }
1511
+
1430
1512
  NAPI_STATUS_THROWS(GetProperty(env, options, "manualWALFlush", dbOptions.manual_wal_flush));
1431
1513
  NAPI_STATUS_THROWS(GetProperty(env, options, "walManualFlush", dbOptions.manual_wal_flush));
1432
1514
 
@@ -2444,6 +2526,128 @@ NAPI_METHOD(cache_get_handle) {
2444
2526
  return result;
2445
2527
  }
2446
2528
 
2529
+ NAPI_METHOD(write_buffer_manager_init) {
2530
+ NAPI_ARGV(1);
2531
+
2532
+ size_t bufferSize = 256 * 1024 * 1024; // 256 MiB
2533
+ NAPI_STATUS_THROWS(GetProperty(env, argv[0], "bufferSize", bufferSize));
2534
+
2535
+ bool allowStall = false;
2536
+ NAPI_STATUS_THROWS(GetProperty(env, argv[0], "allowStall", allowStall));
2537
+
2538
+ std::shared_ptr<rocksdb::Cache> cache;
2539
+ {
2540
+ napi_value cacheValue;
2541
+ NAPI_STATUS_THROWS(napi_get_named_property(env, argv[0], "cache", &cacheValue));
2542
+
2543
+ napi_valuetype cacheType;
2544
+ NAPI_STATUS_THROWS(napi_typeof(env, cacheValue, &cacheType));
2545
+
2546
+ if (cacheType == napi_object || cacheType == napi_bigint) {
2547
+ napi_value handleValue = cacheValue;
2548
+ if (cacheType == napi_object) {
2549
+ NAPI_STATUS_THROWS(napi_get_named_property(env, cacheValue, "handle", &handleValue));
2550
+ }
2551
+
2552
+ bool lossless;
2553
+ int64_t ptr;
2554
+ NAPI_STATUS_THROWS(napi_get_value_bigint_int64(env, handleValue, &ptr, &lossless));
2555
+ if (!lossless) {
2556
+ napi_throw_error(env, nullptr, "invalid cache handle");
2557
+ return NULL;
2558
+ }
2559
+
2560
+ cache = *reinterpret_cast<std::shared_ptr<rocksdb::Cache>*>(ptr);
2561
+ } else if (cacheType != napi_undefined && cacheType != napi_null) {
2562
+ napi_throw_error(env, nullptr, "invalid cache");
2563
+ return NULL;
2564
+ }
2565
+ }
2566
+
2567
+ auto wbm = new std::shared_ptr<rocksdb::WriteBufferManager>(
2568
+ std::make_shared<rocksdb::WriteBufferManager>(bufferSize, cache, allowStall));
2569
+
2570
+ napi_value result;
2571
+ NAPI_STATUS_THROWS(
2572
+ napi_create_external(env, wbm, Finalize<std::shared_ptr<rocksdb::WriteBufferManager>>, wbm, &result));
2573
+
2574
+ return result;
2575
+ }
2576
+
2577
+ NAPI_METHOD(write_buffer_manager_get_handle) {
2578
+ NAPI_ARGV(1);
2579
+
2580
+ std::shared_ptr<rocksdb::WriteBufferManager>* wbm;
2581
+ NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast<void**>(&wbm)));
2582
+
2583
+ napi_value result;
2584
+ NAPI_STATUS_THROWS(napi_create_bigint_int64(env, reinterpret_cast<intptr_t>(wbm), &result));
2585
+
2586
+ return result;
2587
+ }
2588
+
2589
+ NAPI_METHOD(write_buffer_manager_get_usage) {
2590
+ NAPI_ARGV(1);
2591
+
2592
+ std::shared_ptr<rocksdb::WriteBufferManager>* wbm;
2593
+ NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast<void**>(&wbm)));
2594
+
2595
+ napi_value result;
2596
+ NAPI_STATUS_THROWS(napi_create_object(env, &result));
2597
+
2598
+ napi_value memoryUsage;
2599
+ NAPI_STATUS_THROWS(napi_create_double(env, static_cast<double>((*wbm)->memory_usage()), &memoryUsage));
2600
+ NAPI_STATUS_THROWS(napi_set_named_property(env, result, "memoryUsage", memoryUsage));
2601
+
2602
+ napi_value mutableMemoryUsage;
2603
+ NAPI_STATUS_THROWS(
2604
+ napi_create_double(env, static_cast<double>((*wbm)->mutable_memtable_memory_usage()), &mutableMemoryUsage));
2605
+ NAPI_STATUS_THROWS(napi_set_named_property(env, result, "mutableMemoryUsage", mutableMemoryUsage));
2606
+
2607
+ napi_value bufferSize;
2608
+ NAPI_STATUS_THROWS(napi_create_double(env, static_cast<double>((*wbm)->buffer_size()), &bufferSize));
2609
+ NAPI_STATUS_THROWS(napi_set_named_property(env, result, "bufferSize", bufferSize));
2610
+
2611
+ return result;
2612
+ }
2613
+
2614
+ // Probes whether io_uring is actually usable in this process: RocksDB gates its
2615
+ // async MultiGet / prefetch I/O on io_uring_setup succeeding at runtime and
2616
+ // falls back to serial reads SILENTLY when the syscall is denied (seccomp — the
2617
+ // default Docker/containerd profiles since late 2023 — or the
2618
+ // kernel.io_uring_disabled sysctl) or missing (ENOSYS). io_uring_setup(0, NULL)
2619
+ // never succeeds; a functional kernel rejects the arguments (EINVAL/EFAULT)
2620
+ // while a blocked one fails with EPERM/EACCES/ENOSYS before looking at them.
2621
+ NAPI_METHOD(io_uring_available) {
2622
+ #if defined(__linux__) && defined(SYS_io_uring_setup)
2623
+ errno = 0;
2624
+ const long rc = syscall(SYS_io_uring_setup, 0, nullptr);
2625
+ const bool available = rc >= 0 || (errno != ENOSYS && errno != EPERM && errno != EACCES);
2626
+ if (rc >= 0) {
2627
+ close(static_cast<int>(rc));
2628
+ }
2629
+
2630
+ napi_value result;
2631
+ NAPI_STATUS_THROWS(napi_get_boolean(env, available, &result));
2632
+
2633
+ return result;
2634
+ #elif defined(__linux__)
2635
+ // Built without any syscall number for io_uring_setup (pre-io_uring-era
2636
+ // headers): this binary cannot use io_uring regardless of the running
2637
+ // kernel, so report it unavailable — the Linux contract stays boolean.
2638
+ napi_value result;
2639
+ NAPI_STATUS_THROWS(napi_get_boolean(env, false, &result));
2640
+
2641
+ return result;
2642
+ #else
2643
+ // Not applicable on this platform.
2644
+ napi_value result;
2645
+ NAPI_STATUS_THROWS(napi_get_null(env, &result));
2646
+
2647
+ return result;
2648
+ #endif
2649
+ }
2650
+
2447
2651
  NAPI_INIT() {
2448
2652
  NAPI_EXPORT_FUNCTION(db_init);
2449
2653
  NAPI_EXPORT_FUNCTION(db_open);
@@ -2486,4 +2690,10 @@ NAPI_INIT() {
2486
2690
 
2487
2691
  NAPI_EXPORT_FUNCTION(cache_init);
2488
2692
  NAPI_EXPORT_FUNCTION(cache_get_handle);
2693
+
2694
+ NAPI_EXPORT_FUNCTION(write_buffer_manager_init);
2695
+ NAPI_EXPORT_FUNCTION(write_buffer_manager_get_handle);
2696
+ NAPI_EXPORT_FUNCTION(write_buffer_manager_get_usage);
2697
+
2698
+ NAPI_EXPORT_FUNCTION(io_uring_available);
2489
2699
  }
package/index.js CHANGED
@@ -6,6 +6,7 @@ const ModuleError = require('module-error')
6
6
  const binding = require('./binding')
7
7
  const { ChainedBatch } = require('./chained-batch')
8
8
  const { RocksCache } = require('./cache')
9
+ const { RocksWriteBufferManager } = require('./write-buffer-manager')
9
10
  const { Iterator } = require('./iterator')
10
11
  const fs = require('node:fs')
11
12
  const assert = require('node:assert')
@@ -414,3 +415,12 @@ class RocksLevel extends AbstractLevel {
414
415
 
415
416
  exports.RocksLevel = RocksLevel
416
417
  exports.RocksCache = RocksCache
418
+ exports.RocksWriteBufferManager = RocksWriteBufferManager
419
+
420
+ // null on platforms where io_uring does not apply (non-Linux); boolean on
421
+ // Linux, where `false` means RocksDB's async_io silently degrades to serial
422
+ // reads (seccomp, kernel.io_uring_disabled, a kernel without io_uring, or a
423
+ // binary built without an io_uring syscall number).
424
+ exports.ioUringAvailable = function ioUringAvailable () {
425
+ return binding.io_uring_available()
426
+ }
package/iterator.js CHANGED
@@ -16,6 +16,7 @@ const kFirst = Symbol('first')
16
16
  const kPosition = Symbol('position')
17
17
  const kBusy = Symbol('busy')
18
18
  const kPendingClose = Symbol('pendingClose')
19
+ const kHasFilter = Symbol('hasFilter')
19
20
 
20
21
  const kEmpty = Object.freeze([])
21
22
 
@@ -32,6 +33,7 @@ class Iterator extends AbstractIterator {
32
33
  this[kDB] = db
33
34
  this[kBusy] = false
34
35
  this[kPendingClose] = null
36
+ this[kHasFilter] = options.keyFilter != null || options.valueFilter != null
35
37
  }
36
38
 
37
39
  [Symbol.asyncDispose] () {
@@ -85,15 +87,41 @@ class Iterator extends AbstractIterator {
85
87
  const size = this[kFirst] ? 1 : 1000
86
88
  this[kFirst] = false
87
89
 
88
- try {
89
- const { rows, finished } = binding.iterator_nextv_sync(this[kContext], size, null)
90
- this[kCache] = rows
91
- this[kFinished] = finished
92
- this[kPosition] = 0
93
-
94
- setImmediate(() => this._next(callback))
95
- } catch (err) {
96
- process.nextTick(callback, err)
90
+ if (this[kHasFilter]) {
91
+ try {
92
+ this[kDB][kRef]()
93
+ this[kBusy] = true
94
+ binding.iterator_nextv(this[kContext], size, null, (err, result) => {
95
+ this[kBusy] = false
96
+ this[kDB][kUnref]()
97
+
98
+ if (err) {
99
+ callback(err)
100
+ } else {
101
+ this[kCache] = result.rows
102
+ this[kFinished] = result.finished
103
+ this[kPosition] = 0
104
+ this._next(callback)
105
+ }
106
+
107
+ this._flushPendingClose()
108
+ })
109
+ } catch (err) {
110
+ this[kBusy] = false
111
+ this[kDB][kUnref]()
112
+ process.nextTick(callback, err)
113
+ }
114
+ } else {
115
+ try {
116
+ const { rows, finished } = binding.iterator_nextv_sync(this[kContext], size, null)
117
+ this[kCache] = rows
118
+ this[kFinished] = finished
119
+ this[kPosition] = 0
120
+
121
+ setImmediate(() => this._next(callback))
122
+ } catch (err) {
123
+ process.nextTick(callback, err)
124
+ }
97
125
  }
98
126
  }
99
127
 
@@ -189,10 +217,25 @@ class Iterator extends AbstractIterator {
189
217
  return callback[kPromise]
190
218
  }
191
219
 
220
+ _nextvCached (size) {
221
+ const end = Math.min(this[kCache].length, this[kPosition] + size * 2)
222
+ const rows = this[kCache].slice(this[kPosition], end)
223
+ this[kPosition] = end
224
+
225
+ const finished = this[kFinished] && this[kPosition] >= this[kCache].length
226
+ const limited = !finished && rows.length >= size * 2
227
+
228
+ return { rows, finished, limited }
229
+ }
230
+
192
231
  _nextvSync (size, options) {
193
232
  assert(this[kContext])
194
233
  assert(!this[kBusy])
195
234
 
235
+ if (this[kPosition] < this[kCache].length) {
236
+ return this._nextvCached(size)
237
+ }
238
+
196
239
  if (this[kFinished]) {
197
240
  return { rows: [], finished: true }
198
241
  }
@@ -210,7 +253,9 @@ class Iterator extends AbstractIterator {
210
253
  callback = fromCallback(callback, kPromise)
211
254
 
212
255
  try {
213
- if (this[kFinished]) {
256
+ if (this[kPosition] < this[kCache].length) {
257
+ process.nextTick(callback, null, this._nextvCached(size))
258
+ } else if (this[kFinished]) {
214
259
  process.nextTick(callback, null, { rows: [], finished: true })
215
260
  } else {
216
261
  this[kDB][kRef]()
@@ -239,6 +284,8 @@ class Iterator extends AbstractIterator {
239
284
  }
240
285
 
241
286
  _closeSync () {
287
+ assert(!this[kBusy])
288
+
242
289
  this[kCache] = kEmpty
243
290
 
244
291
  if (this[kContext]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/rocksdb",
3
- "version": "16.0.6",
3
+ "version": "16.0.9",
4
4
  "description": "A low-level Node.js RocksDB binding",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
package/release.sh CHANGED
@@ -3,12 +3,41 @@ set -e
3
3
 
4
4
  cd "$(dirname "$0")"
5
5
 
6
+ export DOCKER_HOST="${DOCKER_HOST:-ssh://nxtop@hq-test-srv1.nxt.io}"
7
+
8
+ # Fail fast: npm publish needs a valid login, so check before the slow builds.
9
+ if ! npm whoami --registry https://registry.npmjs.org > /dev/null 2>&1; then
10
+ echo "Not logged in to npm, run 'npm login' first." >&2
11
+ exit 1
12
+ fi
13
+
6
14
  # Fail fast: npm version refuses a dirty tree, so check before the slow builds.
7
15
  if [ -n "$(git status --porcelain)" ]; then
8
16
  echo "Working tree is not clean, commit or stash changes first." >&2
9
17
  exit 1
10
18
  fi
11
19
 
20
+ # Fail fast: don't build/publish on a branch that's behind or diverged from origin.
21
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
22
+ echo "Fetching origin..."
23
+ git fetch origin "$BRANCH"
24
+
25
+ LOCAL=$(git rev-parse HEAD)
26
+ REMOTE=$(git rev-parse "origin/$BRANCH")
27
+ BASE=$(git merge-base HEAD "origin/$BRANCH")
28
+
29
+ if [ "$LOCAL" = "$REMOTE" ]; then
30
+ : # up to date
31
+ elif [ "$LOCAL" = "$BASE" ]; then
32
+ echo "Branch '$BRANCH' is behind origin, pull the latest changes first." >&2
33
+ exit 1
34
+ elif [ "$REMOTE" = "$BASE" ]; then
35
+ : # local is ahead, fine to push
36
+ else
37
+ echo "Branch '$BRANCH' has diverged from origin, reconcile before releasing." >&2
38
+ exit 1
39
+ fi
40
+
12
41
  # Keep the local arm64 build targeting the same node version as the Docker image.
13
42
  NODE_TARGET=$(sed -n 's/^FROM node:\([0-9.]*\).*/\1/p' Dockerfile)
14
43
  if [ -z "$NODE_TARGET" ]; then
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const binding = require('./binding')
4
+
5
+ const kWriteBufferManagerContext = Symbol('writeBufferManagerContext')
6
+
7
+ class RocksWriteBufferManager {
8
+ constructor (options = {}) {
9
+ this[kWriteBufferManagerContext] = binding.write_buffer_manager_init(options)
10
+ }
11
+
12
+ get handle () {
13
+ return binding.write_buffer_manager_get_handle(this[kWriteBufferManagerContext])
14
+ }
15
+
16
+ get usage () {
17
+ return binding.write_buffer_manager_get_usage(this[kWriteBufferManagerContext])
18
+ }
19
+ }
20
+
21
+ exports.RocksWriteBufferManager = RocksWriteBufferManager