dokumentation 12 seiten

This commit is contained in:
Hikyu 2026-07-07 15:44:24 +02:00
parent 13154fbb53
commit 3a09a7285e
68 changed files with 7690 additions and 279 deletions

View File

@ -37,13 +37,32 @@
\@writefile{toc}{\contentsline {subsection}{\numberline {3.1}Vor- und Nachteile des Key-Value-Modells}{8}{subsection.3.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}\protected@file@percent }
\@writefile{tdo}{\contentsline {todo}{Wir koennten Regionen Traversierung Pathfinding einbauen}{8}{section*.1}\protected@file@percent }
\pgfsyspdfmark {pgfid1}{19562756}{23935922}
\@writefile{tdo}{\contentsline {todo}{Wir haben keine Beziehungen, die ueber mehrere Knoten hinweg gehen. Keine komlexen beziehungen wo der graph einen hohen nutzen mit sich bringt}{8}{section*.2}\protected@file@percent }
\pgfsyspdfmark {pgfid2}{19562756}{18172368}
\BKM@entry{id=13,dest={73656374696F6E2E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
\@writefile{toc}{\contentsline {section}{\numberline {4}Fazit}{9}{section.4}\protected@file@percent }
\BKM@entry{id=13,dest={73756273656374696F6E2E332E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
\@writefile{toc}{\contentsline {subsection}{\numberline {3.4}Fazit}{9}{subsection.3.4}\protected@file@percent }
\BKM@entry{id=14,dest={73656374696F6E2E34},srcline={368}}{5C3337365C3337375C3030304D5C303030615C303030725C3030306B5C303030745C303030725C303030655C303030635C303030685C303030655C303030725C303030635C303030685C30303065}
\BKM@entry{id=15,dest={73756273656374696F6E2E342E31},srcline={374}}{5C3337365C3337375C303030425C303030655C303030745C303030725C303030615C303030635C303030685C303030745C303030655C303030745C303030655C3030305C3034305C303030445C303030615C303030745C303030655C3030306E5C303030625C303030615C3030306E5C3030306B5C303030655C3030306E}
\BKM@entry{id=16,dest={73756273756273656374696F6E2E342E312E31},srcline={376}}{5C3337365C3337375C303030525C303030655C303030645C303030695C30303073}
\BKM@entry{id=17,dest={73756273756273656374696F6E2E342E312E32},srcline={384}}{5C3337365C3337375C3030304D5C303030655C3030306D5C303030635C303030615C303030635C303030685C303030655C30303064}
\BKM@entry{id=18,dest={73756273756273656374696F6E2E342E312E33},srcline={392}}{5C3337365C3337375C303030415C303030725C303030615C3030306E5C303030675C3030306F5C303030445C303030425C3030305C3034305C3030305C3035305C3030306D5C303030755C3030306C5C303030745C303030695C3030306D5C3030306F5C303030645C303030615C3030306C5C3030305C303531}
\BKM@entry{id=19,dest={73756273756273656374696F6E2E342E312E34},srcline={401}}{5C3337365C3337375C303030415C3030306D5C303030615C3030307A5C3030306F5C3030306E5C3030305C3034305C303030445C303030795C3030306E5C303030615C3030306D5C3030306F5C303030445C30303042}
\@writefile{toc}{\contentsline {section}{\numberline {4}Marktrecherche}{10}{section.4}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {4.1}Betrachtete Datenbanken}{10}{subsection.4.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.1}Redis}{10}{subsubsection.4.1.1}\protected@file@percent }
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.2}Memcached}{10}{subsubsection.4.1.2}\protected@file@percent }
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.3}ArangoDB (multimodal)}{10}{subsubsection.4.1.3}\protected@file@percent }
\BKM@entry{id=20,dest={73756273756273656374696F6E2E342E312E35},srcline={410}}{5C3337365C3337375C303030655C303030745C303030635C30303064}
\BKM@entry{id=21,dest={73756273756273656374696F6E2E342E312E36},srcline={416}}{5C3337365C3337375C303030525C303030695C303030615C3030306B5C3030305C3034305C3030304B5C30303056}
\BKM@entry{id=22,dest={73756273656374696F6E2E342E32},srcline={422}}{5C3337365C3337375C3030304E5C303030755C303030745C3030307A5C303030775C303030655C303030725C303030745C303030615C3030306E5C303030615C3030306C5C303030795C303030735C30303065}
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.4}Amazon DynamoDB}{11}{subsubsection.4.1.4}\protected@file@percent }
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.5}etcd}{11}{subsubsection.4.1.5}\protected@file@percent }
\@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.6}Riak KV}{11}{subsubsection.4.1.6}\protected@file@percent }
\@writefile{toc}{\contentsline {subsection}{\numberline {4.2}Nutzwertanalyse}{11}{subsection.4.2}\protected@file@percent }
\@writefile{lot}{\contentsline {table}{\numberline {4}{\ignorespaces Nutzwertanalyse der sechs Key-Value-Datenbanken}}{11}{table.4}\protected@file@percent }
\newlabel{tab:nutzwertanalyse}{{4}{11}{Nutzwertanalyse der sechs Key-Value-Datenbanken}{table.4}{}}
\BKM@entry{id=23,dest={73756273656374696F6E2E342E33},srcline={457}}{5C3337365C3337375C303030445C303030615C303030745C303030655C3030306E5C303030625C303030615C3030306E5C3030306B5C303030615C303030755C303030735C303030775C303030615C303030685C3030306C}
\@writefile{toc}{\contentsline {subsection}{\numberline {4.3}Datenbankauswahl}{12}{subsection.4.3}\protected@file@percent }
\global\@namedef{scr@dte@section@lastmaxnumwidth}{12.14694pt}
\global\@namedef{scr@dte@subsection@lastmaxnumwidth}{19.70898pt}
\global\@namedef{scr@dte@subsubsection@lastmaxnumwidth}{28.84563pt}
\@writefile{toc}{\providecommand\tocbasic@end@toc@file{}\tocbasic@end@toc@file}
\gdef \@abspage@last{9}
\gdef \@abspage@last{12}

View File

@ -1,9 +1,12 @@
# Fdb version 4
["pdflatex"] 1780313071.59521 "/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" "Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1780313072.62514 0
"/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" 1780313071.55337 16584 03bd9bb6b9242fada2b5fd161766d7ed ""
["pdflatex"] 1783431678.04028 "/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" "Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1783431679.12842 0
"/home/hikyu/Documents/Uni/2026 Sommer/Moderne Datenbanken/Ringstats_Dokumentation.tex" 1783431677.99026 22127 9c02fde45b05485b4d6e44f2a7e8ba1e ""
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-t1.enc" 1775415801 2971 def0b6c1f0b107b3b936def894055589 ""
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-ts1.enc" 1775415801 2900 1537cc8184ad1792082cd229ecc269f4 ""
"/usr/share/texmf-dist/fonts/map/fontname/texfonts.map" 1775415801 3524 cb3e574dea2d1052e39280babc910dc8 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1095.tfm" 1775415801 3584 21b378cca2e40816b0e6d74a4dc98f04 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm" 1775415801 3584 402da0b29eafbad07963b1224b222f18 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1095.tfm" 1775415801 3584 929cdff2b7a8c11bd4d49fd68cb0ae70 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1200.tfm" 1775415801 3584 f80ddd985bd00e29e9a6047ebd9d4781 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1728.tfm" 1775415801 3584 3c76ccb63eda935a68ba65ba9da29f1a ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecss1200.tfm" 1775415801 3584 db562fb1d1c474cc55f0ce8a1dd94fe7 ""
@ -12,6 +15,8 @@
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecsx1728.tfm" 1775415801 3584 a5b048a4e362640a1d2d34e624589d34 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecsx2488.tfm" 1775415801 3584 8530f700a3778253a0a6d6639fda16c7 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ectt1200.tfm" 1775415801 1536 487c9b46984a816c7ed238d0674595c7 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/tcrm1200.tfm" 1775415801 1536 74b7293ec3713bb7fdca8dd1bd1f469c ""
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm" 1775415801 1528 abec98dbc43e172678c11b3b9031252a ""
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi12.tfm" 1775415801 1524 4414a8315f39513458b80dfc63bff03a ""
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1775415801 1512 f21f83efb36853c0b70002322c1ab3ad ""
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1775415801 1520 eccf95517727cb11801f4f1aee3a21b4 ""
@ -23,7 +28,9 @@
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm" 1775415801 1124 6c73e740cf17375f03eec0ee63599741 ""
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1775415801 1116 933a60c408fc0a863a92debe84b2d294 ""
"/usr/share/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1775415801 1120 8b7d695260f3cff42e636090a8002094 ""
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1095.pfb" 1775415801 154600 ea54091d31de803b613ba9e80ca51709 ""
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb" 1775415801 140176 d4962f948b4cc0adf4d3dde77a128c95 ""
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1095.pfb" 1775415801 145929 f25e56369a345c4ff583b067cd87ce8e ""
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb" 1775415801 136101 f533469f523533d38317ab5729d00c8a ""
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1728.pfb" 1775415801 131438 3aa300b3e40e5c8ba7b4e5c6cebc5dd6 ""
"/usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb" 1775415801 148436 6a185415b8a1b77c58fee2272f3574e2 ""
@ -160,11 +167,11 @@
"/usr/share/texmf-dist/tex/latex/xcolor/xcolor.sty" 1775415801 55384 b454dec21c2d9f45ec0b793f0995b992 ""
"/usr/share/texmf-dist/tex/latex/xkeyval/xkeyval.sty" 1775415801 5006 d7e899bb5d9717c7f214e9bc0284de7b ""
"/usr/share/texmf-dist/web2c/texmf.cnf" 1775415801 43569 fd570f2fa160877d211e859f687312ba ""
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1777228096 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1778762763 3734533 c370cc050c4a9823a1fe1ab265afc57b ""
"Ringstats_Dokumentation.aux" 1780313072.58542 8525 dc2f45dff6b0575fd760f48e262e0d7d "pdflatex"
"Ringstats_Dokumentation.tex" 1780313071.55337 16584 03bd9bb6b9242fada2b5fd161766d7ed ""
"Ringstats_Dokumentation.toc" 1780313072.58642 1305 1401ae3dd1fbee665ef3e10a49daf4b1 "pdflatex"
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1780436122 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1780436120 3734532 282dcc45f1c728134b92f993ed6db952 ""
"Ringstats_Dokumentation.aux" 1783431679.08329 11920 6f4c4930e4f31ec828a5b3aab7f8ac7d "pdflatex"
"Ringstats_Dokumentation.tex" 1783431677.99026 22127 9c02fde45b05485b4d6e44f2a7e8ba1e ""
"Ringstats_Dokumentation.toc" 1783431679.08529 2164 0e5aae9731a2850f18801d399dcfe537 "pdflatex"
"assets/er-modell.png" 1778764597.46144 805989 0b7323ca4b38e6518c24fd9e5684655b ""
(generated)
"Ringstats_Dokumentation.aux"

View File

@ -276,8 +276,17 @@ INPUT ./assets/er-modell.png
INPUT ./assets/er-modell.png
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ectt1200.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecrm1095.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmr10.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1095.tfm
INPUT /usr/share/texmf-dist/fonts/tfm/jknappen/ec/tcrm1200.tfm
INPUT /usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-ts1.enc
INPUT Ringstats_Dokumentation.aux
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1095.pfb
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1095.pfb
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm1728.pfb
INPUT /usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb

View File

@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.5.14) 1 JUN 2026 13:24
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.6.2) 7 JUL 2026 15:41
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@ -725,7 +725,7 @@ LaTeX Font Info: External font `cmex10' loaded for size
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
/enc/dvips/cm-super/cm-super-t1.enc}]
[2]
<assets/er-modell.png, id=48, 3968.8275pt x 4311.10625pt>
<assets/er-modell.png, id=68, 3968.8275pt x 4311.10625pt>
File: assets/er-modell.png Graphic file (type png)
<use assets/er-modell.png>
Package pdftex.def Info: assets/er-modell.png used on input line 87.
@ -740,44 +740,37 @@ Overfull \hbox (9.63942pt too wide) in paragraph at lines 296--308
[][]
[]
[7]
Package hyperref Info: bookmark level for unknown todo defaults to 0 on input l
ine 326.
Overfull \hbox (11.24713pt too wide) in paragraph at lines 326--326
[]
[]
Overfull \hbox (11.24713pt too wide) in paragraph at lines 331--331
[]
[]
[8] [9] (./Ringstats_Dokumentation.aux)
[7] [8] [9] [10]
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <10.95> on input line 429.
[11] [12{/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-ts1.enc}] (./
Ringstats_Dokumentation.aux)
***********
LaTeX2e <2025-11-01>
L3 programming layer <2026-01-19>
***********
)
Here is how much of TeX's memory you used:
24546 strings out of 469462
479912 string characters out of 5468614
1009471 words of memory out of 5000000
52828 multiletter control sequences out of 15000+600000
638939 words of font info for 63 fonts, out of 8000000 for 9000
24519 strings out of 469462
478765 string characters out of 5468615
1015471 words of memory out of 5000000
52785 multiletter control sequences out of 15000+600000
641744 words of font info for 69 fonts, out of 8000000 for 9000
14 hyphenation exceptions out of 8191
108i,8n,108p,11313b,869s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb></usr/share/t
exmf-dist/fonts/type1/public/cm-super/sfrm1200.pfb></usr/share/texmf-dist/fonts
/type1/public/cm-super/sfrm1728.pfb></usr/share/texmf-dist/fonts/type1/public/c
m-super/sfsx1200.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx144
0.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1728.pfb></usr/sha
re/texmf-dist/fonts/type1/public/cm-super/sfsx2488.pfb></usr/share/texmf-dist/f
onts/type1/public/cm-super/sftt1200.pfb>
Output written on Ringstats_Dokumentation.pdf (9 pages, 608048 bytes).
108i,8n,108p,11313b,463s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/share/texmf-dist/fonts/type1/public/cm-super/sfbx1095.pfb></usr/share/t
exmf-dist/fonts/type1/public/cm-super/sfbx1200.pfb></usr/share/texmf-dist/fonts
/type1/public/cm-super/sfrm1095.pfb></usr/share/texmf-dist/fonts/type1/public/c
m-super/sfrm1200.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfrm172
8.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1200.pfb></usr/sha
re/texmf-dist/fonts/type1/public/cm-super/sfsx1440.pfb></usr/share/texmf-dist/f
onts/type1/public/cm-super/sfsx1728.pfb></usr/share/texmf-dist/fonts/type1/publ
ic/cm-super/sfsx2488.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sft
t1200.pfb>
Output written on Ringstats_Dokumentation.pdf (12 pages, 668797 bytes).
PDF statistics:
172 PDF objects out of 1000 (max. 8388607)
141 compressed objects within 2 object streams
29 named destinations out of 1000 (max. 500000)
122 words of extra memory for PDF output out of 10000 (max. 10000000)
252 PDF objects out of 1000 (max. 8388607)
212 compressed objects within 3 object streams
41 named destinations out of 1000 (max. 500000)
202 words of extra memory for PDF output out of 10000 (max. 10000000)

Binary file not shown.

Binary file not shown.

View File

@ -337,7 +337,7 @@ Für dieses einfache Schreiben und Lesen bringt der Graph mehr Aufwand mit, als
% \section{Bau der app bis zur Endpräsi}
\newpage
\section{Fazit}
\subsection{Fazit}
% Mehr coole sachen die wir gelernt haben.
% Warum wir uns fuer dieses Datenbankmodell entschieden haben.
Wir haben die gleichen Daten aus unserem ER-Modell in drei Datenbanken abgebildet und dabei gesehen,
@ -363,4 +363,113 @@ Genau dafür ist Redis gemacht.
Die komplexen Abfragen, bei denen MongoDB oder Neo4j stark wären, brauchen wir kaum,
und unsere Daten sind ohnehin nicht tief verschachtelt.
Deshalb passt die schnelle und einfache Lösung am besten zu dem, was Ringstats braucht.
\newpage
\section{Marktrecherche}
In der Zwischenpräsentation haben wir uns für das Key-Value-Modell entschieden.
In diesem Kapitel schauen wir uns den Markt für Key-Value-Datenbanken genauer an.
Wir stellen sechs Kandidaten vor, darunter ArangoDB eine multimodale Datenbank,
und führen anschließend eine Nutzwertanalyse durch.
\subsection{Betrachtete Datenbanken}
\subsubsection{Redis}
Redis ist ein In-Memory Key-Value Store und unsere Wahl aus der Zwischenpräsentation.
Alle Daten liegen im Arbeitsspeicher, wodurch Lese- und Schreibzugriffe im Bereich
von Mikrosekunden liegen. Neben einfachen Strings bringt Redis Datentypen wie
Hashes und Sets mit, auf denen unser Schlüsselaufbau aus
Kapitel~2.2 direkt aufsetzt. Seit Version 8 steht Redis wieder unter einer Open-Source-Lizenz (AGPLv3)
und kostet uns damit nichts.
\subsubsection{Memcached}
Memcached ist eine verteilte In-Memory-Cache Datenbank und in Sachen
Geschwindigkeit vergleichbar mit Redis. Dafür ist der Funktionsumfang bewusst
klein gehalten: Es gibt nur einfache Key-Value-Paare, keine Datentypen wie Sets
oder Hashes, und keine Persistenz. Unser Schlüsselaufbau mit
SETs für Ausrüstung und Regionen lässt sich also nicht direkt abbilden
Memcached steht unter der BSD-Lizenz.
\subsubsection{ArangoDB (multimodal)}
ArangoDB ist eine multimodale Datenbank und vereint Dokumente, Graphen und
Key-Value-Zugriffe in einem System mit der gemeinsamen Abfragesprache AQL.
Damit hätten wir unsere drei Modellierungen aus Kapitel~2 theoretisch in einer
einzigen Datenbank abbilden können. Der Nachteil ist der Langsame zugriff auf die Daten.
Seit Version 3.12 steht ArangoDB unter der Business Source
License. Für unser Uni-Projekt ist das kostenlos, für einen
kommerziellen Betrieb gäbe es Einschränkungen.
\subsubsection{Amazon DynamoDB}
DynamoDB ist der verwaltete Key-Value- und Dokument-Store von AWS. Die Datenbank
skaliert beliebig. Der große Haken ist das Preismodell:
DynamoDB läuft nur in der AWS-Cloud und wird pro Anfrage und Speicher
abgerechnet. Bei unseren dauerhaften Schreibzugriffen von bis zu 10 Anfragen
pro Sekunde würden laufende Kosten entstehen, womit unser
Muss-Kriterium \glqq keine Lizenzkosten\grqq{} verletzt ist. Zudem ist die
Latenz deutlich höher durch den Netzwerkweg in die Cloud.
\subsubsection{etcd}
etcd ist ein Key-Value Store, der hauptsächlich zum Speichern von
Einstellungen und Konfigurationen benutzt wird, zum Beispiel intern von
Kubernetes, ist also nicht dafür gedacht, laufend Spielerdaten zu speichern wie bei uns.
Die Lizenz ist Apache~2.0 und damit kostenlos.
\subsubsection{Riak KV}
Riak KV ist ein verteilter Key-Value Store.
Seine Stärke liegt in der Ausfallsicherheit: Daten werden
automatisch über mehrere Knoten repliziert, und der Cluster bleibt auch beim
Ausfall einzelner Knoten verfügbar. Riak KV steht unter der Apache-2.0-Lizenz.
\subsection{Nutzwertanalyse}
Wir bewerten alle sechs Datenbanken mit den Auswahlkriterien und Gewichten. Jedes Kriterium wird pro Datenbank mit 0 bis 2 Punkten
bewertet und mit dem Gewicht multipliziert.
\begin{table}[h]
\centering
\small
\begin{tabular}{l c c c c c c c}
\hline
\textbf{Kriterium (Gewicht)} & & \textbf{Redis} & \textbf{Memc.} & \textbf{Arango} & \textbf{Dynamo} & \textbf{etcd} & \textbf{Riak} \\ \hline
Hohe Performanz & (5) & 2 & 2 & 1 & 1 & 1 & 1 \\
Daten-Streaming & (3) & 2 & 0 & 0 & 2 & 1 & 0 \\
Niedrige Latenz & (3) & 2 & 2 & 1 & 1 & 1 & 1 \\
Skalierbarkeit & (1) & 1 & 1 & 1 & 2 & 0 & 2 \\
Keine Lizenzkosten & (5) & 2 & 2 & 2 & 0 & 2 & 2 \\
Indexing & (2) & 1 & 0 & 1 & 1 & 0 & 1 \\
Vektorrechnungen & (1) & 1 & 0 & 1 & 0 & 0 & 0 \\
Präferenz & (1) & 1 & 0 & 0 & 0 & 0 & 0 \\ \hline
\textbf{Gewichtete Summe} & & \textbf{37} & \textbf{27} & \textbf{22} & \textbf{18} & \textbf{21} & \textbf{22} \\
\hline
\end{tabular}
\caption{Nutzwertanalyse der sechs Key-Value-Datenbanken}
\label{tab:nutzwertanalyse}
\end{table}
Redis liegtklar vorne, weil es als einziger Kandidat sowohl bei
Performanz und Latenz als auch beim Daten-Streaming die volle Punktzahl erreicht.
Memcached kommt bei der reinen Geschwindigkeit zwar mit, scheitert aber am
Muss-Kriterium Daten-Streaming. DynamoDB fällt aber wegen der laufenden Kosten am Muss-Kriterium \glqq keine Lizenzkosten\grqq{} durch.
ArangoDB ist als multimodale
Datenbank flexibel, aber nicht auf unser schreiblastiges Live-Szenario
zugeschnitten, etcd ist für Konfigurationsdaten statt Anwendungsdaten gebaut,
und bei Riak KV kommt zur fehlenden Streaming-Funktion die kaum noch aktive
Entwicklung hinzu.
\subsection{Datenbankauswahl}
Aus den sechs betrachteten Datenbanken wählen wir drei aus, die wir umsetzen
und miteinander vergleichen, jeweils eine pro Gruppenmitglied:
\begin{itemize}
\item \textbf{Redis} (Justin Rost) --- der Sieger unserer Nutzwertanalyse
und unsere Wahl aus der Zwischenpräsentation, bisher aber noch nicht
umgesetzt. Redis dient im Vergleich als Referenz, an der sich die beiden
anderen Datenbanken messen müssen.
\item \textbf{Memcached} (Yasin Ergün) --- der direkteste Konkurrent, da
ebenfalls In-Memory und ähnlich schnell. Der Vergleich zeigt, welche
Redis-Funktionen wir tatsächlich brauchen und was uns bei einem reinen
Cache fehlen würde.
\item \textbf{ArangoDB} (Michelle Reichelt) --- die multimodale Datenbank
im Feld. Der Vergleich zeigt, was uns eine einzige Datenbank für alle drei
Modellierungen aus Kapitel~2 gebracht hätte und was sie an Performanz kostet.
\end{itemize}
\end{document}

View File

@ -11,5 +11,15 @@
\contentsline {subsection}{\numberline {3.1}Vor- und Nachteile des Key-Value-Modells}{8}{subsection.3.1}%
\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}%
\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}%
\contentsline {section}{\numberline {4}Fazit}{9}{section.4}%
\contentsline {subsection}{\numberline {3.4}Fazit}{9}{subsection.3.4}%
\contentsline {section}{\numberline {4}Marktrecherche}{10}{section.4}%
\contentsline {subsection}{\numberline {4.1}Betrachtete Datenbanken}{10}{subsection.4.1}%
\contentsline {subsubsection}{\numberline {4.1.1}Redis}{10}{subsubsection.4.1.1}%
\contentsline {subsubsection}{\numberline {4.1.2}Memcached}{10}{subsubsection.4.1.2}%
\contentsline {subsubsection}{\numberline {4.1.3}ArangoDB (multimodal)}{10}{subsubsection.4.1.3}%
\contentsline {subsubsection}{\numberline {4.1.4}Amazon DynamoDB}{11}{subsubsection.4.1.4}%
\contentsline {subsubsection}{\numberline {4.1.5}etcd}{11}{subsubsection.4.1.5}%
\contentsline {subsubsection}{\numberline {4.1.6}Riak KV}{11}{subsubsection.4.1.6}%
\contentsline {subsection}{\numberline {4.2}Nutzwertanalyse}{11}{subsection.4.2}%
\contentsline {subsection}{\numberline {4.3}Datenbankauswahl}{12}{subsection.4.3}%
\providecommand \tocbasic@end@toc@file {}\tocbasic@end@toc@file

View File

@ -39,7 +39,9 @@
\@writefile{toc}{\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}\protected@file@percent }
\BKM@entry{id=13,dest={73656374696F6E2E34},srcline={340}}{5C3337365C3337375C303030465C303030615C3030307A5C303030695C30303074}
\@writefile{toc}{\contentsline {section}{\numberline {4}Fazit}{9}{section.4}\protected@file@percent }
\BKM@entry{id=14,dest={73656374696F6E2E35},srcline={368}}{5C3337365C3337375C3030304D5C303030615C303030725C3030306B5C303030745C303030725C303030655C303030635C303030685C303030655C303030725C303030635C303030685C30303065}
\@writefile{toc}{\contentsline {section}{\numberline {5}Marktrecherche}{10}{section.5}\protected@file@percent }
\global\@namedef{scr@dte@section@lastmaxnumwidth}{12.14694pt}
\global\@namedef{scr@dte@subsection@lastmaxnumwidth}{19.70898pt}
\@writefile{toc}{\providecommand\tocbasic@end@toc@file{}\tocbasic@end@toc@file}
\gdef \@abspage@last{9}
\gdef \@abspage@last{10}

View File

@ -1,5 +1,5 @@
# Fdb version 4
["pdflatex"] 1780687793.04109 "Ringstats_Dokumentation.tex" "build/Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1780687794.00438 0
["pdflatex"] 1783429170.25382 "Ringstats_Dokumentation.tex" "build/Ringstats_Dokumentation.pdf" "Ringstats_Dokumentation" 1783429171.46916 0
"/usr/share/texmf-dist/fonts/enc/dvips/cm-super/cm-super-t1.enc" 1775415801 2971 def0b6c1f0b107b3b936def894055589 ""
"/usr/share/texmf-dist/fonts/map/fontname/texfonts.map" 1775415801 3524 cb3e574dea2d1052e39280babc910dc8 ""
"/usr/share/texmf-dist/fonts/tfm/jknappen/ec/ecbx1200.tfm" 1775415801 3584 402da0b29eafbad07963b1224b222f18 ""
@ -161,10 +161,10 @@
"/usr/share/texmf-dist/web2c/texmf.cnf" 1775415801 43569 fd570f2fa160877d211e859f687312ba ""
"/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map" 1780436122 5385779 47c0c393a77fd4a098bacfd57d66b88a ""
"/var/lib/texmf/web2c/pdftex/pdflatex.fmt" 1780436120 3734532 282dcc45f1c728134b92f993ed6db952 ""
"Ringstats_Dokumentation.tex" 1780687790.47947 16588 b04ea6d6404d252977bf34dce650e1c0 ""
"Ringstats_Dokumentation.tex" 1783429156.65473 16624 26aaf733867a9bf977b13d601b9ee80c ""
"assets/er-modell.png" 1778764597.46144 805989 0b7323ca4b38e6518c24fd9e5684655b ""
"build/Ringstats_Dokumentation.aux" 1780687793.96743 8079 e14d3fbb21e9c686a9967800d978e8e7 "pdflatex"
"build/Ringstats_Dokumentation.toc" 1780687793.96843 1305 1401ae3dd1fbee665ef3e10a49daf4b1 "pdflatex"
"build/Ringstats_Dokumentation.aux" 1783429171.42113 8408 fc30f1f377b4ce8b0a4e3079c56db12c "pdflatex"
"build/Ringstats_Dokumentation.toc" 1783429171.42313 1376 618376327eb7843bc7a20a1d978c49f9 "pdflatex"
(generated)
"build/Ringstats_Dokumentation.aux"
"build/Ringstats_Dokumentation.log"

View File

@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.6.2) 5 JUN 2026 21:29
This is pdfTeX, Version 3.141592653-2.6-1.40.29 (TeX Live 2026/Arch Linux) (preloaded format=pdflatex 2026.6.2) 7 JUL 2026 14:59
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@ -722,7 +722,7 @@ LaTeX Font Info: External font `cmex10' loaded for size
{/var/lib/texmf/fonts/map/pdftex/updmap/pdftex.map}{/usr/share/texmf-dist/fonts
/enc/dvips/cm-super/cm-super-t1.enc}]
[2]
<assets/er-modell.png, id=48, 3968.8275pt x 4311.10625pt>
<assets/er-modell.png, id=50, 3968.8275pt x 4311.10625pt>
File: assets/er-modell.png Graphic file (type png)
<use assets/er-modell.png>
Package pdftex.def Info: assets/er-modell.png used on input line 87.
@ -737,17 +737,17 @@ Overfull \hbox (9.63942pt too wide) in paragraph at lines 296--308
[][]
[]
[7] [8] [9] (build/Ringstats_Dokumentation.aux)
[7] [8] [9] [10] (build/Ringstats_Dokumentation.aux)
***********
LaTeX2e <2025-11-01>
L3 programming layer <2026-01-19>
***********
)
Here is how much of TeX's memory you used:
24473 strings out of 469462
477938 string characters out of 5468615
1009471 words of memory out of 5000000
52757 multiletter control sequences out of 15000+600000
24476 strings out of 469462
477960 string characters out of 5468615
1010471 words of memory out of 5000000
52758 multiletter control sequences out of 15000+600000
638939 words of font info for 63 fonts, out of 8000000 for 9000
14 hyphenation exceptions out of 8191
108i,8n,108p,11253b,461s stack positions out of 10000i,1000n,20000p,200000b,200000s
@ -758,10 +758,10 @@ m-super/sfsx1200.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx144
0.pfb></usr/share/texmf-dist/fonts/type1/public/cm-super/sfsx1728.pfb></usr/sha
re/texmf-dist/fonts/type1/public/cm-super/sfsx2488.pfb></usr/share/texmf-dist/f
onts/type1/public/cm-super/sftt1200.pfb>
Output written on build/Ringstats_Dokumentation.pdf (9 pages, 607609 bytes).
Output written on build/Ringstats_Dokumentation.pdf (10 pages, 609620 bytes).
PDF statistics:
170 PDF objects out of 1000 (max. 8388607)
139 compressed objects within 2 object streams
27 named destinations out of 1000 (max. 500000)
122 words of extra memory for PDF output out of 10000 (max. 10000000)
179 PDF objects out of 1000 (max. 8388607)
147 compressed objects within 2 object streams
29 named destinations out of 1000 (max. 500000)
130 words of extra memory for PDF output out of 10000 (max. 10000000)

Binary file not shown.

View File

@ -12,4 +12,5 @@
\contentsline {subsection}{\numberline {3.2}Vor- und Nachteile des dokumentenorientierten Modells}{8}{subsection.3.2}%
\contentsline {subsection}{\numberline {3.3}Vor- und Nachteile des Graph-Modells}{8}{subsection.3.3}%
\contentsline {section}{\numberline {4}Fazit}{9}{section.4}%
\contentsline {section}{\numberline {5}Marktrecherche}{10}{section.5}%
\providecommand \tocbasic@end@toc@file {}\tocbasic@end@toc@file

48
endpraesi/bench-all.ts Normal file
View File

@ -0,0 +1,48 @@
const targets = [
{ name: "Redis", dir: "client-redis/client-redit" },
{ name: "Memcached", dir: "client-memcached" },
{ name: "ArangoDB", dir: "client-arangodb" },
];
const results: any[] = [];
for (const t of targets) {
console.log(`\n>>> Running benchmark for ${t.name} ...`);
const proc = Bun.spawnSync(["bun", "bench.ts"], { cwd: t.dir, stdout: "pipe", stderr: "pipe" });
const out = proc.stdout.toString();
const err = proc.stderr.toString();
process.stdout.write(out.split("\n").filter(l => !l.startsWith("##RESULT##")).join("\n"));
if (err.trim()) process.stderr.write(err);
const line = out.split("\n").find(l => l.startsWith("##RESULT## "));
if (line) results.push(JSON.parse(line.slice("##RESULT## ".length)));
}
function pad(s: string, n: number) {
return s.padEnd(n);
}
function padNum(s: string, n: number) {
return s.padStart(n);
}
const testMeta = [
{ idx: 0, title: "Anwendungsfall 1 - Schreiben (Live-Player-Sync, 1 Schluessel)" },
{ idx: 1, title: "Anwendungsfall 1 - Lesen (Live-Player-Sync, 1 Schluessel)" },
{ idx: 2, title: "Anwendungsfall 2 - Lesen (Region / Fast Travel, 14 Schluessel)" },
{ idx: 3, title: "Anwendungsfall 2 - Schreiben (Region / Fast Travel, 14 Schluessel)" },
];
console.log("\n\n==================== VERGLEICH ====================");
for (const m of testMeta) {
console.log(`\n${m.title}`);
console.log(` ${pad("DB", 12)} | ${padNum("Ops/s", 11)} | ${padNum("ms/Op", 10)} | ${padNum("Gesamt ms", 11)}`);
console.log(` ${"-".repeat(12)}-+-${"-".repeat(11)}-+-${"-".repeat(10)}-+-${"-".repeat(11)}`);
const rows = results.map(r => ({ db: r.db, t: r.tests[m.idx] }));
const fastest = Math.max(...rows.map(x => x.t.opsPerSec));
for (const row of rows) {
const mark = row.t.opsPerSec === fastest ? " <-- am schnellsten" : "";
console.log(` ${pad(row.db, 12)} | ${padNum(Math.round(row.t.opsPerSec).toString(), 11)} | ${padNum(row.t.avgMs.toFixed(4), 10)} | ${padNum(row.t.ms.toFixed(1), 11)}${mark}`);
}
}
console.log("\n==================================================");

View File

@ -0,0 +1,35 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `arangojs` for ArangoDB. ArangoDB has no built-in Bun client.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Datenmodell (ArangoDB als Key-Value)
ArangoDB ist multimodal, wir nutzen es hier bewusst als reinen Key-Value-Store,
damit der Vergleich direkt zu Redis passt. Alles liegt in einer Collection `kv`.
Jedes Dokument hat den Redis-Key als `_key` und den Wert im Feld `value`.
- `player:{id}` -> value = Objekt
- `player:{id}:weapon` / `:armor` / `:talisman` -> value = Array mit item_ids
- `item:{id}`, `enemy:{id}`, `region:{id}` -> value = Objekt
- `region:{id}:items` / `:enemies`, `enemy:{id}:items` -> value = Array mit ids
- `weapon:{id}`, `armor:{id}`, `shield:{id}`, `talisman:{id}`, `item_effect:{id}`, `item_type:{id}` -> value = Objekt
## Coding Style Preferences
- Write everything WITHOUT comments.

View File

@ -0,0 +1,8 @@
# Projekt-Dokumentation: Entwicklungsreihenfolge
1. Projekt init (gleicher Aufbau wie client-redit)
2. Docker compose mit arangodb
3. Einfache verbindung (arangojs)
4. ArangoDB getestet (mit save und document)
5. Init-Skript (db/init.ts) mit ~100 statischen Entitäten erstellt (gleiche Daten wie Redis, aber als Key-Value-Dokumente in einer Collection)
6. Client (index.ts) aktualisiert Player-Daten live und liest statische Gegner-Daten

View File

@ -0,0 +1,33 @@
# client-arangodb
ArangoDB-Variante des Ringstats-Clients. Gleicher Aufbau wie `client-redit`.
ArangoDB ist multimodal, wir nutzen es hier bewusst als reinen Key-Value-Store,
damit der Vergleich direkt zu Redis passt. Alles liegt in einer Collection `kv`,
jedes Dokument hat den Redis-Key als `_key` und den Wert im Feld `value`.
To install dependencies:
```bash
bun install
```
Datenbank starten:
```bash
docker compose up -d
```
Datenbank befüllen (~100 statische Entitäten):
```bash
bun run db/init.ts
```
Live-Client starten:
```bash
bun run index.ts
```
ArangoDB läuft auf Port `48529`, damit es parallel zu Redis und Memcached laufen kann.
Beim ersten Start braucht ArangoDB kurz, bis es bereit ist. Die Skripte warten automatisch.

View File

@ -0,0 +1,92 @@
import { Database } from "arangojs";
const db = new Database({ url: "http://localhost:48529" });
const kv = db.collection("kv");
const DB_NAME = "ArangoDB";
const N1 = 2000;
const N2 = 300;
const region1Enemies = ["1", "2", "3", "11", "12", "13", "14"];
const region1Items = ["1", "8", "21", "26"];
const playerObj = (i: number) => ({
id: 1,
name: "Jonas",
health: 50 + (i % 50),
stamina: 50 + (i % 50),
level: 1 + (i % 20),
region_id: 1,
});
const enemyObj = { id: 0, name: "Benchmark Enemy", health: 1000, level: 20, damage: 50 };
const itemObj = { id: 0, name: "Benchmark Item", gear_type: "weapon", gear_id: 1 };
const regionObj = { id: 1, name: "Limgrave" };
async function getVal(key: string): Promise<any> {
const doc = await kv.document(key, { graceful: true });
return doc ? doc.value : null;
}
async function setVal(key: string, value: any) {
await kv.save({ _key: key, value }, { overwriteMode: "replace" });
}
async function writeSingle(i: number) {
await setVal("bench:player:1", playerObj(i));
}
async function readSingle(_i: number) {
await getVal("bench:player:1");
}
async function readRegion(_i: number) {
const region = await getVal("region:1");
void region?.name;
const enemyIds = (await getVal("region:1:enemies")) || [];
const itemIds = (await getVal("region:1:items")) || [];
for (const id of enemyIds) await getVal(`enemy:${id}`);
for (const id of itemIds) await getVal(`item:${id}`);
}
async function writeRegion(_i: number) {
await setVal("bench:region:1", regionObj);
await setVal("bench:region:1:enemies", region1Enemies);
await setVal("bench:region:1:items", region1Items);
for (const id of region1Enemies) await setVal(`bench:enemy:${id}`, { ...enemyObj, id });
for (const id of region1Items) await setVal(`bench:item:${id}`, { ...itemObj, id });
}
async function timed(label: string, group: string, iters: number, opsPerIter: number, fn: (i: number) => Promise<void>) {
const warmup = Math.min(50, iters);
for (let i = 0; i < warmup; i++) await fn(i);
const start = performance.now();
for (let i = 0; i < iters; i++) await fn(i);
const ms = performance.now() - start;
const totalOps = iters * opsPerIter;
const opsPerSec = totalOps / (ms / 1000);
const avgMs = ms / totalOps;
return { label, group, iters, opsPerIter, totalOps, ms, opsPerSec, avgMs };
}
function fmt(r: any) {
return ` ${r.label.padEnd(11)} | ${String(r.totalOps).padStart(6)} Ops | ${r.ms.toFixed(1).padStart(9)} ms | ${Math.round(r.opsPerSec).toString().padStart(9)} Ops/s | ${r.avgMs.toFixed(4)} ms/Op`;
}
await db.version();
const w1 = await timed("Schreiben", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, writeSingle);
const r1 = await timed("Lesen", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, readSingle);
const r2 = await timed("Lesen", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, readRegion);
const w2 = await timed("Schreiben", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, writeRegion);
console.log(`\n===== Benchmark: ${DB_NAME} =====`);
console.log(w1.group);
console.log(fmt(w1));
console.log(fmt(r1));
console.log(r2.group);
console.log(fmt(r2));
console.log(fmt(w2));
console.log("##RESULT## " + JSON.stringify({ db: DB_NAME, tests: [w1, r1, r2, w2] }));
process.exit(0);

View File

@ -0,0 +1,35 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "client-arangodb",
"dependencies": {
"arangojs": "^9.2.0",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
"arangojs": ["arangojs@9.3.0", "", { "dependencies": { "@types/node": "^20.11.26" } }, "sha512-+z/TxumH8ywsXAN0oyQAxMnBMtDbd4tjFhGXzygAHD8+YDzEC705STLBh38KGbwxWeekalu2XEHUtrJ/NONgTQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"bun-types/@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
"bun-types/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
}
}

View File

@ -0,0 +1,56 @@
import { Database } from "arangojs";
import { effects, types, weapons, armors, shields, talismans, items, regions, enemies } from "../../data";
const db = new Database({ url: "http://localhost:48529" });
console.log("Populating database with ~100 static Ringstats entries...");
async function waitForArango() {
for (let i = 0; i < 30; i++) {
try {
await db.version();
return;
} catch {
console.log("Waiting for ArangoDB to be ready...");
await Bun.sleep(2000);
}
}
throw new Error("ArangoDB not reachable on localhost:48529");
}
await waitForArango();
const kv = db.collection("kv");
if (!(await kv.exists())) {
await kv.create();
}
const docs: { _key: string; value: any }[] = [];
const collect = (prefix: string, list: any[]) => {
for (const obj of list) {
const { items, enemies, ...data } = obj;
docs.push({ _key: `${prefix}:${obj.id}`, value: data });
if (items && items.length > 0) {
docs.push({ _key: `${prefix}:${obj.id}:items`, value: items });
}
if (enemies && enemies.length > 0) {
docs.push({ _key: `${prefix}:${obj.id}:enemies`, value: enemies });
}
}
};
collect("item_effect", effects);
collect("item_type", types);
collect("weapon", weapons);
collect("armor", armors);
collect("shield", shields);
collect("talisman", talismans);
collect("item", items);
collect("region", regions);
collect("enemy", enemies);
await kv.import(docs, { overwrite: true, onDuplicate: "replace" });
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into ArangoDB.`);
console.log("Database successfully seeded.");
process.exit(0);

View File

@ -0,0 +1,8 @@
services:
arangodb:
image: arangodb:latest
environment:
ARANGO_NO_AUTH: "1"
ports:
- "48529:8529"
restart: always

View File

@ -0,0 +1,153 @@
import { Database } from "arangojs";
const db = new Database({ url: "http://localhost:48529" });
const kv = db.collection("kv");
const colors = {
reset: "\x1b[0m",
cyan: "\x1b[36m",
green: "\x1b[32m",
yellow: "\x1b[33m",
magenta: "\x1b[35m",
red: "\x1b[31m",
gray: "\x1b[90m",
bold: "\x1b[1m",
};
let totalRequests = 0;
function log(msg: string, color: string = colors.reset) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
console.log(`${colors.gray}[${timestamp}]${colors.reset} ${color}${msg}${colors.reset}`);
}
async function getVal(key: string): Promise<any> {
const doc = await kv.document(key, { graceful: true });
return doc ? doc.value : null;
}
async function setVal(key: string, value: any) {
await kv.save({ _key: key, value }, { overwriteMode: "replace" });
}
const introMessage = "Starting Ringstats Desktop Application...";
log(introMessage, colors.cyan);
const pingStart = performance.now();
await db.version();
totalRequests++;
log(`Connected to Ringstats Database successfully. (Reqs: 1 | Total: ${totalRequests} | Time: ${(performance.now() - pingStart).toFixed(2)}ms)`, colors.green);
log("Looking for Elden Ring process...", colors.gray);
await Bun.sleep(1500);
log("Found Elden Ring process!", colors.green);
log("Equipping initial gear...", colors.gray);
await setVal("player:1:weapon", ["1"]);
await setVal("player:1:armor", ["11"]);
await setVal("player:1:talisman", ["26"]);
totalRequests += 3;
let playerHealth = 100;
let playerStamina = 100;
let playerLevel = 1;
let currentRegionId = 1;
log("Entering live sync loop (press Ctrl+C to exit)...", colors.gray);
while (true) {
playerHealth = Math.floor(Math.max(10, playerHealth - (Math.random() * 10) + 5));
playerStamina = Math.floor(Math.max(10, playerStamina - (Math.random() * 20) + 10));
if (Math.random() > 0.9) {
playerLevel += 1;
}
if (Math.random() > 0.85) {
currentRegionId = Math.floor(Math.random() * 16) + 1;
const startTravel = performance.now();
const region = await getVal(`region:${currentRegionId}`);
const regionEnemiesIds = (await getVal(`region:${currentRegionId}:enemies`)) || [];
const regionItemsIds = (await getVal(`region:${currentRegionId}:items`)) || [];
const enemyNames = await Promise.all(regionEnemiesIds.map(async (id: string) => (await getVal(`enemy:${id}`))?.name));
const itemNames = await Promise.all(regionItemsIds.map(async (id: string) => (await getVal(`item:${id}`))?.name));
const queries = 3 + regionEnemiesIds.length + regionItemsIds.length;
totalRequests += queries;
const travelTime = (performance.now() - startTravel).toFixed(2);
console.log();
log(`>>> Fast Traveled to: [ID: ${currentRegionId}] ${region?.name} <<<`, colors.magenta + colors.bold);
log(`[Region Data] Enemies nearby: ${enemyNames.join(", ") || "None"}`, colors.magenta);
log(`[Region Data] Items nearby: ${itemNames.join(", ") || "None"}`, colors.magenta);
log(`[Stats] Query Time: ${travelTime}ms | Reqs in Block: ${queries} | Total Reqs: ${totalRequests}`, colors.gray);
console.log();
}
if (Math.random() > 0.90) {
const newWeaponId = Math.floor(Math.random() * 10) + 1;
const newArmorId = Math.floor(Math.random() * 10) + 11;
const newTalismanId = Math.floor(Math.random() * 5) + 26;
const startSwap = performance.now();
await setVal("player:1:weapon", [newWeaponId.toString()]);
await setVal("player:1:armor", [newArmorId.toString()]);
await setVal("player:1:talisman", [newTalismanId.toString()]);
const weaponName = (await getVal(`item:${newWeaponId}`))?.name;
const armorName = (await getVal(`item:${newArmorId}`))?.name;
const talismanName = (await getVal(`item:${newTalismanId}`))?.name;
totalRequests += 6;
const swapTime = (performance.now() - startSwap).toFixed(2);
console.log();
log(`>>> Gear Swapped <<<`, colors.cyan + colors.bold);
log(`Equipped: ${weaponName} | ${armorName} | ${talismanName}`, colors.cyan);
log(`[Stats] Query Time: ${swapTime}ms | Reqs in Block: 6 | Total Reqs: ${totalRequests}`, colors.gray);
console.log();
}
const startSync = performance.now();
await setVal("player:1", {
id: 1,
name: "Jonas",
health: playerHealth,
stamina: playerStamina,
level: playerLevel,
region_id: currentRegionId
});
totalRequests += 1;
const syncTime = (performance.now() - startSync).toFixed(2);
log(`[Live Sync] [ID: 1] Jonas - Lvl: ${playerLevel} | HP: ${playerHealth} | Stamina: ${playerStamina}`, colors.green);
log(`[Stats] Query Time: ${syncTime}ms | Reqs in Block: 1 | Total Reqs: ${totalRequests}`, colors.gray);
if (Math.random() > 0.7) {
const startLock = performance.now();
const enemyIds = (await getVal(`region:${currentRegionId}:enemies`)) || [];
totalRequests += 1;
const randomEnemyId = enemyIds.length > 0 ? enemyIds[Math.floor(Math.random() * enemyIds.length)] : null;
if (randomEnemyId) {
console.log();
log("--- Target Lock Detected ---", colors.red + colors.bold);
const enemyInfo = await getVal(`enemy:${randomEnemyId}`);
const dropItemIds = (await getVal(`enemy:${randomEnemyId}:items`)) || [];
const dropItemNames = await Promise.all(dropItemIds.map(async (id: string) => (await getVal(`item:${id}`))?.name));
const queries = 2 + dropItemIds.length;
totalRequests += queries;
const lockTime = (performance.now() - startLock).toFixed(2);
log(`Overlay -> [ID: ${enemyInfo.id}] ${enemyInfo.name} | Lvl: ${enemyInfo.level} | HP: ${enemyInfo.health} | DMG: ${enemyInfo.damage}`, colors.yellow);
log(`Drops -> ${dropItemNames.length > 0 ? dropItemNames.join(", ") : "None"}`, colors.yellow);
log(`[Stats] Query Time: ${lockTime}ms | Reqs in Block: ${queries + 1} | Total Reqs: ${totalRequests}`, colors.gray);
console.log();
}
}
await Bun.sleep(2000);
}

View File

@ -0,0 +1,15 @@
{
"name": "client-arangodb",
"module": "index.ts",
"type": "module",
"private": true,
"dependencies": {
"arangojs": "^9.2.0"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

View File

@ -0,0 +1,35 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `memjs` for Memcached. Memcached has no built-in Bun client.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Datenmodell (Memcached)
Memcached kennt keine Hashes oder Sets, nur reine Key-Value-Paare. Jedes Objekt
wird deshalb unter einem flachen Key als JSON-String abgelegt. Listen (z.B. die
Items einer Region) landen als JSON-Array im Value unter einem eigenen Key.
- `player:{id}` -> JSON Objekt
- `player:{id}:weapon` / `:armor` / `:talisman` -> JSON Array mit item_ids
- `item:{id}`, `enemy:{id}`, `region:{id}` -> JSON Objekt
- `region:{id}:items` / `:enemies`, `enemy:{id}:items` -> JSON Array mit ids
- `weapon:{id}`, `armor:{id}`, `shield:{id}`, `talisman:{id}`, `item_effect:{id}`, `item_type:{id}` -> JSON Objekt
## Coding Style Preferences
- Write everything WITHOUT comments.

View File

@ -0,0 +1,8 @@
# Projekt-Dokumentation: Entwicklungsreihenfolge
1. Projekt init (gleicher Aufbau wie client-redit)
2. Docker compose mit memcached
3. Einfache verbindung (memjs)
4. Memcached getestet (mit set und get)
5. Init-Skript (db/init.ts) mit ~100 statischen Entitäten erstellt (gleiche Daten wie Redis, aber als flache JSON-Values)
6. Client (index.ts) aktualisiert Player-Daten live und liest statische Gegner-Daten

View File

@ -0,0 +1,31 @@
# client-memcached
Memcached-Variante des Ringstats-Clients. Gleicher Aufbau wie `client-redit`,
nur mit Memcached statt Redis. Da Memcached keine Hashes oder Sets kennt, liegt
jedes Objekt als flacher Key mit einem JSON-String als Value.
To install dependencies:
```bash
bun install
```
Datenbank starten:
```bash
docker compose up -d
```
Datenbank befüllen (~100 statische Entitäten):
```bash
bun run db/init.ts
```
Live-Client starten:
```bash
bun run index.ts
```
Memcached läuft auf Port `41211`, damit es parallel zu Redis und ArangoDB laufen kann.

View File

@ -0,0 +1,91 @@
import { Client } from "memjs";
const memcached = Client.create("localhost:41211");
const DB_NAME = "Memcached";
const N1 = 2000;
const N2 = 300;
const region1Enemies = ["1", "2", "3", "11", "12", "13", "14"];
const region1Items = ["1", "8", "21", "26"];
const playerObj = (i: number) => ({
id: 1,
name: "Jonas",
health: 50 + (i % 50),
stamina: 50 + (i % 50),
level: 1 + (i % 20),
region_id: 1,
});
const enemyObj = { id: 0, name: "Benchmark Enemy", health: 1000, level: 20, damage: 50 };
const itemObj = { id: 0, name: "Benchmark Item", gear_type: "weapon", gear_id: 1 };
const regionObj = { id: 1, name: "Limgrave" };
async function getJson(key: string): Promise<any> {
const { value } = await memcached.get(key);
return value ? JSON.parse(value.toString()) : null;
}
async function setJson(key: string, value: any) {
await memcached.set(key, JSON.stringify(value), {});
}
async function writeSingle(i: number) {
await setJson("bench:player:1", playerObj(i));
}
async function readSingle(_i: number) {
await getJson("bench:player:1");
}
async function readRegion(_i: number) {
const region = await getJson("region:1");
void region?.name;
const enemyIds = (await getJson("region:1:enemies")) || [];
const itemIds = (await getJson("region:1:items")) || [];
for (const id of enemyIds) await getJson(`enemy:${id}`);
for (const id of itemIds) await getJson(`item:${id}`);
}
async function writeRegion(_i: number) {
await setJson("bench:region:1", regionObj);
await setJson("bench:region:1:enemies", region1Enemies);
await setJson("bench:region:1:items", region1Items);
for (const id of region1Enemies) await setJson(`bench:enemy:${id}`, { ...enemyObj, id });
for (const id of region1Items) await setJson(`bench:item:${id}`, { ...itemObj, id });
}
async function timed(label: string, group: string, iters: number, opsPerIter: number, fn: (i: number) => Promise<void>) {
const warmup = Math.min(50, iters);
for (let i = 0; i < warmup; i++) await fn(i);
const start = performance.now();
for (let i = 0; i < iters; i++) await fn(i);
const ms = performance.now() - start;
const totalOps = iters * opsPerIter;
const opsPerSec = totalOps / (ms / 1000);
const avgMs = ms / totalOps;
return { label, group, iters, opsPerIter, totalOps, ms, opsPerSec, avgMs };
}
function fmt(r: any) {
return ` ${r.label.padEnd(11)} | ${String(r.totalOps).padStart(6)} Ops | ${r.ms.toFixed(1).padStart(9)} ms | ${Math.round(r.opsPerSec).toString().padStart(9)} Ops/s | ${r.avgMs.toFixed(4)} ms/Op`;
}
await memcached.set("__ping__", "1", {});
const w1 = await timed("Schreiben", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, writeSingle);
const r1 = await timed("Lesen", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, readSingle);
const r2 = await timed("Lesen", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, readRegion);
const w2 = await timed("Schreiben", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, writeRegion);
console.log(`\n===== Benchmark: ${DB_NAME} =====`);
console.log(w1.group);
console.log(fmt(w1));
console.log(fmt(r1));
console.log(r2.group);
console.log(fmt(r2));
console.log(fmt(w2));
console.log("##RESULT## " + JSON.stringify({ db: DB_NAME, tests: [w1, r1, r2, w2] }));
process.exit(0);

View File

@ -0,0 +1,34 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "client-memcached",
"dependencies": {
"memjs": "^1.3.2",
},
"devDependencies": {
"@types/bun": "latest",
"@types/memjs": "^1.3.3",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/memjs": ["@types/memjs@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-OqZuZ2T0ErrygA/WUqQjmsB1GpxAt84OCgKPKbsSWwYKA/oe/lTiqvqMgFOv/Ngl8p+nytgdhrdxJtikOgimCg=="],
"@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"memjs": ["memjs@1.3.2", "", {}, "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
}
}

View File

@ -0,0 +1,36 @@
import { Client } from "memjs";
import { effects, types, weapons, armors, shields, talismans, items, regions, enemies } from "../../data";
const memcached = Client.create("localhost:41211");
console.log("Populating database with ~100 static Ringstats entries...");
const setJson = async (key: string, value: any) => {
await memcached.set(key, JSON.stringify(value), {});
};
const setEntities = async (prefix: string, list: any[]) => {
for (const obj of list) {
const { items, enemies, ...data } = obj;
await setJson(`${prefix}:${obj.id}`, data);
if (items && items.length > 0) {
await setJson(`${prefix}:${obj.id}:items`, items);
}
if (enemies && enemies.length > 0) {
await setJson(`${prefix}:${obj.id}:enemies`, enemies);
}
}
};
await setEntities("item_effect", effects);
await setEntities("item_type", types);
await setEntities("weapon", weapons);
await setEntities("armor", armors);
await setEntities("shield", shields);
await setEntities("talisman", talismans);
await setEntities("item", items);
await setEntities("region", regions);
await setEntities("enemy", enemies);
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into Memcached.`);
console.log("Database successfully seeded.");
process.exit(0);

View File

@ -0,0 +1,6 @@
services:
memcached:
image: memcached:alpine
ports:
- "41211:11211"
restart: always

View File

@ -0,0 +1,153 @@
import { Client } from "memjs";
const memcached = Client.create("localhost:41211");
const colors = {
reset: "\x1b[0m",
cyan: "\x1b[36m",
green: "\x1b[32m",
yellow: "\x1b[33m",
magenta: "\x1b[35m",
red: "\x1b[31m",
gray: "\x1b[90m",
bold: "\x1b[1m",
};
let totalRequests = 0;
function log(msg: string, color: string = colors.reset) {
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
console.log(`${colors.gray}[${timestamp}]${colors.reset} ${color}${msg}${colors.reset}`);
}
async function getJson(key: string): Promise<any> {
const { value } = await memcached.get(key);
return value ? JSON.parse(value.toString()) : null;
}
async function setJson(key: string, value: any) {
await memcached.set(key, JSON.stringify(value), {});
}
const introMessage = "Starting Ringstats Desktop Application...";
log(introMessage, colors.cyan);
const pingStart = performance.now();
await memcached.set("__ping__", "1", {});
await memcached.get("__ping__");
totalRequests++;
log(`Connected to Ringstats Database successfully. (Reqs: 1 | Total: ${totalRequests} | Time: ${(performance.now() - pingStart).toFixed(2)}ms)`, colors.green);
log("Looking for Elden Ring process...", colors.gray);
await Bun.sleep(1500);
log("Found Elden Ring process!", colors.green);
log("Equipping initial gear...", colors.gray);
await setJson("player:1:weapon", ["1"]);
await setJson("player:1:armor", ["11"]);
await setJson("player:1:talisman", ["26"]);
totalRequests += 3;
let playerHealth = 100;
let playerStamina = 100;
let playerLevel = 1;
let currentRegionId = 1;
log("Entering live sync loop (press Ctrl+C to exit)...", colors.gray);
while (true) {
playerHealth = Math.floor(Math.max(10, playerHealth - (Math.random() * 10) + 5));
playerStamina = Math.floor(Math.max(10, playerStamina - (Math.random() * 20) + 10));
if (Math.random() > 0.9) {
playerLevel += 1;
}
if (Math.random() > 0.85) {
currentRegionId = Math.floor(Math.random() * 16) + 1;
const startTravel = performance.now();
const region = await getJson(`region:${currentRegionId}`);
const regionEnemiesIds = (await getJson(`region:${currentRegionId}:enemies`)) || [];
const regionItemsIds = (await getJson(`region:${currentRegionId}:items`)) || [];
const enemyNames = await Promise.all(regionEnemiesIds.map(async (id: string) => (await getJson(`enemy:${id}`))?.name));
const itemNames = await Promise.all(regionItemsIds.map(async (id: string) => (await getJson(`item:${id}`))?.name));
const queries = 3 + regionEnemiesIds.length + regionItemsIds.length;
totalRequests += queries;
const travelTime = (performance.now() - startTravel).toFixed(2);
console.log();
log(`>>> Fast Traveled to: [ID: ${currentRegionId}] ${region?.name} <<<`, colors.magenta + colors.bold);
log(`[Region Data] Enemies nearby: ${enemyNames.join(", ") || "None"}`, colors.magenta);
log(`[Region Data] Items nearby: ${itemNames.join(", ") || "None"}`, colors.magenta);
log(`[Stats] Query Time: ${travelTime}ms | Reqs in Block: ${queries} | Total Reqs: ${totalRequests}`, colors.gray);
console.log();
}
if (Math.random() > 0.90) {
const newWeaponId = Math.floor(Math.random() * 10) + 1;
const newArmorId = Math.floor(Math.random() * 10) + 11;
const newTalismanId = Math.floor(Math.random() * 5) + 26;
const startSwap = performance.now();
await setJson("player:1:weapon", [newWeaponId.toString()]);
await setJson("player:1:armor", [newArmorId.toString()]);
await setJson("player:1:talisman", [newTalismanId.toString()]);
const weaponName = (await getJson(`item:${newWeaponId}`))?.name;
const armorName = (await getJson(`item:${newArmorId}`))?.name;
const talismanName = (await getJson(`item:${newTalismanId}`))?.name;
totalRequests += 6;
const swapTime = (performance.now() - startSwap).toFixed(2);
console.log();
log(`>>> Gear Swapped <<<`, colors.cyan + colors.bold);
log(`Equipped: ${weaponName} | ${armorName} | ${talismanName}`, colors.cyan);
log(`[Stats] Query Time: ${swapTime}ms | Reqs in Block: 6 | Total Reqs: ${totalRequests}`, colors.gray);
console.log();
}
const startSync = performance.now();
await setJson("player:1", {
id: 1,
name: "Jonas",
health: playerHealth,
stamina: playerStamina,
level: playerLevel,
region_id: currentRegionId
});
totalRequests += 1;
const syncTime = (performance.now() - startSync).toFixed(2);
log(`[Live Sync] [ID: 1] Jonas - Lvl: ${playerLevel} | HP: ${playerHealth} | Stamina: ${playerStamina}`, colors.green);
log(`[Stats] Query Time: ${syncTime}ms | Reqs in Block: 1 | Total Reqs: ${totalRequests}`, colors.gray);
if (Math.random() > 0.7) {
const startLock = performance.now();
const enemyIds = (await getJson(`region:${currentRegionId}:enemies`)) || [];
totalRequests += 1;
const randomEnemyId = enemyIds.length > 0 ? enemyIds[Math.floor(Math.random() * enemyIds.length)] : null;
if (randomEnemyId) {
console.log();
log("--- Target Lock Detected ---", colors.red + colors.bold);
const enemyInfo = await getJson(`enemy:${randomEnemyId}`);
const dropItemIds = (await getJson(`enemy:${randomEnemyId}:items`)) || [];
const dropItemNames = await Promise.all(dropItemIds.map(async (id: string) => (await getJson(`item:${id}`))?.name));
const queries = 2 + dropItemIds.length;
totalRequests += queries;
const lockTime = (performance.now() - startLock).toFixed(2);
log(`Overlay -> [ID: ${enemyInfo.id}] ${enemyInfo.name} | Lvl: ${enemyInfo.level} | HP: ${enemyInfo.health} | DMG: ${enemyInfo.damage}`, colors.yellow);
log(`Drops -> ${dropItemNames.length > 0 ? dropItemNames.join(", ") : "None"}`, colors.yellow);
log(`[Stats] Query Time: ${lockTime}ms | Reqs in Block: ${queries + 1} | Total Reqs: ${totalRequests}`, colors.gray);
console.log();
}
}
await Bun.sleep(2000);
}

View File

@ -0,0 +1,16 @@
{
"name": "client-memcached",
"module": "index.ts",
"type": "module",
"private": true,
"dependencies": {
"memjs": "^1.3.2"
},
"devDependencies": {
"@types/bun": "latest",
"@types/memjs": "^1.3.3"
},
"peerDependencies": {
"typescript": "^5"
}
}

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@ -0,0 +1,81 @@
import { RedisClient } from "bun";
const redis = new RedisClient("redis://localhost:40379");
const DB_NAME = "Redis";
const N1 = 2000;
const N2 = 300;
const region1Enemies = ["1", "2", "3", "11", "12", "13", "14"];
const region1Items = ["1", "8", "21", "26"];
const playerObj = (i: number) => ({
id: 1,
name: "Jonas",
health: 50 + (i % 50),
stamina: 50 + (i % 50),
level: 1 + (i % 20),
region_id: 1,
});
const enemyObj = { id: 0, name: "Benchmark Enemy", health: 1000, level: 20, damage: 50 };
const itemObj = { id: 0, name: "Benchmark Item", gear_type: "weapon", gear_id: 1 };
const regionObj = { id: 1, name: "Limgrave" };
async function writeSingle(i: number) {
await redis.hset("bench:player:1", playerObj(i));
}
async function readSingle(_i: number) {
await redis.hgetall("bench:player:1");
}
async function readRegion(_i: number) {
await redis.hget("region:1", "name");
const enemyIds = await redis.smembers("region:1:enemies");
const itemIds = await redis.smembers("region:1:items");
for (const id of enemyIds) await redis.hgetall(`enemy:${id}`);
for (const id of itemIds) await redis.hgetall(`item:${id}`);
}
async function writeRegion(_i: number) {
await redis.hset("bench:region:1", regionObj);
await redis.sadd("bench:region:1:enemies", ...region1Enemies);
await redis.sadd("bench:region:1:items", ...region1Items);
for (const id of region1Enemies) await redis.hset(`bench:enemy:${id}`, { ...enemyObj, id });
for (const id of region1Items) await redis.hset(`bench:item:${id}`, { ...itemObj, id });
}
async function timed(label: string, group: string, iters: number, opsPerIter: number, fn: (i: number) => Promise<void>) {
const warmup = Math.min(50, iters);
for (let i = 0; i < warmup; i++) await fn(i);
const start = performance.now();
for (let i = 0; i < iters; i++) await fn(i);
const ms = performance.now() - start;
const totalOps = iters * opsPerIter;
const opsPerSec = totalOps / (ms / 1000);
const avgMs = ms / totalOps;
return { label, group, iters, opsPerIter, totalOps, ms, opsPerSec, avgMs };
}
function fmt(r: any) {
return ` ${r.label.padEnd(11)} | ${String(r.totalOps).padStart(6)} Ops | ${r.ms.toFixed(1).padStart(9)} ms | ${Math.round(r.opsPerSec).toString().padStart(9)} Ops/s | ${r.avgMs.toFixed(4)} ms/Op`;
}
await redis.ping();
const w1 = await timed("Schreiben", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, writeSingle);
const r1 = await timed("Lesen", "Anwendungsfall 1: Live-Player-Sync (1 Schluessel)", N1, 1, readSingle);
const r2 = await timed("Lesen", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, readRegion);
const w2 = await timed("Schreiben", "Anwendungsfall 2: Region laden / Fast Travel (14 Schluessel)", N2, 14, writeRegion);
console.log(`\n===== Benchmark: ${DB_NAME} =====`);
console.log(w1.group);
console.log(fmt(w1));
console.log(fmt(r1));
console.log(r2.group);
console.log(fmt(r2));
console.log(fmt(w2));
console.log("##RESULT## " + JSON.stringify({ db: DB_NAME, tests: [w1, r1, r2, w2] }));
process.exit(0);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,125 @@
import { write } from "bun";
const API_BASE = "https://eldenring.fanapis.com/api";
async function fetchAll(endpoint: string) {
let allData: any[] = [];
let page = 0;
const limit = 100;
while (true) {
const res = await fetch(`${API_BASE}/${endpoint}?limit=${limit}&page=${page}`);
if (!res.ok) {
console.error(`Error fetching ${endpoint}:`, res.status);
break;
}
const data = await res.json();
if (!data.data || data.data.length === 0) {
break;
}
allData = allData.concat(data.data);
console.log(`Fetched ${allData.length} ${endpoint}...`);
if (data.data.length < limit) {
break;
}
page++;
}
return allData;
}
function extractRegion(entityRegion: string | null | undefined, locationStr: string | null | undefined, locationLookup: Map<string, string>, uniqueRegions: Set<string>): string | null {
// 1. If the entity has a region, use it (and clean up typos from API)
if (entityRegion) {
let r = entityRegion;
if (r === "Liunia of the Lakes") r = "Liurnia of the Lakes";
if (r === "Mountaintop of the Giants") r = "Mountaintops of the Giants";
if (r === "Altus Pleateau") r = "Altus Plateau";
uniqueRegions.add(r);
return r;
}
// 2. Lookup via locations map
if (locationStr) {
for (const [locName, regionName] of locationLookup.entries()) {
if (locationStr.toLowerCase().includes(locName.toLowerCase())) {
return regionName;
}
}
// 3. Fallback: check if the location string itself contains a known dynamic region
for (const region of uniqueRegions) {
if (locationStr.toLowerCase().includes(region.toLowerCase())) {
return region;
}
}
}
return null;
}
async function main() {
console.log("Fetching locations...");
const locations = await fetchAll("locations");
const uniqueRegions = new Set<string>();
const locationLookup = new Map<string, string>();
// Normalize regions and build a lookup map: LocationName -> Region
for (const loc of locations) {
if (loc.region) {
let r = loc.region;
if (r === "Liunia of the Lakes" || r === "Liurnia") r = "Liurnia of the Lakes";
if (r === "Mountaintop of the Giants") r = "Mountaintops of the Giants";
if (r === "Altus Pleateau") r = "Altus Plateau";
uniqueRegions.add(r);
locationLookup.set(loc.name, r);
}
}
console.log("Fetching bosses...");
const bosses = await fetchAll("bosses");
console.log("Fetching creatures...");
const creatures = await fetchAll("creatures");
console.log("Fetching npcs...");
const npcs = await fetchAll("npcs");
const livingEntities = [
...bosses.map(b => ({ ...b, _type: "boss" })),
...creatures.map(c => ({ ...c, _type: "creature" })),
...npcs.map(n => ({ ...n, _type: "npc" }))
];
const cleanedEntities = livingEntities.map(entity => {
// combine all possible location info
const cleanRegion = extractRegion(entity.region, entity.location, locationLookup, uniqueRegions);
return {
id: entity.id,
name: entity.name,
type: entity._type,
cleanRegion: cleanRegion || "Unknown",
originalLocation: entity.location,
drops: entity.drops || []
};
});
// Save regions based on what was dynamically found in the API
const regionsData = Array.from(uniqueRegions).map((r, i) => ({ id: i + 1, name: r }));
console.log("Fetching items...");
const items = await fetchAll("items");
// you might also want weapons, armors etc depending on what "items" means to the user.
// The fan API separates items, weapons, armors, etc. Let's just fetch items for now.
const output = {
regions: regionsData,
entities: cleanedEntities,
items: items.map(i => ({ id: i.id, name: i.name, type: i.type, effect: i.effect }))
};
await write("./db/fanapi_data.json", JSON.stringify(output, null, 2));
console.log("Saved to db/fanapi_data.json");
}
main().catch(console.error);

View File

@ -0,0 +1,34 @@
import { RedisClient } from "bun";
import { effects, types, weapons, armors, shields, talismans, items, regions, enemies } from "../../data";
const redis = new RedisClient("redis://localhost:40379");
console.log("Populating database with ~100 static Ringstats entries...");
// Helper for hset
const setHash = async (prefix: string, list: any[]) => {
for (const obj of list) {
// extract items/enemies sets if they exist
const { items, enemies, ...hashData } = obj;
await redis.hset(`${prefix}:${obj.id}`, hashData);
if (items && items.length > 0) {
await redis.sadd(`${prefix}:${obj.id}:items`, ...items);
}
if (enemies && enemies.length > 0) {
await redis.sadd(`${prefix}:${obj.id}:enemies`, ...enemies);
}
}
};
await setHash("item_effect", effects);
await setHash("item_type", types);
await setHash("weapon", weapons);
await setHash("armor", armors);
await setHash("shield", shields);
await setHash("talisman", talismans);
await setHash("item", items);
await setHash("region", regions);
await setHash("enemy", enemies);
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into Redis.`);
console.log("Database successfully seeded.");
process.exit(0);

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@ -1,9 +1,8 @@
import { RedisClient } from "bun";
const redis = new RedisClient("redis://localhost:40379");
/**
* KI GENERIERT
*/
console.log("Populating database with ~100 static Ringstats entries...");
const effects = [
export const effects = [
{ id: 1, name: "Stärke", value: 5 },
{ id: 2, name: "Geschick", value: 5 },
{ id: 3, name: "Blutung", value: 50 },
@ -16,7 +15,7 @@ const effects = [
{ id: 10, name: "Blitzschaden", value: 25 },
];
const types = [
export const types = [
{ id: 1, item_id: 101, name: "Dolch" },
{ id: 2, item_id: 102, name: "Gerades Schwert" },
{ id: 3, item_id: 103, name: "Großschwert" },
@ -29,20 +28,20 @@ const types = [
{ id: 10, item_id: 110, name: "Talisman-Amulett" },
];
const weapons = [
{ id: 1, item_effect_id: 3, item_type_id: 4, weight: 5.5, damage: 115 }, // Uchigatana
{ id: 2, item_effect_id: 3, item_type_id: 4, weight: 6.5, damage: 118 }, // Rivers of Blood
{ id: 3, item_effect_id: 8, item_type_id: 3, weight: 10.0, damage: 160 }, // Dark Moon Greatsword
{ id: 4, item_effect_id: 1, item_type_id: 3, weight: 23.0, damage: 210 }, // Guts Greatsword
{ id: 5, item_effect_id: 3, item_type_id: 1, weight: 1.5, damage: 74 }, // Reduvia
{ id: 6, item_effect_id: 9, item_type_id: 2, weight: 4.0, damage: 105 }, // Blasphemous Blade
{ id: 7, item_effect_id: 10, item_type_id: 4, weight: 6.0, damage: 110 }, // Dragonscale Blade
{ id: 8, item_effect_id: 1, item_type_id: 3, weight: 15.0, damage: 175 }, // Claymore
{ id: 9, item_effect_id: 2, item_type_id: 1, weight: 2.0, damage: 80 }, // Misericorde
{ id: 10, item_effect_id: 6, item_type_id: 2, weight: 4.5, damage: 100 }, // Icerind Hatchet (simulated as straight sword for simplicity)
export const weapons = [
{ id: 1, item_effect_id: 3, item_type_id: 4, weight: 5.5, damage: 115 },
{ id: 2, item_effect_id: 3, item_type_id: 4, weight: 6.5, damage: 118 },
{ id: 3, item_effect_id: 8, item_type_id: 3, weight: 10.0, damage: 160 },
{ id: 4, item_effect_id: 1, item_type_id: 3, weight: 23.0, damage: 210 },
{ id: 5, item_effect_id: 3, item_type_id: 1, weight: 1.5, damage: 74 },
{ id: 6, item_effect_id: 9, item_type_id: 2, weight: 4.0, damage: 105 },
{ id: 7, item_effect_id: 10, item_type_id: 4, weight: 6.0, damage: 110 },
{ id: 8, item_effect_id: 1, item_type_id: 3, weight: 15.0, damage: 175 },
{ id: 9, item_effect_id: 2, item_type_id: 1, weight: 2.0, damage: 80 },
{ id: 10, item_effect_id: 6, item_type_id: 2, weight: 4.5, damage: 100 },
];
const armors = [
export const armors = [
{ id: 1, item_effect_id: 1, item_type_id: 5, weight: 4.6, defense: 14 },
{ id: 2, item_effect_id: 1, item_type_id: 6, weight: 10.6, defense: 35 },
{ id: 3, item_effect_id: 1, item_type_id: 7, weight: 3.5, defense: 11 },
@ -55,7 +54,7 @@ const armors = [
{ id: 10, item_effect_id: 10, item_type_id: 6, weight: 7.0, defense: 22 },
];
const shields = [
export const shields = [
{ id: 1, item_effect_id: 1, item_type_id: 9, weight: 3.5, damage: 45, defense: 100 },
{ id: 2, item_effect_id: 8, item_type_id: 9, weight: 4.5, damage: 55, defense: 100 },
{ id: 3, item_effect_id: 9, item_type_id: 9, weight: 5.5, damage: 65, defense: 100 },
@ -63,7 +62,7 @@ const shields = [
{ id: 5, item_effect_id: 6, item_type_id: 9, weight: 3.0, damage: 40, defense: 95 },
];
const talismans = [
export const talismans = [
{ id: 1, item_effect_id: 1 },
{ id: 2, item_effect_id: 2 },
{ id: 3, item_effect_id: 7 },
@ -71,8 +70,7 @@ const talismans = [
{ id: 5, item_effect_id: 9 },
];
const items = [
// Weapons (id 1-10)
export const items = [
{ id: 1, name: "Uchigatana", gear_type: "weapon", gear_id: 1 },
{ id: 2, name: "Rivers of Blood", gear_type: "weapon", gear_id: 2 },
{ id: 3, name: "Dark Moon Greatsword", gear_type: "weapon", gear_id: 3 },
@ -83,7 +81,6 @@ const items = [
{ id: 8, name: "Claymore", gear_type: "weapon", gear_id: 8 },
{ id: 9, name: "Misericorde", gear_type: "weapon", gear_id: 9 },
{ id: 10, name: "Icerind Hatchet", gear_type: "weapon", gear_id: 10 },
// Armors (id 11-20)
{ id: 11, name: "Knight Helm", gear_type: "armor", gear_id: 1 },
{ id: 12, name: "Knight Armor", gear_type: "armor", gear_id: 2 },
{ id: 13, name: "Knight Gauntlets", gear_type: "armor", gear_id: 3 },
@ -94,13 +91,11 @@ const items = [
{ id: 18, name: "Radahn Lion Armor", gear_type: "armor", gear_id: 8 },
{ id: 19, name: "Carian Knight Helm", gear_type: "armor", gear_id: 9 },
{ id: 20, name: "Carian Knight Armor", gear_type: "armor", gear_id: 10 },
// Shields (id 21-25)
{ id: 21, name: "Brass Shield", gear_type: "shield", gear_id: 1 },
{ id: 22, name: "Carian Knight Shield", gear_type: "shield", gear_id: 2 },
{ id: 23, name: "Kite Shield", gear_type: "shield", gear_id: 3 },
{ id: 24, name: "Banished Knight Shield", gear_type: "shield", gear_id: 4 },
{ id: 25, name: "Heater Shield", gear_type: "shield", gear_id: 5 },
// Talismans (id 26-30)
{ id: 26, name: "Erdtree's Favor", gear_type: "talisman", gear_id: 1 },
{ id: 27, name: "Radagon's Soreseal", gear_type: "talisman", gear_id: 2 },
{ id: 28, name: "Crimson Amber Medallion", gear_type: "talisman", gear_id: 3 },
@ -128,7 +123,7 @@ const items = [
{ id: 50, name: "Ghost Glovewort [9]", gear_type: "material" },
];
const regions = [
export const regions = [
{ id: 1, name: "Limgrave", items: ["1", "8", "21", "26"], enemies: ["1", "2", "3", "11", "12", "13", "14"] },
{ id: 2, name: "Liurnia of the Lakes", items: ["3", "15", "16", "19", "20", "22", "29"], enemies: ["4", "5", "15", "16", "17", "18"] },
{ id: 3, name: "Caelid", items: ["4", "17", "18"], enemies: ["6", "7", "19", "20", "21", "22"] },
@ -147,7 +142,7 @@ const regions = [
{ id: 16, name: "Leyndell, Ashen Capital", items: [], enemies: [] },
];
const enemies = [
export const enemies = [
{ id: 1, region_id: 1, name: "Tree Sentinel", health: 3000, level: 20, damage: 70, items: ["1", "47", "34"] },
{ id: 2, region_id: 1, name: "Margit, the Fell Omen", health: 4100, level: 25, damage: 100, items: ["26", "33", "34"] },
{ id: 3, region_id: 1, name: "Godrick the Grafted", health: 6000, level: 30, damage: 150, items: ["33", "39"] },
@ -179,32 +174,3 @@ const enemies = [
{ id: 29, region_id: 6, name: "Beastman of Farum Azula", health: 1000, level: 80, damage: 200, items: ["37", "48"] },
{ id: 30, region_id: 6, name: "Banished Knight", health: 2000, level: 85, damage: 280, items: ["24", "37", "48", "50"] },
];
// Helper for hset
const setHash = async (prefix: string, list: any[]) => {
for (const obj of list) {
// extract items/enemies sets if they exist
const { items, enemies, ...hashData } = obj;
await redis.hset(`${prefix}:${obj.id}`, hashData);
if (items && items.length > 0) {
await redis.sadd(`${prefix}:${obj.id}:items`, ...items);
}
if (enemies && enemies.length > 0) {
await redis.sadd(`${prefix}:${obj.id}:enemies`, ...enemies);
}
}
};
await setHash("item_effect", effects);
await setHash("item_type", types);
await setHash("weapon", weapons);
await setHash("armor", armors);
await setHash("shield", shields);
await setHash("talisman", talismans);
await setHash("item", items);
await setHash("region", regions);
await setHash("enemy", enemies);
console.log(`Inserted ${effects.length + types.length + weapons.length + armors.length + shields.length + talismans.length + items.length + regions.length + enemies.length} primary entities into Redis.`);
console.log("Database successfully seeded.");
process.exit(0);

View File

@ -1,118 +0,0 @@
import { serve, RedisClient } from "bun";
import index from "./index.html";
const redis = new RedisClient("redis://localhost:40379");
const server = serve({
routes: {
"/*": index,
"/api/player": async (req) => {
try {
const player = await redis.hgetall("player:1");
if (!player || !player.id) {
return Response.json({ error: "Player not found" }, { status: 404 });
}
const currentRegionId = player.region_id;
let region = null;
if (currentRegionId) {
const regionName = await redis.hget(`region:${currentRegionId}`, "name");
const enemiesIds = await redis.smembers(`region:${currentRegionId}:enemies`);
const itemsIds = await redis.smembers(`region:${currentRegionId}:items`);
const enemies = [];
for (const id of enemiesIds) {
const enemy = await redis.hgetall(`enemy:${id}`);
if (enemy && enemy.id) enemies.push(enemy);
}
const items = [];
for (const id of itemsIds) {
const item = await redis.hgetall(`item:${id}`);
if (item && item.id) items.push(item);
}
region = {
id: currentRegionId,
name: regionName,
enemies,
items
};
}
return Response.json({ player, region });
} catch (e: any) {
return Response.json({ error: e.message }, { status: 500 });
}
},
"/api/universe": async (req) => {
try {
// Fetch all regions 1-16
const regions = [];
for (let i = 1; i <= 16; i++) {
const name = await redis.hget(`region:${i}`, "name");
if (name) {
const enemiesIds = await redis.smembers(`region:${i}:enemies`);
const itemsIds = await redis.smembers(`region:${i}:items`);
regions.push({ id: i.toString(), name, enemiesIds, itemsIds });
}
}
// Fetch enemies 1-30
const enemies = [];
for (let i = 1; i <= 30; i++) {
const enemy = await redis.hgetall(`enemy:${i}`);
if (enemy && enemy.id) {
const itemsIds = await redis.smembers(`enemy:${i}:items`);
enemy.itemsIds = itemsIds || [];
enemies.push(enemy);
}
}
// Fetch items 1-50
const items = [];
for (let i = 1; i <= 50; i++) {
const item = await redis.hgetall(`item:${i}`);
if (item && item.id) {
if (item.gear_type && item.gear_id) {
const gearStats = await redis.hgetall(`${item.gear_type}:${item.gear_id}`);
if (gearStats.item_type_id) {
const typeInfo = await redis.hgetall(`item_type:${gearStats.item_type_id}`);
if (typeInfo && typeInfo.name) gearStats.Item_Type = typeInfo.name;
delete gearStats.item_type_id;
}
if (gearStats.item_effect_id) {
const effectInfo = await redis.hgetall(`item_effect:${gearStats.item_effect_id}`);
if (effectInfo && effectInfo.name) {
gearStats.Item_Effect = `${effectInfo.name} ${effectInfo.value ? `(+${effectInfo.value})` : ""}`;
}
delete gearStats.item_effect_id;
}
delete gearStats.id; // redundant
item.stats = gearStats;
}
items.push(item);
}
}
return Response.json({ regions, enemies, items });
} catch (e: any) {
return Response.json({ error: e.message }, { status: 500 });
}
}
},
development: process.env.NODE_ENV !== "production" && {
hmr: true,
console: true,
},
});
console.log(`🚀 Server running at ${server.url}`);

34
endpraesi/website/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@ -5,10 +5,12 @@
"": {
"name": "bun-react-template",
"dependencies": {
"arangojs": "^9.2.0",
"bun-plugin-tailwind": "^0.1.2",
"clsx": "^2.1.1",
"framer-motion": "^12.42.2",
"lucide-react": "^1.23.0",
"memjs": "^1.3.2",
"react": "^19",
"react-dom": "^19",
"tailwind-merge": "^3.6.0",
@ -16,6 +18,7 @@
},
"devDependencies": {
"@types/bun": "latest",
"@types/memjs": "^1.3.3",
"@types/react": "^19",
"@types/react-dom": "^19",
},
@ -56,12 +59,16 @@
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/memjs": ["@types/memjs@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-OqZuZ2T0ErrygA/WUqQjmsB1GpxAt84OCgKPKbsSWwYKA/oe/lTiqvqMgFOv/Ngl8p+nytgdhrdxJtikOgimCg=="],
"@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"arangojs": ["arangojs@9.3.0", "", { "dependencies": { "@types/node": "^20.11.26" } }, "sha512-+z/TxumH8ywsXAN0oyQAxMnBMtDbd4tjFhGXzygAHD8+YDzEC705STLBh38KGbwxWeekalu2XEHUtrJ/NONgTQ=="],
"bun": ["bun@1.3.14", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.14", "@oven/bun-darwin-x64": "1.3.14", "@oven/bun-darwin-x64-baseline": "1.3.14", "@oven/bun-freebsd-aarch64": "1.3.14", "@oven/bun-freebsd-x64": "1.3.14", "@oven/bun-linux-aarch64": "1.3.14", "@oven/bun-linux-aarch64-android": "1.3.14", "@oven/bun-linux-aarch64-musl": "1.3.14", "@oven/bun-linux-x64": "1.3.14", "@oven/bun-linux-x64-android": "1.3.14", "@oven/bun-linux-x64-baseline": "1.3.14", "@oven/bun-linux-x64-musl": "1.3.14", "@oven/bun-linux-x64-musl-baseline": "1.3.14", "@oven/bun-windows-aarch64": "1.3.14", "@oven/bun-windows-x64": "1.3.14", "@oven/bun-windows-x64-baseline": "1.3.14" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg=="],
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
@ -76,6 +83,8 @@
"lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="],
"memjs": ["memjs@1.3.2", "", {}, "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg=="],
"motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="],
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
@ -93,5 +102,9 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"arangojs/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
"arangojs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
}
}

View File

@ -9,10 +9,12 @@
"build": "bun run build.ts"
},
"dependencies": {
"arangojs": "^9.2.0",
"bun-plugin-tailwind": "^0.1.2",
"clsx": "^2.1.1",
"framer-motion": "^12.42.2",
"lucide-react": "^1.23.0",
"memjs": "^1.3.2",
"react": "^19",
"react-dom": "^19",
"tailwind-merge": "^3.6.0",
@ -21,6 +23,7 @@
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/bun": "latest"
"@types/bun": "latest",
"@types/memjs": "^1.3.3"
}
}

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect } from "react";
import "./index.css";
import {
Database, Home, Search, Search as SearchIcon, Map as MapIcon, Shield, Sword,
Heart, Activity, User, Box, ChevronDown, ChevronRight,
Zap, Crosshair, Droplets
import {
Database, Home, Search, Search as SearchIcon, Map as MapIcon, Shield, Sword,
Heart, Activity, User, Box, ChevronDown, ChevronRight,
Zap, Crosshair, Droplets, Clock
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { clsx, type ClassValue } from "clsx";
@ -13,18 +13,33 @@ function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
function Ms({ ms, className }: { ms?: number, className?: string }) {
if (ms == null) return null;
return (
<span className={cn("inline-flex items-center gap-1 font-mono text-[10px] text-indigo-300/80 tabular-nums bg-indigo-500/10 border border-indigo-500/20 rounded px-1.5 py-0.5", className)}>
<Clock className="w-2.5 h-2.5" />
{ms.toFixed(3)} ms
</span>
);
}
type Player = { id: string, name: string, health: string, stamina: string, level: string, region_id: string };
type Enemy = { id: string, name: string, health: string, level: string, damage: string, itemsIds: string[], region_id: string };
type Item = { id: string, name: string, gear_type?: string, gear_id?: string, stats?: Record<string, string> };
type Region = { id: string, name: string, enemiesIds: string[], itemsIds: string[], enemies?: Enemy[], items?: Item[] };
type Enemy = { id: string, name: string, health: string, level: string, damage: string, itemsIds: string[], region_id: string, _ms?: number };
type Item = { id: string, name: string, gear_type?: string, gear_id?: string, stats?: Record<string, string>, _ms?: number };
type Region = { id: string, name: string, enemiesIds: string[], itemsIds: string[], enemies?: Enemy[], items?: Item[], _ms?: number };
type Timing = { op: string, key: string, ms: number };
type Timings = { count: number, total: number, ops: Timing[] };
export function App() {
const [activeTab, setActiveTab] = useState<"home" | "search">("home");
const [db, setDb] = useState("redis");
const [dbDropdownOpen, setDbDropdownOpen] = useState(false);
const [playerData, setPlayerData] = useState<{player: Player, region: Region} | null>(null);
const [universe, setUniverse] = useState<{regions: Region[], enemies: Enemy[], items: Item[]} | null>(null);
const [playerTimings, setPlayerTimings] = useState<Timings | null>(null);
const [universeTimings, setUniverseTimings] = useState<Timings | null>(null);
// Modals
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
@ -32,20 +47,27 @@ export function App() {
const [selectedEnemy, setSelectedEnemy] = useState<Enemy | null>(null);
useEffect(() => {
fetch("/api/universe")
setUniverse(null);
setUniverseTimings(null);
fetch(`/api/universe?db=${db}`)
.then(r => r.json())
.then(data => setUniverse(data))
.then(data => {
setUniverse(data);
setUniverseTimings(data._timings || null);
})
.catch(console.error);
}, []);
}, [db]);
useEffect(() => {
if (db !== "redis") return;
setPlayerData(null);
setPlayerTimings(null);
const fetchPlayer = async () => {
try {
const res = await fetch("/api/player");
const res = await fetch(`/api/player?db=${db}`);
if (res.ok) {
const data = await res.json();
setPlayerData(data);
setPlayerTimings(data._timings || null);
}
} catch (e) {
console.error(e);
@ -68,10 +90,12 @@ export function App() {
const dbOptions = [
{ id: "redis", name: "Redis" },
{ id: "mongodb", name: "MongoDB" },
{ id: "neo4j", name: "Neo4j" }
{ id: "memcached", name: "Memcached" },
{ id: "arangodb", name: "ArangoDB" }
];
const dbLabel = dbOptions.find(o => o.id === db)?.name || db;
return (
<div className="flex h-screen bg-slate-950 text-slate-200 overflow-hidden font-sans selection:bg-indigo-500/30">
@ -157,7 +181,7 @@ export function App() {
db === option.id ? "bg-indigo-500/10 text-indigo-300" : "hover:bg-white/5 text-slate-300"
)}
>
{option.name} {option.id !== "redis" && "(WIP)"}
{option.name}
</button>
))}
</motion.div>
@ -173,21 +197,13 @@ export function App() {
<div className="absolute top-0 left-1/4 w-96 h-96 bg-indigo-500/10 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-[120px] pointer-events-none" />
{db !== "redis" ? (
<div className="h-full flex flex-col items-center justify-center text-slate-500">
<Database className="w-16 h-16 mb-4 opacity-20" />
<h2 className="text-xl font-medium">Database not implemented yet.</h2>
<p>Please select Redis to view live data.</p>
</div>
) : (
<AnimatePresence mode="wait">
{activeTab === "home" ? (
<HomeView key="home" data={playerData} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} onEnemyClick={setSelectedEnemy} />
) : (
<SearchView key="search" universe={universe} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} onEnemyClick={setSelectedEnemy} />
)}
</AnimatePresence>
)}
<AnimatePresence mode="wait">
{activeTab === "home" ? (
<HomeView key="home" data={playerData} timings={playerTimings} dbLabel={dbLabel} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} />
) : (
<SearchView key="search" universe={universe} timings={universeTimings} dbLabel={dbLabel} onItemClick={setSelectedItem} onRegionClick={setSelectedRegion} />
)}
</AnimatePresence>
</div>
</main>
@ -205,6 +221,7 @@ export function App() {
<div className="flex items-center gap-3">
<Box className="w-5 h-5 text-indigo-400" />
<h3 className="text-lg font-bold text-white">{selectedItem.name}</h3>
<Ms ms={selectedItem._ms} />
</div>
<button onClick={() => setSelectedItem(null)} className="text-slate-400 hover:text-white transition-colors">
@ -290,6 +307,7 @@ export function App() {
<div className="flex items-center gap-3">
<Sword className="w-5 h-5 text-rose-400" />
<h3 className="text-lg font-bold text-white">{selectedEnemy.name}</h3>
<Ms ms={selectedEnemy._ms} />
</div>
<button onClick={() => setSelectedEnemy(null)} className="text-slate-400 hover:text-white transition-colors">
@ -382,6 +400,7 @@ export function App() {
<div className="flex items-center gap-3">
<MapIcon className="w-5 h-5 text-purple-400" />
<h3 className="text-lg font-bold text-white">{selectedRegion.name}</h3>
<Ms ms={selectedRegion._ms} />
</div>
<button onClick={() => setSelectedRegion(null)} className="text-slate-400 hover:text-white transition-colors">
@ -445,22 +464,80 @@ export function App() {
);
}
function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
function TimingPanel({ title, timings, regionCached }: { title: string, timings: Timings | null, regionCached?: boolean }) {
const [open, setOpen] = useState(false);
if (!timings) return null;
return (
<div className="bg-slate-900/60 border border-white/5 rounded-2xl overflow-hidden">
<button
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between p-4 hover:bg-white/5 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20">
<Clock className="w-4 h-4 text-indigo-400" />
</div>
<span className="text-sm font-medium text-slate-300">{title}</span>
{regionCached && (
<span className="font-mono text-[10px] text-emerald-300/90 tabular-nums bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-0.5">
Region aus Cache
</span>
)}
</div>
<div className="flex items-center gap-4">
<span className="text-xs text-slate-500 tabular-nums">{timings.count} Ops</span>
<span className="text-sm font-mono font-bold text-indigo-300 tabular-nums">{timings.total.toFixed(2)} ms</span>
<motion.div animate={{ rotate: open ? 180 : 0 }}>
<ChevronDown className="w-4 h-4 text-slate-500" />
</motion.div>
</div>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="border-t border-white/5 bg-black/20"
>
<div className="max-h-72 overflow-y-auto divide-y divide-white/5">
{timings.ops.map((op, i) => (
<div key={i} className="flex items-center justify-between px-4 py-2 text-xs gap-4">
<span className="font-mono text-slate-400 truncate">
<span className="text-indigo-300/70">{op.op}</span> <span className="text-slate-500">{op.key}</span>
</span>
<span className="font-mono text-slate-300 tabular-nums shrink-0">{op.ms.toFixed(3)} ms</span>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function HomeView({ data, timings, dbLabel, onItemClick, onRegionClick }: { data: any, timings: Timings | null, dbLabel: string, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
if (!data) return (
<div className="h-full flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500"></div>
</div>
);
const { player, region } = data;
const { player, region, regionCached } = data;
return (
<motion.div
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="max-w-5xl mx-auto space-y-8"
>
<TimingPanel title={`${dbLabel} · Live Sync (Player + Region)`} timings={timings} regionCached={regionCached} />
{/* Player Stats Card */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="col-span-1 md:col-span-2 bg-gradient-to-br from-slate-900 to-slate-800/80 p-8 rounded-3xl border border-white/10 relative overflow-hidden group">
@ -515,6 +592,7 @@ function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick
</div>
<h3 className="text-sm text-slate-400 font-medium tracking-widest uppercase mb-2">Current Location</h3>
<div className="text-2xl font-bold text-white group-hover:text-purple-100 transition-colors">{region?.name || "Unknown"}</div>
{region?._ms != null && <div className="mt-2 flex justify-center"><Ms ms={region._ms} /></div>}
<div className="mt-3 text-xs text-purple-400 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1">
View Details <ChevronRight className="w-3 h-3" />
</div>
@ -576,6 +654,7 @@ function HomeView({ data, onItemClick, onRegionClick }: { data: any, onItemClick
<div>
<div className="font-medium text-white truncate max-w-[120px]">{item.name}</div>
<div className="text-xs text-slate-500 capitalize">{item.gear_type || "Item"}</div>
<div className="mt-1"><Ms ms={item._ms} /></div>
</div>
</button>
))}
@ -607,6 +686,7 @@ function EnemyCard({ enemy, onItemClick, allItems, allRegions }: { enemy: Enemy,
<div className="font-bold text-white text-lg">{enemy.name}</div>
<div className="flex items-center gap-3 mt-1">
<span className="text-xs font-medium px-2 py-0.5 rounded bg-slate-800 text-slate-300">Lvl {enemy.level}</span>
<Ms ms={enemy._ms} />
{regionName && (
<span className="text-xs font-medium text-slate-400 flex items-center gap-1 truncate max-w-[150px]">
<MapIcon className="w-3 h-3 text-purple-400" />
@ -675,7 +755,7 @@ function EnemyCard({ enemy, onItemClick, allItems, allRegions }: { enemy: Enemy,
);
}
function SearchView({ universe, onItemClick, onRegionClick }: { universe: any, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
function SearchView({ universe, timings, dbLabel, onItemClick, onRegionClick }: { universe: any, timings: Timings | null, dbLabel: string, onItemClick: (item: Item) => void, onRegionClick: (r: Region) => void }) {
const [query, setQuery] = useState("");
if (!universe) return (
@ -691,15 +771,17 @@ function SearchView({ universe, onItemClick, onRegionClick }: { universe: any, o
const itemsMatch = universe.items.filter((i: Item) => i.name.toLowerCase().includes(q));
return (
<motion.div
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="max-w-5xl mx-auto space-y-8 h-full flex flex-col"
>
<TimingPanel title={`${dbLabel} · Universe geladen`} timings={timings} />
<div className="relative">
<SearchIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}

View File

@ -0,0 +1,244 @@
import { serve, RedisClient } from "bun";
import { Client } from "memjs";
import { Database } from "arangojs";
import index from "./index.html";
const redis = new RedisClient("redis://localhost:40379");
const memcached = Client.create("localhost:41211");
const arango = new Database({ url: "http://localhost:48529" });
const arangoKv = arango.collection("kv");
type Timing = { op: string; key: string; ms: number };
type Adapter = {
timings: Timing[];
getObject: (key: string) => Promise<any>;
getField: (key: string, field: string) => Promise<any>;
getIds: (key: string) => Promise<string[]>;
};
function stringifyObj(o: any) {
if (!o) return null;
const out: any = {};
for (const k in o) out[k] = o[k] == null ? o[k] : String(o[k]);
return out;
}
function redisAdapter(): Adapter {
const timings: Timing[] = [];
const track = async <T>(op: string, key: string, fn: () => Promise<T>): Promise<T> => {
const start = performance.now();
const res = await fn();
timings.push({ op, key, ms: performance.now() - start });
return res;
};
return {
timings,
getObject: (key) =>
track("hgetall", key, async () => {
const o = await redis.hgetall(key);
return o && Object.keys(o).length ? o : null;
}),
getField: (key, field) => track("hget", key, () => redis.hget(key, field)),
getIds: (key) => track("smembers", key, async () => (await redis.smembers(key)) || []),
};
}
function memcachedAdapter(): Adapter {
const timings: Timing[] = [];
const getJson = async (key: string) => {
const { value } = await memcached.get(key);
return value ? JSON.parse(value.toString()) : null;
};
const track = async <T>(op: string, key: string, fn: () => Promise<T>): Promise<T> => {
const start = performance.now();
const res = await fn();
timings.push({ op, key, ms: performance.now() - start });
return res;
};
return {
timings,
getObject: (key) => track("get", key, async () => stringifyObj(await getJson(key))),
getField: (key, field) => track("get", key, async () => (await getJson(key))?.[field] ?? null),
getIds: (key) => track("get", key, async () => ((await getJson(key)) || []).map(String)),
};
}
function arangoAdapter(): Adapter {
const timings: Timing[] = [];
const getVal = async (key: string) => {
const doc = await arangoKv.document(key, { graceful: true });
return doc ? doc.value : null;
};
const track = async <T>(op: string, key: string, fn: () => Promise<T>): Promise<T> => {
const start = performance.now();
const res = await fn();
timings.push({ op, key, ms: performance.now() - start });
return res;
};
return {
timings,
getObject: (key) => track("document", key, async () => stringifyObj(await getVal(key))),
getField: (key, field) => track("document", key, async () => (await getVal(key))?.[field] ?? null),
getIds: (key) => track("document", key, async () => ((await getVal(key)) || []).map(String)),
};
}
function getAdapter(db: string): Adapter {
if (db === "memcached") return memcachedAdapter();
if (db === "arangodb") return arangoAdapter();
return redisAdapter();
}
function timingSummary(timings: Timing[]) {
const total = timings.reduce((s, t) => s + t.ms, 0);
return { count: timings.length, total, ops: timings };
}
const sumMs = (timings: Timing[]) => timings.reduce((s, t) => s + t.ms, 0);
const regionCache: Record<string, { id: string; region: any }> = {};
async function handlePlayer(adapter: Adapter, db: string) {
const player = await adapter.getObject("player:1");
if (!player || !player.id) return null;
const currentRegionId = player.region_id;
let region = null;
let regionCached = false;
if (currentRegionId) {
const cached = regionCache[db];
if (cached && cached.id === currentRegionId) {
region = cached.region;
regionCached = true;
} else {
const regionStart = sumMs(adapter.timings);
const regionName = await adapter.getField(`region:${currentRegionId}`, "name");
const enemiesIds = await adapter.getIds(`region:${currentRegionId}:enemies`);
const itemsIds = await adapter.getIds(`region:${currentRegionId}:items`);
const regionMs = sumMs(adapter.timings) - regionStart;
const enemies = [];
for (const id of enemiesIds) {
const s = sumMs(adapter.timings);
const enemy = await adapter.getObject(`enemy:${id}`);
if (enemy && enemy.id) {
enemy._ms = sumMs(adapter.timings) - s;
enemies.push(enemy);
}
}
const items = [];
for (const id of itemsIds) {
const s = sumMs(adapter.timings);
const item = await adapter.getObject(`item:${id}`);
if (item && item.id) {
item._ms = sumMs(adapter.timings) - s;
items.push(item);
}
}
region = { id: currentRegionId, name: regionName, enemies, items, _ms: regionMs };
regionCache[db] = { id: currentRegionId, region };
}
}
return { player, region, regionCached };
}
async function handleUniverse(adapter: Adapter) {
const regions = [];
for (let i = 1; i <= 16; i++) {
const s = sumMs(adapter.timings);
const name = await adapter.getField(`region:${i}`, "name");
if (name) {
const enemiesIds = await adapter.getIds(`region:${i}:enemies`);
const itemsIds = await adapter.getIds(`region:${i}:items`);
regions.push({ id: i.toString(), name, enemiesIds, itemsIds, _ms: sumMs(adapter.timings) - s });
}
}
const enemies = [];
for (let i = 1; i <= 30; i++) {
const s = sumMs(adapter.timings);
const enemy = await adapter.getObject(`enemy:${i}`);
if (enemy && enemy.id) {
const itemsIds = await adapter.getIds(`enemy:${i}:items`);
enemy.itemsIds = itemsIds || [];
enemy._ms = sumMs(adapter.timings) - s;
enemies.push(enemy);
}
}
const items = [];
for (let i = 1; i <= 50; i++) {
const s = sumMs(adapter.timings);
const item = await adapter.getObject(`item:${i}`);
if (item && item.id) {
if (item.gear_type && item.gear_id) {
const gearStats = await adapter.getObject(`${item.gear_type}:${item.gear_id}`);
if (gearStats) {
if (gearStats.item_type_id) {
const typeInfo = await adapter.getObject(`item_type:${gearStats.item_type_id}`);
if (typeInfo && typeInfo.name) gearStats.Item_Type = typeInfo.name;
delete gearStats.item_type_id;
}
if (gearStats.item_effect_id) {
const effectInfo = await adapter.getObject(`item_effect:${gearStats.item_effect_id}`);
if (effectInfo && effectInfo.name) {
gearStats.Item_Effect = `${effectInfo.name} ${effectInfo.value ? `(+${effectInfo.value})` : ""}`;
}
delete gearStats.item_effect_id;
}
delete gearStats.id;
item.stats = gearStats;
}
}
item._ms = sumMs(adapter.timings) - s;
items.push(item);
}
}
return { regions, enemies, items };
}
const server = serve({
routes: {
"/*": index,
"/api/player": async (req) => {
const db = new URL(req.url).searchParams.get("db") || "redis";
try {
const adapter = getAdapter(db);
const result = await handlePlayer(adapter, db);
if (!result) {
return Response.json({ error: "Player not found", db }, { status: 404 });
}
return Response.json({ ...result, db, _timings: timingSummary(adapter.timings) });
} catch (e: any) {
return Response.json({ error: e.message, db }, { status: 500 });
}
},
"/api/universe": async (req) => {
const db = new URL(req.url).searchParams.get("db") || "redis";
try {
const adapter = getAdapter(db);
const result = await handleUniverse(adapter);
return Response.json({ ...result, db, _timings: timingSummary(adapter.timings) });
} catch (e: any) {
return Response.json({ error: e.message, db }, { status: 500 });
}
},
},
development: process.env.NODE_ENV !== "production" && {
hmr: true,
console: true,
},
});
console.log(`🚀 Server running at ${server.url}`);

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B