Skip to content

Commit 438f2d7

Browse files
mverzilliThunkar
andauthored
chore: forward port sqlite changes (#24947)
Forward port the following PRs from v5-next to next: - #24631 - #24647 - #24739 - #24740 - #24743 --------- Co-authored-by: Gregorio Juliana <gregojquiros@gmail.com>
1 parent 5dd0a31 commit 438f2d7

30 files changed

Lines changed: 1365 additions & 184 deletions

File tree

playground/src/components/navbar/components/NetworkSelector.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import AddIcon from '@mui/icons-material/Add';
77
import { AddNetworksDialog } from './AddNetworkDialog';
88
import CircularProgress from '@mui/material/CircularProgress';
99
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
10-
import { createStore } from '@aztec/kv-store/sqlite-opfs';
10+
import { AztecSQLiteOPFSStore, storePoolDirectory } from '@aztec/kv-store/sqlite-opfs';
1111
import { AztecContext } from '../../../aztecContext';
1212
import { navbarButtonStyle, navbarSelect } from '../../../styles/common';
1313
import { NETWORKS } from '../../../utils/networks';
@@ -50,10 +50,12 @@ export function NetworkSelector() {
5050
}
5151
setIsContextInitialized(true);
5252
WebLogger.create(setLogs, setTotalLogCount);
53-
const store = await createStore('playground_data', {
54-
dataDirectory: 'playground',
55-
dataStoreMapSizeKb: 1e6,
56-
});
53+
const store = await AztecSQLiteOPFSStore.open(
54+
WebLogger.getInstance().createLogger('playground_data'),
55+
'playground_data',
56+
false,
57+
storePoolDirectory('playground_data'),
58+
);
5759
const playgroundDB = PlaygroundDB.getInstance();
5860
playgroundDB.init(store, WebLogger.getInstance().createLogger('playground_db').info);
5961
setPlaygroundDB(PlaygroundDB.getInstance());

yarn-project/kv-store/scripts/run-browser-tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ set -euo pipefail
1515

1616
cd "$(dirname "$0")/.."
1717

18-
files=$(find src/deprecated/indexeddb src/sqlite-opfs -name '*.test.ts' 2>/dev/null | sort)
18+
# -type f: vitest failure screenshots land in __screenshots__/<test-file>/, creating
19+
# directories whose names match the *.test.ts glob.
20+
files=$(find src/deprecated/indexeddb src/sqlite-opfs -type f -name '*.test.ts' 2>/dev/null | sort)
1921

2022
if [ -z "$files" ]; then
2123
echo "No test files found in src/deprecated/indexeddb or src/sqlite-opfs"

yarn-project/kv-store/src/deprecated/indexeddb/index.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,9 @@
1-
import { type Logger, createLogger } from '@aztec/foundation/log';
2-
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
1+
import { createLogger } from '@aztec/foundation/log';
32

4-
import { initStoreForRollupAndSchemaVersion } from '../../utils.js';
53
import { AztecIndexedDBStore } from './store.js';
64

75
export { AztecIndexedDBStore } from './store.js';
86

9-
/**
10-
* @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead.
11-
*/
12-
export async function createStore(
13-
name: string,
14-
config: DataStoreConfig,
15-
schemaVersion: number | undefined = undefined,
16-
log: Logger = createLogger('kv-store'),
17-
) {
18-
let { dataDirectory } = config;
19-
if (typeof dataDirectory !== 'undefined') {
20-
dataDirectory = `${dataDirectory}/${name}`;
21-
}
22-
23-
log.info(
24-
dataDirectory
25-
? `Creating ${name} data store at directory ${dataDirectory} with map size ${config.dataStoreMapSizeKb} KB`
26-
: `Creating ${name} ephemeral data store with map size ${config.dataStoreMapSizeKb} KB`,
27-
);
28-
const store = await AztecIndexedDBStore.open(createLogger('kv-store:indexeddb'), dataDirectory ?? '', false);
29-
return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log);
30-
}
31-
327
/**
338
* @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead.
349
*/
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { isCorruptionMessage, isDecryptFailureMessage } from './errors.js';
4+
5+
/**
6+
* The two classifiers must stay disjoint: a decrypt failure (wrong/missing key)
7+
* is recoverable by re-keying, whereas a corrupt image is not. Mixing them would
8+
* make the worker mis-tag failures and consumers take the wrong recovery path.
9+
*/
10+
describe('sqlite-opfs error classification', () => {
11+
it('classifies SQLITE_CORRUPT / malformed-image messages as corruption', () => {
12+
expect(isCorruptionMessage('SQLITE_CORRUPT: sqlite3 result code 11: database disk image is malformed')).toBe(true);
13+
expect(isCorruptionMessage('database disk image is malformed')).toBe(true);
14+
});
15+
16+
it('does not classify decrypt-failure messages as corruption', () => {
17+
expect(isCorruptionMessage('file is not a database')).toBe(false);
18+
expect(isCorruptionMessage('file is encrypted or is not a database')).toBe(false);
19+
});
20+
21+
it('classifies sqlite3mc decrypt-failure messages as decrypt failures', () => {
22+
expect(isDecryptFailureMessage('file is not a database')).toBe(true);
23+
expect(isDecryptFailureMessage('file is encrypted or is not a database')).toBe(true);
24+
});
25+
26+
it('does not classify corruption messages as decrypt failures', () => {
27+
expect(isDecryptFailureMessage('database disk image is malformed')).toBe(false);
28+
expect(isDecryptFailureMessage('SQLITE_CORRUPT: sqlite3 result code 11')).toBe(false);
29+
});
30+
});

yarn-project/kv-store/src/sqlite-opfs/errors.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,53 @@ const SQLITE3MC_DECRYPT_ERROR_PATTERNS: readonly RegExp[] = [
4242
export function isDecryptFailureMessage(message: string): boolean {
4343
return SQLITE3MC_DECRYPT_ERROR_PATTERNS.some(p => p.test(message));
4444
}
45+
46+
/**
47+
* Error thrown by sqlite-opfs when the on-disk database image is corrupt
48+
* (`SQLITE_CORRUPT`, result code 11 — "database disk image is malformed").
49+
*
50+
* Distinct from {@link SqliteEncryptionError}: no key recovers a corrupt image,
51+
* so there is nothing to retry. The only escape is to delete the store and start
52+
* fresh, so consumers pattern-match on `instanceof SqliteCorruptionError` to wipe
53+
* and reopen rather than surfacing a dead-end error.
54+
**/
55+
export class SqliteCorruptionError extends Error {
56+
constructor(message: string, opts?: { cause?: unknown }) {
57+
super(message, opts?.cause !== undefined ? { cause: opts.cause } : undefined);
58+
this.name = 'SqliteCorruptionError';
59+
}
60+
}
61+
62+
/** Error thrown when another browser context already owns a store's OPFS pool. */
63+
export class SqlitePoolBusyError extends Error {
64+
constructor(public readonly poolDirectory: string) {
65+
super(`SQLite-OPFS pool "${poolDirectory}" is already in use by another store instance`);
66+
this.name = 'SqlitePoolBusyError';
67+
}
68+
}
69+
70+
/** Error thrown when the browser context does not expose the Web Locks API required by the OPFS SAH pool. */
71+
export class SqliteWebLocksUnavailableError extends Error {
72+
constructor() {
73+
super('SQLite-OPFS requires the Web Locks API, but it is unavailable in this browser context');
74+
this.name = 'SqliteWebLocksUnavailableError';
75+
}
76+
}
77+
78+
/**
79+
* Strings/codes raised by SQLite when a database image is corrupt. Kept disjoint
80+
* from {@link SQLITE3MC_DECRYPT_ERROR_PATTERNS}: "file is not a database"
81+
* (SQLITE_NOTADB) is the decrypt signal, whereas a malformed image is the
82+
* genuinely-unrecoverable SQLITE_CORRUPT.
83+
**/
84+
const SQLITE_CORRUPTION_ERROR_PATTERNS: readonly RegExp[] = [
85+
/database disk image is malformed/i,
86+
/\bSQLITE_CORRUPT\b/i,
87+
];
88+
89+
/**
90+
* Returns `true` if `message` matches one of the known SQLite corruption strings.
91+
**/
92+
export function isCorruptionMessage(message: string): boolean {
93+
return SQLITE_CORRUPTION_ERROR_PATTERNS.some(p => p.test(message));
94+
}

yarn-project/kv-store/src/sqlite-opfs/index.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
1-
import { type Logger, createLogger } from '@aztec/foundation/log';
2-
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
1+
import { createLogger } from '@aztec/foundation/log';
32

4-
import { initStoreForRollupAndSchemaVersion } from '../utils.js';
53
import { AztecSQLiteOPFSStore } from './store.js';
64

75
export { AztecSQLiteOPFSStore } from './store.js';
8-
export { SqliteEncryptionError } from './errors.js';
6+
export {
7+
SqliteCorruptionError,
8+
SqliteEncryptionError,
9+
SqlitePoolBusyError,
10+
SqliteWebLocksUnavailableError,
11+
} from './errors.js';
912
export type { SqliteEncryptionErrorCode } from './errors.js';
10-
11-
export async function createStore(
12-
name: string,
13-
config: DataStoreConfig,
14-
schemaVersion: number | undefined = undefined,
15-
log: Logger = createLogger('kv-store'),
16-
) {
17-
const { dataDirectory } = config;
18-
log.info(
19-
dataDirectory
20-
? `Creating ${name} SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`
21-
: `Creating ${name} ephemeral SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`,
22-
);
23-
const store = await AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), name, false);
24-
return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log);
25-
}
13+
export { OPFS_POOL_DIR_PREFIX, deletePoolDirectory, deleteStore, listStores, storePoolDirectory } from './manage.js';
2614

