Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

BLite.Wasm: a committed write is lost on a page reload unless a checkpoint ran, and CheckpointAsync does not reliably prevent it

Open
#289 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
csharp, wasm

Research direction

Start with the browser harness tests in tests/BLite.Wasm.BrowserTests, especially the Committed_write_survives_reload rows and the large-database checkpoint case. Trace recovery ordering through src/BLite.Wasm/BLiteWasm.cs, browser page flushing in src/BLite.Wasm/Storage/IndexedDbPageStorage.cs and OpfsPageStorage.cs, and checkpoint coordination in src/BLite.Core/Storage/StorageEngine.Recovery.cs. Done means acknowledged IndexedDB and OPFS writes survive reloads, including while a background checkpoint is running, and the affected known-candidate tests pass.

Written by the indexing model from the issue text.

Description

bug package/core severity/critical

A document inserted through the auto-commit API on IndexedDB or OPFS, with the engine then left open or disposed, is gone after a page reload. The WAL still holds the transaction's records; the reopen replays them, truncates the WAL, and the collection holds 0 documents. A second reopen finds 0 too, so the loss is permanent. Only CheckpointAsync before the reload keeps the document, and past 16 MB of WAL even that fails in half the runs, because CheckpointAsync returns at once while a background checkpoint is running.

WASM_SUPPORT.md calls the browser WALs "WAL for crash recovery in browser contexts" and the sample's counter "persistent": "Reload the page to verify that the count survives browser restarts." BLiteWasm.CreateAsync says the same of its recovery call (src/BLite.Wasm/BLiteWasm.cs:121-123: "recover committed-but-not-checkpointed transactions so data survives page reloads"). README "Transactions & ACID": "Durable: WAL ensures data safety even in power loss."

Found by the browser harness (tests/BLite.Wasm.BrowserTests, https://github.com/EntglDb/BLite/pull/295), which carries the affected rows as known candidate BW1 until this is fixed. Same result in all four publish variants (trimmed, TrimMode=full, untrimmed, AOT), so it is not a trimming problem.

Repro

Rows Committed_write_survives_reload IndexedDb/commit-only, IndexedDb/dispose, Opfs/commit-only, Opfs/dispose and Typed_write_survives_reload */checkpoint=False/indexed=False (6 of 6 fail every run); Large_database_open_time_and_memory */5MB (fails in 6 of 12 runs).

// tab, first load
var engine = await BLiteWasm.CreateAsync("durable", WasmStorageBackend.IndexedDb);
await engine.InsertAsync("docs", engine.CreateDocument(["name", "v"], b => b.AddString("name", "m1").AddInt32("v", 1)));
// leave it open (or engine.Dispose()); reload the page

// tab, second load
var engine = await BLiteWasm.CreateAsync("durable", WasmStorageBackend.IndexedDb);
await foreach (var doc in engine.FindAllAsync("docs")) ...   // nothing
dotnet test tests/BLite.Wasm.BrowserTests --filter "FullyQualifiedName~Committed_write_survives_reload"

The IndexedDB object-store counts the test prints: meta=1 pages=3 wal=4 before the reload, pages=5 wal=0 after the reopen, 0 documents. With CheckpointAsync before the reload the marker survives.

For the 5 MB rows: 5,242 documents of 1,000 bytes through InsertBulkAsync, CheckpointAsync, reload, reopen. In 6 of 12 runs CheckpointAsync returned in 0 ms with 1,089–1,119 records still in the WAL, and the reopened database held 0 documents; in the 6 runs where it took 95–135 ms all documents were there.

Expected: what InsertAsync acknowledged is there after the reload, as on desktop, where the same sequence survives a process exit.

Actual: the WAL is replayed and truncated, and the data is gone.

Mechanism (verified on 0450b97; the first part is an inference consistent with the page and WAL counts, no test isolates it)

  • Both browser page stores keep every page in a managed cache. Their synchronous Flush() is a no-op (src/BLite.Wasm/Storage/IndexedDbPageStorage.cs:216, OpfsPageStorage.cs:217); pages reach IndexedDB or OPFS only in FlushAsync, which only a checkpoint calls.
  • The engine writes state outside the WAL and relies on it being durable at once: page allocation and the next page id, the header, the key dictionary, through WritePageImmediate (src/BLite.Core/Storage/StorageEngine.Pages.cs:135). On desktop PageFile.WritePage is durable when it returns; in the browser these writes stay in the cache until the next checkpoint. After a reload the replayed page images refer to pages and dictionary keys that were never persisted (keyMapHasName=False in the test's output after the reopen), and RecoverAsync then truncates the WAL that held the data.
  • StorageEngine(IPageStorage, IWriteAheadLog) documents that "the caller is responsible for replaying WAL before constructing the engine" (src/BLite.Core/Storage/StorageEngine.cs:420-422) and reads the dictionary and KV roots in its constructor; BLiteWasm.CreateAsync constructs first and calls RecoverAsync after (src/BLite.Wasm/BLiteWasm.cs:118-125).
  • Dispose drains the group-commit writer and checkpoints only when pages are queued to be freed (StorageEngine.cs:462-498); it does not flush committed pages, so a disposed engine loses them the same way.
  • Past MaxWalSize (16 MB, StorageEngine.cs:124) a commit starts a background checkpoint (_ = Task.Run(() => CheckpointAsync()), StorageEngine.GroupCommit.cs:292). While it runs, an explicit CheckpointAsync returns at once (StorageEngine.Recovery.cs:59, _checkpointRunning), although BLiteEngine.CheckpointAsync promises to "guarantee that the page file is fully up-to-date on disk" (src/BLite.Core/BLiteEngine.cs:440-442). That is the 0 ms checkpoint of the 5 MB rows; the memory side of it is #292.

This is the browser face of the class behind #260, #261 and #262: writes the engine makes outside the WAL. On desktop they are durable at once, so a crash leaks pages (#261) or splits a two-write update (#262); in the browser they are not durable at all until a checkpoint, so a reload loses the committed data they describe.

Affected versions: BLite.Wasm since it was added in #63 (830e83e, v4.4.0) through v5.2.3. The background checkpoint since group commit (d72b73a, v3.6.2).

Fix considerations

  • Make the browser WAL the only thing a reload needs: either replay the WAL before the engine constructor reads anything (the order the constructor documents), and make every write that bypasses the WAL (WritePageImmediate, allocation, header) go through the WAL or through FlushAsync before the commit is acknowledged; or checkpoint on every commit in the browser (the WAL is in IndexedDB already, so a commit costs one IndexedDB transaction either way).
  • CheckpointAsync should wait for a running checkpoint and then run its own (or wait and return once the running one covers its commits), so that its documented guarantee holds; that applies on desktop too.
  • Dispose on a browser backend should flush committed pages, or the docs should say that a tab must call CheckpointAsync before it closes.
  • Test: the harness's reload rows, which stay KNOWN until the entry is removed.

Related: #260, #261, #262 (the same class on desktop, found by the WAL crash test), #209 / #242 (checkpoint on dispose for queued frees), #57 / #63 (BLite.Wasm). Found with #290, #291, #292 and #293 by the same harness.

Dominant language
C#
Stars
66
Forks
6
Avg merge
1d 2h
Merged PRs (30d)
58

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from EntglDb/BLite

All issues in EntglDb/BLite

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.