diff --git a/cli/cli/src/cmd/cache/clear.js b/cli/cli/src/cmd/cache/clear.js new file mode 100644 index 0000000..5fe06c1 --- /dev/null +++ b/cli/cli/src/cmd/cache/clear.js @@ -0,0 +1,364 @@ +import { rm, readdir } from 'node:fs/promises'; +import { createInterface } from 'node:readline'; +import { Cache, decodeCacheKey, createStorageDriver, createPackumentKey } from '@_all_docs/cache'; + +export const usage = `Usage: _all_docs cache clear [options] + +Clear cache entries. + +Options: + --packuments Clear packument cache only + --partitions Clear partition cache only + --checkpoints Clear checkpoint files only + --registry Clear entries for specific registry origin + --match-origin Clear entries matching origin key (e.g., paces.exale.com~javpt) + --package Clear cache for specific package + --older-than Clear entries older than duration (e.g., 7d, 24h, 30m) + --dry-run Show what would be cleared without deleting + --interactive Prompt for confirmation before clearing + +Examples: + _all_docs cache clear # Clear everything + _all_docs cache clear --packuments # Clear only packuments + _all_docs cache clear --partitions # Clear only partitions + _all_docs cache clear --checkpoints # Clear only checkpoints + _all_docs cache clear --registry https://registry.npmjs.com + _all_docs cache clear --match-origin paces.exale.com~javpt + _all_docs cache clear --dry-run # Preview what would be cleared + _all_docs cache clear --interactive # Confirm before clearing + _all_docs cache clear --packuments --older-than 7d + _all_docs cache clear --package lodash +`; + +/** + * Parse duration string to milliseconds + * @param {string} duration - Duration string (e.g., "7d", "24h", "30m") + * @returns {number} Duration in milliseconds + */ +function parseDuration(duration) { + const match = duration.match(/^(\d+)(d|h|m|s)$/); + if (!match) { + throw new Error(`Invalid duration format: ${duration}. Use format like 7d, 24h, 30m, or 60s`); + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + const multipliers = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000 + }; + + return value * multipliers[unit]; +} + +/** + * Prompt user for confirmation + * @param {string} message - Confirmation message + * @returns {Promise} User's response + */ +async function confirm(message) { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise(resolve => { + rl.question(`${message} [y/N] `, answer => { + rl.close(); + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + }); + }); +} + +/** + * Clear entries from a cache directory + * @param {Object} options - Clear options + * @returns {Promise<{cleared: number, skipped: number}>} + */ +async function clearCache({ cachePath, registry, origin, packageName, olderThan, dryRun, entityType }) { + const env = { CACHE_DIR: cachePath }; + const driver = await createStorageDriver(env); + const cache = new Cache({ path: cachePath, driver }); + + let cleared = 0; + let skipped = 0; + const now = Date.now(); + + // Fast path: clear everything if no filters + if (!registry && !origin && !packageName && !olderThan) { + // Count entries first + for await (const key of cache.keys('')) { + try { + const decoded = decodeCacheKey(key); + if (!entityType || decoded.type === entityType) { + const displayName = decoded.type === 'packument' + ? decoded.packageName + : `partition:${decoded.startKey || ''}..${decoded.endKey || ''}`; + + if (dryRun) { + console.log(` [dry-run] Would delete: ${displayName}`); + } + cleared++; + } + } catch { + cleared++; + } + } + + if (!dryRun && cleared > 0) { + await driver.clear(); + console.log(` Cleared ${cleared} entries`); + } + + return { cleared, skipped }; + } + + // If clearing specific package, construct the key directly + if (packageName) { + const registryUrl = registry || 'https://registry.npmjs.com'; + const key = createPackumentKey(packageName, registryUrl); + + try { + const exists = await cache.has(key); + if (exists) { + if (dryRun) { + console.log(` [dry-run] Would delete: ${packageName} (${registryUrl})`); + } else { + await driver.delete(key); + console.log(` Deleted: ${packageName}`); + } + cleared++; + } else { + console.log(` Not found: ${packageName}`); + skipped++; + } + } catch (error) { + console.error(` Error clearing ${packageName}: ${error.message}`); + skipped++; + } + + return { cleared, skipped }; + } + + // Iterate all entries with filters + for await (const key of cache.keys('')) { + try { + const decoded = decodeCacheKey(key); + + // Filter by entity type + if (entityType && decoded.type !== entityType) { + continue; + } + + // Filter by registry URL + if (registry) { + const keyForRegistry = createPackumentKey('test', registry); + const decodedRegistry = decodeCacheKey(keyForRegistry); + if (decoded.origin !== decodedRegistry.origin) { + skipped++; + continue; + } + } + + // Filter by origin key directly + if (origin && !key.includes(`:${origin}:`)) { + skipped++; + continue; + } + + // Filter by age + if (olderThan) { + try { + const info = await driver.info(key); + if (info && info.time) { + const age = now - info.time; + if (age < olderThan) { + skipped++; + continue; + } + } + } catch { + // If we can't get info, skip the age check + } + } + + // Clear the entry + const displayName = decoded.type === 'packument' + ? decoded.packageName + : `partition:${decoded.startKey || ''}..${decoded.endKey || ''}`; + + if (dryRun) { + console.log(` [dry-run] Would delete: ${displayName} (${decoded.origin})`); + } else { + await driver.delete(key); + console.log(` Deleted: ${displayName}`); + } + cleared++; + } catch (error) { + // Skip entries that can't be decoded or deleted + skipped++; + } + } + + return { cleared, skipped }; +} + +/** + * Clear checkpoint files + * @param {string} checkpointDir - Checkpoint directory path + * @param {boolean} dryRun - Whether to do a dry run + * @returns {Promise<{cleared: number}>} + */ +async function clearCheckpoints(checkpointDir, dryRun) { + let cleared = 0; + + try { + const files = await readdir(checkpointDir); + + for (const file of files) { + if (file.endsWith('.checkpoint.json')) { + const filePath = `${checkpointDir}/${file}`; + if (dryRun) { + console.log(` [dry-run] Would delete: ${file}`); + } else { + await rm(filePath); + console.log(` Deleted: ${file}`); + } + cleared++; + } + } + } catch (error) { + if (error.code !== 'ENOENT') { + console.error(` Error clearing checkpoints: ${error.message}`); + } + } + + return { cleared }; +} + +export const command = async cli => { + if (cli.values.help) { + console.log(usage); + return; + } + + const clearPackuments = cli.values.packuments; + const clearPartitions = cli.values.partitions; + const clearCheckpointsFlag = cli.values.checkpoints; + const registry = cli.values.registry; + const origin = cli.values['match-origin']; + const packageName = cli.values.package; + const olderThanStr = cli.values['older-than']; + const dryRun = cli.values['dry-run']; + const interactive = cli.values.interactive; + + // Parse duration if provided + let olderThan = null; + if (olderThanStr) { + try { + olderThan = parseDuration(olderThanStr); + } catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); + } + } + + // If no specific type selected, clear all + const clearAll = !clearPackuments && !clearPartitions && !clearCheckpointsFlag; + + // Build summary of what will be cleared + const targets = []; + if (clearAll || clearPackuments) targets.push('packuments'); + if (clearAll || clearPartitions) targets.push('partitions'); + if (clearAll || clearCheckpointsFlag) targets.push('checkpoints'); + + let description = `Clear ${targets.join(', ')}`; + if (registry) description += ` for registry ${registry}`; + if (origin) description += ` matching origin ${origin}`; + if (packageName) description += ` for package ${packageName}`; + if (olderThan) description += ` older than ${olderThanStr}`; + + console.log(description); + console.log(); + + // Interactive confirmation + if (interactive && !dryRun) { + const confirmed = await confirm('Are you sure you want to proceed?'); + if (!confirmed) { + console.log('Aborted.'); + process.exit(0); + } + console.log(); + } + + let totalCleared = 0; + let totalSkipped = 0; + + // Clear packuments + if (clearAll || clearPackuments) { + const packumentsDir = cli.dir('packuments'); + console.log(`Packuments (${packumentsDir}):`); + + const result = await clearCache({ + cachePath: packumentsDir, + registry, + origin, + packageName, + olderThan, + dryRun, + entityType: 'packument' + }); + + totalCleared += result.cleared; + totalSkipped += result.skipped; + + if (result.cleared === 0 && result.skipped === 0) { + console.log(' (empty)'); + } + console.log(); + } + + // Clear partitions + if ((clearAll || clearPartitions) && !packageName) { + const partitionsDir = cli.dir('partitions'); + console.log(`Partitions (${partitionsDir}):`); + + const result = await clearCache({ + cachePath: partitionsDir, + registry, + origin, + olderThan, + dryRun, + entityType: 'partition' + }); + + totalCleared += result.cleared; + totalSkipped += result.skipped; + + if (result.cleared === 0 && result.skipped === 0) { + console.log(' (empty)'); + } + console.log(); + } + + // Clear checkpoints + if ((clearAll || clearCheckpointsFlag) && !packageName && !registry && !origin) { + const checkpointsDir = `${cli.dir('packuments')}/../checkpoints`; + console.log(`Checkpoints (${checkpointsDir}):`); + + const result = await clearCheckpoints(checkpointsDir, dryRun); + totalCleared += result.cleared; + + if (result.cleared === 0) { + console.log(' (empty)'); + } + console.log(); + } + + // Summary + const action = dryRun ? 'Would clear' : 'Cleared'; + console.log(`${action} ${totalCleared} entries${totalSkipped > 0 ? `, skipped ${totalSkipped}` : ''}`); +}; diff --git a/cli/cli/src/cmd/cache/clear.test.js b/cli/cli/src/cmd/cache/clear.test.js new file mode 100644 index 0000000..457a1ef --- /dev/null +++ b/cli/cli/src/cmd/cache/clear.test.js @@ -0,0 +1,393 @@ +import { describe, it, beforeEach, afterEach, mock } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { join } from 'node:path'; +import { mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs'; + +/** + * Mock storage driver for testing cache clear functionality + */ +class MockStorageDriver { + constructor() { + this.store = new Map(); + this.metadata = new Map(); + } + + async get(key) { + const value = this.store.get(key); + if (!value) throw new Error(`Key not found: ${key}`); + return value; + } + + async put(key, value) { + this.store.set(key, value); + this.metadata.set(key, { time: Date.now() }); + } + + async has(key) { + return this.store.has(key); + } + + async delete(key) { + this.store.delete(key); + this.metadata.delete(key); + } + + async *list(prefix) { + for (const key of this.store.keys()) { + if (key.startsWith(prefix)) { + yield key; + } + } + } + + async clear() { + this.store.clear(); + this.metadata.clear(); + } + + async info(key) { + return this.metadata.get(key) || null; + } + + setEntryTime(key, time) { + if (this.metadata.has(key)) { + this.metadata.get(key).time = time; + } else { + this.metadata.set(key, { time }); + } + } +} + +// Import the module under test - we'll test the exported functions +// Since the command uses dynamic imports and side effects, we'll test +// the pure utility functions directly + +describe('cache clear', () => { + describe('parseDuration', () => { + // Test the duration parsing logic + it('should parse days correctly', () => { + const value = 7; + const unit = 'd'; + const expected = 7 * 24 * 60 * 60 * 1000; + + const multipliers = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000 + }; + + const result = value * multipliers[unit]; + assert.equal(result, expected); + }); + + it('should parse hours correctly', () => { + const value = 24; + const unit = 'h'; + const expected = 24 * 60 * 60 * 1000; + + const multipliers = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000 + }; + + const result = value * multipliers[unit]; + assert.equal(result, expected); + }); + + it('should parse minutes correctly', () => { + const value = 30; + const unit = 'm'; + const expected = 30 * 60 * 1000; + + const multipliers = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000 + }; + + const result = value * multipliers[unit]; + assert.equal(result, expected); + }); + + it('should parse seconds correctly', () => { + const value = 60; + const unit = 's'; + const expected = 60 * 1000; + + const multipliers = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000 + }; + + const result = value * multipliers[unit]; + assert.equal(result, expected); + }); + + it('should reject invalid duration format', () => { + const invalidFormats = ['7days', 'abc', '7', '7w', '-7d', '7.5d']; + const pattern = /^(\d+)(d|h|m|s)$/; + + for (const format of invalidFormats) { + assert.equal(pattern.test(format), false, `Expected ${format} to be invalid`); + } + }); + + it('should accept valid duration format', () => { + const validFormats = ['7d', '24h', '30m', '60s', '1d', '100h']; + const pattern = /^(\d+)(d|h|m|s)$/; + + for (const format of validFormats) { + assert.equal(pattern.test(format), true, `Expected ${format} to be valid`); + } + }); + }); + + describe('MockStorageDriver', () => { + let driver; + + beforeEach(() => { + driver = new MockStorageDriver(); + }); + + it('should store and retrieve values', async () => { + await driver.put('test-key', { data: 'value' }); + const result = await driver.get('test-key'); + assert.deepEqual(result, { data: 'value' }); + }); + + it('should check key existence', async () => { + assert.equal(await driver.has('missing'), false); + await driver.put('exists', { data: true }); + assert.equal(await driver.has('exists'), true); + }); + + it('should delete keys', async () => { + await driver.put('to-delete', { data: 'temp' }); + assert.equal(await driver.has('to-delete'), true); + await driver.delete('to-delete'); + assert.equal(await driver.has('to-delete'), false); + }); + + it('should clear all entries', async () => { + await driver.put('key1', { n: 1 }); + await driver.put('key2', { n: 2 }); + await driver.put('key3', { n: 3 }); + + assert.equal(driver.store.size, 3); + + await driver.clear(); + + assert.equal(driver.store.size, 0); + }); + + it('should list keys by prefix', async () => { + await driver.put('prefix:1', { n: 1 }); + await driver.put('prefix:2', { n: 2 }); + await driver.put('other:1', { n: 3 }); + + const keys = []; + for await (const key of driver.list('prefix:')) { + keys.push(key); + } + + assert.equal(keys.length, 2); + assert.ok(keys.includes('prefix:1')); + assert.ok(keys.includes('prefix:2')); + }); + + it('should get entry metadata info', async () => { + const before = Date.now(); + await driver.put('with-meta', { data: true }); + const after = Date.now(); + + const info = await driver.info('with-meta'); + + assert.ok(info); + assert.ok(info.time >= before); + assert.ok(info.time <= after); + }); + + it('should return null for missing entry info', async () => { + const info = await driver.info('nonexistent'); + assert.equal(info, null); + }); + + it('should allow setting custom entry time for age filtering tests', async () => { + await driver.put('old-entry', { data: 'old' }); + + const oldTime = Date.now() - (10 * 24 * 60 * 60 * 1000); // 10 days ago + driver.setEntryTime('old-entry', oldTime); + + const info = await driver.info('old-entry'); + assert.equal(info.time, oldTime); + }); + }); + + describe('cache key filtering', () => { + it('should identify packument keys', () => { + const packumentKey = 'v1:packument:npm:6c6f64617368'; // lodash + const parts = packumentKey.split(':'); + + assert.equal(parts[1], 'packument'); + }); + + it('should identify partition keys', () => { + const partitionKey = 'v1:partition:npm:61:62'; // a to b + const parts = partitionKey.split(':'); + + assert.equal(parts[1], 'partition'); + }); + + it('should extract origin from key', () => { + const key = 'v1:packument:npm:6c6f64617368'; + const parts = key.split(':'); + + assert.equal(parts[2], 'npm'); + }); + + it('should match origin in key', () => { + const key = 'v1:packument:custom.reg.io:6c6f64617368'; + const originToMatch = 'custom.reg.io'; + + assert.ok(key.includes(`:${originToMatch}:`)); + }); + }); + + describe('checkpoint file handling', () => { + const testDir = join(import.meta.dirname, 'test-clear-fixtures'); + const checkpointDir = join(testDir, 'checkpoints'); + + beforeEach(() => { + mkdirSync(checkpointDir, { recursive: true }); + }); + + afterEach(() => { + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true, force: true }); + } + }); + + it('should identify checkpoint files by extension', () => { + const checkpointFile = 'fetch-list.checkpoint.json'; + assert.ok(checkpointFile.endsWith('.checkpoint.json')); + }); + + it('should not match non-checkpoint JSON files', () => { + const regularFile = 'config.json'; + assert.equal(regularFile.endsWith('.checkpoint.json'), false); + }); + + it('should handle checkpoint directory creation', () => { + assert.ok(existsSync(checkpointDir)); + }); + + it('should clean up checkpoint files', async () => { + // Create some checkpoint files + writeFileSync(join(checkpointDir, 'test1.checkpoint.json'), '{}'); + writeFileSync(join(checkpointDir, 'test2.checkpoint.json'), '{}'); + writeFileSync(join(checkpointDir, 'config.json'), '{}'); // Should not be deleted + + const { readdir, rm } = await import('node:fs/promises'); + const files = await readdir(checkpointDir); + + const checkpointFiles = files.filter(f => f.endsWith('.checkpoint.json')); + assert.equal(checkpointFiles.length, 2); + + // Clean up checkpoint files + for (const file of checkpointFiles) { + await rm(join(checkpointDir, file)); + } + + const remaining = await readdir(checkpointDir); + assert.equal(remaining.length, 1); + assert.equal(remaining[0], 'config.json'); + }); + }); + + describe('dry run behavior', () => { + let driver; + let logs; + let originalLog; + + beforeEach(() => { + driver = new MockStorageDriver(); + logs = []; + originalLog = console.log; + console.log = (...args) => logs.push(args.join(' ')); + }); + + afterEach(() => { + console.log = originalLog; + }); + + it('should not delete entries in dry run mode', async () => { + await driver.put('test-key', { data: 'value' }); + + // Simulate dry run - just log without deleting + const dryRun = true; + if (dryRun) { + console.log('[dry-run] Would delete: test-key'); + } else { + await driver.delete('test-key'); + } + + assert.equal(await driver.has('test-key'), true); + assert.ok(logs.some(l => l.includes('[dry-run]'))); + }); + + it('should delete entries when not in dry run mode', async () => { + await driver.put('test-key', { data: 'value' }); + + const dryRun = false; + if (dryRun) { + console.log('[dry-run] Would delete: test-key'); + } else { + await driver.delete('test-key'); + } + + assert.equal(await driver.has('test-key'), false); + }); + }); + + describe('age-based filtering', () => { + let driver; + + beforeEach(() => { + driver = new MockStorageDriver(); + }); + + it('should skip entries newer than threshold', async () => { + await driver.put('new-entry', { data: 'new' }); + + const olderThan = 7 * 24 * 60 * 60 * 1000; // 7 days + const now = Date.now(); + const info = await driver.info('new-entry'); + const age = now - info.time; + + // New entry should be younger than threshold + assert.ok(age < olderThan); + }); + + it('should include entries older than threshold', async () => { + await driver.put('old-entry', { data: 'old' }); + + // Simulate old entry + const oldTime = Date.now() - (10 * 24 * 60 * 60 * 1000); // 10 days ago + driver.setEntryTime('old-entry', oldTime); + + const olderThan = 7 * 24 * 60 * 60 * 1000; // 7 days + const now = Date.now(); + const info = await driver.info('old-entry'); + const age = now - info.time; + + // Old entry should be older than threshold + assert.ok(age >= olderThan); + }); + }); +}); diff --git a/cli/cli/src/jack.js b/cli/cli/src/jack.js index 4691a31..eb7e45c 100644 --- a/cli/cli/src/jack.js +++ b/cli/cli/src/jack.js @@ -203,6 +203,46 @@ const cli = ack } }) + .flag({ + packuments: { + description: 'Target packument cache (for cache clear)' + }, + partitions: { + description: 'Target partition cache (for cache clear)' + }, + checkpoints: { + description: 'Target checkpoint files (for cache clear)' + }, + 'dry-run': { + description: 'Show what would be done without making changes' + }, + interactive: { + short: 'i', + description: 'Prompt for confirmation before destructive operations' + } + }) + + .opt({ + 'older-than': { + hint: 'duration', + description: `Clear entries older than duration (e.g., 7d, 24h, 30m) + + Valid units: d (days), h (hours), m (minutes), s (seconds) + ` + }, + package: { + hint: 'name', + description: 'Target specific package by name (for cache clear)' + }, + 'match-origin': { + hint: 'key', + description: `Clear entries matching origin key (for cache clear) + + Use the encoded origin format (e.g., paces.exale.com~javpt) + ` + } + }) + .flag({ version: { short: 'v', diff --git a/cli/cli/src/output.js b/cli/cli/src/output.js index d2d012e..8df0ffb 100644 --- a/cli/cli/src/output.js +++ b/cli/cli/src/output.js @@ -8,7 +8,13 @@ async function outputCommand({ command, usage, name }, cli) { } if (cli.values.help) { - console.log(usage().usage()); + if (typeof usage === 'string') { + console.log(usage); + } else if (typeof usage === 'function') { + console.log(usage().usage()); + } else { + console.log(cli.usage()); + } return; } diff --git a/doc/cli-reference.md b/doc/cli-reference.md index e5014d1..ebb0767 100644 --- a/doc/cli-reference.md +++ b/doc/cli-reference.md @@ -359,29 +359,61 @@ Total Cache Size: 1.14 GB ### cache clear -Clear cache entries. +Clear cache entries with flexible filtering options. ```bash -npx _all_docs cache clear +npx _all_docs cache clear [options] ``` **Options:** -- `--type ` - Clear type: partition, packument, all -- `--older-than ` - Only clear entries older than N days -- `--pattern ` - Clear entries matching pattern -- `--yes` - Skip confirmation prompt +- `--packuments` - Clear packument cache only +- `--partitions` - Clear partition cache only +- `--checkpoints` - Clear checkpoint files only +- `--registry ` - Clear entries for specific registry origin +- `--match-origin ` - Clear entries matching origin key (e.g., `custom.reg.io`) +- `--package ` - Clear cache for specific package +- `--older-than ` - Clear entries older than duration (e.g., `7d`, `24h`, `30m`, `60s`) +- `--dry-run` - Show what would be cleared without deleting +- `--interactive`, `-i` - Prompt for confirmation before clearing + +**Duration format:** +- `d` - days (e.g., `7d` = 7 days) +- `h` - hours (e.g., `24h` = 24 hours) +- `m` - minutes (e.g., `30m` = 30 minutes) +- `s` - seconds (e.g., `60s` = 60 seconds) + +**Examples:** + +```bash +# Clear everything (all cache types) +npx _all_docs cache clear -**Example:** +# Clear only packuments +npx _all_docs cache clear --packuments -```bash -# Clear all cache (with confirmation) -npx _all_docs cache clear --type all +# Clear only partitions +npx _all_docs cache clear --partitions + +# Clear only checkpoint files +npx _all_docs cache clear --checkpoints -# Clear old partitions -npx _all_docs cache clear --type partition --older-than 30 --yes +# Clear cache for a specific package +npx _all_docs cache clear --package lodash -# Clear specific patterns -npx _all_docs cache clear --pattern "*express*" --yes +# Clear cache for a specific registry +npx _all_docs cache clear --registry https://registry.npmjs.com + +# Clear entries older than 7 days +npx _all_docs cache clear --older-than 7d + +# Clear old packuments only +npx _all_docs cache clear --packuments --older-than 7d + +# Preview what would be cleared (dry run) +npx _all_docs cache clear --dry-run + +# Interactive mode - confirm before clearing +npx _all_docs cache clear --interactive ``` --- @@ -560,6 +592,7 @@ NODE_OPTIONS="--max-old-space-size=4096" \ # Validate and fix npx _all_docs cache validate-partitions --fix -# Or clear and restart -npx _all_docs cache clear --type all --yes +# Or clear and restart (preview first with --dry-run) +npx _all_docs cache clear --dry-run +npx _all_docs cache clear ``` \ No newline at end of file diff --git a/src/packument/client.js b/src/packument/client.js index 97443de..4755137 100644 --- a/src/packument/client.js +++ b/src/packument/client.js @@ -82,7 +82,7 @@ export class PackumentClient extends BaseHTTPClient { */ async request(packageName, options = {}) { await this.ensureInitialized(); - + // Build URL const url = new URL(`/${encodeURIComponent(packageName)}`, this.origin); diff --git a/workers/node/storage.js b/workers/node/storage.js index 2c86681..7cbc6da 100644 --- a/workers/node/storage.js +++ b/workers/node/storage.js @@ -91,6 +91,23 @@ export class NodeStorageDriver { entries.map(({ key, value }) => this.put(key, value)) ); } + + /** + * Clear all entries from the cache + * @returns {Promise} + */ + async clear() { + await cacache.rm.all(this.cachePath); + } + + /** + * Get metadata info for a cache entry + * @param {string} key - Cache key + * @returns {Promise} Entry info or null if not found + */ + async info(key) { + return cacache.get.info(this.cachePath, key); + } } // Export a factory function that matches the runtime interface