Skip to content

feat(apple): unlink background topics a later launch no longer subscribes to - #2022

Merged
ArnabChatterjee20k merged 2 commits into
feat/apple-push-apnsfrom
feat/apple-push-reconcile-topics
Oct 13, 2026
Merged

ArnabChatterjee20k merged 2 commits into
feat/apple-push-apnsfrom
feat/apple-push-reconcile-topics

Conversation

@ArnabChatterjee20k

Copy link
Copy Markdown
Member

What does this PR do?

Removes topic subscribers that an earlier launch of the app created and a later launch no longer uses. Without this, every message on such a topic wakes the device with an empty "New message" banner.

Stacked on #2000.

The problem

A background subscription creates a subscriber row on the server: "this device's target wants news". The server decides whether to push through APNs from that row, so a killed app can still be woken. The row has to outlive the process.

If version 1 of an app subscribed to news in the background and version 2 doesn't:

  1. The update doesn't touch the row, and version 2 never mentions news, so nothing deletes it.
  2. Every news message still makes the server push the placeholder to the device.
  3. The Notification Service Extension fetches only the app's current background topics, so it gets nothing, and the banner stays "New message".

unsubscribe, update(background: false), close() and sign-out already removed rows within a launch. Only a topic that quietly disappears between launches was left behind, because the SDK kept its record of linked topics in memory only.

The fix

  • The list of linked topics is saved on disk (Push.LinkedTopics), per target, in the store shared with the extension, under the same cross-process lock.
    • A link adds to it.
    • An unlink removes from it, only once the server confirms (404 counts as removed).
    • Deleting the target, on sign-out or close(), clears the target's entry.
  • Reconcile once per launch. 30 s after the device registers (Push.reconcileDelay), Push takes every live Push of the user and unlinks each saved topic that none of them has as a background subscription. It never runs in the extension.
  • A topic subscribed later is linked again as usual, so an app that subscribes from a screen opened after 30 s loses background delivery for that topic only until it subscribes.

Test Plan

  • Unit tests (24 in total, 2 new):

    • saved links survive a new instance, standing in for a relaunch, and removal works per topic and per target;
    • picking the topics no subscription claims.
  • Lint, macOS, iOS Simulator and Linux (swift:6.1-jammy, from the e2e fixture) builds; Generation suite and Twig lint.

  • iOS Simulator against a real local Appwrite, its broker and the APNs sandbox, with topic news:

    Step news subscribers on the server
    Version 1 subscribes to news in the background 1
    Version 1, after its own 30 s reconcile, still subscribed 1 (kept)
    Version 2, without news, at ~12 s 1
    Version 2 at ~37 s 0 (unlinked)
    Message to news with the app terminated no push reached the device
    Version 2 subscribing to news only after 40 s: ~34 s / ~48 s 0 / 1 (linked again)
    Message to news while it is claimed, app terminated Delivered: "News while claimed"
    Sign out 0, and the device's target deleted

Related PRs and Issues

…ibes to

Topic subscribers outlive the process so APNs can wake a killed app,
but a launch that stops subscribing to a topic, such as after an app
update, left its subscriber on the server, and every message on it
woke the device with a placeholder the extension could not fill.

Push now keeps the topics it linked per target in the shared store.
30 seconds after the device registers, once per launch, it unlinks the
saved topics no background subscription of the user claims. A topic
subscribed later is linked again.
@hansi-codes

hansi-codes Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

🔵 Tier A · Mergeable after minor fixes

The reconciliation and unlink claim checks address the earlier findings, but target deletion can still race a new background subscription and lose its APNs target.

This pull request persists APNs-linked topics per target and reconciles them after a launch, removing stale subscribers while retaining topics claimed by live background subscriptions. It also deletes a registered target when no background subscriptions remain and ensures a topic claimed during an unlink is linked again. The tests cover linked-topic persistence, target registration tracking, and stale-topic selection.

Latest changes: The new commits schedule reconciliation from any subscription, delete registered targets when no background claims remain, and re-link a topic if a same-user Push claims it while an unlink is in flight.

