diff --git a/.changeset/fix-initdb-fs-option-leak.md b/.changeset/fix-initdb-fs-option-leak.md new file mode 100644 index 000000000..608e1c7b4 --- /dev/null +++ b/.changeset/fix-initdb-fs-option-leak.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Fix `PGlite.create({ fs })` on a fresh database calling the provided filesystem's `init()` twice: the inner initdb instance no longer inherits the user-provided `fs` and always runs on its own scratch filesystem. Previously any VFS holding exclusive resources (e.g. OPFS sync access handles) failed with a contention error on first create. diff --git a/packages/pglite/src/pglite.ts b/packages/pglite/src/pglite.ts index e23ac32c4..10d421ee9 100644 --- a/packages/pglite/src/pglite.ts +++ b/packages/pglite/src/pglite.ts @@ -558,6 +558,11 @@ export class PGlite pgInitDbOpts.dataDir = undefined pgInitDbOpts.extensions = undefined pgInitDbOpts.loadDataDir = undefined + // The initdb instance must run on its own scratch filesystem: a + // user-provided `fs` must not leak into it, or its second `init()` + // collides with resources the outer instance already holds (e.g. an + // OPFS VFS's sync access handles, which are exclusive per file). + pgInitDbOpts.fs = undefined const pg_initDb = await PGlite.create(pgInitDbOpts) // Initialize the database diff --git a/packages/pglite/tests/initdb-fs-option.test.ts b/packages/pglite/tests/initdb-fs-option.test.ts new file mode 100644 index 000000000..36346b7f6 --- /dev/null +++ b/packages/pglite/tests/initdb-fs-option.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from 'vitest' +import { PGlite, MemoryFS } from '../dist/index.js' + +// Regression test: creating a fresh database with a user-provided `fs` option +// boots an inner PGlite instance to run initdb. That inner instance must run +// on its own scratch filesystem — if the user's `fs` leaks into it, the +// filesystem's `init()` runs twice against the same backing resources, which +// breaks any VFS holding exclusive resources (e.g. OPFS sync access handles). +describe('initdb with user-provided fs option', () => { + it('does not re-init the provided fs for the inner initdb instance', async () => { + class CountingFS extends MemoryFS { + initCount = 0 + async init(...args: Parameters) { + this.initCount++ + return super.init(...args) + } + } + + const fs = new CountingFS() + const pg = await PGlite.create({ fs }) + + expect(fs.initCount).toBe(1) + + // The store initialized and works end-to-end on the provided fs. + await pg.exec('CREATE TABLE t (id int)') + await pg.exec('INSERT INTO t VALUES (1)') + const res = await pg.query<{ id: number }>('SELECT id FROM t') + expect(res.rows).toEqual([{ id: 1 }]) + await pg.close() + }) +})