Repository: zincware/ZnDraw (language: python)

Issue:
Efficiency-increase
fix bug in atoms cache deletion


Discussion / hints:

Fix patch:
diff --git a/examples/md.ipynb b/examples/md.ipynb
index 80c86ea7b..3c55cc1d7 100644
--- a/examples/md.ipynb
+++ b/examples/md.ipynb
@@ -23,13 +23,16 @@
     "from ase import units\n",
     "\n",
     "from ase.calculators.emt import EMT\n",
+    "\n",
     "size = 2\n",
     "\n",
     "# Set up a crystal\n",
-    "atoms = FaceCenteredCubic(directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],\n",
-    "                          symbol=\"Cu\",\n",
-    "                          size=(size, size, size),\n",
-    "                          pbc=True)\n",
+    "atoms = FaceCenteredCubic(\n",
+    "    directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],\n",
+    "    symbol=\"Cu\",\n",
+    "    size=(size, size, size),\n",
+    "    pbc=True,\n",
+    ")\n",
     "\n",
     "# Describe the interatomic interactions with the Effective Medium Theory\n",
     "atoms.calc = EMT()\n",
@@ -45,8 +48,10 @@
     "    \"\"\"Function to print the potential, kinetic and total energy.\"\"\"\n",
     "    epot = a.get_potential_energy() / len(a)\n",
     "    ekin = a.get_kinetic_energy() / len(a)\n",
-    "    print('Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '\n",
-    "          'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin))"
+    "    print(\n",
+    "        \"Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  \"\n",
+    "        \"Etot = %.3feV\" % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin)\n",
+    "    )"
    ]
   },
   {
@@ -55,7 +60,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "vis = ZnDraw(url=\"http://127.0.0.1:47823/\", token=\"fcd45c3917d34b1a8329bd5a5f172c0f\")\n"
+    "vis = ZnDraw(url=\"http://127.0.0.1:47823/\", token=\"fcd45c3917d34b1a8329bd5a5f172c0f\")"
    ]
   },
   {
diff --git a/examples/molecules.ipynb b/examples/molecules.ipynb
index bd74292aa..4f1bd3302 100644
--- a/examples/molecules.ipynb
+++ b/examples/molecules.ipynb
@@ -57,7 +57,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "vis.socket.sleep(0.5) # if we just started the server, we need to wait a bit"
+    "vis.socket.sleep(0.5)  # if we just started the server, we need to wait a bit"
    ]
   },
   {
@@ -126,7 +126,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "vis.display(2) # display the second molecule"
+    "vis.display(2)  # display the second molecule"
    ]
   },
   {
@@ -135,7 +135,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "vis[0] = s22['Water_dimer']"
+    "vis[0] = s22[\"Water_dimer\"]"
    ]
   },
   {
@@ -144,7 +144,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "vis.append(s22['Formic_acid_dimer'])"
+    "vis.append(s22[\"Formic_acid_dimer\"])"
    ]
   },
   {
diff --git a/poetry.lock b/poetry.lock
index 9523dc747..6108a5b02 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -59,6 +59,17 @@ scipy = ">=1.1.0"
 docs = ["pillow", "sphinx", "sphinx-rtd-theme"]
 test = ["pytest (>=5.0.0)", "pytest-mock (>=3.3.0)", "pytest-xdist (>=1.30.0)"]
 
+[[package]]
+name = "async-timeout"
+version = "4.0.3"
+description = "Timeout context manager for asyncio programs"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
+    {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
+]
+
 [[package]]
 name = "attrs"
 version = "23.2.0"
@@ -2160,6 +2171,87 @@ files = [
 [package.dependencies]
 wcwidth = "*"
 
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.9"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
+    {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
+    {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
+    {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
+    {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
+    {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
+    {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
+]
+
 [[package]]
 name = "pycparser"
 version = "2.21"
@@ -2619,6 +2711,24 @@ ase = ">=3.22.1,<4.0.0"
 networkx = ">=3.1,<4.0"
 rdkit = ">=2023.3.2,<2024.0.0"
 
+[[package]]
+name = "redis"
+version = "5.0.1"
+description = "Python client for Redis database and key-value store"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"},
+    {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"},
+]
+
+[package.dependencies]
+async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""}
+
+[package.extras]
+hiredis = ["hiredis (>=1.0.0)"]
+ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
+
 [[package]]
 name = "requests"
 version = "2.31.0"
@@ -2911,6 +3021,20 @@ files = [
     {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
 ]
 
+[[package]]
+name = "splines"
+version = "0.3.1"
+description = "Splines in Euclidean Space and Beyond"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "splines-0.3.1-py3-none-any.whl", hash = "sha256:52945e8547257a545ce9caf3d628ea6f5552c1c560d6dd6857806405e023ff11"},
+    {file = "splines-0.3.1.tar.gz", hash = "sha256:8a5cf09709254b7872e8bbbfa03aad1dcfa19ec7a49575f3de987c3be17e8a7a"},
+]
+
+[package.dependencies]
+NumPy = "*"
+
 [[package]]
 name = "sqlalchemy"
 version = "2.0.25"
@@ -3348,4 +3472,4 @@ rdkit = ["rdkit2ase"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "2753655b19fa1a57b44d39de0081fba40e9e0169457b4e1a1b1ed0b5acb3a33b"
+content-hash = "d38615ec635182a283d01346a7c2935a75d30065671d1a564e0b157d9466df52"
diff --git a/pyproject.toml b/pyproject.toml
index 36b15fc95..9927f32b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,9 @@ flask-caching = "^2.1.0"
 decorator = "^5.1.1"
 celery = "^5.3.6"
 sqlalchemy = "^2.0.25"
+psycopg2-binary = "^2.9.9"
+redis = "^5.0.1"
+splines = "^0.3.1"
 
 
 [tool.poetry.group.dev.dependencies]
diff --git a/zndraw/app.py b/zndraw/app.py
index 4ad49fc9d..ed72248c5 100644
--- a/zndraw/app.py
+++ b/zndraw/app.py
@@ -65,6 +65,29 @@ def setup_cache():
 
 def setup_worker() -> list:
     """Setup the worker."""
+    import os
+    import platform
+
+    my_env = os.environ.copy()
+    if platform.system() == "Darwin" and platform.processor() == "arm":
+        # fix celery worker issue on apple silicon
+        my_env["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
+
+    io_worker = subprocess.Popen(
+        [
+            "celery",
+            "-A",
+            "zndraw.make_celery",
+            "worker",
+            "--loglevel=info",
+            "--concurrency=6",
+            "--hostname=io_worker",
+            "--queues=io",
+            "--prefetch-multiplier=10",
+        ],
+        env=my_env,
+    )
+
     fast_worker = subprocess.Popen(
         [
             "celery",
@@ -72,10 +95,12 @@ def setup_worker() -> list:
             "zndraw.make_celery",
             "worker",
             "--loglevel=info",
-            "--concurrency=16",
+            "--concurrency=6",
             "--hostname=fast_worker",
             "--queues=fast,celery",
-        ]
+            "--prefetch-multiplier=1",
+        ],
+        env=my_env,
     )
 
     slow_worker = subprocess.Popen(
@@ -88,9 +113,10 @@ def setup_worker() -> list:
             "--concurrency=1",
             "--hostname=slow_worker",
             "--queues=slow",
-        ]
+        ],
+        env=my_env,
     )
-    return [fast_worker, slow_worker]
+    return [io_worker, fast_worker, slow_worker]
 
 
 def create_app() -> Flask:
@@ -169,7 +195,10 @@ def run(self, browser=False):
         self._workers = setup_worker()
 
         config = GlobalConfig.load()
-        pathlib.Path(config.database.path).expanduser().unlink(missing_ok=True)
+        try:
+            pathlib.Path(config.database.path).expanduser().unlink(missing_ok=True)
+        except AttributeError:
+            pass  # only for sqlite config
         engine = create_engine(config.database.get_path())
         Base.metadata.create_all(engine)
 
diff --git a/zndraw/base.py b/zndraw/base.py
index fd6a13c94..e2e19bb5a 100644
--- a/zndraw/base.py
+++ b/zndraw/base.py
@@ -1,20 +1,31 @@
-import collections
 import dataclasses
+import logging
 from abc import abstractmethod
+from collections.abc import MutableSequence
 
 import numpy as np
 import socketio
+import splines
+
+log = logging.getLogger(__name__)
 
 
 @dataclasses.dataclass
-class ZnDrawBase(collections.abc.MutableSequence):
+class ZnDrawBase(MutableSequence):
     token: str
     url: str
 
     socket: socketio.Client = dataclasses.field(default_factory=socketio.Client)
 
     def __post_init__(self):
+        self.url = self.url.replace("http", "ws")
+        log.critical(f"Connecting to {self.url}")
+        self.socket.connect(self.url, wait_timeout=1)
+        self.socket.emit("join", str(self.token))
+
+    def reconnect(self):
         self.socket.connect(self.url)
+        self.socket.emit("join", str(self.token))
 
     @abstractmethod
     def log(self, message: str):
@@ -64,3 +75,23 @@ def points(self, value: np.ndarray):
     @abstractmethod
     def segments(self) -> np.ndarray:
         pass
+
+    @property
+    def figure(self):
+        raise NotImplementedError("Gathering figure from webclient not implemented yet")
+
+    @figure.setter
+    def figure(self, fig: str):
+        data = {"figure": fig, "token": self.token}
+        self.socket.emit("analysis:figure", data)
+
+    @property
+    def atoms(self):
+        return self[self.step]
+
+    @staticmethod
+    def calculate_segments(points: np.ndarray) -> np.ndarray:
+        if points.shape[0] <= 1:
+            return points
+        t = np.linspace(0, len(points) - 1, len(points) * 50)
+        return splines.CatmullRom(points).evaluate(t)
diff --git a/zndraw/data.py b/zndraw/data.py
index 038534ebc..e566f008b 100644
--- a/zndraw/data.py
+++ b/zndraw/data.py
@@ -1,6 +1,5 @@
 import dataclasses
 
-import numpy as np
 import znframe
 
 
@@ -16,7 +15,7 @@ class RoomSetData:
         Whether to update the database with the new data.
     """
 
-    points: np.ndarray | None = None
+    points: list[list[float]] | None = None
     bookmarks: dict[int, str] | None = None
     step: int | None = None
     selection: list[int] | None = None
@@ -32,17 +31,29 @@ def to_dict(self) -> dict:
 
 @dataclasses.dataclass
 class RoomGetData:
-    points: bool = False
-    bookmarks: bool = False
-    step: bool = False
-    selection: bool = False
-    length: bool = False
-    segments: bool = False
-    frames: list[int] | None = None
+    points: bool | list[list[float]] = False
+    bookmarks: bool | dict[str, str] = False
+    step: bool | int = False
+    selection: bool | list[int] = False
+    length: bool | int = False
+    segments: bool | list[list[float]] = False
+    frames: list[int] | None | list[dict] = None
 
     def to_dict(self) -> dict:
         return dataclasses.asdict(self)
 
+    @classmethod
+    def get_current_state(cls):
+        return cls(
+            points=True,
+            bookmarks=True,
+            step=True,
+            selection=True,
+            length=True,
+            segments=True,
+            frames=["current"],
+        )
+
 
 @dataclasses.dataclass
 class CeleryTaskData:
@@ -69,7 +80,7 @@ class CeleryTaskData:
 
     target: str
     event: str
-    data: dict | None | str
+    data: dict | None | str | int
     disconnect: bool = False
     timeout: int = 60
     authentication: str = None
diff --git a/zndraw/db/schema.py b/zndraw/db/schema.py
index 4529820e2..11b493790 100644
--- a/zndraw/db/schema.py
+++ b/zndraw/db/schema.py
@@ -25,6 +25,7 @@ class Room(Base):
     frames = relationship("Frame", back_populates="room")
     room_modifiers = relationship("RoomModifier", back_populates="room")
     bookmarks = relationship("Bookmark", back_populates="room")
+    launched_jobs = relationship("QueueItem", back_populates="room")
 
     def __repr__(self):
         return f"<Room(token={self.token}, currentStep={self.currentStep})>"
@@ -144,7 +145,12 @@ class QueueItem(Base):
     __tablename__ = "queue_items"
 
     id = Column(Integer, primary_key=True, autoincrement=True)
+    job_name = Column(String)
     job_id = Column(String)
+    datetime = Column(String)
+    status = Column(String, default="queued")
 
     queue_name = Column(String, ForeignKey("queues.name"))
     queue = relationship("Queue", back_populates="jobs")
+    room_token = Column(String, ForeignKey("rooms.token"))
+    room = relationship("Room", back_populates="launched_jobs")
diff --git a/zndraw/modify/__init__.py b/zndraw/modify/__init__.py
index aaea5cc71..03d5579c0 100644
--- a/zndraw/modify/__init__.py
+++ b/zndraw/modify/__init__.py
@@ -65,7 +65,8 @@ def run(self, vis: "ZnDraw", **kwargs) -> None:
         points = vis.points
         atom_ids = vis.selection
         atoms = vis.atoms
-        assert len(points) == 2
+        if len(points) != 2:
+            raise ValueError("Please draw exactly 2 points to rotate around.")
 
         angle = self.angle if self.direction == "left" else -self.angle
         angle = angle / self.steps
@@ -76,11 +77,10 @@ def run(self, vis: "ZnDraw", **kwargs) -> None:
         for _ in range(self.steps):
             # rotate the selected atoms around the vector
             atoms_selected.rotate(angle, vector, center=points[0])
-            # merge the selected and remaining atoms
-            atoms = atoms_selected + atoms_remaining
+            # update the positions of the selected atoms
+            atoms.positions[atom_ids] = atoms_selected.positions
             vis.append(atoms)
             time.sleep(self.sleep)
-        vis.selection = []
 
 
 class Delete(UpdateScene):
@@ -114,7 +114,8 @@ def run(self, vis: "ZnDraw", **kwargs) -> None:
             del vis[vis.step + 1 :]
 
         atoms = vis.atoms
-        atoms_selected, atoms_remaining = self.apply_selection(vis.selection, atoms)
+        atoms_ids = vis.selection
+        atoms_selected, atoms_remaining = self.apply_selection(atoms_ids, atoms)
         if self.steps > len(vis.segments):
             raise ValueError(
                 "The number of steps must be less than the number of segments. You can add more points to increase the number of segments."
@@ -129,9 +130,8 @@ def run(self, vis: "ZnDraw", **kwargs) -> None:
             # move the selected atoms along the vector
             atoms_selected.positions += vector
             # merge the selected and remaining atoms
-            atoms = atoms_selected + atoms_remaining
+            atoms.positions[atoms_ids] = atoms_selected.positions
             vis.append(atoms)
-        vis.selection = []
 
 
 class Duplicate(UpdateScene):
diff --git a/zndraw/select/__init__.py b/zndraw/select/__init__.py
index db7e440ca..b03c6faf5 100644
--- a/zndraw/select/__init__.py
+++ b/zndraw/select/__init__.py
@@ -6,6 +6,8 @@
 import numpy as np
 from pydantic import BaseModel, Field
 
+from zndraw import ZnDraw
+
 try:
     from zndraw.select import mda  # noqa: F401
 except ImportError:
@@ -130,6 +132,15 @@ def run(self, vis) -> None:
         vis.selection = list(set(total_ids))
 
 
+class UpdateSelection(SelectionBase):
+    """Reload Selection."""
+
+    discriminator: t.Literal["UpdateSelection"] = Field("UpdateSelection")
+
+    def run(self, vis: ZnDraw) -> None:
+        vis.selection = vis.selection
+
+
 def get_selection_class(methods):
     class Selection(SelectionBase):
         method: methods = Field(
diff --git a/zndraw/server/events.py b/zndraw/server/events.py
index 7800aa9fd..8726e33fb 100644
--- a/zndraw/server/events.py
+++ b/zndraw/server/events.py
@@ -21,7 +21,6 @@
     CeleryTaskData,
     DeleteAtomsData,
     FrameData,
-    JoinData,
     MessageData,
     ModifierRegisterData,
     RoomGetData,
@@ -31,7 +30,6 @@
     SchemaData,
     SubscribedUserData,
 )
-from .utils import get_queue_position
 
 log = logging.getLogger(__name__)
 
@@ -54,6 +52,7 @@ def _webclients_room(data: dict) -> str:
 def connect():
     try:
         token = str(session["token"])
+        log.critical(f"-------------->>> connecting (webclient) {request.sid}")
         URL = f"http://127.0.0.1:{current_app.config['PORT']}"
         # if you connect through Python, you don't have a token
         read_file_chain = chain(
@@ -148,6 +147,7 @@ def connect():
     except KeyError:
         # clients that connect directly via socketio do not call "join" to
         # register their token
+        log.critical(f"-------------->>> connecting (pyclient) {request.sid}")
         session["token"] = None
 
 
@@ -175,99 +175,25 @@ def celery_task_call(msg: CeleryTaskData):
 
 @io.on("disconnect")
 def disconnect():
-    log.critical(f"-------------->>> disconnecting {request.sid}")
     token = str(session["token"])
 
-    with Session() as ses:
-        room = ses.query(db_schema.Room).filter_by(token=token).first()
-        client = ses.query(db_schema.Client).filter_by(sid=request.sid).first()
-        if client is not None:
-            ses.delete(client)
-            if client.host:
-                new_host = ses.query(db_schema.Client).filter_by(room=room).first()
-                if new_host is not None:
-                    new_host.host = True
-            ses.commit()
-
-    with Session() as ses:
-        room = ses.query(db_schema.Room).filter_by(token=token).first()
-        clients = ses.query(db_schema.Client).filter_by(room=room).all()
-        connected_users = [{"name": client.name} for client in clients]
-
-    emit(
-        "connectedUsers",
-        list(reversed(connected_users)),
-        to=_webclients_room({"token": token}),
-    )
-
-    # ROOM_HOSTS = cache.get("ROOM_HOSTS")
-    # if token in ROOM_HOSTS:
-    #     if request.sid in ROOM_HOSTS[token]:
-    #         ROOM_HOSTS[token].remove(request.sid)
-    #     if len(ROOM_HOSTS[token]) == 0:
-    #         del ROOM_HOSTS[token]
-    #     cache.set("ROOM_HOSTS", ROOM_HOSTS)
-
-    # pyclients only
-    # check if the SID is in MODIFIER_HOSTS or ROOM_MODIFIER_HOSTS
-    with Session() as ses:
-        global_modifier_client = (
-            ses.query(db_schema.GlobalModifierClient).filter_by(sid=request.sid).all()
-        )
-        for gmc in global_modifier_client:
-            ses.delete(gmc)
-
-        room_modifier_client = (
-            ses.query(db_schema.RoomModifierClient).filter_by(sid=request.sid).all()
-        )
-        for rmc in room_modifier_client:
-            ses.delete(rmc)
-        ses.commit()
-
-    # MODIFIER_HOSTS = cache.get("MODIFIER_HOSTS")
-    # for name, sids in MODIFIER_HOSTS.items():
-    #     while request.sid in sids:
-    #         MODIFIER_HOSTS[name].remove(request.sid)
-    # cache.set("MODIFIER_HOSTS", MODIFIER_HOSTS)
+    url = request.url_root
+    if current_app.config["upgrade_insecure_requests"] and not "127.0.0.1" in url:
+        url = url.replace("http://", "https://")
+    url = url.replace("http", "ws")
 
-    # ROOM_MODIFIER_HOSTS = cache.get("ROOM_MODIFIER_HOSTS")
-    # if token in ROOM_MODIFIER_HOSTS:
-    #     for name, sids in ROOM_MODIFIER_HOSTS[token].items():
-    #         while request.sid in sids:
-    #             ROOM_MODIFIER_HOSTS[token][name].remove(request.sid)
-
-    # # !!!!!!!!!!!!!!
-    # # TODO: remove schema if no more hosts are connected and send to webclients
-
-    # cache.set("ROOM_MODIFIER_HOSTS", ROOM_MODIFIER_HOSTS)
+    tasks.on_disconnect.delay(token=token, sid=request.sid, url=url)
 
 
 @io.on("join")
-@typecast
-def join(data: JoinData):
+def join(token: str):
     """
     Arguments:
         data: {"token": str, "uuid": str}
     """
-    # only used by pyclients that only connect via socket (no HTML)
-    # session["authenticated"] = data.auth_token == app.config["AUTH_TOKEN"]
-    # log.debug(f"Client {request.sid} is {session['authenticated'] = }")
-    # PYCLIENTS = cache.get("pyclients")
-    # if data.uuid in PYCLIENTS:
-    #     log.critical(
-    #         f"UUID {data.uuid} is already registered in {app.config['pyclients']}."
-    #     )
-    #     emit("message:log", f"UUID {data.uuid} is already registered", to=request.sid)
-    # PYCLIENTS[data.uuid] = request.sid
-    # cache.set("pyclients", PYCLIENTS)
-    session["token"] = data.token
-    join_room(f"{data.token}")
-    join_room(f"pyclients_{data.token}")
-    # join_room(f"pyclients_{data.token}")
-    # PER_TOKEN_DATA = cache.get("PER-TOKEN-DATA")
-    # if data.token not in PER_TOKEN_DATA:
-    #     PER_TOKEN_DATA[data.token] = {}
-    # cache.set("PER-TOKEN-DATA", PER_TOKEN_DATA)
+    session["token"] = token
+    join_room(f"{token}")
+    join_room(f"pyclients_{token}")
 
 
 @io.on("analysis:figure")
@@ -503,11 +429,11 @@ def debug(data: dict):
     emit("debug", data, include_self=False, to=_webclients_room(data))
 
 
-@io.on("connectedUsers:subscribe:step")
+@io.on("connectedUsers:subscribe:camera")
 @typecast
-def connected_users_subscribe_step(data: SubscribedUserData):
+def connected_users_subscribe_camera(data: SubscribedUserData):
     """
-    Subscribe to step updates for connected users.
+    Subscribe to camera updates for connected users.
 
     data: {user: str}
     """
@@ -522,39 +448,36 @@ def connected_users_subscribe_step(data: SubscribedUserData):
         )
         if (
             controller_client.sid == request.sid
-            or controller_client.step_controller == current_client
+            or controller_client.camera_controller == current_client
         ):
-            current_client.step_controller = None
+            current_client.camera_controller = None
         else:
-            current_client.step_controller = controller_client
+            current_client.camera_controller = controller_client
         ses.commit()
 
 
-@io.on("connectedUsers:subscribe:camera")
-@typecast
-def connected_users_subscribe_camera(data: SubscribedUserData):
-    """
-    Subscribe to camera updates for connected users.
-
-    data: {user: str}
-    """
+@io.on("camera:update")
+def camera_update(data: dict):
     token = str(session["token"])
+    # TODO: store this in the session
     with Session() as ses:
         room = ses.query(db_schema.Room).filter_by(token=token).first()
         if room is None:
             raise ValueError("No room found for token.")
         current_client = ses.query(db_schema.Client).filter_by(sid=request.sid).first()
-        controller_client = (
-            ses.query(db_schema.Client).filter_by(name=data.user).first()
+        camera_subscribers = (
+            ses.query(db_schema.Client)
+            .filter_by(camera_controller=current_client)
+            .all()
         )
-        if (
-            controller_client.sid == request.sid
-            or controller_client.camera_controller == current_client
-        ):
-            current_client.camera_controller = None
-        else:
-            current_client.camera_controller = controller_client
-        ses.commit()
+        camera_subscribers = [client.sid for client in camera_subscribers]
+
+    emit(
+        "camera:update",
+        data,
+        include_self=False,
+        to=camera_subscribers,
+    )
 
 
 @io.on("scene:update")
@@ -648,9 +571,8 @@ def modifier_run(data: dict):
         "modifier:run:enqueue", to=f"webclients_{session['token']}", include_self=False
     )
     # split into separate streams based on the modifier name
-    tasks.run_modifier(
-        f"http://127.0.0.1:{current_app.config['PORT']}", session["token"], data
-    )
+    url = f"http://127.0.0.1:{current_app.config['PORT']}"
+    tasks.run_modifier(url, session["token"], data)
 
 
 def _register_global_modifier(data):
@@ -729,25 +651,7 @@ def modifier_register(data: ModifierRegisterData):
 @io.on("modifier:available")
 def modifier_available(available: bool):
     """Update the modifier availability."""
-    with Session() as ses:
-        global_modifier_client = (
-            ses.query(db_schema.GlobalModifierClient).filter_by(sid=request.sid).first()
-        )
-        room_modifier_client = (
-            ses.query(db_schema.RoomModifierClient).filter_by(sid=request.sid).first()
-        )
-        if global_modifier_client is not None:
-            global_modifier_client.available = available
-        elif room_modifier_client is not None:
-            room_modifier_client.available = available
-        ses.commit()
-
-
-@io.on("modifier:queue:update")
-def modifier_queue_update(data: dict):
-    """Update the modifier queue."""
-    queue_position = get_queue_position(data["queue_name"], data["job_id"])
-    emit("modifier:queue:update", queue_position, to=f"webclients_{session['token']}")
+    tasks.activate_modifier.delay(request.sid, available)
 
 
 @io.on("ping")
@@ -757,7 +661,10 @@ def ping() -> str:
 
 @io.on("room:get")
 def room_get(data: RoomGetData):
-    tasks.handle_room_get.delay(data, session["token"], request.url_root, request.sid)
+    url = request.url_root
+    if current_app.config["upgrade_insecure_requests"] and not "127.0.0.1" in url:
+        url = url.replace("http://", "https://")
+    tasks.handle_room_get.delay(data, session["token"], url, request.sid)
 
 
 @io.on("room:set")
@@ -769,8 +676,32 @@ def room_set(data: RoomSetData):
         include_self=False,
         to=f"webclients_{session['token']}",
     )
-    print(f"room_set: {data}")
+    url = request.url_root
+    if current_app.config["upgrade_insecure_requests"] and not "127.0.0.1" in url:
+        url = url.replace("http://", "https://")
+
     if data.update_database:
-        print("room_set: update_database")
         # TODO: we need to differentiate, if the data comes from a pyclient or a webclient
-        tasks.handle_room_set.delay(data.to_dict(), session["token"], request.url_root)
+        # TODO: for fast updates, e.g. points, step during play this is not fast enough
+        tasks.handle_room_set.delay(data.to_dict(), session["token"], url, request.sid)
+
+
+@io.on("step:update")
+def step_update(step: int):
+    timestamp = datetime.datetime.utcnow().isoformat()
+    session["step-update"] = timestamp
+
+    data = RoomSetData(step=step)
+
+    emit(
+        "room:set",
+        data.to_dict(),
+        include_self=False,
+        to=f"webclients_{session['token']}",
+    )
+    io.sleep(1)
+    if session["step-update"] == timestamp:
+        url = request.url_root
+        if current_app.config["upgrade_insecure_requests"] and not "127.0.0.1" in url:
+            url = url.replace("http://", "https://")
+        tasks.handle_room_set.delay(data.to_dict(), session["token"], url, request.sid)
diff --git a/zndraw/server/tasks.py b/zndraw/server/tasks.py
index e7d962208..1f4653239 100644
--- a/zndraw/server/tasks.py
+++ b/zndraw/server/tasks.py
@@ -6,7 +6,7 @@
 import ase.io
 import znframe
 import znh5md
-from celery import shared_task
+from celery import chain, shared_task
 from socketio import Client
 
 from zndraw.analyse import get_analysis_class
@@ -22,7 +22,7 @@
 from ..app import cache
 from ..data import CeleryTaskData, RoomGetData, RoomSetData
 from ..utils import typecast
-from .utils import insert_into_queue, remove_job_from_queue
+from .utils import get_queue_position, insert_into_queue, update_job_status
 
 log = logging.getLogger(__name__)
 
@@ -219,7 +219,9 @@ def modifier_schema(url: str, token: str):
 
 @shared_task
 def scene_trash(url: str, token: str):
-    vis = ZnDraw(url=url, token=token)
+    from zndraw.zndraw_worker import ZnDrawWorker
+
+    vis = ZnDrawWorker(token=token, url=url)
     del vis[vis.step + 1 :]
     if len(vis.selection) == 0:
         vis.append(ase.Atoms())
@@ -232,8 +234,9 @@ def scene_trash(url: str, token: str):
         vis.append(atoms)
     vis.selection = []
     vis.points = []
+    vis.step = len(vis) - 1
 
-    vis.socket.sleep(10)
+    vis.socket.sleep(1)
     vis.socket.disconnect()
 
 
@@ -282,14 +285,20 @@ def read_file(url: str, target: str, token: str):
         if len(atoms_list) > 0:
             vis.extend(atoms_list)
 
+        vis.step = len(vis) - 1
     else:
         vis.upload(target)
 
+    vis.socket.sleep(1)
+    vis.socket.disconnect()
+
 
 @shared_task
 def run_selection(url: str, token: str, data: dict):
+    from zndraw.zndraw_worker import ZnDrawWorker
+
+    vis = ZnDrawWorker(token=str(token), url=url)
     print(datetime.datetime.now().isoformat())
-    vis = ZnDraw(url=url, token=token)
 
     config = GlobalConfig.load()
     cls = get_selection_class(config.get_selection_methods())
@@ -302,13 +311,16 @@ def run_selection(url: str, token: str, data: dict):
 
     print(datetime.datetime.now().isoformat())
 
-    vis.socket.sleep(10)
+    vis.socket.sleep(1)
     vis.socket.disconnect()
 
 
 @shared_task
 def run_analysis(url: str, token: str, data: dict):
+    from zndraw.zndraw_worker import ZnDrawWorker
+
     vis = ZnDraw(url=url, token=token)
+    vis = ZnDrawWorker(token=str(token), url=url)
 
     msg = CeleryTaskData(
         target=f"webclients_{vis.token}", event="analysis:run:running", data=None
@@ -335,35 +347,34 @@ def run_analysis(url: str, token: str, data: dict):
     vis.socket.emit("celery:task:emit", asdict(msg))
 
 
-def get_vis_obj(
-    url: str,
-    token: str,
-    queue_name: str,
-    request_id: str,
-):
-    """
-    Return a celery task that runs the modifier. The decorator constructs complex python objects for the celery task
-    """
-    vis = ZnDraw(url=url, token=token)
-    insert_into_queue(queue_name=queue_name, job_id=request_id)
-
-    def on_finished():
-        # TODO: disconnect the modifier, release the worker
-        remove_job_from_queue(queue_name, request_id)
-        vis.socket.disconnect()
+@shared_task
+def update_queue_positions(queue_name, url):
+    from zndraw.zndraw_worker import ZnDrawWorker
 
-    vis.socket.on("modifier:run:finished", on_finished)
-    return vis
+    if queue_name == "slow":
+        queue_positions = get_queue_position(queue_name)
+        worker = ZnDrawWorker(token="None", url=url)
+        for position, room_token in queue_positions:
+            msg = CeleryTaskData(
+                target=f"webclients_{room_token}",
+                event="modifier:queue:update",
+                data=position,
+                disconnect=False,
+            )
+            worker.socket.emit("celery:task:emit", msg.to_dict())
+        worker.socket.sleep(0.5)
+        worker.socket.disconnect()
+    else:
+        return None
 
 
 @shared_task(bind=True)
-def _run_global_modifier(self, url: str, token: str, data):
-    vis = get_vis_obj(
-        url,
-        token,
-        queue_name="slow",
-        request_id=self.request.id,
-    )
+def _run_global_modifier(self, url: str, token: str, data, queue_job_id: str):
+    from zndraw.zndraw_worker import ZnDrawWorker
+
+    vis = ZnDrawWorker(token=str(token), url=url)
+    vis.socket.on("modifier:run:finished", lambda: vis.socket.disconnect())
+
     name = data["method"]["discriminator"]
     while True:
         with Session() as ses:
@@ -381,6 +392,7 @@ def _run_global_modifier(self, url: str, token: str, data):
             )
 
         if assigned_hosts == 0:
+            update_job_status(job_id=queue_job_id, status="failed:no_host")
             msg = CeleryTaskData(
                 target=f"webclients_{vis.token}",
                 event="modifier:run:finished",
@@ -397,22 +409,20 @@ def _run_global_modifier(self, url: str, token: str, data):
             return
 
         if host is None:
-            vis.socket.emit(
-                "modifier:queue:update",
-                {"queue_name": "slow", "job_id": self.request.id},
-            )
             vis.socket.sleep(1)
             log.critical("No modifier available")
             continue
 
         # run the modifier
+        to_get = RoomGetData.get_current_state()
+        cache = vis.get_properties(**to_get.to_dict())
         msg = CeleryTaskData(
             target=host.sid,
             event="modifier:run",
-            data={"params": data, "token": vis.token},
+            data={"params": data, "token": vis.token, "cache": cache},
         )
         vis.socket.emit("celery:task:emit", asdict(msg))
-
+        update_job_status(job_id=queue_job_id, status="running")
         # add additional 5 seconds for communication overhead
         for _ in range(int(host.timeout + 5)):
             if vis.socket.connected:
@@ -421,6 +431,10 @@ def _run_global_modifier(self, url: str, token: str, data):
                 vis.socket.sleep(1)
             else:
                 log.critical("Modifier finished")
+                status = "finished"
+                log.critical("SETTING ")
+                update_job_status(job_id=queue_job_id, status=status)
+                update_queue_positions(queue_name="slow", url=url)
                 return
 
         print("modifier timed out")
@@ -438,18 +452,18 @@ def _run_global_modifier(self, url: str, token: str, data):
             disconnect=True,
         )
         vis.socket.emit("celery:task:emit", asdict(msg))
+        update_job_status(job_id=queue_job_id, status="failed:timeout")
+        update_queue_positions(queue_name="slow", url=url)
         return
 
 
 @shared_task(bind=True)
-def _run_room_modifier(self, url: str, token: str, data):
+def _run_room_modifier(self, url: str, token: str, data, queue_job_id: str):
+    from zndraw.zndraw_worker import ZnDrawWorker
+
+    vis = ZnDrawWorker(token=str(token), url=url)
+
     name = data["method"]["discriminator"]
-    vis = get_vis_obj(
-        url,
-        token,
-        queue_name="custom",
-        request_id=self.request.id,
-    )
     with Session() as ses:
         room = ses.query(db_schema.Room).filter_by(token=vis.token).first()
         room_modifier = (
@@ -487,13 +501,10 @@ def _run_room_modifier(self, url: str, token: str, data):
 
 
 @shared_task(bind=True)
-def _run_default_modifier(self, url: str, token: str, data):
-    vis = get_vis_obj(
-        url,
-        token,
-        queue_name="fast",
-        request_id=self.request.id,
-    )
+def _run_default_modifier(self, url: str, token: str, data: dict, queue_job_id: str):
+    from zndraw.zndraw_worker import ZnDrawWorker
+
+    vis = ZnDrawWorker(token=str(token), url=url)
     config = GlobalConfig.load()
     cls = get_modify_class(config.get_modify_methods())
     modifier = cls(**data)
@@ -506,8 +517,12 @@ def _run_default_modifier(self, url: str, token: str, data):
 
     try:
         modifier.run(vis)
+        status = "finished"
     except Exception as err:
-        vis.log(f"Error: {err}")
+        # exception type
+        exception_type = type(err).__name__
+        vis.log(f"Error: {exception_type}:{err}")
+        status = f"failed: {exception_type}:{err}"
 
     msg = CeleryTaskData(
         target=f"webclients_{vis.token}",
@@ -517,23 +532,48 @@ def _run_default_modifier(self, url: str, token: str, data):
     )
 
     vis.socket.emit("celery:task:emit", asdict(msg))
+    update_job_status(job_id=queue_job_id, status=status)
+    vis.socket.disconnect()
 
 
-def run_modifier(url: str, token: str, data: dict):
-    name = data["method"]["discriminator"]
+def route_modifier_to_queue(name: str, token: str) -> str:
     with Session() as ses:
         room = ses.query(db_schema.Room).filter_by(token=token).first()
         room_modifiers = ses.query(db_schema.RoomModifier).filter_by(room=room).all()
         modifiers = ses.query(db_schema.GlobalModifier).all()
         custom_global_modifiers = [modifier.name for modifier in modifiers]
-
+        custom_room_modifiers = [modifier.name for modifier in room_modifiers]
     if name in custom_global_modifiers:
-        _run_global_modifier.delay(url, token, data)
-    elif name in room_modifiers:
-        _run_room_modifier.delay(url, token, data)
+        queue_name = "slow"
+    elif name in custom_room_modifiers:
+        queue_name = "custom"
+    else:
+        queue_name = "default"
+    return queue_name
+
+
+def run_modifier(url: str, token: str, data: dict):
+    name = data["method"]["discriminator"]
+    queue_name = route_modifier_to_queue(name, token)
+    queue_job_id = insert_into_queue(
+        queue_name=queue_name, job_name=name, room_token=token
+    )
+    if queue_name == "slow":
+        task_chain = chain(
+            update_queue_positions.si(queue_name, url),
+            _run_global_modifier.si(url, token, data, queue_job_id),
+        )
+    elif queue_name == "custom":
+        task_chain = chain(
+            update_queue_positions.si(queue_name, url),
+            _run_room_modifier.si(url, token, data, queue_job_id),
+        )
     else:
-        _run_default_modifier.delay(url, token, data)
-    return
+        task_chain = chain(
+            update_queue_positions.si(queue_name, url),
+            _run_default_modifier.si(url, token, data, queue_job_id),
+        )
+    task_chain.delay()
 
 
 @shared_task
@@ -541,53 +581,110 @@ def run_modifier(url: str, token: str, data: dict):
 def handle_room_get(data: RoomGetData, token: str, url: str, target: str):
     from zndraw.zndraw_worker import ZnDrawWorker
 
-    print("I AM RUNNING THE CELERY TASK")
-    print(50 * "-")
     worker = ZnDrawWorker(token=token, url=url)
-    #  TODO: I think this should use `RoomGetData`
-    #  and we do unions bool | datatype there
-    answer = RoomSetData()
-    if data.step:
-        answer.step = worker.step
-    if data.points:
-        answer.points = worker.points.tolist()
-    if data.bookmarks:
-        answer.bookmarks = worker.bookmarks
-    if data.selection:
-        answer.selection = worker.selection
-    # if data.length:
-    #     answer.length = len(worker)
-    # if data.segments:
-    #     answer.segments = worker.segments.tolist()
-    if data.frames:
-        answer.frames = [
-            znframe.Frame.from_atoms(x).to_dict(built_in_types=False)
-            for x in worker[data.frames]
-        ]
-    msg = CeleryTaskData(
-        target=target,
-        event="room:get",
-        data=answer.to_dict(),
-    )
+    answer = worker.get_properties(**data.to_dict())
+    msg = CeleryTaskData(target=target, event="room:get", data=answer, disconnect=True)
     worker.socket.emit("celery:task:emit", msg.to_dict())
 
 
 @shared_task
 @typecast
-def handle_room_set(data: RoomSetData, token: str, url: str):
+def handle_room_set(data: RoomSetData, token: str, url: str, source: str):
     from zndraw.zndraw_worker import ZnDrawWorker
 
     worker = ZnDrawWorker(token=token, url=url, emit=False)
-    if data.step:
+    if data.frames:
+        # must be first, before step
+        # frames should either all be None or all be not None
+        if any(frame is None for frame in data.frames.values()) and any(
+            frame is not None for frame in data.frames.values()
+        ):
+            raise ValueError("All frames must be None or not None")
+        is_removing = all(frame is None for frame in data.frames.values())
+        indices = list(data.frames.keys())
+        if is_removing:
+            log.critical(f"Removing frames {indices}")
+            del worker[indices]
+        else:
+            frames = [znframe.Frame.from_dict(frame) for frame in data.frames.values()]
+            worker[indices] = frames
+    if data.step is not None:
         worker.step = data.step
-    if data.points:
+    if data.points is not None:
         worker.points = data.points
-    if data.bookmarks:
+    if data.bookmarks is not None:
         worker.bookmarks = data.bookmarks
-    if data.selection:
+    if data.selection is not None:
         worker.selection = data.selection
-    if data.frames:
-        for idx, frame in data.frames.items():
-            worker[idx] = znframe.Frame.from_dict(frame).to_atoms()
 
+    msg = CeleryTaskData(
+        target=source,
+        event="room:set:finished",
+        data=None,
+        disconnect=True,
+    )
+    worker.socket.emit("celery:task:emit", msg.to_dict())
     # worker.commit() and a mode, that waits for all updates before commiting
+
+
+@shared_task
+def activate_modifier(sid: str, available: bool):
+    log.critical(f"modifier:available {sid} {available}")
+    with Session() as ses:
+        global_modifier_client = (
+            ses.query(db_schema.GlobalModifierClient).filter_by(sid=sid).first()
+        )
+        room_modifier_client = (
+            ses.query(db_schema.RoomModifierClient).filter_by(sid=sid).first()
+        )
+        if global_modifier_client is not None:
+            global_modifier_client.available = available
+        elif room_modifier_client is not None:
+            room_modifier_client.available = available
+        ses.commit()
+
+
+@shared_task
+def on_disconnect(token: str, sid: str, url: str):
+    # from zndraw.zndraw_worker import ZnDrawWorker
+
+    # worker = ZnDrawWorker(token=token, url=url)
+    with Session() as ses:
+        room = ses.query(db_schema.Room).filter_by(token=token).first()
+        client = ses.query(db_schema.Client).filter_by(sid=sid).first()
+        if client is not None:
+            ses.delete(client)
+            if client.host:
+                new_host = ses.query(db_schema.Client).filter_by(room=room).first()
+                if new_host is not None:
+                    new_host.host = True
+            ses.commit()
+
+    with Session() as ses:
+        global_modifier_client = (
+            ses.query(db_schema.GlobalModifierClient).filter_by(sid=sid).all()
+        )
+        for gmc in global_modifier_client:
+            ses.delete(gmc)
+
+        room_modifier_client = (
+            ses.query(db_schema.RoomModifierClient).filter_by(sid=sid).all()
+        )
+        for rmc in room_modifier_client:
+            ses.delete(rmc)
+        ses.commit()
+
+    with Session() as ses:
+        room = ses.query(db_schema.Room).filter_by(token=token).first()
+        clients = ses.query(db_schema.Client).filter_by(room=room).all()
+        connected_users = [{"name": client.name} for client in clients]
+
+    # msg = CeleryTaskData(
+    #     target=f"webclients_{token}",
+    #     event="connectedUsers",
+    #     data=list(reversed(connected_users)),
+    #     disconnect=True,
+    # )
+    # # TODO this can not work, because it triggers itself
+
+    # worker.socket.emit("celery:task:emit", msg.to_dict())
diff --git a/zndraw/server/utils.py b/zndraw/server/utils.py
index 13132cebc..835be104e 100644
--- a/zndraw/server/utils.py
+++ b/zndraw/server/utils.py
@@ -1,3 +1,6 @@
+import uuid
+from datetime import datetime
+
 from sqlalchemy.orm import Session
 
 from ..db import Session as ses
@@ -13,32 +16,40 @@ def get_queue(session: Session, queue_name: str) -> Queue:
     return queue
 
 
-def insert_into_queue(queue_name: str, job_id: str) -> None:
+def get_room_by_token(session: Session, token: str):
+    return session.query(Room).filter_by(token=token).one()
+
+
+def insert_into_queue(queue_name: str, job_name: str, room_token: str) -> str:
+    job_id = uuid.uuid4().hex
     with ses() as session:
         queue = get_queue(session, queue_name)
-        job = QueueItem(job_id=job_id)
+        room = get_room_by_token(session, room_token)
+        job = QueueItem(job_name=job_name, job_id=job_id, datetime=datetime.utcnow())
         queue.jobs.append(job)
+        room.launched_jobs.append(job)
         session.commit()
+    return job_id
 
 
-def remove_job_from_queue(queue_name: str, job_id: str) -> None:
+def update_job_status(job_id: str | int, status: str) -> None:
     with ses() as session:
-        queue = get_queue(session, queue_name)
-        for job in queue.jobs:
-            if job.job_id == job_id:
-                queue.jobs.remove(job)
-
+        session.query(QueueItem).filter_by(job_id=job_id).update(dict(status=status))
         session.commit()
 
 
-def get_queue_position(queue_name: str, job_id: str) -> int:
+def get_queue_position(queue_name: str) -> list[tuple[int, str]]:
+    results = []
     with ses() as session:
-        queue = get_queue(session, queue_name)
-        for i, job in enumerate(queue.jobs):
-            if job.job_id == job_id:
-                return i
-        return -1
-
-
-def get_room_by_token(session: Session, token: str):
-    return session.query(Room).filter_by(token=token).one()
+        queueing_jobs = (
+            session.query(QueueItem)
+            .filter_by(status="queued", queue_name=queue_name)
+            .all()
+        )
+        rooms = [job.room_token for job in queueing_jobs]
+        for room in rooms:
+            largest_idx = max(
+                [i + 1 for i, job in enumerate(queueing_jobs) if job.room_token == room]
+            )
+            results.append((largest_idx, room))
+    return results
diff --git a/zndraw/settings.py b/zndraw/settings.py
index e263e479f..d7521c691 100644
--- a/zndraw/settings.py
+++ b/zndraw/settings.py
@@ -40,6 +40,7 @@
     "zndraw.select.Random",
     "zndraw.select.IdenticalSpecies",
     "zndraw.select.Neighbour",
+    "zndraw.select.UpdateSelection",
 ]
 
 
@@ -59,16 +60,43 @@ def to_dict(self):
         )
 
 
-class CeleryConfig(pydantic.BaseModel):
-    data_folder: str = "~/.zincware/zndraw/celery/out"
-    data_folder_processed: str = "~/.zincware/zndraw/celery/processed"
+class CeleryBaseConfig(pydantic.BaseModel):
+    name: t.Literal["CeleryBaseConfig"] = "CeleryBaseConfig"
     broker: str = "filesystem://"
     result_backend: str = "cache"
     cache_backend: str = "memory"
     task_ignore_result: bool = True
-    task_routes: dict = {
-        "*._run_global_modifier": {"queue": "slow"},
-    }
+
+    @property
+    def task_routes(self):
+        return {
+            "*._run_global_modifier": {"queue": "slow"},
+            "*.update_atoms": {"queue": "io"},
+            "*.get_selection_schema": {"queue": "io"},
+            "*.scene_schema": {"queue": "io"},
+            "*.geometries_schema": {"queue": "io"},
+            "*.analysis_schema": {"queue": "io"},
+            "*.handle_room_get": {"queue": "io"},
+            "*.handle_room_set": {"queue": "io"},
+            "*.activate_modifier": {"queue": "io"},
+            "*on_disconnect": {"queue": "io"},
+        }
+
+    def to_dict(self):
+        return dict(
+            broker_url=self.broker,
+            result_backend=self.result_backend,
+            cache_backend=self.cache_backend,
+            task_ignore_result=self.task_ignore_result,
+            task_routes=self.task_routes,
+        )
+
+
+class CeleryFileSystemConfig(CeleryBaseConfig):
+    name: t.Literal["CeleryFileSystemConfig"] = "CeleryFileSystemConfig"
+    data_folder: str = "~/.zincware/zndraw/celery/out"
+    data_folder_processed: str = "~/.zincware/zndraw/celery/processed"
+    task_ignore_result: bool = True
 
     def to_dict(self):
         return dict(
@@ -85,21 +113,44 @@ def to_dict(self):
         )
 
 
-class DatabaseConfig(pydantic.BaseModel):
+class DatabaseSQLiteConfig(pydantic.BaseModel):
+    name: t.Literal["DatabaseSQLiteConfig"] = "DatabaseSQLiteConfig"
+
     path: str = "~/.zincware/zndraw/database.sqlite"
 
     def get_path(self) -> str:
         path = pathlib.Path(self.path).expanduser()
         path.parent.mkdir(parents=True, exist_ok=True)
         return f"sqlite:///{path}"
-        # return f"sqlite:///{path}?check_same_thread=False"
 
 
-class GlobalConfig(pydantic.BaseModel):
-    cache: CacheSettings = CacheSettings()
-    celery: CeleryConfig = CeleryConfig()
-    database: DatabaseConfig = DatabaseConfig()
+class DatabasePostgresConfig(pydantic.BaseModel):
+    name: t.Literal["DatabasePostgresConfig"] = "DatabasePostgresConfig"
+
+    host: str = "localhost"
+    port: int = 5432
+    database: str = "zndraw"
+    user: str = "zndraw"
+    password: str = "zndraw"
+
+    def get_path(self) -> str:
+        return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
+
 
+class GlobalConfig(pydantic.BaseModel):
+    cache: CacheSettings = pydantic.Field(default_factory=CacheSettings)
+    celery: t.Union[CeleryBaseConfig, CeleryFileSystemConfig] = pydantic.Field(
+        default_factory=CeleryFileSystemConfig, discriminator="name"
+    )
+    database: t.Union[DatabasePostgresConfig, DatabaseSQLiteConfig] = pydantic.Field(
+        default_factory=DatabaseSQLiteConfig, discriminator="name"
+    )
+
+    # Socket settings
+    read_batch_size: int = 1
+    max_socket_data_size: int | float = 1024 * 8
+
+    # Webclient Interface
     analysis_functions: t.List[str] = _ANALYSIS_FUNCTIONS
     modify_functions: t.List[str] = _MODIFY_FUNCTIONS
     bonds_functions: t.List[str] = _BONDS_FUNCTIONS
diff --git a/zndraw/static/World/World.js b/zndraw/static/World/World.js
index 125d19f17..132a499a2 100644
--- a/zndraw/static/World/World.js
+++ b/zndraw/static/World/World.js
@@ -147,6 +147,8 @@ class Player {
   }
 
   play() {
+    // TODO: do not update the database upon play but only on stop,
+    //  because otherwise, the db will not keep up
     if (this.playing) {
       this.go_forward();
       setTimeout(() => this.play(), 1000 / this.fps);
@@ -315,6 +317,7 @@ class World {
     step = parseInt(step);
     loop.setStep(step);
     const slider = document.getElementById("frameProgress");
+    const currentStep = parseInt(slider.ariaValueNow);
     slider.ariaValueNow = step;
     // update slider.style with with the percentage of the slider.value
     const percentage = (slider.ariaValueNow / slider.ariaValueMax) * 100;
@@ -323,7 +326,7 @@ class World {
     document.getElementById("info").innerHTML =
       `${slider.ariaValueNow} / ${slider.ariaValueMax}`;
     if (emit) {
-      this.socket.emit("room:set", { step: step, update_database: true });
+      this.socket.emit("step:update", step);
     }
   }
 
diff --git a/zndraw/static/World/systems/select.js b/zndraw/static/World/systems/select.js
index 7e27f922b..61f441893 100644
--- a/zndraw/static/World/systems/select.js
+++ b/zndraw/static/World/systems/select.js
@@ -79,8 +79,7 @@ class Selection {
       }
     });
 
-    window.addEventListener("pointerdown", this.onPointerDown.bind(this));
-    window.addEventListener("dblclick", this.onDoubleClick.bind(this));
+    window.addEventListener("click", this.onClick.bind(this));
   }
 
   getIntersections(object) {
@@ -100,12 +99,6 @@ class Selection {
 
     const particleIntersects = this.getIntersections(particlesGroup);
     if (particleIntersects.length > 0) {
-      const instanceId = particleIntersects[0].instanceId;
-      particlesGroup.click(
-        instanceId,
-        this.shift_pressed,
-        particleIntersects[0].object,
-      );
       const params = document.getElementById(
         "selection-json-editor",
       ).parameters;
@@ -165,7 +158,12 @@ class Selection {
     return false;
   }
 
-  onPointerDown(event) {
+  onClick(event) {
+    // detect double click
+    if (event.detail === 2) {
+      this.onDoubleClick(event);
+      return;
+    }
     const particlesGroup = this.scene.getObjectByName("particlesGroup");
     const anchorPoints = this.scene.getObjectByName("AnchorPoints");
     const canvas3D = this.scene.getObjectByName("canvas3D");
diff --git a/zndraw/static/pycom/Cache.js b/zndraw/static/pycom/Cache.js
index 6879ccfa6..f4f3e4306 100644
--- a/zndraw/static/pycom/Cache.js
+++ b/zndraw/static/pycom/Cache.js
@@ -110,8 +110,9 @@ class Cache {
 
   setFrames(data) {
     const slider = document.getElementById("frameProgress");
+    const indicesToDelete = [];
     for (const [index, atoms] of Object.entries(data)) {
-      if (atoms === undefined) {
+      if (atoms === null) {
         delete this._cache[index];
       } else {
         this._cache[index] = new Atoms({
@@ -127,6 +128,18 @@ class Cache {
         slider.ariaValueMax = Object.keys(this._cache).length - 1;
       }
     }
+    // reset the keys of the cache to go from 0 to n
+    const newCache = {};
+    let i = 0;
+    for (const key in this._cache) {
+      console.log(key);
+      newCache[i] = this._cache[key];
+      i++;
+    }
+    this._cache = newCache;
+    // no longer required?
+    // this.world.setStep(this.world.getStep());
+    slider.ariaValueMax = Object.keys(this._cache).length - 1;
   }
 }
 
diff --git a/zndraw/utils.py b/zndraw/utils.py
index f87e3077f..ea227e2d9 100644
--- a/zndraw/utils.py
+++ b/zndraw/utils.py
@@ -12,6 +12,7 @@
 import ase
 import datamodel_code_generator
 import numpy as np
+import znframe
 from decorator import decorator
 
 SHARED = {"atoms": None}
@@ -23,6 +24,15 @@ def split_list_into_chunks(lst, n):
         yield lst[i : i + n]
 
 
+def estimate_max_batch_size_for_socket(frames: list[znframe.Frame]):
+    from .settings import GlobalConfig
+
+    max_size = GlobalConfig().max_socket_data_size
+    sizes = [sys.getsizeof(frame) for frame in frames]
+    largest_frame = max(sizes)
+    return int(max_size / largest_frame * 0.9)
+
+
 def rgb2hex(value):
     r, g, b = np.array(value * 255, dtype=int)
     return "#%02x%02x%02x" % (r, g, b)
@@ -150,3 +160,18 @@ def ensure_path(path: str):
     p = pathlib.Path(path).expanduser()
     p.mkdir(parents=True, exist_ok=True)
     return p.as_posix()
+
+
+def wrap_and_check_index(index: int | slice | list[int], length: int) -> list[int]:
+    is_slice = isinstance(index, slice)
+    if is_slice:
+        index = list(range(*index.indices(length)))
+    index = [index] if isinstance(index, int) else index
+    index = [i if i >= 0 else length + i for i in index]
+    # check if index is out of range
+    for i in index:
+        if i >= length:
+            raise IndexError(f"Index {i} out of range for length {length}")
+        if i < 0:
+            raise IndexError(f"Index {i-length} out of range for length {length}")
+    return index
diff --git a/zndraw/zndraw.py b/zndraw/zndraw.py
index 75a9ac6be..fb11b29cc 100644
--- a/zndraw/zndraw.py
+++ b/zndraw/zndraw.py
@@ -1,22 +1,27 @@
 import dataclasses
 import logging
-import threading
 import typing as t
+from threading import Lock
 
 import ase
 import ase.io
 import numpy as np
 import socketio
+from socketio import exceptions as socketio_exceptions
 from znframe.frame import Frame
 
-from zndraw.data import CeleryTaskData, FrameData, ModifierRegisterData
+from zndraw.data import CeleryTaskData, ModifierRegisterData
 from zndraw.modify import UpdateScene, get_modify_class
 from zndraw.settings import GlobalConfig
-from zndraw.utils import (
-    ZnDrawLoggingHandler,
-)
 
-from .utils import split_list_into_chunks
+from .base import ZnDrawBase
+from .data import RoomGetData, RoomSetData
+from .utils import (
+    estimate_max_batch_size_for_socket,
+    split_list_into_chunks,
+    wrap_and_check_index,
+)
+from .zndraw_frozen import FrozenZnDraw
 
 log = logging.getLogger(__name__)
 
@@ -36,7 +41,7 @@ class Config:
 
 
 @dataclasses.dataclass
-class ZnDrawBase:  # collections.abc.MutableSequence
+class ZnDraw(ZnDrawBase):
     """
 
     Attributes
@@ -52,23 +57,26 @@ class ZnDrawBase:  # collections.abc.MutableSequence
         not just the current session.
     """
 
-    url: str
-    token: str = None
     display_new: bool = True
-    auth_token: str = None
+    auth_token: str | None = None
     config: Config = dataclasses.field(default_factory=Config)
     _modifiers: dict = dataclasses.field(default_factory=dict)
 
-    _target_sid: str = None
+    _target_sid: str | None = None
+
+    _lock: Lock = dataclasses.field(default_factory=Lock)
+    _data = None
 
     def __post_init__(self):
-        self.socket = socketio.Client()
         if isinstance(self.config, dict):
             self.config = Config(**self.config)
         self.socket.on("disconnect", self._on_disconnect)
         self.socket.on("connect", self._on_connect)
         self.socket.on("modifier:run", self._pre_modifier_run)
         self.socket.on("message:log", lambda data: print(data))
+        self.socket.on("room:get", lambda data: setattr(self, "_data", data))
+        self.socket.on("room:set:finished", lambda *args: self._lock.release())
+        super().__post_init__()
 
     def _on_disconnect(self):
         log.critical(f"Disconnected from server: {self._modifiers}")
@@ -95,53 +103,66 @@ def _connect(self):
 
     def reconnect(self) -> None:
         """Reconnect to the server."""
-        log.critical("Reconnecting to server")
-        self.socket.disconnect()
-        self._connect()
-        self._on_connect()
-        self.socket.sleep(0.1)
-        log.critical("Reconnected to server")
+        super().reconnect()
+
+        for k, v in self._modifiers.items():
+            log.critical(f"Re-registering modifier {k}")
+            msg = ModifierRegisterData(
+                schema=v["cls"].model_json_schema(),
+                name=k,
+                default=v["default"],
+                timeout=v["run_kwargs"]["timeout"],
+            )
+            self.socket.emit(
+                "modifier:register",
+                dataclasses.asdict(msg),
+            )
+
+            self.socket.emit(
+                "modifier:available",
+                True,
+            )
+
+    def get_data(self, **data: dict) -> RoomGetData:
+        data = RoomGetData(**data)
+        with self._lock:
+            self._data = None
+            self.socket.emit("room:get", data.to_dict())
+            while self._data is None:
+                self.socket.sleep(seconds=1)
+                # generous timeout
+            # self._data.pop("update_database", None) # TODO: this should not happen
+            data = RoomGetData(**self._data)
+            self._data = None
+            return data
+
+    def set_data(self, **data: dict) -> None:
+        data = RoomSetData(**data)
+        self._lock.acquire(blocking=True)  # might need to have a while loop here
+        self.socket.emit("room:set", data.to_dict())
 
     def __len__(self) -> int:
-        return int(self.socket.call("atoms:length", timeout=self.config.call_timeout))
+        return self.get_data(length=True).length
 
     def __setitem__(self, index, value):
         assert isinstance(index, int), "Index must be an integer"
         if isinstance(value, ase.Atoms):
             value = Frame.from_atoms(value)
-        self.socket.emit(
-            "atoms:upload",
-            [
-                dataclasses.asdict(
-                    FrameData(
-                        index=index,
-                        data=value.to_dict(built_in_types=False),
-                        update=True,
-                        update_database=True,
-                    )
-                )
-            ],
+        self.set_data(
+            frames={index: value.to_dict(built_in_types=False)},
+            step=index,
+            update_database=True,
         )
 
-    def __delitem__(self, index):
+    def __delitem__(self, index: int | slice | list[int]):
         if (
             isinstance(index, int)
             or isinstance(index, slice)
             or isinstance(index, list)
         ):
             length = len(self)
-            is_slice = isinstance(index, slice)
-            if is_slice:
-                index = range(*index.indices(length))
-
-            index = [index] if isinstance(index, int) else index
-            index = [i if i >= 0 else length + i for i in index]
-
-            data = {"index": index, "token": self.token}
-
-            self.socket.emit("atoms:delete", data)
-            if not is_slice and (index[0] >= length or index[-1] >= length):
-                raise IndexError("Index out of range")
+            index = wrap_and_check_index(index, length)
+            self.set_data(frames={i: None for i in index}, update_database=True)
         else:
             raise TypeError("Index must be an integer, slice or list[int]")
 
@@ -149,14 +170,14 @@ def insert(self, index, value: t.Union[ase.Atoms, Frame]):
         """Insert atoms before index"""
         if isinstance(value, ase.Atoms):
             value = Frame.from_atoms(value)
-
-        data = list(self)
-        data.insert(index, value)
-        for idx, val in enumerate(data):
-            self[idx] = val
-
-        # TODO: why is this not working at the moment?
-        # self.socket.emit("atoms:insert", {index: value.to_dict()})
+        log.warning(
+            "Currently `insert` is very taxing on the server, use with caution!"
+        )
+        index = wrap_and_check_index(index, len(self))[0]
+        data_after = self[index:]
+        self[index] = value
+        del self[index + 1 :]
+        self.extend(data_after)
 
     def append(self, value: t.Union[ase.Atoms, Frame]) -> None:
         """Append atoms to the end of the list"""
@@ -174,50 +195,30 @@ def extend(
         else:
             if isinstance(values[0], ase.Atoms):
                 values = [Frame.from_atoms(val) for val in values]
+            batch_size = estimate_max_batch_size_for_socket(values)
             indices = list(range(size, size + len(values)))
-            data = [
-                dataclasses.asdict(
-                    FrameData(
-                        index=i,
-                        data=val.to_dict(built_in_types=False),
-                        update=True,
-                        update_database=True,
-                    )
-                )
+            all_data = [
+                (i, val.to_dict(built_in_types=False))
                 for i, val in zip(indices, values)
             ]
-            batch_size = GlobalConfig.load().read_batch_size
-            for chunk in split_list_into_chunks(data, batch_size):
-                self.socket.emit(
-                    "atoms:upload",
-                    chunk,
-                )
+
+            for chunk in split_list_into_chunks(all_data, batch_size):
+                batch = {tup[0]: tup[1] for tup in chunk}
+                self.set_data(frames=batch, update_database=True)
 
     def __getitem__(self, index) -> t.Union[ase.Atoms, list[ase.Atoms]]:
         length = len(self)
-        is_scalar = isinstance(index, int)
-        is_sclice = isinstance(index, slice)
-        if is_sclice:
-            index = range(*index.indices(length))
-
-        index = [index] if isinstance(index, int) else index
-        index = [i if i >= 0 else length + i for i in index]
-
-        downloaded_data = self.socket.call(
-            "atoms:download", index, timeout=self.config.call_timeout
-        )
+        index = wrap_and_check_index(index, length)
+        data = self.get_data(frames=index).frames
 
         atoms_list = []
-
-        for val in downloaded_data.values():
+        for idx, val in zip(index, data):
             if val is None:
-                raise IndexError("Index out of range")
+                raise IndexError(f"Index {idx} out of range")
             atoms_list.append(Frame.from_dict(val).to_atoms())
 
-        data = atoms_list[0] if is_scalar else atoms_list
-        if data == [] and not is_sclice:
-            raise IndexError("Index out of range")
-        return data
+        return_data = atoms_list[0] if len(index) == 1 else atoms_list
+        return return_data
 
     def log(self, message: str) -> None:
         """Log a message to the console"""
@@ -233,11 +234,11 @@ def log(self, message: str) -> None:
     @property
     def atoms(self) -> ase.Atoms:
         """Return the atoms at the current step."""
-        return self[self.step]
+        return self[self.step]  # type: ignore
 
     @property
     def points(self) -> np.ndarray:
-        data = self.socket.call("points:get", timeout=self.config.call_timeout)
+        data = self.get_data(points=True).points
         return np.array(data)
 
     @points.setter
@@ -249,37 +250,29 @@ def points(self, value: t.Union[np.ndarray, list]) -> None:
                 assert len(value[0]) == 3
             except (TypeError, AssertionError):
                 raise ValueError("Points must be a list of 3D coordinates")
-        self.socket.emit("points:set", value)
+        self.set_data(points=value, update_database=True)
 
     @property
     def segments(self) -> np.ndarray:
-        data = self.socket.call("scene:segments", timeout=self.config.call_timeout)
-        return np.array(data)
+        return self.calculate_segments(self.points)
 
     @property
     def step(self) -> int:
-        step = int(
-            self.socket.call(
-                "scene:step",
-                timeout=self.config.call_timeout,
-            )
-        )
+        step = self.get_data(step=True).step
         return step
 
     @step.setter
     def step(self, index):
-        if index > len(self) - 1:
-            raise IndexError(f"Index {index} out of range for length {len(self)}")
-        data = {"index": index, "token": self.token}
-        self.socket.emit("scene:set", data)
+        index = wrap_and_check_index(index, len(self))[0]
+        self.set_data(step=index, update_database=True)
 
     @property
     def selection(self) -> list[int]:
-        return self.socket.call("selection:get", timeout=self.config.call_timeout)
+        return self.get_data(selection=True).selection
 
     @selection.setter
     def selection(self, value: list[int]):
-        self.socket.emit("selection:set", value)
+        self.set_data(selection=value, update_database=True)
 
     def play(self):
         self.socket.emit("scene:play")
@@ -298,21 +291,25 @@ def figure(self, fig: str):
 
     @property
     def bookmarks(self) -> dict:
-        return self.socket.call("bookmarks:get", timeout=self.config.call_timeout)
+        return self.get_data(bookmarks=True).bookmarks
 
     @bookmarks.setter
     def bookmarks(self, value: dict):
-        self.socket.emit("bookmarks:set", value)
+        self.set_data(bookmarks=value, update_database=True)
 
     def _pre_modifier_run(self, data) -> None:
         self.socket.emit("modifier:available", False)
-        vis = type(self)(self.url, data["token"])
         msg = CeleryTaskData(
-            target=f"webclients_{vis.token}", event="modifier:run:running", data=None
+            target=f"webclients_{data['token']}",
+            event="modifier:run:running",
+            data=None,
         )
 
-        vis.socket.emit("celery:task:emit", dataclasses.asdict(msg))
+        self.socket.emit("celery:task:emit", dataclasses.asdict(msg))
         try:
+            vis = FrozenZnDraw(
+                url=self.url, token=data["token"], cached_data=data["cache"]
+            )
             config = GlobalConfig.load()
             cls = get_modify_class(
                 config.get_modify_methods(
@@ -320,105 +317,35 @@ def _pre_modifier_run(self, data) -> None:
                 )
             )
             modifier = cls(**data["params"])
-            modifier.run(
-                vis,
-                **self._modifiers[modifier.method.__class__.__name__]["run_kwargs"],
+            try:
+                modifier.run(
+                    vis,
+                    **self._modifiers[modifier.method.__class__.__name__]["run_kwargs"],
+                )
+            except Exception as err:
+                vis.log(f"Modifier failed with error: {repr(err)}")
+
+            vis.socket.sleep(1)
+            vis.socket.disconnect()
+        except socketio_exceptions.ConnectionError as err:
+            msg = CeleryTaskData(
+                target=f"webclients_{data['token']}",
+                event="message:log",
+                data="Could not establish connection " + str(err),
+                disconnect=False,
             )
-        except Exception as err:
-            vis.log(f"Modifier failed with error: {repr(err)}")
+            self.socket.emit("celery:task:emit", dataclasses.asdict(msg))
+
         msg = CeleryTaskData(
-            target=f"{vis.token}",
+            target=f"{data['token']}",
             event="modifier:run:finished",
             data=None,
-            disconnect=True,
+            disconnect=False,
         )
-        vis.socket.emit("celery:task:emit", dataclasses.asdict(msg))
+        self.socket.emit("celery:task:emit", dataclasses.asdict(msg))
         self.socket.emit("modifier:available", True)
         print("Modifier finished!!!!!!!!")
 
-
-@dataclasses.dataclass
-class ZnDraw(ZnDrawBase):
-    """ZnDraw client."""
-
-    url: str = None
-
-    jupyter: bool = False
-
-    def __post_init__(self):
-        super().__post_init__()
-        if self.url is None:
-            from zndraw.utils import get_port
-            from zndraw.view import view
-
-            port = get_port()
-            self._view_thread = threading.Thread(
-                target=view,
-                kwargs={
-                    "filename": None,
-                    "port": port,
-                    "open_browser": False,
-                    "webview": False,
-                    "fullscreen": False,
-                    "start": 0,
-                    "stop": None,
-                    "step": 1,
-                    "compute_bonds": True,
-                },
-            )
-            self._view_thread.start()
-            self.url = f"http://127.0.0.1:{port}"
-
-        self._connect()
-
-    def close(self):
-        import time
-        import urllib.request
-
-        self.socket.disconnect()
-
-        time.sleep(1)
-
-        # open self.url/exit
-        try:
-            urllib.request.urlopen(f"{self.url}/exit")
-        except Exception:
-            pass
-        if hasattr(self, "_view_thread"):
-            log.debug("Waiting for ZnDraw client to close")
-            # self._view_thread.terminate()
-            self._view_thread.join()
-            # raise ValueError("ZnDraw client closed")
-
-    def _repr_html_(self):
-        from IPython.display import IFrame
-
-        return IFrame(src=self.url, width="100%", height="600px")._repr_html_()
-
-    def get_logging_handler(self) -> ZnDrawLoggingHandler:
-        return ZnDrawLoggingHandler(self)
-
-    def reconnect(self) -> None:
-        super().reconnect()
-
-        for k, v in self._modifiers.items():
-            log.critical(f"Re-registering modifier {k}")
-            msg = ModifierRegisterData(
-                schema=v["cls"].model_json_schema(),
-                name=k,
-                default=v["default"],
-                timeout=v["run_kwargs"]["timeout"],
-            )
-            self.socket.emit(
-                "modifier:register",
-                dataclasses.asdict(msg),
-            )
-
-            self.socket.emit(
-                "modifier:available",
-                True,
-            )
-
     def register_modifier(
         self,
         cls: UpdateScene,
diff --git a/zndraw/zndraw_frozen.py b/zndraw/zndraw_frozen.py
new file mode 100644
index 000000000..6c4eea360
--- /dev/null
+++ b/zndraw/zndraw_frozen.py
@@ -0,0 +1,165 @@
+import logging
+import typing as t
+
+import ase
+import numpy as np
+import socketio
+from znframe import Frame as ZnFrame
+
+from .base import ZnDrawBase
+from .data import RoomSetData
+from .utils import (
+    estimate_max_batch_size_for_socket,
+    split_list_into_chunks,
+    wrap_and_check_index,
+)
+
+log = logging.getLogger(__name__)
+
+
+class FrozenZnDraw(ZnDrawBase):
+    # TODO: take in _original instead and change the RoomSetData to accept token instead. Will remove a lot of the boilerplate for pushing data
+    def __init__(self, token, url, cached_data: dict):
+        self.socket = socketio.Client()
+        self.socket.connect(url, wait_timeout=5)
+        self.socket.emit("join", str(token))
+        self.url = url
+        self.token = token
+        self._cached_data = self.cache_from_dict(cached_data)
+
+    def __len__(self):
+        return self._cached_data["length"]
+
+    def __setitem__(self, index, value):
+        assert isinstance(index, int), "Index must be an integer"
+        if isinstance(value, ase.Atoms):
+            value = ZnFrame.from_atoms(value)
+        self.set_data(
+            frames={index: value.to_dict(built_in_types=False)},
+            step=index,
+            update_database=True,
+        )
+
+    def __getitem__(self, *args, **kwargs):
+        raise NotImplementedError(
+            "This method is not implemented on the frozen object. Please use the zndraw.ZnDraw object instead."
+        )
+
+    def insert(self, *args, **kwargs):
+        raise NotImplementedError("Don't use inserts. Use append or extend instead.")
+
+    def __delitem__(self, index: int | slice | list[int]):
+        if (
+            isinstance(index, int)
+            or isinstance(index, slice)
+            or isinstance(index, list)
+        ):
+            length = len(self)
+            index = wrap_and_check_index(index, length)
+            self.set_data(frames={i: None for i in index}, update_database=True)
+            self._cached_data["length"] -= len(index)
+        else:
+            raise TypeError("Index must be an integer, slice or list[int]")
+
+    def append(self, value: t.Union[ase.Atoms, ZnFrame]) -> None:
+        """Append atoms to the end of the list"""
+        if isinstance(value, ase.Atoms):
+            value = ZnFrame.from_atoms(value)
+        self[len(self)] = value
+        self._cached_data["length"] += 1
+
+    def extend(
+        self, values: t.Union[ase.Atoms, ZnFrame, list[ase.Atoms], list[ZnFrame]]
+    ) -> None:
+        """Extend the list by appending all the items in the given list"""
+        size = len(self)
+        if not isinstance(values, list):
+            self.append(values)
+        else:
+            if isinstance(values[0], ase.Atoms):
+                values = [ZnFrame.from_atoms(val) for val in values]
+            batch_size = estimate_max_batch_size_for_socket(values)
+            log.critical(f"Batch size: {batch_size}")
+            indices = list(range(size, size + len(values)))
+            all_data = [
+                (i, val.to_dict(built_in_types=False))
+                for i, val in zip(indices, values)
+            ]
+            chunks = list(split_list_into_chunks(all_data, batch_size))
+            for i, chunk in enumerate(chunks):
+                self._cached_data["length"] += len(chunk)
+                batch = {tup[0]: tup[1] for tup in chunk}
+                if i == len(chunks) - 1:
+                    # Only send the step if it's the last batch, otherwise it will be set to the last index of the batch
+                    self.set_data(
+                        frames=batch, update_database=True, step=len(self) - 1
+                    )
+                else:
+                    self.set_data(frames=batch, update_database=True)
+
+    def log(self, message: str) -> None:
+        """Log a message to the console"""
+        print(message)
+        self.socket.emit(
+            "message:log",
+            {
+                "message": message,
+                "token": self.token,
+            },
+        )
+
+    def set_data(self, **data: dict) -> None:
+        data = RoomSetData(**data)
+        log.critical("Trying to set data")
+        self.socket.emit("room:set", data.to_dict())
+
+    @property
+    def atoms(self):
+        return self._cached_data["atoms"]
+
+    @property
+    def points(self):
+        return self._cached_data["points"]
+
+    @points.setter
+    def points(self, value):
+        self.set_data(points=value, update_database=True)
+
+    @property
+    def segments(self):
+        return self.calculate_segments(self._cached_data["points"])
+
+    @property
+    def step(self):
+        return self._cached_data["step"]
+
+    @step.setter
+    def step(self, value):
+        self.set_data(step=value, update_database=True)
+
+    @property
+    def selection(self):
+        return self._cached_data["selection"]
+
+    @selection.setter
+    def selection(self, value):
+        self.set_data(selection=value, update_database=True)
+
+    @property
+    def bookmarks(self):
+        return self._cached_data["bookmarks"]
+
+    @bookmarks.setter
+    def bookmarks(self, value):
+        self.set_data(bookmarks=value, update_database=True)
+
+    @staticmethod
+    def cache_from_dict(data: dict):
+        return dict(
+            atoms=ZnFrame.from_dict(data["frames"][0]).to_atoms(),
+            points=np.array(data["points"]),
+            step=data["step"],
+            selection=data["selection"],
+            bookmarks=data["bookmarks"],
+            length=data["length"],
+        )
diff --git a/zndraw/zndraw_worker.py b/zndraw/zndraw_worker.py
index 7bdf1273b..4e69d05da 100644
--- a/zndraw/zndraw_worker.py
+++ b/zndraw/zndraw_worker.py
@@ -1,17 +1,22 @@
 import dataclasses
+import logging
 from typing import List, Union
 
 import ase
 import numpy as np
+from sqlalchemy import func as sql_func
 from znframe.frame import Frame as ZnFrame
 
 from zndraw.data import CeleryTaskData, RoomSetData
 from zndraw.settings import GlobalConfig
 
+log = logging.getLogger(__name__)
+
 from .base import ZnDrawBase
 from .db import Session
 from .db.schema import Bookmark, Frame, Room
 from .server.utils import get_room_by_token
+from .utils import wrap_and_check_index
 
 
 def _any_to_list(
@@ -40,8 +45,16 @@ class ZnDrawWorker(ZnDrawBase):
 
     def __len__(self) -> int:
         with Session() as session:
-            room = session.query(Room).get(self.token)
-            return len(room.frames)
+            return self._get_len(session, self.token)
+
+    @staticmethod
+    def _get_len(session, token) -> int:
+        max_idx = (
+            session.query(sql_func.max(Frame.index))
+            .filter(Frame.room_token == token)
+            .scalar()
+        )
+        return max_idx + 1 if max_idx is not None else 0
 
     def __setitem__(
         self,
@@ -49,14 +62,19 @@ def __setitem__(
         value: ZnFrame | ase.Atoms | list[ase.Atoms] | list[ZnFrame],
     ):
         value = _any_to_list(value)
+        if isinstance(index, str):
+            index = int(index)
         if isinstance(index, int):
             index = [index]
         if isinstance(index, slice):
             index = list(range(len(self)))[index]
         if len(index) != len(value):
             # TODO: support all the ways python lists can be slice set
-            raise ValueError("Length of index and value must match")
-
+            raise ValueError(
+                f"Length of index ({len(index)}) and value ({len(value)}) must match"
+            )
+        log.critical(82 * "-")
+        log.critical(f"Index: {index}")
         # we emit first, because sending the data takes longer, but emit is faster
         if self.emit:
             self.socket.emit(
@@ -66,12 +84,12 @@ def __setitem__(
                         idx: frame.to_dict(built_in_types=False)
                         for idx, frame in zip(index, value)
                     },
-                    step=len(self) - 1 + len(index),
+                    step=index[-1],
                 ).to_dict(),
             )
-
         with Session() as session:
             room = session.query(Room).get(self.token)
+            room.currentStep = index[-1]
             for _index, _value in zip(index, value):
                 frame = session.query(Frame).filter_by(index=_index, room=room).first()
                 if frame is None:
@@ -160,9 +178,7 @@ def points(self, value: Union[np.ndarray, list]):
 
     @property
     def segments(self) -> np.ndarray:
-        with Session() as session:
-            room = get_room_by_token(session, self.token)
-            return np.array(room.segments)
+        return self.calculate_segments(self.points)
 
     @property
     def step(self) -> int:
@@ -172,8 +188,9 @@ def step(self) -> int:
 
     @step.setter
     def step(self, idx: int):
-        if idx < 0 or idx >= len(self):
-            raise IndexError(f"Index {idx} out of range")
+        length = len(self)
+        if idx < 0 or idx >= length:
+            raise IndexError(f"Index {idx} out of range for {length} frames")
         with Session() as session:
             room = get_room_by_token(session, self.token)
             room.currentStep = idx
@@ -223,8 +240,47 @@ def insert(self, index: int, atoms: ase.Atoms | ZnFrame):
         del self[index + 1 :]
         self.extend(data_after)
 
+    def get_properties(self, **kwargs):
+        with Session() as session:
+            room = get_room_by_token(session, self.token)
+
+            answer = {}
+            for key, collect in kwargs.items():
+                if collect:
+                    if key == "frames":
+                        indices = [
+                            x if not x == "current" else room.currentStep
+                            for x in kwargs["frames"]
+                        ]
+                        indices = wrap_and_check_index(
+                            indices, self._get_len(session, self.token)
+                        )
+                        log.critical(f"Indices: {indices}")
+                        collected_frames = (
+                            session.query(Frame)
+                            .filter(Frame.index.in_(indices), Frame.room == room)
+                            .all()
+                        )
+                        answer["frames"] = [
+                            ZnFrame.from_dict(frame.data).to_dict(built_in_types=False)
+                            for frame in collected_frames
+                        ]
+                    if key == "length":
+                        answer["length"] = len(room.frames)
+                    elif key == "points":
+                        answer["points"] = room.points
+                    elif key == "step":
+                        answer["step"] = room.currentStep
+                    elif key == "selection":
+                        answer["selection"] = room.selection
+                    elif key == "bookmarks":
+                        answer["bookmarks"] = {
+                            bm.step: bm.text for bm in room.bookmarks
+                        }
+        return answer
+
     def log(self, message: str):
-        self.socket.emit("message:log", message)
+        self.socket.emit("message:log", {"message": message, "token": self.token})
 
     def upload(self, target: str):
         """Emit all frames to the target (webclient)."""


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 1f11c5be3..dbb1d2a83 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -58,7 +58,7 @@ def server():
         server_proc.join()
 
 
-@pytest.fixture()
+@pytest.fixture(scope="session")
 def sio_server():
     port = get_port()
 
@@ -79,4 +79,5 @@ def ping(sid):
 
     t = threading.Thread(target=run_server, args=(port,), daemon=True)
     t.start()
-    return f"http://localhost:{port}"
+    time.sleep(1)
+    yield f"http://localhost:{port}"
diff --git a/tests/test_db.py b/tests/test_db.py
index 22fd4f191..78cd77f2c 100644
--- a/tests/test_db.py
+++ b/tests/test_db.py
@@ -7,7 +7,7 @@
 from sqlalchemy import create_engine
 from sqlalchemy.orm import sessionmaker
 
-from zndraw.data import RoomSetData
+from zndraw.data import RoomGetData, RoomSetData
 from zndraw.db import schema
 from zndraw.db.schema import Base
 from zndraw.utils import typecast
@@ -89,6 +89,27 @@ def test_zndraw_worker_get(room_session, sio_server):
         # TODO: test slicing
 
 
+def test_zndraw_worker_get_multiple(room_session, sio_server):
+    with mock.patch("zndraw.zndraw_worker.Session", room_session):
+        worker = ZnDrawWorker(token="test_token", url=sio_server)
+        request = RoomGetData(points=True, bookmarks=True, selection=True, step=True)
+        answer = worker.get_properties(**request.to_dict())
+        npt.assert_array_equal(answer["points"], [[0, 0, 0], [1, 1, 1]])
+        assert answer["bookmarks"] == {1: "bm-1", 2: "bm-2"}
+        assert answer["selection"] == [1, 2]
+        assert answer["step"] == 5
+
+        request_idx = [0, 1, 5]
+        request = RoomGetData(
+            points=True, bookmarks=True, selection=True, step=True, frames=request_idx
+        )
+        answer = worker.get_properties(**request.to_dict())
+        assert len(answer["frames"]) == len(request_idx)
+        for i, frame in enumerate(answer["frames"]):
+            atoms = znframe.Frame.from_dict(frame).to_atoms()
+            assert atoms == s22[request_idx[i]]
+
+
 def test_zndraw_worker_set_atoms(room_session, sio_server):
     with mock.patch("zndraw.zndraw_worker.Session", room_session):
         worker = ZnDrawWorker(token="test_token", url=sio_server)
diff --git a/tests/test_socket.py b/tests/test_socket.py
index dcadd1c9e..f827b21ca 100644
--- a/tests/test_socket.py
+++ b/tests/test_socket.py
@@ -64,4 +64,4 @@ def on_answer(data):
     worker.log("test")
     while answer is None:
         worker.socket.sleep(0.1)
-    assert answer == "test"
+    assert answer == {"message": "test", "token": "test_token"}
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 29a085f20..f83b13909 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,7 +1,7 @@
 import pytest
 
 from zndraw.data import RoomGetData
-from zndraw.utils import typecast_kwargs
+from zndraw.utils import typecast_kwargs, wrap_and_check_index
 
 
 def test_typecast_kwargs():
@@ -23,3 +23,30 @@ def func(text: str, **data: RoomGetData):
 
     with pytest.raises(TypeError):
         data = func(text="Hello World", points=True, bookmarks=True)
+
+
+@pytest.mark.parametrize(
+    "inp, expected",
+    [
+        ((0, 10), [0]),
+        ((-1, 10), [9]),
+        (([0, 1, 2], 10), [0, 1, 2]),
+        ((slice(0, 10), 10), list(range(0, 10))),
+        ((slice(0, 10, 2), 10), list(range(0, 10, 2))),
+    ],
+)
+def test_wrap_and_check_index(inp, expected):
+    assert wrap_and_check_index(*inp) == expected
+
+
+@pytest.mark.parametrize(
+    "inp",
+    [
+        ((10, 10)),
+        ((-11, 10)),
+        (([0, 1, 2], 2)),
+    ],
+)
+def test_wrap_and_check_index_raises_exceptions(inp):
+    with pytest.raises(IndexError):
+        wrap_and_check_index(*inp)