Skip to content

Vaynerov Technologies

Wir entwickeln nicht einfach — wir zaubern jede Codezeile und jeden Pixel.

Alle ArtikelBaugeschichten

Ein Museum, in dem noch andere sind

Version eins war ein Museum für eine Person. Version zwei ist dasselbe Gebäude, durch das nun andere laufen: vier Zeichen, um hineinzukommen, bis zu acht Besucher, Stimmen, die leiser werden, wenn jemand weitergeht – und kein einziges Byte davon auf einem Server.

Edward AmirainGründer, Vaynerov Technologies
Veröffentlicht 13 Min. Lesezeit
Auf dieser Seite
  1. Ein Raum ist ein Kanal, kein Datensatz
  2. Ein Relay ohne Autorität
  3. Acht Menschen in Wolle und Leinen
  4. Stimmen, die verklingen, wenn man weggeht
  5. Besuchen, ohne beschrieben zu werden
  6. Die Naht, die Version eins offen ließ
  7. Das Gebäude selbst wurde besser
  8. Was leise ausgeliefert wurde

Vor sechs Wochen habe ich ein Museum für alles, was eine URL hat beschrieben – den abgesicherten Fetch, die Parse-Kaskade, einen Grundriss, der sich nie verschiebt. Es funktionierte, und es war ein Gebäude für genau eine Person. Man konnte einen Link einfügen, ein Ding aufhängen, durch die eigenen Säle laufen – und dann blieb mit dem Gebauten nur noch eines zu tun: es jemandem zu beschreiben.

Version zwei ist deshalb dasselbe Museum mit anderen Leuten darin. Sie öffnen einen Raum, lesen vier Zeichen vor, und bis zu sieben weitere Besucher betreten Ihr Gebäude – als Figuren, die man quer durch eine Galerie sieht, mit Stimmen, die leiser werden, während jemand in den nächsten Raum weiterzieht. Das Gebäude selbst wurde in der Zeit besser: drei Galeriearten statt einer, Skulptur auf Podesten, ein Himmel, der sich mit dem Tag ändert, und ein geführter Rundgang, der endlich ein echter Kamera-Controller ist.

Nichts davon wird irgendwo gespeichert. Diese Beschränkung hat mehr Designarbeit erledigt als jedes Feature, deshalb kommt sie zuerst.

8
Besucher in einem Raum
0
serverseitig gespeicherte Bytes
246
Oberflächen-Strings ×6 Sprachen
303
Unit-Tests

Die Betriebsform von Reliquary v2. Die zweite Zahl ist die, mit der schon v1 ausgeliefert wurde; sie zu halten, sobald mehrere Leute im Haus sind, ist der Sinn des meisten, was folgt.

Ein Raum ist ein Kanal, kein Datensatz

Der naheliegende Weg, gemeinsames Besuchen zu bauen, ist eine Raumtabelle: eine Zeile pro Raum, eine Anwesenheitsliste, Verlauf für den Chat. Es ist zugleich der Punkt, an dem Ihr Museum verlässt Ihren Browser nie still zu …außer den Teilen, die wir jetzt aufbewahren wird. Also gibt es nirgends einen Raumdatensatz. Ein Raum ist ein Kanal-Topic – vt:reliquary:v1:<CODE> – auf einem Broadcast-Relay in Echtzeit, und der Browser des Gastgebers ist der Raum. Er lebt, solange der Presence-Key des Gastgebers auf dem Kanal sitzt; schließt der Gastgeber den Tab, ist es vorbei.

Keinen Datensatz zu haben, hat eine Folge, die ich nicht vorhergesehen hatte: es gibt keine Möglichkeit zu fragen, ob ein Raum existiert. Ein Topic, auf dem niemand ist, abonniert sich genauso bereitwillig wie ein belebtes. Also beantworten Anwesenheit und eine Frist die Frage – ein beitretender Gast wartet sechs Sekunden auf das Auftauchen eines Gastgeber-Keys, und dieser Timeout ist die Definition von „diesen Raum gibt es nicht“. Bevor ein Gastgeber einen Code belegt, lässt ihn dieselbe Abwesenheit 900 ms auf dem Topic sitzen, um zu sehen, ob dort schon jemand wohnt.

