Cache is king baby! Let's go 🚀 (#1574)

* Implement the first naive version of `readdir` cache

* Purge the entire cache on every single mutation

Right now we're going to use the very naive, but safe, approach to purge the entire cache whenever there is change in the user's fs. We're going to incrementally improve this; but for now, better safe than sorry!

* Add socket event listeners to flush cache on file system item changes

This update introduces event listeners for 'item.added', 'item.renamed', and 'item.moved' events, triggering a cache flush on each event to ensure data consistency in the file system module.

* increase exp time for the cache

* Update readdir.js

* Update index.js
This commit is contained in:
Nariman Jelveh
2025-09-17 15:36:42 -07:00
committed by GitHub
parent 214fd3010e
commit e37166dae0
15 changed files with 91 additions and 9 deletions

9
package-lock.json generated
View File

@@ -2278,9 +2278,9 @@
"link": true
},
"node_modules/@heyputer/kv.js": {
"version": "0.1.91",
"resolved": "https://registry.npmjs.org/@heyputer/kv.js/-/kv.js-0.1.91.tgz",
"integrity": "sha512-TzgPFVicgaxkz4mIavE8UdfICQ2Oql9BWkFlJAWEQ9cl+EXWmV1f7sQ0NRJxhzLahfVUdgoWsTXqx/ndr/9KBg==",
"version": "0.1.92",
"resolved": "https://registry.npmjs.org/@heyputer/kv.js/-/kv.js-0.1.92.tgz",
"integrity": "sha512-D+trimrG/V6mU5zeQrKyH476WotvvRn0McttxiFxEzWLiMqR6aBmQ5apeKrZAheglHmwf0D3FO5ykmU2lCuLvQ==",
"license": "MIT",
"dependencies": {
"minimatch": "^9.0.0"
@@ -20419,6 +20419,9 @@
"name": "@heyputer/puterjs",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@heyputer/kv.js": "^0.1.92"
},
"devDependencies": {
"concurrently": "^8.2.2",
"webpack-cli": "^5.1.4"

View File

@@ -954,7 +954,7 @@ async function UIDesktop(options) {
{
html: i18n('refresh'),
onClick: function () {
refresh_item_container(el_desktop);
refresh_item_container(el_desktop, { consistency: 'strong' });
}
},
// -------------------------------------------

View File

@@ -2340,7 +2340,10 @@ async function UIWindow(options) {
menu_items.push({
html: i18n('refresh'),
onClick: function(){
refresh_item_container(el_window_body, options);
refresh_item_container(el_window_body, {
...options,
consistency: 'strong',
});
}
})
// -------------------------------------------

View File

@@ -108,7 +108,7 @@ const refresh_item_container = function(el_item_container, options){
$(el_item_container).find('.item').removeItems()
// get items
puter.fs.readdir(container_path).then((fsentries)=>{
puter.fs.readdir({path: container_path, consistency: options.consistency }).then((fsentries)=>{
// Check if the same folder is still loading since el_item_container's
// data-path might have changed by other operations while waiting for the response to this `readdir`.
if($(el_item_container).attr('data-path') !== container_path)

View File

@@ -16,5 +16,8 @@
"devDependencies": {
"concurrently": "^8.2.2",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@heyputer/kv.js": "^0.1.92"
}
}

View File

@@ -28,6 +28,7 @@ import { FilesystemService } from './services/Filesystem.js';
import { FSRelayService } from './services/FSRelay.js';
import { NoPuterYetService } from './services/NoPuterYet.js';
import { XDIncomingService } from './services/XDIncoming.js';
import kvjs from '@heyputer/kv.js';
// TODO: This is for a safe-guard below; we should check if we can
// generalize this behavior rather than hard-coding it.
@@ -115,6 +116,9 @@ export default globalThis.puter = (function() {
constructor(options) {
options = options ?? {};
// Initialize the cache using kv.js
this._cache = new kvjs();
// "modules" in puter.js are external interfaces for the developer
this.modules_ = [];
// "services" in puter.js are used by modules and may interact with each other

View File

@@ -104,6 +104,19 @@ export class PuterJSFileSystemModule extends AdvancedBase {
}
bindSocketEvents() {
this.socket.on('item.added', (item) => {
// todo: NAIVE PURGE
puter._cache.flushall();
});
this.socket.on('item.renamed', (item) => {
// todo: NAIVE PURGE
puter._cache.flushall();
});
this.socket.on('item.moved', (item) => {
// todo: NAIVE PURGE
puter._cache.flushall();
});
this.socket.on('connect', () => {
if(puter.debugMode)
console.log('FileSystem Socket: Connected', this.socket.id);
@@ -112,6 +125,10 @@ export class PuterJSFileSystemModule extends AdvancedBase {
this.socket.on('disconnect', () => {
if(puter.debugMode)
console.log('FileSystem Socket: Disconnected');
// todo: NAIVE PURGE
// purge cache on disconnect since we may have become out of sync
puter._cache.flushall();
});
this.socket.on('reconnect', (attempt) => {

View File

@@ -55,6 +55,9 @@ const copy = function (...args) {
// if user is copying an item to where its source is, change the name so there is no conflict
dedupe_name: (options.dedupe_name || options.dedupeName),
}));
// todo: EXTREMELY NAIVE CACHE PURGE
puter._cache.flushall();
})
}

View File

@@ -53,6 +53,9 @@ const mkdir = function (...args) {
original_client_socket_id: this.socket.id,
create_missing_parents: (options.recursive || options.createMissingParents) ?? false,
}));
// todo: EXTREMELY NAIVE CACHE PURGE
puter._cache.flushall();
})
}

View File

@@ -65,6 +65,10 @@ const move = function (...args) {
new_metadata: (options.new_metadata || options.newMetadata),
original_client_socket_id: options.excludeSocketID,
}));
// todo: EXTREMELY NAIVE CACHE PURGE
puter._cache.flushall();
})
}

View File

@@ -20,11 +20,33 @@ const readdir = async function (...args) {
}
return new Promise(async (resolve, reject) => {
// consistency levels
if(!options.consistency){
options.consistency = 'strong';
}
// Either path or uid is required
if(!options.path && !options.uid){
throw new Error({ code: 'NO_PATH_OR_UID', message: 'Either path or uid must be provided.' });
}
// Generate cache key based on path or uid
let cacheKey;
if(options.path){
cacheKey = 'readdir:' + options.path;
}else if(options.uid){
cacheKey = 'readdir:' + options.uid;
}
if(options.consistency === 'eventual'){
// Check cache
const cachedResult = await puter._cache.get(cacheKey);
if(cachedResult){
resolve(cachedResult);
return;
}
}
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
@@ -40,7 +62,21 @@ const readdir = async function (...args) {
const xhr = utils.initXhr('/readdir', this.APIOrigin, this.authToken);
// set up event handlers for load and error events
utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
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 = 20 * 1024 * 1024;
const EXPIRE_TIME = 30;
if(resultSize <= MAX_CACHE_SIZE){
// UPSERT the cache
await puter._cache.set(cacheKey, result, { EX: EXPIRE_TIME });
}
resolve(result);
}, reject);
// Build request payload - support both path and uid parameters
const payload = {

View File

@@ -50,6 +50,8 @@ const rename = function (...args) {
}
xhr.send(JSON.stringify(dataToSend));
// todo: EXTREMELY NAIVE CACHE PURGE
puter._cache.flushall();
})
}

View File

@@ -429,6 +429,9 @@ const upload = async function(items, dirPath, options = {}){
options.start();
}
// todo: EXTREMELY NAIVE CACHE PURGE
puter._cache.flushall();
// send request
xhr.send(fd);
})

View File

@@ -55,6 +55,9 @@ const write = async function (targetPath, data, options = {}) {
throw new Error({ code: 'field_invalid', message: 'write() data parameter is an invalid type' });
}
// todo: EXTREMELY NAIVE CACHE PURGE
puter._cache.flushall();
// perform upload
return this.upload(data, parent, options);
}

View File

@@ -10,8 +10,6 @@ import webpack from 'webpack';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
console.log('ENV CHECK!!!', process.env.PUTER_ORIGIN, process.env.PUTER_API_ORIGIN);
export default {
entry: './src/index.js',
output: {