Bug Description
JdbFactoryWeb.start() in web_interop.dart line 35 uses a force-unwrap (!) on a map lookup that can return null:
var list = databases[notificationRevision.name]!;
The databases map (defined in JdbFactoryIdb) is local to each tab's JdbFactoryIdb instance. When Tab B writes to a database, sembast broadcasts the revision via BroadcastChannel('sembast_web_storage_revision'). Tab A receives it, but if Tab A hasn't opened that same database, the map lookup returns null and the ! crashes.
Steps to Reproduce
- Open the app in Tab 1, open database "alpha" (
databaseFactoryWeb.openDatabase('alpha'))
- Open the same app in Tab 2, open database "beta" (
databaseFactoryWeb.openDatabase('beta'))
- In Tab 2, write a record to database "beta"
- Tab 1 crashes with:
Null check operator used on a null value at web_interop.dart:35
Tab 1 only has "alpha" in its databases map but receives a BroadcastChannel notification for "beta".
Minimal Reproducible Example
pubspec.yaml:
dependencies:
flutter:
sdk: flutter
sembast_web: ^2.4.4
web: ^1.1.1
lib/main.dart:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sembast_web/sembast_web.dart';
void main() {
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
debugPrint('*** FlutterError: ${details.exception}');
};
runApp(const MyApp());
}, (error, stackTrace) {
debugPrint('*** Unhandled error: $error');
debugPrint('*** Stack trace: $stackTrace');
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'sembast_web BroadcastChannel Bug',
home: const BugReproPage(),
);
}
}
class BugReproPage extends StatefulWidget {
const BugReproPage({super.key});
@override
State<BugReproPage> createState() => _BugReproPageState();
}
class _BugReproPageState extends State<BugReproPage> {
Database? _dbAlpha;
Database? _dbBeta;
final _logs = <String>[];
void _log(String msg) {
if (!mounted) return;
setState(() => _logs.add('[${DateTime.now().toIso8601String().substring(11, 19)}] $msg'));
}
Future<void> _openAlpha() async {
_dbAlpha = await databaseFactoryWeb.openDatabase('alpha');
_log('Opened "alpha"');
}
Future<void> _openBeta() async {
_dbBeta = await databaseFactoryWeb.openDatabase('beta');
_log('Opened "beta"');
}
Future<void> _writeBeta() async {
if (_dbBeta == null) { _log('Open beta first!'); return; }
final store = StoreRef<String, String>.main();
await store.record('key_${DateTime.now().millisecondsSinceEpoch}').put(_dbBeta!, 'value');
_log('Wrote to "beta" — check other tab for crash');
}
@override
void dispose() { _dbAlpha?.close(); _dbBeta?.close(); super.dispose(); }
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
Wrap(spacing: 8, children: [
ElevatedButton(onPressed: _openAlpha, child: const Text('Open Alpha')),
ElevatedButton(onPressed: _openBeta, child: const Text('Open Beta')),
ElevatedButton(onPressed: _writeBeta, child: const Text('Write Beta')),
]),
Expanded(child: SelectableText(_logs.join('\n'))),
]),
);
}
}
Error Output
*** Unhandled error: Unexpected null value.
*** Stack trace:
package:sembast_web/src/web_interop.dart 35:54 <fn>
dart:async/zone.dart 1410:47 _rootRunUnary
...
Suggested Fix
Replace the force-unwrap with a null-safe lookup and skip unknown databases:
@override
void start() {
stop();
_revisionSubscription = notificationRevisionStream.listen((
notificationRevision,
) {
var list = databases[notificationRevision.name];
if (list == null) return; // Database not open in this tab, ignore
for (var jdbDatabase in list) {
jdbDatabase.addRevision(notificationRevision.revision);
}
});
}
Environment
- sembast_web: 2.4.4
- Flutter: 3.32.4 (web)
- Dart: 3.10.3
- Browsers: Chrome, Safari, Firefox (all affected)
Real-World Impact
In our production app (~102 errors/24h), this happens when a user has the app open in two tabs. Each tab opens different sembast databases (e.g., different cache databases). A write in one tab triggers a BroadcastChannel notification that crashes the other tab.
In dart2js production builds, the ! assertion is optimized away, so instead of a clear "Null check operator" error, it manifests as TypeError: Cannot read properties of null (reading 'length') on the for loop iterating the null list.
Bug Description
JdbFactoryWeb.start()inweb_interop.dartline 35 uses a force-unwrap (!) on a map lookup that can return null:The
databasesmap (defined inJdbFactoryIdb) is local to each tab'sJdbFactoryIdbinstance. When Tab B writes to a database, sembast broadcasts the revision viaBroadcastChannel('sembast_web_storage_revision'). Tab A receives it, but if Tab A hasn't opened that same database, the map lookup returns null and the!crashes.Steps to Reproduce
databaseFactoryWeb.openDatabase('alpha'))databaseFactoryWeb.openDatabase('beta'))Null check operator used on a null valueatweb_interop.dart:35Tab 1 only has "alpha" in its
databasesmap but receives aBroadcastChannelnotification for "beta".Minimal Reproducible Example
pubspec.yaml:
lib/main.dart:
Error Output
Suggested Fix
Replace the force-unwrap with a null-safe lookup and skip unknown databases:
Environment
Real-World Impact
In our production app (~102 errors/24h), this happens when a user has the app open in two tabs. Each tab opens different sembast databases (e.g., different cache databases). A write in one tab triggers a
BroadcastChannelnotification that crashes the other tab.In dart2js production builds, the
!assertion is optimized away, so instead of a clear "Null check operator" error, it manifests asTypeError: Cannot read properties of null (reading 'length')on theforloop iterating the null list.