forked from google/fledge-shim
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate from idb-keyval to native IndexedDB
Up until now, we basically had only one table and it had conceptually only one column, so we didn't have to worry about atomicity guarantees; everything was automatically atomic. As we prepare to store more kinds of data, we need transactional consistency, to avoid a situation where, e.g., one tab does a read-update-write and in doing so overwrites a write made concurrently from another tab with stale data. So idb-keyval is not for us anymore. This change prepares for that migration. Currently, there's no support for database versioning; if the code is deployed as-is and then later the version is upgraded, stuff will break. I'm hoping to fix this later; see google#80.
- Loading branch information
1 parent
0c7afa8
commit 9be164b
Showing
15 changed files
with
273 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/** | ||
* @license | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
/** | ||
* @fileoverview Utilities for performing IndexedDB operations, in order to | ||
* persistently store and retrieve data client-side. | ||
*/ | ||
|
||
import { assert, assertionError } from "../lib/shared/types"; | ||
|
||
const DB_NAME = "fledge-shim"; | ||
const STORE_NAME = "interest-groups"; | ||
|
||
const dbPromise = new Promise<IDBDatabase>((resolve, reject) => { | ||
const dbRequest = indexedDB.open(DB_NAME, /* version= */ 1); | ||
dbRequest.onupgradeneeded = ({ oldVersion, newVersion }) => { | ||
// This should be called iff the database is just now being created for the | ||
// first time. | ||
assert(oldVersion === 0); | ||
assert(newVersion === 1); | ||
dbRequest.result.createObjectStore(STORE_NAME); | ||
}; | ||
dbRequest.onsuccess = () => { | ||
resolve(dbRequest.result); | ||
}; | ||
dbRequest.onerror = () => { | ||
reject(dbRequest.error); | ||
}; | ||
dbRequest.onblocked = () => { | ||
// Since the version number is 1 (the lowest allowed), it shouldn't be | ||
// possible for an earlier version of the same database to already be open. | ||
reject(assertionError()); | ||
}; | ||
}); | ||
|
||
/** | ||
* Runs an arbitrary operation on the IndexedDB object store. `callback` has to | ||
* be synchronous, but it can create IndexedDB requests, and those requests' | ||
* `onsuccess` handlers can create further requests, and so forth; the | ||
* transaction will be committed and the promise resolved after such a task | ||
* finishes with no further pending requests. Such requests need not register | ||
* `onerror` handlers, unless they need to do fine-grained error handling; if an | ||
* exception is thrown and not caught, the transaction will be aborted without | ||
* committing any writes, and the promise rejected. | ||
*/ | ||
export async function useStore( | ||
txMode: IDBTransactionMode, | ||
callback: (store: IDBObjectStore) => void | ||
): Promise<void> { | ||
const db = await dbPromise; | ||
return new Promise((resolve, reject) => { | ||
// The FLEDGE API does not offer callers any guarantees about when writes | ||
// will be committed; for example, `joinAdInterestGroup` has a synchronous | ||
// API that triggers a background task but does not allow the caller to | ||
// await that task. Therefore, strict durability is not required for | ||
// correctness. So we'll improve latency and user battery life by opting | ||
// into relaxed durability, which allows the browser and OS to economize on | ||
// potentially expensive writes to disk. | ||
const tx = db.transaction(STORE_NAME, txMode, { durability: "relaxed" }); | ||
tx.oncomplete = () => { | ||
resolve(); | ||
}; | ||
tx.onabort = () => { | ||
reject(tx.error); | ||
}; | ||
// No need to explicitly install an onerror handler since an error aborts | ||
// the transaction. | ||
const store = tx.objectStore(STORE_NAME); | ||
try { | ||
callback(store); | ||
} catch (error: unknown) { | ||
tx.abort(); | ||
throw error; | ||
} | ||
}); | ||
} | ||
|
||
declare global { | ||
interface IDBDatabase { | ||
/** | ||
* The `options` parameter is in the IndexedDB spec and is supported by | ||
* Chrome, but is absent from the default TypeScript type definitions. | ||
* | ||
* @see https://www.w3.org/TR/IndexedDB/#database-interface | ||
*/ | ||
transaction( | ||
storeNames: string | Iterable<string>, | ||
mode?: IDBTransactionMode, | ||
options?: { durability?: "default" | "strict" | "relaxed" } | ||
): IDBTransaction; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/** | ||
* @license | ||
* Copyright 2021 Google LLC | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { assertInstance } from "../lib/shared/types"; | ||
import { clearStorageBeforeAndAfter } from "../testing/storage"; | ||
import { useStore } from "./indexeddb"; | ||
|
||
describe("useStore", () => { | ||
clearStorageBeforeAndAfter(); | ||
|
||
const value = "IndexedDB value"; | ||
const key = "IndexedDB key"; | ||
|
||
it("should read its own writes across multiple transactions", async () => { | ||
await useStore("readwrite", (store) => { | ||
store.put(value, key); | ||
}); | ||
await useStore("readonly", (store) => { | ||
const retrievalRequest = store.get(key); | ||
retrievalRequest.onsuccess = () => { | ||
expect(retrievalRequest.result).toBe(value); | ||
}; | ||
}); | ||
}); | ||
|
||
it("should reject if the transaction is aborted", () => | ||
expectAsync( | ||
useStore("readonly", (store) => { | ||
store.transaction.abort(); | ||
}) | ||
).toBeRejectedWith(null)); | ||
|
||
it("should not commit the transaction if the main callback throws", async () => { | ||
const errorMessage = "oops"; | ||
await expectAsync( | ||
useStore("readwrite", (store) => { | ||
store.add(value, key); | ||
throw new Error(errorMessage); | ||
}) | ||
).toBeRejectedWithError(errorMessage); | ||
await useStore("readonly", (store) => { | ||
const countRequest = store.count(); | ||
countRequest.onsuccess = () => { | ||
expect(countRequest.result).toBe(0); | ||
}; | ||
}); | ||
}); | ||
|
||
const otherValue = "other IndexedDB value"; | ||
const otherKey = "other IndexedDB key"; | ||
|
||
it("should not commit the transaction if an illegal operation is attempted", async () => { | ||
await useStore("readwrite", (store) => { | ||
store.put(value, key); | ||
}); | ||
await expectAsync( | ||
useStore("readwrite", (store) => { | ||
store.add(otherValue, otherKey); | ||
// add requires that the given key not already exist. | ||
store.add(otherValue, key); | ||
}) | ||
).toBeRejectedWith( | ||
jasmine.objectContaining({ | ||
constructor: DOMException, | ||
name: "ConstraintError", | ||
}) | ||
); | ||
await useStore("readonly", (store) => { | ||
const retrievalRequest = store.get(otherKey); | ||
retrievalRequest.onsuccess = () => { | ||
expect(retrievalRequest.result).toBeUndefined(); | ||
}; | ||
}); | ||
}); | ||
|
||
it("should commit the transaction if an error is recovered from", async () => { | ||
await useStore("readwrite", (store) => { | ||
store.put(value, key); | ||
}); | ||
await useStore("readwrite", (store) => { | ||
store.add(otherValue, otherKey); | ||
// add requires that the given key not already exist. | ||
const badRequest = store.add(otherValue, key); | ||
badRequest.onsuccess = fail; | ||
badRequest.onerror = (event) => { | ||
assertInstance(badRequest.error, DOMException); | ||
expect(badRequest.error.name).toBe("ConstraintError"); | ||
event.preventDefault(); | ||
}; | ||
}); | ||
await useStore("readonly", (store) => { | ||
const retrievalRequest = store.get(otherKey); | ||
retrievalRequest.onsuccess = () => { | ||
expect(retrievalRequest.result).toBe(otherValue); | ||
}; | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.