File Explorer

/var/runtime/node_modules/@aws-sdk/node_modules/undici/lib/cache

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

sqlite-cache-store.js11.2 KB · 468 lines
'use strict' const { Writable } = require('node:stream')const { assertCacheKey, assertCacheValue } = require('../util/cache.js') let DatabaseSync const VERSION = 3 // 2gbconst MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000 /** * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore * @implements {CacheStore} * * @typedef {{ *  id: Readonly<number>, *  body?: Uint8Array *  statusCode: number *  statusMessage: string *  headers?: string *  vary?: string *  etag?: string *  cacheControlDirectives?: string *  cachedAt: number *  staleAt: number *  deleteAt: number * }} SqliteStoreValue */module.exports = class SqliteCacheStore {  #maxEntrySize = MAX_ENTRY_SIZE  #maxCount = Infinity   /**   * @type {import('node:sqlite').DatabaseSync}   */  #db   /**   * @type {import('node:sqlite').StatementSync}   */  #getValuesQuery   /**   * @type {import('node:sqlite').StatementSync}   */  #updateValueQuery   /**   * @type {import('node:sqlite').StatementSync}   */  #insertValueQuery   /**   * @type {import('node:sqlite').StatementSync}   */  #deleteExpiredValuesQuery   /**   * @type {import('node:sqlite').StatementSync}   */  #deleteByUrlQuery   /**   * @type {import('node:sqlite').StatementSync}   */  #countEntriesQuery   /**   * @type {import('node:sqlite').StatementSync | null}   */  #deleteOldValuesQuery   /**   * @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts   */  constructor (opts) {    if (opts) {      if (typeof opts !== 'object') {        throw new TypeError('SqliteCacheStore options must be an object')      }       if (opts.maxEntrySize !== undefined) {        if (          typeof opts.maxEntrySize !== 'number' ||          !Number.isInteger(opts.maxEntrySize) ||          opts.maxEntrySize < 0        ) {          throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')        }         if (opts.maxEntrySize > MAX_ENTRY_SIZE) {          throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')        }         this.#maxEntrySize = opts.maxEntrySize      }       if (opts.maxCount !== undefined) {        if (          typeof opts.maxCount !== 'number' ||          !Number.isInteger(opts.maxCount) ||          opts.maxCount < 0        ) {          throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')        }        this.#maxCount = opts.maxCount      }    }     if (!DatabaseSync) {      DatabaseSync = require('node:sqlite').DatabaseSync    }    this.#db = new DatabaseSync(opts?.location ?? ':memory:')     this.#db.exec(`      PRAGMA journal_mode = WAL;      PRAGMA synchronous = NORMAL;      PRAGMA temp_store = memory;      PRAGMA optimize;       CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (        -- Data specific to us        id INTEGER PRIMARY KEY AUTOINCREMENT,        url TEXT NOT NULL,        method TEXT NOT NULL,         -- Data returned to the interceptor        body BUF NULL,        deleteAt INTEGER NOT NULL,        statusCode INTEGER NOT NULL,        statusMessage TEXT NOT NULL,        headers TEXT NULL,        cacheControlDirectives TEXT NULL,        etag TEXT NULL,        vary TEXT NULL,        cachedAt INTEGER NOT NULL,        staleAt INTEGER NOT NULL      );       CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_getValuesQuery ON cacheInterceptorV${VERSION}(url, method, deleteAt);      CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteByUrlQuery ON cacheInterceptorV${VERSION}(deleteAt);    `)     this.#getValuesQuery = this.#db.prepare(`      SELECT        id,        body,        deleteAt,        statusCode,        statusMessage,        headers,        etag,        cacheControlDirectives,        vary,        cachedAt,        staleAt      FROM cacheInterceptorV${VERSION}      WHERE        url = ?        AND method = ?      ORDER BY        deleteAt ASC    `)     this.#updateValueQuery = this.#db.prepare(`      UPDATE cacheInterceptorV${VERSION} SET        body = ?,        deleteAt = ?,        statusCode = ?,        statusMessage = ?,        headers = ?,        etag = ?,        cacheControlDirectives = ?,        cachedAt = ?,        staleAt = ?      WHERE        id = ?    `)     this.#insertValueQuery = this.#db.prepare(`      INSERT INTO cacheInterceptorV${VERSION} (        url,        method,        body,        deleteAt,        statusCode,        statusMessage,        headers,        etag,        cacheControlDirectives,        vary,        cachedAt,        staleAt      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)    `)     this.#deleteByUrlQuery = this.#db.prepare(      `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`    )     this.#countEntriesQuery = this.#db.prepare(      `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`    )     this.#deleteExpiredValuesQuery = this.#db.prepare(      `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`    )     this.#deleteOldValuesQuery = this.#maxCount === Infinity      ? null      : this.#db.prepare(`        DELETE FROM cacheInterceptorV${VERSION}        WHERE id IN (          SELECT            id          FROM cacheInterceptorV${VERSION}          ORDER BY cachedAt ASC          LIMIT ?        )      `)  }   close () {    this.#db.close()  }   /**   * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key   * @returns {(import('../../types/cache-interceptor.d.ts').default.GetResult & { body?: Buffer }) | undefined}   */  get (key) {    assertCacheKey(key)     const value = this.#findValue(key)    return value      ? {          body: value.body ? Buffer.from(value.body.buffer, value.body.byteOffset, value.body.byteLength) : undefined,          statusCode: value.statusCode,          statusMessage: value.statusMessage,          headers: value.headers ? JSON.parse(value.headers) : undefined,          etag: value.etag ? value.etag : undefined,          vary: value.vary ? JSON.parse(value.vary) : undefined,          cacheControlDirectives: value.cacheControlDirectives            ? JSON.parse(value.cacheControlDirectives)            : undefined,          cachedAt: value.cachedAt,          staleAt: value.staleAt,          deleteAt: value.deleteAt        }      : undefined  }   /**   * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key   * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>}} value   */  set (key, value) {    assertCacheKey(key)     const url = this.#makeValueUrl(key)    const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body    const size = body?.byteLength     if (size && size > this.#maxEntrySize) {      return    }     const existingValue = this.#findValue(key, true)    if (existingValue) {      // Updating an existing response, let's overwrite it      this.#updateValueQuery.run(        body,        value.deleteAt,        value.statusCode,        value.statusMessage,        value.headers ? JSON.stringify(value.headers) : null,        value.etag ? value.etag : null,        value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,        value.cachedAt,        value.staleAt,        existingValue.id      )    } else {      // New response, let's insert it      this.#insertValueQuery.run(        url,        key.method,        body,        value.deleteAt,        value.statusCode,        value.statusMessage,        value.headers ? JSON.stringify(value.headers) : null,        value.etag ? value.etag : null,        value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,        value.vary ? JSON.stringify(value.vary) : null,        value.cachedAt,        value.staleAt      )      this.#prune()    }  }   /**   * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key   * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value   * @returns {Writable | undefined}   */  createWriteStream (key, value) {    assertCacheKey(key)    assertCacheValue(value)     let size = 0    /**     * @type {Buffer[] | null}     */    const body = []    const store = this     return new Writable({      decodeStrings: true,      write (chunk, encoding, callback) {        size += chunk.byteLength         if (size < store.#maxEntrySize) {          body.push(chunk)        } else {          this.destroy()        }         callback()      },      final (callback) {        store.set(key, { ...value, body })        callback()      }    })  }   /**   * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key   */  delete (key) {    if (typeof key !== 'object') {      throw new TypeError(`expected key to be object, got ${typeof key}`)    }     this.#deleteByUrlQuery.run(this.#makeValueUrl(key))  }   #prune () {    if (Number.isFinite(this.#maxCount) && this.size <= this.#maxCount) {      return 0    }     {      const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes      if (removed) {        return removed      }    }     {      const removed = this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes      if (removed) {        return removed      }    }     return 0  }   /**   * Counts the number of rows in the cache   * @returns {Number}   */  get size () {    const { total } = this.#countEntriesQuery.get()    return total  }   /**   * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key   * @returns {string}   */  #makeValueUrl (key) {    return `${key.origin}/${key.path}`  }   /**   * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key   * @param {boolean} [canBeExpired=false]   * @returns {SqliteStoreValue | undefined}   */  #findValue (key, canBeExpired = false) {    const url = this.#makeValueUrl(key)    const { headers, method } = key     /**     * @type {SqliteStoreValue[]}     */    const values = this.#getValuesQuery.all(url, method)     if (values.length === 0) {      return undefined    }     const now = Date.now()    for (const value of values) {      if (now >= value.deleteAt && !canBeExpired) {        continue      }       let matches = true       if (value.vary) {        const vary = JSON.parse(value.vary)         for (const header in vary) {          if (!headerValueEquals(headers[header], vary[header])) {            matches = false            break          }        }      }       if (matches) {        return value      }    }     return undefined  }} /** * @param {string|string[]|null|undefined} lhs * @param {string|string[]|null|undefined} rhs * @returns {boolean} */function headerValueEquals (lhs, rhs) {  if (lhs == null && rhs == null) {    return true  }   if ((lhs == null && rhs != null) ||      (lhs != null && rhs == null)) {    return false  }   if (Array.isArray(lhs) && Array.isArray(rhs)) {    if (lhs.length !== rhs.length) {      return false    }     for (let i = 0; i < lhs.length; i++) {      if (lhs[i] !== rhs[i]) {        return false      }    }     return true  }   return lhs === rhs}