diff --git a/.changeset/fuzzy-dodos-work.md b/.changeset/fuzzy-dodos-work.md new file mode 100644 index 000000000..aeac90bf9 --- /dev/null +++ b/.changeset/fuzzy-dodos-work.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Report a clear error when OPFS-AHP is used outside a Dedicated Web Worker. diff --git a/docs/docs/filesystems.md b/docs/docs/filesystems.md index d3ef16ad3..c96319eec 100644 --- a/docs/docs/filesystems.md +++ b/docs/docs/filesystems.md @@ -86,7 +86,7 @@ The IndexedDB filesystem works at the file level, storing whole files (Postgres ## OPFS AHP FS -The OPFS AHP filesystem is built on top of the [Origin Private Filesystem](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) in the browser and uses an "access handle pool". It is only available when PGlite is run in a Web Worker, this could be any worker you configure. We provide a [Multi Tab Worker](./multi-tab-worker.md) to aid in using PGlite from multiple tabs in the browser. +The OPFS AHP filesystem is built on top of the [Origin Private Filesystem](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) in the browser and uses an "access handle pool". It is only available when PGlite is run in a Dedicated Web Worker. We provide a [Multi Tab Worker](./multi-tab-worker.md) to aid in using PGlite from multiple tabs in the browser. To use the OPFS AHP FS you can use one of these methods: diff --git a/packages/pglite/src/fs/opfs-ahp-support.ts b/packages/pglite/src/fs/opfs-ahp-support.ts new file mode 100644 index 000000000..965d29d28 --- /dev/null +++ b/packages/pglite/src/fs/opfs-ahp-support.ts @@ -0,0 +1,19 @@ +interface FileSystemFileHandleConstructor { + prototype: object +} + +export function isOpfsAhpSupported( + fileSystemFileHandle: + | FileSystemFileHandleConstructor + | undefined = typeof FileSystemFileHandle === 'undefined' + ? undefined + : FileSystemFileHandle, +): boolean { + return ( + typeof ( + fileSystemFileHandle?.prototype as { + createSyncAccessHandle?: unknown + } + )?.createSyncAccessHandle === 'function' + ) +} diff --git a/packages/pglite/src/fs/opfs-ahp.ts b/packages/pglite/src/fs/opfs-ahp.ts index 433af52bc..c3b00b613 100644 --- a/packages/pglite/src/fs/opfs-ahp.ts +++ b/packages/pglite/src/fs/opfs-ahp.ts @@ -1,6 +1,7 @@ import { BaseFilesystem, ERRNO_CODES, type FsStats } from './base.js' import type { PostgresMod } from '../postgresMod.js' import { PGlite } from '../pglite.js' +import { isOpfsAhpSupported } from './opfs-ahp-support.js' export interface OpfsAhpOptions { initialPoolSize?: number @@ -129,6 +130,12 @@ export class OpfsAhpFS extends BaseFilesystem { } async #init() { + if (!isOpfsAhpSupported()) { + throw new Error( + 'OPFS-AHP is only supported in a Dedicated Web Worker because FileSystemFileHandle.createSyncAccessHandle() is not available in this context.', + ) + } + this.#opfsRootAh = await navigator.storage.getDirectory() this.#rootAh = await this.#resolveOpfsDirectory(this.dataDir!, { create: true, diff --git a/packages/pglite/tests/opfs-ahp.test.ts b/packages/pglite/tests/opfs-ahp.test.ts new file mode 100644 index 000000000..9080904e6 --- /dev/null +++ b/packages/pglite/tests/opfs-ahp.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, vi } from 'vitest' + +vi.mock('../src/fs/base.js', () => ({ + BaseFilesystem: class { + protected dataDir?: string + + constructor(dataDir?: string) { + this.dataDir = dataDir + } + }, + ERRNO_CODES: {}, +})) + +vi.mock('../src/pglite.js', () => ({ PGlite: class {} })) + +import { OpfsAhpFS } from '../src/fs/opfs-ahp.js' +import { isOpfsAhpSupported } from '../src/fs/opfs-ahp-support.js' + +describe('isOpfsAhpSupported', () => { + it('rejects environments without the sync access handle API', () => { + expect(isOpfsAhpSupported(undefined)).toBe(false) + expect(isOpfsAhpSupported({ prototype: {} })).toBe(false) + }) + + it('accepts the API shape exposed in Dedicated Web Workers', () => { + expect( + isOpfsAhpSupported({ + prototype: { createSyncAccessHandle: () => undefined }, + }), + ).toBe(true) + }) +}) + +describe('OpfsAhpFS', () => { + it('explains that OPFS-AHP requires a Dedicated Web Worker', async () => { + const fs = new OpfsAhpFS('test') + + await expect(fs.init(undefined as never, {})).rejects.toThrow( + 'OPFS-AHP is only supported in a Dedicated Web Worker because FileSystemFileHandle.createSyncAccessHandle() is not available in this context.', + ) + }) +})