fix: flush durable outboxes after reconnect (Closes #927)
All checks were successful
CI / lint (pull_request) Successful in 1m59s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 56s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-16 01:32:11 +00:00
parent 76eed47487
commit 64e764b996
6 changed files with 92 additions and 7 deletions

View File

@ -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));

View File

@ -1307,6 +1307,7 @@
<script src="static/outbox-coordinator.js"></script>
<script src="static/background-issue-sync.js"></script>
<script src="static/issue-outbox.js"></script>
<script src="static/reconnect-outboxes.js"></script>
<script src="static/authored-outbox.js"></script>
<script src="static/offline-issue-close.js"></script>
<script src="static/offline-issue-blocker.js"></script>

View File

@ -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;

View File

@ -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',

View File

@ -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 '<script src="static/reconnect-outboxes.js"></script>' in index
assert "const reconnectOutboxes = createReconnectOutboxes({" in dashboard
assert "restoreIdentity: login => { activeFlushLogin = login; confirmedOwnerLogin = login; }" in dashboard
assert "window.addEventListener('online', reconnectOutboxes);" in dashboard

View File

@ -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",