Compare commits

...

11 Commits

Author SHA1 Message Date
achichenkov
ea14432d3b Merge remote-tracking branch 'origin/main' 2025-03-04 13:03:47 +03:00
achichenkov
880b8b6aad fix(Map): little bit up performance for windows manager 2025-03-04 13:03:28 +03:00
CI
2a0d7654e7 chore: release version v1.53.4
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-04 08:26:26 +00:00
guarzo
4eb1f641ae fix: add retry on kills retrieval (#207) 2025-03-04 11:13:59 +04:00
alpha02x
2da5a243ec fix: add missing masses to wh sizes const (#215) 2025-03-04 11:10:00 +04:00
guarzo
5ac8ccbe5c refactor: split up node hooks (#173)
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-03-01 13:45:34 +04:00
CI
0568533550 chore: release version v1.53.3
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-27 16:55:52 +00:00
achichenkov
178abc2af2 fix(Map): little bit up performance for windows manager 2025-02-27 17:17:12 +03:00
CI
adb2a5f459 chore: release version v1.53.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-27 10:36:38 +00:00
Dmitry Popov
ada1571e1e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-02-27 10:15:23 +01:00
Dmitry Popov
5931c00ff3 chore: release version v1.53.0 2025-02-27 10:15:20 +01:00
27 changed files with 615 additions and 186 deletions

View File

@@ -2,6 +2,31 @@
<!-- changelog -->
## [v1.53.4](https://github.com/wanderer-industries/wanderer/compare/v1.53.3...v1.53.4) (2025-03-04)
### Bug Fixes:
* add retry on kills retrieval (#207)
* add missing masses to wh sizes const (#215)
## [v1.53.3](https://github.com/wanderer-industries/wanderer/compare/v1.53.2...v1.53.3) (2025-02-27)
### Bug Fixes:
* Map: little bit up performance for windows manager
## [v1.53.2](https://github.com/wanderer-industries/wanderer/compare/v1.53.1...v1.53.2) (2025-02-27)
## [v1.53.1](https://github.com/wanderer-industries/wanderer/compare/v1.53.0...v1.53.1) (2025-02-26)

View File

@@ -33,6 +33,7 @@ import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import { ResizableAreaNode } from '@/hooks/Mapper/components/map/components/ResizableAreaNode';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
@@ -59,6 +60,20 @@ const initialNodes: Node<SolarSystemRawType>[] = [
// },
// type: 'custom',
// },
{
id: '1231213',
type: 'resizableAreaNode',
width: 200,
height: 100,
position: { x: 100, y: 100 },
data: {
width: 200,
height: 100,
bgColor: 'rgba(255, 0, 0, 0.2)',
// этот коллбэк переопределим позже
onResize: () => {},
},
},
];
const initialEdges = [
@@ -128,6 +143,7 @@ const MapComp = ({
const nodeTypes = useMemo(() => {
return {
custom: nodeComponent,
resizableAreaNode: ResizableAreaNode,
};
}, [nodeComponent]);
@@ -192,6 +208,9 @@ const MapComp = ({
changes[0].selected = getNodes().filter(node => node.selected).length === 1;
}
// eslint-disable-next-line no-console
console.log('JOipP', `changes`, changes);
const nextChanges = changes.reduce((acc, change) => {
return [...acc, change];
}, [] as NodeChange[]);

View File

@@ -0,0 +1,54 @@
import { NodeProps } from 'reactflow';
import { Rnd } from 'react-rnd';
export type ResizableAreaData = {
width: number;
height: number;
bgColor: string;
onResize: (nodeId: string, newWidth: number, newHeight: number) => void;
};
export const ResizableAreaNode = ({ id, data, selected, dragging, ...rest }: NodeProps<ResizableAreaData>) => {
const { width = 200, height = 100, bgColor = 'rgba(255, 0, 0, 0.2)', onResize } = data;
// eslint-disable-next-line no-console
// console.log('JOipP', `ResizableAreaNode`, data);
return (
<div
style={{
position: 'relative',
width,
height,
border: selected ? '2px solid red' : '2px dashed #999',
backgroundColor: bgColor,
pointerEvents: dragging ? 'none' : 'auto',
}}
{...rest}
>
<Rnd
enableUserSelectHack={false}
disableDragging={true}
style={{ width: '100%', height: '100%' }}
size={{ width, height }}
onResizeStop={(e, direction, ref, delta, position) => {
const newWidth = parseFloat(ref.style.width);
const newHeight = parseFloat(ref.style.height);
onResize?.(id, newWidth, newHeight);
}}
enableResizing={{
top: true,
right: true,
bottom: true,
left: true,
topRight: true,
bottomRight: true,
bottomLeft: true,
topLeft: true,
}}
>
<div style={{ width: '100%', height: '100%', padding: 8 }}>
Resizable Area: {width} x {height}
</div>
</Rnd>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from './ResizableAreaNode';

View File

@@ -2,6 +2,7 @@ import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/Sy
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
import { useMemo } from 'react';
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
@@ -17,17 +18,44 @@ type KillsBookmarkTooltipProps = {
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
// Limit the kills shown to match the killsCount parameter
const limitedKills = useMemo(() => {
if (!detailedKills || detailedKills.length === 0) return [];
return detailedKills.slice(0, killsCount);
}, [detailedKills, killsCount]);
if (!killsCount || limitedKills.length === 0 || !systemId || isLoading) return null;
// Calculate a reasonable height for the tooltip based on the number of kills
// but cap it to avoid excessively large tooltips
const maxKillsToShow = Math.min(limitedKills.length, 20);
const tooltipHeight = Math.max(200, Math.min(500, maxKillsToShow * 35));
const tooltipContent = (
<div style={{ width: '100%', minWidth: '300px', overflow: 'hidden' }}>
<SystemKillsContent
kills={detailedKills}
systemNameMap={systemNameMap}
onlyOneSystem={true}
autoSize={true}
limit={killsCount}
/>
<div
style={{
width: '400px',
height: `${tooltipHeight}px`,
maxHeight: '500px',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<div className="p-2 border-b border-stone-700 bg-stone-800 text-stone-200 font-medium">
System Kills ({limitedKills.length})
</div>
<div className="flex-1 overflow-hidden">
<SystemKillsContent
kills={limitedKills}
systemNameMap={systemNameMap}
onlyOneSystem={true}
// Don't use autoSize here as we want the virtual scroller to handle scrolling
autoSize={false}
// We've already limited the kills to match killsCount
limit={undefined}
/>
</div>
</div>
);

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks/useSolarSystemLogic';
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,

View File

@@ -753,8 +753,11 @@ export const SHIP_SIZES_SIZE = {
export const SHIP_MASSES_SIZE: Record<number, ShipSizeStatus> = {
5_000_000: ShipSizeStatus.small,
62_000_000: ShipSizeStatus.medium,
300_000_000: ShipSizeStatus.large,
375_000_000: ShipSizeStatus.large,
1_000_000_000: ShipSizeStatus.freight,
1_350_000_000: ShipSizeStatus.capital,
1_800_000_000: ShipSizeStatus.capital,
2_000_000_000: ShipSizeStatus.capital,
};

View File

@@ -58,7 +58,11 @@ export const useMapInit = () => {
update(updateData);
if (systems) {
rf.setNodes(systems.map(convertSystem2Node));
// rf.setNod7es(systems.map(convertSystem2Node));
const prev = rf.getNodes();
const areaNodes = prev.filter(x => x.type !== 'resizableAreaNode');
rf.setNodes([...areaNodes, ...systems.map(convertSystem2Node)]);
}
if (connections) {

View File

@@ -1,3 +1,11 @@
export * from './useMapHandlers';
export * from './useUpdateNodes';
export * from './useNodesEdgesState';
export * from './useBackgroundVars';
export * from './useKillsCounter';
export * from './useSystemName';
export * from './useNodesEdgesState';
export * from './useSolarSystemNode';
export * from './useUnsplashedSignatures';
export * from './useUpdateNodes';
export * from './useNodeKillsCount';

View File

@@ -27,13 +27,12 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
const filteredKills = useMemo(() => {
if (!allKills || allKills.length === 0) return [];
return [...allKills]
.sort((a, b) => {
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
return bTime - aTime;
})
.slice(0, 10);
// Sort kills by time, most recent first, but don't limit the number of kills
return [...allKills].sort((a, b) => {
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
return bTime - aTime;
});
}, [allKills]);
return {

View File

@@ -0,0 +1,31 @@
import { useMemo } from 'react';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
interface UseLabelsInfoParams {
labels: string | null;
linkedSigPrefix: string | null;
isShowLinkedSigId: boolean;
}
export type LabelInfo = {
id: string;
shortName: string;
};
function sortedLabels(labels: string[]): LabelInfo[] {
if (!labels) return [];
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
}
export function useLabelsInfo({ labels, linkedSigPrefix, isShowLinkedSigId }: UseLabelsInfoParams) {
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
const labelCustom = useMemo(() => {
if (isShowLinkedSigId && linkedSigPrefix) {
return labelsManager.customLabel ? `${linkedSigPrefix}${labelsManager.customLabel}` : linkedSigPrefix;
}
return labelsManager.customLabel;
}, [linkedSigPrefix, isShowLinkedSigId, labelsManager]);
return { labelsInfo, labelCustom };
}

View File

@@ -0,0 +1,42 @@
import { useEffect, useState, useCallback } from 'react';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
interface Kill {
solar_system_id: number | string;
kills: number;
}
interface MapEvent {
name: Commands;
data?: any;
payload?: Kill[];
}
export function useNodeKillsCount(
systemId: number | string,
initialKillsCount: number | null
): number | null {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
useEffect(() => {
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
const handleEvent = useCallback((event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(
kill => kill.solar_system_id.toString() === systemId.toString()
);
if (killForSystem && typeof killForSystem.kills === 'number') {
setKillsCount(killForSystem.kills);
}
return true;
}
return false;
}, [systemId]);
useMapEventListener(handleEvent);
return killsCount;
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { MapSolarSystemType } from '../map.types';
import { NodeProps } from 'reactflow';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
@@ -7,19 +7,12 @@ import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
import { CharacterTypeRaw, Commands, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
import { useMapEventListener } from '@/hooks/Mapper/events';
export type LabelInfo = {
id: string;
shortName: string;
};
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
import { useSystemName } from './useSystemName';
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
@@ -34,11 +27,6 @@ const SpaceToClass: Record<string, string> = {
[Spaces.Gallente]: 'Gallente',
};
function sortedLabels(labels: string[]): LabelInfo[] {
if (!labels) return [];
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
}
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
const localCounterCharacters = useMemo(() => {
return nodeVars.charactersInSystem
@@ -127,21 +115,19 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
const linkedSigPrefix = useMemo(() => (linkedSigEveId ? linkedSigEveId.split('-')[0] : null), [linkedSigEveId]);
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
const labelCustom = useMemo(() => {
if (isShowLinkedSigId && linkedSigPrefix) {
return labelsManager.customLabel ? `${linkedSigPrefix}${labelsManager.customLabel}` : linkedSigPrefix;
}
return labelsManager.customLabel;
}, [linkedSigPrefix, isShowLinkedSigId, labelsManager]);
const { labelsInfo, labelCustom } = useLabelsInfo({
labels,
linkedSigPrefix,
isShowLinkedSigId,
});
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo(() => {
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
}, [charactersInSystem, userCharacters]);
const hasUserCharacters = useMemo(
() => charactersInSystem.some(x => userCharacters.includes(x.eve_id)),
[charactersInSystem, userCharacters],
);
const dbClick = useDoubleClick(() => {
outCommand({
@@ -153,54 +139,19 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null;
const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) {
return '';
}
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
}
return temporary_name;
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name: solar_system_name || '',
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
});
const systemName = useMemo(() => {
if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName;
}
return solar_system_name;
}, [isTempSystemNameEnabled, solar_system_name, computedTemporaryName]);
const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures);
const customName = useMemo(() => {
if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name;
}
if (solar_system_name !== name && name) {
return name;
}
return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
}
return prepareUnsplashedChunks(
systemSigs
.filter(s => s.group === 'Wormhole' && !s.linked_system)
.map(s => ({
eve_id: s.eve_id,
type: s.type,
custom_info: s.custom_info,
kind: s.kind,
name: s.name,
group: s.group,
})) as UnsplashedSignatureType[],
);
}, [isShowUnsplashedSignatures, systemSigs]);
// Ensure hubs are always strings.
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const nodeVars: SolarSystemNodeVars = {
@@ -225,12 +176,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
dbClick,
sortedStatics,
effectName: effect_name,
regionName: region_name,
solarSystemId: solar_system_id.toString(),
solarSystemName: solar_system_name,
locked,
hubs: hubsAsStrings,
name: name,
name,
isConnecting,
hoverNodeId,
charactersInSystem,
@@ -239,6 +188,8 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
isThickConnections,
classTitle: class_title,
temporaryName: computedTemporaryName,
regionName: region_name,
solarSystemName: solar_system_name,
};
return nodeVars;
@@ -281,25 +232,3 @@ export interface SolarSystemNodeVars {
classTitle: string | null;
temporaryName?: string | null;
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
useEffect(() => {
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
useMapEventListener(event => {
if (event.name === Commands.killsUpdated && event.data?.toString() === systemId.toString()) {
//@ts-ignore
if (event.payload && typeof event.payload.kills === 'number') {
// @ts-ignore
setKillsCount(event.payload.kills);
}
return true;
}
return false;
});
return killsCount;
}

View File

@@ -0,0 +1,49 @@
// useSystemName.ts
import { useMemo } from 'react';
interface UseSystemNameParams {
isTempSystemNameEnabled: boolean;
temporary_name?: string | null;
solar_system_name: string;
isShowLinkedSigIdTempName: boolean;
linkedSigPrefix: string | null;
name?: string | null;
}
export function useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name,
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
}: UseSystemNameParams) {
const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) {
return '';
}
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
}
return temporary_name ?? '';
}, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]);
const systemName = useMemo(() => {
if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName;
}
return solar_system_name;
}, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]);
const customName = useMemo(() => {
if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name;
}
if (solar_system_name !== name && name) {
return name;
}
return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
return { systemName, computedTemporaryName, customName };
}

View File

@@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { SystemSignature } from '@/hooks/Mapper/types';
import { prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
export function useUnsplashedSignatures(systemSigs: SystemSignature[], isShowUnsplashedSignatures: boolean) {
return useMemo(() => {
if (!isShowUnsplashedSignatures) {
return {
unsplashedLeft: [] as SystemSignature[],
unsplashedRight: [] as SystemSignature[],
};
}
const chunks = prepareUnsplashedChunks(
systemSigs
.filter(s => s.group === 'Wormhole' && !s.linked_system)
.map(s => ({
eve_id: s.eve_id,
type: s.type,
custom_info: s.custom_info,
kind: s.kind,
name: s.name,
group: s.group,
})) as UnsplashedSignatureType[],
);
const [unsplashedLeft, unsplashedRight] = chunks;
return { unsplashedLeft, unsplashedRight };
}, [isShowUnsplashedSignatures, systemSigs]);
}

View File

@@ -6,9 +6,9 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
const useThrottle = () => {
const throttleSeed = useRef<number | null>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const throttleFunction = useRef((func: any, delay = 200) => {
if (!throttleSeed.current) {
// Call the callback immediately for the first time
func();
throttleSeed.current = setTimeout(() => {
throttleSeed.current = null;
@@ -75,7 +75,7 @@ export const useUpdateNodes = (nodes: Node<SolarSystemRawType>[]) => {
const visibleNodes = new Set(nodes.filter(x => isNodeVisible(x, viewport)).map(x => x.id));
update({ visibleNodes });
}, [nodes]);
}, [getViewport, nodes, update]);
useOnViewportChange({
onChange: () => throttle(updateNodesVisibility.bind(this)),
@@ -84,5 +84,5 @@ export const useUpdateNodes = (nodes: Node<SolarSystemRawType>[]) => {
useEffect(() => {
updateNodesVisibility();
}, [nodes]);
}, [nodes, updateNodesVisibility]);
};

View File

@@ -1,9 +1,10 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
import { useMapEventListener, MapEvent } from '@/hooks/Mapper/events';
interface UseSystemKillsProps {
systemId?: string;
@@ -18,11 +19,8 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH
const byId: Record<string, DetailedKill> = {};
for (const kill of [...existing, ...incoming]) {
if (!kill.kill_time) {
continue;
}
if (!kill.kill_time) continue;
const killTimeMs = new Date(kill.kill_time).valueOf();
if (killTimeMs >= cutoff) {
byId[kill.killmail_id] = kill;
}
@@ -31,15 +29,42 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH
return Object.values(byId);
}
interface DetailedKillsEvent extends MapEvent<Commands> {
payload: Record<string, DetailedKill[]>;
}
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
const { data, update } = useMapRootState();
const { detailedKills = {}, systems = [] } = data;
const [settings] = useKillsWidgetSettings();
const excludedSystems = settings.excludedSystems;
// When showing all visible kills, filter out excluded systems;
// when showAllVisible is false, ignore the exclusion filter.
const updateDetailedKills = useCallback(
(newKillsMap: Record<string, DetailedKill[]>) => {
update(prev => {
const oldKills = prev.detailedKills ?? {};
const updated = { ...oldKills };
for (const [sid, killsArr] of Object.entries(newKillsMap)) {
updated[sid] = killsArr;
}
return { ...prev, detailedKills: updated };
}, true);
},
[update],
);
useMapEventListener((event: MapEvent<Commands>) => {
if (event.name === Commands.detailedKillsUpdated) {
const detailedEvent = event as DetailedKillsEvent;
if (systemId && !Object.keys(detailedEvent.payload).includes(systemId.toString())) {
return false;
}
updateDetailedKills(detailedEvent.payload);
return true;
}
return false;
});
const effectiveSystemIds = useMemo(() => {
if (showAllVisible) {
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
@@ -49,7 +74,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0);
const mergeKillsIntoGlobal = useCallback(
@@ -64,10 +88,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
updated[sid] = combined;
}
return {
...prev,
detailedKills: updated,
};
return { ...prev, detailedKills: updated };
});
},
[update, sinceHours],
@@ -95,7 +116,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
since_hours: sinceHours,
};
} else {
// If there's no system and not showing all, do nothing
setIsLoading(false);
return;
}
@@ -105,14 +125,11 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
data: requestData,
});
// Single system => `resp.kills`
if (resp?.kills) {
const arr = resp.kills as DetailedKill[];
const sid = systemId ?? 'unknown';
mergeKillsIntoGlobal({ [sid]: arr });
}
// multiple systems => `resp.systems_kills`
else if (resp?.systems_kills) {
} else if (resp?.systems_kills) {
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
} else {
console.warn('[useSystemKills] Unexpected kills response =>', resp);
@@ -142,7 +159,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
} else if (systemId) {
return detailedKills[systemId] ?? [];
} else if (didFallbackFetch.current) {
// if we already did a fallback, we may have data for multiple systems
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
}
return [];
@@ -153,9 +169,8 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
useEffect(() => {
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
didFallbackFetch.current = true;
// Cancel any queued debounced calls, then do the fallback.
debouncedFetchKills.cancel();
fetchKills(true); // forceFallback => fetch as though showAllVisible is true
fetchKills(true);
}
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
@@ -164,14 +179,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
if (showAllVisible || systemId) {
debouncedFetchKills();
// Clean up the debounce on unmount or changes
return () => debouncedFetchKills.cancel();
}
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]);
const refetch = useCallback(() => {
debouncedFetchKills.cancel();
fetchKills(); // immediate (non-debounced) call
fetchKills();
}, [debouncedFetchKills, fetchKills]);
return {

View File

@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'
import styles from './WindowManager.module.scss';
import debounce from 'lodash.debounce';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import fastDeepEqual from 'fast-deep-equal';
const MIN_WINDOW_SIZE = 100;
const SNAP_THRESHOLD = 10;
@@ -100,6 +101,8 @@ export const WindowManager: React.FC<WindowManagerProps> = ({
);
const refPrevSize = useRef({ w: 0, h: 0 });
const ref = useRef({ windows, viewPort, onChange });
ref.current = { windows, viewPort, onChange };
useEffect(() => {
if (!viewPort) {
@@ -110,6 +113,16 @@ export const WindowManager: React.FC<WindowManagerProps> = ({
}, [viewPort]);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const next = initialWindows.map(({ content, ...x }) => x);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const prev = ref.current.windows.map(({ content, ...x }) => x);
// Here we avoid unnecessary renders if changes was emitted from here.
if (fastDeepEqual(next, prev)) {
return;
}
setWindows(initialWindows.slice(0));
}, [initialWindows]);
@@ -120,9 +133,6 @@ export const WindowManager: React.FC<WindowManagerProps> = ({
const startMousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const startWindowStateRef = useRef<{ x: number; y: number; width: number; height: number }>(DefaultWindowState);
const ref = useRef({ windows, viewPort, onChange });
ref.current = { windows, viewPort, onChange };
const onDebouncedChange = useMemo(() => {
return debounce(() => {
ref.current.onChange?.({

View File

@@ -50,4 +50,8 @@ export default {
render(hooks) {
this._rootEl.render(<Mapper hooks={hooks} />);
},
destroyed() {
this._rootEl.unmount();
},
};

View File

@@ -16,6 +16,7 @@
"@shopify/draggable": "^1.1.3",
"clsx": "^2.1.1",
"daisyui": "^4.11.1",
"fast-deep-equal": "^3.1.3",
"live_select": "file:../deps/live_select",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
@@ -31,6 +32,7 @@
"react-event-hook": "^3.1.2",
"react-flow-renderer": "^10.3.17",
"react-hook-form": "^7.53.1",
"react-rnd": "^10.5.2",
"react-usestateref": "^1.0.9",
"reactflow": "^11.11.4",
"tailwindcss": "^3.3.6",

View File

@@ -1568,6 +1568,11 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
@@ -2844,7 +2849,7 @@ lines-and-columns@^1.1.6:
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
"live_select@file:../deps/live_select":
version "1.4.2"
version "1.5.4"
locate-path@^6.0.0:
version "6.0.0"
@@ -3139,10 +3144,10 @@ path-type@^5.0.0:
integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==
"phoenix@file:../deps/phoenix":
version "1.7.14"
version "1.7.20"
"phoenix_html@file:../deps/phoenix_html":
version "4.1.0"
version "4.2.1"
"phoenix_live_view@file:../deps/phoenix_live_view":
version "0.20.17"
@@ -3340,6 +3345,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
re-resizable@6.11.2:
version "6.11.2"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.11.2.tgz#2e8f7119ca3881d5b5aea0ffa014a80e5c1252b3"
integrity sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==
react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
@@ -3356,6 +3366,14 @@ react-dom@^18.3.1:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-draggable@4.4.6:
version "4.4.6"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e"
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"
react-error-boundary@^4.0.13:
version "4.0.13"
resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz"
@@ -3402,6 +3420,15 @@ react-refresh@^0.14.2:
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz"
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
react-rnd@^10.5.2:
version "10.5.2"
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.5.2.tgz#47a22c104fb640dae71f149e2c005c879de833bd"
integrity sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==
dependencies:
re-resizable "6.11.2"
react-draggable "4.4.6"
tslib "2.6.2"
react-transition-group@^4.4.1:
version "4.4.5"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"
@@ -3878,7 +3905,7 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tslib@^2.6.2:
tslib@2.6.2, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==

View File

@@ -0,0 +1,18 @@
defmodule WandererApp.Utils.HttpUtil do
@moduledoc """
Utility functions for HTTP operations and error handling.
"""
@doc """
Determines if an HTTP error is retriable.
Returns `true` for common transient errors like timeouts and server errors (500, 502, 503, 504).
"""
def retriable_error?(:timeout), do: true
def retriable_error?("Unexpected status: 500"), do: true
def retriable_error?("Unexpected status: 502"), do: true
def retriable_error?("Unexpected status: 503"), do: true
def retriable_error?("Unexpected status: 504"), do: true
def retriable_error?("Request failed"), do: true
def retriable_error?(_), do: false
end

View File

@@ -7,6 +7,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
use Retry
alias WandererApp.Zkb.KillsProvider.{Parser, KillsCache, ZkbApi}
alias WandererApp.Utils.HttpUtil
@page_size 200
@max_pages 2
@@ -190,9 +191,28 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
defp parse_partial(_other, _cutoff_dt), do: :skip
defp fetch_full_killmail(k_id, k_hash) do
case WandererApp.Esi.get_killmail(k_id, k_hash) do
{:ok, full_km} -> {:ok, full_km}
{:error, reason} -> {:error, reason}
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_killmail(k_id, k_hash) do
{:ok, full_km} ->
{:ok, full_km}
{:error, :timeout} ->
Logger.warning("[Fetcher] ESI get_killmail timeout => kill_id=#{k_id}, retrying...")
raise "ESI timeout, will retry"
{:error, :not_found} ->
Logger.warning("[Fetcher] ESI get_killmail not_found => kill_id=#{k_id}")
{:error, :not_found}
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.warning("[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}")
raise "ESI error: #{inspect(reason)}, will retry"
else
Logger.warning("[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}")
{:error, reason}
end
end
end
end

View File

@@ -10,6 +10,11 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
require Logger
alias WandererApp.Zkb.KillsProvider.KillsCache
alias WandererApp.Utils.HttpUtil
use Retry
# Maximum retries for enrichment calls
@max_enrichment_retries 2
@doc """
Merges the 'partial' from zKB and the 'full' killmail from ESI, checks its time
@@ -254,12 +259,33 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
nil -> km
0 -> km
eve_id ->
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, %{"name" => char_name}} ->
Map.put(km, name_key, char_name)
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, %{"name" => char_name}} ->
{:ok, char_name}
_ ->
km
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
raise "Character info timeout, will retry"
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}" end)
raise "Character info error: #{inspect(reason)}, will retry"
else
Logger.debug(fn -> "[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}" end)
:skip
end
end
end
case result do
{:ok, char_name} -> Map.put(km, name_key, char_name)
_ -> km
end
end
end
@@ -269,18 +295,36 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
nil -> km
0 -> km
corp_id ->
case WandererApp.Esi.get_corporation_info(corp_id) do
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_corporation_info(corp_id) do
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
{:ok, {ticker, corp_name}}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Corporation info timeout, retrying => id=#{corp_id}" end)
raise "Corporation info timeout, will retry"
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}" end)
raise "Corporation info error: #{inspect(reason)}, will retry"
else
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
:skip
end
end
end
case result do
{:ok, {ticker, corp_name}} ->
km
|> Map.put(ticker_key, ticker)
|> Map.put(name_key, corp_name)
{:error, reason} ->
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
km
_ ->
km
_ -> km
end
end
end
@@ -290,14 +334,36 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
nil -> km
0 -> km
alliance_id ->
case WandererApp.Esi.get_alliance_info(alliance_id) do
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.Esi.get_alliance_info(alliance_id) do
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
{:ok, {alliance_ticker, alliance_name}}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Alliance info timeout, retrying => id=#{alliance_id}" end)
raise "Alliance info timeout, will retry"
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}" end)
raise "Alliance info error: #{inspect(reason)}, will retry"
else
Logger.debug(fn -> "[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}" end)
:skip
end
end
end
case result do
{:ok, {alliance_ticker, alliance_name}} ->
km
|> Map.put(ticker_key, alliance_ticker)
|> Map.put(name_key, alliance_name)
_ ->
km
_ -> km
end
end
end
@@ -307,13 +373,31 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
nil -> km
0 -> km
type_id ->
case WandererApp.CachedInfo.get_ship_type(type_id) do
{:ok, nil} -> km
{:ok, %{name: ship_name}} -> Map.put(km, name_key, ship_name)
{:error, reason} ->
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
km
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
case WandererApp.CachedInfo.get_ship_type(type_id) do
{:ok, nil} -> :skip
{:ok, %{name: ship_name}} -> {:ok, ship_name}
{:error, :timeout} ->
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
raise "Ship type timeout, will retry"
{:error, :not_found} ->
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.debug(fn -> "[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}" end)
raise "Ship type error: #{inspect(reason)}, will retry"
else
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
:skip
end
end
end
case result do
{:ok, ship_name} -> Map.put(km, name_key, ship_name)
_ -> km
end
end

View File

@@ -7,8 +7,11 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
require Logger
alias WandererApp.Zkb.KillsProvider.Parser
alias WandererApp.Esi
alias WandererApp.Utils.HttpUtil
use Retry
@heartbeat_interval 1_000
@max_esi_retries 3
# Called by `KillsProvider.handle_connect`
def handle_connect(_status, _headers, %{connected: _} = state) do
@@ -69,14 +72,39 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
case Esi.get_killmail(kill_id, kill_hash) do
{:ok, full_esi_data} ->
# Merge partial zKB fields (like totalValue) onto ESI data
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
Parser.parse_and_store_killmail(enriched)
result = retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
case Esi.get_killmail(kill_id, kill_hash) do
{:ok, full_esi_data} ->
# Merge partial zKB fields (like totalValue) onto ESI data
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
Parser.parse_and_store_killmail(enriched)
:ok
{:error, :timeout} ->
Logger.warning("[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying...")
raise "ESI timeout, will retry"
{:error, :not_found} ->
Logger.warning("[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}")
:skip
{:error, reason} ->
if HttpUtil.retriable_error?(reason) do
Logger.warning("[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}")
raise "ESI error: #{inspect(reason)}, will retry"
else
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
:skip
end
end
end
case result do
:ok -> :ok
:skip -> :skip
{:error, reason} ->
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
Logger.error("[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}")
:skip
end
end

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.53.1"
@version "1.53.4"
def project do
[