Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
76 changes: 61 additions & 15 deletions src/getter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -43,8 +54,11 @@ function getFromDB(objectStore, url) {

async function loadResource(config, url) {
if (! url.includes('://')) {
url = url.replace(/^\//, '');
url = `${config.protocol}://${config.hostName}/${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");
Expand All @@ -66,10 +80,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)
}
Expand All @@ -79,7 +97,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");
Expand All @@ -89,11 +107,39 @@ function sizeDB(config) {
request.onerror = () => reject(request.error);
});
} else {
return Promise.reject()
return Promise.reject(new Error('Cache not available'))
}
}

async function invalidateCache(config) {
if (canUseCache(config, db)) {
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}`);
});
resolve(true);
};
request.onerror = () => reject(new Error(request.error));
});
} else {
throw new Error('cache not available');
}
}

function clearDB(config) {
function clearCache(config) {
if (canUseCache(config, db)) {
return new Promise((resolve, reject) => {
const transaction = db.transaction("cache", "readwrite");
Expand All @@ -103,8 +149,8 @@ function clearDB(config) {
request.onerror = () => reject(request.error);
});
} else {
return Promise.reject()
return Promise.reject(new Error('Cache not available'))
}
}

export { loadResource, openDB, sizeDB, clearDB }
export { loadResource, openCache, sizeCache, clearCache, invalidateCache }
18 changes: 11 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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]
})
Expand All @@ -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)
}

Expand All @@ -65,11 +65,15 @@ export class Pokedex {
}

getCacheLength() {
return sizeDB(this.config)
return sizeCache(this.config)
}

clearCache() {
return clearDB(this.config)
return clearCache(this.config)
}

invalidateCache() {
return invalidateCache(this.config)
}

resource(path) {
Expand All @@ -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)}`)
})
}

Expand Down
74 changes: 72 additions & 2 deletions test/test.html.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Expand Down Expand Up @@ -372,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 () => {
Expand Down
53 changes: 53 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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 () {
Expand All @@ -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
);
});
});
});