Compare commits

...

33 Commits

Author SHA1 Message Date
CI
d00b4843a7 chore: release version v1.77.15 2025-09-10 07:29:33 +00:00
Aleksei Chichenkov
6068de2c71 Merge pull request #514 from wanderer-industries/unnecessary-rerenders
fix(Map): Fix problem with unnecessary rerenders and loads routes if …
2025-09-10 10:29:03 +03:00
DanSylvest
73da427c6b fix(Map): Fix problem with unnecessary rerenders and loads routes if move/positioning widgets. 2025-09-10 10:10:17 +03:00
CI
9b7ec0ddfe chore: [skip ci] 2025-09-08 22:07:20 +00:00
CI
c2f5f14c44 chore: release version v1.77.14 2025-09-08 22:07:20 +00:00
Dmitry Popov
0b7c3588d5 fix: Fixed issue with loading connection info 2025-09-09 00:06:49 +02:00
CI
a51fac5736 chore: [skip ci] 2025-09-07 22:27:12 +00:00
CI
726c3d0704 chore: release version v1.77.13 2025-09-07 22:27:12 +00:00
Dmitry Popov
8dd564dbd0 fix: Updated character tracking, added an extra check for offline characters to reduce errors 2025-09-08 00:26:40 +02:00
CI
e33c65cddc chore: [skip ci] 2025-09-07 19:28:25 +00:00
CI
f2fbd2ead0 chore: release version v1.77.12 2025-09-07 19:28:25 +00:00
Dmitry Popov
123a2e45eb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-07 21:27:56 +02:00
Dmitry Popov
f8d2d9c680 fix: Decreased character tracking grace period 2025-09-07 21:27:53 +02:00
CI
9dcbef9a79 chore: [skip ci] 2025-09-07 19:16:08 +00:00
CI
0b14857a12 chore: release version v1.77.11 2025-09-07 19:16:08 +00:00
Dmitry Popov
bd3d516f60 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-07 21:15:39 +02:00
Dmitry Popov
40d0bd8cea fix: Fixed CSP errors 2025-09-07 21:15:35 +02:00
CI
968deeb254 chore: [skip ci] 2025-09-04 09:17:39 +00:00
CI
959041be52 chore: release version v1.77.10 2025-09-04 09:17:39 +00:00
Dmitry Popov
3319520179 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-04 11:17:08 +02:00
Dmitry Popov
580fcf3657 fix: Removed invalid invite options 2025-09-04 11:17:04 +02:00
CI
53dae7c520 chore: [skip ci] 2025-09-04 09:11:38 +00:00
CI
6d59d709f1 chore: release version v1.77.9 2025-09-04 09:11:38 +00:00
Dmitry Popov
4343e9070c fix: Auto select following char system on start 2025-09-04 11:10:59 +02:00
CI
b62373fb5f chore: [skip ci] 2025-09-03 14:38:52 +00:00
CI
3da98f8e56 chore: release version v1.77.8 2025-09-03 14:38:52 +00:00
Dmitry Popov
494d24952e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-03 16:38:26 +02:00
Dmitry Popov
8a6b17bd7b fix: Updated character tracking 2025-09-03 16:38:23 +02:00
CI
d2e859a74e chore: [skip ci] 2025-09-03 13:03:26 +00:00
CI
4a78d55d22 chore: release version v1.77.7 2025-09-03 13:03:26 +00:00
Dmitry Popov
dc252b8c4b Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-03 15:02:57 +02:00
Dmitry Popov
c433205e89 fix: Updated character tracking 2025-09-03 15:02:53 +02:00
CI
d6bc5b57b1 chore: [skip ci] 2025-09-02 17:34:32 +00:00
26 changed files with 785 additions and 755 deletions

View File

@@ -2,6 +2,87 @@
<!-- changelog -->
## [v1.77.15](https://github.com/wanderer-industries/wanderer/compare/v1.77.14...v1.77.15) (2025-09-10)
### Bug Fixes:
* Map: Fix problem with unnecessary rerenders and loads routes if move/positioning widgets.
## [v1.77.14](https://github.com/wanderer-industries/wanderer/compare/v1.77.13...v1.77.14) (2025-09-08)
### Bug Fixes:
* Fixed issue with loading connection info
## [v1.77.13](https://github.com/wanderer-industries/wanderer/compare/v1.77.12...v1.77.13) (2025-09-07)
### Bug Fixes:
* Updated character tracking, added an extra check for offline characters to reduce errors
## [v1.77.12](https://github.com/wanderer-industries/wanderer/compare/v1.77.11...v1.77.12) (2025-09-07)
### Bug Fixes:
* Decreased character tracking grace period
## [v1.77.11](https://github.com/wanderer-industries/wanderer/compare/v1.77.10...v1.77.11) (2025-09-07)
### Bug Fixes:
* Fixed CSP errors
## [v1.77.10](https://github.com/wanderer-industries/wanderer/compare/v1.77.9...v1.77.10) (2025-09-04)
### Bug Fixes:
* Removed invalid invite options
## [v1.77.9](https://github.com/wanderer-industries/wanderer/compare/v1.77.8...v1.77.9) (2025-09-04)
### Bug Fixes:
* Auto select following char system on start
## [v1.77.8](https://github.com/wanderer-industries/wanderer/compare/v1.77.7...v1.77.8) (2025-09-03)
### Bug Fixes:
* Updated character tracking
## [v1.77.7](https://github.com/wanderer-industries/wanderer/compare/v1.77.6...v1.77.7) (2025-09-03)
### Bug Fixes:
* Updated character tracking
## [v1.77.6](https://github.com/wanderer-industries/wanderer/compare/v1.77.5...v1.77.6) (2025-09-02)

View File

@@ -1,3 +1,10 @@
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import type { PanelPosition } from '@reactflow/core';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
@@ -16,8 +23,6 @@ import ReactFlow, {
import 'reactflow/dist/style.css';
import classes from './Map.module.scss';
import { MapProvider, useMapState } from './MapProvider';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import {
ContextMenuConnection,
ContextMenuRoot,
@@ -26,14 +31,9 @@ import {
useContextMenuRootHandlers,
} from './components';
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import type { PanelPosition } from '@reactflow/core';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };

View File

@@ -1,4 +1,6 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useEventBuffer } from '@/hooks/Mapper/hooks';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
@@ -11,6 +13,20 @@ export const useMapInit = () => {
const ref = useRef({ rf, data, update });
ref.current = { update, data, rf };
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
const { rf } = ref.current;
rf.setNodes(systems.map(convertSystem2Node));
}, []);
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
const { rf } = ref.current;
rf.setEdges(connections.map(convertConnection2Edge));
}, []);
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
return useCallback(
({
systems,
@@ -24,7 +40,6 @@ export const useMapInit = () => {
hubs,
}: CommandInit) => {
const { update } = ref.current;
const { rf } = ref.current;
const updateData: Partial<MapData> = {};
@@ -63,11 +78,13 @@ export const useMapInit = () => {
update(updateData);
if (systems) {
rf.setNodes(systems.map(convertSystem2Node));
handleUpdateSystems(systems);
// rf.setNodes(systems.map(convertSystem2Node));
}
if (connections) {
rf.setEdges(connections.map(convertConnection2Edge));
handleUpdateConnections(connections);
// rf.setEdges(connections.map(convertConnection2Edge));
}
},
[],

View File

@@ -1,7 +1,7 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();

View File

@@ -1,4 +1,3 @@
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
@@ -19,8 +18,11 @@ import {
CommandUpdateSystems,
MapHandlers,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import {
useCenterSystem,
useCommandsCharacters,
useCommandsConnections,
useMapAddSystems,
@@ -28,10 +30,8 @@ import {
useMapInit,
useMapRemoveSystems,
useMapUpdateSystems,
useCenterSystem,
useSelectSystems,
} from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
const mapInit = useMapInit();
@@ -49,91 +49,87 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
useCommandsCharacters();
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated:
case Commands.characterActivityData:
case Commands.trackingCharactersData:
case Commands.updateActivity:
case Commands.updateTracking:
case Commands.userSettingsUpdated:
// do nothing
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated:
case Commands.characterActivityData:
case Commands.trackingCharactersData:
case Commands.updateActivity:
case Commands.updateTracking:
case Commands.userSettingsUpdated:
// do nothing
break;
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
},
[],
);
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
}, []);
};

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
const useThrottle = () => {
const throttleSeed = useRef<number | null>(null);

View File

@@ -3,6 +3,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
@@ -64,12 +65,8 @@ export const useLoadRoutes = ({
systems?.length,
connections,
hubs,
routesSettings,
...Object.keys(routesSettings)
.sort()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
.map(x => routesSettings[x]),
// we need make it flat recursively
...flattenValues(routesSettings),
...deps,
]);

View File

@@ -1,6 +1,7 @@
export * from './useClipboard';
export * from './useConfirmPopup';
export * from './useEventBuffer';
export * from './useHotkey';
export * from './usePageVisibility';
export * from './useSkipContextMenu';
export * from './useThrottle';
export * from './useConfirmPopup';

View File

@@ -0,0 +1,41 @@
import debounce from 'lodash.debounce';
import { useCallback, useRef } from 'react';
export type UseEventBufferHandler<T> = (event: T) => void;
export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
// @ts-ignore
const eventsBufferRef = useRef<T[]>([]);
const eventTick = useCallback(
debounce(() => {
if (eventsBufferRef.current.length === 0) {
return;
}
const event = eventsBufferRef.current.shift()!;
handler(event);
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
if (eventsBufferRef.current.length > 0) {
eventTick();
}
}, 10),
[],
);
const eventTickRef = useRef(eventTick);
eventTickRef.current = eventTick;
// @ts-ignore
const handleEvent = useCallback(event => {
if (!eventTickRef.current) {
return;
}
eventsBufferRef.current.push(event);
eventTickRef.current();
}, []);
return { handleEvent };
};

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { CommandInit } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { CommandInit } from '@/hooks/Mapper/types';
import { useCallback } from 'react';
export const useMapInit = () => {
const { update } = useMapRootState();

View File

@@ -1,4 +1,3 @@
import { ForwardedRef, useImperativeHandle } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
@@ -8,24 +7,25 @@ import {
CommandCharactersUpdated,
CommandCharacterUpdated,
CommandCommentAdd,
CommandCommentRemoved,
CommandInit,
CommandLinkSignatureToSystem,
CommandMapUpdated,
CommandPingAdded,
CommandPingCancelled,
CommandPresentCharacters,
CommandRemoveConnections,
CommandRemoveSystems,
CommandRoutes,
Commands,
CommandSignaturesUpdated,
CommandTrackingCharactersData,
CommandUpdateConnection,
CommandUpdateSystems,
CommandUserSettingsUpdated,
Commands,
MapHandlers,
CommandCommentRemoved,
CommandPingAdded,
CommandPingCancelled,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { ForwardedRef, useImperativeHandle } from 'react';
import {
useCommandComments,
@@ -39,9 +39,9 @@ import {
useUserRoutes,
} from './api';
import { useCommandsActivity } from './api/useCommandsActivity';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { DetailedKill } from '../../types/kills';
import { useCommandsActivity } from './api/useCommandsActivity';
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapInit = useMapInit();
@@ -63,127 +63,123 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const { pingAdded, pingCancelled } = useCommandPings();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.updateActivity:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
emitMapEvent({ name: type, data });
},
};
},
[],
);
emitMapEvent({ name: type, data });
},
};
}, []);
};

