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:
@@ -3,7 +3,9 @@ import { invoke } from '@tauri-apps/api/core';
|
|||||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CALL_TO_PLAY_CONNECTING_MESSAGE,
|
||||||
callToPlayEvent,
|
callToPlayEvent,
|
||||||
|
callToPlayPublishErrorMessage,
|
||||||
extendDeadline,
|
extendDeadline,
|
||||||
pruneCallToPlayEvents,
|
pruneCallToPlayEvents,
|
||||||
reduceCallToPlayEvents,
|
reduceCallToPlayEvents,
|
||||||
@@ -75,6 +77,11 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
|
|||||||
const ready = peerId !== null;
|
const ready = peerId !== null;
|
||||||
setActorId(peerId);
|
setActorId(peerId);
|
||||||
setTransportReady(ready);
|
setTransportReady(ready);
|
||||||
|
if (ready) {
|
||||||
|
setError(current =>
|
||||||
|
current === CALL_TO_PLAY_CONNECTING_MESSAGE ? null : current
|
||||||
|
);
|
||||||
|
}
|
||||||
if (ready && retry !== undefined) {
|
if (ready && retry !== undefined) {
|
||||||
window.clearInterval(retry);
|
window.clearInterval(retry);
|
||||||
retry = undefined;
|
retry = undefined;
|
||||||
@@ -104,7 +111,10 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to register Call to Play listener:', 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> => {
|
): Promise<boolean> => {
|
||||||
if (actorId === null) {
|
if (actorId === null) {
|
||||||
setTransportReady(false);
|
setTransportReady(false);
|
||||||
setError('Call to Play needs an active LAN peer. Choose a game folder first.');
|
setError(CALL_TO_PLAY_CONNECTING_MESSAGE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const event = callToPlayEvent(callId, actorId, actor, action);
|
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 });
|
const accepted = await invoke<boolean>('publish_call_to_play', { event });
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
setTransportReady(false);
|
setTransportReady(false);
|
||||||
setError('Call to Play needs an active LAN peer. Choose a game folder first.');
|
setError(CALL_TO_PLAY_CONNECTING_MESSAGE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
setTransportReady(true);
|
setTransportReady(true);
|
||||||
@@ -138,7 +148,7 @@ export const useCallToPlay = (username: string): UseCallToPlay => {
|
|||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('publish_call_to_play failed:', err);
|
console.error('publish_call_to_play failed:', err);
|
||||||
setError('Could not send this Call to Play update.');
|
setError(callToPlayPublishErrorMessage(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [actor, actorId]);
|
}, [actor, actorId]);
|
||||||
|
|||||||
@@ -8,6 +8,21 @@ import {
|
|||||||
export const CHECKIN_LEAD_MS = 15 * 60_000;
|
export const CHECKIN_LEAD_MS = 15 * 60_000;
|
||||||
export const EXPIRED_RETENTION_MS = 5 * 60_000;
|
export const EXPIRED_RETENTION_MS = 5 * 60_000;
|
||||||
export const TERMINAL_RETENTION_MS = 15 * 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 = (
|
export const extendDeadline = (
|
||||||
now: number,
|
now: number,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
CALL_TO_PLAY_CONNECTING_MESSAGE,
|
||||||
CHECKIN_LEAD_MS,
|
CHECKIN_LEAD_MS,
|
||||||
EXPIRED_RETENTION_MS,
|
EXPIRED_RETENTION_MS,
|
||||||
TERMINAL_RETENTION_MS,
|
TERMINAL_RETENTION_MS,
|
||||||
activeCallCount,
|
activeCallCount,
|
||||||
|
callToPlayPublishErrorMessage,
|
||||||
extendDeadline,
|
extendDeadline,
|
||||||
phaseOf,
|
phaseOf,
|
||||||
bumpTime,
|
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', () => {
|
Deno.test('reduction is order-independent and deduplicates events and messages', () => {
|
||||||
const message = event('message-event', 'Bob', {
|
const message = event('message-event', 'Bob', {
|
||||||
SendMessage: { message_id: 'message-1', text: 'Ready?' },
|
SendMessage: { message_id: 'message-1', text: 'Ready?' },
|
||||||
|
|||||||
Reference in New Issue
Block a user