Skip to content

Commit

Permalink
consolidate cleanup function
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronshaf committed Nov 17, 2024
1 parent b8cc036 commit 08e1eb1
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 65 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ await cache.removeItem('key');
// Count stored chunks
const totalChunks = await cache.count();

// Removes expired items, busted items, and limits chunks
// Runs at interval
cache.cleanup();

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

Expand Down
46 changes: 46 additions & 0 deletions packages/idb-cache-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const App = () => {
const [setTime, setSetItemTime] = useState<number | null>(null);
const [getTime, setGetItemTime] = useState<number | null>(null);
const [countTime, setCountTime] = useState<number | null>(null);
const [cleanupTime, setCleanupTime] = useState<number | null>(null);
const [clearTime, setClearTime] = useState<number | null>(null);

const [itemSize, setItemSize] = useState<number>(getInitialItemSize());
Expand Down Expand Up @@ -196,6 +197,19 @@ const App = () => {
);
}, [contentKey]);

const cleanup = useCallback(async () => {
const cache = cacheRef.current;
if (!cache) {
console.error("Cache is not initialized.");
return;
}

const start = performance.now();
await cache.cleanup();
const end = performance.now();
setCleanupTime(end - start);
}, []);

const count = useCallback(async () => {
const cache = cacheRef.current;
if (!cache) {
Expand Down Expand Up @@ -451,6 +465,38 @@ const App = () => {
</View>
</Test>

{/* cleanup Performance */}
<Test>
<Button
data-testid="cleanup-button"
color="primary"
onClick={cleanup}
disabled={!cacheReady}
>
cleanup
</Button>

<View padding="medium 0 0 0">
<Flex>
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
<Flex.Item shouldGrow>
<Metric
renderLabel="cleanup"
data-testid="cleanup-time"
renderValue={
cleanupTime !== null ? (
`${Math.round(cleanupTime)} ms`
) : (
<BlankStat />
)
}
/>
</Flex.Item>
<Flex.Item size="33.3%">&nbsp;</Flex.Item>
</Flex>
</View>
</Test>

{/* clear Performance */}
<Test>
<Button
Expand Down
2 changes: 2 additions & 0 deletions packages/idb-cache-app/tests/test-3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test("cache buster (1)", async ({ page }) => {
await page.getByTestId("get-item-button").click();
await expect(page.getByTestId("hash2").getByText("1vz68t")).toBeVisible();
await page.getByTestId("reset-cacheBuster").click();
await page.getByTestId("cleanup-button").click();
await page.getByTestId("count-button").click();
await expect(page.getByTestId("count-value").getByText("0")).toBeVisible();
await page.getByTestId("get-item-button").click();
Expand All @@ -22,6 +23,7 @@ test("cache buster (1)", async ({ page }) => {
await page.getByTestId("count-button").click();
await expect(page.getByTestId("count-value").getByText("2")).toBeVisible();
await page.getByTestId("reset-cacheBuster").click();
await page.getByTestId("cleanup-button").click();
await page.getByTestId("get-item-button").click();
await expect(
page.getByTestId("hash2").locator("div").filter({ hasText: "------" })
Expand Down
112 changes: 47 additions & 65 deletions packages/idb-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,19 @@ export class IDBCache implements AsyncStorage {

this.cleanupIntervalId = window.setInterval(async () => {
try {
await this.cleanupCache();
await this.cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}, this.cleanupInterval);

this.initWorker(cacheKey, cacheBuster)
.then(() => {
this.cleanupCache().catch((error) =>
console.error("Initial cleanup failed:", error)
);
this.flushBustedCacheItems().catch((error) =>
console.error("Failed to flush old cache items:", error)
);
setTimeout(() => {
this.cleanup().catch((error) =>
console.error("Initial cleanup failed:", error)
);
}, 10000);
})
.catch((error) => {
console.error("Worker initialization failed:", error);
Expand Down Expand Up @@ -191,34 +190,55 @@ export class IDBCache implements AsyncStorage {
}

/**
* Flushes items from the cache that do not match the current cacheBuster.
* Cleans up the cache by removing expired items, flushing busted cache items, and enforcing the maxTotalChunks limit.
* @throws {DatabaseError} If there is an issue accessing the database.
*/
private async flushBustedCacheItems(): Promise<void> {
public async cleanup(): Promise<void> {
try {
const db = await this.dbReadyPromise;
const transaction = db.transaction(this.storeName, "readwrite");
const store = transaction.store;
const index = store.index("byCacheBuster");
const timestampIndex = store.index("byTimestamp");
const cacheBusterIndex = store.index("byCacheBuster");
const now = Date.now();

// 1. Remove expired items
let cursor = await timestampIndex.openCursor();
while (cursor) {
const { timestamp } = cursor.value;
if (timestamp <= now) {
const age = now - timestamp;
if (this.debug) {
console.debug(
`Deleting expired item with timestamp ${timestamp}. It is ${age}ms older than the expiration.`
);
}
await cursor.delete();
} else {
break; // Since the index is ordered, no need to check further
}
cursor = await cursor.continue();
}

// 2. Flush busted cache items
const currentCacheBuster = this.cacheBuster;

const lowerBoundRange = IDBKeyRange.upperBound(currentCacheBuster, true);
const upperBoundRange = IDBKeyRange.lowerBound(currentCacheBuster, true);

const deleteItemsInRange = async (range: IDBKeyRange) => {
let itemsDeleted = 0;
let cursor = await index.openCursor(range);
while (cursor) {
let rangeCursor = await cacheBusterIndex.openCursor(range);
while (rangeCursor) {
if (this.debug) {
console.debug(
"Deleting item with cacheBuster:",
cursor.value.cacheBuster
rangeCursor.value.cacheBuster
);
}
await cursor.delete();
await rangeCursor.delete();
itemsDeleted++;
cursor = await cursor.continue();
rangeCursor = await rangeCursor.continue();
}
return itemsDeleted;
};
Expand All @@ -228,55 +248,7 @@ export class IDBCache implements AsyncStorage {
deleteItemsInRange(upperBoundRange),
]);

await transaction.done;
if (this.debug) {
const total = itemsDeleted.reduce((acc, curr) => acc + (curr || 0), 0);
if (total > 0) {
console.debug("Flushed old cache items with different cacheBuster.");
}
}
} catch (error) {
console.error("Error during flushBustedCacheItems:", error);
if (error instanceof DatabaseError) {
throw error;
}
throw new DatabaseError("Failed to flush old cache items.");
}
}

/**
* Cleans up the cache by removing expired items and enforcing the maxTotalChunks limit.
* This method consolidates the functionality of cleanupExpiredItems and cleanupExcessChunks.
* @throws {DatabaseError} If there is an issue accessing the database.
*/
private async cleanupCache(): Promise<void> {
try {
const db = await this.dbReadyPromise;
const transaction = db.transaction(this.storeName, "readwrite");
const store = transaction.store;
const timestampIndex = store.index("byTimestamp");
const cacheBusterIndex = store.index("byCacheBuster");
const now = Date.now();

// 1. Remove expired items
let cursor = await timestampIndex.openCursor();
while (cursor) {
const { timestamp } = cursor.value;
if (timestamp <= now) {
const age = now - timestamp;
if (this.debug) {
console.debug(
`Deleting expired item with timestamp ${timestamp}. It is ${age}ms older than the expiration.`
);
}
await cursor.delete();
} else {
break; // Since the index is ordered, no need to check further
}
cursor = await cursor.continue();
}

// 2. Enforce maxTotalChunks limit
// 3. Enforce maxTotalChunks limit
if (this.maxTotalChunks !== undefined) {
const totalChunks = await store.count();
if (totalChunks > this.maxTotalChunks) {
Expand Down Expand Up @@ -309,8 +281,18 @@ export class IDBCache implements AsyncStorage {
}

await transaction.done;

if (this.debug) {
const totalDeleted = itemsDeleted.reduce(
(acc, curr) => acc + (curr || 0),
0
);
if (totalDeleted > 0) {
console.debug("Flushed old cache items with different cacheBuster.");
}
}
} catch (error) {
console.error("Error during cleanupCache:", error);
console.error("Error during cleanup:", error);
if (error instanceof DatabaseError) {
throw error;
}
Expand Down

0 comments on commit 08e1eb1

Please sign in to comment.