Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/examples/OPFSCoopSyncVFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ export class OPFSCoopSyncVFS extends FacadeVFS {
// for the retried open.
const persistentFile = new PersistentFile(null);
this.persistentFiles.set(path, persistentFile);

// Carry the cause to the retried open, which reports
// SQLITE_CANTOPEN from a branch that has no error of its own.
// Every other error return of this VFS records its cause here
// first, so without this one a caller cannot tell a file held
// by another context from a file that is not there - and
// xGetLastError may still be holding an older error.
this.lastError = e;
console.error(e);
}
})());
Expand Down
2 changes: 2 additions & 0 deletions test/OPFSCoopSyncVFS.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { vfs_xAccess } from "./vfs_xAccess.js";
import { vfs_xClose } from "./vfs_xClose.js";
import { vfs_xRead } from "./vfs_xRead.js";
import { vfs_xWrite } from "./vfs_xWrite.js";
import { vfs_open_last_error } from "./vfs_open_last_error.js";

const CONFIG = 'OPFSCoopSyncVFS';
const BUILDS = ['default', 'asyncify', 'jspi'];
Expand All @@ -22,6 +23,7 @@ describe(CONFIG, function() {
vfs_xClose(context);
vfs_xRead(context);
vfs_xWrite(context);
vfs_open_last_error(context);
});
}
});
5 changes: 5 additions & 0 deletions test/test-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ maybeReset().then(async () => {
return value.apply(target, args);
};
}

// Plain properties are passed through, so a test can read the VFS
// state a call left behind - lastError, for one. Without this the
// proxy answers undefined for everything that is not a method.
return value;
}
});

Expand Down
130 changes: 130 additions & 0 deletions test/vfs_open_last_error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as Comlink from 'comlink';
import * as VFS from '../src/VFS.js';

const HOLDER_SRC = `
let handle = null;
self.onmessage = async ({ data }) => {
if (data.type === 'take') {
try {
const root = await navigator.storage.getDirectory();
const file = await root.getFileHandle(data.name, { create: true });
handle = await file.createSyncAccessHandle();
self.postMessage({ ok: true });
} catch (e) {
self.postMessage({ ok: false, error: e.name });
}
} else {
try { handle?.close(); } catch {}
handle = null;
self.postMessage({ ok: true });
}
};
`;

/**
* Holds an exclusive access handle on a file, in a worker of its own:
* createSyncAccessHandle is not available on the main thread.
*/
function createHolder() {
const url = URL.createObjectURL(
new Blob([HOLDER_SRC], { type: 'text/javascript' }));
const worker = new Worker(url);
const next = () => new Promise((resolve, reject) => {
const bound = setTimeout(() => reject(new Error('holder timed out')), 10_000);
worker.addEventListener('message', ({ data }) => {
clearTimeout(bound);
resolve(data);
}, { once: true });
});
return {
take(name) {
worker.postMessage({ type: 'take', name });
return next();
},
release() {
worker.postMessage({ type: 'release' });
return next();
},
dispose() {
worker.terminate();
URL.revokeObjectURL(url);
}
};
}

/**
* The cause of an open that failed in its asynchronous phase. A VFS that
* cannot open a database synchronously reports SQLITE_BUSY, does the work,
* and answers the retried call - so the error is raised in one call and
* reported in the next. It has to be carried across, or the caller is left
* with a bare SQLITE_CANTOPEN and no way to tell a file held elsewhere from
* one that does not exist.
* @param {import('./TestContext.js').TestContext} context
*/
export function vfs_open_last_error(context) {
describe('vfs_open_last_error', function() {
beforeAll(async function() {
// Clear persistent storage.
const proxy = await context.create();
await context.destroy(proxy);
});

const cleanup = [];
beforeEach(function() {
cleanup.splice(0);
});

afterEach(async function() {
for (const fn of cleanup.reverse()) {
await fn();
}
});

it('should record the cause of an open that failed asynchronously',
async function() {
const name = 'demo';
const holder = createHolder();
cleanup.push(() => holder.dispose());

const taken = await holder.take(name);
if (!taken.ok) {
// The engine grants a second handle on the same file, so nothing
// here can be held from another context.
pending(`cannot hold an exclusive handle: ${taken.error}`);
return;
}
cleanup.push(() => holder.release());

const proxy = await context.create({ reset: false });
cleanup.push(() => context.destroy(proxy));
const vfs = proxy.vfs;

// Drive xOpen directly: the failure has to be observable without a
// connection to ask sqlite3_errmsg, which is exactly the caller's
// situation when sqlite3_open_v2 is what failed.
const pOutFlags = Comlink.proxy(new DataView(new ArrayBuffer(4)));
const flags = VFS.SQLITE_OPEN_CREATE |
VFS.SQLITE_OPEN_READWRITE |
VFS.SQLITE_OPEN_MAIN_DB;

let rc;
do {
const nRetryOps = await proxy.module.retryOps.length;
for (let i = 0; i < nRetryOps; i++) {
await proxy.module.retryOps[i];
}
rc = await vfs.jOpen(name, 1, flags, pOutFlags);
} while (rc === VFS.SQLITE_BUSY);

// The file is held elsewhere, so the open fails. That is expected.
expect(rc).toEqual(VFS.SQLITE_CANTOPEN);

// What must not be lost is why. xGetLastError reports whatever
// lastError holds, and the asynchronous phase is the only place in
// this VFS that failed without setting it.
const lastError = await vfs.lastError;
expect(lastError).toBeTruthy();
expect(lastError?.name).toEqual('NoModificationAllowedError');
});
});
}
Loading