Verdict New comments Fixed Still open
💬 Commented 1 2 0

Note

Not approving while a bug finding is open: A new background claim can be lost during target deletion

Finding Where
🟡 A new background claim can be lost during target deletion templates/apple/Sources/Services/Push.swift.twig:1453
Fix with agent prompt
### Issue 1
templates/apple/Sources/Services/Push.swift.twig:1453
**A new background claim can be lost during target deletion**

This branch decides there are no claims before awaiting `deleteTarget`, but a background subscribe can begin while that DELETE is in flight. If its target upsert completes first, the DELETE can remove the target after `register` has already finished, leaving the new subscription without APNs delivery until another registration occurs.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
📂 Walkthrough · 2
File Change
templates/apple/Sources/Services/Push.swift.twig Schedules reconciliation from subscribe calls, handles registered targets with no remaining claims, and rechecks same-user claimants after unlink.
templates/apple/Tests/PushTests.swift.twig Adds coverage that registered target state survives topic removal until the target is deleted.
✅ Fixed since the last review · 2
  • Reconcile launches with no background subscriptions too · templates/apple/Sources/Services/Push.swift.twig:1500
  • Recheck claims across all pushes after the delete · templates/apple/Sources/Services/Push.swift.twig:1449

Reviewed the commits since c99a01a · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

hansi-codes[bot]
hansi-codes Bot previously requested changes Oct 13, 2026

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Tier B · 1 blocking finding to address. Summary

Comment thread templates/apple/Sources/Services/Push.swift.twig Outdated
Comment thread templates/apple/Sources/Services/Push.swift.twig
…recheck every Push after an unlink

A launch that dropped every background subscription never reconciled,
so the target and its subscribers stayed and kept waking the device.
Every subscribe now schedules the launch's reconcile, the store records
the targets this install registered, and with no background
subscription left the reconcile deletes the target, as close() does.

An unlink re-linked the topic only when the Push that unlinked it still
claimed it; a sibling Push of the user claiming it during the delete
lost its link. The check now covers every live Push of the user.
@hansi-codes
hansi-codes Bot dismissed their stale review October 13, 2026 13:34

The findings that requested changes are resolved. See the summary comment for what is still open.

@ArnabChatterjee20k
ArnabChatterjee20k merged commit d38eecf into feat/apple-push-apns Oct 13, 2026
58 of 61 checks passed

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Tier A · See the inline comments. Summary

guard let push = pushes.first(where: { !$0.backgroundTopics().isEmpty }) else {
// No background subscription this launch: the target and its subscribers go, as on close().
if apnsLinkedTopics.isRegistered(targetId), let push = fallback ?? pushes.first {
await deleteTarget(push, targetId: targetId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A new background claim can be lost during target deletion

This branch decides there are no claims before awaiting deleteTarget, but a background subscribe can begin while that DELETE is in flight. If its target upsert completes first, the DELETE can remove the target after register has already finished, leaving the new subscription without APNs delivery until another registration occurs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: templates/apple/Sources/Services/Push.swift.twig
Line: 1453

Comment:
**A new background claim can be lost during target deletion**

This branch decides there are no claims before awaiting `deleteTarget`, but a background subscribe can begin while that DELETE is in flight. If its target upsert completes first, the DELETE can remove the target after `register` has already finished, leaving the new subscription without APNs delivery until another registration occurs.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · concurrency · Reply if this doesn't apply.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed on the base branch, since this PR is merged into #2000: 966125e. After the DELETE, deleteTarget registers the device again and links the topics for any Push of the user that has background subscriptions by then.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don’t see that fix in the revision available here: reconcile checks for claims before awaiting deletion, and deleteTarget then performs the DELETE without re-registering any claims that appeared in the meantime. If registration’s upsert finishes before the DELETE, the target can still be removed. (reconcile and deleteTarget)

Please make sure commit 966125e16 is included in the reviewed stack; I’ll recheck once it’s visible here.

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