Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3a454fe
Upgrade abstract-level to 2.0.2
Eranziel Feb 26, 2025
f42faf5
Refactor type declarations to remove callbacks
Eranziel Mar 10, 2025
df34e1c
Initial async refactor of guest
Eranziel Mar 10, 2025
e092f55
Fix method returns so they resolve correctly
Eranziel Mar 14, 2025
a64c78e
WIP TODOs
Eranziel Mar 14, 2025
0dc88e7
Convert host-response callbacks to promises
Eranziel Mar 17, 2025
10c2f00
Fix promise resolvers/rejectors
Eranziel Mar 25, 2025
d8f3405
Refactor data decode callback to be closer to old implementation
Eranziel Mar 25, 2025
5107870
Simplify promise factory usage
Eranziel Mar 25, 2025
6b1779b
Convert host decode handlers to async
Eranziel Mar 25, 2025
b445744
Upgrade memory-level to 2.0.0
Eranziel Mar 25, 2025
9ea7323
Fix host createRpcStream for async db.open
Eranziel Mar 25, 2025
ff43b0a
Add test script which includes stack trace in output
Eranziel Mar 25, 2025
fd5aec3
Move promise factory out of ManyLevelGuest so iterator can also use it
Eranziel Mar 26, 2025
d31fe1e
Async clear
Eranziel Apr 3, 2025
84324f9
Rename iterator classes for clarity
Eranziel Apr 1, 2025
fb092f1
Initial async refactor for iterators
Eranziel Apr 3, 2025
6465521
WIP TODOs
Eranziel Apr 3, 2025
469c7db
Fix infinite wait for more data after ended
Eranziel Apr 3, 2025
319df00
Replace promiseFactory with a more conventional use of promises
Eranziel Apr 9, 2025
ce6f245
Abstract Level 2.0 removes support for Node <16
Eranziel Apr 9, 2025
eb9076d
Promisify pipeline and finished in guest
Eranziel Apr 10, 2025
89aaa1d
Promisify stream finished in host
Eranziel Apr 10, 2025
fc36e0e
Refactor to avoid uncaught exceptions
Eranziel Apr 18, 2025
a19ef44
Use createRpcStream directly instead of the back-compat method
Eranziel Apr 17, 2025
2fa5dcf
Clean up done TODOs
Eranziel May 7, 2025
5f7559c
Merge pull request #1 from ForgeVTT/cl-2.0-refactor
Eranziel May 7, 2025
7adbb79
kRpcStream.destroy() is not a promise, needs conventional try..catch
Eranziel Nov 11, 2025
e3e77e4
Merge pull request #2 from ForgeVTT/fix/get-tests-passing
Eranziel Nov 20, 2025
d7729a0
Support abstract-level 3 test semantics
germanoeich Jun 17, 2026
8d4b371
Support v3 has and snapshot APIs
germanoeich Jun 18, 2026
1295525
Expand v3 API coverage
germanoeich Jun 18, 2026
fca50cd
Add opt-in getSync support
germanoeich Jun 18, 2026
67f301e
Clarify snapshot encoding
germanoeich Jul 8, 2026
4ea68e5
Merge pull request #3 from germanoeich/v3
Eranziel Jul 14, 2026
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
494 changes: 346 additions & 148 deletions guest.js

Large diffs are not rendered by default.

198 changes: 153 additions & 45 deletions host.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

const lpstream = require('@vweevers/length-prefixed-stream')
const ModuleError = require('module-error')
const { Duplex, finished } = require('readable-stream')
const { Duplex, promises: readablePromises } = require('readable-stream')
const { finished } = readablePromises
const { input, output } = require('./tags')

const rangeOptions = new Set(['gt', 'gte', 'lt', 'lte'])
Expand All @@ -23,7 +24,6 @@ const kBusy = Symbol('busy')
const kPendingSeek = Symbol('pendingSeek')
const kLimit = Symbol('limit')
const kReadAhead = Symbol('readAhead')
const noop = () => {}
const limbo = Symbol('limbo')

