hpi-sam / digital-fuesim-manv

A German simulation system for training emergency medical services leadership personnel on how to manage Mass Casualty Incidents.
https://fuesim-manv.de/
GNU Affero General Public License v3.0
16 stars 8 forks source link

State management and synchronisation library #1

Closed Dassderdie closed 2 years ago

Dassderdie commented 2 years ago

Requirements

Synchronisation between server/clients

(local) State management client

both

TODOs

libraries to investigate

realtime database

client-side state management (stores)

Dassderdie commented 2 years ago

See this branch for a proof of concept.

Dassderdie commented 2 years ago

Updated concept art

Dassderdie commented 2 years ago

We decided to first create the state data structure/typings to better understand the problems we are facing.

Dassderdie commented 2 years ago

Today we finished the state data structure and discussed about reusing the actions from the store to make changes on the server. There is also a second iteration of the algorithm for optimistic updates: Optimistic updates 2.pdf optimistic updates 2 diagram

Dassderdie commented 2 years ago

@ClFeSc I fixed the classes (we didn't initialize any of the properties -> I mostly moved them into a constructor for now). There is now also a solution to import all the stuff in the shared folder in the frontend and backend (incl. adding dependencies (uuid) to the shared package). To see the typings in the frontend you have to run in the shared folder npm i and npm run build as well as npm i in the frontend again. For now always if you change something in shared you have to run npm run build again - maybe project references could be a solution in the future...

Dassderdie commented 2 years ago

Problem:

The frontend relies on immutable objects. Therefore e.g. the native Map type is seldom used with such frameworks and instead the "old" approach of an object dictionary.

new Map(map.entries()).set('key', 'value'); or cloneMap(map).('key', 'value'); The performance for my naive implementation of adding an element to an immutable Map is around half as fast as with the object dictionary.

{ ...object, ['key']: 'value' }; The .stringify() method (superjson etc.) is slower too (no idea by what factor).

We have the following options (the reducer function (= applies action on the state) is shared between frontend and backend):

Exactly the same goes for Set...

Dassderdie commented 2 years ago

with immer this doesn't look so bad now:

setState(
    produce(getState(), (state) => {
        // this isn't needed thanks to "immer"s produce
        // state.viewports = clone(state.viewports);
        state.viewports.set(viewport.id, viewport);
    })
);

ImmutableAssign didn't work with Map (the reference wasn't renewed -> the object got mutated).

Therefore I now use immerjs. You have to enableMapSet to patch them. The documentation states that it modifies the methods of Map and Set there -> maybe later on problems when trying to stringify it...

NGXS has the ability to use the Redux devtools. This would in theory enable time travel in the store state for better debugging. Sadly it seems like it doesn't work with Map or Set, but only plain objects.

Dassderdie commented 2 years ago

I'm trying out NGRX out now instead of ngxs, because it seems to make it easier to reuse the actions in the backend without the need of the whole store.

Dassderdie commented 2 years ago

I added socket.io. Actions are now send via websocket to the backend, and then broadcasted to all clients.

https://user-images.githubusercontent.com/18506183/145633041-93c815f4-e746-423c-9ba9-818418981381.mp4

Problems I'm working on:

Dassderdie commented 2 years ago

To add any action you have to add the Action and the reducer-function:

exercise.actions.ts

export class RemoveViewport implements Action {
    readonly type = '[Viewport] Remove viewport';

    constructor(public viewportId: UUID) {}
}

exercise.reducer.ts

{
    ...
    '[Viewport] Remove viewport': (state, { viewportId }) => {
        state.viewports.delete(viewportId);
        return state;
    },
    ...
};

Use it like this:

exerciseReducer(state, action)

The code to send an action in the frontend looks like this: app.component.ts

this.apiService.sendAction(new ExerciseActions.AddViewport(viewport));
Dassderdie commented 2 years ago

Im currently thinking about wether the object literal syntax would sometimes be better (especially in cases with a lot of constructor arguments):

this.apiService.sendAction({
      type: '[Viewport] Add viewport',
      viewport,
});
Dassderdie commented 2 years ago
Dassderdie commented 2 years ago

I just noticed a problem with our current API design: The current idea is to first get the state and then all apply all the actions that we receive via "performAction". But there is no way of knowing which state we get and which actions were already applied.

I would therefore propose the following:

The frontend

  1. subscribes to performAction and saves the actions it receives
  2. gets the State
  3. Forgets all actions whose timestamp is before the timestamp of the state
  4. Applies all the other actions in order

We would have to check whether a timestamp (Date.now()) is somehow security relevant...

ClFeSc commented 2 years ago

I think we could just count the actions and return the action number of the last applied action with the state.

Dassderdie commented 2 years ago

Yep, that's better than a timestamp 👍

Dassderdie commented 2 years ago

About performance: I think it would be nice if we could parallelise multiple exercises. So all the code for an exercise should be encapsulated so that you can easily make one thread per active exercise later on.

Dassderdie commented 2 years ago

I'm afraid this issue and pull request is going too big. Therefore I propose the following:

Dassderdie commented 2 years ago
Dassderdie commented 2 years ago

The pull request has been merged and issues for all unresolved tasks have been created. The experiment was a complete success! ;)