forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
d869d6b51c
commit
a64b2cb575
4 files changed
+98
-17
No files matched your search
@@ -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({
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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
@@ -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.`
|
||||
|
||||
Reference in new issue
Block a user