Der Code besteht aus vier Zeichen und wird als REL-7K2M angezeigt. Das Alphabet hat dreißig Symbole: kein 0 oder O, kein 1, I oder L, kein U – die ersten beiden Paare sind die klassischen Verwechslungen beim Vorlesen, und das U fehlt, damit keine Ziehung etwas ergibt, das ein Besucher lieber nicht laut sagen möchte. Dreißig hoch vier sind 810,000 Codes, gezogen aus crypto.getRandomValues. Das ist Privatsphäre durch Verborgenheit, und sie sagt es auch: einen Code zu erraten ist die einzige Tür in das Museum eines anderen, deshalb hat der Gastgeber einen Knopf zum Schließen des Raums, und jedes eintreffende Byte wird ohnehin neu validiert.

Ein Relay ohne Autorität

Die Zugangsdaten, die einen Browser auf diesen Kanal bringen, sind absichtlich öffentlich – die Marketingseiten liefern sie ohnehin an jeden Besucher aus. Die ehrliche Beschreibung des Transports ist ein dummes Rohr, in das jeder mit vier Zeichen alles Beliebige senden kann, und alles Nachgelagerte ist auf dieser Annahme gebaut.

Jede Nachricht wird beim Eintreffen neu validiert, nach Typ und nach Grenze, in einer einzigen Funktion, die die gesamte Vertrauensgrenze dieses Features ist. Exponate, die von einem Peer kommen, laufen erneut durch sanitizeExhibit – dieselbe Funktion, durch die auch der eigene Speicher des Besuchers gefiltert wird, importiert und nie kopiert, damit beide nicht auseinanderdriften. Eine URL, die von der Platte abgelehnt wird, wird auch von der Leitung abgelehnt. Die Sender-ID ist eine Behauptung und kein Ausweis, und es gibt bewusst keine Signaturschicht, anders als beim Draht des 4D-Schachs, wo ein gefälschter Zug eine Partie stehlen könnte. Hier gibt es nichts zu stehlen.

Der Sender ist nie der Bereiniger. Das Aufhängen eines Gastes ist eine Bitte; der Gastgeber bereinigt sie erneut, wendet sie über den eigenen Store an und sendet aus, was tatsächlich gelandet ist.

Verteidigt werden musste dagegen die Verstärkung. Ein dreißig Byte großes hello, das einen Resync über sechzig Exponate erkauft, ist ein Hebel, der auf den Raum zeigt, deshalb hat jede Nachricht, deren Antwort mehr kostet als die Nachricht trägt, einen Boden pro Sender: zwei Sekunden zwischen Resyncs, 400 ms zwischen Chatzeilen, drei Sekunden zwischen Aufhäng-Anfragen. Tippen ist langsamer als 400 ms; eine Schleife nicht. Der Sync wird bei 48 KB oder 24 Elementen zerlegt, je nachdem, was zuerst kommt – und eine leere Sammlung sendet trotzdem einen Chunk mit leerer Liste, denn ein Gast, der nichts empfängt, kann „der Gastgeber besitzt nichts“ nicht von „der Sync kommt noch“ unterscheiden.

Der Kanal trägt Vorstellungen und nichts mit Autorität. Audio berührt ihn nie: Das Relay reicht zwei Browsern die undurchsichtigen Verbindungsblobs des jeweils anderen, und die Medien gehen direkt.

Position ist die eine Nachricht, die sich wiederholt – zehnmal pro Sekunde, pro Besucher, solange der Raum offen ist –, deshalb wird sie auf Zentimeter und Milliradiant quantisiert. Das ist nicht nur Kompression: Der Quantisierer ist zugleich der Leerlaufdetektor, denn zwei Posen, die auf dieselben ganzen Zahlen quantisieren, sind dieselbe Pose, und Stillstehen beendet die Übertragung damit ohne eigenen Test.

