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
parent
19858698ee
commit
847a28df3e
@ -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…
Reference in new issue