Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a2f07b8
Use socketPath option
rikmarais Aug 15, 2023
86eaad5
Override _open for better error handling
rikmarais Aug 16, 2023
6a2370b
Update checks for opening status
rikmarais Aug 16, 2023
b502149
Reorder callback and early exit
rikmarais Aug 16, 2023
102ae90
Merge pull request #1 from ForgeVTT/override_open_improve_err_handling
kakaroto Aug 16, 2023
0b959f8
Do not pass the error back to kConnect
kakaroto Aug 17, 2023
1ef3e6b
Only call cb if it exists
rikmarais Aug 17, 2023
ec727f3
Merge pull request #2 from ForgeVTT/run_cb_if_exists
kakaroto Aug 17, 2023
752aecc
Bump classic-level to 2.0.0
Eranziel Mar 10, 2025
ca43ae7
Initial async refactor. No more callbacks!
Eranziel Mar 10, 2025
3bd3551
Avoid potentially infinite retries
Eranziel Mar 10, 2025
f00dd2f
Abstract Level 2.0 no longer supports Node <16
Eranziel Apr 10, 2025
b0a451c
Bump readable-stream to 4.0.0
Eranziel Apr 17, 2025
9f3b393
Use updated fork of many-level
Eranziel Apr 17, 2025
83e2761
Promisify pipeline
Eranziel Apr 17, 2025
2aa9e3f
Add test with a single database
Eranziel Apr 17, 2025
545af99
Prevent uncaught rejections, allow kConnect to resolve _open early
Eranziel Apr 18, 2025
9bf119d
Update basic tests
Eranziel Apr 18, 2025
dad1111
Refactor bytewise test to async
Eranziel Apr 18, 2025
e6b3bca
Add explanatory comment
Eranziel Apr 18, 2025
35d1e43
Use git+ssh, pin to specific commit
Eranziel Apr 18, 2025
82eb64d
Update many-level dependency commit
Eranziel May 7, 2025
9702f65
Merge pull request #3 from ForgeVTT/cl-2.0-refactor
Eranziel May 7, 2025
6e69fb1
docs: JSDoc all the things
wyrmisis Nov 10, 2025
b3840bc
chore: package-lock.json was never committed
wyrmisis Nov 10, 2025
3820dbd
chore: set connected to false on disconnected socket; add "closed" st…
wyrmisis Nov 11, 2025
d6b4af4
chore: WIP planning comments
Eranziel Nov 11, 2025
4be82c6
fix: multi-process tests - refactor to use async
Eranziel Nov 11, 2025
1944005
fix: refactor sublevel tests to be async
wyrmisis Nov 11, 2025
64f4c41
fix: refactor election tests to be a bit more straightforward
wyrmisis Nov 11, 2025
8b6991d
feat: add basic test for three databases
wyrmisis Nov 11, 2025
0552f97
chore: add and use faucet to format test output
wyrmisis Nov 11, 2025
32a8990
fix: update many-level to latest Forge fork
wyrmisis Nov 12, 2025
52cab09
test: rewrite multi-process tests to be clearer; add tests for multi-…
wyrmisis Nov 17, 2025
68584ef
fix: listen for flush before removing the flush listener
wyrmisis Nov 17, 2025
ac8c067
test: rewrite open-close tests; rename file to something less ambiguous
wyrmisis Nov 17, 2025
502d04e
Improve test messages
Eranziel Nov 18, 2025
342fec2
Reset connection attempt timer on socket close
Eranziel Nov 18, 2025
99d5076
Update many-level after PR merge
Eranziel Nov 20, 2025
512c6e1
Merge pull request #4 from ForgeVTT/fix/get-tests-passing
Eranziel Nov 20, 2025
92b0f25
Refactor leader connection flow
germanoeich Jun 17, 2026
5033687
Support getSync followers
germanoeich Jun 18, 2026
df1da8d
Move getSync worker helper
germanoeich Jun 18, 2026
9d46e93
Bump classic-level to 3.0.0
germanoeich Jun 18, 2026
f1dbf58
Address review feedback
germanoeich Jul 8, 2026
c5483a6
Fix flaky test - ensure db1 is always leader
Eranziel Jul 9, 2026
93440f4
Update many-level dependency
Eranziel Jul 14, 2026
3c14bce
Merge pull request #5 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
45 changes: 45 additions & 0 deletions get-sync-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict'

const { workerData, parentPort } = require('worker_threads')
const net = require('net')
const { ManyLevelGuest } = require('many-level')

