DEV Community

Ekong Ikpe
Ekong Ikpe

Posted on

Gnoke SaveNative: A Durability Layer for the File System Access API on Mobile Browsers

I was building offline-first apps for the Gnoke Suite and ran into the same wall every time.

The File System Access API works beautifully — until your phone decides otherwise.

A background OS kill. A tab reload at the wrong moment. Ten concurrent writes racing for the same stream. Any of these silently drops your data. No error. No warning. Just gone.

The browser fantasy is: pick a folder, write files, done.

The mobile reality is: your process dies, your handle goes stale, and your writable stream errors out mid-write. 😬

So I built a survival layer around it.


Meet gnoke-savenative

A two-layer write pipeline for browser apps. Native filesystem first, IndexedDB shelf as instant fallback.

npm install gnoke-savenative
Enter fullscreen mode Exit fullscreen mode
import { saveNative } from 'gnoke-savenative';
import { openDB }     from 'idb';

// Mount once (user gesture required)
const handle = await saveNative.mount(openDB);
let workspace = { handle, db: await saveNative._db(openDB) };

// Write — native first, shelf if it fails
await saveNative.write(workspace, 'notes.txt', 'Hello world');

// After reload — wake restores handle and auto-flushes shelf
workspace = await saveNative.wake(openDB);
Enter fullscreen mode Exit fullscreen mode

The Survival Loop

Every write has a guaranteed outcome — it's either on disk or in the shelf.

write()
  ↓ native success → file on disk ✓
  ↓ native failure → shelved in IndexedDB

wake() after reload
  ↓ handle restored
  ↓ _flush() drains shelf → file on disk ✓
Enter fullscreen mode Exit fullscreen mode

Writes are never dropped — only delayed. Recovery is automatic. Visibility is optional via hooks.


What makes it mobile-ready 📱

On desktop, the File System Access API mostly just works. On mobile (tested on Infinix Android, Chrome), three things will break a naive implementation:

1. OS background kills

The browser process dies. The handle survives in IndexedDB. But the write that was in-flight is gone. The shelf catches it.

2. Stale writable streams

Open a stream, come back after the OS has changed something on disk — the stream errors. Every write goes through a fresh createWritable() call.

3. Concurrent write races — this is where most implementations silently fail 🤷

The File System Access API does not serialize concurrent writes to the same file. Fire ten writes at once and they fight over the same stream — most fail with no error. gnoke-savenative maintains a per-filename queue so writes process in strict order. This is not retry logic — it's guaranteed ordering backed by a persistent fallback. v0.1.1 was specifically a concurrency patch after stress testing exposed false shelf activations on clean writes.


The API

saveNative.mount(openDB)                    // pick folder, stash handle
saveNative.wake(openDB)                     // restore handle, auto-flush shelf
saveNative.write(workspace, name, content)  // queued write with shelf fallback
Enter fullscreen mode Exit fullscreen mode

Optional hooks for UI feedback:

saveNative.onWriteFailure  = (name, err)   => { /* shelved */ };
saveNative.onFlushProgress = (done, total) => { /* draining */ };
saveNative.onFlushComplete = (count)       => { /* recovered */ };
Enter fullscreen mode Exit fullscreen mode

How it was built

This came out of a real stress test — a Ghost Editor testbench on a real Infinix device, with hard reloads, app switches, and 10 concurrent writes fired at once.

The pattern is essentially a write-ahead buffer with eventual durability: attempt the native write, fall back to the shelf on failure, replay on wake. The same principle behind WAL in database engines — applied to the browser filesystem. 🧠

v0.1 proved the shelf worked.

v0.1.1 eliminated false shelf activations under concurrent writes.

After the stress test showed zero shelf activations on clean concurrent writes, it was ready to ship. ✅


Try it

👉 github.com/edmundsparrow/gnoke-savenative

Zero dependencies (brings your own openDB). MIT licensed. Vanilla JS ES module.

Drop it in any project via CDN — no npm, no build step:

<!-- ES module -->
<script type="module">
  import { saveNative } from 'https://cdn.jsdelivr.net/gh/edmundsparrow/gnoke-savenative/gnoke-savenative.js';
  import { openDB }     from 'https://unpkg.com/idb?module';
</script>

<!-- Or plain script tag — window.saveNative available globally -->
<script src="https://cdn.jsdelivr.net/gh/edmundsparrow/gnoke-savenative/gnoke-savenative.js"></script>
Enter fullscreen mode Exit fullscreen mode

— Edmund Sparrow, Gnoke Suite

Top comments (1)

Collapse
 
edmundsparrow profile image
Ekong Ikpe • Edited

Why this matters

If you're using the File System Access API on mobile, you're not actually getting reliable disk writes — you're getting best-effort I/O under an unstable lifecycle.

Tabs get killed. Writes get interrupted. Concurrent operations collide. And when it fails, it often fails silently.

That means your app can lose user data without throwing a single error.

Gnoke-savenative exists to fix that gap — turning unsafe writes into a controlled, recoverable pipeline.

Desktop success is not a durability guarantee on mobile.