refactor: move the OPFS journal onto the IndexedDB kernel, and make the fake

IndexedDB enforce unique indexes

저널은 이제 트랜잭션 안 요청 28곳이 각자 오류를 보고한다. 코드 줄은
1744 -> 1724로 줄었고 전체 줄이 1817 -> 1881로 는 것은 주석이다. 이행 전
이 파일의 주석은 6줄이었다.

사양의 OP-1 전제는 틀렸다. "중복 put의 답이 달라진다"고 봤으나 이행 전후
모두 CONFLICT / retryable:false / recovery:NONE이다. 요청 오류를 아무도
처리하지 않으면 스토어가 바로 그 에러로 abort해서 transaction.error가
request.error와 같기 때문이다. 바뀐 것은 답이 아니라 출처다.

그 과정에서 가짜 IndexedDB가 unique 인덱스를 전혀 강제하지 않는 것을
찾았다. 보강 전에는 중복 begin이 ok:true로 성공했다 — 브라우저가 거부할
상태를 테스트가 조용히 허용하고 있었다. put/add에 검사를 넣었다.
unique:true 인덱스는 레포 전체에서 이 저널의 2개뿐이라 반경이 좁고, 전체
test:unit으로 파급이 없음을 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 19:51:09 +09:00
co-authored by Claude Opus 5
parent 3366a81f0f
commit 217c1dd52a
3 changed files with 1123 additions and 704 deletions
+44
View File
@@ -146,6 +146,48 @@ function compareKeys(first: IDBValidKey, second: IDBValidKey): number {
return String(left) < String(right) ? -1 : 1;
}
/**
* A unique index rejects a second row whose index key already belongs to
* another primary key, which is how a browser reports it: a request-level
* `ConstraintError` that then aborts the transaction.
*
* The fake enforced primary keys only, so the one `unique: true` index in the
* repository — `indexeddb-opfs-journal.ts`'s logical-key index, whose store is
* keyed by `transactionId` instead — could never fail here the way it fails in
* a browser. Every other index in `src/adapters` is non-unique, so this check
* is a no-op for them.
*/
function assertUniqueIndexes(
state: StoreState,
key: string,
value: unknown,
): void {
for (const [name, index] of state.indexes) {
if (!index.unique || typeof index.keyPath !== "string") continue;
const indexKey = readPath(value, index.keyPath);
// An absent index key keeps the row out of the index entirely, so it
// cannot collide with anything.
if (indexKey === undefined) continue;
for (const [otherKey, otherValue] of state.data) {
// Replacing a row never collides with the row it replaces.
if (otherKey === key) continue;
const otherIndexKey = readPath(otherValue, index.keyPath);
if (otherIndexKey === undefined) continue;
if (
compareKeys(
indexKey as IDBValidKey,
otherIndexKey as IDBValidKey,
) === 0
) {
throw new DOMException(
`Unique index ${name} already holds this key.`,
"ConstraintError",
);
}
}
}
}
class FakeUpgradeTransaction {
aborted = false;
@@ -483,6 +525,7 @@ class FakeObjectStore {
this.assertWritable();
return this.request(() => {
const key = primaryKey(this.state, value);
assertUniqueIndexes(this.state, key, value);
this.state.data.set(key, structuredClone(value));
return key as IDBValidKey;
});
@@ -495,6 +538,7 @@ class FakeObjectStore {
if (this.state.data.has(key)) {
throw new DOMException("Key already exists.", "ConstraintError");
}
assertUniqueIndexes(this.state, key, value);
this.state.data.set(key, structuredClone(value));
return key as IDBValidKey;
});