diff --git a/lib/util/cache/package/index.ts b/lib/util/cache/package/index.ts index bfbc4b2f375d7d..8d2d6dace2021c 100644 --- a/lib/util/cache/package/index.ts +++ b/lib/util/cache/package/index.ts @@ -66,21 +66,7 @@ export async function init(config: AllConfig): Promise { // istanbul ignore if if (config.cacheDir && config.useSqliteCache) { - const sqlite = await SqlitePackageCache.init(config.cacheDir); - cacheProxy = { - get: (namespace: string, key: string) => - Promise.resolve(sqlite.get(namespace, key)), - set: ( - namespace: string, - key: string, - value: unknown, - ttlMinutes: number, - ) => Promise.resolve(sqlite.set(namespace, key, value, ttlMinutes)), - cleanup: () => { - sqlite.close(); - return Promise.resolve(); - }, - }; + cacheProxy = await SqlitePackageCache.init(config.cacheDir); return; } diff --git a/lib/util/cache/package/sqlite.spec.ts b/lib/util/cache/package/sqlite.spec.ts index b2514c9582dfb5..ccee0baf256435 100644 --- a/lib/util/cache/package/sqlite.spec.ts +++ b/lib/util/cache/package/sqlite.spec.ts @@ -2,13 +2,13 @@ import { withDir } from 'tmp-promise'; import { SqlitePackageCache } from './sqlite'; function withSqlite( - fn: (sqlite: SqlitePackageCache) => T | Promise, + fn: (sqlite: SqlitePackageCache) => Promise, ): Promise { return withDir( async ({ path }) => { const sqlite = await SqlitePackageCache.init(path); const res = await fn(sqlite); - sqlite.close(); + await sqlite.cleanup(); return res; }, { unsafeCleanup: true }, @@ -22,10 +22,10 @@ describe('util/cache/package/sqlite', () => { }); it('should set and get', async () => { - const res = await withSqlite((sqlite) => { - sqlite.set('foo', 'bar', { foo: 'foo' }); - sqlite.set('foo', 'bar', { bar: 'bar' }); - sqlite.set('foo', 'bar', { baz: 'baz' }); + const res = await withSqlite(async (sqlite) => { + await sqlite.set('foo', 'bar', { foo: 'foo' }); + await sqlite.set('foo', 'bar', { bar: 'bar' }); + await sqlite.set('foo', 'bar', { baz: 'baz' }); return sqlite.get('foo', 'bar'); }); expect(res).toEqual({ baz: 'baz' }); diff --git a/lib/util/cache/package/sqlite.ts b/lib/util/cache/package/sqlite.ts index a48bac96cf77c3..c27c80d06554fc 100644 --- a/lib/util/cache/package/sqlite.ts +++ b/lib/util/cache/package/sqlite.ts @@ -1,13 +1,30 @@ +import { brotliCompressSync, brotliDecompressSync, constants } from 'node:zlib'; import Sqlite from 'better-sqlite3'; import type { Database, Statement } from 'better-sqlite3'; import * as upath from 'upath'; import { logger } from '../../../logger'; import { ensureDir } from '../../fs'; +function compress(input: unknown): Buffer { + const jsonStr = JSON.stringify(input); + return brotliCompressSync(jsonStr, { + params: { + [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, + [constants.BROTLI_PARAM_QUALITY]: 8, + }, + }); +} + +function decompress(input: Buffer): T { + const buf = brotliDecompressSync(input); + const jsonStr = buf.toString('utf8'); + return JSON.parse(jsonStr) as T; +} + export class SqlitePackageCache { private readonly upsertStatement: Statement; private readonly getStatement: Statement; - private readonly cleanupStatement: Statement; + private readonly deleteExpiredRows: Statement; private readonly countStatement: Statement; static async init(cacheDir: string): Promise { @@ -29,8 +46,8 @@ export class SqlitePackageCache { CREATE TABLE IF NOT EXISTS package_cache ( namespace TEXT NOT NULL, key TEXT NOT NULL, - data TEXT NOT NULL, expiry INTEGER NOT NULL, + data BLOB NOT NULL, PRIMARY KEY (namespace, key) ) `, @@ -63,7 +80,7 @@ export class SqlitePackageCache { ) .pluck(true); - this.cleanupStatement = client.prepare(` + this.deleteExpiredRows = client.prepare(` DELETE FROM package_cache WHERE expiry <= unixepoch() `); @@ -73,21 +90,33 @@ export class SqlitePackageCache { .pluck(true); } - set(namespace: string, key: string, value: unknown, ttlMinutes = 5): void { - const data = JSON.stringify(value); + set( + namespace: string, + key: string, + value: unknown, + ttlMinutes = 5, + ): Promise { + const data = compress(value); const ttlSeconds = ttlMinutes * 60; this.upsertStatement.run({ namespace, key, data, ttlSeconds }); + return Promise.resolve(); } - get(namespace: string, key: string): T | undefined { - const res = this.getStatement.get({ namespace, key }) as string | undefined; - return res ? JSON.parse(res) : undefined; + get(namespace: string, key: string): Promise { + const data = this.getStatement.get({ namespace, key }) as + | Buffer + | undefined; + if (!data) { + return Promise.resolve(undefined); + } + const res = decompress(data); + return Promise.resolve(res); } - private cleanup(): void { + private cleanupExpired(): void { const start = Date.now(); const totalCount = this.countStatement.get() as number; - const { changes: deletedCount } = this.cleanupStatement.run(); + const { changes: deletedCount } = this.deleteExpiredRows.run(); const finish = Date.now(); const durationMs = finish - start; logger.debug( @@ -95,8 +124,9 @@ export class SqlitePackageCache { ); } - close(): void { - this.cleanup(); + cleanup(): Promise { + this.cleanupExpired(); this.client.close(); + return Promise.resolve(); } }