321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
|
import type { TelemetrySnapshot } from "./contractsTelemetry";
|
|
import type { BalanceSettings } from "./contractsBalance";
|
|
import type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
|
import type { RaceSnapshot } from "./contractsRaces";
|
|
import type { AuthSessionResponse, ForgotPasswordResponse, RegisterResponse } from "./contractsAuth";
|
|
import type { PlayerIdentitySummary } from "./contractsIdentity";
|
|
import type { ShipAutomationCatalogSnapshot } from "./contractsShipAutomation";
|
|
import type { FactionSnapshot } from "./contractsFactions";
|
|
import type { ShipSnapshot } from "./contractsShips";
|
|
import type { StationSnapshot } from "./contractsInfrastructure";
|
|
import { clearAuthSession, getAuthSession, setAuthSession } from "./authSession";
|
|
import { getEffectivePlayerIdentityId } from "./effectiveIdentitySession";
|
|
import type {
|
|
PlayerAssetAssignmentCommandRequest,
|
|
PlayerAutomationPolicyCommandRequest,
|
|
PlayerDirectiveCommandRequest,
|
|
PlayerOrganizationCommandRequest,
|
|
PlayerOrganizationMembershipCommandRequest,
|
|
PlayerPolicyCommandRequest,
|
|
PlayerStrategicIntentCommandRequest,
|
|
} from "./playerFactionCommands";
|
|
import type {
|
|
ShipDefaultBehaviorCommandRequest,
|
|
ShipOrderCommandRequest,
|
|
} from "./shipCommands";
|
|
|
|
export interface WorldStreamScope {
|
|
scopeKind?: string;
|
|
systemId?: string | null;
|
|
bubbleId?: string | null;
|
|
}
|
|
|
|
async function fetchJson<T>(input: RequestInfo | URL, init?: RequestInit, options?: { skipAuth?: boolean; skipRefresh?: boolean }): Promise<T> {
|
|
const headers = new Headers(init?.headers);
|
|
if (!options?.skipAuth) {
|
|
const session = getAuthSession();
|
|
if (session?.accessToken) {
|
|
headers.set("Authorization", `Bearer ${session.accessToken}`);
|
|
}
|
|
if (session?.roles.some((role) => role === "gm" || role === "admin")) {
|
|
const effectivePlayerId = getEffectivePlayerIdentityId();
|
|
if (effectivePlayerId) {
|
|
headers.set("X-Act-As-Player-Id", effectivePlayerId);
|
|
}
|
|
}
|
|
}
|
|
|
|
const response = await fetch(input, {
|
|
...init,
|
|
headers,
|
|
});
|
|
if (response.status === 401 && !options?.skipAuth && !options?.skipRefresh) {
|
|
const refreshed = await tryRefreshSession();
|
|
if (refreshed) {
|
|
return fetchJson<T>(input, init, { skipRefresh: true });
|
|
}
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`${init?.method ?? "GET"} ${typeof input === "string" ? input : input.toString()} failed with ${response.status}`);
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
async function tryRefreshSession(): Promise<boolean> {
|
|
const session = getAuthSession();
|
|
if (!session?.refreshToken) {
|
|
return false;
|
|
}
|
|
|
|
const response = await fetch("/api/auth/refresh", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ refreshToken: session.refreshToken }),
|
|
});
|
|
if (!response.ok) {
|
|
clearAuthSession();
|
|
return false;
|
|
}
|
|
|
|
const nextSession = await response.json() as AuthSessionResponse;
|
|
setAuthSession(nextSession);
|
|
return true;
|
|
}
|
|
|
|
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
|
return fetchJson<WorldSnapshot>("/api/world", { signal }, { skipAuth: true });
|
|
}
|
|
|
|
export function openWorldStream(
|
|
afterSequence: number,
|
|
handlers: {
|
|
onDelta: (delta: WorldDelta, rawBytes: number) => void;
|
|
onOpen?: () => void;
|
|
onError?: () => void;
|
|
},
|
|
scope?: WorldStreamScope,
|
|
) {
|
|
const query = new URLSearchParams({
|
|
afterSequence: String(afterSequence),
|
|
});
|
|
if (scope?.scopeKind) {
|
|
query.set("scopeKind", scope.scopeKind);
|
|
}
|
|
if (scope?.systemId) {
|
|
query.set("systemId", scope.systemId);
|
|
}
|
|
if (scope?.bubbleId) {
|
|
query.set("bubbleId", scope.bubbleId);
|
|
}
|
|
|
|
const stream = new EventSource(`/api/world/stream?${query.toString()}`);
|
|
stream.addEventListener("open", () => handlers.onOpen?.());
|
|
stream.addEventListener("error", () => handlers.onError?.());
|
|
stream.addEventListener("world-delta", (event) => {
|
|
const message = event as MessageEvent<string>;
|
|
handlers.onDelta(
|
|
JSON.parse(message.data) as WorldDelta,
|
|
new Blob([message.data]).size,
|
|
);
|
|
});
|
|
return stream;
|
|
}
|
|
|
|
export async function fetchTelemetry(signal?: AbortSignal) {
|
|
return fetchJson<TelemetrySnapshot>("/api/telemetry", { signal });
|
|
}
|
|
|
|
export async function fetchBalance(signal?: AbortSignal) {
|
|
return fetchJson<BalanceSettings>("/api/balance", { signal });
|
|
}
|
|
|
|
export async function updateBalance(settings: BalanceSettings) {
|
|
return fetchJson<BalanceSettings>("/api/balance", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
}
|
|
|
|
export async function createFaction(request: { factionId: string }) {
|
|
return fetchJson<FactionSnapshot>("/api/gm/factions", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function spawnShip(request: { factionId: string; systemId: string; shipId?: string | null; behaviorKind?: string | null }) {
|
|
return fetchJson<ShipSnapshot>("/api/gm/ships", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function spawnStation(request: { factionId: string; systemId: string; objective?: string | null; label?: string | null }) {
|
|
return fetchJson<StationSnapshot>("/api/gm/stations", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function resetWorld() {
|
|
return fetchJson<WorldSnapshot>("/api/world/reset", {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export async function register(request: { email: string; password: string }) {
|
|
return fetchJson<RegisterResponse>("/api/auth/register", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
}, { skipAuth: true, skipRefresh: true });
|
|
}
|
|
|
|
export async function login(request: { email: string; password: string }) {
|
|
const session = await fetchJson<AuthSessionResponse>("/api/auth/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
}, { skipAuth: true, skipRefresh: true });
|
|
setAuthSession(session);
|
|
return session;
|
|
}
|
|
|
|
export async function forgotPassword(request: { email: string }) {
|
|
return fetchJson<ForgotPasswordResponse>("/api/auth/forgot-password", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
}, { skipAuth: true, skipRefresh: true });
|
|
}
|
|
|
|
export async function resetPassword(request: { token: string; newPassword: string }) {
|
|
await fetchJson<void>("/api/auth/reset-password", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
}, { skipAuth: true, skipRefresh: true });
|
|
}
|
|
|
|
export async function fetchPlayerFaction(signal?: AbortSignal) {
|
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction", { signal });
|
|
}
|
|
|
|
export async function fetchRaces(signal?: AbortSignal) {
|
|
return fetchJson<RaceSnapshot[]>("/api/auth/races", { signal }, { skipAuth: true });
|
|
}
|
|
|
|
export async function completePlayerOnboarding(request: { name: string; raceId: string }) {
|
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/onboarding", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function fetchPlayerIdentities(signal?: AbortSignal) {
|
|
return fetchJson<PlayerIdentitySummary[]>("/api/player-faction/identities", { signal });
|
|
}
|
|
|
|
export async function fetchShipAutomationCatalog(signal?: AbortSignal) {
|
|
return fetchJson<ShipAutomationCatalogSnapshot>("/api/ships/catalog", { signal }, { skipAuth: true });
|
|
}
|
|
|
|
export async function createPlayerOrganization(request: PlayerOrganizationCommandRequest) {
|
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/organizations", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function deletePlayerOrganization(organizationId: string) {
|
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/organizations/${organizationId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
export async function updatePlayerOrganizationMembership(organizationId: string, request: PlayerOrganizationMembershipCommandRequest) {
|
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/organizations/${organizationId}/membership`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function upsertPlayerDirective(request: PlayerDirectiveCommandRequest, directiveId?: string | null) {
|
|
const path = directiveId ? `/api/player-faction/directives/${directiveId}` : "/api/player-faction/directives";
|
|
return fetchJson<PlayerFactionSnapshot>(path, {
|
|
method: directiveId ? "PUT" : "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function deletePlayerDirective(directiveId: string) {
|
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/directives/${directiveId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
export async function upsertPlayerAssignment(assetId: string, request: PlayerAssetAssignmentCommandRequest) {
|
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/assignments/${assetId}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function upsertPlayerPolicy(request: PlayerPolicyCommandRequest, policyId?: string | null) {
|
|
const path = policyId ? `/api/player-faction/policies/${policyId}` : "/api/player-faction/policies";
|
|
return fetchJson<PlayerFactionSnapshot>(path, {
|
|
method: policyId ? "PUT" : "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function upsertPlayerAutomationPolicy(request: PlayerAutomationPolicyCommandRequest, automationPolicyId?: string | null) {
|
|
const path = automationPolicyId ? `/api/player-faction/automation-policies/${automationPolicyId}` : "/api/player-faction/automation-policies";
|
|
return fetchJson<PlayerFactionSnapshot>(path, {
|
|
method: automationPolicyId ? "PUT" : "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function updatePlayerStrategicIntent(request: PlayerStrategicIntentCommandRequest) {
|
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/strategic-intent", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function enqueueShipOrder(shipId: string, request: ShipOrderCommandRequest) {
|
|
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/orders`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function updateShipDefaultBehavior(shipId: string, request: ShipDefaultBehaviorCommandRequest) {
|
|
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/default-behavior`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
|
|
export async function removeShipOrder(shipId: string, orderId: string) {
|
|
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/orders/${orderId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|