Acht Menschen in Wolle und Leinen

Ganz oben in der Spezifikation von v1 standen zwei Zeilen: keine neuen npm-Abhängigkeiten, keine Binärassets. Die zweite entschied, was ein Besucher ist. Keine Uploads, keine heruntergeladenen Figuren – ein Besucher besteht aus vier kleinen ganzen Zahlen und einem Namen, und die gesamte Ausgabe der Garderobe passt in eine Presence-Nutzlast.

Acht Paletten, nach Stoff benannt statt nach Farbe: Schiefer, Kamel, Pflaume, Moos, Rost, Tinte, Leinen, Grünspan. Das Vokabular der Galerie heißt Nussbaum, Kalkputz und Messing unter tiefer Sonne, und jedes Kleidungsstück liegt in diesem Bereich, statt sich mit ihm anzulegen. Genau eine Palette greift nach Türkis, zwei Stufen niedriger im Chroma – ein Besucher in einem türkisstichigen Mantel, keine Lichtquelle, die durch den Raum spaziert. Türkis ist hier weiterhin ein Skalpell.

Die vier Kopfformen sind eine einzige Kurve. Eine Superellipse – |y/ry|^n + |x/rx|^n = 1 – ist bei n = 2 eine Ellipse und nähert sich mit wachsendem n einem Rechteck; rund, oval, eckig und Kamm sind vier Parametersätze davon, der eckige Kopf bei 3.6, ein weicher Rotationswürfel statt einer Kiste mit Ecken. Eine Funktion liefert die Silhouette, die 3D-Figur drechselt sie, die flache Vorschau der Garderobe spiegelt sie in einen Pfad – der Auswahldialog kann also keinen Kopf versprechen, den die Figur nicht behält. Die Größe ist ein enges Band, 0.9× bis 1.1× einer 1.70 m großen Figur.

Die Entscheidung, bei der ich mit mir selbst zu streiten erwartete, war die Kollision. Peers sind Geister: Kein Avatar steuert einen Collider bei, und dem Gehcontroller wird nie gesagt, dass es sie gibt. Was wir zeichnen, ist eine Schätzung – eine federgeglättete Vermutung darüber, wo jemand vor ein paar hundert Millisekunden war – und von einer Schätzung geschubst zu werden ist schlimmer, als durch eine Person zu laufen, die wirklich da ist. Und eine Tür ist 2.3 m breit: Ein einziger untätiger Besucher, der fest darin steht, versiegelt einen Flügel im Museum eines Fremden, ohne dass man ihn bitten könnte weiterzugehen. Sich zu überlappen ist zwei Sekunden Peinlichkeit; eingesperrt zu sein ist ein kaputter Besuch.

Stimmen, die verklingen, wenn man weggeht

Der Textchat war ein Vormittag. Sprache ist das, was aus einer geteilten Seite einen geteilten Raum macht. Audio fließt von Browser zu Browser über direkte Peer-Verbindungen; der Kanal reicht zwei Besuchern nur die undurchsichtigen Sitzungsblobs des jeweils anderen. Genau das setzt die Obergrenze bei acht, Gastgeber eingeschlossen: Ein volles Mesh aus acht sind 28 Verbindungen und sieben Upstreams pro Person, ungefähr das, was der Uplink eines Laptops und der Encoder eines Telefons überstehen. Höher hieße Medienserver, nicht größere Konstante.

