feat: extended map popup

This commit is contained in:
Dawid Wysokiński 2023-02-04 09:54:53 +01:00
parent fafbf587da
commit a2f8b65399
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
3 changed files with 187 additions and 99 deletions

View File

@ -1,6 +1,11 @@
import differenceInSeconds from 'date-fns/differenceInSeconds';
import addSeconds from 'date-fns/addSeconds';
import { ServerConfig, TWHelpClient, UnitInfo } from './lib/twhelp';
import {
Ennoblement,
ServerConfig,
TWHelpClient,
UnitInfo,
} from './lib/twhelp';
import { Cache, InMemoryStorage } from './lib/cache';
declare global {
interface Window {
@ -27,7 +32,11 @@ class TWHelpConnector {
private static readonly SERVER_CONFIG_CACHE_KEY =
'extended_map_popup_server_config';
private static readonly UNIT_INFO_CACHE_KEY = 'extended_map_popup_unit_info';
private static readonly VILLAGE_CACHE_KEY_PREFIX =
'extended_map_popup_village_';
private readonly localStorageCache = new Cache(localStorage);
private readonly inMemoryCache = new Cache(new InMemoryStorage());
constructor(
private readonly client: TWHelpClient,
private readonly version: string,
@ -35,44 +44,44 @@ class TWHelpConnector {
) {}
serverConfig() {
return this.withCache(TWHelpConnector.SERVER_CONFIG_CACHE_KEY, 3600, () => {
return this.client.serverConfig(this.version, this.server);
});
return this.localStorageCache.load(
TWHelpConnector.SERVER_CONFIG_CACHE_KEY,
3600,
() => {
return this.client.serverConfig(this.version, this.server);
}
);
}
unitInfo() {
return this.withCache(TWHelpConnector.UNIT_INFO_CACHE_KEY, 3600, () => {
return this.client.unitInfo(this.version, this.server);
});
return this.localStorageCache.load(
TWHelpConnector.UNIT_INFO_CACHE_KEY,
3600,
() => {
return this.client.unitInfo(this.version, this.server);
}
);
}
private async withCache<T>(
key: string,
ttl: number,
func: () => Promise<T>
): Promise<T> {
try {
const fromCache = JSON.parse(localStorage.getItem(key) ?? '');
if (
fromCache.date &&
differenceInSeconds(new Date(), new Date(fromCache.date)) <= ttl
) {
return fromCache.data;
}
// eslint-disable-next-line no-empty
} catch (err) {}
async latestEnnoblement(id: number, cacheOnly?: boolean) {
const key = TWHelpConnector.VILLAGE_CACHE_KEY_PREFIX + id.toString();
const res = await func();
if (cacheOnly) {
return this.inMemoryCache.get<Ennoblement | null>(key);
}
localStorage.setItem(
key,
JSON.stringify({
data: res,
date: new Date(),
})
);
return res;
return this.inMemoryCache.load(key, 60, async () => {
const ennoblements = await this.client.villageEnnoblements(
this.version,
this.server,
id,
{
limit: 1,
sort: ['createdAt:DESC'],
}
);
return ennoblements.data.length > 0 ? ennoblements.data[0] : null;
});
}
}
@ -92,14 +101,16 @@ class Popup {
window.TWMap.popup.displayForVillage = this.display.bind(this);
}
private loadVillage(villageId: string) {
private async loadVillage(villageId: string) {
window.TWMap.popup._loadVillage(villageId);
console.log(villageId);
await this.displayLatestEnnoblement(parseInt(villageId), false);
}
private display(village: { id: string }, x: number, y: number) {
private async display(village: { id: string }, x: number, y: number) {
window.TWMap.popup._displayForVillage(village, x, y);
this.displayArrivalTimes(x, y);
await this.displayLatestEnnoblement(parseInt(village.id), true);
}
private displayArrivalTimes(x: number, y: number) {
@ -148,6 +159,33 @@ class Popup {
tbody.appendChild(tr);
}
private async displayLatestEnnoblement(id: number, cacheOnly: boolean) {
const ennoblement = await this.connector.latestEnnoblement(id, cacheOnly);
const tbody = document.querySelector('#map_popup #info_content tbody');
if (!(tbody instanceof HTMLTableSectionElement)) {
return;
}
let tr = tbody.querySelector('#extended_map_popup_ennobled_at');
if (!tr) {
tr = document.createElement('tr');
tr.id = 'extended_map_popup_ennobled_at';
tbody.appendChild(tr);
}
tr.innerHTML = `
<td>
Ennobled at:
</td>
<td>
${
ennoblement ? new Date(ennoblement.createdAt).toLocaleString() : 'Never'
}
</td>
`;
}
}
class ExtendedMapPopup {
@ -163,7 +201,6 @@ class ExtendedMapPopup {
async run() {
const config = await this.connector.serverConfig();
const unitInfo = await this.connector.unitInfo();
console.log(config, unitInfo);
new Popup(
config,

65
src/lib/cache.ts Normal file
View File

@ -0,0 +1,65 @@
import addSeconds from 'date-fns/addSeconds';
import isAfter from 'date-fns/isAfter';
export interface Storage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
export class InMemoryStorage {
private readonly data = new Map<string, string>();
getItem(key: string): string | null {
const d = this.data.get(key);
return d ? d : null;
}
setItem(key: string, value: string) {
this.data.set(key, value);
}
removeItem(key: string) {
this.data.delete(key);
}
}
export class Cache {
constructor(private readonly storage: Storage) {}
// load first tries to load data from storage, if there is no data or it is expired then it executes the given function and caches the result
async load<T>(key: string, ttl: number, func: () => Promise<T>): Promise<T> {
const data = this.get<T>(key);
if (data) {
return data;
}
const res = await func();
this.storage.setItem(
key,
JSON.stringify({
data: res,
exp: addSeconds(new Date(), ttl),
})
);
return res;
}
get<T>(key: string): T | null {
try {
const fromStorage = JSON.parse(this.storage.getItem(key) ?? '');
if (!fromStorage.exp || !fromStorage.data) {
return null;
}
if (isAfter(new Date(), new Date(fromStorage.exp))) {
this.storage.removeItem(key);
return null;
}
return fromStorage.data;
} catch (err) {
return null;
}
}
}

View File

@ -208,24 +208,9 @@ export class TWHelpClient {
id: number,
queryParams?: ListTribeChangesQueryParams
): Promise<ListResult<TribeChange>> {
const params = new URLSearchParams();
if (queryParams?.limit) {
params.set('limit', queryParams.limit.toString());
}
if (queryParams?.offset) {
params.set('offset', queryParams.offset.toString());
}
if (Array.isArray(queryParams?.sort)) {
queryParams?.sort.forEach((s) => {
params.append('sort', s);
});
}
const queryString = queryParams ? this.buildQueryString(queryParams) : '';
const resp = await this.client.get(
`/api/v1/versions/${version}/servers/${server}/players/${id}/tribe-changes?${params.toString()}`
`/api/v1/versions/${version}/servers/${server}/players/${id}/tribe-changes?${queryString}`
);
return {
data: resp.data.data,
@ -239,24 +224,9 @@ export class TWHelpClient {
id: number,
queryParams?: ListEnnoblementsParams
): Promise<ListResult<Ennoblement>> {
const params = new URLSearchParams();
if (queryParams?.limit) {
params.set('limit', queryParams.limit.toString());
}
if (queryParams?.offset) {
params.set('offset', queryParams.offset.toString());
}
if (Array.isArray(queryParams?.sort)) {
queryParams?.sort.forEach((s) => {
params.append('sort', s);
});
}
const queryString = queryParams ? this.buildQueryString(queryParams) : '';
const resp = await this.client.get(
`/api/v1/versions/${version}/servers/${server}/players/${id}/ennoblements?${params.toString()}`
`/api/v1/versions/${version}/servers/${server}/players/${id}/ennoblements?${queryString}`
);
return {
data: resp.data.data,
@ -270,24 +240,9 @@ export class TWHelpClient {
id: number,
queryParams?: ListPlayerSnapshotsParams
): Promise<ListResult<PlayerSnapshot>> {
const params = new URLSearchParams();
if (queryParams?.limit) {
params.set('limit', queryParams.limit.toString());
}
if (queryParams?.offset) {
params.set('offset', queryParams.offset.toString());
}
if (Array.isArray(queryParams?.sort)) {
queryParams?.sort.forEach((s) => {
params.append('sort', s);
});
}
const queryString = queryParams ? this.buildQueryString(queryParams) : '';
const resp = await this.client.get(
`/api/v1/versions/${version}/servers/${server}/players/${id}/history?${params.toString()}`
`/api/v1/versions/${version}/servers/${server}/players/${id}/history?${queryString}`
);
return {
data: resp.data.data,
@ -301,18 +256,9 @@ export class TWHelpClient {
id: number,
queryParams?: ListPlayerOtherServersParams
): Promise<ListResult<PlayerWithServer>> {
const params = new URLSearchParams();
if (queryParams?.limit) {
params.set('limit', queryParams.limit.toString());
}
if (queryParams?.offset) {
params.set('offset', queryParams.offset.toString());
}
const queryString = queryParams ? this.buildQueryString(queryParams) : '';
const resp = await this.client.get(
`/api/v1/versions/${version}/servers/${server}/players/${id}/other-servers?${params.toString()}`
`/api/v1/versions/${version}/servers/${server}/players/${id}/other-servers?${queryString}`
);
return {
data: resp.data.data,
@ -320,6 +266,46 @@ export class TWHelpClient {
};
}
public async villageEnnoblements(
version: string,
server: string,
id: number,
queryParams?: ListEnnoblementsParams
): Promise<ListResult<Ennoblement>> {
const queryString = queryParams ? this.buildQueryString(queryParams) : '';
const resp = await this.client.get(
`/api/v1/versions/${version}/servers/${server}/villages/${id}/ennoblements?${queryString}`
);
return {
data: resp.data.data,
total: this.parseTotal(resp.headers),
};
}
private buildQueryString(
queryParams: Record<string, string | boolean | number | Date | string[]>
): string {
const params = new URLSearchParams();
for (const [name, val] of Object.entries(queryParams)) {
if (Array.isArray(val)) {
val.forEach((s) => {
params.append(name, s);
});
continue;
}
if (val instanceof Date) {
params.set(name, val.toISOString());
continue;
}
params.set(name, val.toString());
}
return params.toString();
}
private parseTotal(
headers: RawAxiosResponseHeaders | AxiosResponseHeaders
): number {