fix(call-to-play): explain peer startup state

Treat an unavailable actor ID or an explicit not-ready result as normal peer
startup and tell the user that LAN connection is still in progress. Clear that
message when snapshot registration succeeds.

Map obsolete, missing-history, and active-history-limit store failures to
distinct guidance without marking a healthy transport unavailable. Preserve a
generic message for unexpected publish failures.

Test Plan:
- just frontend-test
- just build
- git diff --cached --check
This commit is contained in:
2026-07-23 18:07:08 +02:00
parent 8d3affe19c
commit 2c204ac258
3 changed files with 59 additions and 4 deletions
@@ -3,7 +3,9 @@ import { invoke } from '@tauri-apps/api/core';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import {
CALL_TO_PLAY_CONNECTING_MESSAGE,
callToPlayEvent,
callToPlayPublishErrorMessage,
extendDeadline,
pruneCallToPlayEvents,
reduceCallToPlayEvents,
@@ -75,6 +77,11 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
const ready = peerId !== null;
setActorId(peerId);
setTransportReady(ready);
if (ready) {
setError(current =>
current === CALL_TO_PLAY_CONNECTING_MESSAGE ? null : current
);
}
if (ready && retry !== undefined) {
window.clearInterval(retry);
retry = undefined;
@@ -104,7 +111,10 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
}
} catch (err) {
console.error('Failed to register Call to Play listener:', err);
if (!cancelled) setError('Call to Play networking is unavailable.');
if (!cancelled) {
setTransportReady(false);
setError('Call to Play networking is unavailable.');
}
}
};
@@ -122,7 +132,7 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
): Promise<boolean> => {
if (actorId === null) {
setTransportReady(false);
setError('Call to Play needs an active LAN peer. Choose a game folder first.');
setError(CALL_TO_PLAY_CONNECTING_MESSAGE);
return false;
}
const event = callToPlayEvent(callId, actorId, actor, action);
@@ -130,7 +140,7 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
const accepted = await invoke<boolean>('publish_call_to_play', { event });
if (!accepted) {
setTransportReady(false);
setError('Call to Play needs an active LAN peer. Choose a game folder first.');
setError(CALL_TO_PLAY_CONNECTING_MESSAGE);
return false;
}
setTransportReady(true);
@@ -138,7 +148,7 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
return true;
} catch (err) {
console.error('publish_call_to_play failed:', err);
setError('Could not send this Call to Play update.');
setError(callToPlayPublishErrorMessage(err));
return false;
}
}, [actor, actorId]);
@@ -8,6 +8,21 @@ import {
export const CHECKIN_LEAD_MS = 15 * 60_000;
export const EXPIRED_RETENTION_MS = 5 * 60_000;
export const TERMINAL_RETENTION_MS = 15 * 60_000;
export const CALL_TO_PLAY_CONNECTING_MESSAGE =
'Call to Play is still connecting to the LAN. Try again in a moment.';
export const callToPlayPublishErrorMessage = (error: unknown): string => {
const detail = error instanceof Error ? error.message : String(error);
if (detail.includes('Call to Play event is obsolete')
|| detail.includes('Call to Play history is missing')
) {
return 'This Call to Play has expired or already finished.';
}
if (detail.includes('Call to Play event history is full')) {
return 'Call to Play has reached its active update limit. Start or cancel an active call, then try again.';
}
return 'Could not send this Call to Play update.';
};
export const extendDeadline = (
now: number,
@@ -1,8 +1,10 @@
import {
CALL_TO_PLAY_CONNECTING_MESSAGE,
CHECKIN_LEAD_MS,
EXPIRED_RETENTION_MS,
TERMINAL_RETENTION_MS,
activeCallCount,
callToPlayPublishErrorMessage,
extendDeadline,
phaseOf,
bumpTime,
@@ -178,6 +180,34 @@ Deno.test('adding time extends from the current deadline or the current time', (
);
});
Deno.test('publish failures distinguish startup and store outcomes', () => {
assertEquals(
CALL_TO_PLAY_CONNECTING_MESSAGE,
'Call to Play is still connecting to the LAN. Try again in a moment.',
'peer startup message',
);
assertEquals(
callToPlayPublishErrorMessage('Call to Play event is obsolete'),
'This Call to Play has expired or already finished.',
'obsolete call message',
);
assertEquals(
callToPlayPublishErrorMessage('Call to Play history is missing'),
'This Call to Play has expired or already finished.',
'missing expired history message',
);
assertEquals(
callToPlayPublishErrorMessage(new Error('Call to Play event history is full')),
'Call to Play has reached its active update limit. Start or cancel an active call, then try again.',
'active history limit message',
);
assertEquals(
callToPlayPublishErrorMessage('channel closed'),
'Could not send this Call to Play update.',
'unexpected failure message',
);
});
Deno.test('reduction is order-independent and deduplicates events and messages', () => {
const message = event('message-event', 'Bob', {
SendMessage: { message_id: 'message-1', text: 'Ready?' },