mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-27 07:36:01 +00:00
Forget cached entries when sharing changes
Nothing invalidated the SDK entry cache on share or unshare — the socket handlers only fire on item mutations — so is_shared, which now rides in the cached entry, stayed stale for every consistency: 'eventual' read. That includes the GUI's own listing refresh, which defaults to it, so a badge would not have appeared until an unrelated write flushed the cache.
This commit is contained in:
@@ -9,9 +9,11 @@ import readdir from './readdir.js';
|
||||
import readdirSubdomains from './readdirSubdomains.js';
|
||||
import rename from './rename.js';
|
||||
import revokeReadURL from './revokeReadUrl.js';
|
||||
import share from './share.js';
|
||||
import sign from './sign.js';
|
||||
import space from './space.js';
|
||||
import stat from './stat.js';
|
||||
import unshare from './unshare.js';
|
||||
import write from './write.js';
|
||||
|
||||
/**
|
||||
@@ -82,7 +84,8 @@ const makeFS = () => ({
|
||||
// write delegates to upload, which has its own tests.
|
||||
upload: vi.fn(async () => ({ uid: 'written' })),
|
||||
copy, delete: deleteFSEntry, getReadURL, mkdir, move, read, readdir,
|
||||
readdirSubdomains, rename, revokeReadURL, sign, space, stat, write,
|
||||
readdirSubdomains, rename, revokeReadURL, share, sign, space, stat,
|
||||
unshare, write,
|
||||
});
|
||||
|
||||
const makeCache = () => {
|
||||
@@ -411,6 +414,35 @@ describe('stat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('re-reads a cached item after its sharing changes', async () => {
|
||||
FakeXHR.respondWith = () => ({ uid: 'u1', is_dir: false, is_shared: false });
|
||||
await fs.stat('/a/file.txt');
|
||||
|
||||
FakeXHR.respondWith = () => ({ status: 'success', results: [{ status: 'success', uid: 's1', mode: 'read' }] });
|
||||
await fs.share('/a/file.txt', 'friend', 'read');
|
||||
|
||||
// Without invalidation the stale is_shared: false would be served here.
|
||||
FakeXHR.requests = [];
|
||||
FakeXHR.respondWith = () => ({ uid: 'u1', is_dir: false, is_shared: true });
|
||||
const item = await fs.stat({ path: '/a/file.txt', consistency: 'eventual' });
|
||||
expect(FakeXHR.requests).toHaveLength(1);
|
||||
expect(item.is_shared).toBe(true);
|
||||
});
|
||||
|
||||
it('re-reads a cached item after a share is withdrawn', async () => {
|
||||
FakeXHR.respondWith = () => ({ uid: 'u1', is_dir: false, is_shared: true });
|
||||
await fs.stat('/a/file.txt');
|
||||
|
||||
FakeXHR.respondWith = () => ({ revoked: 1 });
|
||||
await fs.unshare('/a/file.txt', 'friend');
|
||||
|
||||
FakeXHR.requests = [];
|
||||
FakeXHR.respondWith = () => ({ uid: 'u1', is_dir: false, is_shared: false });
|
||||
const item = await fs.stat({ path: '/a/file.txt', consistency: 'eventual' });
|
||||
expect(FakeXHR.requests).toHaveLength(1);
|
||||
expect(item.is_shared).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a share-carrying result out of the cache', async () => {
|
||||
FakeXHR.respondWith = () => ({ uid: 'u1', is_dir: false, shares: [] });
|
||||
await fs.stat('/a/cached.txt', { returnShares: true });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
|
||||
import { defineOperation, firstDefined } from './scaffold.js';
|
||||
import { toShare, toShareItems, toShareRecipients } from './shareUtil.js';
|
||||
import { invalidateShareCache, toShare, toShareItems, toShareRecipients } from './shareUtil.js';
|
||||
|
||||
/** @typedef {import('../types.js').ShareOptions} ShareOptions */
|
||||
/** @typedef {import('../types.js').ShareMode} ShareMode */
|
||||
@@ -53,6 +53,7 @@ const share = defineOperation({
|
||||
code: String(first.code ?? 'share_failed'),
|
||||
};
|
||||
}
|
||||
invalidateShareCache(items);
|
||||
return ok.map(toShare);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Shared helpers for the sharing operations.
|
||||
|
||||
import path from 'path-browserify';
|
||||
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
|
||||
|
||||
/** @typedef {import('../types.js').Share} Share */
|
||||
@@ -84,3 +85,22 @@ export const toShare = (row) => ({
|
||||
modified: /** @type {number} */ (row.modified ?? 0),
|
||||
size: /** @type {number | null} */ (row.size ?? null),
|
||||
});
|
||||
|
||||
/**
|
||||
* Forget cached entries for items whose sharing just changed — `is_shared`
|
||||
* rides in the cached entry, and nothing else invalidates it. Items addressed
|
||||
* by uid have no key to drop, so those flush the cache instead.
|
||||
*
|
||||
* @param {Array<{ path?: string, uid?: string }>} items
|
||||
*/
|
||||
export const invalidateShareCache = (items) => {
|
||||
if ( ! puter?._cache ) return;
|
||||
for ( const item of items ) {
|
||||
if ( ! item.path ) {
|
||||
puter._cache.flushall();
|
||||
return;
|
||||
}
|
||||
puter._cache.del(`item:${ item.path}`);
|
||||
puter._cache.del(`readdir:${ path.dirname(item.path)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
|
||||
import { defineOperation, firstDefined } from './scaffold.js';
|
||||
import { toShareItems, toShareRecipients } from './shareUtil.js';
|
||||
import { invalidateShareCache, toShareItems, toShareRecipients } from './shareUtil.js';
|
||||
|
||||
/** @typedef {import('../types.js').UnshareOptions} UnshareOptions */
|
||||
/** @typedef {import('../types.js').ShareRecipient} ShareRecipient */
|
||||
@@ -28,17 +28,20 @@ import { toShareItems, toShareRecipients } from './shareUtil.js';
|
||||
const unshare = defineOperation({
|
||||
positional: ['path', 'recipient'],
|
||||
request (options) {
|
||||
const items = toShareItems(options, (path) => getAbsolutePathForApp(path));
|
||||
return {
|
||||
endpoint: '/share/revoke',
|
||||
body: {
|
||||
recipients: toShareRecipients(
|
||||
firstDefined(options, 'recipient', 'recipients'),
|
||||
),
|
||||
items: toShareItems(options, (path) => getAbsolutePathForApp(path)),
|
||||
items,
|
||||
},
|
||||
transform: (/** @type {{ revoked?: number }} */ response) => {
|
||||
const revoked = Number(response.revoked ?? 0);
|
||||
if ( revoked > 0 ) invalidateShareCache(items);
|
||||
return { revoked };
|
||||
},
|
||||
transform: (/** @type {{ revoked?: number }} */ response) => ({
|
||||
revoked: Number(response.revoked ?? 0),
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user