View File

@@ -1,7 +1,8 @@
import { useEventBuffer } from '@/hooks/Mapper/hooks';
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { RefObject, useCallback, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
// const inIndex = 0;
// const prevEventTime = +new Date();
@@ -10,10 +11,28 @@ const LAST_VERSION_KEY = 'wandererLastVersion';
// @ts-ignore
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
const visible = usePageVisibility();
const wasHiddenOnce = useRef(false);
const visibleRef = useRef(visible);
visibleRef.current = visible;
// @ts-ignore
const handleBufferedEvent = useCallback(({ type, body }) => {
if (!visibleRef.current) {
return;
}
handlerRefs.forEach(ref => {
if (!ref.current) {
return;
}
ref.current?.command(type, body);
});
}, []);
const { handleEvent: handleMapEvent } = useEventBuffer<any>(handleBufferedEvent);
// TODO - do not delete THIS code it needs for debug
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
// defaultValue: false,
@@ -54,52 +73,6 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
[hooksRef.current],
);
// @ts-ignore
const eventsBufferRef = useRef<{ type; body }[]>([]);
const eventTick = useCallback(
debounce(() => {
if (eventsBufferRef.current.length === 0) {
return;
}
const { type, body } = eventsBufferRef.current.shift()!;
handlerRefs.forEach(ref => {
if (!ref.current) {
return;
}
ref.current?.command(type, body);
});
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
if (eventsBufferRef.current.length > 0) {
eventTick();
}
}, 10),
[],
);
const eventTickRef = useRef(eventTick);
eventTickRef.current = eventTick;
// @ts-ignore
const handleMapEvent = useCallback(({ type, body }) => {
// TODO - do not delete THIS code it needs for debug
// const currentTime = +new Date();
// const timeDiff = currentTime - prevEventTime;
// prevEventTime = currentTime;
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
if (!eventTickRef.current || !visibleRef.current) {
return;
}
eventsBufferRef.current.push({ type, body });
eventTickRef.current();
}, []);
useEffect(() => {
if (!visible && !wasHiddenOnce.current) {
wasHiddenOnce.current = true;

View File

@@ -0,0 +1,132 @@
const TYPE_ORDER = [
'undefined',
'null',
'boolean',
'number',
'bigint',
'string',
'symbol',
'function',
'date',
'regexp',
'other',
] as const;
type TypeTag = (typeof TYPE_ORDER)[number];
const getTypeTag = (v: unknown): TypeTag => {
if (v === undefined) return 'undefined';
if (v === null) return 'null';
const t = typeof v;
if (t === 'boolean' || t === 'number' || t === 'bigint' || t === 'string' || t === 'symbol' || t === 'function')
return t as TypeTag;
const tag = Object.prototype.toString.call(v);
if (tag === '[object Date]') return 'date';
if (tag === '[object RegExp]') return 'regexp';
return 'other';
};
const cmp = (a: unknown, b: unknown): number => {
const ta = getTypeTag(a);
const tb = getTypeTag(b);
if (ta !== tb) return TYPE_ORDER.indexOf(ta) - TYPE_ORDER.indexOf(tb);
switch (ta) {
case 'undefined':
case 'null':
return 0;
case 'boolean':
return (a as boolean) === (b as boolean) ? 0 : a ? 1 : -1;
case 'number': {
const na = a as number,
nb = b as number;
const aIsNaN = Number.isNaN(na),
bIsNaN = Number.isNaN(nb);
if (aIsNaN || bIsNaN) return aIsNaN && bIsNaN ? 0 : aIsNaN ? 1 : -1; // NaN в конец чисел
return na === nb ? 0 : na < nb ? -1 : 1;
}
case 'bigint': {
const ba = a as bigint,
bb = b as bigint;
return ba === bb ? 0 : ba < bb ? -1 : 1;
}
case 'string':
return (a as string).localeCompare(b as string);
case 'symbol': {
const da = (a as symbol).description ?? '';
const db = (b as symbol).description ?? '';
return da.localeCompare(db);
}
case 'function':
// @ts-ignore
return ((a as Function).name || '').localeCompare((b as Function).name || '');
case 'date':
return (a as Date).getTime() - (b as Date).getTime();
case 'regexp':
return a!.toString().localeCompare(b!.toString());
default:
return String(a).localeCompare(String(b));
}
};
const isIterable = (v: unknown): v is Iterable<unknown> =>
v != null && typeof (v as any)[Symbol.iterator] === 'function';
const pushTypedArrayValues = (v: unknown, out: unknown[]) => {
if (ArrayBuffer.isView(v) && !(v instanceof DataView)) {
// @ts-ignore
out.push(...(v as ArrayLike<number> as any));
return true;
}
return false;
};
/**
* Generate this func with ChatGPT 5. Cause it pure func and looks like what i need
* May be in net we can find smtng like that
* @param input
*/
export const flattenValues = (input: unknown): unknown[] => {
const out: unknown[] = [];
const seen = new WeakSet<object>();
const visit = (v: unknown): void => {
const tag = getTypeTag(v);
if (tag !== 'other') {
out.push(v);
return;
}
if (v && typeof v === 'object') {
if (seen.has(v)) return;
seen.add(v);
if (pushTypedArrayValues(v, out)) return;
if (v instanceof Map) {
for (const val of v.values()) visit(val);
return;
}
if (v instanceof Set) {
for (const val of v.values()) visit(val);
return;
}
if (Array.isArray(v) || isIterable(v)) {
for (const item of v as Iterable<unknown>) visit(item);
return;
}
for (const key of Object.keys(v)) {
// @ts-ignore
visit((v as never)[key]);
}
return;
}
out.push(v);
};
visit(input);
return out.sort(cmp);
};

View File

@@ -37,13 +37,14 @@ defmodule WandererApp.Character.Tracker do
}
@pause_tracking_timeout :timer.minutes(60 * 10)
@offline_timeout :timer.minutes(10)
@offline_timeout :timer.minutes(5)
@online_error_timeout :timer.minutes(10)
@ship_error_timeout :timer.minutes(10)
@location_error_timeout :timer.minutes(10)
@online_forbidden_ttl :timer.seconds(7)
@offline_check_delay_ttl :timer.seconds(15)
@online_limit_ttl :timer.seconds(7)
@forbidden_ttl :timer.seconds(5)
@forbidden_ttl :timer.seconds(10)
@limit_ttl :timer.seconds(5)
@location_limit_ttl :timer.seconds(1)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
@@ -71,18 +72,19 @@ defmodule WandererApp.Character.Tracker do
WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
|> case do
nil ->
WandererApp.Cache.insert(
"character:#{character_id}:last_online_time",
DateTime.utc_now()
)
:ok
last_online_time ->
duration = DateTime.diff(DateTime.utc_now(), last_online_time, :millisecond)
if duration >= @offline_timeout do
pause_tracking(character_id)
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Cache.delete("character:#{character_id}:last_online_time")
:ok
else
@@ -186,7 +188,9 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Character.get_character_state!()
|> update_online()
def update_online(%{track_online: true, character_id: character_id} = character_state) do
def update_online(
%{track_online: true, character_id: character_id, is_online: is_online} = character_state
) do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
when not is_nil(access_token) ->
@@ -197,8 +201,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
_ ->
# Monitor cache for potential evictions before ESI call
case WandererApp.Esi.get_character_online(eve_id,
access_token: access_token,
character_id: character_id
@@ -211,70 +213,67 @@ defmodule WandererApp.Character.Tracker do
"character:#{character_id}:last_online_time",
DateTime.utc_now()
)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
else
# Delay next online updates for offline characters
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
ttl: @offline_check_delay_ttl
)
end
if online.online == true && online.online != is_online do
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
WandererApp.Cache.delete("character:#{character_id}:corporation_info_forbidden")
end
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
try do
WandererApp.Character.update_character(character_id, online)
rescue
error ->
Logger.error("DB_ERROR: Failed to update character in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_online"
)
if online.online != is_online do
try do
WandererApp.Character.update_character(character_id, online)
rescue
error ->
Logger.error("DB_ERROR: Failed to update character in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_online"
)
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
end
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
end
update = %{
character_state
| is_online: online.online,
track_ship: online.online,
track_location: online.online
}
try do
WandererApp.Character.update_character_state(character_id, %{
character_state
| is_online: online.online,
track_ship: online.online,
track_location: online.online
})
rescue
error ->
Logger.error("DB_ERROR: Failed to update character state in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_state"
)
try do
WandererApp.Character.update_character_state(character_id, update)
rescue
error ->
Logger.error("DB_ERROR: Failed to update character state in database",
character_id: character_id,
error: inspect(error),
operation: "update_character_state"
)
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
# Re-raise to maintain existing error handling
reraise error, __STACKTRACE__
end
end
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_online",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character online tracking failed #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
endpoint: "character_online"
)
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
@@ -301,28 +300,6 @@ defmodule WandererApp.Character.Tracker do
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_online",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character online tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_online",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:online_forbidden",
true,
@@ -332,15 +309,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_online",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character online tracking failed",
Logger.error("ESI_ERROR: Character online tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -417,21 +386,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_info",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character info tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
endpoint: "character_info"
)
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
@@ -443,33 +397,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_info",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character info tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_info",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
@@ -479,21 +406,13 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_info",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
ttl: @forbidden_ttl
)
Logger.error("ESI_ERROR: Character info tracking failed",
Logger.error("ESI_ERROR: Character info tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -540,21 +459,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_ship",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character ship tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
endpoint: "character_ship"
)
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
@@ -573,34 +477,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_ship",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character ship tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_ship",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
@@ -610,15 +486,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_ship",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character ship tracking failed",
Logger.error("ESI_ERROR: Character ship tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -641,14 +509,6 @@ defmodule WandererApp.Character.Tracker do
{:error, error}
_ ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_ship",
error_type: "wrong_response",
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character ship tracking failed - wrong response",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -711,14 +571,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_location",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character location tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -740,34 +592,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_location",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character location tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_location",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
@@ -777,15 +601,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_location",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character location tracking failed",
Logger.error("ESI_ERROR: Character location tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -804,14 +620,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
_ ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_location",
error_type: "wrong_response",
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character location tracking failed - wrong response",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -873,14 +681,6 @@ defmodule WandererApp.Character.Tracker do
:ok
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_wallet",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.warning("ESI_ERROR: Character wallet tracking failed",
character_id: character_id,
tracking_pool: tracking_pool,
@@ -899,34 +699,6 @@ defmodule WandererApp.Character.Tracker do
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
reset_seconds =
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
remaining =
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
# Emit telemetry for tracking
:telemetry.execute(
[:wanderer_app, :esi, :rate_limited],
%{
reset_duration: reset_timeout,
count: 1
},
%{
endpoint: "character_wallet",
tracking_pool: tracking_pool,
character_id: character_id
}
)
Logger.warning("ESI_RATE_LIMITED: Character wallet tracking rate limited",
character_id: character_id,
tracking_pool: tracking_pool,
endpoint: "character_wallet",
reset_seconds: reset_seconds,
remaining_requests: remaining
)
WandererApp.Cache.put(
"character:#{character_id}:wallet_forbidden",
true,
@@ -936,15 +708,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
{:error, error} ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_wallet",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character wallet tracking failed",
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -960,15 +724,7 @@ defmodule WandererApp.Character.Tracker do
{:error, :skipped}
error ->
# Emit telemetry for tracking
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
endpoint: "character_wallet",
error_type: error,
tracking_pool: tracking_pool,
character_id: character_id
})
Logger.error("ESI_ERROR: Character wallet tracking failed",
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -1073,6 +829,7 @@ defmodule WandererApp.Character.Tracker do
)
when old_corporation_id != corporation_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
true ->
@@ -1112,6 +869,17 @@ defmodule WandererApp.Character.Tracker do
state
|> Map.merge(%{corporation_id: corporation_id})
{:error, :error_limited, headers} ->
reset_timeout = get_reset_timeout(headers)
WandererApp.Cache.put(
"character:#{character_id}:corporation_info_forbidden",
true,
ttl: reset_timeout
)
state
error ->
Logger.warning(
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",

View File

@@ -13,10 +13,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
}
@check_start_queue_interval :timer.seconds(1)
@garbage_collection_interval :timer.minutes(15)
@garbage_collection_interval :timer.minutes(5)
@untrack_characters_interval :timer.minutes(5)
@inactive_character_timeout :timer.minutes(10)
@untrack_character_timeout :timer.minutes(10)
@inactive_character_timeout :timer.minutes(5)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -116,13 +115,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def add_to_untrack_queue(map_id, character_id) do
if not WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
WandererApp.Cache.insert(
"#{map_id}:#{character_id}:untrack_requested",
DateTime.utc_now()
)
end
WandererApp.Cache.insert_or_update(
"character_untrack_queue",
[{map_id, character_id}],
@@ -134,8 +126,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def remove_from_untrack_queue(map_id, character_id) do
WandererApp.Cache.delete("#{map_id}:#{character_id}:untrack_requested")
WandererApp.Cache.insert_or_update(
"character_untrack_queue",
[],
@@ -239,50 +229,32 @@ defmodule WandererApp.Character.TrackerManager.Impl do
WandererApp.Cache.lookup!("character_untrack_queue", [])
|> Task.async_stream(
fn {map_id, character_id} ->
untrack_timeout_reached =
if WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
untrack_requested =
WandererApp.Cache.lookup!(
"#{map_id}:#{character_id}:untrack_requested",
DateTime.utc_now()
)
remove_from_untrack_queue(map_id, character_id)
duration = DateTime.diff(DateTime.utc_now(), untrack_requested, :millisecond)
duration >= @untrack_character_timeout
else
false
end
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
Logger.debug(fn -> "Untrack timeout reached: #{inspect(untrack_timeout_reached)}" end)
{:ok, character_state} =
WandererApp.Character.Tracker.update_settings(character_id, %{
map_id: map_id,
track: false
})
if untrack_timeout_reached do
remove_from_untrack_queue(map_id, character_id)
{:ok, character} = WandererApp.Character.get_character(character_id)
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
{:ok, _updated} =
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
ship: character.ship,
ship_name: character.ship_name,
ship_item_id: character.ship_item_id,
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
})
{:ok, character_state} =
WandererApp.Character.Tracker.update_settings(character_id, %{
map_id: map_id,
track: false
})
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, _updated} =
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
ship: character.ship,
ship_name: character.ship_name,
ship_item_id: character.ship_item_id,
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
})
WandererApp.Character.update_character_state(character_id, character_state)
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
end
WandererApp.Character.update_character_state(character_id, character_state)
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,

