mirror of https://codeberg.org/pzp/pzp-gc.git
add types in JSDoc
This commit is contained in:
parent
889d003be4
commit
597f3a3bce
242
lib/index.js
242
lib/index.js
|
@ -1,68 +1,200 @@
|
|||
const makeDebug = require('debug')
|
||||
// @ts-ignore
|
||||
const multicb = require('multicb')
|
||||
const makeDebug = require('debug')
|
||||
|
||||
module.exports = {
|
||||
name: 'gc',
|
||||
manifest: {},
|
||||
permissions: {
|
||||
anonymous: {},
|
||||
},
|
||||
/**
|
||||
* @typedef {ReturnType<import('ppppp-db').init>} PPPPPDB
|
||||
* @typedef {ReturnType<import('ppppp-goals').init>} PPPPPGoal
|
||||
* @typedef {{
|
||||
* gc: {
|
||||
* maxLogBytes: number
|
||||
* compactionInterval?: number
|
||||
* }
|
||||
* }} ExpectedConfig
|
||||
* @typedef {{gc?: Partial<ExpectedConfig['gc']>}} Config
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {T extends void ?
|
||||
* (...args: [Error] | []) => void :
|
||||
* (...args: [Error] | [null, T]) => void
|
||||
* } CB
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{ db: PPPPPDB | null }} peer
|
||||
* @returns {asserts peer is { db: PPPPPDB }}
|
||||
*/
|
||||
function assertDBExists(peer) {
|
||||
if (!peer.db) throw new Error('gc plugin requires ppppp-db plugin')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ goals: PPPPPGoal | null }} peer
|
||||
* @returns {asserts peer is { goals: PPPPPGoal }}
|
||||
*/
|
||||
function assertGoalsExists(peer) {
|
||||
if (!peer.goals) throw new Error('gc plugin requires ppppp-goals plugin')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Config} config
|
||||
* @returns {asserts config is ExpectedConfig}
|
||||
*/
|
||||
function assertValidConfig(config) {
|
||||
if (typeof config.gc?.maxLogBytes !== 'number') {
|
||||
throw new Error('gc requires config.gc.maxLogBytes')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ db: PPPPPDB | null, goals: PPPPPGoal | null }} peer
|
||||
* @param {Config} config
|
||||
*/
|
||||
function initGC(peer, config) {
|
||||
// Assertions
|
||||
assertDBExists(peer)
|
||||
assertGoalsExists(peer)
|
||||
assertValidConfig(config)
|
||||
|
||||
// Constants
|
||||
const COMPACTION_INTERVAL = config.gc?.compactionInterval ?? 120e3
|
||||
|
||||
// State
|
||||
const debug = makeDebug('ppppp:gc')
|
||||
let lastCompacted = Date.now()
|
||||
let stopMonitoringLogSize = /** @type {CallableFunction | null} */ (null)
|
||||
let hasCompactionScheduled = false
|
||||
let hasCleanupScheduled = false
|
||||
|
||||
/**
|
||||
* @param {any} peer
|
||||
* @param {{
|
||||
* gc?: {
|
||||
* maxLogBytes?: number
|
||||
* }
|
||||
* }} config
|
||||
* Deletes messages that don't correspond to any goal.
|
||||
* @private
|
||||
* @param {CB<void>} cb
|
||||
*/
|
||||
init(peer, config) {
|
||||
// Assertions
|
||||
if (!peer.goals) throw new Error('gc requires the goals plugin')
|
||||
if (typeof config.gc?.maxLogBytes !== 'number') {
|
||||
throw new Error('gc requires config.gc.maxLogBytes')
|
||||
}
|
||||
|
||||
// State
|
||||
const debug = makeDebug('ppppp:gc')
|
||||
|
||||
/**
|
||||
* Deletes messages that don't correspond to any goal.
|
||||
* @private
|
||||
*/
|
||||
function cleanup(cb) {
|
||||
debug('cleanup goalless started')
|
||||
const done = multicb({ pluck: 1 })
|
||||
let waiting = false
|
||||
for (const rec of peer.db.records()) {
|
||||
if (!rec.msg) continue
|
||||
const purpose = peer.goals.getRecordPurpose(rec)
|
||||
if (purpose === 'none') {
|
||||
peer.db.del(rec.id, done())
|
||||
waiting = true
|
||||
} else if (purpose === 'trail') {
|
||||
peer.db.erase(rec.id, done())
|
||||
waiting = true
|
||||
}
|
||||
function cleanup(cb) {
|
||||
assertDBExists(peer)
|
||||
assertGoalsExists(peer)
|
||||
debug('cleanup goalless started')
|
||||
const done = multicb({ pluck: 1 })
|
||||
let waiting = false
|
||||
for (const rec of peer.db.records()) {
|
||||
if (!rec.msg) continue
|
||||
const purpose = peer.goals.getRecordPurpose(rec)
|
||||
if (purpose === 'none') {
|
||||
peer.db.del(rec.id, done())
|
||||
waiting = true
|
||||
} else if (purpose === 'trail') {
|
||||
peer.db.erase(rec.id, done())
|
||||
waiting = true
|
||||
}
|
||||
function whenEnded(err) {
|
||||
// prettier-ignore
|
||||
if (err) debug('cleanup goalless ended with an error %s', err.message ?? err)
|
||||
}
|
||||
/** @param {Error=} err */
|
||||
function whenEnded(err) {
|
||||
// prettier-ignore
|
||||
if (err) debug('cleanup goalless ended with an error %s', err.message ?? err)
|
||||
else debug('cleanup goalless ended')
|
||||
cb()
|
||||
cb()
|
||||
}
|
||||
if (waiting) done(whenEnded)
|
||||
else whenEnded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact the log (remove deleted records by filling in all the blanks).
|
||||
* @private
|
||||
* @param {number} waitPeriod
|
||||
* @param {CB<void>} cb
|
||||
*/
|
||||
function compact(waitPeriod, cb) {
|
||||
assertDBExists(peer)
|
||||
const log = peer.db._getLog() // TODO: use public API?
|
||||
debug('compaction started')
|
||||
/** @param {Error=} err */
|
||||
function whenEnded(err) {
|
||||
if (err) debug('compaction ended with an error %s', err.message ?? err)
|
||||
else debug('compaction ended')
|
||||
cb()
|
||||
}
|
||||
if (waitPeriod > 0) {
|
||||
setTimeout(log.compact, waitPeriod, whenEnded)
|
||||
} else {
|
||||
log.compact(whenEnded)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor the log size and schedule compaction and/or cleanup.
|
||||
*/
|
||||
function monitorLogSize() {
|
||||
assertDBExists(peer)
|
||||
function checkLogSize() {
|
||||
assertDBExists(peer)
|
||||
assertValidConfig(config)
|
||||
peer.db.logStats((err, stats) => {
|
||||
if (err) return
|
||||
const percentUsed = (stats.totalBytes / config.gc.maxLogBytes) * 100
|
||||
const needsCompaction = stats.deletedBytes > 0
|
||||
|
||||
// Schedule compaction
|
||||
if (needsCompaction && !hasCompactionScheduled) {
|
||||
const nextCompacted = lastCompacted + COMPACTION_INTERVAL
|
||||
const waitPeriod = Math.max(0, nextCompacted - Date.now())
|
||||
hasCompactionScheduled = true
|
||||
compact(waitPeriod, () => {
|
||||
hasCompactionScheduled = false
|
||||
})
|
||||
}
|
||||
|
||||
// Schedule clean up
|
||||
if (percentUsed > 80 && !hasCleanupScheduled) {
|
||||
hasCleanupScheduled = true
|
||||
cleanup(() => {
|
||||
hasCleanupScheduled = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let count = 0
|
||||
stopMonitoringLogSize = peer.db.onRecordAdded(() => {
|
||||
count += 1
|
||||
if (count >= 1000) {
|
||||
count = 0
|
||||
checkLogSize()
|
||||
}
|
||||
if (waiting) done(whenEnded)
|
||||
else whenEnded()
|
||||
}
|
||||
})
|
||||
checkLogSize()
|
||||
}
|
||||
|
||||
function forceImmediately(cb) {
|
||||
debug('force immediately')
|
||||
cleanup(cb)
|
||||
function start() {
|
||||
if (!stopMonitoringLogSize) {
|
||||
monitorLogSize()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initiate,
|
||||
forceImmediately,
|
||||
function stop() {
|
||||
if (stopMonitoringLogSize) {
|
||||
stopMonitoringLogSize()
|
||||
stopMonitoringLogSize = null
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CB<void>} cb
|
||||
*/
|
||||
function forceImmediately(cb) {
|
||||
debug('force immediately')
|
||||
cleanup(cb)
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
forceImmediately,
|
||||
}
|
||||
}
|
||||
|
||||
exports.name = 'gc'
|
||||
exports.init = initGC
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"multicb": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "4.1.9",
|
||||
"bs58": "^5.0.0",
|
||||
"c8": "7",
|
||||
"ppppp-caps": "github:staltz/ppppp-caps",
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"include": ["lib/**/*.js"],
|
||||
"exclude": ["coverage/", "node_modules/", "test/"],
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["es2022", "dom"],
|
||||
"module": "node16",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "es2021"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue