Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ Get a value from the database by `key`. The optional `options` object may contai

Returns a promise for the value. If the `key` was not found then the value will be `undefined`.

### `db.getSync(key[, options])`

Synchronously get a value from the database by `key`. This blocks the event loop but can be significantly faster than `db.get()`. Options are the same. Returns the value, or `undefined` if not found.

### `db.getMany(keys[, options])`

Get multiple values from the database by an array of `keys`. The optional `options` object may contain:
Expand Down Expand Up @@ -1509,6 +1513,12 @@ If the database indicates support of snapshots via `db.supports.implicitSnapshot

The default `_get()` returns a promise for an `undefined` value. It must be overridden.

### `db._getSync(key, options)`

Synchronously get a value by `key`. Receives the same options as `db._get()`. Must return a value, or `undefined` if not found.

The default `_getSync()` throws a [`LEVEL_NOT_SUPPORTED`](#level_not_supported) error. It should be overridden but support of `_getSync()` is currently opt-in. Set `manifest.getSync` to `true` in order to enable tests.

### `db._getMany(keys, options)`

Get multiple values by an array of `keys`. The `options` object will always have the following properties: `keyEncoding` and `valueEncoding`. Must return a promise. If an error occurs, reject the promise. Otherwise resolve the promise with an array of values. If a key does not exist, set the relevant value to `undefined`.
Expand Down
67 changes: 67 additions & 0 deletions abstract-level.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,73 @@ class AbstractLevel extends EventEmitter {
return undefined
}

getSync (key, options) {
if (this.status !== 'open') {
throw new ModuleError('Database is not open', {
code: 'LEVEL_DATABASE_NOT_OPEN'
})
}

this._assertValidKey(key)

// Fast-path for default options (known encoding, no cloning, no snapshot)
if (options == null) {
const encodedKey = this.#keyEncoding.encode(key)
const mappedKey = this.prefixKey(encodedKey, this.#keyEncoding.format, true)
const value = this._getSync(mappedKey, this.#defaultOptions.entryFormat)

try {
return value !== undefined ? this.#valueEncoding.decode(value) : undefined
} catch (err) {
throw new ModuleError('Could not decode value', {
code: 'LEVEL_DECODE_ERROR',
cause: err
})
}
}

const snapshot = options.snapshot
const keyEncoding = this.keyEncoding(options.keyEncoding)
const valueEncoding = this.valueEncoding(options.valueEncoding)
const keyFormat = keyEncoding.format
const valueFormat = valueEncoding.format

// Forward encoding options. Avoid cloning if possible.
if (options.keyEncoding !== keyFormat || options.valueEncoding !== valueFormat) {
options = { ...options, keyEncoding: keyFormat, valueEncoding: valueFormat }
}

const encodedKey = keyEncoding.encode(key)
const mappedKey = this.prefixKey(encodedKey, keyFormat, true)

let value

// Keep snapshot open during operation
snapshot?.ref()

try {
value = this._getSync(mappedKey, options)
} finally {
// Release snapshot
snapshot?.unref()
}

try {
return value !== undefined ? valueEncoding.decode(value) : undefined
} catch (err) {
throw new ModuleError('Could not decode value', {
code: 'LEVEL_DECODE_ERROR',
cause: err
})
}
}

_getSync (key, options) {
throw new ModuleError('Database does not support getSync()', {
code: 'LEVEL_NOT_SUPPORTED'
})
}

async getMany (keys, options) {
options = getOptions(options, this.#defaultOptions.entry)

Expand Down
4 changes: 4 additions & 0 deletions lib/abstract-sublevel.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ module.exports = function ({ AbstractLevel }) {
return this.#parent.get(key, options)
}

_getSync (key, options) {
return this.#parent.getSync(key, options)
}

async _getMany (keys, options) {
return this.#parent.getMany(keys, options)
}
Expand Down
102 changes: 99 additions & 3 deletions test/encoding-buffer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ exports.all = function (test, testCommon) {
const db = testCommon.factory()
await db.open()
await db.put('test', testBuffer(), { valueEncoding: 'buffer' })

t.same(await db.get('test', { valueEncoding: 'buffer' }), testBuffer())

if (testCommon.supports.getSync) {
t.same(db.getSync('test', { valueEncoding: 'buffer' }), testBuffer(), 'sync')
}

return db.close()
})

Expand All @@ -20,7 +26,13 @@ exports.all = function (test, testCommon) {
const db = testCommon.factory({ valueEncoding: 'buffer' })
await db.open()
await db.put('test', testBuffer())

t.same(await db.get('test'), testBuffer())

if (testCommon.supports.getSync) {
t.same(db.getSync('test'), testBuffer(), 'sync')
}

return db.close()
})

Expand All @@ -29,7 +41,13 @@ exports.all = function (test, testCommon) {
const db = testCommon.factory()
await db.open()
await db.put(testBuffer(), 'test', { keyEncoding: 'buffer' })

t.same(await db.get(testBuffer(), { keyEncoding: 'buffer' }), 'test')

if (testCommon.supports.getSync) {
t.same(db.getSync(testBuffer(), { keyEncoding: 'buffer' }), 'test', 'sync')
}

return db.close()
})

Expand All @@ -38,7 +56,13 @@ exports.all = function (test, testCommon) {
const db = testCommon.factory()
await db.open()
await db.put(Buffer.from('foo🐄'), 'test', { keyEncoding: 'utf8' })

t.same(await db.get(Buffer.from('foo🐄'), { keyEncoding: 'utf8' }), 'test')

if (testCommon.supports.getSync) {
t.same(db.getSync(Buffer.from('foo🐄'), { keyEncoding: 'utf8' }), 'test', 'sync')
}

return db.close()
})

Expand All @@ -47,8 +71,15 @@ exports.all = function (test, testCommon) {
const db = testCommon.factory()
await db.open()
await db.put('test', 'foo🐄', { valueEncoding: 'buffer' })

t.same(await db.get('test', { valueEncoding: 'buffer' }), Buffer.from('foo🐄'))
t.same(await db.get('test', { valueEncoding: 'utf8' }), 'foo🐄')

if (testCommon.supports.getSync) {
t.same(db.getSync('test', { valueEncoding: 'buffer' }), Buffer.from('foo🐄'), 'sync')
t.same(db.getSync('test', { valueEncoding: 'utf8' }), 'foo🐄', 'sync')
}

return db.close()
})

Expand All @@ -62,11 +93,19 @@ exports.all = function (test, testCommon) {
const promise1 = db.put(a, a).then(async () => {
const value = await db.get(Buffer.from(a), enc)
t.same(value, Buffer.from(a), 'got buffer value')

if (testCommon.supports.getSync) {
t.same(db.getSync(Buffer.from(a), enc), Buffer.from(a), 'got buffer value (sync)')
}
})

const promise2 = db.put(Buffer.from(b), Buffer.from(b), enc).then(async () => {
const value = await db.get(b)
t.same(value, b, 'got string value')

if (testCommon.supports.getSync) {
t.same(db.getSync(b), b, 'got string value (sync)')
}
})

await Promise.all([promise1, promise2])
Expand Down Expand Up @@ -218,12 +257,10 @@ exports.all = function (test, testCommon) {
const db = testCommon.factory({ keyEncoding })
await db.open()

// These are equal when compared as strings but not when compared as buffers
const one = Buffer.from('80', 'hex')
const two = Buffer.from('c0', 'hex')

t.ok(two.toString() === one.toString(), 'would be equal when not byte-aware')
t.ok(two.compare(one) > 0, 'but greater when byte-aware')

await db.put(one, 'one')
t.is(await db.get(one), 'one', 'value one ok')

Expand All @@ -232,6 +269,65 @@ exports.all = function (test, testCommon) {

return db.close()
})

if (testCommon.supports.getSync) {
test(`storage is byte-aware (${keyEncoding} encoding) (sync)`, async function (t) {
const db = testCommon.factory({ keyEncoding })
await db.open()

// These are equal when compared as strings but not when compared as buffers
const one = Buffer.from('80', 'hex')
const two = Buffer.from('c0', 'hex')

await db.put(one, 'one')
t.is(db.getSync(one), 'one', 'value one ok')

await db.put(two, 'two')
t.is(db.getSync(one), 'one', 'value one did not change')

return db.close()
})
}

test(`respects buffer offset and length (${keyEncoding} encoding)`, async function (t) {
const db = testCommon.factory({ keyEncoding })
await db.open()

const a = Buffer.from('000102', 'hex')
const b = a.subarray(1) // 0102
const c = a.subarray(0, 1) // 00

await db.put(a, 'a')
await db.put(b, 'b')
await db.put(c, 'c')

t.is(await db.get(a), 'a', 'value a ok')
t.is(await db.get(b), 'b', 'value b ok')
t.is(await db.get(c), 'c', 'value c ok')

return db.close()
})

if (testCommon.supports.getSync) {
test(`respects buffer offset (${keyEncoding} encoding) (sync)`, async function (t) {
const db = testCommon.factory({ keyEncoding })
await db.open()

const a = Buffer.from('000102', 'hex')
const b = a.subarray(1) // 0102
const c = a.subarray(0, 1) // 00

await db.put(a, 'a')
await db.put(b, 'b')
await db.put(c, 'c')

t.is(db.getSync(a), 'a', 'value a ok')
t.is(db.getSync(b), 'b', 'value b ok')
t.is(db.getSync(c), 'c', 'value c ok')

return db.close()
})
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions test/encoding-custom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ exports.all = function (test, testCommon) {
t.same(await db.get(entry.key), entry.value)
}

if (testCommon.supports.getSync) {
for (const entry of entries) {
t.same(db.getSync(entry.key), entry.value)
}
}

return db.close()
}
}
26 changes: 22 additions & 4 deletions test/encoding-decode-error-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ exports.all = function (test, testCommon) {
})

// NOTE: adapted from encoding-down
test('decode error is wrapped by get() and getMany()', async function (t) {
t.plan(4)
test('decode error is wrapped by get() and variants', async function (t) {
t.plan(testCommon.supports.getSync ? 6 : 4)

const key = testKey()
const valueEncoding = {
Expand All @@ -37,11 +37,20 @@ exports.all = function (test, testCommon) {
t.is(err.code, 'LEVEL_DECODE_ERROR')
t.is(err.cause.message, 'decode error xyz')
}

if (testCommon.supports.getSync) {
try {
db.getSync(key, { valueEncoding })
} catch (err) {
t.is(err.code, 'LEVEL_DECODE_ERROR')
t.is(err.cause.message, 'decode error xyz')
}
}
})

// NOTE: adapted from encoding-down
test('get() and getMany() yield decode error if stored value is invalid', async function (t) {
t.plan(4)
test('get() and variants yield decode error if stored value is invalid', async function (t) {
t.plan(testCommon.supports.getSync ? 6 : 4)

const key = testKey()
await db.put(key, 'this {} is [] not : json', { valueEncoding: 'utf8' })
Expand All @@ -59,6 +68,15 @@ exports.all = function (test, testCommon) {
t.is(err.code, 'LEVEL_DECODE_ERROR')
t.is(err.cause.name, 'SyntaxError') // From JSON.parse()
}

if (testCommon.supports.getSync) {
try {
db.getSync(key, { valueEncoding: 'json' })
} catch (err) {
t.is(err.code, 'LEVEL_DECODE_ERROR')
t.is(err.cause.name, 'SyntaxError') // From JSON.parse()
}
}
})

test('decode error teardown', async function (t) {
Expand Down
6 changes: 6 additions & 0 deletions test/encoding-json-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ exports.all = function (test, testCommon) {
await db.batch(operations)
await Promise.all([...entries.map(testGet), testIterator()])

if (testCommon.supports.getSync) {
for (const entry of entries) {
t.same(db.getSync(entry.key), entry.value)
}
}

return db.close()

async function testGet (entry) {
Expand Down
11 changes: 11 additions & 0 deletions test/encoding-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ exports.all = function (test, testCommon) {
if (!deferred) await db.open()
await db.put(1, 2)
t.is(await db.get(1), '2')
testCommon.supports.getSync && t.is(db.getSync(1), '2')
return db.close()
})
}
Expand All @@ -68,15 +69,25 @@ exports.all = function (test, testCommon) {
const key = testKey()
const data = { thisis: 'json' }
await db.put(key, JSON.stringify(data), { valueEncoding: 'utf8' })

t.same(await db.get(key, { valueEncoding: 'json' }), data, 'got parsed object')

if (testCommon.supports.getSync) {
t.same(db.getSync(key, { valueEncoding: 'json' }), data, 'got parsed object (sync)')
}
})

// NOTE: adapted from encoding-down
test('can decode from json to string', async function (t) {
const data = { thisis: 'json' }
const key = testKey()
await db.put(key, data, { valueEncoding: 'json' })

t.same(await db.get(key, { valueEncoding: 'utf8' }), JSON.stringify(data), 'got unparsed JSON string')

if (testCommon.supports.getSync) {
t.same(db.getSync(key, { valueEncoding: 'utf8' }), JSON.stringify(data), 'got unparsed JSON string (sync)')
}
})

// NOTE: adapted from encoding-down
Expand Down
Loading