{"record":{"id":"9a7aae498f430d72","repo":"Hmbown/CodeWhale","slug":"this-saved-trace-changed-in-another-tab-reopen-it-before","errorCode":null,"errorMessage":"This saved trace changed in another tab. Reopen it before updating, or use Save separate copy.","messagePattern":"This saved trace changed in another tab\\. Reopen it before updating, or use Save separate copy\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/ui/storage.ts","lineNumber":82,"sourceCode":"      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  }\n  async getEntry(key: string): Promise<SavedTrace> {","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/ui/storage.ts#L64-L100","documentation":"This is the version-conflict branch of the same optimistic lock: save(trace, expected) compares the stored record's savedAt with expected.savedAt. If another tab modified the trace (advancing savedAt by at least 1 ms) after you loaded it, the save aborts with this message so concurrent edits never silently overwrite each other.","triggerScenarios":"Calling save(trace, expected) where traces.get(key) returns a record whose savedAt differs from expected.savedAt — i.e. the record was updated elsewhere between load and save.","commonSituations":"Two tabs editing the same saved trace simultaneously; one tab auto-saves while the other holds a stale copy; replaying/importing overwrites a trace in another tab; long-lived editor sessions where the stored savedAt moved on.","solutions":["Reopen the trace to load its current savedAt and reapply your edits, then save with the fresh expected reference","Use Save separate copy (save without expected) to keep your edits under a new key without clobbering","Diff your edit against the current stored version before re-saving to avoid losing the other tab's changes","Avoid editing the same trace in multiple tabs, or refresh after external saves"],"exampleFix":"// before: stale expected reference\nawait library.save(myEdits, staleRef); // savedAt mismatch -> throws\n\n// after: reload current version, merge, save with fresh expected\nconst fresh = (await library.list()).find(t => t.key === staleRef.key);\nconst current = await library.getEntry(fresh.key);\nawait library.save(merge(current.trace, myEdits), { key: fresh.key, savedAt: fresh.savedAt });","handlingStrategy":"try-catch","validationCode":"const current = (await library.list()).find(s => s.key === expected.key);\nif (current && current.savedAt !== expected.savedAt) console.warn('trace changed elsewhere; reopen before updating');","typeGuard":"const isChangedElsewhere = (e) => e instanceof Error && e.message.includes('changed in another tab');","tryCatchPattern":"try {\n  await library.save(trace, expected);\n} catch (e) {\n  if (isChangedElsewhere(e)) {\n    const fresh = await library.getEntry(expected.key); // reopen to get new savedAt\n    await library.save(merge(fresh.trace, trace), { key: expected.key, savedAt: fresh.savedAt });\n  } else throw e;\n}","preventionTips":["Always send the savedAt you loaded as `expected` so conflicts are detected","Reopen/reload a trace before updating if it may have been edited elsewhere","Avoid editing the same trace concurrently in multiple tabs","Surface a merge or 'save as copy' choice when a version conflict is detected"],"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"}