diff --git a/.changeset/friendly-queens-invite.md b/.changeset/friendly-queens-invite.md new file mode 100644 index 000000000..26c52535c --- /dev/null +++ b/.changeset/friendly-queens-invite.md @@ -0,0 +1,5 @@ +--- +'@powersync/node': patch +--- + +[`node:sqlite`] Prevent `database is locked` errors when instantiating the database. diff --git a/packages/node/src/db/WorkerConnectionPool.ts b/packages/node/src/db/WorkerConnectionPool.ts index 068baebc3..d05c21725 100644 --- a/packages/node/src/db/WorkerConnectionPool.ts +++ b/packages/node/src/db/WorkerConnectionPool.ts @@ -1,24 +1,24 @@ +import * as Comlink from 'comlink'; import fs from 'node:fs/promises'; import * as path from 'node:path'; import { Worker } from 'node:worker_threads'; -import * as Comlink from 'comlink'; import { BaseObserver, BatchedUpdateNotification, DBAdapter, DBAdapterListener, - LockContext, - Transaction, DBLockOptions, - QueryResult + LockContext, + QueryResult, + Transaction } from '@powersync/common'; import { Remote } from 'comlink'; import { AsyncResource } from 'node:async_hooks'; +import { isBundledToCommonJs } from '../utils/modules.js'; import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js'; import { RemoteConnection } from './RemoteConnection.js'; import { NodeDatabaseImplementation, NodeSQLOpenOptions } from './options.js'; -import { isBundledToCommonJs } from '../utils/modules.js'; export type BetterSQLite3LockContext = LockContext & { executeBatch(query: string, params?: any[][]): Promise; @@ -135,10 +135,12 @@ export class WorkerConnectionPool extends BaseObserver implem if (this.options.initializeConnection) { await this.options.initializeConnection(connection, isWriter); } - - await connection.execute('pragma journal_mode = WAL'); if (!isWriter) { await connection.execute('pragma query_only = true'); + } else { + // We only need to enable this on the writer connection. + // We can get `database is locked` errors if we enable this on concurrently opening read connections. + await connection.execute('pragma journal_mode = WAL'); } return connection; diff --git a/packages/node/tests/PowerSyncDatabase.test.ts b/packages/node/tests/PowerSyncDatabase.test.ts index 4e0a9a731..e3660841a 100644 --- a/packages/node/tests/PowerSyncDatabase.test.ts +++ b/packages/node/tests/PowerSyncDatabase.test.ts @@ -1,11 +1,12 @@ import * as path from 'node:path'; import { Worker } from 'node:worker_threads'; -import { vi, expect, test } from 'vitest'; -import { AppSchema, databaseTest, tempDirectoryTest } from './utils'; +import { LockContext } from '@powersync/common'; +import { randomUUID } from 'node:crypto'; +import { expect, test, vi } from 'vitest'; import { CrudEntry, CrudTransaction, PowerSyncDatabase } from '../lib'; import { WorkerOpener } from '../lib/db/options'; -import { LockContext } from '@powersync/common'; +import { AppSchema, databaseTest, tempDirectoryTest } from './utils'; test('validates options', async () => { await expect(async () => { @@ -203,3 +204,26 @@ databaseTest('getCrudTransactions', async ({ database }) => { const remainingTransaction = await database.getNextCrudTransaction(); expect(remainingTransaction?.crud).toHaveLength(15); }); + +// This is not a SemVer check, but is basic enough to skip this test on older versions of Node.js +tempDirectoryTest.skipIf(process.versions.node < '22.5.0')( + 'should not present database is locked errors on startup', + async ({ tmpdir }) => { + for (let i = 0; i < 10; i++) { + const database = new PowerSyncDatabase({ + schema: AppSchema, + database: { + dbFilename: `${randomUUID()}.sqlite`, + dbLocation: tmpdir, + implementation: { + type: 'node:sqlite' + } + } + }); + + // This should not throw + await database.waitForReady(); + await database.close(); + } + } +); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e3b97df79..a3ee417db 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,7 @@ packages: - packages/* - tools/* - docs/ + +onlyBuiltDependencies: + - better-sqlite3 + - better-sqlite3-multiple-ciphers