// TODO: make use of db.supports manifest
Expand Down Expand Up @@ -65,7 +65,7 @@ function createRpcStream (db, options, streamOptions) {
const predel = options.predel
const prebatch = options.prebatch

db.open({ passive: true }, ready)
db.open({ passive: true }).then(() => ready()).catch(ready)

// TODO: send events to guest. Challenges:
// - Need to know encodings or emit encoded data; current abstract-level events don't suffice
Expand All @@ -83,14 +83,23 @@ function createRpcStream (db, options, streamOptions) {
if (err) return stream.destroy(err)

const iterators = new Map()
const snapshots = new Map()

finished(stream, function () {
const cleanup = async () => {
await finished(stream).catch(() => null)
for (const iterator of iterators.values()) {
iterator.close()
}

for (const snapshot of snapshots.values()) {
snapshot.close()
}

iterators.clear()
})
snapshots.clear()
}
// Don't await
cleanup()

decode.on('data', function (data) {
if (!data.length) return
Expand Down Expand Up @@ -118,6 +127,10 @@ function createRpcStream (db, options, streamOptions) {
case input.iteratorSeek: return oniteratorseek(req)
case input.clear: return readonly ? onreadonly(req) : onclear(req)
case input.getMany: return ongetmany(req)
case input.has: return onhas(req)
case input.hasMany: return onhasmany(req)
case input.snapshot: return onsnapshot(req)
case input.snapshotClose: return onsnapshotclose(req)
}
})

Expand All @@ -131,54 +144,108 @@ function createRpcStream (db, options, streamOptions) {
encode.write(encodeMessage(msg, output.getManyCallback))
}

function onput (req) {
preput(req.key, req.value, function (err) {
if (err) return callback(req.id, err)
db.put(req.key, req.value, encodingOptions, function (err) {
callback(req.id, err, null)
})
})
function hasCallback (id, err, value) {
const msg = { id, error: errorCode(err), value }
encode.write(encodeMessage(msg, output.hasCallback))
}

function onget (req) {
db.get(req.key, encodingOptions, function (err, value) {
callback(req.id, err, value)
})
function hasManyCallback (id, err, values) {
const msg = { id, error: errorCode(err), values }
encode.write(encodeMessage(msg, output.hasManyCallback))
}

function ongetmany (req) {
db.getMany(req.keys, encodingOptions, function (err, values) {
getManyCallback(req.id, err, values.map(value => ({ value })))
function snapshot (id) {
if (!id) return undefined

const snapshot = snapshots.get(id)
if (snapshot !== undefined) return snapshot

throw new ModuleError('Snapshot is not open', {
code: 'LEVEL_SNAPSHOT_NOT_OPEN'
})
}

function ondel (req) {
predel(req.key, function (err) {
if (err) return callback(req.id, err)
db.del(req.key, encodingOptions, function (err) {
callback(req.id, err)
})
})
async function onput (req) {
preput(req.key, req.value, function (err) { return callback(req.id, err) })
try {
await db.put(req.key, req.value, encodingOptions)
} catch (err) {
return callback(req.id, err, null)
}
}

async function onget (req) {
try {
const value = await db.get(req.key, withSnapshot(encodingOptions, snapshot(req.snapshot)))
return callback(req.id, null, value)
} catch (err) {
return callback(req.id, err, null)
}
}

async function ongetmany (req) {
try {
const values = await db.getMany(req.keys, withSnapshot(encodingOptions, snapshot(req.snapshot)))
return getManyCallback(req.id, null, values.map(value => ({ value })))
} catch (err) {
return getManyCallback(req.id, err, [])
}
}

async function onhas (req) {
try {
const value = await db.has(req.key, withSnapshot(encodingOptions, snapshot(req.snapshot)))
return hasCallback(req.id, null, value)
} catch (err) {
return hasCallback(req.id, err, false)
}
}

async function onhasmany (req) {
try {
const values = await db.hasMany(req.keys, withSnapshot(encodingOptions, snapshot(req.snapshot)))
return hasManyCallback(req.id, null, values)
} catch (err) {
return hasManyCallback(req.id, err, [])
}
}

async function ondel (req) {
predel(req.key, function (err) { return callback(req.id, err) })
try {
await db.del(req.key, encodingOptions)
return callback(req.id, null)
} catch (err) {
return callback(req.id, err)
}
}

function onreadonly (req) {
callback(req.id, new ModuleError('Database is readonly', { code: 'LEVEL_READONLY' }))
}

function onbatch (req) {
prebatch(req.ops, function (err) {
if (err) return callback(req.id, err)

db.batch(req.ops, encodingOptions, function (err) {
callback(req.id, err)
})
})
async function onbatch (req) {
prebatch(req.key, function (err) { return callback(req.id, err) })
try {
await db.batch(req.ops, encodingOptions)
return callback(req.id, null)
} catch (err) {
return callback(req.id, err)
}
}

function oniterator ({ id, seq, options, consumed, bookmark, seek }) {
function oniterator ({ id, seq, options, consumed, bookmark, seek, snapshot: snapshotId }) {
if (iterators.has(id)) return

const it = new Iterator(db, id, seq, options, consumed, encode)
try {
options = withSnapshot(options, snapshot(snapshotId))
} catch (err) {
const data = { id, error: errorCode(err), seq }
encode.write(encodeMessage(data, output.iteratorError))
return
}

const it = new ManyLevelHostIterator(db, id, seq, options, consumed, encode)
iterators.set(id, it)

if (seek) {
Expand Down Expand Up @@ -211,15 +278,38 @@ function createRpcStream (db, options, streamOptions) {
if (it !== undefined) it.close()
}

function onclear (req) {
db.clear(cleanRangeOptions(req.options), function (err) {
callback(req.id, err)
})
async function onclear (req) {
try {
const options = cleanRangeOptions(req.options)
await db.clear(withSnapshot(options, snapshot(options.snapshot)))
return callback(req.id, null)
} catch (err) {
return callback(req.id, err)
}
}

function onsnapshot (req) {
if (snapshots.has(req.id)) return

try {
snapshots.set(req.id, db.snapshot())
} catch (err) {
snapshots.set(req.id, {
close () {},
error: err
})
}
}

async function onsnapshotclose (req) {
const snapshot = snapshots.get(req.id)
snapshots.delete(req.id)
if (snapshot !== undefined) await snapshot.close()
}
}
}

class Iterator {
class ManyLevelHostIterator {
constructor (db, id, seq, options, consumed, encode) {
options = cleanRangeOptions(options)

Expand All @@ -245,7 +335,7 @@ class Iterator {
this.pendingAcks = 0
}

next (first) {
async next (first) {
if (this[kBusy] || this[kClosed]) return
if (this[kEnded] || this.pendingAcks > 1) return

Expand All @@ -269,7 +359,12 @@ class Iterator {
if (size <= 0) {
process.nextTick(this[kHandleMany], null, [])
} else {
this[kIterator].nextv(size, this[kHandleMany])
try {
const nextVal = await this[kIterator].nextv(size)
this[kHandleMany](null, nextVal)
} catch (err) {
this[kHandleMany](err, [])
}
}
}

Expand Down Expand Up @@ -341,10 +436,10 @@ class Iterator {
}
}

close () {
async close () {
if (this[kClosed]) return
this[kClosed] = true
this[kIterator].close(noop)
await this[kIterator].close()
}
}

Expand Down Expand Up @@ -399,3 +494,16 @@ function cleanRangeOptions (options) {

return result
}

function withSnapshot (options, snapshot) {
if (snapshot === undefined) {
if (!options || !hasOwnProperty.call(options, 'snapshot')) return options

const result = { ...options }
delete result.snapshot
return result
}

if (snapshot.error) throw snapshot.error
return { ...options, snapshot }
}
21 changes: 15 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
AbstractLevel,
AbstractDatabaseOptions,
AbstractOpenOptions,
NodeCallback
AbstractGetOptions
} from 'abstract-level'

// Requires `npm install @types/readable-stream`.
Expand All @@ -25,8 +25,6 @@ export class ManyLevelGuest<KDefault = string, VDefault = string>

open (): Promise<void>
open (options: GuestOpenOptions): Promise<void>
open (callback: NodeCallback<void>): void
open (options: GuestOpenOptions, callback: NodeCallback<void>): void

/**
* Create a duplex guest stream to be piped into a host stream. Until that's done,
Expand Down Expand Up @@ -91,6 +89,12 @@ declare interface GuestDatabaseOptions<K, V> extends AbstractDatabaseOptions<K,
*/
retry?: boolean

/**
* Enable {@link ManyLevelGuest.getSync}. If a function is provided, it will be called
* for remote reads after keys and options have been encoded by `abstract-level`.
*/
getSync?: boolean | GuestGetSync

/**
* An {@link AbstractLevel} option that has no effect on {@link ManyLevelGuest}.
*/
Expand Down Expand Up @@ -144,6 +148,11 @@ declare interface GuestRef {
unref: () => void
}

declare type GuestGetSync = (
key: Buffer,
options: AbstractGetOptions<Buffer, Buffer>
) => Buffer | Uint8Array | undefined

/**
* Options for the {@link ManyLevelHost} constructor.
*/
Expand All @@ -158,17 +167,17 @@ declare interface HostOptions {
/**
* A function to be called before `db.put()` operations.
*/
preput?: (key: Buffer, value: Buffer, callback: NodeCallback<void>) => void
preput?: (key: Buffer, value: Buffer) => Promise<void>

/**
* A function to be called before `db.del()` operations.
*/
predel?: (key: Buffer, callback: NodeCallback<void>) => void
predel?: (key: Buffer) => Promise<void>

/**
* A function to be called before `db.batch()` operations.
*/
prebatch?: (operations: HostBatchOperation[], callback: NodeCallback<void>) => void
prebatch?: (operations: HostBatchOperation[]) => Promise<void>
}

declare type HostBatchOperation = HostBatchPutOperation | HostBatchDelOperation
Expand Down
Loading