npm package: @strategies/collaborate-on-fire
Description: Multi-user shared state persistence using patches built on firebase
Author: Armin Akhavan
Latest version: 1.2.3

# collaborate-on-fire

Use collaborate-on-fire to enable multi-user shared-state environments using [mobx-keystone](https://github.com/xaviergonz/mobx-keystone) and [firebase](https://github.com/firebase/).

## Motivation
Firebase provides many mechanisms required for creating multi-user environments. It is trivial to create a chat-client or a simple game where each user controls only their own state. However, true collaboration in web-based tools often requires operating on the same set of elements simultaneously. This creates conflicts when trying to centralize state in firestore or realtime db. This solution uses the patch feature of mobx-keystone to send patches from actions performed by one user to all other users.

## Features

- Synchronize shared file state between multiple users:
  - Watch for changes to the shared file data and push these changes to all other users using realtime-db.
  - Apply incoming patches from other users to the local document state.
  - Collect patches to be sent using a configurable interval, to reduce traffic for more intensive operations.
- Synchronize independent user state - such as cursor position or selection.
- Load a file from cloud firestore, and track any incoming patches while it loads to ensure consistency.
- Save the file to cloud firestore based on the most recent user to update.
- Undo/Redo support: incoming changes do not get added to the stack. Users will only undo/redo their own changes.

## Example

https://github.com/sasakiassociates/collaborate-on-fire-example

## Installation

Run the following command to install `collaborate-on-fire` in your project:

```sh
yarn add collaborate-on-fire
```
OR
```sh
npm install collaborate-on-fire
```

## Usage

### Initialize Firebase
Firebase should be initialized using initializeApp before creating the MultiUserPersistence instance.
```ts
firebase.initializeApp(firebaseConfig);
```

Create a RootStore class with a file, userStore and undoManager that implement the IRootStore interface. Once the user id has been determined (via login) the `MultiUserPersistence` instance can be created.

```ts
const stateContainer: IStateContainer = new KeystoneContainer();//see example for KeystoneContainer
const rootStore: IRootStore = new RootStore();
firebase.auth().onAuthStateChanged(user => {
    new MultiUserPersistence(rootStore, stateContainer, user.uid);
});
```
## Considerations
Think carefully about what needs to persist in your file, what can go in temporary state (e.g. interface state) and what (if anything) needs to be synced as user state.

Be careful applying snapshots e.g. to undo a change because that will undo globally and delete all actions by another user. e.g. if one were to use the practice of reverting a modal's changes to a previous snapshot on cancel this would not work well in a multi-user setup: if one user left a modal open, came back and cancelled it later, it would revert all changes by other users while that modal was open. The built-in mobx-keystone undo middleware uses patches instead of snapshots and therefore works well with this patch-based approach.

## Realtime Transaction Counter
The Realtime Transaction Counter (RTC) is a centralized counter used to keep track of patches and the order in which they are applied. The same counter is saved on the firestore file's metadata so that patches since the last save can be applied when loading.

## IRootStore.undoManager (optional)
It is best practice to exclude user patches from other users from the undo/redo stack. If using the mobx-keystone undo middleware, this can be specified on the rootStore object and `withoutUndo` will be wrapped around incoming patches. 

```ts
import {UndoManager, undoMiddleware} from "mobx-keystone";
import IRootStore from "collaborate-on-fire";

class RootStore implements IRootStore {
    persist: FileStore;
    undoManager: UndoManager;
    userStore: UserStore;

    constructor(persist: FileStore, userStore: UserStore) {
        this.persist = persist;
        this.userStore = userStore;
        this.undoManager = undoMiddleware(persist);
    }
};
```


## SingleUserPersistence
If you want to disable the multi-user behavior - for example while testing or for a demo version, you can simply use SingleUserPersistence instead (the user id is not needed)

```ts
new SingleUserPersistence(rootStore, stateContainer);
```

## Keystone
For an example using keystone, take a look at `collaborate-on-fire-example`.

## License

MIT