improve types of dependencies

This commit is contained in:
Andre Staltz 2024-01-19 12:24:38 +02:00
parent 9e41400cdc
commit f8a2006eb1
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
11 changed files with 155 additions and 96 deletions

2
.gitignore vendored
View File

@ -3,7 +3,7 @@ node_modules
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
coverage coverage
**/*.d.ts lib/**/*.d.ts
*~ *~
# For misc scripts and experiments: # For misc scripts and experiments:

16
declarations/atomic-file-rw.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
type CB<T> = (...args: [NodeJS.ErrnoException] | [null, T]) => void
declare module 'atomic-file-rw' {
export function readFile(
path: string,
encodingOrOpts: string | { encoding: string },
cb: CB<string>
): void
export function writeFile(
path: string,
data: string,
encodingOrOpts: string | { encoding: string },
cb: CB<string>
): void
export function deleteFile(path: string, cb: CB<null>): void
}

10
declarations/multicb.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module 'multicb' {
type Opts = {
pluck?: number
spread?: boolean
}
type CB<T> = (...args: [Error] | [null, T] | []) => void
type Done<T> = ((cb: CB<T>) => void) & (() => CB<T>)
function multicb<T>(opts?: Opts): Done<T>
export = multicb
}

12
declarations/mutexify.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare module 'mutexify' {
type CB<T> = T extends void
? (...args: [NodeJS.ErrnoException] | []) => void
: (...args: [NodeJS.ErrnoException] | [null, T]) => void
export type Mutexify<T> = (
fn: (
unlock: (cb: CB<T>, ...args: [Error] | [null, T]) => void
) => void
) => void
function mutexify<T>(): Mutexify<T>
export = mutexify
}

10
declarations/obz.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module 'obz' {
type Remove = () => void
export interface Obz<X> {
(listener: (value: X) => void): Remove
set(value: X): this
value: X
}
function createObz(): Obz
export = createObz
}

View File

@ -1,16 +1,22 @@
const FS = require('fs') const FS = require('fs')
const Path = require('path') const Path = require('path')
// @ts-ignore
const atomic = require('atomic-file-rw') const atomic = require('atomic-file-rw')
// @ts-ignore
const multicb = require('multicb') const multicb = require('multicb')
// @ts-ignore
const mutexify = require('mutexify') const mutexify = require('mutexify')
const ReadyGate = require('./utils/ready-gate') const ReadyGate = require('./utils/ready-gate')
// TODO: fs is only supported in node.js. We should support browser by replacing // TODO: fs is only supported in node.js. We should support browser by replacing
// fs.readdir with a browser "file" that just lists all ghost files. // fs.readdir with a browser "file" that just lists all ghost files.
/**
* @typedef {import('./index').MsgID} MsgID
*/
/**
* @template T
* @typedef {import('mutexify').Mutexify<T>} Mutexify
*/
/** /**
* @template T * @template T
* @typedef {T extends void ? * @typedef {T extends void ?
@ -22,14 +28,11 @@ const ReadyGate = require('./utils/ready-gate')
class Ghosts { class Ghosts {
/** @type {string} */ /** @type {string} */
#basePath #basePath
/** @type {ReadyGate} */ /** @type {ReadyGate} */
#loaded #loaded
/** @type {Map<MsgID, Map<string, number>>} */
/** @type {Map<string, Map<string, number>>} */
#maps #maps
/** @type {Mutexify<void>} */
/** @type {(fn: (unlock: (cb: CB<void>, ...args: ([Error] | [null, null])) => void) => void) => void} */
#writeLock #writeLock
static encodingOpts = { encoding: 'utf-8' } static encodingOpts = { encoding: 'utf-8' }
@ -58,7 +61,7 @@ class Ghosts {
cb() cb()
}) })
}) })
done((/** @type {any} */ err) => { done((err, _) => {
// prettier-ignore // prettier-ignore
if (err) throw new Error('GhostDB failed to load', { cause: err }) if (err) throw new Error('GhostDB failed to load', { cause: err })
this.#loaded.setReady() this.#loaded.setReady()
@ -96,21 +99,17 @@ class Ghosts {
* @param {CB<Map<string, number>>} cb * @param {CB<Map<string, number>>} cb
*/ */
#read(tangleID, cb) { #read(tangleID, cb) {
atomic.readFile( atomic.readFile(this.#path(tangleID), Ghosts.encodingOpts, (err, str) => {
this.#path(tangleID), // Load Map
Ghosts.encodingOpts, /** @type {Map<string, number>} */
(/** @type {any} */ err, /** @type {any} */ str) => { let map
// Load Map if (err && err.code === 'ENOENT') map = new Map()
/** @type {Map<string, number>} */
let map
if (err && err.code === 'ENOENT') map = new Map()
// prettier-ignore // prettier-ignore
else if (err) return cb(new Error('GhostDB.read() failed to read ghost file', { cause: err })) else if (err) return cb(new Error('GhostDB.read() failed to read ghost file', { cause: err }))
else map = this.#deserialize(str) else map = this.#deserialize(str)
cb(null, map) cb(null, map)
} })
)
} }
/** /**
@ -148,11 +147,11 @@ class Ghosts {
this.#path(tangleID), this.#path(tangleID),
this.#serialize(newMap), this.#serialize(newMap),
Ghosts.encodingOpts, Ghosts.encodingOpts,
(/** @type {any} */ err) => { (err, _) => {
// prettier-ignore // prettier-ignore
if (err) return unlock(cb, new Error('GhostDB.save() failed to write ghost file', { cause: err })) if (err) return unlock(cb, new Error('GhostDB.save() failed to write ghost file', { cause: err }))
this.#maps.set(tangleID, newMap) this.#maps.set(tangleID, newMap)
unlock(cb, null, null) unlock(cb, null, void 0)
} }
) )
}) })
@ -167,12 +166,12 @@ class Ghosts {
remove(tangleID, msgID, cb) { remove(tangleID, msgID, cb) {
this.#writeLock((unlock) => { this.#writeLock((unlock) => {
this.#loaded.onReady(() => { this.#loaded.onReady(() => {
if (!this.#maps.has(tangleID)) return unlock(cb, null, null) if (!this.#maps.has(tangleID)) return unlock(cb, null, void 0)
const map = /** @type {Map<string, number>} */ ( const map = /** @type {Map<string, number>} */ (
this.#maps.get(tangleID) this.#maps.get(tangleID)
) )
if (!map.has(msgID)) return unlock(cb, null, null) if (!map.has(msgID)) return unlock(cb, null, void 0)
const newMap = new Map(map) const newMap = new Map(map)
newMap.delete(msgID) newMap.delete(msgID)
@ -181,11 +180,11 @@ class Ghosts {
this.#path(tangleID), this.#path(tangleID),
this.#serialize(newMap), this.#serialize(newMap),
Ghosts.encodingOpts, Ghosts.encodingOpts,
(/** @type {any} */ err) => { (err, _) => {
// prettier-ignore // prettier-ignore
if (err) return unlock(cb,new Error('GhostDB.save() failed to write ghost file', { cause: err })) if (err) return unlock(cb,new Error('GhostDB.save() failed to write ghost file', { cause: err }))
this.#maps.set(tangleID, newMap) this.#maps.set(tangleID, newMap)
unlock(cb, null, null) unlock(cb, null, void 0)
} }
) )
}) })

View File

@ -2,7 +2,6 @@ const Path = require('path')
const promisify = require('promisify-4loc') const promisify = require('promisify-4loc')
const b4a = require('b4a') const b4a = require('b4a')
const base58 = require('bs58') const base58 = require('bs58')
// @ts-ignore
const Obz = require('obz') const Obz = require('obz')
const Keypair = require('ppppp-keypair') const Keypair = require('ppppp-keypair')
const Log = require('./log') const Log = require('./log')
@ -30,6 +29,12 @@ const { decrypt } = require('./encryption')
* @typedef {Buffer | Uint8Array} B4A * @typedef {Buffer | Uint8Array} B4A
* @typedef {{global: {keypair: Keypair; path: string}}} ExpectedConfig * @typedef {{global: {keypair: Keypair; path: string}}} ExpectedConfig
* @typedef {{global: {keypair: Keypair; path?: string}}} Config * @typedef {{global: {keypair: Keypair; path?: string}}} Config
* @typedef {{
* close: {
* (errOrEnd: boolean, cb?: CB<void>): void,
* hook(hookIt: (this: unknown, fn: any, args: any) => any): void
* };
* }} Peer
*/ */
/** /**
@ -71,6 +76,11 @@ const { decrypt } = require('./encryption')
* } CB * } CB
*/ */
/**
* @template T
* @typedef {import('obz').Obz<T>} Obz
*/
/** /**
* @param {Config} config * @param {Config} config
* @returns {asserts config is ExpectedConfig} * @returns {asserts config is ExpectedConfig}
@ -173,7 +183,7 @@ class DBTangle extends MsgV4.Tangle {
} }
/** /**
* @param {any} peer * @param {Peer} peer
* @param {Config} config * @param {Config} config
*/ */
function initDB(peer, config) { function initDB(peer, config) {
@ -181,13 +191,11 @@ function initDB(peer, config) {
/** @type {Array<Rec | null>} */ /** @type {Array<Rec | null>} */
const recs = [] const recs = []
/** @type {WeakMap<Rec, Misc>} */ /** @type {WeakMap<Rec, Misc>} */
const miscRegistry = new WeakMap() const miscRegistry = new WeakMap()
/** @type {Map<string, EncryptionFormat>} */ /** @type {Map<string, EncryptionFormat>} */
const encryptionFormats = new Map() const encryptionFormats = new Map()
/** @type {Obz<Rec>} */
const onRecordAdded = Obz() const onRecordAdded = Obz()
const codec = { const codec = {
@ -225,9 +233,8 @@ function initDB(peer, config) {
const ghosts = new Ghosts(Path.join(config.global.path, 'db', 'ghosts')) const ghosts = new Ghosts(Path.join(config.global.path, 'db', 'ghosts'))
peer.close.hook(function (/** @type {any} */ fn, /** @type {any} */ args) { peer.close.hook(function hookToCloseDB(fn, args) {
log.close(() => { log.close(() => {
// @ts-ignore
fn.apply(this, args) fn.apply(this, args)
}) })
}) })

View File

@ -13,20 +13,14 @@ class ErrorWithCode extends Error {
* @param {number} offset * @param {number} offset
*/ */
function nanOffsetErr(offset) { function nanOffsetErr(offset) {
return new ErrorWithCode( return new ErrorWithCode(`Offset ${offset} is not a number`, 'INVALID_OFFSET')
`Offset ${offset} is not a number`,
'ERR_AAOL_INVALID_OFFSET'
)
} }
/** /**
* @param {number} offset * @param {number} offset
*/ */
function negativeOffsetErr(offset) { function negativeOffsetErr(offset) {
return new ErrorWithCode( return new ErrorWithCode(`Offset ${offset} is negative`, 'INVALID_OFFSET')
`Offset ${offset} is negative`,
'ERR_AAOL_INVALID_OFFSET'
)
} }
/** /**
@ -36,12 +30,12 @@ function negativeOffsetErr(offset) {
function outOfBoundsOffsetErr(offset, logSize) { function outOfBoundsOffsetErr(offset, logSize) {
return new ErrorWithCode( return new ErrorWithCode(
`Offset ${offset} is beyond log size ${logSize}`, `Offset ${offset} is beyond log size ${logSize}`,
'ERR_AAOL_OFFSET_OUT_OF_BOUNDS' 'OFFSET_OUT_OF_BOUNDS'
) )
} }
function deletedRecordErr() { function deletedRecordErr() {
return new ErrorWithCode('Record has been deleted', 'ERR_AAOL_DELETED_RECORD') return new ErrorWithCode('Record has been deleted', 'DELETED_RECORD')
} }
function delDuringCompactErr() { function delDuringCompactErr() {

View File

@ -1,14 +1,14 @@
const fs = require('fs') const fs = require('fs')
const b4a = require('b4a') const b4a = require('b4a')
const p = require('promisify-tuple') const p = require('promisify-tuple')
const AtomicFile = require('atomic-file-rw')
const mutexify = require('mutexify')
const Obz = require('obz') // @ts-ignore
const Cache = require('@alloc/quick-lru') // @ts-ignore const Cache = require('@alloc/quick-lru') // @ts-ignore
const RAF = require('polyraf') // @ts-ignore const RAF = require('polyraf') // @ts-ignore
const Obv = require('obz') // @ts-ignore
const AtomicFile = require('atomic-file-rw') // @ts-ignore
const debounce = require('lodash.debounce') // @ts-ignore const debounce = require('lodash.debounce') // @ts-ignore
const isBufferZero = require('is-buffer-zero') // @ts-ignore const isBufferZero = require('is-buffer-zero') // @ts-ignore
const debug = require('debug')('ppppp-db:log') // @ts-ignore const debug = require('debug')('ppppp-db:log')
const mutexify = require('mutexify')
const { const {
deletedRecordErr, deletedRecordErr,
@ -26,6 +26,16 @@ const Record = require('./record')
* @typedef {number} BlockIndex * @typedef {number} BlockIndex
*/ */
/**
* @template T
* @typedef {import('mutexify').Mutexify<T>} Mutexify
*/
/**
* @template T
* @typedef {import('obz').Obz<T>} Obz
*/
/** /**
* @template T * @template T
* @typedef {{ * @typedef {{
@ -120,63 +130,60 @@ function Log(filename, opts) {
let latestBlockIndex = /** @type {number | null} */ (null) let latestBlockIndex = /** @type {number | null} */ (null)
let nextOffsetInBlock = /** @type {number | null} */ (null) let nextOffsetInBlock = /** @type {number | null} */ (null)
let deletedBytes = 0 let deletedBytes = 0
const lastRecOffset = Obv() // offset of last written record /** Offset of last written record @type {Obz<number>} */
const lastRecOffset = Obz()
let compacting = false let compacting = false
const compactionProgress = Obv() const compactionProgress = Obz()
compactionProgress.set(COMPACTION_PROGRESS_START) compactionProgress.set(COMPACTION_PROGRESS_START)
/** @type {Array<CB<any>>} */ /** @type {Array<CB<any>>} */
const waitingCompaction = [] const waitingCompaction = []
AtomicFile.readFile( AtomicFile.readFile(statsFilename, 'utf8', function onStatsLoaded(err, json) {
statsFilename, if (err) {
'utf8', // prettier-ignore
/** @type {CB<string>} */ function doneLoadingStatsFile(err, json) { if (err.code !== 'ENOENT') debug('Failed loading stats file: %s', err.message)
if (err) { deletedBytes = 0
} else {
try {
const stats = JSON.parse(json)
deletedBytes = stats.deletedBytes
} catch (err) {
// prettier-ignore // prettier-ignore
if (err.code !== 'ENOENT') debug('Failed loading stats file: %s', err.message) debug('Failed parsing stats file: %s', /** @type {Error} */ (err).message)
deletedBytes = 0 deletedBytes = 0
} else {
try {
const stats = JSON.parse(json)
deletedBytes = stats.deletedBytes
} catch (err) {
// prettier-ignore
debug('Failed parsing stats file: %s', /** @type {Error} */ (err).message)
deletedBytes = 0
}
} }
}
raf.stat( raf.stat(
/** @type {CB<{size: number}>} */ function onRAFStatDone(err, stat) { /** @type {CB<{size: number}>} */ function onRAFStatDone(err, stat) {
// prettier-ignore // prettier-ignore
if (err && err.code !== 'ENOENT') debug('Failed to read %s stats: %s', filename, err.message) if (err && err.code !== 'ENOENT') debug('Failed to read %s stats: %s', filename, err.message)
const fileSize = stat ? stat.size : -1 const fileSize = stat ? stat.size : -1
if (fileSize <= 0) { if (fileSize <= 0) {
debug('Opened log file, which is empty') debug('Opened log file, which is empty')
latestBlockBuf = b4a.alloc(blockSize) latestBlockBuf = b4a.alloc(blockSize)
latestBlockIndex = 0 latestBlockIndex = 0
nextOffsetInBlock = 0 nextOffsetInBlock = 0
cache.set(0, latestBlockBuf) cache.set(0, latestBlockBuf)
lastRecOffset.set(-1) lastRecOffset.set(-1)
// @ts-ignore
while (waitingLoad.length) waitingLoad.shift()()
} else {
const blockStart = fileSize - blockSize
loadLatestBlock(blockStart, function onLoadedLatestBlock(err) {
if (err) throw err
// prettier-ignore
debug('Opened log file, last record is at log offset %d, block %d', lastRecOffset.value, latestBlockIndex)
// @ts-ignore // @ts-ignore
while (waitingLoad.length) waitingLoad.shift()() while (waitingLoad.length) waitingLoad.shift()()
} else { })
const blockStart = fileSize - blockSize
loadLatestBlock(blockStart, function onLoadedLatestBlock(err) {
if (err) throw err
// prettier-ignore
debug('Opened log file, last record is at log offset %d, block %d', lastRecOffset.value, latestBlockIndex)
// @ts-ignore
while (waitingLoad.length) waitingLoad.shift()()
})
}
} }
) }
} )
) })
/** /**
* @param {number} blockStart * @param {number} blockStart
@ -236,7 +243,7 @@ function Log(filename, opts) {
return getBlockStart(offset) / blockSize return getBlockStart(offset) / blockSize
} }
/** @type {(fn: (unlock: (cb: CB<any>, ...args: ([Error] | [null, any])) => void) => void) => void} */ /** @type {Mutexify<any>} */
const writeLock = mutexify() const writeLock = mutexify()
/** /**
@ -697,7 +704,10 @@ function Log(filename, opts) {
*/ */
function saveStats(cb) { function saveStats(cb) {
const stats = JSON.stringify({ deletedBytes }) const stats = JSON.stringify({ deletedBytes })
AtomicFile.writeFile(statsFilename, stats, 'utf8', cb) AtomicFile.writeFile(statsFilename, stats, 'utf8', (err, _) => {
if (err) return cb(new Error('Failed to save stats file', { cause: err }))
cb()
})
} }
/** @type {CB<void>} */ /** @type {CB<void>} */

View File

@ -41,7 +41,7 @@ test('Log deletes', async (t) => {
await assert.rejects(p(log._get)(offset2), (err) => { await assert.rejects(p(log._get)(offset2), (err) => {
assert.ok(err) assert.ok(err)
assert.equal(err.message, 'Record has been deleted') assert.equal(err.message, 'Record has been deleted')
assert.equal(err.code, 'ERR_AAOL_DELETED_RECORD') assert.equal(err.code, 'DELETED_RECORD')
return true return true
}) })
@ -103,7 +103,7 @@ test('Log deletes', async (t) => {
await assert.rejects(p(log2._get)(offset2), (err) => { await assert.rejects(p(log2._get)(offset2), (err) => {
assert.ok(err) assert.ok(err)
assert.equal(err.message, 'Record has been deleted') assert.equal(err.message, 'Record has been deleted')
assert.equal(err.code, 'ERR_AAOL_DELETED_RECORD') assert.equal(err.code, 'DELETED_RECORD')
return true return true
}) })

View File

@ -1,5 +1,5 @@
{ {
"include": ["lib/**/*.js"], "include": ["declarations", "lib/**/*.js"],
"exclude": ["coverage/", "node_modules/", "test/"], "exclude": ["coverage/", "node_modules/", "test/"],
"compilerOptions": { "compilerOptions": {
"checkJs": true, "checkJs": true,
@ -11,6 +11,7 @@
"module": "node16", "module": "node16",
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "es2022" "target": "es2022",
"typeRoots": ["node_modules/@types", "declarations"]
} }
} }