diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 8e9d2a4..0f9c130 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -6962,19 +6962,25 @@
setStatus('Offline');
if (!hasContextSnapshot) await hydrateOfflineWork();
}
- function reconnectLiveData() {
- setStatus('Reconnecting…');
- contextPoller.refresh({ force: true }).then(snapshot => {
+ const reconnectOutboxes = createReconnectOutboxes({
+ refresh: async () => {
+ setStatus('Reconnecting…');
+ const snapshot = await contextPoller.refresh({ force: true });
if (!snapshot) {
offlineStatus.hidden = false;
setOfflineWorkMode(true);
- return;
+ return null;
}
offlineStatus.hidden = true;
setOfflineWorkMode(false);
if (selectedReview && offlineReview) openReviewSheet(selectedReview, reviewTrigger);
- });
- }
+ return snapshot;
+ },
+ restoreIdentity: login => { activeFlushLogin = login; confirmedOwnerLogin = login; },
+ flushIssue: flushIssueOutbox,
+ flushAuthored: flushAuthoredOutbox,
+ flushNotificationReads: flushNotificationReadOutbox,
+ });
async function setOfflineWorkEnabled(enabled) {
keepWorkOffline.checked = enabled;
offlineWorkStore.setEnabled(enabled);
@@ -7032,7 +7038,7 @@
updateDeliveryReceiptControls();
if (!navigator.onLine) await showOfflineStatus();
window.addEventListener('offline', showOfflineStatus);
- window.addEventListener('online', reconnectLiveData);
+ window.addEventListener('online', reconnectOutboxes);
qs('#refresh').addEventListener('click', load);
qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget));
diff --git a/frontend/index.html b/frontend/index.html
index 53d7377..bf33e6d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1307,6 +1307,7 @@
+
diff --git a/frontend/reconnect-outboxes.js b/frontend/reconnect-outboxes.js
new file mode 100644
index 0000000..85f1fd2
--- /dev/null
+++ b/frontend/reconnect-outboxes.js
@@ -0,0 +1,25 @@
+function createReconnectOutboxes({
+ refresh,
+ restoreIdentity,
+ flushIssue,
+ flushAuthored,
+ flushNotificationReads,
+}) {
+ return async function reconnectOutboxes() {
+ const snapshot = await refresh();
+ const freshness = snapshot?.freshness?.sections?.context;
+ const identityFresh = snapshot?.context && !snapshot.context.error &&
+ !freshness?.stale && !freshness?.degraded && !freshness?.revalidating;
+ const login = identityFresh ? String(snapshot.context.user?.login || '').trim() : '';
+ if (!login) return false;
+ restoreIdentity(login);
+ await Promise.all([
+ flushIssue(),
+ flushAuthored(),
+ flushNotificationReads(),
+ ]);
+ return true;
+ };
+}
+
+if (typeof module !== 'undefined' && module.exports) module.exports = createReconnectOutboxes;
diff --git a/frontend/service-worker.js b/frontend/service-worker.js
index c5c2774..18a21c8 100644
--- a/frontend/service-worker.js
+++ b/frontend/service-worker.js
@@ -32,6 +32,7 @@ const SHELL = [
BASE + 'static/draft-capacity-dialog.js',
BASE + 'static/outbox-coordinator.js',
BASE + 'static/issue-outbox.js',
+ BASE + 'static/reconnect-outboxes.js',
BASE + 'static/authored-outbox.js',
BASE + 'static/offline-issue-close.js',
BASE + 'static/offline-issue-blocker.js',
diff --git a/tests/test_reconnect_outboxes.py b/tests/test_reconnect_outboxes.py
new file mode 100644
index 0000000..28b91ab
--- /dev/null
+++ b/tests/test_reconnect_outboxes.py
@@ -0,0 +1,51 @@
+import json
+import subprocess
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+RECONNECT = ROOT / "frontend" / "reconnect-outboxes.js"
+
+
+def run_node(script: str) -> dict:
+ result = subprocess.run(
+ ["node", "-e", script], cwd=ROOT, text=True, capture_output=True, check=True
+ )
+ return json.loads(result.stdout)
+
+
+def test_successful_reconnect_restores_identity_and_flushes_unchanged_outboxes():
+ result = run_node(
+ f"""
+const createReconnectOutboxes = require({json.dumps(str(RECONNECT))});
+const calls = [];
+const reconnect = createReconnectOutboxes({{
+ refresh: async () => ({{context:{{user:{{login:'timmy'}}}}}}),
+ restoreIdentity: login => calls.push(['identity', login]),
+ flushIssue: () => calls.push(['issue']),
+ flushAuthored: () => calls.push(['authored']),
+ flushNotificationReads: () => calls.push(['notification-read']),
+}});
+reconnect().then(result => process.stdout.write(JSON.stringify({{result, calls}})));
+"""
+ )
+
+ assert result == {
+ "result": True,
+ "calls": [
+ ["identity", "timmy"],
+ ["issue"],
+ ["authored"],
+ ["notification-read"],
+ ],
+ }
+
+
+def test_dashboard_uses_reconnect_flush_after_live_identity_refresh():
+ index = (ROOT / "frontend" / "index.html").read_text()
+ dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
+
+ assert '' in index
+ assert "const reconnectOutboxes = createReconnectOutboxes({" in dashboard
+ assert "restoreIdentity: login => { activeFlushLogin = login; confirmedOwnerLogin = login; }" in dashboard
+ assert "window.addEventListener('online', reconnectOutboxes);" in dashboard
diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py
index caadc02..68342ab 100644
--- a/tests/test_service_worker.py
+++ b/tests/test_service_worker.py
@@ -930,6 +930,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/draft-capacity-dialog.js",
"/dashboard/static/outbox-coordinator.js",
"/dashboard/static/issue-outbox.js",
+ "/dashboard/static/reconnect-outboxes.js",
"/dashboard/static/authored-outbox.js",
"/dashboard/static/offline-issue-close.js",
"/dashboard/static/offline-issue-blocker.js",