So, welcome to my talk about Trixity, one matrix SDK for almost everything.
I added it written in Kotlin a few days ago, so maybe there are some Kotlin fanboys here.
Yeah, let me first introduce myself.
I am Benedict and my friends often see me as a killjoy when it comes to data protection
and data security, but I convince them to come to matrix anyhow.
So I have 20 users, family and friends on my own matrix home server.
My first contact with matrix was four to five years ago and I gained a lot of experience
with it since then.
And so much I found it connect to X and this is just a company that is developing a Timmy
and that is a TI messenger for the medical health factor in Germany.
Now let's start with Trixity.
Trixity is a matrix SDK and it is for developing clients, bots, app servers and servers.
It is multi-platform capable, so everyone thinks Kotlin is JVM only, it is not.
You can compile it to JS and you can compile it to native, that's important for iOS.
So we all have that targets with Trixity.
And it's also developed test-driven, so we have a high test coverage and it is licensed
under the Apache 2 license.
You may wonder why another SDK?
So back in January 2020 there were only a few multi-platform SDKs to choose from.
If I remember correctly there was the Matrix Rust SDK, but it was in a very early stage
and there was the Dart SDK, but very likely this forces you to use Flutter in the UI.
So there is no real free choice which UI framework you want to use, especially when you want
to use native UI technologies, for example on Android Compose or on iOS Swift.
Additionally, most SDKs didn't have a very strict typing of events and the rest end points.
And also the extensibility was a bit limited.
Even if the next point is not that important, SDKs were really bound to its purpose.
So you've had a SDK for a client, for a server, for a bot and so on.
So you have to learn a new SDK for each target, each purpose of application for Matrix.
Why choose Kotlin?
Choose Kotlin because it is a statically typed language which compiles, as I mentioned, to
JVM, JS and native.
And you don't need bindings like in Rust, when you use JS you get JS, you don't need
to make bindings over VASM or something.
And on native you can just call it from your Swift or Objective-C code in X code and have
access to trixity.
Moreover, besides shared common code, it is possible to write platform-specific code.
You just define a common interface and depending on the platform, the actual implementation
can be different.
This way you have access to platform-specific APIs and libraries which can be very helpful
when implementing encryption like AES for attachments.
So you have on each platform, can you use the native encryption algorithm the platform
gives you already.
And last but not least, you can define your own domain-specific language.
You will see later what I did with that.
So let's start with the core of trixity.
The core contains all basic data structures of the spec and its serialization algorithms.
This includes events, identifiers like user IDs, event IDs and so on and other things
like cross-signing keys, device keys.
One goal of developing trixity was the ability to add custom events which are strictly typed.
So this is achieved by mapping event types to just a serializer.
In this example, we add a new type of m.room.pum.cat of the Kotlin type cat event content.
So you have access to all fields of this cat event content and don't have to mess around
with the JSON.
The next layer of trixity is the API layer.
Each API has its model which defines all endpoints of the API.
The actual client and server implementation just use these endpoints.
And so as a consequence, there is no need to define things twice.
They are using the same Kotlin object.
So a Kotlin object represents an endpoint on the matrix side, Kotlin class, not Kotlin
object, sorry.
The best way to show this to you is with an example.
This example is the endpoint leave room.
You just implement matrix endpoint, give him the types and in this case, unit is the response.
So we don't get a JSON as response, just a hated HTTP OK or an empty JSON.
And you can also define a request, a URL, HTTP method and all that.
And you can use this to call, to use a client, a matrix client on the client side to call
these endpoints.
So you create a leave room object, a request and you get the response.
That's all on the client side.
And the same thing on the server side.
So you define an endpoint, give it the type, you expect as a request and in the context
object you have access to the request and can answer with a response.
To make it a bit more easier for developers, there is a bit of abstraction on top of that.
So you can also just call leave room.
So you don't have to know which endpoint are there existing.
You just type point on your IDE and see, OK, there's a leave room, I can leave a room.
And the same on the server side.
So you just need to implement an interface and see all your endpoints you have to implement
to be a fully-featured matrix server API.
Regardless of the API, there is Trick City ARM and Trick City Crypto.
Trick City ARM is just a wrapper for a lip-arm.
As mentioned, a platform-independent implementation doesn't need to worry about the actual platform-specific
implementations.
So you have, when you use Trick City ARM, you don't need to know how lip-arm is accessed.
So on the JVM, I use JNA.
On JS, this is done via VASM and on native, just see interrupt from Kotlin.
Lip-arm is also packaged into Trick City ARM.
So as a developer, you don't need to ship the build-c library.
And it is just loaded automatically.
So you don't need to init your encryption, like in other libraries, as it's just loaded.
My plan is to migrate that to Vodosemak, but currently, UNIFFI, we heard of that in another
talk, does not support VASM targets.
So currently, I can Vodosemak just only use in Kotlin, JVM and native, but I also want
to use JavaScript.
So this project is currently on eyes.
Trick City Crypto currently implements the key management and allows to decrypt and encrypt
events.
And in the future, it will be more, so you can reuse the completely, complete crypto
stuff, for example, in app services.
Trick City Client allows you to, oh, sorry.
The most abstract layer are Trick City Client and Trick City App Service.
While Trick City App Service is still very basic and does not have a persistent layer,
Trick City Client allows you to choose which database and which media store implementation
you want to use.
And on top of that, there is something that isn't released yet.
We are not sure how to release, because we have to make money with our company.
It is Trick City Messenger.
This is just the view model representation of a messenger.
So you only have to implement a thin UI layer, where when the user clicks the button, the
UI sends this to the view model, and the view model says, OK, send a message, or go to
this room, or any other stuff.
And with this approach, we have implemented an iOS client in a few weeks with one person.
So Trick City Client allows you to implement a fully-featured Matrix client or bot.
So if you were at the Matrix Rust SDK talk, you can just use their representation and
instead of Rust, you write Kotlin.
So everything that Matrix Rust SDK does also does Trick City.
Some features like Sliding Zinc aren't there, because we want to follow the stable Matrix
specs, so we don't implement any MSCs.
So we have all the E2E features, the exchangeability data stores and media stores, we have reactive
cache on top of that, notification, thumbnail generation, all that stuff you need to implement
the client.
There are already some media store wrappers that we implemented for all targets, targets
expect browsers, we just use the FI system and on browsers we use indexDB.
Next I want to talk about how I accidentally created a cache.
So on the left side you see the relation between the UI, Trick City and the storage
layer.
And because reactive UIs are really common, I wanted Trick City to give the UI access
to the data in a reactive way.
So if anything changes, the UI should immediately know about this.
But the question is how?
On the one hand there are a few databases which support listeners to react to changes
to the database, but on the other hand this would limit support for multiple supported
databases because finding a common interface for listeners would be hard.
So I started implementing an intermediate layer based on Kotlin flows.
The flow in Kotlin is a reactive data structure.
So you have a producer on the one side and a consumer on the other side.
So if the producer changes anything, the consumers immediately know about that.
And what does the intermediate layer, it talks to a very thin database layer which only knows
about save, read and delete data.
And if someone wants data from this layer, it just reads it from the database or if someone
changes something in this layer, it just writes it to the database.
And the values are kept in this layer as long as they are subscribed from anyone.
So this means that if anyone else subscribes to a value, he will immediately get the current
value because there is no additional database call needed because it is persisted in the
intermediate layer.
This goes so far that even if there are no subscribers anymore, I just keep the value
a bit longer in this layer.
So if someone asks for a value for example 10 seconds later and the value is still stored,
he gets the value and there is no database call needed.
And you can now guess what I implemented, it's just cache.
So as you see with this cache, everything in Trixity is reactive.
These are just a few examples, so you can just get all users or check if a user can
invite another one, you immediately get the notification if anything has changed.
As mentioned, the database layer is really thin, so we implemented many database layers.
So SQL-based one, we are via exposed for JVM-based targets.
We implemented one with Realm that can be also used on native targets like iOS and for
browsers we have IndexedDB.
So most of the data changes, when a Zinc is processed, most of the data changes when a
Zinc is processed.
So it is way more performant to make a large transaction around the Zinc.
So you don't have a transaction, every time the cache writes something into the database,
Trixity just spends a large transaction around Zinc, so you have thousands of writes in
one transaction.
So everything fine, no, then there was Realm, and Realm is just a really fast database,
but Realm only allows one write transaction at the time.
So if another one wants to write to the database, he needs to wait until the first transaction
ended.
And the problem is that while the Zinc is running, it may be needed that we have to
wait for outdated keys to be updated to decrypt on stuff.
So if the outdated keys part of Trixity want to write something in the database, he needs
to wait until the Zinc is ended, but the Zinc waits for the keys to be updated, so we have
a deadlock there.
This is one of the reasons why I introduced Azure Zinc transactions.
The other reason was that most of the time the Zinc processing, as I find out with some
benchmark, was wasted due to writing to the database.
So processing a Zinc takes a long time because there are so many IO operations that the user
have to wait until all operations are done.
So what does a Zinc transaction in Trixity mean that all changes to the database are
collected and processed in the background?
So database operations are decoupled from the cache layer, and they are just written
in the background.
If everything fails, it is war-backed, but that's irrelevant in the normal use case.
So we can process even more Zincs at the same time as if we would wait that the transaction
has finished.
And this gave Trixity a huge performance boost.
Actually I released it last week, and I've wrote an integration test which just fails
if it is not 50% faster.
So it is always green, I don't know.
The next thing I did completely different in Trixity are timelines.
So normally Zincs are sent as fragment from the server to the client.
So one fragment contains a few timeline events, and if there's a gap, you get a token.
So you know as a client, okay, there's a gap, I need to fetch to fill that gap, and so on.
And these fragments normally are saved as is to the database in clients.
In Trixity, I use another approach.
There I have each timeline event pointing to each other.
And if there's a gap, the timeline event knows about this.
So this allows Trixity to again benefit from content flows.
So we have a producer that is the room starting from a timeline event, and a subscriber who
wants the next timeline event to fill its timeline.
So this allows us to go really fast through the timeline and bid the timeline under the
top, and it makes it easier to fill the gaps, because we don't have another layer, fragments,
we just have timeline events.
And this way, it's also possible to very easy connect upgraded rooms.
So that one I released yesterday, I think, or two days ago.
So the timeline event just shows to another timeline in another room.
So timelines with room upgrades are invisible for users of Trixity.
You just get an infinite timeline until you reach the oldest room and the first timeline
event.
And finally, a small example.
So if you want to write a bot, that's a good start to use Trixity just to get a feeling
about it, how it works.
You can just call get timeline events from now on.
And what this does is it subscribes to the flow that I mentioned, which built the timeline,
and rates until the timeline event is decrypted, because the timeline itself also is a flow.
So if everything changes, it is redacted, or there's a reaction, or a replacement, the
timeline event flow changes.
So this get timeline events from now on just wraps it down, so you get a timeline event
that is decrypted.
And you can see we can just check what type it has, and when we have checked the type,
we have access to body, and then we have send message.
So when you call send message, you don't have to worry about if the room is encrypted
or not.
You can just use the DSL that I created to write text messages, image messages, and so
on, and you can also form relations with that.
So you can say like here, yeah, this is a reply to the timeline event I just got.
And this has extensible events in mind, so if in the future there are other content blocks
that are added, we can just extend the DSL, and you can very, very intuitive write your
content with Trixity into an event, into an extensible event.
So here are some projects that are using Trixity and that I know about.
There is a Spotify bot, a Mensa bot.
Someone has created some extensions to better use it for bots and so on.
And there is Trixity examples.
That is from me, this is just a ping bot, part of it you saw here.
It is E2E enabled, you can just run it in your browser, on your Linux machine, or on
your iOS client, or via the JVM on Android.
And there is also Timmy Messenger, that's our messenger from our company, but it's not
open source yet.
We plan to, but we don't know how, because licensing.
Yeah, just try it out and come to me if you have questions.
I'm a bit around.
This is the matrix room, this is my matrix ID.
And if we have a bit of time, I just can show you a small demo, I think.
Yeah, I made a small performance comparison.
It's not representative, because it just runs once on my machine, and there was no warm
up or multiple runs.
Yeah, on the left side you see our Timmy client, but basically it's just using Trixity.
On the right side there is Element, and in the middle is Fluffy Chat.
And now you can give me your bets, who is the fastest.
Yeah, let's see, when the red zoom comes, the response from the server reached the client.
So I just looked into the Synapse logs when the response was sent.
So we just wait a few seconds, and then we see who is first.
And you can look into opening rooms, because we have this caching, it is very fast in our
client, but I must say Fluffy Chat is also very fast regarding opening rooms.
So, oh, Trixity was the fastest.
And we can open rooms, and you see open rooms is also a lot faster than on Element.
And there was Fluffy Chat, and Fluffy Chat also is very fast.
Yeah, I also have a desktop demo, but there Neko is the fastest.
This is Neko, this is Timmy, Element on the web.
It's a bit hard, this comparison, because Element runs in the web and does not have the
multi-threading other clients have.
So Neko, just three seconds.
I can just chat around, and the next is Timmy on the left, top left side, also very fast
opening rooms, and switching rooms, because it is cached all the time in events.
Then there was Element, and I think also Fluffy Chat, yeah, Fluffy Chat also.
Yeah, okay, that was my talk, thank you.
Questions?
How do you prevent data loss with your async transactions?
The transactions are run each after.
So if one transaction fails, the other transactions are just run, and the next one starts with
the alt token.
What happens if, say, your battery runs out whilst a bunch of transactions are queued?
If your battery runs out when all those transactions are queued, so they haven't been written to
the database.
Yeah, then they are gone.
Your client has to do the work again, but mostly this doesn't happen.
If you close your client, all transactions are written that are just opened, but it depends
on your platform if it is killed hardly, or a trick city have a bit of time to write
the transactions back to the database.
But it's still very fast to write, so it's just a bit snappier on mobile devices, which
are not that fast.
Like my smartphone from 2016, Element, I can't run Element on that, because it's too slow,
and sending messages, 10 seconds later, the message is, oh, okay, yes, yes, now.
Now we send the message.
I don't have this problem, because zooms are just faster than the slow I owe writing to
the database we have on old smartphones, for example.
Another question.
It's nice to write.
I like DSLs.
In Kotlin, we have them all over the language, and it feels very intuitive, because your
IDE gives you suggestions, what methods they are, and it's a lot easier to read, I think.
There's Rust, and there's Kotlin, but is there any way to realize the amount of things that
the user has to learn to use all these things?
I didn't understand the question, acoustically.
There's a lot of language learning to make any progress.
Is there any effort to unify this, or towards Rust, maybe?
To be honest, I don't like Rust.
I just like a higher level of implementing stuff, so we didn't spoke, I didn't spoke
with the Matrix Rust team.
I think we are done with the time, and the last question from the audience would be that
we can open the windows and the doors a bit to get more air in.
Thank you very much.
Yep.
That's it.