const port = workerData.port

parentPort.on('message', async function ({ payload, semaphore }) {
try {
const value = await get(payload.socketPath, payload.key)
port.postMessage({ value })
} catch (err) {
port.postMessage({
error: {
code: err && err.code,
message: err && err.message,
stack: err && err.stack
}
})
} finally {
Atomics.store(semaphore, 0, 1)
Atomics.notify(semaphore, 0, 1)
}
})

async function get (socketPath, key) {
const db = new ManyLevelGuest({
keyEncoding: 'buffer',
valueEncoding: 'buffer',
retry: false,
_remote: () => net.connect(socketPath)
})

await db.open()

try {
return await db.get(Buffer.from(key), {
keyEncoding: 'buffer',
valueEncoding: 'buffer'
})
} finally {
await db.close()
}
}
86 changes: 86 additions & 0 deletions get-sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict'

const ModuleError = require('module-error')
const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads')
const path = require('path')

const leaders = new Map()

// HERE BE DRAGONS
//
// getSync() must be synchronous from abstract-level's perspective, but a follower can only reach the leader through async socket IO.
// We do that async work in a worker thread, then block this thread with Atomics.wait() until the worker replies.
// This exists because real LevelDB getSync() blocks too, and some callers may require the sync API surface.

// If you are wondering why God has forsaken us and given us JavaScript: same.
// This is the pinnacle of cursed code and war crimes. A monstrocity that should never have been born,
// let alone be considered "useful" for any situation other than inflicting psychological damage.
exports.createGetSync = function createGetSync (socketPath) {
let syncRead

return function getSync (key, options) {
if (options.snapshot !== undefined) {
throw new ModuleError('Snapshots are not supported by rave-level getSync() followers', {
code: 'LEVEL_NOT_SUPPORTED'
})
}

const leader = leaders.get(socketPath)

if (leader !== undefined) {
return leader._getSync(key, options)
}

if (syncRead === undefined) {
syncRead = createSyncReadWorker()
}

return syncRead({ socketPath, key })
}
}

exports.registerLeader = function registerLeader (socketPath, db) {
leaders.set(socketPath, db)

return function unregisterLeader () {
if (leaders.get(socketPath) === db) {
leaders.delete(socketPath)
}
}
}

function createSyncReadWorker () {
const { port1, port2 } = new MessageChannel()
const worker = new Worker(path.join(__dirname, 'get-sync-worker.js'), {
workerData: { port: port2 },
transferList: [port2]
})

worker.unref()
port1.unref()

return function syncRead (payload) {
const semaphore = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT))

worker.postMessage({ payload, semaphore })

Atomics.wait(semaphore, 0, 0)

const message = receiveMessageOnPort(port1).message

if (message.error) {
throw remoteError(message.error)
}

return message.value === undefined ? undefined : Buffer.from(message.value)
}
}

function remoteError (error) {
const err = new ModuleError(error.message || 'Could not get value', {
code: error.code || 'LEVEL_REMOTE_ERROR'
})

if (error.stack) err.stack = error.stack
return err
}
98 changes: 95 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import { AbstractLevel, AbstractDatabaseOptions } from 'abstract-level'
import {
AbstractLevel,
AbstractDatabaseOptions,
AbstractOpenOptions,
AbstractGetOptions,
AbstractGetManyOptions,
AbstractHasOptions,
AbstractHasManyOptions,
AbstractPutOptions,
AbstractDelOptions,
AbstractBatchOperation,
AbstractBatchOptions,
AbstractChainedBatch,
AbstractChainedBatchWriteOptions,
AbstractIteratorOptions,
AbstractIterator,
AbstractKeyIterator,
AbstractKeyIteratorOptions,
AbstractValueIterator,
AbstractValueIteratorOptions,
AbstractSnapshot
} from 'abstract-level'

/**
* Use a [LevelDB](https://github.com/google/leveldb) database from multiple processes
Expand All @@ -8,7 +29,7 @@ import { AbstractLevel, AbstractDatabaseOptions } from 'abstract-level'
* @template VDefault The default type of values if not overridden on operations.
*/
export class RaveLevel<KDefault = string, VDefault = string>
extends AbstractLevel<Buffer, KDefault, VDefault> {
extends AbstractLevel<string | Buffer | Uint8Array, KDefault, VDefault> {
/**
* Database constructor.
*
Expand All @@ -20,12 +41,56 @@ export class RaveLevel<KDefault = string, VDefault = string>
location: string,
options?: DatabaseOptions<KDefault, VDefault> | undefined
)

/**
* Whether this instance is the leader process for the database.
*/
isLeader: boolean

open (): Promise<void>
open (options: OpenOptions): Promise<void>

get (key: KDefault): Promise<VDefault | undefined>
get<K = KDefault, V = VDefault> (key: K, options: GetOptions<K, V>): Promise<V | undefined>

getSync (key: KDefault): VDefault | undefined
getSync<K = KDefault, V = VDefault> (key: K, options: GetOptions<K, V>): V | undefined

getMany (keys: KDefault[]): Promise<Array<VDefault | undefined>>
getMany<K = KDefault, V = VDefault> (keys: K[], options: GetManyOptions<K, V>): Promise<Array<V | undefined>>

has (key: KDefault): Promise<boolean>
has<K = KDefault> (key: K, options: HasOptions<K>): Promise<boolean>

hasMany (keys: KDefault[]): Promise<boolean[]>
hasMany<K = KDefault> (keys: K[], options: HasManyOptions<K>): Promise<boolean[]>

put (key: KDefault, value: VDefault): Promise<void>
put<K = KDefault, V = VDefault> (key: K, value: V, options: PutOptions<K, V>): Promise<void>

del (key: KDefault): Promise<void>
del<K = KDefault> (key: K, options: DelOptions<K>): Promise<void>

batch (operations: Array<BatchOperation<typeof this, KDefault, VDefault>>): Promise<void>
batch<K = KDefault, V = VDefault> (operations: Array<BatchOperation<typeof this, K, V>>, options: BatchOptions<K, V>): Promise<void>
batch (): ChainedBatch<typeof this, KDefault, VDefault>

iterator (): Iterator<typeof this, KDefault, VDefault>
iterator<K = KDefault, V = VDefault> (options: IteratorOptions<K, V>): Iterator<typeof this, K, V>

keys (): KeyIterator<typeof this, KDefault>
keys<K = KDefault> (options: KeyIteratorOptions<K>): KeyIterator<typeof this, K>

values (): ValueIterator<typeof this, KDefault, VDefault>
values<K = KDefault, V = VDefault> (options: ValueIteratorOptions<K, V>): ValueIterator<typeof this, K, V>

snapshot (options?: any | undefined): Snapshot
}

/**
* Options for the {@link RaveLevel} constructor.
*/
declare interface DatabaseOptions<K, V> extends
export interface DatabaseOptions<K, V> extends
Omit<AbstractDatabaseOptions<K, V>, 'createIfMissing' | 'errorIfExists'> {
/**
* If true, operations are retried upon connecting to a new leader. If false,
Expand All @@ -35,4 +100,31 @@ declare interface DatabaseOptions<K, V> extends
* @defaultValue `true`
*/
retry?: boolean

/**
* Custom socket path used for follower connections.
*/
raveSocketPath?: string | undefined
}

export type OpenOptions = AbstractOpenOptions
export type GetOptions<K, V> = AbstractGetOptions<K, V>
export type GetManyOptions<K, V> = AbstractGetManyOptions<K, V>
export type HasOptions<K> = AbstractHasOptions<K>
export type HasManyOptions<K> = AbstractHasManyOptions<K>
export type PutOptions<K, V> = AbstractPutOptions<K, V>
export type DelOptions<K> = AbstractDelOptions<K>
export type BatchOptions<K, V> = AbstractBatchOptions<K, V>
export type ChainedBatchWriteOptions = AbstractChainedBatchWriteOptions

export type BatchOperation<TDatabase, K, V> = AbstractBatchOperation<TDatabase, K, V>
export type ChainedBatch<TDatabase, K, V> = AbstractChainedBatch<TDatabase, K, V>
export type Iterator<TDatabase, K, V> = AbstractIterator<TDatabase, K, V>
export type KeyIterator<TDatabase, K> = AbstractKeyIterator<TDatabase, K>
export type ValueIterator<TDatabase, K, V> = AbstractValueIterator<TDatabase, K, V>

export type IteratorOptions<K, V> = AbstractIteratorOptions<K, V>
export type KeyIteratorOptions<K> = AbstractKeyIteratorOptions<K>
export type ValueIteratorOptions<K, V> = AbstractValueIteratorOptions<K, V>

export type Snapshot = AbstractSnapshot
Loading