offline-push

command
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 18 Imported by: 0

README

offline-push example

Why this exists

examples/notification shows the pattern every caller of SendToGroup used to have to implement by hand: call SendToGroup, inspect the returned DeliveryResult, and if result.None() is true, remember to call your own offline transport yourself. That worked, but it put a correctness burden on every call site - forget the if result.None() branch once, in one handler, and that one notification path silently never falls back to push. There was no way for the library to catch the omission because the library never saw the decision at all.

Registry.WithOfflineChannel removes that burden. You configure one gateway.OfflineChannel on the Registry once, and SendToGroup makes the online/offline decision itself:

  • A group with a live socket somewhere gets the payload written directly. The offline channel is never touched.
  • A group with DeliveryResult.None() - no live socket anywhere in the cluster - is handed to the configured OfflineChannel automatically, on the Registry's own goroutine, without the caller's code branching on the result at all.

This example wires that up for real: Registry.SendToGroup is configured with gateway.WithOfflineChannel(offlineChannel), and offlineChannel is a genuine offline/webpush.Channel - real VAPID signing, real RFC 8291 encryption, a real HTTP POST

  • talking to a local fake push service that stands in for FCM/Mozilla autopush. Nothing in /notify's handler ever calls the push channel; it only calls SendToGroup and reports back what came out.

What to look at

  • main.go's notifyHandler - it is a plain SendToGroup call and a JSON encode of the result. Compare it to examples/notification/main.go's notifyHandler, which has an explicit if result.None() { fakeWebPush(...) } branch. That branch is gone here because the Registry does it internally now.
  • main.go's fallbackObserver - it implements the optional gateway.OfflineObserver extension (OfflineFallback(group string, err error)), which is how an application observes the outcome of a fallback that SendToGroup fired asynchronously and cannot report through its own return value. /status polls a buffer this observer fills.
  • generateDemoSubscriptionKeys - since there is no real browser in this demo to call PushManager.subscribe(), this function mints a fresh P-256 key pair and auth secret in exactly the shape PushSubscription.getKey('p256dh'/'auth') would hand an application server. /subscribe uses it to register a subscription the same way a real /api/push-subscribe endpoint would persist a browser-supplied one.
  • newFakePushService - an httptest.Server standing in for FCM/Mozilla autopush. It cannot decrypt the payload (only the "browser" holding the subscription's private key could), so it only proves the POST was genuine - non-empty encrypted body, TTL header, VAPID Authorization header - before answering 201 Created. Server logs show every push it receives.

What this does NOT demonstrate

Single actor.ActorSystem, no real cluster - identical scope caveat to examples/notification. DeliveryResult.None() here is exact because there is exactly one node and gateway.NewMemoryPresence() gives it a complete local view; it says nothing new about Remote/cross-node accuracy, which is examples/cluster's territory.

Running it

go run ./examples/offline-push/

Listens on http://127.0.0.1:8085. Open that URL in a browser for the demo page, or drive it with curl:

# 1. Notify a user with no connection and no push subscription: SendToGroup still reports
#    None and still routes to the OfflineChannel - it has no way to know in advance that
#    alice has zero subscriptions - but webpush.Channel.Deliver is then a no-op for her
#    group (empty subscription list), so no HTTP call ever leaves the process.
curl -s -X POST http://127.0.0.1:8085/notify -d '{"User":"alice","Msg":"hi"}'
# {"delivered":0,"dropped":0,"remote":0,"none":true}
# The server log gets an [offline-fallback] success=true line (the no-op "succeeded"
# trivially) but no [push-service] line, since there was nothing to POST to.

# 2. Register a (simulated) push subscription for alice, then notify again.
curl -s -X POST http://127.0.0.1:8085/subscribe -d '{"User":"alice"}'
curl -s -X POST http://127.0.0.1:8085/notify -d '{"User":"alice","Msg":"hello offline"}'
# {"delivered":0,"dropped":0,"remote":0,"none":true}
# none=true again - SendToGroup still found no live socket - but this time the Registry's
# configured OfflineChannel fires in the background. Watch the server's stdout:
#   [push-service] received encrypted push for /push/alice/<uuid>: NNNN bytes, ttl=60, auth=true
#   [offline-fallback] group="user:alice" success=true err=<nil>
# or poll it over HTTP:
curl -s http://127.0.0.1:8085/status
# [{"group":"user:alice","success":true,"at":"...")}]

# 3. Connect alice over WebSocket (open http://127.0.0.1:8085/ in a browser, section 1,
#    user "alice"), then notify her again:
curl -s -X POST http://127.0.0.1:8085/notify -d '{"User":"alice","Msg":"hello online"}'
# {"delivered":1,"dropped":0,"remote":0,"none":false}
# delivered=1, none=false: the message reached alice's socket directly. No new
# [push-service] or [offline-fallback] log line appears - the offline channel was never
# touched, exactly as the doc comment on WithOfflineChannel says it should be.

Success criteria

  • Step 1 returns none:true, produces an [offline-fallback] success=true line (the no-op Deliver call), but no [push-service] line (no subscription registered yet, so the channel has nothing to POST to).
  • Step 2 returns none:true and, within about a second, both a [push-service] line (proving a real encrypted POST left the process) and an [offline-fallback] success=true line (proving SendToGroup routed it there on its own) appear in the server log and in GET /status.
  • Step 3 returns delivered:1, none:false and produces neither log line, and the connected WebSocket client (browser demo page section "WebSocket log", or any ws client dialing /ws?user=alice) receives the literal message text.

go vet ./examples/offline-push/... is clean.

Documentation

Overview

Command offline-push demonstrates Registry.WithOfflineChannel: the library-native replacement for the old pattern where every caller of SendToGroup had to inspect DeliveryResult.None itself and remember to invoke its own offline transport (see examples/notification, which still shows that manual style for comparison).

With WithOfflineChannel configured, the Registry makes that decision internally:

  • A group with a live WebSocket connection is delivered to directly; SendToGroup returns a DeliveryResult with Delivered > 0 and the offline channel is never touched.
  • A group with no reachable socket anywhere in the cluster (DeliveryResult.None) is hallowed off, on the Registry's own goroutine, to the configured OfflineChannel - here a real github.com/StringKe/goakt-gateway/offline/webpush.Channel - without the /notify handler below ever branching on the result.

The push "service" the demo talks to is an in-process httptest.Server standing in for a real provider (FCM, Mozilla autopush, ...). It never decrypts the payload - a real push service cannot, only the browser can with the subscription's private key - it only proves that a correctly VAPID-signed, RFC 8291-encrypted POST arrived, which is everything an application-level integration test can observe without a browser in the loop. The "browser" side is stood in by generateDemoSubscriptionKeys, which mints a fresh P-256 key pair the same shape a real PushSubscription.getKey() would hand the application server.

See README.md for what problem this closes and how to drive the demo end to end.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL