Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions src/logging/AutoLogService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import type { AutoLogEntry, AutoLogSession, AutoLogSessionDraft } from "./AutoLo
class FakeAutoLogStore {
sessions: AutoLogSession[] = [];
entries: AutoLogEntry[] = [];
prunedTo: number[] = [];
prunedTo: Array<{ maxBytes: number; protectedSessionId?: string }> = [];
ended: string[] = [];
failNextAppend = false;

async createSession(draft: AutoLogSessionDraft): Promise<AutoLogSession> {
const session: AutoLogSession = {
Expand All @@ -23,11 +24,16 @@ class FakeAutoLogStore {
}

async appendEntries(entries: AutoLogEntry[]): Promise<void> {
if (this.failNextAppend) {
this.failNextAppend = false;
throw new Error("IndexedDB write failed");
}

this.entries.push(...entries);
}

async pruneToMaxBytes(maxBytes: number): Promise<void> {
this.prunedTo.push(maxBytes);
async pruneToMaxBytes(maxBytes: number, protectedSessionId?: string): Promise<void> {
this.prunedTo.push({ maxBytes, protectedSessionId });
}

async endSession(sessionId: string): Promise<void> {
Expand Down Expand Up @@ -83,7 +89,10 @@ describe("AutoLogService", () => {
sequence: 0,
sourceContent: "hello",
});
expect(store.prunedTo).toContain(1000);
expect(store.prunedTo).toContainEqual({
maxBytes: 1000,
protectedSessionId: "session-0",
});

service.dispose();
});
Expand Down Expand Up @@ -123,4 +132,62 @@ describe("AutoLogService", () => {
expect(store.prunedTo).toEqual([]);
service.dispose();
});

it("recovers the flush queue after an IndexedDB write fails", async () => {
const store = new FakeAutoLogStore();
const service = new AutoLogService(store as unknown as AutoLogStore);
service.configureSession({
title: "Test",
mode: "default",
sanitizedUrl: "https://example.test/",
});
usePreferences.getState().setAutologging({ enabled: true, maxBytes: 1000 });

service.recordLine({
type: "serverMessage",
sourceType: "ansi",
sourceContent: "failed",
});
await new Promise((resolve) => setTimeout(resolve, 0));
store.failNextAppend = true;

await expect(service.flush()).rejects.toThrow("IndexedDB write failed");

service.recordLine({
type: "serverMessage",
sourceType: "ansi",
sourceContent: "recovered",
});
await new Promise((resolve) => setTimeout(resolve, 0));
await service.flush();

expect(store.entries.map((entry) => entry.sourceContent)).toEqual(["recovered"]);
service.dispose();
});

it("releases the current session when its final flush fails", async () => {
const store = new FakeAutoLogStore();
const service = new AutoLogService(store as unknown as AutoLogStore);
service.configureSession({
title: "Test",
mode: "default",
sanitizedUrl: "https://example.test/",
});
usePreferences.getState().setAutologging({ enabled: true, maxBytes: 1000 });
await service.startSession();

service.recordLine({
type: "serverMessage",
sourceType: "ansi",
sourceContent: "failed",
});
await new Promise((resolve) => setTimeout(resolve, 0));
store.failNextAppend = true;

await expect(service.endSession()).rejects.toThrow("IndexedDB write failed");
await service.startSession();

expect(store.sessions).toHaveLength(2);
service.dispose();
});
});
23 changes: 13 additions & 10 deletions src/logging/AutoLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class AutoLogService {
console.error("Failed to end autolog session after disabling autologging:", error);
});
} else {
this.store.pruneToMaxBytes(preferences.maxBytes).catch((error) => {
this.store.pruneToMaxBytes(preferences.maxBytes, this.currentSession?.id).catch((error) => {
console.error("Failed to prune autolog sessions:", error);
});
}
Expand Down Expand Up @@ -142,23 +142,26 @@ export class AutoLogService {
this.pendingEntries = [];
const maxBytes = usePreferences.getState().autologging.maxBytes;

this.flushPromise = this.flushPromise
const flushPromise = this.flushPromise
.then(() => this.store.appendEntries(entries))
.then(() => this.store.pruneToMaxBytes(maxBytes));
.then(() => this.store.pruneToMaxBytes(maxBytes, this.currentSession?.id));

return this.flushPromise;
this.flushPromise = flushPromise.catch(() => {});
return flushPromise;
}

async endSession(): Promise<void> {
const session = this.currentSession;
await this.flush();
try {
await this.flush();

if (session) {
await this.store.endSession(session.id);
if (session) {
await this.store.endSession(session.id);
}
} finally {
this.currentSession = null;
this.sequence = 0;
}

this.currentSession = null;
this.sequence = 0;
}

dispose(): void {
Expand Down
19 changes: 19 additions & 0 deletions src/logging/AutoLogStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,23 @@ describe("AutoLogStore", () => {

store.close();
});

it("preserves the active session while pruning and keeps later entries reachable", async () => {
const store = new AutoLogStore();
const activeSession = await store.createSession({ ...draft, title: "Active" }, 100);
const completedSession = await store.createSession({ ...draft, title: "Completed" }, 200);
await store.appendEntries([makeEntry(activeSession.id, 0, "active ".repeat(100))]);
await store.appendEntries([makeEntry(completedSession.id, 0, "completed")]);

await store.pruneToMaxBytes(1, activeSession.id);
await store.appendEntries([makeEntry(activeSession.id, 1, "still active")]);

const sessions = await store.listSessions();
expect(sessions.map((session) => session.title)).toEqual(["Active"]);
expect(sessions[0].lineCount).toBe(2);
expect(await store.getEntries(activeSession.id)).toHaveLength(2);
expect(await store.getEntries(completedSession.id)).toEqual([]);

store.close();
});
});
7 changes: 5 additions & 2 deletions src/logging/AutoLogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ export class AutoLogStore {
return sessions.reduce((total, session) => total + session.byteEstimate, 0);
}

async pruneToMaxBytes(maxBytes: number): Promise<void> {
if (maxBytes <= 0) {
async pruneToMaxBytes(maxBytes: number, protectedSessionId?: string): Promise<void> {
if (maxBytes <= 0 && !protectedSessionId) {
await this.deleteAll();
return;
}
Expand All @@ -223,6 +223,9 @@ export class AutoLogStore {
if (total <= maxBytes) {
return;
}
if (session.id === protectedSessionId) {
continue;
}

await this.deleteSession(session.id);
total -= session.byteEstimate;
Expand Down