Skip to content

Commit

Permalink
Implement count and clear
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronshaf committed Nov 16, 2024
1 parent 9037f09 commit e154c57
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 37 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface AsyncStorage {
getItem: (key: string) => Promise<string | null>;
setItem: (key: string, value: string) => Promise<unknown>;
removeItem: (key: string) => Promise<void>;
clear: () => Promise<void>;
}
```

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

// Clears all items from cache
cache.clear();

// Destroy the cache instance
cache.destroy();
```
Expand Down
120 changes: 113 additions & 7 deletions packages/idb-cache-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "./App.css";
import { IDBCache } from "@instructure/idb-cache";
import { useCallback, useState } from "react";
import { uuid, deterministicHash, generateTextOfSize } from "./utils";
import { deterministicHash, generateTextOfSize } from "./utils";
import { Button } from "@instructure/ui-buttons";
import { Metric } from "@instructure/ui-metric";
import { View } from "@instructure/ui-view";
Expand All @@ -13,13 +13,13 @@ import { NumberInput } from "@instructure/ui-number-input";
// Do *not* store cacheKey to localStorage in production.
let cacheKey: string = localStorage.cacheKey;
if (!cacheKey) {
cacheKey = uuid();
cacheKey = crypto.randomUUID();
localStorage.cacheKey = cacheKey;
}

let cacheBuster: string = localStorage.cacheBuster;
if (!cacheBuster) {
cacheBuster = uuid();
cacheBuster = crypto.randomUUID();
localStorage.cacheBuster = cacheBuster;
}

Expand Down Expand Up @@ -54,20 +54,28 @@ const App = () => {
const [timeToGenerate, setTimeToGenerate] = useState<number | null>(null);
const [setTime, setSetTime] = useState<number | null>(null);
const [getTime, setGetTime] = useState<number | null>(null);
const [countTime, setCountTime] = useState<number | null>(null);
const [clearTime, setClearTime] = useState<number | null>(null);
const [itemSize, setItemSize] = useState<number>(initialItemSize);
const [itemCount, setItemCount] = useState<number | null>(null);
const [randomKey, generateRandomKey] = useState<string>(() =>
crypto.randomUUID(),
);

const encryptAndStore = useCallback(async () => {
const key = crypto.randomUUID();
generateRandomKey(key);
const start1 = performance.now();
const paragraphs = Array.from({ length: DEFAULT_NUM_ITEMS }, (_, index) =>
generateTextOfSize(itemSize, `${cacheBuster}-${index}`),
generateTextOfSize(itemSize, `${cacheBuster}-${key}-${index}`),
);
const end1 = performance.now();
setTimeToGenerate(end1 - start1);

const start2 = performance.now();

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

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

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

const end = performance.now();
setGetTime(end - start);
setHash2(results.length > 0 ? deterministicHash(results.join("")) : null);
setHash2(
results.filter((x) => x).length > 0
? deterministicHash(results.join(""))
: null,
);
}, [randomKey]);

const count = useCallback(async () => {
const start = performance.now();
const count = await cache.count();
const end = performance.now();
setCountTime(end - start);
setItemCount(count);
}, []);

const clear = useCallback(async () => {
const start = performance.now();
await cache.clear();
const end = performance.now();
setClearTime(end - start);
}, []);

return (
Expand Down Expand Up @@ -302,6 +329,85 @@ const App = () => {
</View>
</Flex>
</View>

<View
as="span"
display="inline-block"
margin="none"
padding="medium"
background="primary"
shadow="resting"
>
<Flex direction="column">
<Button color="primary" onClick={count}>
count
</Button>

<View padding="medium 0 0 0">
<Flex>
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
<Flex.Item shouldGrow>
<Metric
renderLabel="clear"
renderValue={
countTime !== null ? (
`${Math.round(countTime)} ms`
) : (
<BlankStat />
)
}
/>
</Flex.Item>
<Flex.Item size="33.3%">
<Metric
renderLabel="chunks"
renderValue={
typeof itemCount === "number" ? (
itemCount
) : (
<BlankStat />
)
}
/>
</Flex.Item>
</Flex>
</View>
</Flex>
</View>

<View
as="span"
display="inline-block"
margin="none"
padding="medium"
background="primary"
shadow="resting"
>
<Flex direction="column">
<Button color="primary" onClick={clear}>
clear
</Button>

<View padding="medium 0 0 0">
<Flex>
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
<Flex.Item shouldGrow>
<Metric
renderLabel="clear"
renderValue={
clearTime !== null ? (
`${Math.round(clearTime)} ms`
) : (
<BlankStat />
)
}
/>
</Flex.Item>
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
</Flex>
</View>
</Flex>
</View>
</div>
</fieldset>
</form>
Expand Down
10 changes: 0 additions & 10 deletions packages/idb-cache-app/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,3 @@ export function generateTextOfSize(
textCache[cacheKey] = result;
return result;
}

export function uuid(): string {
return `${[1e7]}-1e3-4e3-8e3-1e11`.replace(/[018]/g, (c) =>
(
Number.parseInt(c, 10) ^
((crypto.getRandomValues(new Uint8Array(1))[0] & 15) >>
(Number.parseInt(c, 10) / 4))
).toString(16)
);
}
120 changes: 100 additions & 20 deletions packages/idb-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ interface IDBCacheConfig {
gcTime?: number;
}

interface AsyncStorage {
export interface AsyncStorage {
getItem: (key: string) => Promise<string | null>;
setItem: (key: string, value: string) => Promise<unknown>;
removeItem: (key: string) => Promise<void>;
clear: () => Promise<void>;
}

export class IDBCache implements AsyncStorage {
Expand Down Expand Up @@ -546,31 +547,110 @@ export class IDBCache implements AsyncStorage {
}

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

this.pendingRequests.forEach((pending, requestId) => {
pending.reject(
new IDBCacheError("IDBCache instance is being destroyed.")
);
this.pendingRequests.delete(requestId);
});
const totalCount = await store.count();

await transaction.done;

if (this.debug) {
console.debug(`Total entries in cache: ${totalCount}`);
}

if (this.port) {
this.port.postMessage({ type: "destroy" });
this.port.close();
this.port = null;
return totalCount;
} catch (error) {
console.error("Error in count():", error);
if (error instanceof DatabaseError) {
throw error;
}
throw new DatabaseError("Failed to count items in the cache.");
}
}

if (this.worker) {
this.worker.terminate();
this.worker = null;
/**
* Clears all items from the cache without affecting the worker or pending requests.
* @throws {DatabaseError} If there is an issue accessing the database.
*/
async clear(): Promise<void> {
try {
const db = await this.dbReadyPromise;
const transaction = db.transaction(this.storeName, "readwrite");
const store = transaction.store;

await store.clear();

await transaction.done;

if (this.debug) {
console.debug("All items have been cleared from the cache.");
}
} catch (error) {
console.error("Error in clear:", error);
if (error instanceof DatabaseError) {
throw error;
}
if (error instanceof IDBCacheError) {
throw error;
}
throw new DatabaseError("Failed to clear the cache.");
}
}

/**
* Destroys the IDBCache instance by clearing data (optional), releasing resources, and terminating the worker.
* @param options - Configuration options for destruction.
* @param options.clearData - Whether to clear all cached data before destruction.
* @throws {DatabaseError} If there is an issue accessing the database during data clearing.
*/
public async destroy(options?: { clearData?: boolean }): Promise<void> {
const { clearData = false } = options || {};

try {
if (clearData) {
await this.clear();
}

if (this.cleanupIntervalId !== undefined) {
clearInterval(this.cleanupIntervalId);
}

this.workerReadyPromise = null;
this.pendingRequests.forEach((pending, requestId) => {
pending.reject(
new IDBCacheError("IDBCache instance is being destroyed.")
);
this.pendingRequests.delete(requestId);
});

if (this.port) {
this.port.postMessage({ type: "destroy" });
this.port.close();
this.port = null;
}

if (this.worker) {
this.worker.terminate();
this.worker = null;
}

this.workerReadyPromise = null;

if (this.debug) {
console.debug("IDBCache instance has been destroyed.");
}
} catch (error) {
console.error("Error in destroy:", error);
if (error instanceof IDBCacheError) {
throw error;
}
throw new IDBCacheError("Failed to destroy the cache instance.");
}
}
}
Loading

0 comments on commit e154c57

Please sign in to comment.