From 0189b9cd27dae32e0fb317a5f130980889816541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20=C5=81a=C5=9B?= Date: Wed, 13 May 2026 10:47:43 +0200 Subject: [PATCH] fix(web): swallow non-UpdateNotification cross-tab broadcast payloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BroadcastUpdates.get updates` chains `.map(...) → .where(...) → .cast()`. After `.where()` removes nulls, the source element type is still `UpdateNotification?`, and a bare `.cast()` lets DDC infer the cast target from the source rather than the declared `Stream` return type. A peer message that survives `.where()` but isn't an `UpdateNotification` (legacy storage mode, SharedWorker fallback, non-conforming `BroadcastChannel` payload) then throws. Because the downstream `.listen()` in `async_web_database.dart` has no `onError` handler, the cast failure escalates to an uncaught error on the root zone. On Flutter web this surfaces as a `DartError` popup. Two surgical changes: - `broadcast_updates.dart`: make the cast type argument explicit (`.cast()`) so type inference matches the declared return type. - `async_web_database.dart`: attach an `onError` handler on the broadcast subscription so any malformed peer payload degrades silently instead of crashing the host application. No API surface change. Bumped to `0.14.2-wip` per DEVELOPING.md. --- packages/sqlite_async/CHANGELOG.md | 8 ++++++++ .../lib/src/web/database/async_web_database.dart | 6 ++++++ .../lib/src/web/database/broadcast_updates.dart | 2 +- packages/sqlite_async/pubspec.yaml | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index 9d5802d..4737282 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.14.2-wip + +- Web: fix uncaught error from `BroadcastUpdates` when a peer publishes a + message that does not deserialise into an `UpdateNotification`. The bare + `.cast()` after `.where()` is now typed explicitly, and a defensive + `onError` handler is attached to the broadcast subscription so malformed + peer payloads degrade silently instead of crashing the host application. + ## 0.14.1 - Fix update notifications sometimes firing before a write has completed. diff --git a/packages/sqlite_async/lib/src/web/database/async_web_database.dart b/packages/sqlite_async/lib/src/web/database/async_web_database.dart index 6084c66..9ca6bff 100644 --- a/packages/sqlite_async/lib/src/web/database/async_web_database.dart +++ b/packages/sqlite_async/lib/src/web/database/async_web_database.dart @@ -70,6 +70,12 @@ final class AsyncWebDatabaseImpl extends SqliteDatabaseImpl _broadcastUpdatesSubscription = broadcastUpdates.updates.listen((updates) { updatesController.add(updates); + }, onError: (Object error, StackTrace stackTrace) { + // Cross-tab BroadcastChannel may deliver payloads that do not + // deserialise into an UpdateNotification (legacy storage mode, + // malformed peer messages, SharedWorker fallback). Drop them + // rather than crashing the host application via an uncaught + // error on the root zone. }); } } diff --git a/packages/sqlite_async/lib/src/web/database/broadcast_updates.dart b/packages/sqlite_async/lib/src/web/database/broadcast_updates.dart index 5514e74..9ece628 100644 --- a/packages/sqlite_async/lib/src/web/database/broadcast_updates.dart +++ b/packages/sqlite_async/lib/src/web/database/broadcast_updates.dart @@ -25,7 +25,7 @@ class BroadcastUpdates { } }) .where((e) => e != null) - .cast(); + .cast(); } void send(UpdateNotification notification) { diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 2a6e2b9..c4e2a32 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite_async description: High-performance asynchronous interface for SQLite on Dart and Flutter. -version: 0.14.1 +version: 0.14.2-wip resolution: workspace repository: https://github.com/powersync-ja/sqlite_async.dart environment: