diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js
index db4731e8..d5562961 100644
--- a/src/gui/src/IPC.js
+++ b/src/gui/src/IPC.js
@@ -1418,7 +1418,7 @@ const ipc_listener = async (event, handled) => {
source_path, target_path, el_filedialog_window,
}) => {
// source path must be in appdata directory
- const stat_info = await puter.fs.stat(source_path);
+ const stat_info = await puter.fs.stat({path: source_path, consistency: 'eventual'});
if ( ! stat_info.appdata_app || stat_info.appdata_app !== app_uuid ) {
const source_file_owner = stat_info?.appdata_app ?? 'the user';
if ( stat_info.appdata_app && stat_info.appdata_app !== app_uuid ) {
@@ -1471,7 +1471,7 @@ const ipc_listener = async (event, handled) => {
} else {
await puter.fs.move(source_path, target_path);
}
- node = await puter.fs.stat(target_path);
+ node = await puter.fs.stat({path: target_path, consistency: 'eventual'});
},
parent_uuid: $(el_filedialog_window).attr('data-element_uuid'),
});
diff --git a/src/gui/src/UI/UIDesktop.js b/src/gui/src/UI/UIDesktop.js
index 9d04a6d2..af9ad631 100644
--- a/src/gui/src/UI/UIDesktop.js
+++ b/src/gui/src/UI/UIDesktop.js
@@ -1414,7 +1414,7 @@ async function UIDesktop(options) {
}
}
- const stat = await puter.fs.stat(item_path);
+ const stat = await puter.fs.stat({path: item_path, consistency: 'eventual'});
// TODO: DRY everything here with open_item. Unfortunately we can't
// use open_item here because it's coupled with UI logic;
diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js
index 3fb92c41..52abfba3 100644
--- a/src/gui/src/UI/UIItem.js
+++ b/src/gui/src/UI/UIItem.js
@@ -925,7 +925,7 @@ function UIItem(options){
const element = $selected_items[index];
await window.delete_item(element);
}
- const trash = await puter.fs.stat(window.trash_path);
+ const trash = await puter.fs.stat({path: window.trash_path, consistency: 'eventual'});
// update other clients
if(window.socket){
@@ -1375,7 +1375,7 @@ function UIItem(options){
if((alert_resp) === 'Delete'){
await window.delete_item(el_item);
// check if trash is empty
- const trash = await puter.fs.stat(window.trash_path);
+ const trash = await puter.fs.stat({path: window.trash_path, consistency: 'eventual'});
// update other clients
if(window.socket){
window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
@@ -1531,6 +1531,7 @@ $(document).on('click', '.item-has-website-badge', async function(e){
returnSubdomains: true,
returnPermissions: false,
returnVersions: false,
+ consistency: 'eventual',
success: function (fsentry){
if(fsentry.subdomains)
window.open(fsentry.subdomains[0].address, '_blank');
@@ -1544,6 +1545,7 @@ $(document).on('long-hover', '.item-has-website-badge', function(e){
returnSubdomains: true,
returnPermissions: false,
returnVersions: false,
+ consistency: 'eventual',
success: function (fsentry){
var box = e.target.getBoundingClientRect();
diff --git a/src/gui/src/UI/UITaskbar.js b/src/gui/src/UI/UITaskbar.js
index 6ef8a4ac..c0707b4e 100644
--- a/src/gui/src/UI/UITaskbar.js
+++ b/src/gui/src/UI/UITaskbar.js
@@ -260,7 +260,7 @@ async function UITaskbar(options){
//---------------------------------------------
// add `Trash` to the taskbar
//---------------------------------------------
- const trash = await puter.fs.stat(window.trash_path);
+ const trash = await puter.fs.stat({path: window.trash_path, consistency: 'eventual'});
if(window.socket){
window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
}
diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js
index 52dcce45..e7be4d40 100644
--- a/src/gui/src/UI/UIWindow.js
+++ b/src/gui/src/UI/UIWindow.js
@@ -1115,7 +1115,7 @@ async function UIWindow(options) {
// SIDEBAR sharing
// --------------------------------------------------------
if(options.is_dir && !isMobile.phone){
- puter.fs.readdir('/').then(function(shared_users){
+ puter.fs.readdir({path: '/', consistency: 'eventual'}).then(function(shared_users){
let ht = '';
if(shared_users && shared_users.length - 1 > 0){
ht += `
`;
@@ -3161,7 +3161,7 @@ window.update_window_path = async function(el_window, target_path){
// /stat
if(target_path !== '/'){
try{
- puter.fs.stat(target_path, function(fsentry){
+ puter.fs.stat({path: target_path, consistency: 'eventual'}).then(fsentry => {
$(el_window).removeClass('window-' + $(el_window).attr('data-uid'));
$(el_window).addClass('window-' + fsentry.id);
$(el_window).attr('data-uid', fsentry.id);
diff --git a/src/gui/src/UI/UIWindowItemProperties.js b/src/gui/src/UI/UIWindowItemProperties.js
index c7a907d3..af588ca5 100644
--- a/src/gui/src/UI/UIWindowItemProperties.js
+++ b/src/gui/src/UI/UIWindowItemProperties.js
@@ -109,6 +109,7 @@ async function UIWindowItemProperties(item_name, item_path, item_uid, left, top,
returnPermissions: true,
returnVersions: true,
returnSize: true,
+ consistency: 'eventual',
success: function (fsentry){
// hide versions tab if item is a directory
if(fsentry.is_dir){
diff --git a/src/gui/src/UI/UIWindowRequestPermission.js b/src/gui/src/UI/UIWindowRequestPermission.js
index f34a37ee..f700dc87 100644
--- a/src/gui/src/UI/UIWindowRequestPermission.js
+++ b/src/gui/src/UI/UIWindowRequestPermission.js
@@ -152,7 +152,7 @@ async function get_permission_description(permission) {
let fsentry;
if (resource_type === "fs") {
- fsentry = await puter.fs.stat({ uid: resource_id });
+ fsentry = await puter.fs.stat({ uid: resource_id, consistency: 'eventual' });
}
const permission_mappings = {
diff --git a/src/gui/src/UI/UIWindowShare.js b/src/gui/src/UI/UIWindowShare.js
index cf973ca7..28a40d34 100644
--- a/src/gui/src/UI/UIWindowShare.js
+++ b/src/gui/src/UI/UIWindowShare.js
@@ -156,6 +156,7 @@ async function UIWindowShare(items, recipient){
path: items[i].path,
returnSubdomains: true,
returnPermissions: true,
+ consistency: 'eventual',
}).then((fsentry) => {
let recipients = fsentry.shares?.users;
let perm_list = '';
diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js
index 8c486167..2a06347c 100644
--- a/src/gui/src/helpers.js
+++ b/src/gui/src/helpers.js
@@ -879,7 +879,7 @@ window.available_templates = () => {
const loadTemplates = async () => {
try{
// Directly check the templates directory
- const hasTemplateFiles = await puter.fs.readdir(templatesPath)
+ const hasTemplateFiles = await puter.fs.readdir(templatesPath, {consistency: 'eventual'})
if(hasTemplateFiles.length == 0) {
window.file_templates = []
@@ -1646,7 +1646,7 @@ window.move_items = async function(el_items, dest_path, is_undo = false){
// check if trash is empty
if(untrashed_at_least_one_item){
- const trash = await puter.fs.stat(window.trash_path);
+ const trash = await puter.fs.stat({path: window.trash_path,consistency: 'eventual'});
if(window.socket){
window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
}
@@ -2196,7 +2196,7 @@ async function readDirectoryRecursive(path, baseDir = '') {
let allFiles = [];
// Read the directory
- const entries = await puter.fs.readdir(path);
+ const entries = await puter.fs.readdir(path, {consistency: 'eventual'});
if (entries.length === 0) {
allFiles.push({ path });
@@ -2708,7 +2708,7 @@ window.get_profile_picture = async function(username){
let icon;
// try getting profile pic
try{
- let stat = await puter.fs.stat('/' + username + '/Public/.profile');
+ let stat = await puter.fs.stat({path: '/' + username + '/Public/.profile', consistency: 'eventual'});
if(stat.size > 0 && stat.is_dir === false && stat.size < 1000000){
let profile_json = await puter.fs.read('/' + username + '/Public/.profile');
profile_json = await blob2str(profile_json);
diff --git a/src/gui/src/helpers/item_icon.js b/src/gui/src/helpers/item_icon.js
index ec239021..d64e8684 100644
--- a/src/gui/src/helpers/item_icon.js
+++ b/src/gui/src/helpers/item_icon.js
@@ -74,7 +74,7 @@ const item_icon = async (fsentry)=>{
let trash_img = $(`.item[data-path="${html_encode(window.trash_path)}" i] .item-icon-icon`).attr('src')
// if trash_img is undefined that's probably because trash wasn't added anywhere, do a direct lookup to see if trash is empty or no
if(!trash_img){
- let trashstat = await puter.fs.stat(window.trash_path);
+ let trashstat = await puter.fs.stat({path: window.trash_path, consistency: 'eventual'});
if(trashstat.is_empty !== undefined && trashstat.is_empty === true)
trash_img = window.icons['trash.svg'];
else
diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js
index 396ca017..46bb53d4 100644
--- a/src/gui/src/helpers/launch_app.js
+++ b/src/gui/src/helpers/launch_app.js
@@ -141,7 +141,7 @@ const launch_app = async (options)=>{
// if options.args.path is provided, use it as the path
if(options.args?.path){
// if args.path is provided, enforce the directory
- let fsentry = await puter.fs.stat(options.args.path);
+ let fsentry = await puter.fs.stat({path: options.args.path, consistency: 'eventual'});
if(!fsentry.is_dir){
let parent = path.dirname(options.args.path);
if(parent === options.args.path)
diff --git a/src/gui/src/helpers/refresh_item_container.js b/src/gui/src/helpers/refresh_item_container.js
index adb101e3..302f2040 100644
--- a/src/gui/src/helpers/refresh_item_container.js
+++ b/src/gui/src/helpers/refresh_item_container.js
@@ -69,7 +69,7 @@ const refresh_item_container = function(el_item_container, options){
// --------------------------------------------------------
// Folder's configs and properties
// --------------------------------------------------------
- puter.fs.stat(container_path, function(fsentry){
+ puter.fs.stat({path: container_path, consistency: options.consistency ?? 'eventual'}).then(fsentry => {
if(el_window){
$(el_window).attr('data-uid', fsentry.id);
$(el_window).attr('data-sort_by', fsentry.sort_by ?? 'name');
@@ -206,7 +206,7 @@ const refresh_item_container = function(el_item_container, options){
// if this is desktop, add Trash
if($(el_item_container).hasClass('desktop')){
try{
- const trash = await puter.fs.stat(window.trash_path);
+ const trash = await puter.fs.stat({path: window.trash_path, consistency: options.consistency ?? 'eventual'});
UIItem({
appendTo: el_item_container,
uid: trash.id,
diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js
index 470f38f6..4edf2e1d 100644
--- a/src/gui/src/initgui.js
+++ b/src/gui/src/initgui.js
@@ -600,7 +600,7 @@ window.initgui = async function(options){
// -------------------------------------------------------------------------------------
if(!window.embedded_in_popup){
await window.get_auto_arrange_data()
- puter.fs.stat(window.desktop_path, async function(desktop_fsentry){
+ puter.fs.stat({path: window.desktop_path, consistency: 'eventual'}).then(desktop_fsentry => {
UIDesktop({desktop_fsentry: desktop_fsentry});
})
}
@@ -1059,7 +1059,7 @@ window.initgui = async function(options){
// -------------------------------------------------------------------------------------
if(!window.embedded_in_popup){
await window.get_auto_arrange_data();
- puter.fs.stat(window.desktop_path, function (desktop_fsentry) {
+ puter.fs.stat({path: window.desktop_path, consistency: 'eventual'}).then(desktop_fsentry => {
UIDesktop({ desktop_fsentry: desktop_fsentry });
})
}
diff --git a/src/gui/src/keyboard.js b/src/gui/src/keyboard.js
index 646a692c..902ffe08 100644
--- a/src/gui/src/keyboard.js
+++ b/src/gui/src/keyboard.js
@@ -532,7 +532,7 @@ $(document).bind('keydown', async function(e){
const element = $selected_items[index];
await window.delete_item(element);
}
- const trash = await puter.fs.stat(window.trash_path);
+ const trash = await puter.fs.stat({path: window.trash_path, consistency: 'eventual'});
if(window.socket){
window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
}
diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js
index 8c0bf7e4..47d95d2a 100644
--- a/src/gui/src/services/ExecService.js
+++ b/src/gui/src/services/ExecService.js
@@ -133,7 +133,7 @@ export class ExecService extends Service {
try {
// Get file stats to verify it exists
- const file_stat = await puter.fs.stat(first_file_path);
+ const file_stat = await puter.fs.stat({path: first_file_path, consistency: 'eventual'});
// Create file signature for the target app
const file_signature_result = await puter.fs.sign(target_app_info.uuid, {
diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js
index 76d242f1..64e1cce7 100644
--- a/src/puter-js/src/index.js
+++ b/src/puter-js/src/index.js
@@ -398,6 +398,9 @@ export default globalThis.puter = (function() {
}
this.workers = new WorkersHandler(this.authToken);
+
+ // Initialize network connectivity monitoring and cache purging
+ this.initNetworkMonitoring();
}
/**
@@ -630,6 +633,65 @@ export default globalThis.puter = (function() {
}
return this;
}
+
+ /**
+ * Initializes network connectivity monitoring to purge cache when connection is lost
+ * @private
+ */
+ initNetworkMonitoring = function() {
+ // Only initialize in environments that support navigator.onLine and window events
+ if (typeof globalThis.navigator === 'undefined' ||
+ typeof globalThis.addEventListener !== 'function') {
+ return;
+ }
+
+ // Track previous online state
+ let wasOnline = navigator.onLine;
+
+ // Function to handle network state changes
+ const handleNetworkChange = () => {
+ const isOnline = navigator.onLine;
+
+ // If we went from online to offline, purge the cache
+ if (wasOnline && !isOnline) {
+ console.log('Network connection lost - purging cache');
+ this.purgeCache();
+ }
+
+ // Update the previous state
+ wasOnline = isOnline;
+ };
+
+ // Listen for online/offline events
+ globalThis.addEventListener('online', handleNetworkChange);
+ globalThis.addEventListener('offline', handleNetworkChange);
+
+ // Also listen for visibility change as an additional indicator
+ // (some browsers don't fire offline events reliably)
+ if (typeof document !== 'undefined') {
+ document.addEventListener('visibilitychange', () => {
+ // Small delay to allow network state to update
+ setTimeout(handleNetworkChange, 100);
+ });
+ }
+ }
+
+ /**
+ * Purges all cached data
+ * @public
+ */
+ purgeCache = function() {
+ try {
+ if (this._cache && typeof this._cache.flushall === 'function') {
+ this._cache.flushall();
+ console.log('Cache purged successfully');
+ } else {
+ console.warn('Cache purge failed: cache instance not available');
+ }
+ } catch (error) {
+ console.error('Error purging cache:', error);
+ }
+ }
}
diff --git a/src/puter-js/src/modules/FileSystem/index.js b/src/puter-js/src/modules/FileSystem/index.js
index d9a6ee27..f5a34aae 100644
--- a/src/puter-js/src/modules/FileSystem/index.js
+++ b/src/puter-js/src/modules/FileSystem/index.js
@@ -12,6 +12,7 @@ import write from "./operations/write.js";
import sign from "./operations/sign.js";
import symlink from './operations/symlink.js';
import readdir from './operations/readdir.js';
+import stat from './operations/stat.js';
// Why is this called deleteFSEntry instead of just delete? because delete is
// a reserved keyword in javascript
import deleteFSEntry from "./operations/deleteFSEntry.js";
@@ -36,18 +37,19 @@ export class PuterJSFileSystemModule extends AdvancedBase {
symlink = symlink;
getReadURL = getReadURL;
readdir = readdir;
+ stat = stat;
FSItem = FSItem
static NARI_METHODS = {
- stat: {
- positional: ['path'],
- firstarg_options: true,
- async fn (parameters) {
- const svc_fs = await this.context.services.aget('filesystem');
- return svc_fs.filesystem.stat(parameters);
- }
- },
+ // stat: {
+ // positional: ['path'],
+ // firstarg_options: true,
+ // async fn (parameters) {
+ // const svc_fs = await this.context.services.aget('filesystem');
+ // return svc_fs.filesystem.stat(parameters);
+ // }
+ // },
}
/**
@@ -129,7 +131,6 @@ export class PuterJSFileSystemModule extends AdvancedBase {
// todo: NAIVE PURGE
// purge cache on disconnect since we may have become out of sync
puter._cache.flushall();
- console.log('Purged cache on socket disconnect');
});
this.socket.on('reconnect', (attempt) => {
diff --git a/src/puter-js/src/modules/FileSystem/operations/readdir.js b/src/puter-js/src/modules/FileSystem/operations/readdir.js
index 8fc7d5cb..ffabb555 100644
--- a/src/puter-js/src/modules/FileSystem/operations/readdir.js
+++ b/src/puter-js/src/modules/FileSystem/operations/readdir.js
@@ -65,12 +65,18 @@ const readdir = async function (...args) {
// Cache the result if it's not bigger than MAX_CACHE_SIZE
const MAX_CACHE_SIZE = 20 * 1024 * 1024;
- const EXPIRE_TIME = 30;
+ const EXPIRE_TIME = 60 * 60; // 1 hour
if(resultSize <= MAX_CACHE_SIZE){
// UPSERT the cache
await puter._cache.set(cacheKey, result, { EX: EXPIRE_TIME });
}
+
+ // set each individual item's cache
+ for(const item of result){
+ await puter._cache.set('item:' + item.id, item, { EX: EXPIRE_TIME });
+ await puter._cache.set('item:' + item.path, item, { EX: EXPIRE_TIME });
+ }
resolve(result);
}, reject);
diff --git a/src/puter-js/src/modules/FileSystem/operations/stat.js b/src/puter-js/src/modules/FileSystem/operations/stat.js
index 8922b8f7..42689d43 100644
--- a/src/puter-js/src/modules/FileSystem/operations/stat.js
+++ b/src/puter-js/src/modules/FileSystem/operations/stat.js
@@ -19,6 +19,11 @@ const stat = async function (...args) {
}
return new Promise(async (resolve, reject) => {
+ // consistency levels
+ if(!options.consistency){
+ options.consistency = 'strong';
+ }
+
// If auth token is not provided and we are in the web environment,
// try to authenticate with Puter
if(!puter.authToken && puter.env === 'web'){
@@ -30,11 +35,43 @@ const stat = async function (...args) {
}
}
+ // Generate cache key based on path or uid
+ let cacheKey;
+ if(options.path){
+ cacheKey = 'item:' + options.path;
+ }else if(options.uid){
+ cacheKey = 'item:' + options.uid;
+ }
+
+ if(options.consistency === 'eventual' && !options.returnSubdomains && !options.returnPermissions && !options.returnVersions && !options.returnSize){
+ // Check cache
+ const cachedResult = await puter._cache.get(cacheKey);
+ if(cachedResult){
+ resolve(cachedResult);
+ return;
+ }
+ }
+
// create xhr object
const xhr = utils.initXhr('/stat', this.APIOrigin, undefined, "post", "text/plain;actually=json");
// 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 = 60 * 60; // 1 hour
+
+ if(resultSize <= MAX_CACHE_SIZE){
+ // UPSERT the cache
+ await puter._cache.set('item:' + result.path, result, { EX: EXPIRE_TIME });
+ await puter._cache.set('item:' + result.uid, result, { EX: EXPIRE_TIME });
+ }
+
+ resolve(result);
+ }, reject);
let dataToSend = {};
if (options.uid !== undefined) {