feat(Push): add Appwrite Push (MQTT 5) adapter - #142
Conversation
Adds a self-hosted, low-power alternative to FCM/APNS. Publishes notifications over MQTT 5 to a per-device topic, allowing a single persistent TLS connection on the device with a long keep-alive interval (30 minutes by default) — the same model that lets FCM be low-power on Android. - Helpers/MQTT: minimal MQTT 5 codec (control packet encode/decode) so the adapter does not need an external MQTT client dependency. - Adapter/Push/Appwrite: publisher adapter. Connects over TCP/TLS, authenticates with a short-lived HMAC-signed JWT, publishes one QoS 1 PUBLISH per device with content-type and message-expiry properties, maps broker reason codes back to the standard expired-token signal so Appwrite's target invalidation works the same way as for FCM/APNS. Tests: 10 codec round-trip cases, 2 adapter integration cases driven by a fake broker spawned via proc_open. PHPStan level 6 clean, Pint PSR-12 clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Pipeline PUBLISHes up to the broker-advertised Receive Maximum (default 256) and match PUBACKs by packet id. Drops effective send time from N×RTT to ~RTT for large fan-outs. - Verify PUBACK packet id matches the in-flight publish so an out-of-order or duplicate ack cannot attribute success to the wrong device token (Greptile P2, Copilot). - Surface json_encode failures as a RuntimeException instead of silently sending an empty payload to the broker (Greptile P1). - Persistent read buffer on the adapter so coalesced TCP reads do not drop trailing MQTT packets between readPacket() calls (Copilot). - MQTT::encodeConnect throws when a password is supplied without a username (MQTT 5 §3.1.2.9, Greptile P2). - MQTT::encodePublish validates QoS is 0/1/2 instead of silently masking the bits (Copilot). - FakeBroker fixture rewritten on Swoole — drops the pcntl dependency that wasn't installed in the alpine test image, and exercises the same async runtime Appwrite uses in production. Dockerfile installs ext- swoole via PECL for the tests image. - New tests: pipelined send to 64 devices, password-without-username rejection, invalid-QoS rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for contributing! This repository is a read-only mirror; development for this library happens in |
Greptile SummaryAdds an Appwrite MQTT 5 push adapter and a reusable MQTT packet codec.
Confidence Score: 2/5The PR should not merge until failed batches account for every recipient and valid CONNACK property sequences cannot bypass broker flow-control limits. A mid-batch connection failure silently omits unsent recipients from the response, while premature property-parser termination can discard Receive Maximum and make the publisher exceed the broker's permitted in-flight window. Files Needing Attention: src/Utopia/Messaging/Adapter/Push/Appwrite.php; src/Utopia/Messaging/Helpers/MQTT.php Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
src/Utopia/Messaging/Adapter/Push/Appwrite.php:151-155
**Unsent recipients vanish after failure**
When a broker read fails before a batch larger than the in-flight window is fully queued, this branch records failures only for `$inflight` and returns while later recipients remain behind `$cursor`, causing notifications that were never published to have no result and preventing callers from identifying or retrying them.
### Issue 2 of 2
src/Utopia/Messaging/Helpers/MQTT.php:618-619
**Property parsing drops flow control**
When a valid unsupported CONNACK property precedes Receive Maximum, this return discards the remaining properties, so the adapter retains its 256-message window instead of the broker's lower limit and the broker disconnects when that limit is exceeded.
Reviews (1): Last reviewed commit: "fix(Push): address PR review on Appwrite..." | Re-trigger Greptile |
| } catch (\Throwable $error) { | ||
| foreach ($inflight as $token) { | ||
| $response->addResult($token, $error->getMessage()); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Unsent recipients vanish after failure
When a broker read fails before a batch larger than the in-flight window is fully queued, this branch records failures only for $inflight and returns while later recipients remain behind $cursor, causing notifications that were never published to have no result and preventing callers from identifying or retrying them.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Utopia/Messaging/Adapter/Push/Appwrite.php
Line: 151-155
Comment:
**Unsent recipients vanish after failure**
When a broker read fails before a batch larger than the in-flight window is fully queued, this branch records failures only for `$inflight` and returns while later recipients remain behind `$cursor`, causing notifications that were never published to have no result and preventing callers from identifying or retrying them.
How can I resolve this? If you propose a fix, please make it concise.| default: | ||
| return $properties; |
There was a problem hiding this comment.
Property parsing drops flow control
When a valid unsupported CONNACK property precedes Receive Maximum, this return discards the remaining properties, so the adapter retains its 256-message window instead of the broker's lower limit and the broker disconnects when that limit is exceeded.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Utopia/Messaging/Helpers/MQTT.php
Line: 618-619
Comment:
**Property parsing drops flow control**
When a valid unsupported CONNACK property precedes Receive Maximum, this return discards the remaining properties, so the adapter retains its 256-message window instead of the broker's lower limit and the broker disconnects when that limit is exceeded.
How can I resolve this? If you propose a fix, please make it concise.
Rebased on latest main with conflict resolution.
Conflicts resolved: