fix(trash): idempotency guards on delete/restore/permanent (review T9 important #1+#2)
review T9 flagged 2 service-layer defenses: #1: deleteNote/restoreNote/permanentDeleteNote 의 idempotency. 이미 trash 인 노트를 trash 하거나, 이미 active 인 노트를 restore 하거나, 존재하지 않는 노트를 permanentDelete 시 telemetry 가 spurious 하게 emit → restore/trash ratio (T8) 오염. findById 가드로 의미 없는 emit skip. #2: permanentDeleteNote 의 disk cleanup unguarded. store.deleteNoteDirectory 실패 시 (Windows file-lock 등) telemetry 가 영영 emit 안 되고 IPC 호출자가 이미 성공한 작업에 에러 propagate. emptyTrash 와 동일하게 try/catch 로 감싸 best-effort. orphan dir 은 future janitor 가 정리. Tests: 12/12 still pass. typecheck 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,9 @@ export class CaptureService {
|
||||
|
||||
async deleteNote(noteId: string): Promise<void> {
|
||||
// v0.2.3 #4: hard delete → soft delete. media 보존 (restore 시 필요).
|
||||
// 이미 trash 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지.
|
||||
const note = this.repo.findById(noteId);
|
||||
if (!note || note.deletedAt !== null) return;
|
||||
this.repo.trash(noteId, new Date().toISOString());
|
||||
if (this.deps.telemetry) {
|
||||
await this.deps.telemetry.emit({ kind: 'trash', payload: { noteId } }).catch(() => {});
|
||||
@@ -74,6 +77,9 @@ export class CaptureService {
|
||||
}
|
||||
|
||||
async restoreNote(noteId: string): Promise<void> {
|
||||
// 이미 active 인 노트는 telemetry emit skip — restore/trash ratio 오염 방지.
|
||||
const note = this.repo.findById(noteId);
|
||||
if (!note || note.deletedAt === null) return;
|
||||
this.repo.restore(noteId);
|
||||
if (this.deps.telemetry) {
|
||||
await this.deps.telemetry.emit({ kind: 'restore', payload: { noteId } }).catch(() => {});
|
||||
@@ -81,8 +87,14 @@ export class CaptureService {
|
||||
}
|
||||
|
||||
async permanentDeleteNote(noteId: string): Promise<void> {
|
||||
// 존재하지 않는 노트는 emit skip — 메트릭 오염 방지.
|
||||
const note = this.repo.findById(noteId);
|
||||
if (!note) return;
|
||||
this.repo.permanentDelete(noteId);
|
||||
await this.store.deleteNoteDirectory(noteId);
|
||||
// best-effort media cleanup — disk 실패해도 telemetry/IPC 흐름은 그대로 (orphan dir
|
||||
// 은 future janitor 가 정리). emptyTrash 와 동일 패턴.
|
||||
try { await this.store.deleteNoteDirectory(noteId); }
|
||||
catch { /* best-effort */ }
|
||||
if (this.deps.telemetry) {
|
||||
await this.deps.telemetry.emit({ kind: 'permanent_delete', payload: { noteId } }).catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user