Lesson 0003 · ~12 minutes
One skill: know when to leave main-thread
createWritable behind and use a
FileSystemSyncAccessHandle in a
dedicated worker — exclusive lock, byte buffers,
flush/close.
In a design review you can say: “async stream on the main thread vs sync access handle in a dedicated worker,” name the lock, and pick the path that matches throughput needs — without shipping SQLite-wasm yourself.
Lesson 2’s path works on the
main thread and in workers: open handles, then
createWritable / getFile. That is the default
for ordinary app I/O
(web.dev OPFS).
When the hard problem is
high-throughput, in-place byte I/O — Wasm engines, DB
files, random access — OPFS exposes a second path:
FileSystemSyncAccessHandle. Methods are
synchronous so C-style code and tight loops do not fight
promises. Browsers only allow that where it cannot freeze the UI:
dedicated workers
(MDN createSyncAccessHandle).
createWritable → write →
close; getFile to read. Safe default for
text, blobs, exports.
createSyncAccessHandle then sync
read / write / truncate /
flush / close. Fast path for engines.
Start with async main-thread (or worker async) streams. Reach for a sync access handle only when you need low-level random access or Wasm/DB throughput — and plan a dedicated worker for that work.
Same root and file handle as lesson 2. The open call is still async (the name is confusing). The methods on the result are sync (web.dev):
// Inside a dedicated worker
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("db.bin", { create: true });
const access = await fileHandle.createSyncAccessHandle();
// optional: { mode: "readwrite" } (default), "read-only", "readwrite-unsafe"
// … sync I/O …
access.close(); // release lock
Constraints from MDN FileSystemSyncAccessHandle:
InvalidStateError if you try).
readwrite: one exclusive writer).
| Method | Does |
|---|---|
getSize() |
File length in bytes |
read(buffer, { at }) |
Fill an ArrayBuffer / view; optional offset
|
write(buffer, { at }) |
Write bytes; returns written count — check for partial writes |
truncate(size) |
Resize file (e.g. empty with 0) |
flush() |
Persist writes to storage |
close() |
End use; release the lock |
const enc = new TextEncoder();
const dec = new TextDecoder();
let size = access.getSize();
access.write(enc.encode("Some text"), { at: size });
access.flush();
size = access.getSize();
const view = new DataView(new ArrayBuffer(size));
access.read(view, { at: 0 });
console.log(dec.decode(view));
access.truncate(4);
access.close();
Buffers, not strings: pass
ArrayBuffer / typed arrays / DataView
(web.dev). Encode text first.
Default mode is exclusive. A second
createSyncAccessHandle() or
createWritable() on the same file fails with
NoModificationAllowedError until
close(). Multi-tab writers need an explicit strategy:
read-only / readwrite-unsafe / message
coordination — do not assume silent sharing
(MDN).
UI, pickers, and many APIs stay on the main thread. Pattern:
new Worker("io.js"),
postMessage commands/payloads.
close → reply.
showSaveFilePicker on the
main thread (not available in workers); copy bytes
after the worker has closed its access handle
(web.dev).
Choose the best default. Equal-length options so formatting doesn’t hint. Feedback is immediate.
You need maximum-throughput random byte I/O on an OPFS file for a Wasm DB engine.
On the main thread, save a short JSON draft string into an OPFS file the user never sees.
A worker already holds a default-mode sync access handle on
db.bin. Another context tries to open the same file for
exclusive write.
Inside a worker, append UTF-8 text to an open sync access handle, then free the file for others.
After a worker finished heavy OPFS work, the product must offer “Save as…” into the user’s Downloads via a picker.
createSyncAccessHandle() is async to open; methods are
sync; dedicated workers + OPFS only.
close(); design multi-tab
deliberately.
flush; export to user-visible FS is a main-thread
copy, not a move.
web.dev — The origin private file system (Thomas Steiner), section Use the origin private file system in a Web Worker. Pair with MDN FileSystemSyncAccessHandle for the method list and lock notes. Still useful deferred skims: Storage for the web (spectrum).