Beide Enden eines Paares erfahren im selben Presence-Ereignis, im selben Augenblick, voneinander, also versuchen beide, das Gespräch zu eröffnen, und beide blockieren sich – das klassische Glare. Die Lösung kostet einen Vergleich: der Peer, dessen ID zuerst sortiert, macht das Angebot, der andere wartet. Peer-IDs sind 32 Hexadezimalzeichen, die Ordnung ist also total und auf beiden Seiten identisch. Sprache ist pro Besucher freiwillig, deshalb befördert sich eine antwortende Seite, die 2.5 Sekunden nichts gehört hat, selbst und bietet trotzdem an.

Dann der Teil, den man tatsächlich hört. Eine Stimme liegt innerhalb von zwei Metern auf voller Verstärkung – Gesprächsabstand – und fällt danach mit schlichter inverser Distanz. Die physikalische Inverse bringt allerdings nie jemanden zum Schweigen: bei vierzehn Metern liegt eine Stimme immer noch bei etwa 14 %, und sieben davon unter dem Raumton ergeben ein Gemurmel, das nach Defekt klingt und nicht nach Museum. Deshalb wird die Kurve auf ihrem letzten Stück von einem Smoothstep exakt auf null gefenstert, an beiden Enden flach, damit niemand beim Weggehen wegknackt. Die Räume hier sind etwa zehn Meter weit, was das Ausblenden zwischen das andere Ende dieses Raums und zwei Räume weiter legt.

Beide Achsen linear ab null. Die gestrichelte Linie ist das inverse Gesetz allein, am fernen Rand noch hörbar; die ausgelieferte Kurve ist dieses Gesetz mal ein Fenster, und genau das gibt der Entfernung in einem Gebäude dieser Größe eine Bedeutung.

Das Panorama ist die seitliche Komponente der Peilung, auf ±0.8 begrenzt, und es fällt unterhalb eines Meters zur Mitte zusammen, damit zwei Besucher, die durcheinander hindurchgehen, nicht von links nach rechts durch Ihre Ohren peitschen.

Sprache betreibt einen eigenen Audiokontext, statt sich den der Spiele-Engine zu leihen, die sich in dem Moment aussetzt, in dem das Dokument verborgen wird: richtig für Schritte, falsch für ein Gespräch. Sie gehorcht trotzdem derselben Stummschaltung, denn wer das Museum stummgeschaltet hat, meinte sei still, nicht die Glöckchen sollen still sein. Push-to-Talk sitzt auf V und schaltet die Mikrofonspur ab, nicht die Verbindung.

Der Rest, benannt statt vergraben: Es gibt noch keinen Relay-Server. Zwei Browser, deren Netze sich weigern, sich direkt zu treffen, verbinden sich nie. Dieses Paar wird in der Besucherliste als nicht verfügbar markiert und behält den Textchat – schlechter als funktionierende Sprache, viel besser als ein Rädchen, das sich ewig dreht.

Besuchen, ohne beschrieben zu werden

Ein Gast sieht sich die Sammlung eines anderen an, und das Versprechen aus der Startmeldung lautet, dass Ihr Museum ein Schlüssel in Ihrem eigenen Browser ist. Ein Besuch rendert deshalb eine vollständig eigene Welt und nicht die, die Ihr Schlüssel hält, und in Ihren Speicher wird nicht geschrieben, während Sie unterwegs sind. Der Wechsel fährt in beide Richtungen unter einem Schleier, und er verschleiert sofort, bevor der zerlegte Sync gelandet ist – die Alternative wäre, einen Gast einen Moment lang in seiner eigenen Galerie stehen zu lassen und rätseln zu lassen, wessen Bilder das sind.

Nur-Lesen entpuppte sich als eine Reihe kleiner Urteile statt als ein einziger Schalter. Die Verwaltungs-Schublade behält ihre Liste und ihr Hinlaufen, aber Entfernen und Leeren sind fort. Die Einfügeleiste bleibt und wird zur Bitte: Das Parsen passiert weiterhin im Browser des Gastes, das bereinigte Ergebnis reist, der Gastgeber bereinigt erneut und antwortet mit einem Urteil, das als Hinweis eintrifft. Ist das Aufhängen aus, ist die Leiste deaktiviert und sagt warum, statt zu verschwinden, denn ein fehlendes Bedienelement ist von einer kaputten Seite nicht zu unterscheiden.

