From 597f3a3bce22f525a72d7f54f9fb4d2457b51256 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Thu, 28 Sep 2023 11:25:14 +0300 Subject: [PATCH] add types in JSDoc --- lib/index.js | 242 ++++++++++++++++++++++++++++++++++++++------------ package.json | 1 + tsconfig.json | 16 ++++ 3 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 tsconfig.json diff --git a/lib/index.js b/lib/index.js index 398ac19..ff0ecda 100644 --- a/lib/index.js +++ b/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} PPPPPDB + * @typedef {ReturnType} PPPPPGoal + * @typedef {{ + * gc: { + * maxLogBytes: number + * compactionInterval?: number + * } + * }} ExpectedConfig + * @typedef {{gc?: Partial}} 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} 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} 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} cb + */ + function forceImmediately(cb) { + debug('force immediately') + cleanup(cb) + } + + return { + start, + stop, + forceImmediately, + } } + +exports.name = 'gc' +exports.init = initGC diff --git a/package.json b/package.json index c2f015c..051c0c3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bd2acd5 --- /dev/null +++ b/tsconfig.json @@ -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" + } +} \ No newline at end of file