fix: events hardening (#3814)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s

This commit is contained in:
Daniel Salazar
2026-09-06 22:44:07 -07:00
committed by GitHub
parent a441e7f748
commit 927317bc4e
10 changed files with 418 additions and 30 deletions
@@ -986,6 +986,95 @@ describe('FSEntryStore timestamps and updates', () => {
thumbnail: 'data:image/png;base64,BB',
});
});
it('updateEntry returns and caches the primary row when the replica lags', async () => {
const user = await makeUser();
const file = await createFile(user, `${user.home}/Documents/stale.txt`);
// Pre-update row, captured before the patch — stands in for what a
// lagging replica would still be serving.
const staleRows = (await server.clients.db.read(
'SELECT * FROM fsentries WHERE uuid = ?',
[file.uuid],
)) as Array<Record<string, unknown>>;
for (const row of staleRows) row.subdomains_agg = null;
const db = server.clients.db;
const originalTryHardRead = (Object.getPrototypeOf(db) as typeof db)
.tryHardRead;
const tryHardReadSpy = vi
.spyOn(db, 'tryHardRead')
.mockImplementation(async (query: string, params: unknown[] = []) => {
if (
query.includes('WHERE uuid = ? LIMIT 1') &&
params[0] === file.uuid
) {
return staleRows;
}
return originalTryHardRead.call(db, query, params);
});
try {
const newPath = `${user.home}/Documents/renamed.txt`;
const updated = await store.updateEntry(file.uuid, {
name: 'renamed.txt',
path: newPath,
});
expect(updated.name).toBe('renamed.txt');
expect(updated.path).toBe(newPath);
await expect(store.getEntryByUuid(file.uuid)).resolves.toMatchObject({
path: newPath,
});
expect(tryHardReadSpy).not.toHaveBeenCalled();
} finally {
tryHardReadSpy.mockRestore();
}
});
it('updateEntryThumbnailByUuidForUser returns the primary thumbnail when the replica lags', async () => {
const owner = await makeUser();
const file = await createFile(
owner,
`${owner.home}/Documents/stale-thumb.txt`,
);
const staleRows = (await server.clients.db.read(
'SELECT * FROM fsentries WHERE uuid = ?',
[file.uuid],
)) as Array<Record<string, unknown>>;
for (const row of staleRows) row.subdomains_agg = null;
const db = server.clients.db;
const originalTryHardRead = (Object.getPrototypeOf(db) as typeof db)
.tryHardRead;
const tryHardReadSpy = vi
.spyOn(db, 'tryHardRead')
.mockImplementation(async (query: string, params: unknown[] = []) => {
if (
query.includes('AND user_id = ?') &&
params[0] === file.uuid
) {
return staleRows;
}
return originalTryHardRead.call(db, query, params);
});
try {
const updated = await store.updateEntryThumbnailByUuidForUser(
owner.userId,
file.uuid,
'data:image/png;base64,NEW',
);
expect(updated.thumbnail).toBe('data:image/png;base64,NEW');
expect(tryHardReadSpy).not.toHaveBeenCalled();
} finally {
tryHardReadSpy.mockRestore();
}
});
});
describe('FSEntryStore listing and pagination', () => {
+8 -6
View File
@@ -1384,7 +1384,7 @@ export class FSEntryStore extends PuterStore {
}
}
const refreshedRows = (await this.clients.db.tryHardRead(
const refreshedRows = (await this.clients.db.pread(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE uuid = ? AND user_id = ? LIMIT 1`,
[uuid, userId],
)) as unknown as FSEntryRow[];
@@ -1950,7 +1950,7 @@ export class FSEntryStore extends PuterStore {
.join(', ');
// By uuid alone — the rows just written belong to the
// parent's owner, not necessarily the acting user.
const rows = (await this.clients.db.tryHardRead(
const rows = (await this.clients.db.pread(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE uuid IN (${placeholders})`,
insertUuidChunk,
)) as unknown as FSEntryRow[];
@@ -2359,7 +2359,7 @@ export class FSEntryStore extends PuterStore {
],
);
const rows = (await this.clients.db.tryHardRead(
const rows = (await this.clients.db.pread(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE uuid = ? LIMIT 1`,
[uuid],
)) as unknown as FSEntryRow[];
@@ -2413,7 +2413,7 @@ export class FSEntryStore extends PuterStore {
// Re-read the row itself rather than going through `getEntryByUuid`:
// that read is cache-first and would hand back the pre-touch
// timestamps (and then re-cache them for another TTL).
const refreshedRows = (await this.clients.db.tryHardRead(
const refreshedRows = (await this.clients.db.pread(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE uuid = ? LIMIT 1`,
[uuid],
)) as unknown as FSEntryRow[];
@@ -2859,7 +2859,9 @@ export class FSEntryStore extends PuterStore {
[...values, uuid],
);
const refreshedRows = (await this.clients.db.tryHardRead(
// Read back from the primary: a lagging replica still holds the pre-update
// row, so tryHardRead would return it (and cache it).
const refreshedRows = (await this.clients.db.pread(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE uuid = ? LIMIT 1`,
[uuid],
)) as unknown as FSEntryRow[];
@@ -2942,7 +2944,7 @@ export class FSEntryStore extends PuterStore {
// rely on TTL (60s) to refresh — username rename is rare enough
// that a broad subtree invalidation isn't worth the round-trips.
await this.#invalidateEntryCache(root);
const refreshedRows = (await this.clients.db.tryHardRead(
const refreshedRows = (await this.clients.db.pread(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE id = ? LIMIT 1`,
[root.id],
)) as unknown as FSEntryRow[];