Locality Social Cloud Documentation

by Malte
Tagged as: locality_social_cloud, state-management

Table of contents:

  1. 1. Project Setup
  2. 2. Supervise your first PubSub
  3. 3. Each PubSub processes each event that was published to its address exactly once.
  4. 4. The PubSub is generally ‘ViewModel’ and ‘Controller’ at the same time. Often it extends ThrottledChangeNotifier.
  5. 5. Advanced Topics
  6. 6. Wrapup

Project Setup

First, register a developer account and an app. You can do that here. Then, write

flutter pub add locality_social_cloud
flutter pub get

to add the Locality Social Cloud to your Flutter App. Finally, obtain your app-ID and app secret from your developer account and write

    await LocalitySocialCloud.up(
        appId: "YOUR_APP_ID",
        appSecret: "YOUR_APP_SECRET",
        username: "YOUR_USERNAME",
        password: "YOUR_PASSWORD"
    );

to set up your social cloud. The provided username and password are of the end-user that use the social cloud. They will be appropriately hashed.

Supervise your first PubSub

LocalitySocialCloud is your primary interface to easily interact with the social cloud. It is a facade. When you make a PubSub, make sure to supervise it. Each PubSub must be supervised to start functioning. Use

LocalitySocialCloud.supervise(YOUR_PUBSUB)

to supervise a PubSub. PubSub-Supervision is needed for state synchronization across devices.

DiscoverUsers

Use a DiscoverUsers object to find existing users. All users you find will have a M-511 public key, so you can autogenerate shared keys with them.

  DiscoverUsers discoverUsers = LocalitySocialCloud.discoverUsers();

Get a Common Key

Once you have logged in (as a side-effect of LocalitySocialCloud.up) and discovered another user, you can perform a ECDH key exchange for the users:

  localitySocialCloud.loggedInUser!.getKeyFor(otherUser);

Encrypt your Traffic

Finally, you can encrypt your traffict with this key

  LocalitySocialCloud.supervise(YOUR_PUBSUB, key: localitySocialCloud.loggedInUser!.getKeyFor(otherUser));

LoggedInUser, LocalityUser, End-to-End encryption

Using LocalitySocialCloud.auth will give you a LoggedInUser. The difference to a LocalityUser is that a LoggedInUser has a private key and an access token, but you need not worry about that. If you have received LocalityUsers, for example from LocalitySocialCloud.discoverUsers(), you can then use your logged in user to get a common ECDH key, like:

Future<ChaCha20Key> getKeyFor(LocalityUser localityUser, {SharedKeyRepository? sharedKeyRepository})

This will get a common ChaCha20Key for the loggedInUser and localityUser. Now, if the other user would perform the same method in the other way round, where he is the loggedInUser and we are the localityUser, he would compute the same ChaCha20Key, because that is, how ECDH works. Yet from the public key, the private key can not be computed. Since M-511 keys are 512 bit numbers and use elliptic curve additions and multiplications, this method may take some time. Even if it is just a couple tens of miliseconds, this would create a significant delay in user interfaces if you have lists of many localityUsers and have to compute like 50 common keys. That is why, in this case, you can pass a sharedKeyRepository. It will cache the common key for a user in an SQFlite database, in an encrypted way, based on the users password. Thus, without the password, an attacker can not steal this database. The common key you computed for another user is then intended to be used, for example, as a key-Parameter in LocalitySocialCloud.supervise to ensure all traffic via that PubSub is encrypted.

PubSub, LocalityEvent

These classes are at the core of the framework and allow you to write most functionality. Each PubSub has an address that keeps its context separate from other PubSubs. You can emit and receive events with the following guarantee:

Each PubSub processes each event that was published to its address exactly once.

This even holds true, if users go offline in between. PubSub is a mixin with this signature:

mixin PubSub {
      Timeline timeline
      String getTopic();
      void onReceive(LocalityEvent localityEvent);
      WaitingMessage send(String event, Map<String,dynamic> payload);
}

The methods ‘getTopic’ and ‘onReceive’ will have to be implemented by you, while the ‘Timeline’ and ‘send’ method are given to you. Generally the recommended pattern of development for many problems is:

The PubSub is generally ‘ViewModel’ and ‘Controller’ at the same time. Often it extends ThrottledChangeNotifier.

In addition to a payload, a LocalityEvent comes with these fields that are autogenerated for you whenever you send an event (a Map<String, dynamic>):

class LocalityEvent {
  Map<String, dynamic> payload;

  String uuid;
  String event;
  String channel;
  int timestamp;
  int servertime;

}

‘channel’ is a combination of the topic and your app-id. ‘uuid’ is a globally unique ID for the event. ‘timestamp’ is milis since epoch on the users device when the event was sent. ‘servertime’ is a timestamp that the event receives from the server when it arrives there. Generally, these fields should not interest you and working with the payload suffices.

Advanced Topics

With advanced topics you can ensure high performance as well as state consistency. As a bonus goody, to power social apps with real people interaction, we will explore the GeoChannels.

Timeline

