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

This PR introduces the Appwrite Push adapter — a self-hosted alternative to FCM/APNS that publishes notifications over MQTT 5 — along with a pure-PHP MQTT 5 codec, a Swoole-based fake broker for integration testing, and accompanying unit tests.

  • MQTT.php implements encoding and decoding for CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, PING, and DISCONNECT packets with a streaming decodePacket that advances a persistent read buffer.
  • Appwrite.php builds a pipelined PUBLISH/PUBACK loop respecting the broker's receiveMaximum flow-control window, issues a short-lived HMAC-signed JWT per connection, and records per-token delivery results.
  • Tests cover round-trip codec correctness, pipelined fan-out to 64 devices, and per-token rejection via PUBACK reason codes.

Confidence Score: 4/5

The adapter and codec are functionally correct against a well-behaved MQTT 5 broker, but several edge-case defects in readProperties and connection reuse have been identified.

The readProperties switch exits early on any unrecognized property identifier, silently discarding every property that follows it in the same packet. Several identifiers emitted by encodeConnack are absent from the switch, so if a real broker sends any of them before receiveMaximum in its CONNACK, the flow-control limit is never parsed.

src/Utopia/Messaging/Helpers/MQTT.php — the readProperties method and the null-propLen handling in the three parse methods deserve a second look before merge.

Important Files Changed

Filename Overview
src/Utopia/Messaging/Helpers/MQTT.php Pure-PHP MQTT 5 codec; readProperties early-returns on any unrecognized property identifier (silently dropping all subsequent properties), and readVariableByteInteger's null return is unchecked in all three parsers.
src/Utopia/Messaging/Adapter/Push/Appwrite.php New MQTT 5 push adapter with pipelined PUBLISH/PUBACK loop; receiveMaximum ratchets down permanently across reused instances, readBuffer is not cleared on reconnect, and rtrim leaves leading whitespace in the endpoint.
tests/Messaging/Adapter/Push/AppwriteTest.php Integration tests launch FakeBroker as a subprocess; covers happy path, pipelining, and per-token reject reason codes. The state temp file is never cleaned up after the test, a minor resource hygiene issue.
tests/Messaging/Adapter/Push/FakeBroker.php Swoole-based TCP broker that handles CONNECT, PUBLISH, PINGREQ, and DISCONNECT; flushes captured packets to a JSON file for test assertions. Requires Swoole extension — missing it gives a misleading 'did not come up in time' failure rather than a skip.
tests/Messaging/Helpers/MQTTTest.php Unit tests for the MQTT codec covering encode/parse round-trips, partial-buffer handling, multi-packet coalescing, and invalid-input guards. Good coverage of the codec's happy paths.

Reviews (4): 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.

@deepshekhardas

Copy link
Copy Markdown
Author

Following up - this PR has been open for 1 month. Let me know if any changes are needed or if the implementation approach needs adjustment.

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.
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