Skip to content

Commit 503af40

Browse files
authored
Invalidate cache/increase coverage
1 parent 0348ef1 commit 503af40

6 files changed

Lines changed: 200 additions & 27 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pokeapi-js-wrapper",
3-
"version": "2.0.1",
3+
"version": "2.0.2",
44
"description": "An API wrapper for PokeAPI",
55
"main": "src/index.js",
66
"type": "module",

src/getter.js

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,34 @@ import { log, canUseCache } from './utils.js'
22

33
var db
44

5-
function openDB(config) {
5+
function openCache(config) {
66
if (config.cache && typeof window !== 'undefined') {
7-
const request = window.indexedDB.open("pokeapi-js-wrapper", 3);
7+
const request = window.indexedDB.open("pokeapi-js-wrapper", 8);
88
return new Promise((resolve, reject) => {
99
request.onerror = (event) => {
1010
log('IndexedDB not available')
1111
reject()
1212
}
1313
request.onupgradeneeded = (event) => {
14-
db = event.target.result;
15-
log('db opened and cache created')
16-
db.createObjectStore("cache", { autoIncrement: false });
17-
resolve(db)
14+
const db = event.target.result;
15+
const transaction = event.target.transaction;
16+
let objectStore;
17+
18+
if (!db.objectStoreNames.contains('cache')) {
19+
objectStore = db.createObjectStore("cache", { autoIncrement: false });
20+
log('Object store "cache" created');
21+
} else {
22+
objectStore = transaction.objectStore("cache");
23+
}
24+
25+
if (!objectStore.indexNames.contains("deploy_date_index")) {
26+
objectStore.createIndex("deploy_date_index", "meta.deploy_date", { unique: false });
27+
log('Index "deploy_date_index" created');
28+
}
1829
}
1930
request.onsuccess = (event) => {
20-
log('db opened')
2131
db = event.target.result;
32+
log('db opened')
2233
resolve(db)
2334
}
2435
request.onversionchange = (event) => {
@@ -43,8 +54,11 @@ function getFromDB(objectStore, url) {
4354

4455
async function loadResource(config, url) {
4556
if (! url.includes('://')) {
46-
url = url.replace(/^\//, '');
47-
url = `${config.protocol}://${config.hostName}/${url}`
57+
if (url.startsWith('/api/v2/')) {
58+
url = `${config.protocol}://${config.hostName}${url}`
59+
} else if (!url.includes('://')) {
60+
url = `${config.protocol}://${config.hostName}${config.versionPath}${url}`
61+
}
4862
}
4963
if (canUseCache(config, db)) {
5064
const transaction = db.transaction("cache", "readonly");
@@ -66,10 +80,14 @@ async function loadUrl(config, url) {
6680
const body = await response.json()
6781
if (response.status === 200) {
6882
if (canUseCache(config, db)) {
83+
const deploy_date = parseInt(response.headers.get('X-PokeAPI-Deploy-Date'))
84+
body.meta = { deploy_date }
6985
const transaction = db.transaction("cache", "readwrite");
7086
const objectStore = transaction.objectStore("cache");
7187
const request = objectStore.add(body, url)
72-
request.onsuccess = () => log(`object cached ${url}`);
88+
request.onsuccess = () => {
89+
log(`object cached ${url}`);
90+
}
7391
request.onerror = () => {
7492
log(request.error)
7593
}
@@ -79,7 +97,7 @@ async function loadUrl(config, url) {
7997
return body
8098
}
8199

82-
function sizeDB(config) {
100+
function sizeCache(config) {
83101
if (canUseCache(config, db)) {
84102
return new Promise((resolve, reject) => {
85103
const transaction = db.transaction("cache", "readwrite");
@@ -89,11 +107,39 @@ function sizeDB(config) {
89107
request.onerror = () => reject(request.error);
90108
});
91109
} else {
92-
return Promise.reject()
110+
return Promise.reject(new Error('Cache not available'))
111+
}
112+
}
113+
114+
async function invalidateCache(config) {
115+
if (canUseCache(config, db)) {
116+
const meta = await loadResource({ ...config, cache: false }, 'meta');
117+
const upstream_deploy_date = parseInt(meta.deploy_date);
118+
119+
return new Promise((resolve, reject) => {
120+
const transaction = db.transaction("cache", "readwrite");
121+
const objectStore = transaction.objectStore("cache");
122+
const index = objectStore.index("deploy_date_index");
123+
124+
const range = IDBKeyRange.upperBound(upstream_deploy_date, true);
125+
const request = index.getAllKeys(range);
126+
127+
request.onsuccess = () => {
128+
const keys = request.result;
129+
keys.forEach(pk => {
130+
objectStore.delete(pk);
131+
log(`invalidated ${pk}`);
132+
});
133+
resolve(true);
134+
};
135+
request.onerror = () => reject(new Error(request.error));
136+
});
137+
} else {
138+
throw new Error('cache not available');
93139
}
94140
}
95141

96-
function clearDB(config) {
142+
function clearCache(config) {
97143
if (canUseCache(config, db)) {
98144
return new Promise((resolve, reject) => {
99145
const transaction = db.transaction("cache", "readwrite");
@@ -103,8 +149,8 @@ function clearDB(config) {
103149
request.onerror = () => reject(request.error);
104150
});
105151
} else {
106-
return Promise.reject()
152+
return Promise.reject(new Error('Cache not available'))
107153
}
108154
}
109155

110-
export { loadResource, openDB, sizeDB, clearDB }
156+
export { loadResource, openCache, sizeCache, clearCache, invalidateCache }

src/index.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import endpoints from './endpoints.json' with { type: "json" }
44
import rootEndpoints from './rootEndpoints.json' with { type: "json" }
5-
import { loadResource, openDB, sizeDB, clearDB } from './getter.js'
5+
import { loadResource, openCache, sizeCache, clearCache, invalidateCache } from './getter.js'
66
import { Config } from './config.js'
77

88
export class Pokedex {
@@ -18,7 +18,7 @@ export class Pokedex {
1818

1919
// if the user has submitted a Name or an ID, return the JSON promise
2020
if (typeof input === 'number' || typeof input === 'string') {
21-
return loadResource(this.config, `${this.config.versionPath}${endpoint[2].replace(':id', input)}`)
21+
return loadResource(this.config, `${endpoint[2].replace(':id', input)}`)
2222
}
2323

2424
// if the user has submitted an Array
@@ -44,7 +44,7 @@ export class Pokedex {
4444
limit = config.limit
4545
}
4646
}
47-
return loadResource(this.config, `${this.config.versionPath}${rootEndpoint[1]}?limit=${limit}&offset=${offset}`)
47+
return loadResource(this.config, `${rootEndpoint[1]}?limit=${limit}&offset=${offset}`)
4848
}
4949
this[rootEndpoint[0]] = this[rootEndpointFullName]
5050
})
@@ -56,7 +56,7 @@ export class Pokedex {
5656

5757
static async init(config) {
5858
config = new Config(config)
59-
await openDB(config)
59+
await openCache(config)
6060
return new Pokedex(config)
6161
}
6262

@@ -65,11 +65,15 @@ export class Pokedex {
6565
}
6666

6767
getCacheLength() {
68-
return sizeDB(this.config)
68+
return sizeCache(this.config)
6969
}
7070

7171
clearCache() {
72-
return clearDB(this.config)
72+
return clearCache(this.config)
73+
}
74+
75+
invalidateCache() {
76+
return invalidateCache(this.config)
7377
}
7478

7579
resource(path) {
@@ -85,7 +89,7 @@ export class Pokedex {
8589

8690
function mapResources(config, endpoint, inputs) {
8791
return inputs.map(input => {
88-
return loadResource(config, `${config.versionPath}${endpoint[2].replace(':id', input)}`)
92+
return loadResource(config, `${endpoint[2].replace(':id', input)}`)
8993
})
9094
}
9195

test/test.html.js

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ describe("pokedex", function () {
4242

4343
describe(".resource(Mixed: array) not cached", function () {
4444
it("should have property name", async function () {
45-
const res = await customP.resource(['/api/v2/pokemon/36', 'api/v2/berry/8', 'https://pokeapi.co/api/v2/ability/9/']);
45+
const res = await customP.resource(['pokemon/37', '/pokemon/36', '/berry/8', 'https://pokeapi.co/api/v2/ability/9/']);
4646
expect(res[0]).to.have.property('name');
4747
expect(res[1]).to.have.property('name');
4848
expect(res[2]).to.have.property('name');
49+
expect(res[3]).to.have.property('name');
4950
});
5051
});
5152

5253
describe(".resource(Path: string)", function () {
5354
it("should have property height", async function () {
54-
const res = await defaultP.resource('/api/v2/pokemon/34');
55+
const res = await defaultP.resource('pokemon/34');
5556
expect(res).to.have.property('height');
5657
});
5758
});
@@ -372,6 +373,75 @@ describe("pokedex", function () {
372373
});
373374
});
374375

376+
describe("Cache", function () {
377+
this.timeout(10000);
378+
const originalFetch = window.fetch;
379+
let P;
380+
let fetchCalls = [];
381+
382+
before(async function () {
383+
P = await Pokedex.init({ cache: true });
384+
window.fetch = async (url) => {
385+
const url_str = url.toString();
386+
fetchCalls.push(url_str);
387+
388+
if (url_str.includes('/pokemon/ditto')) {
389+
return new Response(JSON.stringify({ name: 'ditto' }), {
390+
headers: { 'X-PokeAPI-Deploy-Date': '100' }
391+
});
392+
}
393+
if (url_str.includes('/pokemon/pikachu')) {
394+
return new Response(JSON.stringify({ name: 'pikachu' }), {
395+
headers: { 'X-PokeAPI-Deploy-Date': '300' }
396+
});
397+
}
398+
if (url_str.includes('/meta')) {
399+
return new Response(JSON.stringify({ deploy_date: '200' }));
400+
}
401+
return originalFetch(url);
402+
};
403+
});
404+
405+
beforeEach(async function () {
406+
await P.clearCache();
407+
fetchCalls = [];
408+
});
409+
410+
after(function () {
411+
window.fetch = originalFetch;
412+
});
413+
414+
it("should invalidate old cache entries and keep new ones", async function () {
415+
await P.getPokemonByName('ditto');
416+
await P.getPokemonByName('pikachu');
417+
expect(fetchCalls.length).to.equal(2);
418+
419+
let cacheSize = await P.getCacheLength();
420+
expect(cacheSize).to.equal(2);
421+
422+
fetchCalls = [];
423+
await P.getPokemonByName('ditto');
424+
await P.getPokemonByName('pikachu');
425+
expect(fetchCalls.length).to.equal(0);
426+
427+
await P.invalidateCache();
428+
expect(fetchCalls.length).to.equal(1);
429+
expect(fetchCalls[0]).to.include('/meta');
430+
431+
cacheSize = await P.getCacheLength();
432+
expect(cacheSize).to.equal(1);
433+
434+
fetchCalls = [];
435+
await P.getPokemonByName('ditto');
436+
await P.getPokemonByName('pikachu');
437+
expect(fetchCalls.length).to.equal(1);
438+
expect(fetchCalls[0]).to.include('/pokemon/ditto');
439+
440+
cacheSize = await P.getCacheLength();
441+
expect(cacheSize).to.equal(2);
442+
});
443+
});
444+
375445
const button = document.getElementById('flush-cache-btn');
376446

377447
button.addEventListener('click', async () => {

test/test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe("pokedex", { timeout: 30000 }, function () {
77
let p2;
88

99
const id = 2;
10+
const string = 'pokemon/33';
1011
const path = '/api/v2/pokemon/34';
1112
const url = 'https://pokeapi.co/api/v2/pokemon/35';
1213
const interval = { limit: 10, offset: 34 };
@@ -31,6 +32,36 @@ describe("pokedex", { timeout: 30000 }, function () {
3132
});
3233
});
3334

35+
// --- Resource Methods ---
36+
describe(".resource()", function () {
37+
it("should succeed with a single path", async function () {
38+
const res = await p1.resource(path);
39+
assert.ok(res.height, "Response should have height");
40+
});
41+
it("should succeed with a single path", async function () {
42+
const res = await p1.resource(string);
43+
assert.ok(res.height, "Response should have height");
44+
});
45+
it("should succeed with an array of paths", async function () {
46+
const res = await p1.resource([path, url, string]);
47+
assert.strictEqual(res.length, 3);
48+
assert.ok(res[0].height, 'Should have property height');
49+
assert.ok(res[1].height, 'Should have property height');
50+
assert.ok(res[2].height, 'Should have property height');
51+
});
52+
it("should succeed with an array of paths with trailing /", async function () {
53+
const res = await p1.resource([`${path}/`, `${url}/`, `${string}/`]);
54+
assert.strictEqual(res.length, 3);
55+
assert.ok(res[0].height, 'Should have property height');
56+
assert.ok(res[1].height, 'Should have property height');
57+
assert.ok(res[2].height, 'Should have property height');
58+
});
59+
it("should fail with an invalid path", async function () {
60+
const result = await p1.resource(123);
61+
assert.strictEqual(result, "String or Array is required");
62+
});
63+
});
64+
3465
// --- List Methods ---
3566
describe(".getPokemonsList()", function () {
3667
it("should succeed with default interval", async function () {
@@ -49,4 +80,26 @@ describe("pokedex", { timeout: 30000 }, function () {
4980
);
5081
});
5182
});
83+
84+
// --- IndexedDB ---
85+
describe("IndexedDB", function () {
86+
it(".getCacheLength() should throw an error", async function () {
87+
await assert.rejects(
88+
p1.getCacheLength(),
89+
Error
90+
);
91+
});
92+
it(".clearCache() should throw an error", async function () {
93+
await assert.rejects(
94+
p1.clearCache(),
95+
Error
96+
);
97+
});
98+
it(".invalidateCache() should throw an error", async function () {
99+
await assert.rejects(
100+
p1.invalidateCache(),
101+
Error
102+
);
103+
});
104+
});
52105
});

0 commit comments

Comments
 (0)