View File

@@ -18,7 +18,7 @@ defmodule WandererApp.Character.TrackerPool do
@update_location_interval :timer.seconds(1)
@update_online_interval :timer.seconds(5)
@check_offline_characters_interval :timer.minutes(2)
@check_offline_characters_interval :timer.minutes(5)
@check_online_errors_interval :timer.minutes(1)
@check_ship_errors_interval :timer.minutes(1)
@check_location_errors_interval :timer.minutes(1)
@@ -124,7 +124,7 @@ defmodule WandererApp.Character.TrackerPool do
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
# Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
Process.send_after(self(), :update_location, 300)
Process.send_after(self(), :update_ship, 500)
Process.send_after(self(), :update_info, 1500)
@@ -176,11 +176,15 @@ defmodule WandererApp.Character.TrackerPool do
try do
characters
|> Enum.each(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
character_id
])
end)
|> Task.async_stream(
fn character_id ->
WandererApp.Character.Tracker.update_online(character_id)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
|> Enum.each(fn _result -> :ok end)
rescue
e ->
Logger.error("""
@@ -234,17 +238,7 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Task.async_stream(
fn character_id ->
if WandererApp.Character.can_pause_tracking?(character_id) do
WandererApp.TaskWrapper.start_link(
WandererApp.Character.Tracker,
:check_offline,
[
character_id
]
)
else
:ok
end
WandererApp.Character.Tracker.check_offline(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
@@ -397,11 +391,15 @@ defmodule WandererApp.Character.TrackerPool do
try do
characters
|> Enum.each(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
character_id
])
end)
|> Task.async_stream(
fn character_id ->
WandererApp.Character.Tracker.update_location(character_id)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
|> Enum.each(fn _result -> :ok end)
rescue
e ->
Logger.error("""
@@ -434,11 +432,15 @@ defmodule WandererApp.Character.TrackerPool do
try do
characters
|> Enum.each(fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
character_id
])
end)
|> Task.async_stream(
fn character_id ->
WandererApp.Character.Tracker.update_ship(character_id)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
|> Enum.each(fn _result -> :ok end)
rescue
e ->
Logger.error("""
@@ -473,9 +475,7 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
character_id
])
WandererApp.Character.Tracker.update_info(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
@@ -519,9 +519,7 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Task.async_stream(
fn character_id ->
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
character_id
])
WandererApp.Character.Tracker.update_wallet(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),

View File

@@ -213,86 +213,100 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
def update_characters(%{map_id: map_id} = state) do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
try do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|> Enum.filter(fn character_id -> character_id in presence_character_ids end)
|> Enum.map(fn character_id ->
Task.start_link(fn ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_tracking_status(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
presence_character_ids
|> Task.async_stream(
fn character_id ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_tracking_status(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
character_updates
|> Enum.filter(fn update -> update != :skip end)
|> Enum.map(fn update ->
update
|> case do
{:character_location, location_info, old_location_info} ->
update_location(
character_id,
location_info,
old_location_info,
state
)
character_updates
|> Enum.filter(fn update -> update != :skip end)
|> Enum.map(fn update ->
update
|> case do
{:character_location, location_info, old_location_info} ->
update_location(
character_id,
location_info,
old_location_info,
state
)
:broadcast
:broadcast
{:character_ship, _info} ->
:broadcast
{:character_ship, _info} ->
:broadcast
{:character_online, _info} ->
:broadcast
{:character_online, _info} ->
:broadcast
{:character_tracking, _info} ->
:broadcast
{:character_tracking, _info} ->
:broadcast
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids] |> Enum.uniq()
end
)
:broadcast
:broadcast
{:character_corporation, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
{:character_corporation, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids] |> Enum.uniq()
end
)
:broadcast
:broadcast
_ ->
:skip
end
end)
|> Enum.filter(fn update -> update != :skip end)
|> Enum.uniq()
|> Enum.each(fn update ->
case update do
:broadcast ->
update_character(map_id, character_id)
_ ->
:skip
end
end)
|> Enum.filter(fn update -> update != :skip end)
|> Enum.uniq()
|> Enum.each(fn update ->
case update do
:broadcast ->
update_character(map_id, character_id)
_ ->
:ok
end
end)
_ ->
:ok
end
end)
:ok
:ok
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_characters: #{inspect(reason)}")
end)
end)
rescue
e ->
Logger.error("""
[Map Server] update_characters => exception: #{Exception.message(e)}
#{Exception.format_stacktrace(__STACKTRACE__)}
""")
end
end
defp update_character(map_id, character_id) do

View File

@@ -32,7 +32,7 @@ defmodule WandererApp.Map.Server.Impl do
@backup_state_timeout :timer.minutes(1)
@update_presence_timeout :timer.seconds(5)
@update_characters_timeout :timer.seconds(1)
@update_tracked_characters_timeout :timer.seconds(1)
@update_tracked_characters_timeout :timer.minutes(1)
def new(), do: __struct__()
def new(args), do: __struct__(args)
@@ -96,7 +96,13 @@ defmodule WandererApp.Map.Server.Impl do
)
Process.send_after(self(), :update_characters, @update_characters_timeout)
Process.send_after(self(), :update_tracked_characters, 100)
Process.send_after(
self(),
:update_tracked_characters,
@update_tracked_characters_timeout
)
Process.send_after(self(), :update_presence, @update_presence_timeout)
Process.send_after(self(), :cleanup_connections, 5_000)
Process.send_after(self(), :cleanup_systems, 10_000)

View File

@@ -94,7 +94,7 @@ defmodule WandererApp.Maps do
end
end
def load_characters(map, user_id) do
def load_characters(map, user_id) when not is_nil(map) do
{:ok, user_characters} =
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
@@ -117,6 +117,8 @@ defmodule WandererApp.Maps do
{:ok, %{characters: characters}}
end
def load_characters(_map, _user_id), do: {:ok, %{characters: []}}
def map_character(
%{
name: name,

View File

@@ -8,13 +8,13 @@ defmodule WandererApp.MapChainPassagesRepo do
to
)
|> case do
{:ok, connection} ->
{:ok, %{inserted_at: inserted_at} = _connection} when not is_nil(inserted_at) ->
{:ok, from_passages} =
WandererApp.Api.MapChainPassages.by_connection(%{
map_id: map_id,
from: from,
to: to,
after: connection.inserted_at
after: inserted_at
})
{:ok, to_passages} =
@@ -22,7 +22,7 @@ defmodule WandererApp.MapChainPassagesRepo do
map_id: map_id,
from: to,
to: from,
after: connection.inserted_at
after: inserted_at
})
from_passages =
@@ -39,7 +39,7 @@ defmodule WandererApp.MapChainPassagesRepo do
{:ok, passages}
{:error, _error} ->
_error ->
{:ok, []}
end
end

View File

@@ -318,11 +318,18 @@ defmodule WandererAppWeb.AdminLive do
end
defp apply_action(socket, :add_invite_link, _params, uri) do
invite_types =
if socket.assigns.map_subscriptions_enabled? do
[%{label: "User", id: :user}, %{label: "Admin", id: :admin}]
else
[%{label: "User", id: :user}]
end
socket
|> assign(:active_page, :admin)
|> assign(:uri, URI.parse(uri))
|> assign(:page_title, "Add Invite Link")
|> assign(:invite_types, [%{label: "User", id: :user}, %{label: "Admin", id: :admin}])
|> assign(:invite_types, invite_types)
|> assign(:valid_types, [
%{label: "1D", id: 1},
%{label: "1W", id: 7},

View File

@@ -703,6 +703,18 @@ defmodule WandererAppWeb.MapCoreEventHandler do
Process.send_after(self(), %{event: :load_map_pings}, 200)
Process.send_after(
self(),
%{
event: :maybe_select_system,
payload: %{
character_id: nil,
solar_system_id: nil
}
},
200
)
if needs_tracking_setup do
Process.send_after(self(), %{event: :show_tracking}, 10)

View File

@@ -44,16 +44,24 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
current_user: current_user,
tracked_characters: tracked_characters,
map_id: map_id,
map_user_settings: map_user_settings
map_user_settings: map_user_settings,
main_character_eve_id: main_character_eve_id,
following_character_eve_id: following_character_eve_id
}
} = socket
) do
character =
tracked_characters
|> Enum.find(fn tracked_character -> tracked_character.id == character_id end)
if is_nil(character_id) do
tracked_characters
|> Enum.find(fn tracked_character ->
tracked_character.eve_id == (following_character_eve_id || main_character_eve_id)
end)
else
tracked_characters
|> Enum.find(fn tracked_character -> tracked_character.id == character_id end)
end
is_user_character =
not is_nil(character)
is_user_character = not is_nil(character)
is_select_on_spash =
map_user_settings
@@ -61,10 +69,9 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_following =
case WandererApp.MapUserSettingsRepo.get(map_id, current_user.id) do
{:ok, %{following_character_eve_id: following_character_eve_id}}
when not is_nil(following_character_eve_id) ->
is_user_character && following_character_eve_id == character.eve_id
case is_user_character && not is_nil(following_character_eve_id) do
true ->
following_character_eve_id == character.eve_id
_ ->
false
@@ -77,8 +84,17 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
else
# Always select the system when auto-select is enabled (following or select_on_spash).
# The frontend will handle deselecting other systems
#
select_solar_system_id =
if not is_nil(solar_system_id) do
"#{solar_system_id}"
else
{:ok, character} = WandererApp.Character.get_map_character(map_id, character.id)
"#{character.solar_system_id}"
end
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|> MapEventHandler.push_map_event("select_system", select_solar_system_id)
end
end

View File

@@ -11,7 +11,7 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
require Logger
# 30 minutes
@grace_period_ms :timer.minutes(30)
@grace_period_ms :timer.minutes(10)
@check_remove_queue_interval :timer.seconds(30)
defstruct pending_removals: %{}, timers: %{}, to_remove: []

View File

@@ -70,7 +70,8 @@ defmodule WandererAppWeb.Router do
"'self'",
"https://api.appzi.io",
"https://www.googletagmanager.com",
"https://www.google-analytics.com"
"https://www.google-analytics.com",
"https://*.google-analytics.com"
]
# Define sandbox values individually to ensure proper spacing

View File

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