Lesson 0003 · ~12 minutes

Workers & sync access handles

One skill: know when to leave main-thread createWritable behind and use a FileSystemSyncAccessHandle in a dedicated worker — exclusive lock, byte buffers, flush/close.

Win for this lesson

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.

1. Two OPFS speeds

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).

Main thread / async

createWritablewriteclose; getFile to read. Safe default for text, blobs, exports.

Worker / sync access

createSyncAccessHandle then sync read / write / truncate / flush / close. Fast path for engines.

Default rule (steal this)

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.

2. Opening the handle

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:

3. Sync method map

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.

Lock design-review trap

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).

4. Main thread still owns the product surface

UI, pickers, and many APIs stay on the main thread. Pattern:

  1. Main: new Worker("io.js"), postMessage commands/payloads.
  2. Worker: open OPFS root → file handle → sync access handle → do work → close → reply.
  3. Export to the user-visible FS uses showSaveFilePicker on the main thread (not available in workers); copy bytes after the worker has closed its access handle (web.dev).

5. Practice — pick the path

Choose the best default. Equal-length options so formatting doesn’t hint. Feedback is immediate.

Scenario A

You need maximum-throughput random byte I/O on an OPFS file for a Wasm DB engine.

Scenario B

On the main thread, save a short JSON draft string into an OPFS file the user never sees.

Scenario C

A worker already holds a default-mode sync access handle on db.bin. Another context tries to open the same file for exclusive write.

Scenario D

Inside a worker, append UTF-8 text to an open sync access handle, then free the file for others.

Scenario E

After a worker finished heavy OPFS work, the product must offer “Save as…” into the user’s Downloads via a picker.

6. What to remember

Ask your teacher Fuzzy bits — lock modes, Safari quirks, wiring a worker message protocol — ask in chat. Follow-ups are part of the method.

Primary source (read next)

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).