Die Naht, die Version eins offen ließ

Die Startmeldung endete mit einem Versprechen: ein geführter Rundgang, „hinter derselben Controller-Naht angelegt, die auch der Gehmodus nutzt“. Eine Weile lang war dieser Rundgang das, was solche Dinge üblicherweise werden – eine Kette einmaliger Kamera-Tweens, aus der Renderschleife heraus angestoßen, was hieß, dass die Kamera je nach Minute zwei Besitzer hatte.

Inzwischen gibt es eine zweite Umsetzung. Die Schiene ist eine vorberechnete Haltestellenliste: jedes Werk in Hängungsreihenfolge, jedes in der frontalen Betrachtungspose, die das automatische Hinlaufen ohnehin schon verwendet. Zwischen den Halten fliegt die Kamera den Polygonzug aus Türwegpunkten, statt durch eine Wand zu interpolieren, verweilt 3.6 Sekunden und läuft am Ende wieder um. Ziehen zum Umschauen während der Fahrt legt einen begrenzten Versatz auf die Ausrichtung der Schiene – unterwegs klingt er zur Mitte hin ab, sodass das nächste Kunstwerk immer mittig ankommt, und beim Verweilen bleibt er stehen. Die Bewegungstasten zu berühren ist die eigentliche Übernahme, und sie gibt einen in der aktuellen Pose ans Gehen zurück. Die bessere Folge eines Controllers statt einer Animation: Ein Kunstwerk zu öffnen pausiert den Rundgang jetzt, statt ihn zu beenden.

Die Minimap kommt aus demselben Layoutmodul wie das Gebäude – Eingang oben, Galerien laufen in der Reihenfolge, in der man sie abgeht, das Panel hinunter. Klickt man einen Raum oder einen Punkt an, bringt einen die vorhandene Geh-oder-Schleier-Mechanik dorthin. Andere Besucher erscheinen als Punkte in ihren eigenen Akzentfarben; das einzige Türkis auf dem Panel ist der Keil, der zeigt, wohin man blickt.

Das Gebäude selbst wurde besser

v1 hatte ein einziges Galerierezept: ein langer Saal mit Kunst, die an zwei schlichten Wänden abwechselt. Es ist ein guter Raum, und bei Exponat dreißig ist es der einzige Raum, in dem man je war. Inzwischen gibt es drei Archetypen, durchgewechselt nach Raumindex – den langen Saal, einen Salon (ein breiterer Prunkraum mit Eckpilastern unter einem Deckenoculus) und eine Nischengalerie, in der zwischen den Hängungen flache Vorlagen aus beiden Seitenwänden treten, sodass jedes Werkpaar in seiner eigenen Nische steht.

Heikel wurde das durch die Invariante, auf der das ganze Gebäude ruht: Exponat N+1 anzuhängen darf die Exponate 1 bis N niemals verschieben. Alles, was ein Archetyp ändert, ist deshalb eine reine Funktion des Raumindex, und Raum 0 ist bewusst der lange Saal mit den ursprünglichen Zahlen, sodass jedes im Juni gebaute Museum exakt die Geometrie reproduziert, die es hatte. Bildlose Exponate hingen früher als Schild an der Wand; sie stehen jetzt als gravierte Stele auf einem Podest, an derselben Slotposition.

Und der Himmel bewegte sich. Drei Stimmungen – goldene Stunde, ein blasser Morgen, eine Dämmerung zur blauen Stunde – werden über einen Hash des lokalen Kalenderdatums aus einem gewichteten Rad mit zehn Plätzen gezogen. Die goldene Stunde behält fünf der zehn, weil sie das Gesicht des Hauses ist; man bekommt den ganzen Tag ein Wetter, morgen ein anderes, und zwei Leute in derselben Zeitzone sehen dasselbe Museum. Jede Stimmung trägt den ganzen Lichtvertrag, nicht nur den Himmel: Die Dämmerung hebt das warme Eigenleuchten des Putzes an, statt es zu senken, denn die Wand über einer Tür erreicht weder die tiefe Sonne noch die Kunstspots.

