Skip to content

Commit e154c57

Browse files
committed
Implement count and clear
1 parent 9037f09 commit e154c57

File tree

5 files changed

+279
-37
lines changed

5 files changed

+279
-37
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface AsyncStorage {
77
getItem: (key: string) => Promise<string | null>;
88
setItem: (key: string, value: string) => Promise<unknown>;
99
removeItem: (key: string) => Promise<void>;
10+
clear: () => Promise<void>;
1011
}
1112
```
1213

@@ -47,6 +48,9 @@ console.log(token); // Outputs: 'value'
4748
// Remove an item
4849
await cache.removeItem('key');
4950

51+
// Clears all items from cache
52+
cache.clear();
53+
5054
// Destroy the cache instance
5155
cache.destroy();
5256
```

packages/idb-cache-app/src/App.tsx

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "./App.css";
22
import { IDBCache } from "@instructure/idb-cache";
33
import { useCallback, useState } from "react";
4-
import { uuid, deterministicHash, generateTextOfSize } from "./utils";
4+
import { deterministicHash, generateTextOfSize } from "./utils";
55
import { Button } from "@instructure/ui-buttons";
66
import { Metric } from "@instructure/ui-metric";
77
import { View } from "@instructure/ui-view";
@@ -13,13 +13,13 @@ import { NumberInput } from "@instructure/ui-number-input";
1313
// Do *not* store cacheKey to localStorage in production.
1414
let cacheKey: string = localStorage.cacheKey;
1515
if (!cacheKey) {
16-
cacheKey = uuid();
16+
cacheKey = crypto.randomUUID();
1717
localStorage.cacheKey = cacheKey;
1818
}
1919

2020
let cacheBuster: string = localStorage.cacheBuster;
2121
if (!cacheBuster) {
22-
cacheBuster = uuid();
22+
cacheBuster = crypto.randomUUID();
2323
localStorage.cacheBuster = cacheBuster;
2424
}
2525

@@ -54,20 +54,28 @@ const App = () => {
5454
const [timeToGenerate, setTimeToGenerate] = useState<number | null>(null);
5555
const [setTime, setSetTime] = useState<number | null>(null);
5656
const [getTime, setGetTime] = useState<number | null>(null);
57+
const [countTime, setCountTime] = useState<number | null>(null);
58+
const [clearTime, setClearTime] = useState<number | null>(null);
5759
const [itemSize, setItemSize] = useState<number>(initialItemSize);
60+
const [itemCount, setItemCount] = useState<number | null>(null);
61+
const [randomKey, generateRandomKey] = useState<string>(() =>
62+
crypto.randomUUID(),
63+
);
5864

5965
const encryptAndStore = useCallback(async () => {
66+
const key = crypto.randomUUID();
67+
generateRandomKey(key);
6068
const start1 = performance.now();
6169
const paragraphs = Array.from({ length: DEFAULT_NUM_ITEMS }, (_, index) =>
62-
generateTextOfSize(itemSize, `${cacheBuster}-${index}`),
70+
generateTextOfSize(itemSize, `${cacheBuster}-${key}-${index}`),
6371
);
6472
const end1 = performance.now();
6573
setTimeToGenerate(end1 - start1);
6674

6775
const start2 = performance.now();
6876

6977
for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) {
70-
await cache.setItem(`item-${i}`, paragraphs[i]);
78+
await cache.setItem(`item-${key}-${i}`, paragraphs[i]);
7179
}
7280

7381
const end2 = performance.now();
@@ -81,13 +89,32 @@ const App = () => {
8189
const start = performance.now();
8290

8391
for (let i = 0; i < DEFAULT_NUM_ITEMS; i++) {
84-
const result = await cache.getItem(`item-${i}`);
92+
const result = await cache.getItem(`item-${randomKey}-${i}`);
8593
results.push(result);
8694
}
8795

8896
const end = performance.now();
8997
setGetTime(end - start);
90-
setHash2(results.length > 0 ? deterministicHash(results.join("")) : null);
98+
setHash2(
99+
results.filter((x) => x).length > 0
100+
? deterministicHash(results.join(""))
101+
: null,
102+
);
103+
}, [randomKey]);
104+
105+
const count = useCallback(async () => {
106+
const start = performance.now();
107+
const count = await cache.count();
108+
const end = performance.now();
109+
setCountTime(end - start);
110+
setItemCount(count);
111+
}, []);
112+
113+
const clear = useCallback(async () => {
114+
const start = performance.now();
115+
await cache.clear();
116+
const end = performance.now();
117+
setClearTime(end - start);
91118
}, []);
92119

93120
return (
@@ -302,6 +329,85 @@ const App = () => {
302329
</View>
303330
</Flex>
304331
</View>
332+
333+
<View
334+
as="span"
335+
display="inline-block"
336+
margin="none"
337+
padding="medium"
338+
background="primary"
339+
shadow="resting"
340+
>
341+
<Flex direction="column">
342+
<Button color="primary" onClick={count}>
343+
count
344+
</Button>
345+
346+
<View padding="medium 0 0 0">
347+
<Flex>
348+
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
349+
<Flex.Item shouldGrow>
350+
<Metric
351+
renderLabel="clear"
352+
renderValue={
353+
countTime !== null ? (
354+
`${Math.round(countTime)} ms`
355+
) : (
356+
<BlankStat />
357+
)
358+
}
359+
/>
360+
</Flex.Item>
361+
<Flex.Item size="33.3%">
362+
<Metric
363+
renderLabel="chunks"
364+
renderValue={
365+
typeof itemCount === "number" ? (
366+
itemCount
367+
) : (
368+
<BlankStat />
369+
)
370+
}
371+
/>
372+
</Flex.Item>
373+
</Flex>
374+
</View>
375+
</Flex>
376+
</View>
377+
378+
<View
379+
as="span"
380+
display="inline-block"
381+
margin="none"
382+
padding="medium"
383+
background="primary"
384+
shadow="resting"
385+
>
386+
<Flex direction="column">
387+
<Button color="primary" onClick={clear}>
388+
clear
389+
</Button>
390+
391+
<View padding="medium 0 0 0">
392+
<Flex>
393+
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
394+
<Flex.Item shouldGrow>
395+
<Metric
396+
renderLabel="clear"
397+
renderValue={
398+
clearTime !== null ? (
399+
`${Math.round(clearTime)} ms`
400+
) : (
401+
<BlankStat />
402+
)
403+
}
404+
/>
405+
</Flex.Item>
406+
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
407+
</Flex>
408+
</View>
409+
</Flex>
410+
</View>
305411
</div>
306412
</fieldset>
307413
</form>

packages/idb-cache-app/src/utils.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,3 @@ export function generateTextOfSize(
4444
textCache[cacheKey] = result;
4545
return result;
4646
}
47-
48-
export function uuid(): string {
49-
return `${[1e7]}-1e3-4e3-8e3-1e11`.replace(/[018]/g, (c) =>
50-
(
51-
Number.parseInt(c, 10) ^
52-
((crypto.getRandomValues(new Uint8Array(1))[0] & 15) >>
53-
(Number.parseInt(c, 10) / 4))
54-
).toString(16)
55-
);
56-
}

packages/idb-cache/src/index.ts

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ interface IDBCacheConfig {
4848
gcTime?: number;
4949
}
5050

51-
interface AsyncStorage {
51+
export interface AsyncStorage {
5252
getItem: (key: string) => Promise<string | null>;
5353
setItem: (key: string, value: string) => Promise<unknown>;
5454
removeItem: (key: string) => Promise<void>;
55+
clear: () => Promise<void>;
5556
}
5657

5758
export class IDBCache implements AsyncStorage {
@@ -546,31 +547,110 @@ export class IDBCache implements AsyncStorage {
546547
}
547548

548549
/**
549-
* Destroys the IDBCache instance by clearing intervals, terminating the worker, and rejecting all pending requests.
550+
* Counts the total number of encrypted chunks stored in the cache.
551+
* @returns The total number of entries (chunks) in the cache.
552+
* @throws {DatabaseError} If there is an issue accessing the database.
550553
*/
551-
public destroy() {
552-
if (this.cleanupIntervalId !== undefined) {
553-
clearInterval(this.cleanupIntervalId);
554-
}
554+
async count(): Promise<number> {
555+
try {
556+
const db = await this.dbReadyPromise;
557+
const transaction = db.transaction(this.storeName, "readonly");
558+
const store = transaction.store;
555559

556-
this.pendingRequests.forEach((pending, requestId) => {
557-
pending.reject(
558-
new IDBCacheError("IDBCache instance is being destroyed.")
559-
);
560-
this.pendingRequests.delete(requestId);
561-
});
560+
const totalCount = await store.count();
561+
562+
await transaction.done;
563+
564+
if (this.debug) {
565+
console.debug(`Total entries in cache: ${totalCount}`);
566+
}
562567

563-
if (this.port) {
564-
this.port.postMessage({ type: "destroy" });
565-
this.port.close();
566-
this.port = null;
568+
return totalCount;
569+
} catch (error) {
570+
console.error("Error in count():", error);
571+
if (error instanceof DatabaseError) {
572+
throw error;
573+
}
574+
throw new DatabaseError("Failed to count items in the cache.");
567575
}
576+
}
568577

569-
if (this.worker) {
570-
this.worker.terminate();
571-
this.worker = null;
578+
/**
579+
* Clears all items from the cache without affecting the worker or pending requests.
580+
* @throws {DatabaseError} If there is an issue accessing the database.
581+
*/
582+
async clear(): Promise<void> {
583+
try {
584+
const db = await this.dbReadyPromise;
585+
const transaction = db.transaction(this.storeName, "readwrite");
586+
const store = transaction.store;
587+
588+
await store.clear();
589+
590+
await transaction.done;
591+
592+
if (this.debug) {
593+
console.debug("All items have been cleared from the cache.");
594+
}
595+
} catch (error) {
596+
console.error("Error in clear:", error);
597+
if (error instanceof DatabaseError) {
598+
throw error;
599+
}
600+
if (error instanceof IDBCacheError) {
601+
throw error;
602+
}
603+
throw new DatabaseError("Failed to clear the cache.");
572604
}
605+
}
606+
607+
/**
608+
* Destroys the IDBCache instance by clearing data (optional), releasing resources, and terminating the worker.
609+
* @param options - Configuration options for destruction.
610+
* @param options.clearData - Whether to clear all cached data before destruction.
611+
* @throws {DatabaseError} If there is an issue accessing the database during data clearing.
612+
*/
613+
public async destroy(options?: { clearData?: boolean }): Promise<void> {
614+
const { clearData = false } = options || {};
615+
616+
try {
617+
if (clearData) {
618+
await this.clear();
619+
}
620+
621+
if (this.cleanupIntervalId !== undefined) {
622+
clearInterval(this.cleanupIntervalId);
623+
}
573624

574-
this.workerReadyPromise = null;
625+
this.pendingRequests.forEach((pending, requestId) => {
626+
pending.reject(
627+
new IDBCacheError("IDBCache instance is being destroyed.")
628+
);
629+
this.pendingRequests.delete(requestId);
630+
});
631+
632+
if (this.port) {
633+
this.port.postMessage({ type: "destroy" });
634+
this.port.close();
635+
this.port = null;
636+
}
637+
638+
if (this.worker) {
639+
this.worker.terminate();
640+
this.worker = null;
641+
}
642+
643+
this.workerReadyPromise = null;
644+
645+
if (this.debug) {
646+
console.debug("IDBCache instance has been destroyed.");
647+
}
648+
} catch (error) {
649+
console.error("Error in destroy:", error);
650+
if (error instanceof IDBCacheError) {
651+
throw error;
652+
}
653+
throw new IDBCacheError("Failed to destroy the cache instance.");
654+
}
575655
}
576656
}

0 commit comments

Comments
 (0)