Skip to content

feat(Push): add Appwrite Push (MQTT 5) adapter#129

Open
deepshekhardas wants to merge 1 commit into
utopia-php:mainfrom
deepshekhardas:fix/122-appwrite-push-mqtt5
Open

feat(Push): add Appwrite Push (MQTT 5) adapter#129
deepshekhardas wants to merge 1 commit into
utopia-php:mainfrom
deepshekhardas:fix/122-appwrite-push-mqtt5

Conversation

@deepshekhardas

Copy link
Copy Markdown

Port of PR #122 by abnegate.

Adds Appwrite Push - a self-hosted, low-power alternative to FCM/APNS that publishes notifications over MQTT 5 to per-device topics.

Changes:

  • New MQTT 5 control-packet codec (Helpers/MQTT) - pure PHP, no extra dependency
  • New Appwrite Push adapter for MQTT 5 publishing
  • Fake broker for integration testing
  • Unit and integration tests

@greptile-apps

greptile-apps Bot commented Jun 14, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a new Appwrite Push adapter that publishes MQTT 5 notifications to a self-hosted broker over a pipelined PUBLISH/PUBACK loop, backed by a pure-PHP MQTT 5 codec with no external dependencies.

  • Appwrite.php — new PushAdapter subclass that opens a TLS/TCP socket, authenticates via a short-lived HMAC-JWT CONNECT, fans out one PUBLISH per device token up to receiveMaximum in flight, and drains PUBACKs to track per-token success/failure.
  • MQTT.php — minimal MQTT 5 codec covering CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, DISCONNECT, PINGREQ/RESP encode and decode; used by both the adapter and the test fake broker.
  • Integration tests — a Swoole-based FakeBroker subprocess validates the full handshake-publish-ack cycle, pipelining under burst load, and per-token failure reporting on broker rejection.

Confidence Score: 3/5

The adapter and codec carry several unfixed correctness gaps from prior review rounds; the core happy path works but the code is not ready to merge as-is.

Three issues flagged in earlier review rounds remain unaddressed: the read buffer is never reset between connections, endpoint whitespace is stripped only on the right, and the receive-maximum window shrinks permanently across sends. An additional previously-noted issue in the MQTT codec causes any unrecognised property ID to silently discard all remaining properties in the same packet. The two new findings here — null returns from readVariableByteInteger going unchecked in all four parse methods, and parse_url returning false for malformed URLs with '://' — add to the overall picture of the parser being fragile against non-conforming broker responses.

src/Utopia/Messaging/Adapter/Push/Appwrite.php and src/Utopia/Messaging/Helpers/MQTT.php both need attention before merge.

Important Files Changed

Filename Overview
src/Utopia/Messaging/Adapter/Push/Appwrite.php New MQTT 5 push adapter with pipelined PUBLISH/PUBACK loop; has previously-flagged issues (readBuffer not reset, rtrim instead of trim, receiveMaximum monotonically decreasing) plus a new parse_url false-return gap in resolveEndpoint.
src/Utopia/Messaging/Helpers/MQTT.php Pure-PHP MQTT 5 codec; previously-flagged unknown-property-ID early return in readProperties still present, plus readVariableByteInteger null returns not null-checked in parseConnect, parseConnack, and parseSubscribe.
tests/Messaging/Adapter/Push/AppwriteTest.php Integration tests covering happy-path delivery, pipelining to many tokens, and broker rejection; double stopBroker call in finally is handled safely.
tests/Messaging/Adapter/Push/FakeBroker.php Swoole-based fake MQTT broker for integration testing; handles CONNECT, PUBLISH, DISCONNECT, and PINGREQ, flushes capture state on close events.
tests/Messaging/Helpers/MQTTTest.php Unit tests covering encode/decode round-trips, partial-buffer handling, oversized-string rejection, and packet concatenation; good coverage of codec edge cases.
Dockerfile Adds Swoole PHP extension (required by FakeBroker) via PECL with proper build-dep cleanup; no issues.

Reviews (2): Last reviewed commit: "feat(Push): add Appwrite Push (MQTT 5) a..." | Re-trigger Greptile

}

public function getMaxMessagesPerRequest(): int
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 readBuffer not cleared between process() calls

$this->readBuffer is never reset at the start of each connection. If the adapter instance is reused (e.g., send() is called twice), or if the broker sends an extra packet after the last PUBACK (e.g., a PINGREQ that landed in the buffer just before disconnect), that residual data persists into the next call. On the next invocation readPacket() would immediately return the leftover packet as if it were the new connection's CONNACK, causing handshake() to throw "Broker did not respond with CONNACK" even on a healthy connection.

Add $this->readBuffer = ''; at the start of connect() or at the top of process() to isolate each connection's read state.


private function resolveEndpoint(): string
{
$endpoint = \rtrim($this->endpoint);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 rtrim strips only trailing whitespace, so a leading space in the configured endpoint (e.g., " broker.example.com") would produce a malformed URL like tls:// broker.example.com:8883 that stream_socket_client rejects. Use trim to strip both ends.

Suggested change
$endpoint = \rtrim($this->endpoint);
$endpoint = \trim($this->endpoint);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +381 to +384
$packet = MQTT::decodePacket($this->readBuffer);
if ($packet !== null) {
return $packet;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 receiveMaximum decreases monotonically across process() calls

$this->receiveMaximum is instance state that is only ever updated via min() in handshake(). If the adapter is reused across multiple send() calls and the broker advertises a low receiveMaximum (say 10) on the first call, subsequent connections — even to a different broker endpoint — will be throttled to that minimum permanently for the lifetime of the object. Resetting it to the class-default (or to 65535) at the start of each connect() would make each connection's window independent.

Based on PR utopia-php#122 by abnegate. Adds Appwrite Push - a self-hosted MQTT 5 based push notification adapter with minimal MQTT 5 control-packet codec.
@deepshekhardas deepshekhardas force-pushed the fix/122-appwrite-push-mqtt5 branch from 7e1b5cd to 2dc61de Compare June 17, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant