Improve puterjs caching (#1739)

* add regular cache checking for popular directories

* Update index.js

* implement `readdir` request deduplication to improve performance

* Update index.js
This commit is contained in:
Nariman Jelveh
2025-10-11 21:03:38 -07:00
committed by GitHub
parent 35507b33b7
commit f083e6b060
3 changed files with 168 additions and 96 deletions
+58 -11
View File
@@ -131,6 +131,7 @@ const puterInit = (function() {
// Initialize the cache using kv.js
this._cache = new kvjs({dbName: 'puter_cache'});
this._opscache = new kvjs();
// "modules" in puter.js are external interfaces for the developer
this.modules_ = [];
@@ -493,10 +494,21 @@ const puterInit = (function() {
console.error('Error accessing localStorage:', error);
}
}
// initialize loop for updating caches for major directories
if(this.env === 'gui'){
// check and update gui fs cache regularly
setInterval(puter.checkAndUpdateGUIFScache, 10000);
}
// reinitialize submodules
this.updateSubmodules();
// rao
this.request_rao_();
// perform whoami and cache results
puter.getUser().then((user) => {
this.whoami = user;
});
};
setAPIOrigin = function(APIOrigin) {
@@ -666,7 +678,12 @@ const puterInit = (function() {
// If we went from online to offline, purge the cache
if ( wasOnline && !isOnline ) {
console.log('Network connection lost - purging cache');
this.purgeCache();
try {
this._cache.flushall();
console.log('Cache purged successfully');
} catch( error ) {
console.error('Error purging cache:', error);
}
}
// Update the previous state
@@ -688,18 +705,48 @@ const puterInit = (function() {
};
/**
* Purges all cached data
* @public
* Checks and updates the GUI FS cache for most-commonly used paths
* @private
*/
purgeCache = function() {
try {
this._cache.flushall();
console.log('Cache purged successfully');
} catch( error ) {
console.error('Error purging cache:', error);
}
};
checkAndUpdateGUIFScache = function(){
// only run in gui environment
if(puter.env !== 'gui') return;
// only run if user is authenticated
if(!puter.whoami) return;
let username = puter.whoami.username;
// common paths
let home_path = `/${username}`;
let desktop_path = `/${username}/Desktop`;
let documents_path = `/${username}/Documents`;
let public_path = `/${username}/Public`;
// Home
if(!puter._cache.get('readdir:' + home_path)){
console.log(`/${username} is not cached, refetching cache`);
// fetch home
puter.fs.readdir(home_path);
}
// Desktop
if(!puter._cache.get('readdir:' + desktop_path)){
console.log(`/${username}/Desktop is not cached, refetching cache`);
// fetch desktop
puter.fs.readdir(desktop_path);
}
// Documents
if(!puter._cache.get('readdir:' + documents_path)){
console.log(`/${username}/Documents is not cached, refetching cache`);
// fetch documents
puter.fs.readdir(documents_path);
}
// Public
if(!puter._cache.get('readdir:' + public_path)){
console.log(`/${username}/Public is not cached, refetching cache`);
// fetch public
puter.fs.readdir(public_path);
}
}
}
// Create a new Puter object and return it
+5 -40
View File
@@ -123,62 +123,27 @@ export class PuterJSFileSystemModule extends AdvancedBase {
// });
this.socket.on('item.renamed', (item) => {
// delete old item from cache
puter._cache.del('item:' + item.old_path);
// if a directory
if(item.is_dir){
// delete readdir
puter._cache.del('readdir:' + item.old_path);
// descendants items
const descendants = puter._cache.keys('item:' + item.old_path + '/*');
for(const descendant of descendants){
console.log('Deleting cache for:', descendant);
puter._cache.del(descendant);
}
// descendants readdirs
const descendants_readdir = puter._cache.keys('readdir:' + item.old_path + '/*');
for(const descendant of descendants_readdir){
console.log('Deleting cache for:', descendant);
puter._cache.del(descendant);
}
}
// parent readdir
puter._cache.del('readdir:' + path.dirname(item.old_path));
puter._cache.flushall();
console.log('Flushed cache for item.renamed');
});
this.socket.on('item.removed', (item) => {
// check original_client_socket_id and if it matches this.socket.id, don't invalidate cache
puter._cache.flushall();
console.log('Flushed cache for item.deleted');
console.log('Flushed cache for item.removed');
});
this.socket.on('item.added', (item) => {
// delete item from cache
puter._cache.del('item:' + item.path);
// delete readdir from cache
puter._cache.del('readdir:' + item.path);
// delete descendant items from cache
const descendant_items = puter._cache.keys('item:' + item.path + '/*');
for(const descendant of descendant_items){
puter._cache.del(descendant);
}
// delete descendant readdirs from cache
const descendant_readdirs = puter._cache.keys('readdir:' + item.path + '/*');
for(const descendant of descendant_readdirs){
puter._cache.del(descendant);
}
// delete parent readdir from cache
puter._cache.del('readdir:' + path.dirname(item.path));
puter._cache.flushall();
console.log('Flushed cache for item.added');
});
this.socket.on('item.updated', (item) => {
// check original_client_socket_id and if it matches this.socket.id, don't invalidate cache
puter._cache.flushall();
console.log('Flushed cache for item.updated');
});
this.socket.on('item.moved', (item) => {
// check original_client_socket_id and if it matches this.socket.id, don't invalidate cache
puter._cache.flushall();
console.log('Flushed cache for item.moved');
});
@@ -1,6 +1,14 @@
import * as utils from '../../../lib/utils.js';
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
// Track in-flight requests to avoid duplicate backend calls
// Each entry stores: { promise, timestamp }
const inflightRequests = new Map();
// Time window (in ms) to group duplicate requests together
// Requests made within this window will share the same backend call
const DEDUPLICATION_WINDOW_MS = 2000; // 2 seconds
const readdir = async function (...args) {
let options;
@@ -42,56 +50,108 @@ const readdir = async function (...args) {
}
}
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
reject('Authentication failed.');
}
}
// create xhr object
const xhr = utils.initXhr('/readdir', this.APIOrigin, undefined, "post", "text/plain;actually=json");
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, async (result) => {
// Calculate the size of the result for cache eligibility check
const resultSize = JSON.stringify(result).length;
// Cache the result if it's not bigger than MAX_CACHE_SIZE
const MAX_CACHE_SIZE = 100 * 1024 * 1024;
if(resultSize <= MAX_CACHE_SIZE){
// UPSERT the cache
puter._cache.set(cacheKey, result);
}
// set each individual item's cache
for(const item of result){
puter._cache.set('item:' + item.path, item);
}
resolve(result);
}, reject);
// Build request payload - support both path and uid parameters
const payload = {
// Generate deduplication key based on all request parameters
const deduplicationKey = JSON.stringify({
path: options.path,
uid: options.uid,
no_thumbs: options.no_thumbs,
no_assocs: options.no_assocs,
auth_token: this.authToken
};
consistency: options.consistency,
});
// Add either uid or path to the payload
if (options.uid) {
payload.uid = options.uid;
} else if (options.path) {
payload.path = getAbsolutePathForApp(options.path);
// Check if there's already an in-flight request for the same parameters
const existingEntry = inflightRequests.get(deduplicationKey);
const now = Date.now();
if (existingEntry) {
const timeSinceRequest = now - existingEntry.timestamp;
// Only reuse the request if it's within the deduplication window
if (timeSinceRequest < DEDUPLICATION_WINDOW_MS) {
// Wait for the existing request and return its result
try {
const result = await existingEntry.promise;
resolve(result);
} catch (error) {
reject(error);
}
return;
} else {
// Request is too old, remove it from the tracker
inflightRequests.delete(deduplicationKey);
}
}
xhr.send(JSON.stringify(payload));
// Create a promise for this request and store it to deduplicate concurrent calls
const requestPromise = new Promise(async (resolveRequest, rejectRequest) => {
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
try{
await puter.ui.authenticateWithPuter();
}catch(e){
// if authentication fails, throw an error
rejectRequest('Authentication failed.');
return;
}
}
// create xhr object
const xhr = utils.initXhr('/readdir', this.APIOrigin, undefined, "post", "text/plain;actually=json");
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, async (result) => {
// Calculate the size of the result for cache eligibility check
const resultSize = JSON.stringify(result).length;
// Cache the result if it's not bigger than MAX_CACHE_SIZE
const MAX_CACHE_SIZE = 100 * 1024 * 1024;
if(resultSize <= MAX_CACHE_SIZE){
// UPSERT the cache
puter._cache.set(cacheKey, result);
}
// set each individual item's cache
for(const item of result){
puter._cache.set('item:' + item.path, item);
}
resolveRequest(result);
}, rejectRequest);
// Build request payload - support both path and uid parameters
const payload = {
no_thumbs: options.no_thumbs,
no_assocs: options.no_assocs,
auth_token: this.authToken
};
// Add either uid or path to the payload
if (options.uid) {
payload.uid = options.uid;
} else if (options.path) {
payload.path = getAbsolutePathForApp(options.path);
}
xhr.send(JSON.stringify(payload));
});
// Store the promise and timestamp in the in-flight tracker
inflightRequests.set(deduplicationKey, {
promise: requestPromise,
timestamp: now,
});
// Wait for the request to complete and clean up
try {
const result = await requestPromise;
inflightRequests.delete(deduplicationKey);
resolve(result);
} catch (error) {
inflightRequests.delete(deduplicationKey);
reject(error);
}
})
}