From 357cd050297c1d5f72299e45b5be4da77af573ee Mon Sep 17 00:00:00 2001 From: Naramsim Date: Thu, 23 Apr 2026 15:26:02 +0200 Subject: [PATCH 1/5] feat: add invalidate cache method --- src/getter.js | 64 ++++++++++++++++++++++++++++++++++++++--------- src/index.js | 14 +++++++---- test/test.html.js | 5 ++-- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/getter.js b/src/getter.js index 0e795ad..c09a3da 100644 --- a/src/getter.js +++ b/src/getter.js @@ -2,23 +2,34 @@ import { log, canUseCache } from './utils.js' var db -function openDB(config) { +function openCache(config) { if (config.cache && typeof window !== 'undefined') { - const request = window.indexedDB.open("pokeapi-js-wrapper", 3); + const request = window.indexedDB.open("pokeapi-js-wrapper", 8); return new Promise((resolve, reject) => { request.onerror = (event) => { log('IndexedDB not available') reject() } request.onupgradeneeded = (event) => { - db = event.target.result; - log('db opened and cache created') - db.createObjectStore("cache", { autoIncrement: false }); - resolve(db) + const db = event.target.result; + const transaction = event.target.transaction; + let objectStore; + + if (!db.objectStoreNames.contains('cache')) { + objectStore = db.createObjectStore("cache", { autoIncrement: false }); + log('Object store "cache" created'); + } else { + objectStore = transaction.objectStore("cache"); + } + + if (!objectStore.indexNames.contains("deploy_date_index")) { + objectStore.createIndex("deploy_date_index", "meta.deploy_date", { unique: false }); + log('Index "deploy_date_index" created'); + } } request.onsuccess = (event) => { - log('db opened') db = event.target.result; + log('db opened') resolve(db) } request.onversionchange = (event) => { @@ -44,7 +55,7 @@ function getFromDB(objectStore, url) { async function loadResource(config, url) { if (! url.includes('://')) { url = url.replace(/^\//, ''); - url = `${config.protocol}://${config.hostName}/${url}` + url = `${config.protocol}://${config.hostName}${config.versionPath}${url}` } if (canUseCache(config, db)) { const transaction = db.transaction("cache", "readonly"); @@ -66,10 +77,14 @@ async function loadUrl(config, url) { const body = await response.json() if (response.status === 200) { if (canUseCache(config, db)) { + const deploy_date = parseInt(response.headers.get('X-PokeAPI-Deploy-Date')) + body.meta = { deploy_date } const transaction = db.transaction("cache", "readwrite"); const objectStore = transaction.objectStore("cache"); const request = objectStore.add(body, url) - request.onsuccess = () => log(`object cached ${url}`); + request.onsuccess = () => { + log(`object cached ${url}`); + } request.onerror = () => { log(request.error) } @@ -79,7 +94,7 @@ async function loadUrl(config, url) { return body } -function sizeDB(config) { +function sizeCache(config) { if (canUseCache(config, db)) { return new Promise((resolve, reject) => { const transaction = db.transaction("cache", "readwrite"); @@ -93,7 +108,32 @@ function sizeDB(config) { } } -function clearDB(config) { +async function invalidateCache(config) { + if (canUseCache(config, db)) { + const meta = await loadResource({...config, cache: false}, 'meta') + const upstream_deploy_date = parseInt(meta.deploy_date) + const transaction = db.transaction("cache", "readwrite"); + const objectStore = transaction.objectStore("cache"); + const index = objectStore.index("deploy_date_index") + const range = IDBKeyRange.upperBound(upstream_deploy_date, true); + const request = index.getAllKeys(range); + + request.onsuccess = () => { + const keys = request.result; + keys.forEach(pk => { + objectStore.delete(pk); + log(`invalidated ${pk}`); + }); + return true + }; + request.onerror = () => {throw new Error(request.error); + }; + } else { + throw new Error('cache not available') + } +} + +function clearCache(config) { if (canUseCache(config, db)) { return new Promise((resolve, reject) => { const transaction = db.transaction("cache", "readwrite"); @@ -107,4 +147,4 @@ function clearDB(config) { } } -export { loadResource, openDB, sizeDB, clearDB } +export { loadResource, openCache, sizeCache, clearCache, invalidateCache } diff --git a/src/index.js b/src/index.js index 593f0f8..59c7ed5 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import endpoints from './endpoints.json' with { type: "json" } import rootEndpoints from './rootEndpoints.json' with { type: "json" } -import { loadResource, openDB, sizeDB, clearDB } from './getter.js' +import { loadResource, openCache, sizeCache, clearCache, invalidateCache } from './getter.js' import { Config } from './config.js' export class Pokedex { @@ -18,7 +18,7 @@ export class Pokedex { // if the user has submitted a Name or an ID, return the JSON promise if (typeof input === 'number' || typeof input === 'string') { - return loadResource(this.config, `${this.config.versionPath}${endpoint[2].replace(':id', input)}`) + return loadResource(this.config, `${endpoint[2].replace(':id', input)}`) } // if the user has submitted an Array @@ -44,7 +44,7 @@ export class Pokedex { limit = config.limit } } - return loadResource(this.config, `${this.config.versionPath}${rootEndpoint[1]}?limit=${limit}&offset=${offset}`) + return loadResource(this.config, `${rootEndpoint[1]}?limit=${limit}&offset=${offset}`) } this[rootEndpoint[0]] = this[rootEndpointFullName] }) @@ -56,7 +56,7 @@ export class Pokedex { static async init(config) { config = new Config(config) - await openDB(config) + await openCache(config) return new Pokedex(config) } @@ -72,6 +72,10 @@ export class Pokedex { return clearDB(this.config) } + invalidateCache() { + return invalidateCache(this.config) + } + resource(path) { if (typeof path === 'string') { return loadResource(this.config, path) @@ -85,7 +89,7 @@ export class Pokedex { function mapResources(config, endpoint, inputs) { return inputs.map(input => { - return loadResource(config, `${config.versionPath}${endpoint[2].replace(':id', input)}`) + return loadResource(config, `${endpoint[2].replace(':id', input)}`) }) } diff --git a/test/test.html.js b/test/test.html.js index 64f2007..2a9dc22 100644 --- a/test/test.html.js +++ b/test/test.html.js @@ -42,16 +42,17 @@ describe("pokedex", function () { describe(".resource(Mixed: array) not cached", function () { it("should have property name", async function () { - const res = await customP.resource(['/api/v2/pokemon/36', 'api/v2/berry/8', 'https://pokeapi.co/api/v2/ability/9/']); + const res = await customP.resource(['pokemon/37', '/pokemon/36', '/berry/8', 'https://pokeapi.co/api/v2/ability/9/']); expect(res[0]).to.have.property('name'); expect(res[1]).to.have.property('name'); expect(res[2]).to.have.property('name'); + expect(res[3]).to.have.property('name'); }); }); describe(".resource(Path: string)", function () { it("should have property height", async function () { - const res = await defaultP.resource('/api/v2/pokemon/34'); + const res = await defaultP.resource('pokemon/34'); expect(res).to.have.property('height'); }); }); From 0ace8493a11026f47e3870c780f4a06be54bd99d Mon Sep 17 00:00:00 2001 From: Naramsim Date: Sat, 25 Apr 2026 15:11:29 +0200 Subject: [PATCH 2/5] fix: return a promise --- src/getter.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/getter.js b/src/getter.js index c09a3da..f815c14 100644 --- a/src/getter.js +++ b/src/getter.js @@ -110,26 +110,29 @@ function sizeCache(config) { async function invalidateCache(config) { if (canUseCache(config, db)) { - const meta = await loadResource({...config, cache: false}, 'meta') - const upstream_deploy_date = parseInt(meta.deploy_date) - const transaction = db.transaction("cache", "readwrite"); - const objectStore = transaction.objectStore("cache"); - const index = objectStore.index("deploy_date_index") - const range = IDBKeyRange.upperBound(upstream_deploy_date, true); - const request = index.getAllKeys(range); + const meta = await loadResource({ ...config, cache: false }, 'meta'); + const upstream_deploy_date = parseInt(meta.deploy_date); + + return new Promise((resolve, reject) => { + const transaction = db.transaction("cache", "readwrite"); + const objectStore = transaction.objectStore("cache"); + const index = objectStore.index("deploy_date_index"); + + const range = IDBKeyRange.upperBound(upstream_deploy_date, true); + const request = index.getAllKeys(range); - request.onsuccess = () => { - const keys = request.result; - keys.forEach(pk => { - objectStore.delete(pk); - log(`invalidated ${pk}`); - }); - return true - }; - request.onerror = () => {throw new Error(request.error); - }; + request.onsuccess = () => { + const keys = request.result; + keys.forEach(pk => { + objectStore.delete(pk); + log(`invalidated ${pk}`); + }); + resolve(true); + }; + request.onerror = () => reject(new Error(request.error)); + }); } else { - throw new Error('cache not available') + throw new Error('cache not available'); } } From 09bb88f6660c95b43b42296b1e42aa47853df8d2 Mon Sep 17 00:00:00 2001 From: Naramsim Date: Sat, 9 May 2026 15:42:56 +0200 Subject: [PATCH 3/5] test: test invalidate Cache --- src/index.js | 4 +-- test/test.html.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 59c7ed5..9aee940 100644 --- a/src/index.js +++ b/src/index.js @@ -65,11 +65,11 @@ export class Pokedex { } getCacheLength() { - return sizeDB(this.config) + return sizeCache(this.config) } clearCache() { - return clearDB(this.config) + return clearCache(this.config) } invalidateCache() { diff --git a/test/test.html.js b/test/test.html.js index 2a9dc22..6d96166 100644 --- a/test/test.html.js +++ b/test/test.html.js @@ -373,6 +373,75 @@ describe("pokedex", function () { }); }); +describe("Cache", function () { + this.timeout(10000); + const originalFetch = window.fetch; + let P; + let fetchCalls = []; + + before(async function () { + P = await Pokedex.init({ cache: true }); + window.fetch = async (url) => { + const url_str = url.toString(); + fetchCalls.push(url_str); + + if (url_str.includes('/pokemon/ditto')) { + return new Response(JSON.stringify({ name: 'ditto' }), { + headers: { 'X-PokeAPI-Deploy-Date': '100' } + }); + } + if (url_str.includes('/pokemon/pikachu')) { + return new Response(JSON.stringify({ name: 'pikachu' }), { + headers: { 'X-PokeAPI-Deploy-Date': '300' } + }); + } + if (url_str.includes('/meta')) { + return new Response(JSON.stringify({ deploy_date: '200' })); + } + return originalFetch(url); + }; + }); + + beforeEach(async function () { + await P.clearCache(); + fetchCalls = []; + }); + + after(function () { + window.fetch = originalFetch; + }); + + it("should invalidate old cache entries and keep new ones", async function () { + await P.getPokemonByName('ditto'); + await P.getPokemonByName('pikachu'); + expect(fetchCalls.length).to.equal(2); + + let cacheSize = await P.getCacheLength(); + expect(cacheSize).to.equal(2); + + fetchCalls = []; + await P.getPokemonByName('ditto'); + await P.getPokemonByName('pikachu'); + expect(fetchCalls.length).to.equal(0); + + await P.invalidateCache(); + expect(fetchCalls.length).to.equal(1); + expect(fetchCalls[0]).to.include('/meta'); + + cacheSize = await P.getCacheLength(); + expect(cacheSize).to.equal(1); + + fetchCalls = []; + await P.getPokemonByName('ditto'); + await P.getPokemonByName('pikachu'); + expect(fetchCalls.length).to.equal(1); + expect(fetchCalls[0]).to.include('/pokemon/ditto'); + + cacheSize = await P.getCacheLength(); + expect(cacheSize).to.equal(2); + }); +}); + const button = document.getElementById('flush-cache-btn'); button.addEventListener('click', async () => { From 3176f992e4b5046ac62f5030479a90966c6f8055 Mon Sep 17 00:00:00 2001 From: Naramsim Date: Tue, 12 May 2026 11:31:26 +0200 Subject: [PATCH 4/5] test: increase coverage --- src/getter.js | 11 +++++++---- test/test.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/getter.js b/src/getter.js index f815c14..4aa9fb5 100644 --- a/src/getter.js +++ b/src/getter.js @@ -54,8 +54,11 @@ function getFromDB(objectStore, url) { async function loadResource(config, url) { if (! url.includes('://')) { - url = url.replace(/^\//, ''); - url = `${config.protocol}://${config.hostName}${config.versionPath}${url}` + if (url.startsWith('/api/v2/')) { + url = `${config.protocol}://${config.hostName}${url}` + } else if (!url.includes('://')) { + url = `${config.protocol}://${config.hostName}${config.versionPath}${url}` + } } if (canUseCache(config, db)) { const transaction = db.transaction("cache", "readonly"); @@ -104,7 +107,7 @@ function sizeCache(config) { request.onerror = () => reject(request.error); }); } else { - return Promise.reject() + return Promise.reject(new Error('Cache not available')) } } @@ -146,7 +149,7 @@ function clearCache(config) { request.onerror = () => reject(request.error); }); } else { - return Promise.reject() + return Promise.reject(new Error('Cache not available')) } } diff --git a/test/test.js b/test/test.js index db51b9e..f9dffdd 100644 --- a/test/test.js +++ b/test/test.js @@ -7,6 +7,7 @@ describe("pokedex", { timeout: 30000 }, function () { let p2; const id = 2; + const string = 'pokemon/33'; const path = '/api/v2/pokemon/34'; const url = 'https://pokeapi.co/api/v2/pokemon/35'; const interval = { limit: 10, offset: 34 }; @@ -31,6 +32,36 @@ describe("pokedex", { timeout: 30000 }, function () { }); }); + // --- Resource Methods --- + describe(".resource()", function () { + it("should succeed with a single path", async function () { + const res = await p1.resource(path); + assert.ok(res.height, "Response should have height"); + }); + it("should succeed with a single path", async function () { + const res = await p1.resource(string); + assert.ok(res.height, "Response should have height"); + }); + it("should succeed with an array of paths", async function () { + const res = await p1.resource([path, url, string]); + assert.strictEqual(res.length, 3); + assert.ok(res[0].height, 'Should have property height'); + assert.ok(res[1].height, 'Should have property height'); + assert.ok(res[2].height, 'Should have property height'); + }); + it("should succeed with an array of paths with trailing /", async function () { + const res = await p1.resource([`${path}/`, `${url}/`, `${string}/`]); + assert.strictEqual(res.length, 3); + assert.ok(res[0].height, 'Should have property height'); + assert.ok(res[1].height, 'Should have property height'); + assert.ok(res[2].height, 'Should have property height'); + }); + it("should fail with an invalid path", async function () { + const result = await p1.resource(123); + assert.strictEqual(result, "String or Array is required"); + }); + }); + // --- List Methods --- describe(".getPokemonsList()", function () { it("should succeed with default interval", async function () { @@ -49,4 +80,26 @@ describe("pokedex", { timeout: 30000 }, function () { ); }); }); + + // --- IndexedDB --- + describe("IndexedDB", function () { + it(".getCacheLength() should throw an error", async function () { + await assert.rejects( + p1.getCacheLength(), + Error + ); + }); + it(".clearCache() should throw an error", async function () { + await assert.rejects( + p1.clearCache(), + Error + ); + }); + it(".invalidateCache() should throw an error", async function () { + await assert.rejects( + p1.invalidateCache(), + Error + ); + }); + }); }); \ No newline at end of file From 126494db5eda6d6b5a3a125406d2d3f822c4dd17 Mon Sep 17 00:00:00 2001 From: Naramsim Date: Tue, 12 May 2026 11:53:48 +0200 Subject: [PATCH 5/5] chore: bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c268aaa..2ecfbb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pokeapi-js-wrapper", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pokeapi-js-wrapper", - "version": "2.0.1", + "version": "2.0.2", "license": "MPL-2.0", "devDependencies": { "http-server": "^14.1.1" diff --git a/package.json b/package.json index 332c97d..b69dc63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pokeapi-js-wrapper", - "version": "2.0.1", + "version": "2.0.2", "description": "An API wrapper for PokeAPI", "main": "src/index.js", "type": "module",