content-script DB fix (PR #869) doesn't reach the real extension, and getExistingDB() throws on a never-visited-app profile
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- typescript
Research direction
Start in src/lib/storage/db.ts and read getExistingDB(), then inspect src/lib/storage/db.test.ts and the existing FromExisting/IntoExisting helpers in crud.ts and capture.ts. Run the storage tests and add coverage for a real operation against a freshly deleted database. Review extension/src/offlinecv-core.ts and extension/offlinecv-pin.json for the separate integration work; done requires the fallback, updated documentation, passing tests, and the pinned extension resolving content-script calls to the existing-database variants.
Written by the indexing model from the issue text.
Description
Problem
PR #869 (fix/content-script-db-open-without-version, chore(db): open database without version parameters in content script to prevent upgrade hangs) set out to fix a real bug: the browser extension's content script calls this repo's storage functions, which open IndexedDB via getDB() at this repo's pinned DB_VERSION. If the extension's pinned commit is ahead of a stale, still-open offlinecv.org app tab, that version-mismatch open triggers an upgrade transaction that hangs waiting for the stale tab's connection to close — the content script hangs silently mid-capture.
The PR's fix (getExistingDB() in src/lib/storage/db.ts, plus a …FromExisting/…IntoExisting sibling for every content-script-reachable function) is the right shape of fix, but as merged it has two confirmed defects that mean the hang is not actually closed, and a new failure mode is introduced in its place. Both were found and verified during the /pr-review of PR #869 (review: https://github.com/offlinecv/OfflineCV/pull/869#pullrequestreview-4967949091, head SHA 1ce06ba70d4480f91ff15375640a87f32f42580e).
Defect 1 — the fix never reaches the real, pinned extension
packages/core/src/index.ts renames the barrel's re-exports (captureJob, getRecord, putRecord, deleteRecord, listRecordsUpdatedSince, getSyncCursor, setSyncCursor, listResumeChoices) to point at the new …FromExisting/…IntoExisting variants, under a docblock claiming this "keeps extension2/src/offlinecv-core.ts unchanged" (packages/core/src/index.ts:264), and src/lib/storage/db.ts:41 claims "the browser extension no longer calls this function directly."
Neither claim holds for the one real, pinned extension checkout present in this repo tree: extension/ (nested, gitignored, pinned via extension/offlinecv-pin.json to 116-Ideas/recruidea-extension, pin SHA acc841133f13b9ff26b6b8e8e99dad68e088dd22 as of PR #869's merge base). No extension2 directory exists anywhere in this repo — grep -rn "extension2" packages src docs outside packages/core/src/index.ts's own new comment returns nothing.
The real extension's own barrel, extension/src/offlinecv-core.ts, re-exports captureJob/getRecord/putRecord/deleteRecord/listRecordsUpdatedSince/getSyncCursor/setSyncCursor/listResumeChoices directly from ../../src/lib/storage/*.ts by relative import — never through the @offlinecv/core npm package. grep -rn "@offlinecv/core" extension/src returns zero matches. Concretely:
extension/src/offlinecv-core.ts:43—export { captureJob, type JobCaptureResult } from "../../src/lib/storage/capture.ts";(the plain, unrenamedcaptureJob, notcaptureJobIntoExisting)extension/src/offlinecv-core.ts:107— same pattern forgetRecord/putRecord/deleteRecord/listRecordsUpdatedSincefromcrud.tsextension/src/offlinecv-core.ts:109—export { getSyncCursor, setSyncCursor } from "../../src/lib/storage/sync-cursor.ts";extension/src/offlinecv-core.ts:148—export { listResumeChoices, type ResumeChoice } from "../../src/lib/storage/resumes.ts";extension/src/capture/drain.ts:47—import { captureJob, type JobCaptureResult } from "../offlinecv-core.ts";, thenexport function createDrainer(write: CaptureWriter = captureJob)— the extension's actual capture pipeline, calling the un-fixedcaptureJob
So after PR #869, the extension's real capture pipeline (drain.ts → captureJob → capture.ts's plain captureJob → getJob/saveJob → getRecord/putRecord → getDB()) is unchanged: it still opens at DB_VERSION and can still trigger the exact upgrade-transaction hang PR #869 was filed to close. Nothing in PR #869 touches extension/src/offlinecv-core.ts's imports, and nothing bumps extension/offlinecv-pin.json.
Defect 2 — getExistingDB() throws instead of "reading defensively as empty" on a never-visited-app profile
src/lib/storage/db.ts's new getExistingDB() (added in PR #869) opens indexedDB with no version argument, which is correct for its stated purpose (never trigger an upgrade). Its docblock (src/lib/storage/db.ts:154) also documents the one edge case it knows about — a profile where the app database doesn't exist yet (only the extension has ever been used there) — and claims: "Every caller here already reads a missing store defensively, and that empty-library reading is correct."
That claim is false. When the database doesn't exist yet, openDB(DB_NAME) (no version, no upgrade callback) creates an empty database at version 1 with zero object stores — IndexedDB always fires upgradeneeded going from "no database" to version 1, but since getExistingDB() passes no upgrade callback, nothing runs to create any stores. Every subsequent store operation (db.get, db.put, db.getAll, db.delete) against a store name that isn't in that empty database throws a synchronous NotFoundError — it does not resolve to undefined/[].
Verified directly, first with a minimal repro against raw idb/fake-indexeddb:
NotFoundError: No objectStore named jobs in this database
then end-to-end through the real code path (a temporary test calling captureJobIntoExisting against a freshly-deleted database):
NotFoundError: No objectStore named jobs in this database
❯ new NotFoundError node_modules/fake-indexeddb/build/esm/lib/errors.js:59:5
❯ FDBDatabase.transaction node_modules/fake-indexeddb/build/esm/FDBDatabase.js:146:15
❯ Proxy.method node_modules/idb/build/index.js:228:25
❯ getRecordFromExisting src/lib/storage/crud.ts:211:20
❯ getJobFromExisting src/lib/storage/jobs.ts:78:18
❯ captureJobVia src/lib/storage/capture.ts:140:20
src/lib/storage/db.test.ts's "creates an empty v1 database rather than upgrading, when none exists yet" test only asserts db.version and objectStoreNames — it never calls get/put/getAll/delete against that empty database, so it documents the empty-DB shape without ever exercising this throw. No test in the PR calls a real crud/capture function (captureJobIntoExisting, getRecordFromExisting, putRecordIntoExisting, etc.) against a database that doesn't exist yet.
Failure scenario (both defects combined)
A user installs the browser extension and captures a job posting before ever opening offlinecv.org in that browser profile (or after clearing site data) — a normal first-use path, and the exact case getExistingDB()'s own docblock calls out by name. Today (pre-#869) that capture hangs silently, mid-request, forever. After #869 lands as merged, once the extension repo eventually picks up a version that calls the new …FromExisting/…IntoExisting functions (defect 1 blocks this from happening automatically), the same capture will throw NotFoundError instead of hanging — a different symptom, still a broken capture, with the underlying database left in a broken empty-v1 state that also blocks the app's own later migration path from working normally (needs verification — see Acceptance Criteria).
Root cause
- Defect 1:
@offlinecv/core(packages/core/src/index.ts) is a barrel the extension repo could consume, but is not the one the currently-pinned extension actually consumes. The real extension has its own, independently-maintained mirror barrel (extension/src/offlinecv-core.ts) that imports straight fromsrc/lib/storage/*.tsby relative path, bypassing@offlinecv/core(and therefore this fix) entirely. PR #869 only edited the barrel nobody imports. - Defect 2:
getExistingDB()(src/lib/storage/db.ts:158) never runsupgrade(), so a database created fresh by this function has no object stores; every downstream caller (crud.ts'slooseDB-based helpers,sync-cursor.ts) assumes IndexedDB reads/writes against a missing store degrade gracefully, butidb/native IndexedDB throwNotFoundErrorinstead.
Implementation plan
-
Fix defect 2 first (self-contained, no cross-repo dependency). In
src/lib/storage/db.ts, changegetExistingDB()to detect the store-less/nonexistent-database case and fall back to the full, versionedgetDB()opener in that branch:export async function getExistingDB(): Promise<IDBPDatabase<OfflineCvDB>> { if (existingDbPromise === null) { existingDbPromise = openDB<OfflineCvDB>(DB_NAME).then((db) => { if (db.objectStoreNames.length === 0) { db.close(); return getDB(); // no stale-open-tab hang risk: nothing else can hold a connection to a database that doesn't exist yet } return db; }); } return existingDbPromise; }Confirm this doesn't reintroduce the original hang: the fallback only triggers when the database is empty/nonexistent, which is precisely the case where no other connection (stale app tab or otherwise) can be holding it open, so
getDB()'s versioned open+upgrade can't block on anything.
Update the docblock atsrc/lib/storage/db.ts:154to describe the actual fallback behavior instead of the false "reads defensively as empty" claim.
Add a test tosrc/lib/storage/db.test.tsthat calls a real crud/capture function (e.g.getRecordFromExisting("jobs", "some-id")orcaptureJobIntoExisting({...})) against a freshly-deleted database, asserting it resolves instead of throwing. -
Fix defect 1 (cross-repo coordination — this is the part that actually closes the extension hang). This repo (
offlinecv/OfflineCV) cannot single-handedly fix defect 1: the fix lives in the private116-Ideas/recruidea-extensionrepo, checked out here atextension/.- In
extension/src/offlinecv-core.ts, repoint every re-export this file currently takes fromsrc/lib/storage/*.tsto the…FromExisting/…IntoExistingvariant, matching whatpackages/core/src/index.tsalready does (or import from@offlinecv/coredirectly instead of mirroring individual files, if that refactor is in scope). - Bump
extension/offlinecv-pin.json'sshato a commit ofofflinecv/OfflineCVthat includes both this issue's fix and PR #869, and rebuild. - Confirm via
grep -rn "@offlinecv/core" extension/src(if switching to the package) or a direct read ofextension/src/offlinecv-core.ts(if keeping the mirror pattern) that every content-script-reachable function now resolves to a…FromExisting/…IntoExistingvariant. - This step happens in the extension repo, not here — this offlinecv/OfflineCV issue tracks it because the pin file and its own docblock (
src/lib/storage/db.tsDB_VERSION comment) are the only drift alarm this repo has; close this issue only once the extension repo's PR has landed and the pin is bumped.
- In
-
Restore the actionable pin-bump instruction.
src/lib/storage/db.ts'sDB_VERSIONdocblock (currently starting⚠️ Still worth bumping deliberately even though the browser extension no longer calls this function directly...) should go back to naming the concrete action once defect 1 is actually resolved — point atextension/offlinecv-pin.jsonand state plainly that this repo's CI never compiles the extension, so nothing else will catch a drift.
Acceptance criteria
-
getExistingDB()falls back togetDB()when the opened database has zero object stores, and a test insrc/lib/storage/db.test.tscalls a real crud/capture function (not justgetExistingDB()itself) against a freshly-deleted database and asserts it succeeds instead of throwingNotFoundError. - The fallback is verified not to reintroduce the original upgrade-transaction hang (e.g. a test or written justification that no other connection can be open against a database that doesn't exist yet).
-
src/lib/storage/db.ts'sgetExistingDB()docblock no longer claims callers "already read a missing store defensively" — it describes the actual fallback behavior. -
extension/src/offlinecv-core.ts(in the116-Ideas/recruidea-extensionrepo) repoints its re-exports to the…FromExisting/…IntoExistingvariants (or to@offlinecv/coredirectly), verified by reading the file orgrep -rn "@offlinecv/core" extension/src. -
extension/offlinecv-pin.json'sshais bumped to a commit that includes both this fix and PR #869, on its own commit per the pin file's existing convention. -
src/lib/storage/db.ts'sDB_VERSIONdocblock once again names the concrete pin-bump action and points atextension/offlinecv-pin.json. - Existing suite still green:
npx vitest run src/lib/storage/(277+ tests as of PR #869) andnpm run check:core. - No regression to the app's own (non-extension)
getDB()call sites — they are untouched by this issue's scope.
References
- PR #869: https://github.com/offlinecv/OfflineCV/pull/869 (
fix/content-script-db-open-without-version, head SHA1ce06ba70d4480f91ff15375640a87f32f42580e) - Review with full findings: https://github.com/offlinecv/OfflineCV/pull/869#pullrequestreview-4967949091
src/lib/storage/db.ts—getExistingDB(),DB_VERSIONdocblocksrc/lib/storage/crud.ts—getRecordFromExisting,putRecordIntoExisting,getAllRecordsFromExisting,listRecordsUpdatedSinceFromExisting,deleteRecordFromExistingpackages/core/src/index.ts— the barrel PR #869 edited (not the one the real extension consumes)extension/src/offlinecv-core.ts,extension/src/capture/drain.ts— the real extension's barrel and capture pipeline (private repo116-Ideas/recruidea-extension, nested/gitignored checkout)extension/offlinecv-pin.json— the pin file that needs bumping once the extension-side fix lands
- Dominant language
- TypeScript
- Stars
- 11
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from offlinecv/OfflineCV
-
fix(heuristics): unit number before a Saint-named city drops "St." from the locationPossibly taken @gaal-agent claimed this 9 days ago. Openbug gaal ux:parsing
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Migrate Gaal implement/review/revise to the local gaal watch; keep mechanical workflows on ActionsOpenarchitecture chore github_actions needs-human ready-for-agent
Difficulty 5/5 Over a week Newbie friendliness 8/100
-
Parser: achievement year lost on re-import when '· YYYY' wrapped to its own linePossibly taken @gaal-agent claimed this 4 days ago. Openbug gaal ready-for-agent ux:parsing
Difficulty 3/5 1-2 days Newbie friendliness 25/100
-
Export: auto-bolded metric bullets extract one word per line in stream-order parsers (pypdf/Drive)Possibly taken @gaal-agent claimed this 4 days ago. Openbatch:export-standards bug gaal ready-for-agent ux:edit-export
Difficulty 3/5 1-2 days Newbie friendliness 25/100
-
bug duplicate improvement
Difficulty 3/5 Half a day Newbie friendliness 66/100
All issues in offlinecv/OfflineCV
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
jbacalso24/mimry#459 · 1 comment ·
Maintainers usually reply within 1 day
-
architecture help wanted hive/claimed-by-architect origin/architect stage/030-pr-in-progress tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
kubestellar/console#24422 · 1 comment ·
Maintainers usually reply within 1 day
-
area: backends type: docs
Difficulty 1/5 1-3 hours Newbie friendliness 83/100
schlessera/brain-kit#2383 ·
Maintainers usually reply within 1 day
-
agent duplicate providers tool triaged
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day