From 357cd050297c1d5f72299e45b5be4da77af573ee Mon Sep 17 00:00:00 2001 From: Naramsim Date: Thu, 23 Apr 2026 15:26:02 +0200 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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/8] 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", From 3e1b4d38c03f1cde517db608dd7684cc41177ff2 Mon Sep 17 00:00:00 2001 From: Naramsim Date: Thu, 14 May 2026 15:25:30 +0200 Subject: [PATCH 6/8] fix: allow installing SW/allow sw from other locations than root --- README.md | 2 +- src/config.js | 4 ++++ src/index.js | 2 +- src/installSW.js | 4 ++-- {src => test}/pokeapi-js-wrapper-sw.js | 0 test/test.html.js | 5 ++++- 6 files changed, 12 insertions(+), 5 deletions(-) rename {src => test}/pokeapi-js-wrapper-sw.js (100%) diff --git a/README.md b/README.md index 6816b7c..f99c388 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Pokeapi.co serves its Pokemon images through [Github](https://github.com/PokeAPI `pokeapi-js-wrapper` enables browsers to cache all these images by: 1. enabling the config parameter `cacheImages` - 2. serving [our service worker](https://raw.githubusercontent.com/PokeAPI/pokeapi-js-wrapper/master/src/pokeapi-js-wrapper-sw.js) from the root of your project + 2. serving [our service worker](https://raw.githubusercontent.com/PokeAPI/pokeapi-js-wrapper/master/test/pokeapi-js-wrapper-sw.js) from the root of your project In this way when `pokeapi-js-wrapper`'s `Pokedex` is initialized it will install and start the Service Worker you are serving at the root of your server. The Service Worker will intercept all the calls your HTML/CSS/JS are making to get PokeAPI's images and will cache them. diff --git a/src/config.js b/src/config.js index 01622b8..36008f7 100644 --- a/src/config.js +++ b/src/config.js @@ -8,6 +8,7 @@ class Config { this.timeout = 10 * 1000 // 2 seconds this.cache = true this.cacheImages = false + this.swLocation = '/' if (config.hasOwnProperty('protocol')) { this.protocol = config.protocol @@ -33,6 +34,9 @@ class Config { if (config.hasOwnProperty('cacheImages')) { this.cacheImages = config.cacheImages } + if (config.hasOwnProperty('swLocation')) { + this.swLocation = config.swLocation + } } } export { Config } diff --git a/src/index.js b/src/index.js index 9aee940..0ec9252 100644 --- a/src/index.js +++ b/src/index.js @@ -50,7 +50,7 @@ export class Pokedex { }) if (this.config.cacheImages) { - import('./installSW.js').then(module=>module.installSW()) + import('./installSW.js').then(module=>module.installSW(this.config.swLocation)) } } diff --git a/src/installSW.js b/src/installSW.js index 15146df..dd868d9 100644 --- a/src/installSW.js +++ b/src/installSW.js @@ -1,9 +1,9 @@ import { log } from './utils.js' -export function installSW() { +export function installSW(swLocation) { if (navigator && window && 'serviceWorker' in navigator) { window.addEventListener('load', function() { - navigator.serviceWorker.register('./pokeapi-js-wrapper-sw.js', { scope: './' }) + navigator.serviceWorker.register(`${swLocation}pokeapi-js-wrapper-sw.js`, { scope: './' }) .catch(error => { log('SW installation failed with the following error:') log(error) diff --git a/src/pokeapi-js-wrapper-sw.js b/test/pokeapi-js-wrapper-sw.js similarity index 100% rename from src/pokeapi-js-wrapper-sw.js rename to test/pokeapi-js-wrapper-sw.js diff --git a/test/test.html.js b/test/test.html.js index 6d96166..f935098 100644 --- a/test/test.html.js +++ b/test/test.html.js @@ -8,7 +8,10 @@ describe("service worker", function () { this.timeout(10000); before(async function() { - P = await Pokedex.init({ cacheImages: true }); + P = await Pokedex.init({ + cacheImages: true, + swLocation: '/test/' + }); }); it("should be activated on second run", async function () { From 492261dff5d6d3c4fe1d540c587123b1e4b0afb3 Mon Sep 17 00:00:00 2001 From: Naramsim Date: Thu, 14 May 2026 15:30:44 +0200 Subject: [PATCH 7/8] fix: load sw from absolute path --- README.md | 3 ++- test/cdn.html | 2 +- test/example-sw.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f99c388..6ca1561 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,8 @@ const customOptions = { versionPath: "/api/v2/", cache: true, timeout: 5 * 1000, // 5s - cacheImages: true + cacheImages: true, + swLocation: '/' } const pokedex = await Pokedex.init(customOptions); ``` diff --git a/test/cdn.html b/test/cdn.html index ddd2be1..dc9fbf7 100644 --- a/test/cdn.html +++ b/test/cdn.html @@ -6,7 +6,7 @@ // var PJSW_DEBUG=1 From a4f33e2cb039763968962c7a99871e59243ec7ef Mon Sep 17 00:00:00 2001 From: Naramsim Date: Tue, 19 May 2026 09:08:44 +0200 Subject: [PATCH 8/8] docs: add methods for dealing with cache --- .gitignore | 1 - README.md | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 80ae1ad..30d338b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules -*.js.map lcov.info \ No newline at end of file diff --git a/README.md b/README.md index 6ca1561..5f126a6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A PokeAPI wrapper intended for browsers. Comes fully asynchronous, zero dependen - [Example requests](#example-requests) - [Configuration](#configuration) - [Caching images](#caching-images) + - [Caching methods](#caching-methods) - [Tests](#tests) - [Endpoints](#endpoints) - [Root Endpoints list](#root-endpoints-list) @@ -31,7 +32,7 @@ console.log(await pokedex.getPokemonsList()) ```html