M5: push (FCM + ntfy) + offline catch-up + background notifications
Server (gateway-plugin): - push.py: FCM (HTTP v1 service-account / legacy key) + ntfy backends; NtfyBackend.server_url for app discovery - adapter.py: push on offline broadcast + high-priority push when live; fcm.register; server_caps.push_ntfy_server; outbox now ALWAYS appends so a reconnecting app catches up on live-delivered frames (fixes empty chat after notification tap / activity recreation) - protocol.py / ws_server.py / outbox.py / plugin.yaml: M5 frames + env vars App (Kotlin CMP): - Protocol.kt: notification / fcm.register / sync frames + push caps - GatewayClient.kt: hello carries push creds; auto-sync on reconnect - IrisController.kt: banners, deep-link, notifyMessageIfBackgrounded (system notification on a regular reply when backgrounded) - Android: PlatformPush, AppBridge, IrisNotifications, NtfyListenerService, IrisFirebaseMessagingService, AndroidPush, AndroidSecureStore - Desktop: DesktopPush, DesktopSecureStore - build files + manifest (permissions, services, deep-links) Tests: 35-test tests/gateway/test_android.py suite passes (incl. new regression test_live_delivered_frame_still_parked_for_sync). E2E verified on device: push fire, reconnect sync, background notification, and message replay after ChatStore reset.
This commit is contained in:
1 parent
913ee91024
commit
2ecfe1c05c
28 files changed
+1739
-51
No files matched your search
@@ -28,6 +28,10 @@ logger = logging.getLogger(__name__)
|
||||
DEFAULT_RETENTION_HOURS = 72
|
||||
_REPLAY_LIMIT = 1000
|
||||
_PRUNE_INTERVAL_S = 3600.0
|
||||
# Row cap (docs/08 §8.3): a very busy offline period must not grow the outbox
|
||||
# without bound; the oldest rows beyond the cap are pruned and the app is
|
||||
# told (generic notification, throttled in the adapter).
|
||||
DEFAULT_MAX_ROWS = 5000
|
||||
|
||||
|
||||
class Outbox:
|
||||
@@ -38,12 +42,19 @@ class Outbox:
|
||||
``DeviceRegistry`` / ``ChannelDirectory``).
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: Path, retention_hours: int = DEFAULT_RETENTION_HOURS):
|
||||
def __init__(
|
||||
self,
|
||||
db_path: Path,
|
||||
retention_hours: int = DEFAULT_RETENTION_HOURS,
|
||||
max_rows: int = DEFAULT_MAX_ROWS,
|
||||
):
|
||||
self._db_path = Path(db_path)
|
||||
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._retention_hours = max(1, int(retention_hours))
|
||||
self._max_rows = max(1, int(max_rows))
|
||||
self._lock = threading.Lock()
|
||||
self._last_prune = 0.0
|
||||
self._overflow_pruned = 0
|
||||
self._conn = sqlite3.connect(str(self._db_path), check_same_thread=False)
|
||||
self._conn.row_factory = sqlite3.Row
|
||||
with self._lock:
|
||||
@@ -90,10 +101,37 @@ class Outbox:
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(cursor, chat_id, frame_json, now),
|
||||
)
|
||||
self._enforce_row_cap()
|
||||
self._conn.commit()
|
||||
self._maybe_prune()
|
||||
return cursor
|
||||
|
||||
def take_overflow_pruned(self) -> int:
|
||||
"""Rows pruned by the row cap since the last call (and reset to 0).
|
||||
|
||||
The adapter turns a non-zero count into a (throttled) generic
|
||||
notification so the app knows older frames are gone.
|
||||
"""
|
||||
with self._lock:
|
||||
n = self._overflow_pruned
|
||||
self._overflow_pruned = 0
|
||||
return n
|
||||
|
||||
def _enforce_row_cap(self) -> None:
|
||||
"""Drop the oldest rows beyond ``max_rows`` (caller holds the lock)."""
|
||||
row = self._conn.execute("SELECT COUNT(*) AS n FROM outbox").fetchone()
|
||||
n = int(row["n"]) if row else 0
|
||||
excess = n - self._max_rows
|
||||
if excess <= 0:
|
||||
return
|
||||
self._conn.execute(
|
||||
"DELETE FROM outbox WHERE cursor IN ("
|
||||
" SELECT cursor FROM outbox ORDER BY cursor ASC LIMIT ?)",
|
||||
(excess,),
|
||||
)
|
||||
self._overflow_pruned += excess
|
||||
logger.info("android outbox: row cap pruned %s oldest row(s)", excess)
|
||||
|
||||
def latest_cursor(self) -> int:
|
||||
"""The high-water cursor (0 when nothing has been appended)."""
|
||||
with self._lock:
|
||||
|
||||
Reference in new issue
Block a user