expo-sqlite
Advanced tools
Comparing version
/** | ||
* @hidden | ||
*/ | ||
export declare function checkValidInput(...input: unknown[]): void; | ||
/** | ||
* Update function for the [`setItemAsync()`](#setitemasynckey-value) or [`setItemSync()`](#setitemsynckey-value) method. It computes the new value based on the previous value. The function returns the new value to set for the key. | ||
@@ -17,2 +13,3 @@ * @param prevValue The previous value associated with the key, or `null` if the key was not set. | ||
private db; | ||
private readonly awaitLock; | ||
constructor(databaseName: string); | ||
@@ -115,3 +112,5 @@ /** | ||
close(): Promise<void>; | ||
private getDbAsync; | ||
private getDbSync; | ||
private maybeMigrateDbAsync; | ||
private maybeMigrateDbSync; | ||
@@ -122,2 +121,3 @@ /** | ||
private static mergeDeep; | ||
private checkValidInput; | ||
} | ||
@@ -124,0 +124,0 @@ /** |
@@ -1,14 +0,3 @@ | ||
import { openDatabaseSync } from './index'; | ||
/** | ||
* @hidden | ||
*/ | ||
export function checkValidInput(...input) { | ||
const [key, value] = input; | ||
if (typeof key !== 'string') { | ||
throw new Error(`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${key}`); | ||
} | ||
if (input.length > 1 && typeof value !== 'string' && typeof value !== 'function') { | ||
throw new Error(`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${value}`); | ||
} | ||
} | ||
import AwaitLock from 'await-lock'; | ||
import { openDatabaseAsync, openDatabaseSync } from './index'; | ||
const DATABASE_VERSION = 1; | ||
@@ -27,2 +16,3 @@ const STATEMENT_GET = 'SELECT value FROM storage WHERE key = ?;'; | ||
db = null; | ||
awaitLock = new AwaitLock(); | ||
constructor(databaseName) { | ||
@@ -36,4 +26,4 @@ this.databaseName = databaseName; | ||
async getItemAsync(key) { | ||
checkValidInput(key); | ||
const db = this.getDbSync(); | ||
this.checkValidInput(key); | ||
const db = await this.getDbAsync(); | ||
const result = await db.getFirstAsync(STATEMENT_GET, key); | ||
@@ -47,4 +37,4 @@ return result?.value ?? null; | ||
async setItemAsync(key, value) { | ||
checkValidInput(key, value); | ||
const db = this.getDbSync(); | ||
this.checkValidInput(key, value); | ||
const db = await this.getDbAsync(); | ||
if (typeof value === 'function') { | ||
@@ -55,3 +45,3 @@ await db.withExclusiveTransactionAsync(async (tx) => { | ||
const nextValue = value(prevValue); | ||
checkValidInput(key, nextValue); | ||
this.checkValidInput(key, nextValue); | ||
await tx.runAsync(STATEMENT_SET, key, nextValue); | ||
@@ -67,4 +57,4 @@ }); | ||
async removeItemAsync(key) { | ||
checkValidInput(key); | ||
const db = this.getDbSync(); | ||
this.checkValidInput(key); | ||
const db = await this.getDbAsync(); | ||
const result = await db.runAsync(STATEMENT_REMOVE, key); | ||
@@ -77,3 +67,3 @@ return result.changes > 0; | ||
async getAllKeysAsync() { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
const result = await db.getAllAsync(STATEMENT_GET_ALL_KEYS); | ||
@@ -86,3 +76,3 @@ return result.map(({ key }) => key); | ||
async clearAsync() { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
const result = await db.runAsync(STATEMENT_CLEAR); | ||
@@ -95,6 +85,12 @@ return result.changes > 0; | ||
async closeAsync() { | ||
if (this.db) { | ||
await this.db.closeAsync(); | ||
this.db = null; | ||
await this.awaitLock.acquireAsync(); | ||
try { | ||
if (this.db) { | ||
await this.db.closeAsync(); | ||
this.db = null; | ||
} | ||
} | ||
finally { | ||
this.awaitLock.release(); | ||
} | ||
} | ||
@@ -107,3 +103,3 @@ //#endregion | ||
getItemSync(key) { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
const db = this.getDbSync(); | ||
@@ -118,3 +114,3 @@ const result = db.getFirstSync(STATEMENT_GET, key); | ||
setItemSync(key, value) { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
const db = this.getDbSync(); | ||
@@ -126,3 +122,3 @@ if (typeof value === 'function') { | ||
const nextValue = value(prevValue); | ||
checkValidInput(key, nextValue); | ||
this.checkValidInput(key, nextValue); | ||
db.runSync(STATEMENT_SET, key, nextValue); | ||
@@ -138,3 +134,3 @@ }); | ||
removeItemSync(key) { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
const db = this.getDbSync(); | ||
@@ -206,3 +202,3 @@ const result = db.runSync(STATEMENT_REMOVE, key); | ||
async mergeItem(key, value) { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
await this.setItemAsync(key, (prevValue) => { | ||
@@ -223,3 +219,3 @@ if (prevValue == null) { | ||
return Promise.all(keys.map(async (key) => { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
return [key, await this.getItemAsync(key)]; | ||
@@ -232,6 +228,6 @@ })); | ||
async multiSet(keyValuePairs) { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
await db.withExclusiveTransactionAsync(async (tx) => { | ||
for (const [key, value] of keyValuePairs) { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
await tx.runAsync(STATEMENT_SET, key, value); | ||
@@ -245,6 +241,6 @@ } | ||
async multiRemove(keys) { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
await db.withExclusiveTransactionAsync(async (tx) => { | ||
for (const key of keys) { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
await tx.runAsync(STATEMENT_REMOVE, key); | ||
@@ -259,6 +255,6 @@ } | ||
async multiMerge(keyValuePairs) { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
await db.withExclusiveTransactionAsync(async (tx) => { | ||
for (const [key, value] of keyValuePairs) { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
const prevValue = await tx.getFirstAsync(STATEMENT_GET, key); | ||
@@ -284,2 +280,16 @@ if (prevValue == null) { | ||
//#region Internals | ||
async getDbAsync() { | ||
await this.awaitLock.acquireAsync(); | ||
try { | ||
if (!this.db) { | ||
const db = await openDatabaseAsync(this.databaseName); | ||
await this.maybeMigrateDbAsync(db); | ||
this.db = db; | ||
} | ||
} | ||
finally { | ||
this.awaitLock.release(); | ||
} | ||
return this.db; | ||
} | ||
getDbSync() { | ||
@@ -293,2 +303,16 @@ if (!this.db) { | ||
} | ||
maybeMigrateDbAsync(db) { | ||
return db.withTransactionAsync(async () => { | ||
const result = await db.getFirstAsync('PRAGMA user_version'); | ||
let currentDbVersion = result?.user_version ?? 0; | ||
if (currentDbVersion >= DATABASE_VERSION) { | ||
return; | ||
} | ||
if (currentDbVersion === 0) { | ||
await db.execAsync(MIGRATION_STATEMENT_0); | ||
currentDbVersion = 1; | ||
} | ||
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); | ||
}); | ||
} | ||
maybeMigrateDbSync(db) { | ||
@@ -335,2 +359,11 @@ db.withTransactionSync(() => { | ||
} | ||
checkValidInput(...input) { | ||
const [key, value] = input; | ||
if (typeof key !== 'string') { | ||
throw new Error(`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${key}`); | ||
} | ||
if (input.length > 1 && typeof value !== 'string' && typeof value !== 'function') { | ||
throw new Error(`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${value}`); | ||
} | ||
} | ||
} | ||
@@ -337,0 +370,0 @@ /** |
@@ -13,2 +13,13 @@ # Changelog | ||
## 15.2.10 — 2025-05-08 | ||
### 🐛 Bug fixes | ||
- Fixed parallel issue for `Statement.executeAsync`. ([#36674](https://github.com/expo/expo/pull/36674) by [@kudo](https://github.com/kudo)) | ||
### 💡 Others | ||
- Avoided synchronous API calls for `kv-store`. ([#36669](https://github.com/expo/expo/pull/36669) by [@kudo](https://github.com/kudo)) | ||
- Improved synchronous APIs on web. ([#36670](https://github.com/expo/expo/pull/36670) by [@kudo](https://github.com/kudo)) | ||
## 15.2.9 — 2025-04-30 | ||
@@ -15,0 +26,0 @@ |
@@ -8,10 +8,4 @@ { | ||
"modules": ["expo.modules.sqlite.SQLiteModule"], | ||
"shouldUsePublicationScriptPath": "android/shouldUsePublication.groovy", | ||
"publication": { | ||
"groupId": "host.exp.exponent", | ||
"artifactId": "expo.modules.sqlite", | ||
"version": "15.2.9", | ||
"repository": "local-maven-repo" | ||
} | ||
"shouldUsePublicationScriptPath": "android/shouldUsePublication.groovy" | ||
} | ||
} |
{ | ||
"name": "expo-sqlite", | ||
"version": "15.2.9", | ||
"version": "15.2.10", | ||
"description": "Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.", | ||
@@ -51,3 +51,5 @@ "main": "build/index.js", | ||
}, | ||
"dependencies": {}, | ||
"dependencies": { | ||
"await-lock": "^2.2.2" | ||
}, | ||
"devDependencies": { | ||
@@ -57,3 +59,3 @@ "@testing-library/react-native": "^13.1.0", | ||
"better-sqlite3": "^11.6.0", | ||
"expo-module-scripts": "^4.1.6", | ||
"expo-module-scripts": "^4.1.7", | ||
"react-error-boundary": "^4.0.11" | ||
@@ -66,3 +68,3 @@ }, | ||
}, | ||
"gitHead": "84355076bc31a356aa3d23257f387f221885f53d" | ||
"gitHead": "49c9d53cf0a9fc8179d1c8f5268beadd141f70ca" | ||
} |
@@ -1,22 +0,5 @@ | ||
import { openDatabaseSync, type SQLiteDatabase } from './index'; | ||
import AwaitLock from 'await-lock'; | ||
/** | ||
* @hidden | ||
*/ | ||
export function checkValidInput(...input: unknown[]) { | ||
const [key, value] = input; | ||
import { openDatabaseAsync, openDatabaseSync, type SQLiteDatabase } from './index'; | ||
if (typeof key !== 'string') { | ||
throw new Error( | ||
`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${key}` | ||
); | ||
} | ||
if (input.length > 1 && typeof value !== 'string' && typeof value !== 'function') { | ||
throw new Error( | ||
`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${value}` | ||
); | ||
} | ||
} | ||
/** | ||
@@ -45,2 +28,3 @@ * Update function for the [`setItemAsync()`](#setitemasynckey-value) or [`setItemSync()`](#setitemsynckey-value) method. It computes the new value based on the previous value. The function returns the new value to set for the key. | ||
private db: SQLiteDatabase | null = null; | ||
private readonly awaitLock = new AwaitLock(); | ||
@@ -55,4 +39,4 @@ constructor(private readonly databaseName: string) {} | ||
async getItemAsync(key: string): Promise<string | null> { | ||
checkValidInput(key); | ||
const db = this.getDbSync(); | ||
this.checkValidInput(key); | ||
const db = await this.getDbAsync(); | ||
const result = await db.getFirstAsync<{ value: string }>(STATEMENT_GET, key); | ||
@@ -70,4 +54,4 @@ return result?.value ?? null; | ||
): Promise<void> { | ||
checkValidInput(key, value); | ||
const db = this.getDbSync(); | ||
this.checkValidInput(key, value); | ||
const db = await this.getDbAsync(); | ||
@@ -79,3 +63,3 @@ if (typeof value === 'function') { | ||
const nextValue = value(prevValue); | ||
checkValidInput(key, nextValue); | ||
this.checkValidInput(key, nextValue); | ||
await tx.runAsync(STATEMENT_SET, key, nextValue); | ||
@@ -93,4 +77,4 @@ }); | ||
async removeItemAsync(key: string): Promise<boolean> { | ||
checkValidInput(key); | ||
const db = this.getDbSync(); | ||
this.checkValidInput(key); | ||
const db = await this.getDbAsync(); | ||
const result = await db.runAsync(STATEMENT_REMOVE, key); | ||
@@ -104,3 +88,3 @@ return result.changes > 0; | ||
async getAllKeysAsync(): Promise<string[]> { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
const result = await db.getAllAsync<{ key: string }>(STATEMENT_GET_ALL_KEYS); | ||
@@ -114,3 +98,3 @@ return result.map(({ key }) => key); | ||
async clearAsync(): Promise<boolean> { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
const result = await db.runAsync(STATEMENT_CLEAR); | ||
@@ -124,5 +108,10 @@ return result.changes > 0; | ||
async closeAsync(): Promise<void> { | ||
if (this.db) { | ||
await this.db.closeAsync(); | ||
this.db = null; | ||
await this.awaitLock.acquireAsync(); | ||
try { | ||
if (this.db) { | ||
await this.db.closeAsync(); | ||
this.db = null; | ||
} | ||
} finally { | ||
this.awaitLock.release(); | ||
} | ||
@@ -139,3 +128,3 @@ } | ||
getItemSync(key: string): string | null { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
const db = this.getDbSync(); | ||
@@ -151,3 +140,3 @@ const result = db.getFirstSync<{ value: string }>(STATEMENT_GET, key); | ||
setItemSync(key: string, value: string | SQLiteStorageSetItemUpdateFunction): void { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
const db = this.getDbSync(); | ||
@@ -160,3 +149,3 @@ | ||
const nextValue = value(prevValue); | ||
checkValidInput(key, nextValue); | ||
this.checkValidInput(key, nextValue); | ||
db.runSync(STATEMENT_SET, key, nextValue); | ||
@@ -174,3 +163,3 @@ }); | ||
removeItemSync(key: string): boolean { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
const db = this.getDbSync(); | ||
@@ -253,3 +242,3 @@ const result = db.runSync(STATEMENT_REMOVE, key); | ||
async mergeItem(key: string, value: string): Promise<void> { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
await this.setItemAsync(key, (prevValue) => { | ||
@@ -272,3 +261,3 @@ if (prevValue == null) { | ||
keys.map(async (key): Promise<[string, string | null]> => { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
return [key, await this.getItemAsync(key)]; | ||
@@ -283,6 +272,6 @@ }) | ||
async multiSet(keyValuePairs: [string, string][]): Promise<void> { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
await db.withExclusiveTransactionAsync(async (tx) => { | ||
for (const [key, value] of keyValuePairs) { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
await tx.runAsync(STATEMENT_SET, key, value); | ||
@@ -297,6 +286,6 @@ } | ||
async multiRemove(keys: string[]): Promise<void> { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
await db.withExclusiveTransactionAsync(async (tx) => { | ||
for (const key of keys) { | ||
checkValidInput(key); | ||
this.checkValidInput(key); | ||
await tx.runAsync(STATEMENT_REMOVE, key); | ||
@@ -312,6 +301,6 @@ } | ||
async multiMerge(keyValuePairs: [string, string][]): Promise<void> { | ||
const db = this.getDbSync(); | ||
const db = await this.getDbAsync(); | ||
await db.withExclusiveTransactionAsync(async (tx) => { | ||
for (const [key, value] of keyValuePairs) { | ||
checkValidInput(key, value); | ||
this.checkValidInput(key, value); | ||
const prevValue = await tx.getFirstAsync<{ value: string }>(STATEMENT_GET, key); | ||
@@ -341,2 +330,16 @@ if (prevValue == null) { | ||
private async getDbAsync(): Promise<SQLiteDatabase> { | ||
await this.awaitLock.acquireAsync(); | ||
try { | ||
if (!this.db) { | ||
const db = await openDatabaseAsync(this.databaseName); | ||
await this.maybeMigrateDbAsync(db); | ||
this.db = db; | ||
} | ||
} finally { | ||
this.awaitLock.release(); | ||
} | ||
return this.db; | ||
} | ||
private getDbSync(): SQLiteDatabase { | ||
@@ -351,2 +354,17 @@ if (!this.db) { | ||
private maybeMigrateDbAsync(db: SQLiteDatabase) { | ||
return db.withTransactionAsync(async () => { | ||
const result = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version'); | ||
let currentDbVersion = result?.user_version ?? 0; | ||
if (currentDbVersion >= DATABASE_VERSION) { | ||
return; | ||
} | ||
if (currentDbVersion === 0) { | ||
await db.execAsync(MIGRATION_STATEMENT_0); | ||
currentDbVersion = 1; | ||
} | ||
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`); | ||
}); | ||
} | ||
private maybeMigrateDbSync(db: SQLiteDatabase) { | ||
@@ -397,2 +415,18 @@ db.withTransactionSync(() => { | ||
private checkValidInput(...input: unknown[]) { | ||
const [key, value] = input; | ||
if (typeof key !== 'string') { | ||
throw new Error( | ||
`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${key}` | ||
); | ||
} | ||
if (input.length > 1 && typeof value !== 'string' && typeof value !== 'function') { | ||
throw new Error( | ||
`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${value}` | ||
); | ||
} | ||
} | ||
//#endregion | ||
@@ -399,0 +433,0 @@ } |
@@ -40,3 +40,3 @@ // Copyright 2015-present 650 Industries. All rights reserved. | ||
const resultArray = new Uint8Array(resultBuffer); | ||
const resultJson = result ? serialize({ result }) : serialize({ error }); | ||
const resultJson = error != null ? serialize({ error }) : serialize({ result }); | ||
const resultBytes = new TextEncoder().encode(resultJson); | ||
@@ -121,9 +121,19 @@ const length = resultBytes.length; | ||
let i = 0; | ||
// @ts-expect-error: Remove this when TypeScript supports Atomics.pause | ||
const useAtomicsPause = typeof Atomics.pause === 'function'; | ||
while (Atomics.load(lock, 0) === PENDING) { | ||
// NOTE(kudo): Unfortunate busy loop, | ||
// because we don't have a way for main thread to yield its execution to other callbacks. | ||
// Maybe we can wait for [`Atomics.pause`](https://github.com/tc39/proposal-atomics-microwait) to be implemented. | ||
++i; | ||
if (i > 1000000000) { | ||
throw new Error('Sync operation timeout'); | ||
if (useAtomicsPause) { | ||
if (i > 1_000_000) { | ||
throw new Error('Sync operation timeout'); | ||
} | ||
// @ts-expect-error: Remove this when TypeScript supports Atomics.pause | ||
Atomics.pause(); | ||
} else { | ||
// NOTE(kudo): Unfortunate for the busy loop, | ||
// because we don't have a way for main thread to yield its execution to other callbacks. | ||
if (i > 1000_000_000) { | ||
throw new Error('Sync operation timeout'); | ||
} | ||
} | ||
@@ -130,0 +140,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Unstable ownership
Supply chain riskA new collaborator has begun publishing package versions. Package stability and security risk may be elevated.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
11387
0.59%69596615
-5.14%4
33.33%161
-13.44%1
Infinity%7
16.67%+ Added
+ Added