mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-02 05:52:55 +00:00
Compare commits
23 Commits
tracking-f
...
v1.77.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3da98f8e56 | ||
|
|
494d24952e | ||
|
|
8a6b17bd7b | ||
|
|
d2e859a74e | ||
|
|
4a78d55d22 | ||
|
|
dc252b8c4b | ||
|
|
c433205e89 | ||
|
|
d6bc5b57b1 | ||
|
|
280a286266 | ||
|
|
d5c18b5de3 | ||
|
|
7452e5d011 | ||
|
|
71674b0d52 | ||
|
|
5b4824bd5d | ||
|
|
deda16a7da | ||
|
|
0b7c067de7 | ||
|
|
0d0db8c129 | ||
|
|
9f1b7994a3 | ||
|
|
378df0ac70 | ||
|
|
0e4a132f69 | ||
|
|
631746375d | ||
|
|
7dc01dad54 | ||
|
|
8a9807d3e5 | ||
|
|
39df3c97ce |
62
CHANGELOG.md
62
CHANGELOG.md
@@ -2,6 +2,68 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added grace period to reduce false-positive cases
|
||||
|
||||
## [v1.77.5](https://github.com/wanderer-industries/wanderer/compare/v1.77.4...v1.77.5) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* resolve tracking issues
|
||||
|
||||
## [v1.77.4](https://github.com/wanderer-industries/wanderer/compare/v1.77.3...v1.77.4) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* pr feedback
|
||||
|
||||
* ensure pub/sub occurs after acl api change
|
||||
|
||||
## [v1.77.3](https://github.com/wanderer-industries/wanderer/compare/v1.77.2...v1.77.3) (2025-08-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
## [v1.77.2](https://github.com/wanderer-industries/wanderer/compare/v1.77.1...v1.77.2) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal 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 };
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,6 +54,7 @@ defmodule WandererApp.Application do
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererAppWeb.PresenceGracePeriodManager,
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
@check_start_queue_interval :timer.seconds(1)
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@untrack_characters_interval :timer.minutes(5)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
|
||||
@@ -54,6 +54,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
true
|
||||
)
|
||||
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[character_id],
|
||||
@@ -69,29 +71,25 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def stop_tracking(state, character_id) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
{:ok, %{start_time: start_time}} <-
|
||||
WandererApp.Character.get_character_state(character_id, false) do
|
||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
[],
|
||||
fn tracked_characters ->
|
||||
tracked_characters
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
@@ -129,7 +127,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
[{map_id, character_id} | untrack_queue]
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def update_tracked_characters(map_id) do
|
||||
Task.start_link(fn ->
|
||||
{:ok, map_tracked_character_ids} =
|
||||
{:ok, all_map_tracked_character_ids} =
|
||||
map_id
|
||||
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|
||||
|> case do
|
||||
@@ -67,16 +67,10 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
map_active_tracked_characters =
|
||||
map_tracked_character_ids
|
||||
|> Enum.filter(fn character -> character in tracked_characters end)
|
||||
|
||||
{:ok, old_map_tracked_characters} =
|
||||
{:ok, actual_map_tracked_characters} =
|
||||
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
|
||||
|
||||
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
|
||||
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
@@ -86,8 +80,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
|
||||
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
@@ -95,7 +87,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
is_character_map_active?(map_id, character_id)
|
||||
character_map_active = is_character_map_active?(map_id, character_id)
|
||||
|
||||
character_map_active
|
||||
|> untrack_character(map_id, character_id)
|
||||
end)
|
||||
end
|
||||
@@ -222,8 +216,7 @@ defmodule WandererApp.Map.Server.CharactersImpl 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)
|
||||
presence_character_ids
|
||||
|> Enum.map(fn character_id ->
|
||||
Task.start_link(fn ->
|
||||
character_updates =
|
||||
|
||||
@@ -581,18 +581,27 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
characters_ids =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
{:ok, old_presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:old_presence_character_ids", [])
|
||||
|
||||
new_present_character_ids =
|
||||
presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(old_presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
not_present_character_ids =
|
||||
characters_ids
|
||||
old_presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
CharactersImpl.track_characters(map_id, presence_character_ids)
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:old_presence_character_ids",
|
||||
presence_character_ids
|
||||
)
|
||||
|
||||
CharactersImpl.track_characters(map_id, new_present_character_ids)
|
||||
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
|
||||
|
||||
broadcast!(
|
||||
|
||||
@@ -596,10 +596,24 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
acl -> acl.id
|
||||
end)
|
||||
|
||||
updated_acls = current_acl_ids ++ [new_acl_id]
|
||||
updated_acls =
|
||||
if new_acl_id in current_acl_ids do
|
||||
current_acl_ids
|
||||
else
|
||||
current_acl_ids ++ [new_acl_id]
|
||||
end
|
||||
|
||||
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
|
||||
{:ok, updated_map} ->
|
||||
# Only broadcast if we actually added a new ACL
|
||||
unless new_acl_id in current_acl_ids do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"maps:#{loaded_map.id}",
|
||||
{:map_acl_updated, [new_acl_id], []}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, updated_map}
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -192,6 +192,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_added
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -199,6 +201,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member added event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
end
|
||||
|
||||
@@ -300,6 +305,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_updated
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -307,6 +314,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member updated event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
end
|
||||
|
||||
@@ -385,6 +395,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_removed
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -392,6 +404,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member removed event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
end
|
||||
|
||||
@@ -417,6 +432,14 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# Private Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -38,8 +38,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
:ok = track_characters(characters)
|
||||
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
|
||||
@@ -244,38 +244,41 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
{:ok, user_settings} =
|
||||
WandererApp.MapUserSettingsRepo.create_or_update(map_id, current_user_id, settings)
|
||||
|
||||
{:ok, map_user_settings} =
|
||||
user_settings
|
||||
|> WandererApp.Api.MapUserSettings.update_main_character(%{
|
||||
main_character_eve_id: "#{character_eve_id}"
|
||||
})
|
||||
case Ash.update(user_settings, %{main_character_eve_id: "#{character_eve_id}"},
|
||||
action: :update_main_character
|
||||
) do
|
||||
{:ok, map_user_settings} ->
|
||||
{:ok, tracking_data} =
|
||||
WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user_id)
|
||||
|
||||
{:ok, tracking_data} =
|
||||
WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user_id)
|
||||
{main_character_id, main_character_eve_id} =
|
||||
WandererApp.Character.TrackingUtils.get_main_character(
|
||||
map_user_settings,
|
||||
current_user_characters,
|
||||
current_user_characters
|
||||
)
|
||||
|> case do
|
||||
{:ok, main_character} when not is_nil(main_character) ->
|
||||
{main_character.id, main_character.eve_id}
|
||||
|
||||
{main_character_id, main_character_eve_id} =
|
||||
WandererApp.Character.TrackingUtils.get_main_character(
|
||||
map_user_settings,
|
||||
current_user_characters,
|
||||
current_user_characters
|
||||
)
|
||||
|> case do
|
||||
{:ok, main_character} when not is_nil(main_character) ->
|
||||
{main_character.id, main_character.eve_id}
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 50)
|
||||
|
||||
Process.send_after(self(), %{event: :refresh_user_characters}, 50)
|
||||
{:reply, %{data: tracking_data},
|
||||
socket
|
||||
|> assign(
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_id: main_character_id,
|
||||
main_character_eve_id: main_character_eve_id
|
||||
)}
|
||||
|
||||
{:reply, %{data: tracking_data},
|
||||
socket
|
||||
|> assign(
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_id: main_character_id,
|
||||
main_character_eve_id: main_character_eve_id
|
||||
)}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to update main character: #{inspect(reason)}")
|
||||
{:reply, %{error: "Failed to update main character"}, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
|
||||
@@ -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: main_character_id,
|
||||
solar_system_id: nil
|
||||
}
|
||||
},
|
||||
200
|
||||
)
|
||||
|
||||
if needs_tracking_setup do
|
||||
Process.send_after(self(), %{event: :show_tracking}, 10)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -75,26 +82,19 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
if not must_select? do
|
||||
socket
|
||||
else
|
||||
# Check if we already selected this exact system for this char:
|
||||
last_selected =
|
||||
WandererApp.Cache.lookup!(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
nil
|
||||
)
|
||||
# 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
|
||||
|
||||
if last_selected == solar_system_id do
|
||||
# same system => skip
|
||||
socket
|
||||
else
|
||||
# new system => update cache + push event
|
||||
WandererApp.Cache.put(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
end
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", select_solar_system_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,24 +24,8 @@ defmodule WandererAppWeb.Presence do
|
||||
%{character_id: character_id, tracked: any_tracked, from: from}
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} ->
|
||||
character_id
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_data",
|
||||
presence_data
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
# Delegate all cache operations to the PresenceGracePeriodManager
|
||||
WandererAppWeb.PresenceGracePeriodManager.process_presence_change(map_id, presence_data)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
@@ -0,0 +1,233 @@
|
||||
defmodule WandererAppWeb.PresenceGracePeriodManager do
|
||||
@moduledoc """
|
||||
Manages grace period for character presence tracking.
|
||||
|
||||
This module prevents rapid start/stop cycles of character tracking
|
||||
by introducing a 5-minute grace period before stopping tracking
|
||||
for characters that leave presence.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
# 30 minutes
|
||||
@grace_period_ms :timer.minutes(30)
|
||||
@check_remove_queue_interval :timer.seconds(30)
|
||||
|
||||
defstruct pending_removals: %{}, timers: %{}, to_remove: []
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Process presence changes with grace period logic.
|
||||
|
||||
Updates the cache with the final list of character IDs that should be tracked,
|
||||
accounting for the grace period.
|
||||
"""
|
||||
def process_presence_change(map_id, presence_data) do
|
||||
GenServer.cast(__MODULE__, {:process_presence_change, map_id, presence_data})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
{:ok, %__MODULE__{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:process_presence_change, map_id, presence_data}, state) do
|
||||
# Extract currently tracked character IDs from presence data
|
||||
current_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
# Get previous tracked character IDs from cache
|
||||
previous_tracked_character_ids = get_previous_character_ids(map_id)
|
||||
|
||||
current_set = MapSet.new(current_tracked_character_ids)
|
||||
previous_set = MapSet.new(previous_tracked_character_ids)
|
||||
|
||||
# Characters that just joined (not in previous, but in current)
|
||||
newly_joined = MapSet.difference(current_set, previous_set)
|
||||
|
||||
# Characters that just left (in previous, but not in current)
|
||||
newly_left = MapSet.difference(previous_set, current_set)
|
||||
|
||||
# Process newly joined characters - cancel any pending removals
|
||||
state =
|
||||
state
|
||||
|> cancel_pending_removals(map_id, current_set)
|
||||
|> schedule_removals(map_id, newly_left)
|
||||
|
||||
# Process newly left characters - schedule them for removal after grace period
|
||||
# Calculate the final character IDs (current + still pending removal)
|
||||
pending_for_map = get_pending_removals_for_map(state, map_id)
|
||||
|
||||
final_character_ids = MapSet.union(current_set, pending_for_map) |> MapSet.to_list()
|
||||
|
||||
# Update cache with final character IDs (includes grace period logic)
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", final_character_ids)
|
||||
|
||||
# Only update presence_data if the character IDs actually changed
|
||||
if final_character_ids != previous_tracked_character_ids do
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", presence_data)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:grace_period_expired, map_id, character_id}, state) do
|
||||
Logger.debug(fn -> "Grace period expired for character #{character_id} on map #{map_id}" end)
|
||||
|
||||
# Remove from pending removals and timers
|
||||
state =
|
||||
state
|
||||
|> remove_pending_removal(map_id, character_id)
|
||||
|> remove_after_grace_period(map_id, character_id)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_remove_queue, state) do
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
remove_from_cache_after_grace_period(state)
|
||||
{:noreply, %{state | to_remove: []}}
|
||||
end
|
||||
|
||||
defp cancel_pending_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
acc_state
|
||||
|
||||
timer_ref ->
|
||||
Logger.debug(fn ->
|
||||
"Cancelling grace period for character #{character_id} on map #{map_id} (rejoined)"
|
||||
end)
|
||||
|
||||
Process.cancel_timer(timer_ref)
|
||||
remove_pending_removal(acc_state, map_id, character_id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp schedule_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
# Only schedule if not already pending
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"Scheduling grace period for character #{character_id} on map #{map_id}"
|
||||
end)
|
||||
|
||||
timer_ref =
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:grace_period_expired, map_id, character_id},
|
||||
@grace_period_ms
|
||||
)
|
||||
|
||||
add_pending_removal(acc_state, map_id, character_id, timer_ref)
|
||||
|
||||
_ ->
|
||||
acc_state
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_pending_removal(state, map_id, character_id, timer_ref) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.put(state.pending_removals, pending_key, true),
|
||||
timers: Map.put(state.timers, pending_key, timer_ref)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_pending_removal(state, map_id, character_id) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.delete(state.pending_removals, pending_key),
|
||||
timers: Map.delete(state.timers, pending_key)
|
||||
}
|
||||
end
|
||||
|
||||
defp get_timer_ref(state, map_id, character_id) do
|
||||
Map.get(state.timers, {map_id, character_id})
|
||||
end
|
||||
|
||||
defp get_previous_character_ids(map_id) do
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_character_ids") do
|
||||
nil -> []
|
||||
character_ids -> character_ids
|
||||
end
|
||||
end
|
||||
|
||||
defp get_pending_removals_for_map(state, map_id) do
|
||||
state.pending_removals
|
||||
|> Enum.filter(fn {{pending_map_id, _character_id}, _} -> pending_map_id == map_id end)
|
||||
|> Enum.map(fn {{_map_id, character_id}, _} -> character_id end)
|
||||
|> MapSet.new()
|
||||
end
|
||||
|
||||
defp remove_after_grace_period(%{to_remove: to_remove} = state, map_id, character_id_to_remove) do
|
||||
%{
|
||||
state
|
||||
| to_remove:
|
||||
(to_remove ++ [{map_id, character_id_to_remove}])
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_from_cache_after_grace_period(%{to_remove: to_remove} = state) do
|
||||
# Get current presence data to recalculate without the expired character
|
||||
to_remove
|
||||
|> Enum.each(fn {map_id, character_id_to_remove} ->
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_data") do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
presence_data ->
|
||||
# Recalculate tracked character IDs from current presence data
|
||||
updated_presence_data =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{character_id: character_id} ->
|
||||
character_id != character_id_to_remove
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
updated_presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} ->
|
||||
tracked
|
||||
end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", updated_presence_data)
|
||||
# Update both caches
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"Updated cache after grace period for map #{map_id}, tracked characters: #{inspect(presence_tracked_character_ids)}"
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user