Update Healer Man server

This commit is contained in:
phenom committed 2026-09-23 22:25:39 -04:00
1 parent d869d6b51c
commit a64b2cb575
4 files changed
+98 -17

No files matched your search

+27
View File
@@ -4,6 +4,7 @@ import {
createCharacter,
createLocalAccount,
createOfflineSession,
clearSession,
listCharacters,
loadAccountBossCards,
saveAccountBossCards,
@@ -12,6 +13,7 @@ import {
loadCombatPresentationSettings,
loginLocalAccount,
restoreSession,
saveSession,
updateCharacterActionBindings,
updateCharacterEquipment,
updateCharacterDungeonRunCheckpoint,
@@ -165,6 +167,31 @@ describe("local profile repository", () => {
expect(restoreSession(storage)?.roles).toEqual([]);
});
it("keeps a login across browser sessions and clears it on sign-out", () => {
const persistent = new MemoryStorage();
const firstTab = new MemoryStorage();
const nextTab = new MemoryStorage();
const session = { kind: "account" as const, ownerId: "account-1", displayName: "Healer", accessToken: "server-token", roles: ["gm" as const] };
saveSession(session, persistent, firstTab);
expect(firstTab.getItem(PROFILE_STORAGE_KEYS.session)).toBeNull();
expect(restoreSession(persistent, nextTab)).toMatchObject({ ...session, roles: [] });
clearSession(persistent, nextTab);
expect(restoreSession(persistent, nextTab)).toBeNull();
});
it("moves a previous tab-scoped session into persistent storage", () => {
const persistent = new MemoryStorage();
const previousTab = new MemoryStorage();
const session = { kind: "offline" as const, ownerId: "offline-roster", displayName: "Offline" };
previousTab.setItem(PROFILE_STORAGE_KEYS.session, JSON.stringify(session));
expect(restoreSession(persistent, previousTab)).toMatchObject(session);
expect(previousTab.getItem(PROFILE_STORAGE_KEYS.session)).toBeNull();
expect(restoreSession(persistent, new MemoryStorage())).toMatchObject(session);
});
it("normalizes legacy profiles and persists progression atomically", () => {
const storage = new MemoryStorage();
storage.setItem(PROFILE_STORAGE_KEYS.database, JSON.stringify({
+37 -5
View File
@@ -429,11 +429,25 @@ export function createOfflineSession(): PlayerSession {
return { kind: "offline", ownerId: OFFLINE_OWNER_ID, displayName: "Offline" };
}
export function saveSession(session: PlayerSession, storage: StorageLike | null = browserSessionStorage()): void {
storage?.setItem(SESSION_KEY, JSON.stringify(session));
export function saveSession(
session: PlayerSession,
storage: StorageLike | null = browserLocalStorage(),
transientStorage: StorageLike | null = browserSessionStorage(),
): void {
const serialized = JSON.stringify(session);
try {
if (storage) {
storage.setItem(SESSION_KEY, serialized);
if (transientStorage !== storage) {
try { transientStorage?.removeItem(SESSION_KEY); } catch { /* Storage may be disabled. */ }
}
return;
}
} catch { /* Fall back to tab-scoped storage when persistent storage is unavailable. */ }
try { transientStorage?.setItem(SESSION_KEY, serialized); } catch { /* Storage may be disabled. */ }
}
export function restoreSession(storage: StorageLike | null = browserSessionStorage()): PlayerSession | null {
function readStoredSession(storage: StorageLike | null): PlayerSession | null {
if (!storage) return null;
try {
const session = JSON.parse(storage.getItem(SESSION_KEY) ?? "null") as Partial<PlayerSession> | null;
@@ -451,8 +465,26 @@ export function restoreSession(storage: StorageLike | null = browserSessionStora
}
}
export function clearSession(storage: StorageLike | null = browserSessionStorage()): void {
storage?.removeItem(SESSION_KEY);
export function restoreSession(
storage: StorageLike | null = browserLocalStorage(),
transientStorage: StorageLike | null = browserSessionStorage(),
): PlayerSession | null {
const persistent = readStoredSession(storage);
if (persistent) return persistent;
if (transientStorage === storage) return null;
const transient = readStoredSession(transientStorage);
if (transient) saveSession(transient, storage, transientStorage);
return transient;
}
export function clearSession(
storage: StorageLike | null = browserLocalStorage(),
transientStorage: StorageLike | null = browserSessionStorage(),
): void {
try { storage?.removeItem(SESSION_KEY); } catch { /* Storage may be disabled. */ }
if (transientStorage !== storage) {
try { transientStorage?.removeItem(SESSION_KEY); } catch { /* Storage may be disabled. */ }
}
}
export function listCharacters(ownerId: string, storage: StorageLike | null = browserLocalStorage()): CharacterProfile[] {
+7 -3
View File
@@ -38,6 +38,10 @@ class OnlineApiError extends Error {
}
}
export function isOnlineAuthenticationError(error: unknown): boolean {
return error instanceof OnlineApiError && error.status === 401;
}
let activeSession: PlayerSession | null = null;
let suppressUpload = false;
let pendingCharacters: CharacterProfile[] | null = null;
@@ -135,11 +139,11 @@ function queueCloudSave(ownerId: string, characters: readonly CharacterProfile[]
setRosterChangeListener(queueCloudSave);
export function activateOnlineSession(session: PlayerSession): void {
if (activeSession?.ownerId !== session.ownerId && saveTimer) {
export function activateOnlineSession(session: PlayerSession | null): void {
if (activeSession?.ownerId !== session?.ownerId && saveTimer) {
clearTimeout(saveTimer); saveTimer = null; flushCloudSave();
}
activeSession = session.accessToken ? session : null;
activeSession = session?.accessToken ? session : null;
}
export function retryCloudSave(): void {
+27 -9
View File
@@ -56,6 +56,7 @@ import { manastormShellRuntime } from "./manastormShellRuntimeBridge";
import {
activateOnlineSession,
hydrateOnlineSession,
isOnlineAuthenticationError,
logoutOnlineSession,
refreshOnlineSession,
} from "./onlineAccountClient";
@@ -642,15 +643,17 @@ export function selectedCharacter(state: ShellState): CharacterProfile | null {
return state.characters.find((character) => character.id === state.selectedCharacterId) ?? null;
}
if (restored.session?.accessToken) {
activateOnlineSession(restored.session);
void Promise.all([
hydrateOnlineSession(restored.session),
refreshOnlineSession(restored.session),
]).then(([characters, refreshedSession]) => {
const current = useShellStore.getState().session;
if (current?.ownerId !== restored.session?.ownerId) return;
const restoredOnlineSession = restored.session?.accessToken ? restored.session : null;
if (restoredOnlineSession) {
activateOnlineSession(restoredOnlineSession);
void (async () => {
const refreshedSession = await refreshOnlineSession(restoredOnlineSession);
if (useShellStore.getState().session?.accessToken !== restoredOnlineSession.accessToken) return;
saveSession(refreshedSession);
useShellStore.setState({ session: refreshedSession });
const characters = await hydrateOnlineSession(refreshedSession);
const current = useShellStore.getState().session;
if (current?.accessToken !== restoredOnlineSession.accessToken) return;
useShellStore.setState({
session: refreshedSession,
characters,
@@ -660,7 +663,22 @@ if (restored.session?.accessToken) {
: characters[0]?.id ?? null,
notice: "Cloud roster synchronized.",
});
}).catch((error) => {
})().catch((error) => {
if (useShellStore.getState().session?.accessToken !== restoredOnlineSession.accessToken) return;
if (isOnlineAuthenticationError(error)) {
clearSession();
activateOnlineSession(null);
useShellStore.setState({
phase: "login",
session: null,
characters: [],
bossCards: emptyBossCardCollection(),
selectedCharacterId: null,
activeCharacter: null,
notice: "Your session has expired. Please sign in again.",
});
return;
}
useShellStore.setState({
notice: error instanceof Error
? `${error.message} Using the cached roster.`