Timeline lets you wait for synchronization between the local and global Timeline of a PubSub, before executing functions. When Locality Social Cloud connects to the server, it fetches state synchronization updates from the server. Methods in Timeline.whenSynchronized will be called shortly after all synchronization updates from the server (or Cache, if you use a cache) have been processed. The only method you need from timeline is

somePubSub.timeline.whenSynchronized(() {
    ....
});

ThrottledChangeNotifier

ThrottledChangeNotifier is used when a stateful object with state changes in a massive frequency, such as a PubSub, should be throttled. You can use notifyListeners() on a ThrottledChangeNotifier, but the listeners will only be notified of the current state at a max FPS. You can change it, on standard is 120. This also has the effect that, let us say, you have a List in your state object and have a method that adds 10 elements to the list and calls notifyListeners() each time, then the listeners will still only be notified once with the complete list. You can seamlessly just extend ThrottledChangeNotifier instead of ChangeNotifier.

BEFORE:

class Bla extends ChangeNotifier

AFTER:

class Bla extends ThrottledChangeNotifier.

WaitingEvents

In some cases, you want certain kinds of events to wait for other kinds of events. In that case you can use the WaitingEvents mixin on a PubSub like

class DirectMessagesChat extends ThrottledChangeNotifier with PubSub, WaitingEvents { 
    
}

mixin WaitingEvents on PubSub {
  List<EventWaitInstruction> getEventWaitInstructions();
  void onRelease(LocalityEvent localityEvent);
}

When you use this mixin, it will prompt you to override the ‘getEventWaitInstructions’ method. Use this to provide the instructions, which conditional events should wait for other events before being processed. Events that are waited for will not call ‘onReceive’. If you use WaitingEvents, WaitingEvents itself will override the ‘onReceive’ method. Instead, use ‘onRelease’ in this case. It will only be called after an event saw another event that it waited for.

For example, in a chat, it could look like this:

@override
  List<EventWaitInstruction> getEventWaitInstructions() {
    return [
      EventWaitInstruction(
          event: 'message_seen',
          waitsForEvent: 'message_added',
          traitName: 'message_uuid'
      ),
      EventWaitInstruction(
          event: 'message_reacted',
          waitsForEvent: 'message_added',
          traitName: 'message_uuid'
      ),
      EventWaitInstruction(
          event: 'message_delivered',
          waitsForEvent: 'message_added',
          traitName: 'message_uuid'
      )
    ];
  }

This means, ‘messaged_delivered’ events will get held back, as long as no ‘message_added’ event has not been processed that has a payload with the same ‘message_uuid’. That is, both the ‘message_delivered’ and the ‘message_added’ events must share a ‘message_uuid’ before a particular ‘message_added’ event gets released.

OrderedTimeline

For most cases, you need no authoritative exactness; the order of processing of chat messages, for example, does not matter, as long as they appear in the UI ordered by timestamp. In the most general case, you never have problem with Timeline desync if you can model your problem as something like an abelian group, wher your final state gets computed from order-agnostic state transformations.

However, In some cases, you need a globally authoritative exact timeline; think of an example where you have a limited stock of tickets and many users want to buy these tickets. All users should agree on who bought a ticket first, if it was the last ticket. In this case use can use OrderedTimeline as a mixin on a PubSub. However, you should not do it in all cases, because it means, that for each event(s) received, the ‘result’ has to be computed entirely from scratch. OrderedTimeline will provide you with an ‘onReceivedInOrder’ method that may be executed multiple times for the same event.

mixin OrderedTimeline on PubSub {
  onReceiveInOrder(List<LocalityEvent> event);
}

You will receive the events in temporal order, ordered by servertime. Again, opposing to onReceive, this will be called multiple times for the same events (quadratic runtime).

GeoChannel

Real social interactions have physical proximity as a crucial factor for fun. Only, nobody can touch us. That is why, as a bonus, we offer for you a way to handle geo-objects and write geo-code.

You can obtain a GeoChannel via

LocalitySocialCloud.makeGeoChannel({required String channel})

The GeoChannel works like this:

class Locality {
  double long;
  double lat;
  Map<String, dynamic> metadata;

  Locality({required this.long, required this.lat, required this.metadata});
}

GeoChannel LocalitySocialCloud.makeGeoChannel({required String channel})
 
class GeoChannel
{
  GeoChannel(this.channel);
  void putGeoEntity(String name, Map<String, dynamic> payload, {required double latitude, required double longitude, double lifetimeSeconds = 30})
  Future<List<Locality>> getNearbyEntities(double latitude, double longitude, double range) async
}

Use ‘putGeoEntity’ to place an item in the GeoChannel with a physical location. It will disappear after lifetimeSeconds seconds. You can also add a payload to the geo entity that will be fetched in addition to its name and location by ‘getNearbyEntities’. If there is an existing item with the same name, no new entity is created. Instead, the payload of the old entity is updated. Note that this update is not notified to listeners, you would have to fetch the geo entity again to note the payload change.

Cache

You can set a cache by calling

PubSubSupervisor.cache = Cache(await MessageRepository.getInstance());

Wrapup

Locality Social Cloud offers powerful tools for Flutter Developers to enhance their social apps. Once you get used to modeling your data as sequences of abelian state diffs, your mind will finally be at peace with state management.