diff --git a/cpp/react-native-leveldb.cpp b/cpp/react-native-leveldb.cpp index c6aea3a..1e233a7 100644 --- a/cpp/react-native-leveldb.cpp +++ b/cpp/react-native-leveldb.cpp @@ -12,6 +12,7 @@ using namespace facebook; // TODO(savv): consider re-using unique_ptrs, if they are empty. std::vector> dbs; std::vector> iterators; +std::vector> batches; // Returns false if the passed value is not a string or an ArrayBuffer. bool valueToString(jsi::Runtime& runtime, const jsi::Value& value, std::string* str) { @@ -63,6 +64,24 @@ leveldb::Iterator* valueToIterator(const jsi::Value& value) { return iterators[idx].get(); } +leveldb::WriteBatch* valueToWriteBatch(const jsi::Value& value, std::string* err) { + if (!value.isNumber()) { + *err = "valueToWriteBatch/param-not-a-number"; + return nullptr; + } + int idx = (int)value.getNumber(); + if (idx < 0 || idx >= batches.size()) { + *err = "valueToWriteBatch/idx-out-of-range"; + return nullptr; + } + if (!batches[idx].get()) { + *err = "valueToWriteBatch/null"; + return nullptr; + } + + return batches[idx].get(); +} + void installLeveldb(jsi::Runtime& jsiRuntime, std::string documentDir) { if (documentDir[documentDir.length() - 1] != '/') { documentDir += '/'; @@ -363,6 +382,108 @@ void installLeveldb(jsi::Runtime& jsiRuntime, std::string documentDir) { ); jsiRuntime.global().setProperty(jsiRuntime, "leveldbIteratorValueStr", std::move(leveldbIteratorValueStr)); + auto leveldbNewWriteBatch = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forAscii(jsiRuntime, "leveldbNewWriteBatch"), + 0, + [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + batches.push_back(std::unique_ptr(new leveldb::WriteBatch())); + return jsi::Value((int)batches.size() - 1); + } + ); + jsiRuntime.global().setProperty(jsiRuntime, "leveldbNewWriteBatch", std::move(leveldbNewWriteBatch)); + + auto leveldbWriteWriteBatch = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forAscii(jsiRuntime, "leveldbWriteWriteBatch"), + 2, // dbs index, batches index + [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + std::string dbErr; + leveldb::DB* db = valueToDb(arguments[0], &dbErr); + if (!db) { + throw jsi::JSError(runtime, "leveldbWriteWriteBatch/" + dbErr); + } + + std::string batchErr; + leveldb::WriteBatch* batch = valueToWriteBatch(arguments[1], &batchErr); + if (!batch) { + throw jsi::JSError(runtime, "leveldbWriteBatchPut/" + batchErr); + } + + db->Write(leveldb::WriteOptions(), batch); + + return nullptr; + } + ); + jsiRuntime.global().setProperty(jsiRuntime, "leveldbWriteWriteBatch", std::move(leveldbWriteWriteBatch)); + + auto leveldbWriteBatchPut = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forAscii(jsiRuntime, "leveldbWriteBatchPut"), + 3, // batches index, key, value + [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + std::string key, value; + std::string batchErr; + leveldb::WriteBatch* batch = valueToWriteBatch(arguments[0], &batchErr); + if (!batch) { + throw jsi::JSError(runtime, "leveldbWriteBatchPut/" + batchErr); + } + if (!valueToString(runtime, arguments[1], &key) || !valueToString(runtime, arguments[2], &value)) { + throw jsi::JSError(runtime, "leveldbWriteBatchPut/invalid-params"); + } + + batch->Put(key, value); + + return nullptr; + } + ); + jsiRuntime.global().setProperty(jsiRuntime, "leveldbWriteBatchPut", std::move(leveldbWriteBatchPut)); + + auto leveldbWriteBatchDelete = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forAscii(jsiRuntime, "leveldbWriteBatchDelete"), + 2, // batches index, key + [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + std::string key, value; + std::string batchErr; + leveldb::WriteBatch* batch = valueToWriteBatch(arguments[0], &batchErr); + if (!batch) { + throw jsi::JSError(runtime, "leveldbWriteBatchDelete/" + batchErr); + } + if (!valueToString(runtime, arguments[1], &key)) { + throw jsi::JSError(runtime, "leveldbWriteBatchDelete/invalid-params"); + } + + batch->Delete(key); + + return nullptr; + } + ); + jsiRuntime.global().setProperty(jsiRuntime, "leveldbWriteBatchDelete", std::move(leveldbWriteBatchDelete)); + + auto leveldbWriteBatchClose = jsi::Function::createFromHostFunction( + jsiRuntime, + jsi::PropNameID::forAscii(jsiRuntime, "leveldbWriteBatchClose"), + 1, // batches index + [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value { + std::string batchErr; + leveldb::WriteBatch* batch = valueToWriteBatch(arguments[0], &batchErr); + if (!batch) { + throw jsi::JSError(runtime, "leveldbWriteBatchClose/" + batchErr); + } + + int idx = (int)arguments[0].getNumber(); + if (idx < 0 || idx >= batches.size() || !batches[idx].get()) { + throw jsi::JSError(runtime, "leveldbWriteBatchClose/idx-out-of-bounds"); + } + + batches[idx].reset(); + + return nullptr; + } + ); + jsiRuntime.global().setProperty(jsiRuntime, "leveldbWriteBatchClose", std::move(leveldbWriteBatchClose)); + auto leveldbGetStr = jsi::Function::createFromHostFunction( jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "leveldbGetStr"), diff --git a/example/src/example.ts b/example/src/example.ts index be23704..5955f8d 100644 --- a/example/src/example.ts +++ b/example/src/example.ts @@ -1,4 +1,4 @@ -import {LevelDB} from "react-native-leveldb"; +import {LevelDB, LevelDBWriteBatch} from "react-native-leveldb"; import {bufEquals, getRandomString} from "./test-util"; export function leveldbExample(): boolean { @@ -80,8 +80,75 @@ export function leveldbTestMerge(batchMerge: boolean) { return errors; } +function leveldbTestWriteBatch() { + let nameDst = getRandomString(32) + '.db'; + console.info('leveldbTestWriteBatch: Opening DB', nameDst); + const dbDst = new LevelDB(nameDst, true, true); + + const writeBatch = new LevelDBWriteBatch(); + const key1 = new Uint8Array([1, 2, 3]); + const value1 = new Uint8Array([4, 5, 6]); + const key2 = new Uint8Array([3, 2, 1]); + const value2 = new Uint8Array([6, 5, 4]); + + writeBatch.put(key1.buffer, value1.buffer); + writeBatch.put(key2.buffer, value2.buffer); + dbDst.write(writeBatch); + writeBatch.close(); + + const errors: string[] = [] + if (!bufEquals(dbDst.getBuf(key1.buffer)!, value1)) { + errors.push(`leveldbTestWriteBatch: key1 didn't have expected value: ${new Uint8Array(dbDst.getBuf(key1.buffer)!)}`); + } + if (!bufEquals(dbDst.getBuf(key2.buffer)!, value2)) { + errors.push(`leveldbTestWriteBatch: key2 didn't have expected value: ${new Uint8Array(dbDst.getBuf(key2.buffer)!)}`); + } + + try { + writeBatch.put(key2.buffer, value2.buffer); + errors.push(`leveldbTestWriteBatch: FAILED! No exception after close.`); + } catch(e: any) { + // Expected an exception + } + + const writeBatchDelete = new LevelDBWriteBatch(); + writeBatchDelete.delete(key1.buffer); + writeBatchDelete.delete(key2.buffer); + dbDst.write(writeBatchDelete); + writeBatchDelete.close(); + + if (dbDst.getBuf(key1.buffer) !== null) { + errors.push(`leveldbTestWriteBatch: key1 didn't have expected value: ${new Uint8Array(dbDst.getBuf(key1.buffer)!)}`); + } + if (dbDst.getBuf(key2.buffer) !== null) { + errors.push(`leveldbTestWriteBatch: key2 didn't have expected value: ${new Uint8Array(dbDst.getBuf(key2.buffer)!)}`); + } + + try { + writeBatch.delete(key2.buffer); + errors.push(`leveldbTestWriteBatch: FAILED! No exception after close.`); + } catch(e: any) { + // Expected an exception + } + + return errors +} + + export function leveldbTests() { let s: string[] = []; + + try { + const res = leveldbTestWriteBatch(); + if (res.length) { + s.push('leveldbTestWriteBatch() failed with:' + res.join('; ')); + } else { + s.push('leveldbTestWriteBatch() succeeded'); + } + } catch (e: any) { + s.push('leveldbTestWriteBatch() threw: ' + e.message); + } + try { (global as any).leveldbTestException(); s.push('leveldbTestException: FAILED! No exception.'); diff --git a/src/index.ts b/src/index.ts index 4d0c3b4..f7926a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,6 +67,18 @@ export interface LevelDBIteratorI { compareKey(target: ArrayBuffer | string): number; } +export interface LevelDBWriteBatchI { + // Set the database entry for "k" to "v". + put(k: ArrayBuffer | string, v: ArrayBuffer | string): void; + + // Remove the database entry (if any) for "key". + // It is not an error if "key" did not exist in the database. + delete(k: ArrayBuffer | string): void; + + // Drops the reference to this WriteBatch. + close(): void; +} + export interface LevelDBI { // Close this ref to LevelDB. close(): void; @@ -97,6 +109,27 @@ export interface LevelDBI { newIterator(): LevelDBIteratorI; } +export class LevelDBWriteBatch implements LevelDBWriteBatchI { + ref: number; + + constructor() { + this.ref = g.leveldbNewWriteBatch(); + } + + put(k: ArrayBuffer | string, v: ArrayBuffer | string) { + g.leveldbWriteBatchPut(this.ref, k, v); + } + + delete(k: ArrayBuffer | string) { + g.leveldbWriteBatchDelete(this.ref, k); + } + + close() { + g.leveldbWriteBatchClose(this.ref); + this.ref = -1; + } +} + export class LevelDBIterator implements LevelDBIteratorI { private ref: number; @@ -212,6 +245,10 @@ export class LevelDB implements LevelDBI { return new LevelDBIterator(this.ref); } + write(batch: LevelDBWriteBatch) { + g.leveldbWriteWriteBatch(this.ref, batch.ref); + } + // Merges the data from another LevelDB into this one. All keys from src will be written into this LevelDB, // overwriting any existing values. // batchMerge=true will write all values from src in one transaction, thus ensuring that the dst DB is not left