Skip to content

feat(Push): add Appwrite Push (MQTT 5) adapter - #142

Closed
deepshekhardas wants to merge 2 commits into
utopia-php:mainfrom
deepshekhardas:fix/pr-122-appwrite-push
Closed

feat(Push): add Appwrite Push (MQTT 5) adapter#142
deepshekhardas wants to merge 2 commits into
utopia-php:mainfrom
deepshekhardas:fix/pr-122-appwrite-push

Conversation

@deepshekhardas

Copy link
Copy Markdown

Rebased on latest main with conflict resolution.

Conflicts resolved:

  • Dropped Dockerfile changes (upstream deleted Dockerfile)
  • All new adapter/test files applied cleanly

abnegate and others added 2 commits July 25, 2026 12:39
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>
@github-actions

Copy link
Copy Markdown

Thanks for contributing! This repository is a read-only mirror; development for this library happens in packages/messaging in the utopia-php monorepo. Please open this pull request there instead.

@github-actions github-actions Bot closed this Jul 25, 2026
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an Appwrite MQTT 5 push adapter and a reusable MQTT packet codec.

  • Publishes JSON push envelopes to per-device topics using authenticated QoS 1 MQTT messages.
  • Adds pipelined PUBACK handling, broker flow-control support, and message-expiry properties.
  • Adds codec unit tests and an integration-style fake MQTT broker for adapter tests.

Confidence Score: 2/5

The 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

Filename Overview
src/Utopia/Messaging/Adapter/Push/Appwrite.php Implements the new adapter, but a mid-batch read failure omits every recipient not yet admitted to the in-flight window.
src/Utopia/Messaging/Helpers/MQTT.php Implements MQTT 5 packet encoding and parsing, but prematurely terminates property parsing on valid unsupported properties.
tests/Messaging/Adapter/Push/AppwriteTest.php Covers successful pipelining and rejected tokens but does not exercise a connection failure before all recipients are queued.
tests/Messaging/Helpers/MQTTTest.php Covers codec round trips and partial packets but not mixed supported and unsupported MQTT property ordering.
tests/Messaging/Adapter/Push/FakeBroker.php Provides a focused broker fixture for CONNECT, PUBLISH, PUBACK, ping, and disconnect behavior.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix 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

Comment on lines +151 to +155
} catch (\Throwable $error) {
foreach ($inflight as $token) {
$response->addResult($token, $error->getMessage());
}
return;

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

Fix in Claude Code Fix in Codex

Comment on lines +618 to +619
default:
return $properties;

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

Fix in Claude Code Fix in Codex

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.

2 participants