From 79a6d1cf681e6b58b9d16ea901251a97e510f917 Mon Sep 17 00:00:00 2001 From: nickcastel50 Date: Mon, 17 Jun 2024 13:48:59 -0400 Subject: [PATCH 1/2] * Add OPFS cache clear and OPFS directory snapshot debug utility functions --- package.json | 2 +- src/BaseRoutes.jsx | 6 ++++- src/OPFS/OPFS.worker.js | 57 +++++++++++++++++++++++++++++++++++++++++ src/OPFS/OPFSService.js | 28 ++++++++++++++++++++ src/OPFS/utils.js | 38 +++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9d2b28347..678b50951 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bldrs", - "version": "1.0.1073", + "version": "1.0.1059", "main": "src/index.jsx", "license": "AGPL-3.0", "homepage": "https://github.com/bldrs-ai/Share", diff --git a/src/BaseRoutes.jsx b/src/BaseRoutes.jsx index eca7fea1d..73c4fc248 100644 --- a/src/BaseRoutes.jsx +++ b/src/BaseRoutes.jsx @@ -1,7 +1,7 @@ import React, {useEffect} from 'react' import {Outlet, Route, Routes, useLocation, useNavigate} from 'react-router-dom' import ShareRoutes from './ShareRoutes' -import {checkOPFSAvailability} from './OPFS/utils' +import {checkOPFSAvailability, setUpGlobalDebugFunctions} from './OPFS/utils' import debug from './utils/debug' import {navWith} from './utils/navigate' import useStore from './store/useStore' @@ -47,6 +47,10 @@ export default function BaseRoutes({testElt = null}) { const checkAvailability = async () => { const available = await checkOPFSAvailability() + if (available) { + setUpGlobalDebugFunctions() + } + setIsOpfsAvailable(available) } diff --git a/src/OPFS/OPFS.worker.js b/src/OPFS/OPFS.worker.js index 172c8e97f..5f35a8731 100644 --- a/src/OPFS/OPFS.worker.js +++ b/src/OPFS/OPFS.worker.js @@ -40,12 +40,69 @@ self.addEventListener('message', async (event) => { ['commitHash', 'originalFilePath', 'owner', 'repo', 'branch']) await deleteModelFromOPFS(commitHash, originalFilePath, owner, repo, branch) + } else if (event.data.command === 'clearCache') { + await clearCache() + } else if (event.data.command === 'snapshotCache') { + await snapshotCache() } } catch (error) { self.postMessage({error: error.message}) } }) +/** + * Return directory snapshot of OPFS cache + */ +async function snapshotCache() { + const opfsRoot = await navigator.storage.getDirectory() + + const directoryStructure = await traverseDirectory(opfsRoot) + + // Send the directory structure as a message to the main thread + self.postMessage({completed: true, event: 'snapshot', directoryStructure: directoryStructure}) +} + +/** + * Given a directory handle, traverse the directory + */ +async function traverseDirectory(dirHandle, path = '') { + let entries = '' + for await (const [name, handle] of dirHandle.entries()) { + if (handle.kind === 'directory') { + entries += `${path}/${name}/\n` + entries += await traverseDirectory(handle, `${path}/${name}`) + } else if (handle.kind === 'file') { + entries += `${path}/${name}\n` + } + } + return entries +} + +/** + * Clear OPFS cache + */ +async function clearCache() { + const opfsRoot = await navigator.storage.getDirectory() + await deleteAllEntries(opfsRoot) + + // Send the directory structure as a message to the main thread + self.postMessage({completed: true, event: 'clear'}) +} + +/** + * Delete all entries for a given directory handle + */ +async function deleteAllEntries(dirHandle) { + for await (const [name, handle] of dirHandle.entries()) { + if (handle.kind === 'directory') { + await deleteAllEntries(handle) + await dirHandle.removeEntry(name, {recursive: true}) + } else if (handle.kind === 'file') { + await dirHandle.removeEntry(name) + } + } +} + /** * */ diff --git a/src/OPFS/OPFSService.js b/src/OPFS/OPFSService.js index aac6747c9..147767f93 100644 --- a/src/OPFS/OPFSService.js +++ b/src/OPFS/OPFSService.js @@ -251,6 +251,34 @@ export function opfsReadModel(modelKey) { }) } +/** + * Clears the OPFS cache + */ +export function opfsClearCache() { + if (!workerRef) { + debug().error('Worker not initialized') + return + } + + workerRef.postMessage({ + command: 'clearCache', + }) +} + +/** + * Retrives a directory snapshot of the opfs cache. + */ +export function opfsSnapshotCache() { + if (!workerRef) { + debug().error('Worker not initialized') + return + } + + workerRef.postMessage({ + command: 'snapshotCache', + }) +} + /** * Sets a callback function to handle messages from the worker. * diff --git a/src/OPFS/utils.js b/src/OPFS/utils.js index c05ae2bfa..3d5e607d7 100644 --- a/src/OPFS/utils.js +++ b/src/OPFS/utils.js @@ -6,6 +6,8 @@ import { opfsWriteModelFileHandle, opfsDoesFileExist, opfsDeleteModel, + opfsSnapshotCache, + opfsClearCache, } from '../OPFS/OPFSService.js' import {assertDefined} from '../utils/assert' import debug from '../utils/debug' @@ -186,6 +188,16 @@ function makePromise(callback, originalFilePath, commitHash, owner, repo, branch resolve(false) // Resolve the promise with false } else if (event.data.event === eventStatus) { workerRef.removeEventListener('message', listener) // Remove the event listener + + if (event.data.event === 'clear') { + // eslint-disable-next-line no-console + console.log('OPFS cache cleared.') + } + + if (event.data.directoryStructure) { + // eslint-disable-next-line no-console + console.log(`OPFS Directory Structure:\n${ event.data.directoryStructure}`) + } resolve(true) // Resolve the promise with true } } @@ -220,6 +232,24 @@ export function doesFileExistInOPFS( return makePromise(opfsDoesFileExist, originalFilePath, commitHash, owner, repo, branch, 'exist') } +/** + * Prints a snapshot of the OPFS directory structure + * + * @return {boolean} + */ +export function snapshotOPFS() { + return makePromise(opfsSnapshotCache, null, null, null, null, null, 'snapshot') +} + +/** + * Deletes entirety of OPFS cache + * + * @return {boolean} + */ +export function clearOPFSCache() { + return makePromise(opfsClearCache, null, null, null, null, null, 'clear') +} + /** * Deletes a file from opfs if it exists. * Returns true if file was found and deleted, false otherwise. @@ -317,3 +347,11 @@ export async function checkOPFSAvailability() { return false } } + +/** + * + */ +export function setUpGlobalDebugFunctions() { + window.snapshotOPFS = snapshotOPFS + window.clearOPFSCache = clearOPFSCache +} From 1d046bdb6bc5311d87d83792157544dfa082ddeb Mon Sep 17 00:00:00 2001 From: nickcastel50 Date: Tue, 18 Jun 2024 10:45:39 -0400 Subject: [PATCH 2/2] * add tests for snapshot and clear opfs cache --- package.json | 2 +- src/OPFS/utils.test.js | 44 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 678b50951..17ce9c9b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bldrs", - "version": "1.0.1059", + "version": "1.0.1060", "main": "src/index.jsx", "license": "AGPL-3.0", "homepage": "https://github.com/bldrs-ai/Share", diff --git a/src/OPFS/utils.test.js b/src/OPFS/utils.test.js index 9ede9385d..39d62a049 100644 --- a/src/OPFS/utils.test.js +++ b/src/OPFS/utils.test.js @@ -6,7 +6,9 @@ import { downloadToOPFS, doesFileExistInOPFS, deleteFileFromOPFS, - checkOPFSAvailability} from './utils' + checkOPFSAvailability, + snapshotOPFS, + clearOPFSCache} from './utils' jest.mock('../OPFS/OPFSService.js') @@ -295,4 +297,44 @@ describe('OPFS Test Suite', () => { expect(result).toBe(false) }) }) + + describe('snapshotOPFS', () => { + it('should resolve true if the snapshot was retrieved', async () => { + const mockWorker = { + addEventListener: jest.fn((_, handler) => { + // Simulate successful file deletion + process.nextTick(() => handler({data: {completed: true, event: 'snapshot', directoryStructure: []}})) + }), + removeEventListener: jest.fn(), + } + OPFSService.initializeWorker.mockReturnValue(mockWorker) + + const result = await snapshotOPFS() + + expect(result).toBe(true) + expect(OPFSService.initializeWorker).toHaveBeenCalled() + expect(mockWorker.addEventListener).toHaveBeenCalled() + expect(mockWorker.removeEventListener).toHaveBeenCalledTimes(1) + }) + }) + + describe('clearOPFS', () => { + it('should resolve true if the OPFS cache was cleared', async () => { + const mockWorker = { + addEventListener: jest.fn((_, handler) => { + // Simulate successful file deletion + process.nextTick(() => handler({data: {completed: true, event: 'clear'}})) + }), + removeEventListener: jest.fn(), + } + OPFSService.initializeWorker.mockReturnValue(mockWorker) + + const result = await clearOPFSCache() + + expect(result).toBe(true) + expect(OPFSService.initializeWorker).toHaveBeenCalled() + expect(mockWorker.addEventListener).toHaveBeenCalled() + expect(mockWorker.removeEventListener).toHaveBeenCalledTimes(1) + }) + }) })