Conversation
Implement initial QUIC-over-XMPP transport integration following the XEP-0467 design direction for a single reliable channel, with automatic endpoint selection via DNS SRV and fallback through existing TCP SRV logic. This adds QUIC endpoint planning and socket integration at the app layer, extends vendored xmpp_stone connection settings/APIs to model and attempt QUIC endpoints first, and preserves existing TCP failover behavior when QUIC attempts fail. It also records partial XEP-0467 support in doap.xml and includes the QUIC implementation plan document. Tests and validation run: - dart format on all touched Dart files - flutter analyze - flutter test - (cd vendor/xmpp_stone && dart test) New tests added: - test/quic_endpoint_plan_test.dart - vendor/xmpp_stone/test/connection_quic_failover_test.dart
Re-enable Linux QUIC transport usage and keep flutter_quic as a vendored path dependency so plugin metadata is controlled in-repo. This keeps QUIC active on Linux, Windows, and Android while preserving the existing TCP fallback behavior. Update CI and release workflows to provision Rust toolchains and required targets for flutter_quic native builds, including cargo-ndk for Android cross-compilation. Validation run before commit: - flutter analyze - flutter test - (cd vendor/xmpp_stone && dart test)
Negotiate a conservative QUIC stream limit of 25 and add post-bind stream multiplexing in the client transport. After resource binding is detected on the control stream, the QUIC socket now proactively opens up to 20 auxiliary bidirectional streams and keeps a dedicated control stream for account-scoped traffic. Outgoing stanza routing is deterministic by bare JID: - stanzas addressed to the account bare JID stay on the original control stream - stanzas to other entities are mapped via stable hash to one of 20 auxiliary stream slots Added routing helper tests for bare-JID extraction/normalization and deterministic slot bounds. Tested with: - flutter analyze - flutter test - (cd vendor/xmpp_stone && dart test)
…late The foreground task plugin requires a top-level function annotated with @pragma('vm:entry-point') to be used as the background isolate entry point. Previously, setTaskHandler was called inside an instance method (_startAndroidForegroundService), which meant the background isolate could not find or invoke it, resulting in: MissingPluginException: No implementation found for method start on channel flutter_foreground_task/background Fix by: - Adding a top-level startCallback() function annotated with @pragma('vm:entry-point') that calls setTaskHandler with WimsyForegroundTaskHandler. - Removing the misplaced setTaskHandler call from the instance method. - Passing startCallback as the callback parameter to startService(), so the plugin registers the correct entry point for the background isolate. Fixes WIMSY-6 Tested by running flutter analyze (no new issues), flutter test (all pass except pre-existing quic_happy_eyeballs_test.dart which references a missing file unrelated to this change), and dart test in vendor/xmpp_stone (all 64 tests pass). Co-authored-by: Junie <junie@jetbrains.com>
- Guard Log.logLevel = LogLevel.VERBOSE and Log.logXmpp = true behind an assert() block so verbose XMPP XML logging is only active in debug/profile builds and never leaks stanza content in release builds. - Replace DateTime.now().millisecondsSinceEpoch.remainder(1 << 31) with bareJid.hashCode.abs() % (1 << 31) in both _handleIncomingMessage and _handleIncomingRoomMessage. The deterministic ID means rapid messages from the same contact update the same notification rather than stacking new ones, fixing the grouped-dismiss bug. Tested: flutter analyze (clean), flutter test (145/145 passed), dart test in vendor/xmpp_stone (65/65 passed). Co-authored-by: Junie <junie@jetbrains.com>
ChatMessage gains a copyWith({...}) method covering all 28 fields. A
sentinel object (_absent) is used for nullable parameters so callers can
explicitly clear a field (pass null) vs. leave it unchanged (omit the
argument).
All five mutation sites that previously repeated the full 28-field
constructor are replaced with copyWith:
- lib/xmpp/chat_message_mutations.dart: applyCorrectionInList and
updateReactionsInList (2 sites)
- lib/xmpp/mam_merge_engine.dart: messageId-match merge and
heuristic body/time-window merge (2 sites)
- lib/xmpp/xmpp_service.dart: incoming-message dedup update,
room-message dedup update, _updateFileTransferMessage,
_updateOutgoingStatus, _updateOutgoingRoomStatus (5 sites)
The heuristic merge in mam_merge_engine previously omitted messageId
from the constructor (a latent bug — the field silently became null).
copyWith correctly preserves it; the characterization test in
mam_reliability_regression_test.dart is updated to assert the correct
behaviour.
Tested: flutter analyze (clean), flutter test (145/145 passed),
dart test in vendor/xmpp_stone (65/65 passed).
Co-authored-by: Junie <junie@jetbrains.com>
All 14 scattered SharedPreferences.getInstance() call sites in lib/main.dart are replaced with a single PreferencesService instance that is created once in main() and passed down through the widget tree. New file lib/storage/preferences_service.dart holds all five preference key constants (sentry_opt_in, wimsy_pin_ignored, wimsy_last_jid, wimsy_audio_input, wimsy_video_input) and provides typed getters and setters for each. WimsyApp, _Gatekeeper, WimsyHome, _PresenceMenu, _PinSetupScreen, and _PinUnlockScreen each receive PreferencesService via their constructors. The raw shared_preferences import is removed from main.dart. New test file test/preferences_service_test.dart covers all five preference groups (default values and round-trip get/set) using SharedPreferences.setMockInitialValues. test/widget_test.dart is updated to supply the required PreferencesService to WimsyApp. Tested: flutter analyze (clean), flutter test (156/156 passed), dart test in vendor/xmpp_stone (65/65 passed). Co-authored-by: Junie <junie@jetbrains.com>
Step 1 – Fix verbose logging and notification ID stability:
- Guard Log.logLevel/logXmpp behind assert() so verbose XMPP logging
is only active in debug/profile builds, never in release.
- Replace DateTime.now().millisecondsSinceEpoch.remainder(1<<31) with
bareJid.hashCode.abs() % (1<<31) for deterministic notification IDs,
preventing notification stacking for the same sender.
Step 2 – Add copyWith to ChatMessage and replace all manual copy sites:
- Added ChatMessage.copyWith({...}) with sentinel-object pattern for
nullable fields so callers can explicitly clear them to null.
- Replaced all manual full-constructor copy blocks in
mam_merge_engine.dart (2 sites), chat_message_mutations.dart (2 sites),
and xmpp_service.dart (4 sites) with copyWith calls.
- Updated mam_reliability_regression_test.dart: the heuristic-merge path
now correctly preserves messageId (copyWith keeps all unspecified fields),
fixing a latent bug where the old manual copy omitted messageId.
Step 3 – Centralise SharedPreferences into PreferencesService:
- New file lib/storage/preferences_service.dart with all key constants
and typed getters/setters (sentryOptIn, pinIgnored, lastJid,
audioInputId, videoInputId).
- Initialised once via PreferencesService.load() in main() and passed
down through WimsyApp → _Gatekeeper → WimsyHome → _PresenceMenu /
_PinSetupScreen / _PinUnlockScreen.
- Removed all 14 inline SharedPreferences.getInstance() call sites from
lib/main.dart; removed the now-unused shared_preferences import.
- Added test/preferences_service_test.dart: 10 unit tests covering all
five preference keys using SharedPreferences.setMockInitialValues.
- Updated test/widget_test.dart to pass a PreferencesService to WimsyApp.
Step 4 – Extract LoginScreen from _WimsyHomeState:
- New file lib/login_screen.dart containing LoginScreen stateful widget
with all login form state (7 controllers, endpoint discovery, advanced
options, connect logic).
- Removed _buildLogin, _handleConnect, _buildAdvancedOptions,
_scheduleEndpointDiscovery, _discoverEndpoint, _domainFromBareJid and
all login-only fields from _WimsyHomeState (~1100 lines removed).
- _WimsyHomeState.build now returns LoginScreen(...) when not connected.
Tested: flutter analyze (no issues), flutter test (156/156 pass),
dart test in vendor/xmpp_stone (65/65 pass).
Co-authored-by: Junie <junie@jetbrains.com>
- XEP-0388 (SASL2): upgraded status from 'partial' to 'complete'. The implementation now covers SCRAM authentication, user-agent binding, stream resumption inline, and IAP config-version pipelining (added on the quic branch and covered by sasl2_test.dart). - XEP-0467 (QUIC transport): remains 'partial' — the current implementation uses a single reliable bidirectional channel only; multi-channel and datagram usage are explicitly out of scope for this phase (see doc/plan-quic.md). Note: XmppFileTransferHandler extraction (also part of Step 5 in the plan) was deferred — the file-transfer code is spread across ~50 sites throughout xmpp_service.dart, deeply interleaved with IBB/Jingle event handlers, Sentry spans, and message persistence callbacks. A safe extraction requires a dedicated effort with broader test coverage and is tracked as a follow-up task. Tested: flutter analyze (no issues), flutter test (156/156 pass), dart test in vendor/xmpp_stone (65/65 pass). Co-authored-by: Junie <junie@jetbrains.com>
The copyWith method added in the cleanup pass had no direct test coverage despite its non-trivial sentinel-object pattern for nullable fields. New tests added (12 copyWith + 4 notification ID = 16 new cases): copyWith group: - 'returns identical field values when called with no arguments' — verifies all 29 fields are preserved when copyWith() is called with no arguments. - 'overrides scalar non-nullable fields' — from, to, body, outgoing, edited, acked, receiptReceived, displayed can all be changed independently. - 'overrides nullable String fields via sentinel' — messageId, mamId, stanzaId, oobUrl, oobDescription, rawXml, fileTransferId, fileName, fileMime, fileState, replyToId, replyToJid, replyFallback. - 'overrides nullable int fields via sentinel' — fileSize, fileBytes. - 'clears nullable fields to null via explicit null (sentinel pattern)' — passing null explicitly clears all 20 nullable fields; non-nullable fields are unaffected. - 'updates reactions map independently' — new reactions map is applied; original message is unaffected. - 'sets editedAt when marking a message as edited' — edited + editedAt updated together. - 'does not mutate the original message' — copyWith never modifies the receiver. notification ID stability group: - 'same JID always produces the same notification ID' — deterministic hash. - 'notification ID is non-negative' — abs() ensures no negative IDs. - 'notification ID is within 32-bit signed range' — result < 2^31. - 'different JIDs produce different notification IDs' — no collisions for typical JID strings. Tested: flutter analyze (no issues), flutter test (168/168 pass), dart test in vendor/xmpp_stone (65/65 pass). Co-authored-by: Junie <junie@jetbrains.com>
The existing mutation tests only covered the happy path. Seven new tests add coverage for the negative and boundary cases that are most likely to regress silently. New tests for applyCorrectionInList: - 'returns false when replaceId not found' — list is unchanged when no message matches the replacement ID. - 'returns false when sender bare JID does not match' — a correction from a different user (eve) is rejected even if the message ID matches. - 'returns false when exact sender does not match and matchSenderBare is false' — same bare JID but different resource must not match in strict mode. - 'applies correction to most-recent matching message' — when two messages share an ID the implementation iterates from the end; the last entry is corrected and the first is left untouched. New tests for updateReactionsInList: - 'returns false when stanza ID not found' — no mutation when the target stanza ID is absent from the list. - 'returns false for empty sender' — the empty-sender guard returns false immediately without touching the list. - 'matches by messageId when stanzaId absent' — reactions can be applied via messageId fallback when no stanzaId is set on the message. Tested: flutter analyze (no issues), flutter test (175/175 pass), dart test in vendor/xmpp_stone (65/65 pass). Co-authored-by: Junie <junie@jetbrains.com>
The existing merge engine tests only covered the four main happy/sad paths. Four new tests add coverage for boundary conditions that could silently regress. New tests: - 'does not merge by heuristic when body differs' — a MAM replay with a different body must not be merged into an existing local message even when sender, timestamp, and direction all match. - 'does not merge by heuristic when sender differs' — a message from a different JID is not merged even when body and timestamp match. - 'merges oobUrl and oobDescription via message-id match' — when a MAM replay carries oobDescription and rawXml, both are propagated into the existing message alongside mamId/stanzaId. - 'does not merge by message-id when both mamId and stanzaId already set' — once a message has both archive IDs the merge is skipped entirely, preventing accidental overwrite of already-resolved metadata. Tested: flutter analyze (no issues), flutter test (179/179 pass), dart test in vendor/xmpp_stone (65/65 pass). Co-authored-by: Junie <junie@jetbrains.com>
…IMSY-19) _ensureAuxStream could call connectionOpenBi on a Rust QUIC connection object that had already been disposed by a concurrent close() call. This caused a fatal DroppableDisposedException crash reported in Sentry (WIMSY-19). The fix adds two guards: 1. Check _closed alongside the _connection null-check before calling connectionOpenBi, so we never enter the FFI call if teardown has begun. 2. Re-check _closed after the await returns, since close() may have fired during the async gap while connectionOpenBi was in flight; if so, we throw a StateError (caught by the write() error handler) rather than storing the now-orphaned streams. Tested: flutter analyze (no issues), flutter test (179/179), dart test in vendor/xmpp_stone (65/65). Co-authored-by: Junie <junie@jetbrains.com>
Add debugPrint calls at three key points in the aux stream lifecycle: - Outbound open: logged in _ensureAuxStream after the channel is registered in _auxStreamsBySlot, showing the slot number. - Close: logged in close() before finishing aux send streams, showing the count and slot numbers of all channels being torn down. - Recv loop exit: logged in the finally block of _startRecvLoop for non-control streams, showing which slot's recv loop has ended. No inbound (server-initiated) aux stream acceptance exists in the current implementation, so only outbound opens are covered. Tested: flutter analyze clean; flutter test 179/179; dart test 65/65. Co-authored-by: Junie <junie@jetbrains.com>
The stream features response was arriving in two TCP/QUIC chunks. The old code in prepareStreamResponse appended </stream:stream> to the first (incomplete) chunk, producing malformed XML mid-attribute (e.g. 'https</stream:stream>://...'). A second broken buffering layer in handleResponse then tried to re-assemble using magic fixed offsets (strip 12 chars from start, 13 from end), which failed when the second chunk was a raw continuation rather than a fresh <xmpp_stone>-wrapped response. This caused the connection to hang on stream features and be timed out by the server after 20 seconds. Fix: consolidate all buffering into prepareStreamResponse using the existing (but previously unused) restOfResponse field. After combining any buffered data with the new chunk, attempt a full XML parse. If it fails (incomplete), save the raw combined data to restOfResponse and return '' so handleResponse has nothing to process. Only when the parse succeeds is the wrapped, complete XML emitted. restOfResponse is also reset in _attachOpenedSocket on reconnect. The broken _unparsedXmlResponse field and its magic-offset reassembly logic in handleResponse have been removed entirely. Tested: flutter analyze clean; flutter test 179/179; dart test 65/65. Co-authored-by: Junie <junie@jetbrains.com>
…(WIMSY-19) When close() set _connection = null, it dropped the last Dart-side strong reference to the RustArc, disposing it. Any concurrent _ensureAuxStream call that had already captured the connection in a local variable would then crash during connectionOpenBi's SSE argument encoding phase with DroppableDisposedException. Fix: do not null _connection in close(). The _closed flag already prevents any new use of the connection. The RustArc is released naturally once all local references (in-flight connectionOpenBi calls) drop. Since QuicCapableXmppSocket is not reused after close(), there is no risk of holding a stale reference across reconnects. Tested: flutter analyze (no issues), flutter test 179/179, dart test 65/65. Co-authored-by: Junie <junie@jetbrains.com>
XEP-0198 (urn:xmpp:sm:3) is incompatible with QUIC (XEP-0467): QUIC provides its own reliable, ordered delivery at the transport layer, so stream management's stanza acknowledgement and resumption are redundant and can cause connection hangs. Changes: - Add 'bool get isQuic => false' to XmppWebSocket abstract base class - Override 'isQuic => true' in QuicCapableXmppSocket - Expose 'bool get isQuic' on Connection via '_socket?.isQuic ?? false' - Guard StreamManagementModule.negotiate() and isReady() to return early/false when _connection.isQuic - Add '@OverRide' annotation to QuicCapableXmppSocket.write() - Add 'bool get isQuic => false' to all test fake socket classes that use 'implements XmppWebSocket' (9 files) Tested: flutter analyze (no issues), flutter test (179/179), dart test in vendor/xmpp_stone (65/65). Co-authored-by: Junie <junie@jetbrains.com>
…IMSY-19) The crash occurred when two concurrent callers of _ensureAuxStream (e.g. _startAuxStreamsOpen and a write() call via _selectSendTarget) both captured the same _connection RustArc and both attempted to call connectionOpenBi. The FRB-generated binding transfers the arc via Auto_Owned (consuming it), so the second caller tried to encode an already-disposed arc, causing DroppableDisposedException during SSE argument encoding. Fix: split _ensureAuxStream into a fast synchronous check plus a new _openAuxStream async method. An _auxStreamOpening map caches the in-flight Future per slot; putIfAbsent ensures only one connectionOpenBi call runs at a time per slot, and concurrent callers await the same Future. The in-flight entry is removed in a finally block so retries are possible after failure. _auxStreamOpening is also cleared in close() for hygiene. Tested: flutter analyze clean; flutter test 179/179; dart test 65/65. Co-authored-by: Junie <junie@jetbrains.com>
…leXmppSocket Log 'QUIC aux stream opening slot=N reason=...' immediately before calling connectionOpenBi, so that if the open fails or causes a crash the log shows both which slot was being opened and why: - Post-bind pre-open (from _startAuxStreamsOpen): reason is 'post-bind pre-open slot N of M' - On-demand routing (from _getSendTarget): reason is 'on-demand routing for <bareJid>' The reason is threaded through _ensureAuxStream and _openAuxStream as an optional/named parameter; coalesced concurrent opens for the same slot preserve the reason of the first caller that triggered the open. Tested: flutter analyze clean, flutter test 179/179 pass. Co-authored-by: Junie <junie@jetbrains.com>
…(WIMSY-19) connectionOpenBi uses Auto_Owned FFI transfer: it consumes the RustArc passed as _connection and returns a new one. The previous per-slot coalescing (_auxStreamOpening map) only prevented concurrent opens for the *same* slot. Two opens for *different* slots could still run concurrently, both capturing the same (about-to-be-consumed) arc; the second call would then crash with DroppableDisposedException. Fix: introduce a global async mutex (_auxOpenLock, a chained Future) that serialises all connectionOpenBi calls regardless of slot. Each _openAuxStream call chains onto the previous lock future, reads _connection only after the previous call has completed and updated it, then releases the lock in a finally block. _auxOpenLock is also reset in close() so a reconnect does not wait on a stale chain. Tested: flutter analyze clean, flutter test 179/179 pass. Co-authored-by: Junie <junie@jetbrains.com>
Previously _startAuxStreamsOpen opened aux streams sequentially in a single async loop, blocking each slot open on the previous one. This meant the connection was effectively stalled waiting for all aux streams to be established before proceeding with normal XMPP traffic. Now each slot is fired as an independent unawaited async task. The global _auxOpenLock mutex in _openAuxStream still serialises the underlying connectionOpenBi FFI calls (which use Auto_Owned arc transfer), so there is no race — the tasks simply queue up on the lock rather than blocking the caller. The main (control) stream continues to be used for all connection-state work and for traffic to/from the user's own bare JID, as enforced by the existing _postBindReady and _accountBareJid guards in _selectSendTarget. Tested: flutter analyze clean, flutter test 179/179 pass, dart test vendor/xmpp_stone 65/65 pass. Co-authored-by: Junie <junie@jetbrains.com>
Adds a 'Use QUIC transport (XEP-0467)' checkbox to the advanced options section of the login screen, defaulting to enabled. When disabled, QUIC SRV discovery is skipped and no QUIC endpoints are built, so the connection falls back to plain TCP — allowing QUIC to be isolated as the cause of post-bind stalls without changing any other settings. Changes: - AccountRecord: new useQuic field (default true), persisted in toMap / fromMap (old accounts without the key default to true). - XmppService.connect(): new useQuic parameter (default true) gates both the QUIC SRV lookup and the quicEndpoints assignment. - LoginScreen: _useQuic state loaded from stored account; passed to AccountRecord and service.connect(); checkbox rendered in advanced options between Resource and WebSocket, disabled on web. Tested: flutter analyze clean, flutter test 179/179 pass, dart test vendor/xmpp_stone 65/65 pass. Co-authored-by: Junie <junie@jetbrains.com>
When using QUIC transport, StreamManagementModule.negotiate() was returning early without setting NegotiatorState.DONE. This left the ConnectionNegotiatorManager's negotiator queue permanently stalled: stateListener() was never called, negotiateNextFeature() was never triggered, doneParsingFeatures() was never called, and consequently XmppConnectionState.Ready was never set. The result was that all post-bind activity (presence, carbons enable, roster fetch, MAM catch-up) never happened — the connection appeared to hang silently after resource binding. Fix: emit NegotiatorState.DONE before returning in the QUIC early-exit path, so the queue advances normally and doneParsingFeatures() fires. Tested: flutter analyze clean, flutter test 179/179 pass, dart test vendor/xmpp_stone 65/65 pass. Co-authored-by: Junie <junie@jetbrains.com>
…ady() When using QUIC transport, XEP-0198 Stream Management is intentionally skipped (QUIC provides its own reliability). The negotiate() method already handled this correctly by immediately setting state = DONE and returning early. However, isReady() was returning false for QUIC connections, which caused pickNextNegotiator() in ConnectionNegotiatorManager to skip the SM negotiator entirely via firstWhere(). Since negotiate() was never called, the state never advanced to DONE, and the negotiator queue stalled indefinitely — causing the observed hang after resource binding. Fix: isReady() now returns true for QUIC connections, allowing negotiate() to be called and immediately complete (DONE), so the negotiator queue advances normally to ServiceDiscovery and beyond. Tested: flutter analyze (clean), flutter test (179 passed), dart test in vendor/xmpp_stone (65 passed). Co-authored-by: Junie <junie@jetbrains.com>
Previously the '---Xmpp Sending:---' log line was emitted from
Connection.write(), which runs at queue-time: before the bufferedWrites
flush, before the TCP/WS/QUIC socket handoff, and — crucially on QUIC —
before _selectSendTarget() had picked (or opened) the destination
stream. That made the log line misleading for any diagnostics about
what is actually on the wire, and it hid the fact that a stanza can be
routed to a QUIC aux stream that is still pending an open.
This change moves the send log to the transport layer:
- vendor/xmpp_stone/lib/src/Connection.dart:
- Connection.write() no longer logs; a comment explains where the
log is now emitted and why.
- vendor/xmpp_stone/lib/src/logger/Log.dart:
- Log.xmppp_sending() gains an optional 'channel' parameter; when
set, the header is rendered as '---Xmpp Sending [<channel>]:---'.
- vendor/xmpp_stone/lib/src/connection/XmppWebsocketIo.dart:
- Logs with channel 'tcp' or 'ws' immediately before the actual
socket/sink write, and skips logging entirely if the underlying
socket is null (no transport handoff).
- lib/xmpp/quic_xmpp_socket.dart:
- _QuicSendTarget now carries a channel label ('quic-control' or
'quic-aux-<slot>') that reflects the real stream _selectSendTarget
resolved to, including slot number for bare-JID-hashed aux
streams.
- The write queue logs the payload with that label right before
sendStreamWriteAll, i.e. at the moment bytes are handed to the
QUIC stream.
This directly addresses the wimsy.log observation that a post-bind
disco#info to the server domain appeared to be 'sent' but never
reached the server: the new log will show the exact QUIC stream
(control vs aux-N) the stanza was actually written to.
Testing:
- flutter analyze: clean (no issues)
- flutter test: 179 tests passed
- dart test in vendor/xmpp_stone: 65 tests passed
No new tests were added; the change is limited to log-site plumbing
and existing tests exercise Connection.write and the TCP/WS socket
write paths. The QUIC path has no unit-level test harness in-tree and
is verified at runtime via wimsy.log.
Co-authored-by: Junie <junie@jetbrains.com>
Fix the post-bind hang where ServiceDiscoveryNegotiator's disco#info to the
user's own server domain was queued but never actually written on the QUIC
transport, causing the 20s connect-ready timeout in XmppService and a
ForcefullyClosed reconnect loop.
Root cause: QuicCapableXmppSocket._selectSendTarget routed any stanza whose
'to' was not the account bare JID to an aux QUIC stream, and awaited
_ensureAuxStream on the single serial _writeQueue chain. The post-bind
disco#info is addressed to the account *domain* (e.g. dave.cridland.net),
which hashed to aux slot 0. slot 0's connectionOpenBi() was still in flight
(or hanging) from the pre-open pass started the moment the bind result
arrived, so the write queue deadlocked behind it and the disco stanza was
never handed to any send stream - no 'Xmpp Sending' log, no bytes on the
wire.
Changes in lib/xmpp/quic_xmpp_socket.dart (_selectSendTarget):
* Server-directed stanzas are kept on the control stream: this now covers
stanzas with no 'to', stanzas addressed to the account bare JID, and
stanzas addressed to the user's own server domain. Negotiation IQs
(disco#info, session, etc.) never go to an aux stream.
* For all other bare-JID targets, we no longer await aux-stream creation
on the write queue. If the aux stream is already open we use it; if not,
we kick off _ensureAuxStream in the background and send this payload on
the control stream. Future writes to the same bare JID will use the aux
stream once it is ready, but a slow or failing aux open can never stall
the rest of the connection's outbound traffic again.
Tested: * flutter analyze: clean, no issues
* flutter test: 179 passed
* dart test in vendor/xmpp_stone: 65 passed
* No new unit tests added: the QUIC transport has no in-tree test harness
(it is runtime-verified via wimsy.log); the selection logic is a
straightforward guard around an already-covered code path.
Co-authored-by: Junie <junie@jetbrains.com>
After the previous fix to keep negotiation traffic on the control stream, runtime logs showed that the post-bind aux-stream opens never produced any success or failure log after the initial 'QUIC aux stream opening slot=0' line. Only slot 0 logged at all, because all other slots queue behind _auxOpenLock and slot 0's completer never fires: connectionOpenBi (Quinn open_bi) blocks indefinitely when the peer has not granted additional bidirectional-stream credits via MAX_STREAMS. Changes in lib/xmpp/quic_xmpp_socket.dart _openAuxStream: - Log 'awaiting open lock' for each slot before taking the lock, so we can see that all slots are at least attempting to open. - Log 'calling connectionOpenBi' immediately before the FFI call. - Start a Timer.periodic (2s) that logs 'connectionOpenBi still pending after Nms' so a stuck open is visible in the logs. - Wrap connectionOpenBi in a 10s timeout that throws TimeoutException with an explanatory message, so _auxOpenLock is released and subsequent slots can proceed (or fail) instead of being silently starved. - Log elapsed time on success and on any error (with stack) so future failures are diagnosable from wimsy.log alone. Tested: - flutter analyze: clean - flutter test: 179 passed - dart test in vendor/xmpp_stone: 65 passed No new unit tests: QUIC transport still has no in-tree harness; this is diagnostic plumbing on a runtime-only code path, intended to be validated via wimsy.log on the next QUIC session. Co-authored-by: Junie <junie@jetbrains.com>
When a QUIC aux stream open fails (DroppableDisposedException after
connection teardown, StateError for closed connection, or TimeoutException
from the 10s open timeout), the error was being rethrown inside a
catchError() on an unawaited Future, causing it to surface as a fatal
unhandled exception via PlatformDispatcher.onError and reported to Sentry
as WIMSY-1D, WIMSY-1B, and WIMSY-1C.
The on-demand open path in _selectSendTarget used catchError() which
requires the handler to return a value of the Future's type; rethrowing
was the only way to satisfy the type checker, but that made the error
fatal. Replaced with .then((_) {}, onError: ...) which has a void
onError handler and correctly swallows the error after logging it.
These failures are expected during connection teardown: the payload was
already sent on the control stream, so the aux stream open failure is
non-fatal. The pre-open path in _startAuxStreamsOpen was already safe
(catch inside the async body).
Fixes: WIMSY-1D, WIMSY-1B, WIMSY-1C
Tested: flutter analyze (clean), flutter test (179 passed),
dart test in vendor/xmpp_stone (65 passed).
Co-authored-by: Junie <junie@jetbrains.com>
Log server MAX_STREAMS(bidi) frame counts via connectionStats() at two
key moments:
- immediately after the QUIC handshake completes ('post-connect'), so
we can see the server's initial credit situation in the log;
- when connectionOpenBi times out ('aux-open-timeout slot=N'), so we
know the credit count at the moment of failure.
Add _auxStreamsBlocked flag: set on connectionOpenBi timeout, cleared
when a new MAX_STREAMS(bidi) frame is detected. A periodic watcher
(every 5 s) polls connectionStats() while blocked and resumes aux
stream opens as soon as the server raises the limit.
Change _startAuxStreamsOpen to open slots sequentially rather than
firing all 20 concurrently. This means:
- we stop immediately at the first credit-starvation timeout instead
of queuing 19 more doomed calls that each fail with
DroppableDisposedException (the Auto_Owned RustArc is consumed by
the timed-out slot-0 call and never returned);
- when the watcher detects new credits, _openAuxStreamsSequentially
resumes from where it left off.
Reset all new state (_auxStreamsBlocked, _lastMaxStreamsBidiFrameCount,
_maxStreamsWatchTimer) in close() so a reconnect starts clean.
Tested: flutter analyze (no issues), flutter test (179 passed),
dart test in vendor/xmpp_stone (65 passed).
Co-authored-by: Junie <junie@jetbrains.com>
…chive query When _lastMamIdSeen is non-null (i.e. any session after the first), _primeMamSync now issues a single MAM IQ against the user's own server archive (no to= JID, afterId=_lastMamIdSeen) instead of fanning out one catch-up IQ per DM contact. The server returns all missed DM messages across every contact in one paginated stream; the existing _addMessage routing dispatches each forwarded message to the correct chat by JID. On completion the RSM <last> id from the <fin> IQ result is used to advance _lastMamIdSeen via the existing _bumpLastMamIdSeen helper, so the anchor stays current for the next session. When _lastMamIdSeen is null (fresh install / first session) the previous per-chat fan-out is retained as a fallback so each chat still gets its initial 25-message tail. MUC archives are per-room by protocol and remain as per-room queries regardless. The IQ is built manually (rather than via MessageArchiveManager.queryById) so the IQ id can be captured and an IqRouter response handler registered to receive the <fin> result without polling. New tests: - test/unified_mam_catchup_test.dart: 6 tests covering the <fin> RSM <last> parsing logic (well-formed result, missing fin, missing RSM set, missing last element, non-RESULT type, whitespace trimming). - vendor/xmpp_stone/test/mam_query_xml_test.dart: 1 new test verifying the unified query IQ has no toJid, carries after-id field with the correct anchor value, RSM max=50, and urn:xmpp:mam:2 namespace. Tested: flutter test (246 pass), dart test in vendor/xmpp_stone (82 pass). Co-authored-by: Junie <junie@jetbrains.com>
The server may open bidirectional QUIC streams to push large responses
(e.g. MAM pages) to the client without waiting for the client to open a
stream first. Previously the client had no accept loop, so these streams
were silently ignored: the MAM IQ result never arrived, _lastMamIdSeen
never advanced, and _primeMamSync re-issued the same unified catch-up
query on every Ready cycle — producing a continuous MAM request loop.
Changes:
- vendor/flutter_quic/rust/src/core/connection.rs: add accept_bi() to
QuicConnection, wrapping Quinn's Connection::accept_bi(). Returns
Option<(QuicSendStream, QuicRecvStream)> — None on connection close.
- vendor/flutter_quic/rust/src/api/bridge.rs: add connection_accept_bi()
free function following the same Auto_Owned pattern as connection_open_bi().
Returns (QuicConnection, Option<QuicSendStream>, Option<QuicRecvStream>).
- Regenerated vendor/flutter_quic/rust/src/frb_generated.rs and
vendor/flutter_quic/lib/src/rust/frb_generated.dart via
flutter_rust_bridge_codegen generate.
- lib/xmpp/quic_xmpp_socket.dart:
* _startRecvLoop: add optional String? label parameter so server-stream
recv loops log a meaningful channel name (quic-server-N).
* _startServerStreamAcceptLoop: new method that loops on
connectionAcceptBi(), starts an independent _startRecvLoop for each
accepted stream with its own XML mapper (via _makeAuxMapper), and
exits cleanly when the connection closes or _closed is set.
* connect(): call _startServerStreamAcceptLoop() immediately after
_startControlRecvLoop() so server-pushed streams are accepted from
the moment the QUIC connection is established.
- test/quic_stream_routing_test.dart: add connectionAcceptBi bridge export
test (compile-time regression guard); live-connection behaviour requires
integration testing against a real QUIC server.
Tested: flutter test (247 pass, +1 new); dart test in vendor/xmpp_stone
(82 pass). Rust cargo build clean (7 pre-existing warnings, no errors).
Co-authored-by: Junie <junie@jetbrains.com>
Two related QUIC multi-stream bugs fixed: 1. DroppableDisposedException on aux stream open Root cause: _startServerStreamAcceptLoop called connectionAcceptBi using a local copy of _connection concurrently with connectionOpenBi in _openAuxStream. Both functions used Auto_Owned FFI transfer, consuming the same RustArc; the second caller found it already disposed. Fix: change connection_accept_bi in the Rust bridge to take &QuicConnection (shared reference) instead of QuicConnection (Auto_Owned). Quinn's accept_bi() only needs &self, so this is safe. The accept loop no longer consumes the arc and can run concurrently with connectionOpenBi without any locking. Regenerated frb_generated.rs / frb_generated.dart via flutter_rust_bridge_codegen generate. 2. Ready-burst stanzas sent on quic-control instead of aux streams Root cause: _selectSendTarget fell back to the control stream immediately when the aux stream for a bare JID was not yet open, so the entire post-Ready burst (vCard IQs, avatar polls, caps disco, MAM catch-up) went on quic-control. Fix: introduce _auxStreamPendingQueue (Map<int, List<String>>). When a stanza targets a slot whose aux stream is not yet open, enqueue it and kick off _ensureAuxStream. On success, _flushAuxPendingQueue sends all queued stanzas in order on the aux stream. On failure (disposed arc, timeout), the queue is drained to the control stream so no stanzas are silently dropped. _selectSendTarget now returns null for enqueued payloads; write() checks for null and skips the immediate send. Also updated _auxOpenLock comment to clarify it serialises all Auto_Owned FFI calls (connectionOpenBi, connectionStats, etc.) but NOT connectionAcceptBi which now uses a shared ref. Testing: all 247 flutter tests pass; all 82 xmpp_stone dart tests pass; cargo build clean (no new warnings). Co-authored-by: Junie <junie@jetbrains.com>
When the server opens a bidirectional QUIC stream (e.g. to push a MAM page response), we now pool it rather than immediately starting a receive-only loop. When _ensureAuxStream next needs a stream for a new bare-JID slot it pops from this pool first, avoiding a connectionOpenBi round-trip and conserving the peer's bidi-stream credit budget. Changes: - quic_xmpp_socket.dart: add _serverStreamPool (List<_QuicStreamChannel>). _startServerStreamAcceptLoop now pushes accepted streams onto the pool instead of starting a recv loop immediately (send-stream-less streams still get an immediate recv-only loop as a fallback). _openAuxStream checks the pool first; if non-empty it pops the front entry, assigns it to the slot, starts its recv loop, removes the slot from _auxStreamOpening, and returns without touching connectionOpenBi. Pool is cleared in close() alongside the other stream maps. Added @VisibleForTesting serverStreamPoolSize getter and injectServerStreamForTesting() for future integration tests. - test/quic_stream_routing_test.dart: two new tests verifying serverStreamPoolSize is 0 at construction and after close(). Testing: - flutter test: 249 tests pass (+2 new). - dart test (vendor/xmpp_stone): 82 tests pass. - cargo build (vendor/flutter_quic/rust): clean (no new warnings). Co-authored-by: Junie <junie@jetbrains.com>
… pop Previously, when a server-initiated QUIC stream was accepted and added to _serverStreamPool, its recv loop was NOT started. Inbound stanzas pushed by the server on that stream were silently dropped until the stream was popped from the pool and assigned to a slot (which might never happen if the client had no outbound traffic for that JID). Fix: start _startRecvLoop immediately in _startServerStreamAcceptLoop for every accepted stream (both send+recv pairs and recv-only streams). The send stream is still pooled for outbound reuse as before. Correspondingly, remove the now-redundant _startRecvLoop call in the pool-reuse path of _openAuxStream — the loop is already running, starting a second one would process each inbound chunk twice. Outbound streams opened via connectionOpenBi already started their recv loop immediately (line 901) — that path was correct and is unchanged. Testing: all 249 flutter tests pass; all 82 xmpp_stone dart tests pass. No new tests added — the fix is structural (ordering of side-effects in the accept loop) and the existing quic_stream_routing_test.dart pool tests continue to pass. Co-authored-by: Junie <junie@jetbrains.com>
…red ref) Previously every connection_* FFI function (connection_open_bi, connection_stats, connection_close_reason, connection_rtt_millis, etc.) took QuicConnection by value via flutter_rust_bridge's Auto_Owned ownership-transfer mechanism. This meant only one such call could be in-flight at a time: a second concurrent caller would find the RustOpaque cell already in the 'moved' state and throw DroppableDisposedException. Under high latency / low bandwidth (where connection_open_bi blocks waiting for MAX_STREAMS credits) this race window was wide enough to trigger reliably. Quinn's Connection is Arc-based internally and every method only needs &self, so the Auto_Owned restriction was purely a wrapper artefact. This commit applies the fix already proven for connection_accept_bi to all remaining connection_* functions: Rust (bridge.rs): - connection_open_bi, connection_open_uni: &QuicConnection, return (SendStream, RecvStream) without the connection in the tuple. - connection_send_datagram, connection_send_datagram_wait: &QuicConnection, return Result<()> (no connection in tuple). - connection_read_datagram: &QuicConnection, return Option<Vec<u8>>. - connection_datagram_send_buffer_space, connection_max_datagram_size, connection_remote_address, connection_local_ip, connection_rtt_millis, connection_stable_id, connection_close_reason, connection_stats, connection_peer_transport_params: all &QuicConnection, return just the value (no connection tuple). - endpoint_connect and send/recv stream functions are unchanged. - FRB bindings regenerated (frb_generated.rs, frb_generated.dart, bridge.dart). Dart (quic_xmpp_socket.dart): - Removed _auxOpenLock field and the entire completer/chain pattern in _openAuxStream. Multiple concurrent connectionOpenBi calls are now safe; per-slot coalescing via _auxStreamOpening is retained to avoid duplicate opens for the same JID hash. - Removed 'Re-read _connection after stats call' dance in _openAuxStream and _logConnectionStats: stats no longer consumes the arc. - getQuicStats, _logConnectionStats, _logPeerTransportParams, _logQuicCloseReason: all simplified to use the direct return value instead of destructuring a (connection, value) tuple. - _connectOneCandidateQuic: connectionOpenBi result is now (sendStream, recvStream); connection comes from endpointConnect result. - Stale comments referencing Auto_Owned and _auxOpenLock removed. Testing: - cargo build: clean (no errors, no new warnings). - flutter test: 253 tests pass (+4 new compile-time guards in quic_stream_routing_test.dart verifying connectionOpenBi, connectionStats, connectionCloseReason, connectionRttMillis are exported as Functions from the shared-ref bridge). - dart test (vendor/xmpp_stone): 82 tests pass. Co-authored-by: Junie <junie@jetbrains.com>
…ion errors - Remove unnecessary null check on IqStanza response in xmpp_service.dart (response handler callback type is non-nullable, so the null check was always false and the body was dead code) - Rename underscore-prefixed local functions in unified_mam_catchup_test.dart to satisfy the no_leading_underscores_for_local_identifiers lint rule - Add 30 missing record codec implementations to vendor/flutter_quic/lib/src/rust/frb_generated.dart: - 10 dco_decode_record_*_quic_connection_* methods - 10 sse_decode_record_*_quic_connection_* methods - 10 sse_encode_record_*_quic_connection_* methods These were present as abstract declarations in frb_generated.io.dart but their concrete implementations were missing from frb_generated.dart, causing compilation failures in tests that import flutter_quic. Tested: flutter analyze (no issues), flutter test (253/253 passed) Co-authored-by: Junie <junie@jetbrains.com>
sentry_flutter: 9.14.0 → 9.20.0 sentry: 9.14.0 → 9.20.0 sentry_dart_plugin: 3.2.1 → 3.3.0 Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Also pulls in connectivity_plus_platform_interface 2.0.1 → 2.1.0. Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Also pulls in flutter_secure_storage_darwin 0.2.0 → 0.3.0 and flutter_secure_storage_web 2.1.0 → 2.1.1. Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
async 2.13.0 → 2.13.1 hooks 1.0.2 → 1.0.3 synchronized 3.4.0 → 3.4.0+1 vm_service 15.0.2 → 15.2.0 Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
dart_webrtc 1.8.0 → 1.8.1 flutter_plugin_android_lifecycle 2.0.33 → 2.0.34 image_picker_android 0.8.13+14 → 0.8.13+17 path_provider_android 2.2.22 → 2.2.23 shared_preferences_android 2.4.21 → 2.4.23 url_launcher_android 6.3.28 → 6.3.29 url_launcher_web 2.4.2 → 2.4.3 Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
…interface 2.4.1 → 2.4.2 Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
file_picker 11 made FilePicker an abstract final class with static methods, removing the .platform accessor. Updated all 3 call sites in lib/main.dart from FilePicker.platform.pickFiles(...) to FilePicker.pickFiles(...). Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
The -Wno-error=deprecated-literal-operator flag added in linux/CMakeLists.txt
is Clang-only. GCC does not recognise -Wdeprecated-literal-operator at all
and therefore rejects the -Wno-error= form with a hard error:
cc1: error: '-Wno-error=deprecated-literal-operator':
no option '-Wdeprecated-literal-operator'
This caused the CI Linux build to fail. Fix by wrapping the flag in a
CMAKE_CXX_COMPILER_ID Clang guard so it is only applied when building
with Clang (e.g. macOS), not with GCC (Linux CI).
Tested: flutter analyze (no issues), flutter test (253/253 passed).
Co-authored-by: Junie <junie@jetbrains.com>
connectivity_plus 7.1.1 references NWPath.isUltraConstrained behind an #available(macOS 26.0, *) guard, but Xcode 16.4 ships with the macOS 15.5 SDK which does not define that symbol at all. The Swift compiler rejects the reference even with the availability guard in place, causing the CI macOS build to fail with: error: value of type 'NWPath' has no member 'isUltraConstrained' Pinning to 7.0.0 (the previous release) avoids the broken code until upstream ships a fix. Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
The flutter_webrtc plugin's libwebrtc headers trigger -Wno-error=deprecated-literal-operator, which GCC rejects because it does not recognise -Wdeprecated-literal-operator at all. Clang handles the flag correctly. clang/clang++ are already installed by the 'Install Linux build deps' step; we just need to tell CMake to use them by setting CC=clang and CXX=clang++ on the 'Build Linux' step. This makes the existing if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") guard in linux/CMakeLists.txt effective. Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
… dep Updated vulnerable dependencies to patched versions: - rustls-webpki: 0.103.4/0.103.9/0.103.10 → 0.103.13 (fixes GHSA-82j2-j2ch-gfr8, GHSA-965h-392x-2mh5, GHSA-xgp8-3hg3-c2mh, GHSA-pwjx-qhcg-rvj4) - aws-lc-sys: 0.30.0/0.38.0 → 0.40.0 (fixes GHSA-65p9-r9h6-22vj, GHSA-9f94-5g5w-gf6r, GHSA-hfpc-8r3f-gw53, GHSA-vw5v-4f2q-w9xf, GHSA-394x-vwmw-crm3) - aws-lc-fips-sys: 0.13.7/0.13.12 → 0.13.14 (fixes GHSA-65p9-r9h6-22vj, GHSA-9f94-5g5w-gf6r) - rand: 0.9.2 → 0.9.4 (fixes GHSA-cq8v-f236-94qc) - bytes: 1.10.1 → 1.11.1 (fixes GHSA-434x-w66g-qw3r) - time: 0.3.41 → 0.3.47 (fixes GHSA-r6v5-fh4h-64xc) - tracing-subscriber: 0.3.19 → 0.3.23 (fixes GHSA-xwfj-jgwm-7wp5) - quinn-proto: 0.11.13 → 0.11.14 (fixes GHSA-6xvm-j4wr-6v98) - cargokit github dart dep: 9.17.0 → 9.25.0 (fixes CVE-2012-2055) Added acceptance for CVE-2022-3095-Flutter: Flutter 1.0.0 appears only in vendor/flutter_quic/example/ios/Podfile.lock (example app, not shipped). Tested: flutter analyze (no issues), flutter test (253/253 passed), dart test in vendor/xmpp_stone (82/82 passed). Co-authored-by: Junie <junie@jetbrains.com>
Updated vulnerable crates in vendor/flutter_quic/rust/vendor/quinn-proto/Cargo.lock: - aws-lc-fips-sys 0.13.12 → 0.13.14 (fixes GHSA-9f94-5g5w-gf6r) - aws-lc-sys 0.38.0 → 0.40.0 (fixes GHSA-394x-vwmw-crm3, GHSA-9f94-5g5w-gf6r) - rustls-webpki 0.103.9 → 0.103.13 (fixes GHSA-82j2-j2ch-gfr8, GHSA-pwjx-qhcg-rvj4, GHSA-965h-392x-2mh5, GHSA-xgp8-3hg3-c2mh) - rand 0.9.2 → 0.9.4 (fixes GHSA-cq8v-f236-94qc) - tracing-subscriber 0.3.22 → 0.3.23 Tested: flutter analyze (no issues), flutter test (253/253 passed), dart test in vendor/xmpp_stone (82/82 passed). Co-authored-by: Junie <junie@jetbrains.com>
CVE-2012-2055 is a 2012 CVE for the GitHub web service, not the Dart 'github' pub package. The scanner incorrectly matches by package name. We are already on the latest available version (9.25.0). Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
…d verbosity The -Wno-error=deprecated-literal-operator flag was added previously to suppress a warning from third-party headers, but Flutter's build system already hardcodes CC=clang/CXX=clang++ when invoking CMake on Linux (in build_linux.dart), so the flag is never needed for GCC. Despite the Clang guard, the flag was still causing cc1 errors in CI, so it has been removed entirely. Also added a 'Show compiler versions' step and -v flag to flutter build linux in CI to provide more diagnostic information if build failures recur. Tested: flutter analyze (no issues), flutter test (253/253 passed). Co-authored-by: Junie <junie@jetbrains.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.