Add OPFS + Pyodide cross-device test harness#290
Conversation
Single self-contained HTML file (no build step) for manually testing the
Origin Private File System across desktop and mobile browsers.
- Runs Pyodide in a dedicated Web Worker created from an inline Blob URL,
so createSyncAccessHandle() (the only OPFS write API that works on iOS
Safari) is available.
- Capability panel: userAgent, isSecureContext, crossOriginIsolated,
SharedArrayBuffer, OPFS presence, createSyncAccessHandle feature-detect
(in worker), and storage.estimate() usage/quota.
- Tools: save file (raw OPFS sync access handle), recursive list, load,
run SQL via stdlib sqlite3 over mountNativeFS("/mnt") with syncfs()
persistence, and clear OPFS.
- Insecure-context banner, responsive tap-friendly layout, and a verbose
activity log that surfaces full errors (name/message/stack) per op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
`import sqlite3` failed at worker init with ModuleNotFoundError because
sqlite3 is unvendored in Pyodide and ships as a separate package. Load it
with `await pyodide.loadPackage("sqlite3")` before importing.
Verified end-to-end with Rodney/headless Chrome: Pyodide loads, save/list/
load work, SQL create+insert+select runs, and after a full page reload both
the saved file and the SQLite rows persist (syncfs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
Deploying tools with
|
| Latest commit: |
0bf4354
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://16ad44fe.tools-b1q.pages.dev |
| Branch Preview URL: | https://claude-opfs-pyodide-harness.tools-b1q.pages.dev |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Clicking Save only wrote to the activity log, with no at-a-glance feedback. Add a transient inline message next to the button (✓ Saved N bytes, or a red failure note) that fades after a couple of seconds. The activity log remains the full record. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Common operations (create table, insert, select all, count, list tables, drop table) are now one-tap buttons that fill the textarea and run, making the SQL tool much faster to exercise — especially on mobile. The SQL run logic is refactored into runSqlFromTextarea() shared by the presets and the Run SQL button. Presets are a simple editable array near the bottom of the script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Presets now populate the SQL textarea and focus it, leaving it to the user to review and click Run SQL. Updates the label and comment to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
OPFS getFileHandle() rejects names containing "/", which made saving to a subdirectory fail with "TypeError: Name is invalid". Add resolvePath() to split a path into directory segments and a filename, creating intermediate directories on save, and use it from both saveFile and loadFile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
- Load file now flashes an inline "✓ Loaded N bytes" or "✗ Not found / load failed — see log" so a missing file is visible at the button, not just in the activity log, and stale contents are cleared on failure. - Run SQL now calls syncfs() after every successful statement, not only writes. Merely connecting creates /mnt/test.db in Pyodide's in-memory FS; syncing flushes it to OPFS so it shows up in "List files" even after a read-only query. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
A failing statement (syntax error, missing table, etc.) previously only appeared in the activity log. runOp now takes an optional onError handler; the SQL tool uses it to render the error name + message in #sql-out, so failures are visible right at the tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The "List files" panel only updated on click, so after SQL grew test.db the panel kept showing a stale (often 8192-byte, near-empty) size — confusing, since the data clearly didn't fit. Extract refreshFileList() and call it after a successful SQL sync when the list is already shown, so the displayed size stays consistent with the database. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Investigating a report of test.db showing 8192 bytes despite large row data. On desktop Chrome syncfs() persists growth correctly (reproduced: 2000 rows -> 86 KB on disk, survives reload), so the bug is environment/ device specific (syncfs can resolve "successfully" yet not persist on some browsers). To make that diagnosable: - runSQL now reads the actual on-disk size of test.db straight from raw OPFS after syncfs and returns it (dbBytesOnDisk). The SQL result banner shows it every run, so "write succeeded but size didn't grow" is visible. - Fix the banner that claimed "changes persisted to OPFS (safe to reload)" even after a read-only SELECT (which now also syncs). It now distinguishes a synced write from a read-only query, and reports sync failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The Run SQL tool persisted its database with pyodide.mountNativeFS("/mnt",
...) + nativefs.syncfs(). On macOS Safari (and iOS/iPadOS) this silently
loses data: syncfs() resolves "successfully" and the UI shows the green
"persisted" banner, but the database file on disk never grows past its
initial size (e.g. 8192 bytes) — so after a reload the rows are gone.
Root cause is in how Pyodide's NativeFS writes back to OPFS. It relies on
the File System Access createWritable() stream, which Safari does not
implement reliably for OPFS, and syncfs(populate=true) additionally throws
on iPad. These are long-standing, browser-specific Pyodide bugs:
- pyodide/pyodide#4057
(FS.syncfs with populate=true throws on iPad / iOS)
- pyodide/pyodide#3456
(pyodide.mountNativeFS and syncfs persistence problems)
Reproduced on desktop Chrome that mountNativeFS DOES work there (2000 rows
-> 86 KB on disk, survives reload), confirming the failure is Safari's
createWritable path, not our logic.
Fix: stop using mountNativeFS/syncfs and persist the database ourselves
through createSyncAccessHandle() — the one OPFS write API that works in
every browser including Safari (it is already what the raw Save tool uses).
Each Run SQL now:
1. reads test.db from OPFS into Pyodide's in-memory FS (sync access handle),
2. runs the SQL against that in-memory database,
3. writes the whole database back to OPFS via a sync access handle
(truncate -> write -> flush -> close).
The databases here are small, so copying the whole file per command is cheap
and keeps each command self-contained. dbBytesOnDisk is now read straight
from the bytes we wrote, giving an accurate on-disk size in the result.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Inserts a ~10 KB body so it's easy to watch test.db grow on disk and to exercise larger writes / persistence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Single self-contained HTML file (no build step) for manually testing the
Origin Private File System across desktop and mobile browsers.
so createSyncAccessHandle() (the only OPFS write API that works on iOS
Safari) is available.
SharedArrayBuffer, OPFS presence, createSyncAccessHandle feature-detect
(in worker), and storage.estimate() usage/quota.
run SQL via stdlib sqlite3 over mountNativeFS("/mnt") with syncfs()
persistence, and clear OPFS.
activity log that surfaces full errors (name/message/stack) per op.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01CBcWE5Hk6s9a9NqZ1oS6Vt