feat(#335): prove drift/SQLite-WASM works in a real browser

Step 1 gate, web half. Building is not evidence, so this was run in a browser.

Assets: sqlite3.wasm (748 KB) and drift_worker.js (355 KB), both taken from
the SAME drift-2.34.2 release so they are built against each other. Validated
before use - the wasm has correct WebAssembly magic (0061 736d) and carries
SQLite 3.53.3; the worker is genuine compiled Dart JS.

Note the filename: the release ships `drift_worker.js`, not the
`drift_worker.dart.js` the docs name. The database URI was corrected to match,
which would otherwise have been a silent 404 at runtime.

Browser result, served over HTTP with COOP/COEP and Content-Type:
application/wasm:

  Using WasmStorageImplementation.opfsLocks
  PROBE-OK open+write+read
  PROBE-OK 6MiB round-trip (localStorage cap is 5MiB)

drift selected OPFS, the top storage tier, and round-tripped 6 MiB - data the
current localStorage-backed web build physically cannot hold. No exceptions.

Adds web/_headers for Cloudflare Pages: forces application/wasm on the wasm
asset and sets COOP/COEP for cross-origin isolation. Verified to ship into
build/web. Without COOP/COEP drift still works, falling back to IndexedDB,
which is slower but still unbounded next to localStorage - a performance tier,
not a correctness requirement.

web_probe/ is a standalone entrypoint that exercises ONLY the drift stack, so
a pass or fail is unambiguous without BLE and the rest of the app in the way.
It is not part of the app build.

Still no user data touched and no store switched over. The migration is the
next step.

flutter analyze clean, dart format clean, 497 tests pass.
integration/290-306-335
Strycher 23 hours ago
parent 19858698ee
commit 847a28df3e

@ -31,7 +31,11 @@ class OffbandDatabase extends _$OffbandDatabase {
name: 'offband_store',
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.dart.js'),
// Filename matches the asset published by the drift release, which is
// `drift_worker.js` not the `drift_worker.dart.js` the older docs
// name. Both assets are taken from the SAME drift release so they are
// built against each other.
driftWorker: Uri.parse('drift_worker.js'),
),
);
}

@ -0,0 +1,28 @@
# Cloudflare Pages response headers (#335, web storage via drift/SQLite-WASM).
#
# Syntax: a URL pattern, then indented headers. Cloudflare Pages reads this
# file from the build output and can add, override or remove response headers.
#
# Two things drift needs on the web:
#
# 1. sqlite3.wasm MUST be served as Content-Type: application/wasm, or
# WebAssembly streaming compilation refuses it.
#
# 2. COOP + COEP (cross-origin isolation) unlock drift's best storage backend,
# OPFS (`opfsLocks`). Verified locally: with these headers drift logs
# "Using WasmStorageImplementation.opfsLocks". WITHOUT them it still works,
# falling back to IndexedDB, which is slower but still unbounded compared to
# localStorage's 5 MiB cap. So these are a performance tier, not a
# correctness requirement.
#
# Verify after the first deploy:
# curl -sI https://<host>/sqlite3.wasm | grep -i 'content-type'
# -> expect: content-type: application/wasm
# and check the browser console for the opfsLocks line above.
/sqlite3.wasm
Content-Type: application/wasm
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -0,0 +1,87 @@
// Standalone web probe for #335.
//
// Loads ONLY the drift stack, so a pass or fail is unambiguous: it exercises
// sqlite3.wasm + drift_worker.js in a real browser without the rest of the app
// (BLE, permissions, radio) confusing the result.
//
// Renders the outcome into the DOM so the result is readable without a
// debugger attached.
import 'package:flutter/material.dart';
import 'package:meshcore_open/storage/drift/offband_database.dart';
void main() => runApp(const _ProbeApp());
class _ProbeApp extends StatelessWidget {
const _ProbeApp();
@override
Widget build(BuildContext context) => const MaterialApp(
home: Scaffold(body: Center(child: _Probe())),
);
}
class _Probe extends StatefulWidget {
const _Probe();
@override
State<_Probe> createState() => _ProbeState();
}
class _ProbeState extends State<_Probe> {
String _status = 'running…';
@override
void initState() {
super.initState();
_run();
}
Future<void> _run() async {
final lines = <String>[];
OffbandDatabase? db;
try {
db = OffbandDatabase();
final ok = await db.verifyReadWrite();
final r1 = ok
? 'PROBE-OK open+write+read'
: 'PROBE-FAIL readback mismatch';
lines.add(r1);
// ignore: avoid_print
print(r1);
// The point of the migration: exceed the 5 MiB localStorage ceiling.
final big = 'x' * (6 * 1024 * 1024);
await db
.into(db.storedBlobs)
.insertOnConflictUpdate(
StoredBlobsCompanion.insert(key: 'big', value: big),
);
final row = await (db.select(
db.storedBlobs,
)..where((t) => t.key.equals('big'))).getSingle();
final r2 = row.value.length == big.length
? 'PROBE-OK 6MiB round-trip (localStorage cap is 5MiB)'
: 'PROBE-FAIL 6MiB truncated to ${row.value.length}';
lines.add(r2);
// ignore: avoid_print
print(r2);
await (db.delete(db.storedBlobs)..where((t) => t.key.equals('big'))).go();
} catch (e) {
lines.add('PROBE-FAIL $e');
// ignore: avoid_print
print('PROBE-FAIL $e');
} finally {
await db?.close();
}
if (mounted) setState(() => _status = lines.join('\n'));
}
@override
Widget build(BuildContext context) => Text(
_status,
key: const Key('probe-result'),
textAlign: TextAlign.center,
);
}
Loading…
Cancel
Save

Powered by TurnKey Linux.