{"record":{"id":"5879ac14731d4c76","repo":"Hmbown/CodeWhale","slug":"this-saved-trace-was-deleted-in-another-tab-use-save","errorCode":null,"errorMessage":"This saved trace was deleted in another tab. Use Save separate copy to keep the loaded recording.","messagePattern":"This saved trace was deleted in another tab\\. Use Save separate copy to keep the loaded recording\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/ui/storage.ts","lineNumber":81,"sourceCode":"      catch (e) { this.db = undefined; reject(problem(e as DOMException)); return; }\n      let result: T, failure: Error | undefined;\n      const fail = (error: Error) => { failure ??= error; try { tx.abort(); } catch { reject(failure); } };\n      tx.oncomplete = () => failure ? reject(failure) : resolve(result);\n      tx.onerror = () => { failure ??= problem(tx.error); };\n      tx.onabort = () => reject(failure ?? problem(tx.error));\n      try { action(tx.objectStore('traces'), tx.objectStore('summaries'), value => { result = value; }, fail, tx.objectStore('habitats')); }\n      catch (e) { fail(e as Error); }\n    });\n  }\n  async save(trace: Trace, expected?: SavedReference): Promise<SavedReference> {\n    await this.open(); // Origin error before secure-context-only randomUUID.\n    const key = expected?.key ?? crypto.randomUUID();\n    return this.transaction('readwrite', (traces, metas, done, fail) => {\n      const req = traces.get(key);\n      req.onsuccess = () => {\n        try {\n          const previous: SavedTrace | undefined = req.result;\n          if (expected && !previous) throw missing();\n          if (expected && previous!.savedAt !== expected.savedAt) throw changed();\n          if (!expected && previous) throw new Error('A saved trace identifier collided. Try saving again.');\n          const savedAt = this.nextTime(previous?.savedAt), item: SavedTrace = { key, savedAt, trace };\n          traces.put(item); metas.put(summary(item)); done({ key, savedAt });\n        } catch (e) { fail(e as Error); }\n      };\n    });\n  }\n  private nextTime(previous?: string): string {\n    const last = previous ? Date.parse(previous) : 0;\n    return new Date(Math.max(Date.now(), Number.isFinite(last) ? last + 1 : 0)).toISOString();\n  }\n  async list(): Promise<SavedSummary[]> {\n    return this.transaction('readonly', (_traces, metas, done) => {\n      const req = metas.getAll();\n      req.onsuccess = () => done((req.result as SavedSummary[]).sort((a, b) => b.savedAt.localeCompare(a.savedAt)));\n    });\n  }","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/ui/storage.ts#L63-L99","documentation":"TraceLibrary.save performs optimistic concurrency control on IndexedDB: when saving over an existing trace you must pass `expected` (key + savedAt). If the record with that key is gone at commit time, it was deleted in another tab, and save fails with this message instead of silently recreating it. savedAt acts as the version number; a fresh save (no expected) cannot hit this path.","triggerScenarios":"Calling save(trace, expected) where a traces.get(key) inside the readwrite transaction returns undefined — the record was deleted (another tab, another window) between loading it and saving.","commonSituations":"Two browser tabs open on the same origin, one deleting the trace while the other edits and saves it; a delete in the same app before an async save resolves; stale UI listing a trace that was already removed; storage cleared externally while the page stayed open.","solutions":["Follow the message: re-save without `expected` (Save separate copy) so the trace is stored under a new key","Refresh the trace list and re-open the recording before retrying any update","Reconcile UI state after the conflict — remove the deleted entry from open editors/lists","Coordinate deletes across tabs (storage event or refresh) if multi-tab editing is common"],"exampleFix":"// before: blind update over possibly-deleted record\nawait library.save(editedTrace, loadedRef); // throws if deleted elsewhere\n\n// after: fall back to a fresh save on conflict\ntry { await library.save(editedTrace, loadedRef); }\ncatch (e) { if (String(e.message).includes('deleted in another tab')) await library.save(editedTrace); else throw e; }","handlingStrategy":"try-catch","validationCode":"const stillExists = (await library.list()).some(s => s.key === expected.key);\nif (!stillExists) console.warn('trace was deleted elsewhere; save as a new copy instead of updating');","typeGuard":"const isDeletedElsewhere = (e) => e instanceof Error && e.message.includes('deleted in another tab');","tryCatchPattern":"try {\n  await library.save(trace, expected);\n} catch (e) {\n  if (isDeletedElsewhere(e)) await library.save(trace); // Save separate copy under a new key\n  else throw e;\n}","preventionTips":["Always pass expected with the loaded savedAt when updating an existing trace","Listen for cross-tab deletes (storage events) and drop stale editors","Refresh the saved list before offering update actions","Offer 'Save separate copy' as the primary recovery path in conflict UIs"],"tags":["indexeddb","concurrency","browser"],"backgroundTag":"invalid-state-transition","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}