mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-06 23:20:08 -05:00
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:
+58
-11
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user