2715
export function openTmpStore(ephemeral: boolean = false): Promise<AztecSQLiteOPFSStore> {
2816
return AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), undefined, ephemeral);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { normalizePoolDirectory, withPoolLock } from './pool_lock.js';
2+
3+
/** Prefix for the per-store OPFS SAH pool directories owned by this package. */
4+
export const OPFS_POOL_DIR_PREFIX = '.aztec-kv-';
5+
6+
/**
7+
* OPFS directory holding a store's SAH pool. One directory per store: the SAH-pool VFS allows only one
8+
* concurrent instance per directory and its capacity does not grow automatically, so sharing a pool across
9+
* stores would make concurrently opened stores contend for locks and orphaned stores exhaust pool slots.
10+
*/
11+
export function storePoolDirectory(effectiveName: string): string {
12+
return `${OPFS_POOL_DIR_PREFIX}${effectiveName}`;
13+
}
14+
15+
/**
16+
* Lists the names of every persistent sqlite-opfs store in this origin, by enumerating the per-store pool
17+
* directories. Includes stores other than the current one, so wallets can surface and clean up data for
18+
* networks/versions no longer in use.
19+
*/
20+
export async function listStores(): Promise<string[]> {
21+
const root = await navigator.storage.getDirectory();
22+
const names: string[] = [];
23+
for await (const [entryName, handle] of root.entries()) {
24+
if (handle.kind === 'directory' && entryName.startsWith(OPFS_POOL_DIR_PREFIX)) {
25+
names.push(entryName.slice(OPFS_POOL_DIR_PREFIX.length));
26+
}
27+
}
28+
return names;
29+
}
30+
31+
/**
32+
* Permanently deletes a store by effective name (as returned by {@link listStores}). The store must be closed:
33+
* an open store holds the directory's Web Lock and the removal will reject with `SqlitePoolBusyError`.
34+
*/
35+
export async function deleteStore(effectiveName: string): Promise<void> {
36+
await deletePoolDirectory(storePoolDirectory(effectiveName));
37+
}
38+
39+
/** Permanently deletes one OPFS SAH-pool directory after exclusively locking it. */
40+
export async function deletePoolDirectory(poolDirectory: string): Promise<void> {
41+
poolDirectory = normalizePoolDirectory(poolDirectory);
42+
await withPoolLock(poolDirectory, async () => {
43+
const root = await navigator.storage.getDirectory();
44+
await root.removeEntry(poolDirectory, { recursive: true });
45+
});
46+
}

0 commit comments

Comments
 (0)