Was leise ausgeliefert wurde

Vor dem Mounten des Canvas läuft jetzt eine WebGL-Sonde, und ein echtes Fehlen bekommt ein lokalisiertes Poster statt eines leeren Rechtecks – nur echtes Fehlen, niemals Geräteklasse: Ein Telefon mit funktionierendem WebGL behält das ganze begehbare Museum. Kontextverlust ist an einen Wiederherstellungszustand und ein sauberes Neumounten verdrahtet.

Die Kunstwerke sind Meshes, es gibt also nichts, was sich mit Tab ansteuern ließe. Das Muster, das zu einem Canvas mit Ziehen-Blick passt, ist ein Cursor über der Hängungsreihenfolge: [ und ] schalten durch die Werke, die Kamera geht oder teleportiert unter Schleier zu jedem, und jedes wird höflich angesagt. Enter betrachtet – und ohne gesetzten Cursor nimmt es das Werk, das dem eigenen Standort am nächsten ist.

Und das Museum hat jetzt Tests: 303 über 11 Dateien, vorher keiner. Das ist eher Architektur als Tugend: Das Protokoll, die Raumcodes, die Stimmenmathematik, das Layout und die Schiene sind reine Module ohne React, ohne three.js und ohne Netzwerk darin, und deshalb ist warum kommt die Stimme dieser Person von der falschen Seite ein Test und kein Nachmittag im Browser. Die Oberfläche wuchs von 87 Strings auf 246, alle sechs Sprachen, auf Parität geprüft.

v1 – Juniv2 – heute
Besucher1bis zu 8, Gastgeber eingeschlossen
StimmeNähe-Mesh, direkt zwischen Peers
Geführter Rundgangeine Kette von Kamera-Tweensein Schienen-Controller; Fokus pausiert ihn
Galerieräumeein Saalrezept3 Archetypen, nach Raumindex durchgewechselt
Himmelgoldene Stunde3 Stimmungen, eine pro Kalendertag
Bildlose Loseein Schild an der Wandeine Stele auf einem Podest
Orientierunggehen oder die VerwaltungslisteGrundriss-Minimap mit Punkten der anderen
Oberflächen-Strings87 ×6 Sprachen246 ×6 Sprachen
Unit-Tests0303 über 11 Dateien
Serverseitig gespeichertnichtsweiterhin nichts

Die letzte Zeile ist die These. Alles darüber ist das, was es braucht, damit die letzte Zeile wahr bleibt, sobald mehr als eine Person im Gebäude ist.

Das Datenschutzmodell in Version eins war zu simpel, um daran zu scheitern: keine Datenbank, also nichts zu verlieren. Beim gemeinsamen Besuchen wird dieser Satz normalerweise ergänzt – man braucht irgendwo einen Raum, irgendwo eine Anwesenheitsliste, ein wenig Verlauf, damit der Chat scrollt. Es zeigt sich: Man braucht das nicht. Man braucht einen Kanal, einen Code, kurz genug zum Vorlesen, und die Disziplin, jedes ankommende Byte neu zu validieren, denn ein Relay ohne Autorität ist nur dann sicher, wenn ihm nachgelagert nichts vertraut.

Womit ich nicht gerechnet hatte, ist, wie viel besser das Gebäude nebenbei wurde. Sobald ein Raum andere Leute fassen kann, fällt einem auf, dass jede Galerie gleich aussieht, dass sich der Himmel nie ändert, dass die reinen Textlose sich an ihrer Wand entschuldigen. Allein stört das niemanden. Man sieht es erst, wenn jemand anders darin steht.