diff --git a/docs/architecture/deep-dives/state/State_Diagram.svg b/docs/architecture/deep-dives/state/State_Diagram.svg new file mode 100644 index 00000000..0ba405b6 --- /dev/null +++ b/docs/architecture/deep-dives/state/State_Diagram.svg @@ -0,0 +1,21 @@ + + + + + + + + StateGlobalUserPlatform owned & Platform managedStateDefinitionPlatform owned & Team managedKeyDefinitionTeam owned & Team managedglobaluser_ac06d663-bbbc-4a51-a764-5d105ae6f7cbglobal_stateuser_ac06d663-bbbc-4a51-a764-5d105ae6f7cb_stateglobal_state_keyuser_ac06d663-bbbc-4a51-a764-5d105ae6f7cb_state_key \ No newline at end of file diff --git a/docs/architecture/deep-dives/state/derived-state.md b/docs/architecture/deep-dives/state/derived-state.md new file mode 100644 index 00000000..bb187310 --- /dev/null +++ b/docs/architecture/deep-dives/state/derived-state.md @@ -0,0 +1,129 @@ +# Derived State + +It is common to need to cache the result of expensive work that does not represent true alterations +in application state. Derived state exists to store this kind of data in memory and keep it up to +date when the underlying observable state changes. + +## `DeriveDefinition` + +Derived state has all of the same issues with storage and retrieval that normal state does. Similar +to `KeyDefinition`, derived state depends on `DeriveDefinition`s to define magic string keys to +store and retrieve data from a cache. Unlike normal state, derived state is always stored in memory. +It still takes a `StateDefinition`, but this is used only to define a namespace for the derived +state, the storage location is ignored. _This can lead to collisions if you use the same key for two +different derived state definitions in the same namespace._ + +Derive definitions can be created in two ways: + + + +```typescript +new DeriveDefinition(STATE_DEFINITION, "uniqueKey", _DeriveOptions_); + +// or + +const keyDefinition: KeyDefinition; +DeriveDefinition.from(keyDefinition, _DeriveOptions_); +``` + +The first allows building from basic building blocks, the second recognizes that derived state is +often built from existing state and allows you to create a definition from an existing +`KeyDefinition`. The resulting `DeriveDefinition` will have the same state namespace, key, and +`TFrom` type as the `KeyDefinition` it was built from. + +### Type Parameters + +`DeriveDefinition`s have three type parameters: + +- `TFrom`: The type of the state that the derived state is built from. +- `TTo`: The type of the derived state. +- `TDeps`: defines the dependencies required to derive the state. This is further discussed in + [Derive Definition Options](#derivedefinitionoptions). + +### `DeriveDefinitionOptions` + +[The `DeriveDefinition` section](#deriveDefinitionFactories) specifies a third parameter as +`_DeriveOptions_`, which is used to fully specify the way to transform `TFrom` to `TTo`. + +- `deserializer` - For the same reasons as [Key Definition Options](#keydefinitionoptions), + `DeriveDefinition`s require have a `deserializer` function that is used to convert the stored data + back into the `TTo` type. +- `derive` - A function that takes the current state and returns the derived state. This function + takes two parameters: + - `from` - The latest value of the parent state. + - `deps` - dependencies used to instantiate the derived state. These are provided when the + `DerivedState` class is instantiated. This object should contain all of the application runtime + dependencies for transform the from parent state to the derived state. +- `cleanupDelayMs` (optional) - Takes the number of milliseconds to wait before cleaning up the + state after the last subscriber unsubscribes. Defaults to 1000ms. If you have a particularly + expensive operation, such as decryption of a vault, it may be worth increasing this value to avoid + unnecessary recomputation. + +Specifying dependencies required for your `derive` function is done through the type parameters on +`DerivedState`. + +```typescript +new DerivedState(); +``` + +would require a `deps` object with an `example` property of type `Dependency` to be passed to any +`DerivedState` configured to use the `DerivedDefinition`. + +:::warning + +Both `derive` and `deserializer` functions should take null inputs into consideration. Both parent +state and stored data for deserialization can be `null` or `undefined`. + +::: + +## `DerivedStateProvider` + +The `DerivedState` class has a purpose-built provider which instantiates the +correct `DerivedState` implementation for a given application context. These derived states are +cached within a context, so that multiple instances of the same derived state will share the same +underlying cache, based on the `DeriveDefinition` used to create them. + +Instantiating a `DerivedState` instance requires an observable parent state, the derive definition, +and an object containing the dependencies defined in the `DeriveDefinition` type parameters. + +```typescript +interface DerivedStateProvider { + get: ( + parentState$: Observable, + deriveDefinition: DeriveDefinition, + dependencies: TDeps, + ) => DerivedState; +} +``` + +:::tip + +Any observable can be used as the parent state. If you need to perform some kind of work on data +stored to disk prior to sending to your `derive` functions, that is supported. + +::: + +## `DerivedState` + +`DerivedState` is intended to be built with a provider rather than directly instantiated. The +interface consists of two items: + +```typescript +interface DerivedState { + state$: Observable; + forceValue(value: T): Promise; +} +``` + +- `state$` - An observable that emits the current value of the derived state and emits new values + whenever the parent state changes. +- `forceValue` - A function that takes a value and immediately sets `state$` to that value. This is + useful for clearing derived state from memory without impacting the parent state, such as during + logout. + +:::note + +`forceValue` forces `state$` _once_. It does not prevent the derived state from being recomputed +when the parent state changes. + +::: diff --git a/docs/architecture/deep-dives/state/index.md b/docs/architecture/deep-dives/state/index.md new file mode 100644 index 00000000..f3dc0b0c --- /dev/null +++ b/docs/architecture/deep-dives/state/index.md @@ -0,0 +1,510 @@ +# State Provider Framework + +The state provider framework was designed for the purpose of allowing state to be owned by domains +but also to enforce good practices, reduce boilerplate around account switching, and provide a +trustworthy observable stream of that state. + +## APIs + +- [Storage definitions](#storage-definitions) + - [`StateDefinition`](#statedefinition) + - [`KeyDefinition` & `UserKeyDefinition`](#keydefinition-and-userkeydefinition) +- [`StateProvider`](#stateprovider) +- [`ActiveUserState`](#activeuserstatet) +- [`GlobalState`](#globalstatet) +- [`SingleUserState`](#singleuserstatet) + +### Storage definitions + +In order to store and retrieve data, we need to have constant keys to reference storage locations. +This includes a storage medium (disk or memory) and a unique key. `StateDefinition` and +`KeyDefinition` classes allow for reasonable reuse of partial namespaces while also enabling +expansion to precise keys. They exist to help minimize the potential of overlaps in a distributed +storage framework. + +:::warning + +Once you have created the definitions you need to take extreme caution when changing any part of the +namespace. If you change the name of a `StateDefinition` pointing at `"disk"` without also migrating +data from the old name to the new name you will lose data. Data pointing at `"memory"` can have its +name changed. + +::: + +#### `StateDefinition` + +:::note + +Secure storage is not currently supported as a storage location in the State Provider Framework. For +now, don't migrate data that is stored in secure storage but please contact the Platform team when +you have data you wanted to migrate so we can prioritize a long-term solution. If you need new data +in secure storage, use `StateService` for now. + +::: + +`StateDefinition` is a simple API but a very core part of making the State Provider Framework work +smoothly. It defines a storage location and top-level namespace for storage. Teams will interact +with it only in a single `state-definitions.ts` file in the +[`clients`](https://github.com/bitwarden/clients) repository. This file is located under Platform +team code ownership but teams are expected to create edits to it. A team will edit this file to +include a line such as: + +```typescript +export const MY_DOMAIN_DISK = new StateDefinition("myDomain", "disk"); +``` + +The first argument to the `StateDefinition` constructor is expected to be a human readable, +camelCase-formatted name for your domain or state area. The second argument will either be the +string literal `"disk"` or `"memory"` dictating where all the state using this `StateDefinition` +should be stored. + +The Platform team will be responsible to reviewing all new and updated entries in this file and will +be looking to make sure that there are no duplicate entries containing the same state name and state +location. Teams _can_ have the same state name used for both `"disk"` and `"memory"` locations. +Tests are included to ensure this uniqueness and core naming guidelines so you can ensure a review +for a new `StateDefinition` entry can be done promptly and with very few surprises. + +##### Client-specific storage locations + +An optional third parameter to the `StateDefinition` constructor is provided if you need to specify +client-specific storage location for your state. + +This will most commonly be used to handle the distinction between session and local storage on the +web client. The default `"disk"` storage for the web client is session storage, and local storage +can be specified by defining your state as: + +```typescript +export const MY_DOMAIN_DISK = new StateDefinition("myDomain", "disk", { web: "disk-local" }); +``` + +#### `KeyDefinition` and `UserKeyDefinition` + +`KeyDefinition` and `UserKeyDefinition` build on the [`StateDefinition`](#statedefinition), +specifying a single element of state data within the `StateDefinition`. + +The framework provides both `KeyDefinition` and `UserKeyDefinition` for teams to use. The +`UserKeyDefinition` should be used for defining pieces of state that are scoped at a user level. +These will be consumed via the [`ActiveUserState`](#activeuserstatet) or +[`SingleUserState`](#singleuserstatet) within your consuming services and components. The +`UserKeyDefinition` extends the `KeyDefinition` and provides a way to specify how the state will be +cleaned up on specific user account actions. + +`KeyDefinition`s and `UserKeyDefinition`s can also be instantiated in your own team's code. This +might mean creating it in the same file as the service you plan to consume it or you may want to +have a single `key-definitions.ts` file that contains all the entries for your team. Some example +instantiations are: + +```typescript +const MY_DOMAIN_DATA = new UserKeyDefinition(MY_DOMAIN_DISK, "data", { + // convert to your data from serialized representation `{ foo: string }` to fully-typed `MyState` + deserializer: (jsonData) => MyState.fromJSON(jsonData), + clearOn: ["logout"], // can be lock, logout, both, or an empty array +}); + +// Or if your state is an array, use the built-in helper +const MY_DOMAIN_DATA: UserKeyDefinition = UserKeyDefinition.array( + MY_DOMAIN_DISK, + "data", + { + deserializer: (jsonDataElement) => MyState.fromJSON(jsonDataElement), // provide a deserializer just for the element of the array + }, + { + clearOn: ["logout"], + }, +); + +// record +const MY_DOMAIN_DATA: UserKeyDefinition> = + KeyDefinition.record(MY_DOMAIN_DISK, "data", { + deserializer: (jsonDataValue) => MyState.fromJSON(jsonDataValue), // provide a deserializer just for the value in each key-value pair + clearOn: ["logout"], + }); +``` + +The arguments for defining a `KeyDefinition` or `UserKeyDefinition` are: + +| Argument | Usage | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `stateDefinition` | The `StateDefinition` to which that this key belongs | +| `key` | A human readable, camelCase-formatted name for the key definition. This name should be unique amongst all other `KeyDefinition`s or `UserKeyDefinition`s that consume the same `StateDefinition`. | +| `options` | An object of type [`KeyDefinitionOptions`](#key-definition-options) or [`UserKeyDefinitionOptions`](#key-definition-options), which defines the behavior of the key. | + +:::warning + +It is the responsibility of the team to ensure the uniqueness of the `key` within a +`StateDefinition`. As such, you should never consume the `StateDefinition` of another team in your +own key definition. + +::: + +##### Key Definition Options + +| Option | Required? | Usage | +| ---------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `deserializer` | Yes | Takes a method that gives you your state in it's JSON format and makes you responsible for converting that into JSON back into a full JavaScript object, if you choose to use a class to represent your state that means having its prototype and any method you declare on it. If your state is a simple value like `string`, `boolean`, `number`, or arrays of those values, your deserializer can be as simple as `data => data`. But, if your data has something like `Date`, which gets serialized as a string you will need to convert that back into a `Date` like: `data => new Date(data)`. | +| `cleanupDelayMs` | No | Takes a number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed. Defaults to 1000ms. | +| `clearOn` | Yes, for `UserKeyDefinition` | An additional parameter provided for `UserKeyDefinition` **only**, which allows specification of the user account `ClearEvent`s that will remove the piece of state from persistence. The available values for `ClearEvent` are `logout`, `lock`, or both. An empty array should be used if the state should not ever be removed (e.g. for settings). | + +### `StateProvider` + +`StateProvider` is an injectable service that includes 4 methods for getting state. These four +methods are helpers for invoking their more modular siblings `ActiveStateProvider.get`, +`SingleUserStateProvider.get`, `GlobalStateProvider.get`, and `DerivedStateProvider`. These siblings +can all be injected into your service as well. If you prefer thin dependencies over the slightly +larger changeset required, you can absolutely make use of the more targeted providers. +`StateProvider` has the following type definition (aliasing the targeted providers): + +```typescript +interface StateProvider { + getActive(keyDefinition: KeyDefinition): ActiveUserState; + getUser(userId: UserId, keyDefinition: KeyDefinition): SingleUserState; + getGlobal(keyDefinition: KeyDefinition): GlobalState; + getDerived( + parentState$: Observable, + deriveDefinition: DeriveDefinition, + dependenciess: TDeps, + ); +} +``` + +A very common practice will be to inject `StateProvider` in your service's constructor and call +`getActive`, `getGlobal`, or both in your constructor and then store private properties for the +resulting `ActiveUserState` and / or `GlobalState`. It's less common to need to call `getUser` +in the constructor because it will require you to know the `UserId` of the user you are attempting +to edit. Instead you will add `private` to the constructor argument injecting `StateProvider` and +instead use it in a method like in the below example. + +```typescript +import { FOLDERS_USER_STATE, FOLDERS_GLOBAL_STATE } from "../key-definitions"; + +class FolderService { + private folderGlobalState: GlobalState; + private folderUserState: ActiveUserState>; + + folders$: Observable; + + constructor(private stateProvider: StateProvider) { + this.folderUserState = stateProvider.getActive(FOLDERS_USER_STATE); + this.folderGlobalState = stateProvider.getGlobal(FOLDERS_GLOBAL_STATE); + + this.folders$ = this.folderUserState.pipe( + map((foldersRecord) => this.transform(foldersRecord)), + ); + } + + async clear(userId: UserId): Promise { + await this.stateProvider.getUser(userId, FOLDERS_USER_STATE).update((state) => null); + } +} +``` + +### `ActiveUserState` + +:::warning + +`ActiveUserState` has problems with consider not using it anymore, +[read more](#should-i-use-activeuserstate). + +::: + +`ActiveUserState` is an object to help you maintain and view the state of the currently active +user. If the currently active user changes, like through account switching, the data this object +represents will change along with it. Gone is the need to subscribe to +`StateService.activeAccountUnlocked$`. You can see the type definition of the API on +`ActiveUserState` below: + +```typescript +interface ActiveUserState { + state$: Observable; +} +``` + +The `state$` property provides you with an `Observable` that can be subscribed to. +`ActiveUserState.state$` will emit for the following reasons: + +- The active user changes. +- The chosen storage location emits an update to the key defined by `KeyDefinition`. This can occur + for any reason including: + - A `SingleUserState` method pointing at the same `UserKeyDefinition` as `ActiveUserState` and + pointing at the user that is active that had `update` called + - Someone updates the key directly on the underlying storage service _(please don't do this)_ + +### `GlobalState` + +`GlobalState` has an incredibly similar API surface as `ActiveUserState` except it targets +global-scoped storage and does not emit an update to `state$` when the active user changes, only +when the stored value is updated. + +### `SingleUserState` + +`SingleUserState` behaves very similarly to `GlobalState` where neither will react to active +user changes and you instead give it the user you want it to care about up front, which is publicly +exposed as a `readonly` member. + +Updates to `SingleUserState` or `ActiveUserState` handling the same `KeyDefinition` will cause each +other to emit on their `state$` observables if the `userId` handled by the `SingleUserState` happens +to be active at the time of the update. + +## Migrating + +Migrating data to state providers is incredibly similar to migrating data in general. You create +your own class that extends `Migrator`. That will require you to implement your own +`migrate(migrationHelper: MigrationHelper)` method. `MigrationHelper` already includes methods like +`get` and `set` for getting and settings value to storage by their string key. There are also +methods for getting and setting using your `KeyDefinition` or `KeyDefinitionLike` object to and from +user and global state. An example of how you might use these new helpers is below: + +```typescript +type ExpectedGlobalState = { myGlobalData: string }; + +type ExpectedAccountState = { myUserData: string }; + +const MY_GLOBAL_KEY_DEFINITION: KeyDefinitionLike = { + stateDefinition: { name: "myState" }, + key: "myGlobalKey", +}; +const MY_USER_KEY_DEFINITION: KeyDefinitionLike = { + stateDefinition: { name: "myState" }, + key: "myUserKey", +}; + +export class MoveToStateProvider extends Migrator<10, 11> { + async migrate(migrationHelper: MigrationHelper): Promise { + const existingGlobalData = await migrationHelper.get("global"); + + await migrationHelper.setGlobal(MY_GLOBAL_KEY_DEFINITION, { + myGlobalData: existingGlobalData.myGlobalData, + }); + + const updateAccount = async (userId: string, account: ExpectedAccountState) => { + await migrationHelper.setUser(MY_USER_KEY_DEFINITION, { + myUserData: account.myUserData, + }); + }; + + const accounts = await migrationHelper.getAccounts(); + + await Promise.all(accounts.map(({ userId, account }) => updateAccount(userId, account))); + } +} +``` + +:::note + +`getAccounts` only gets data from the legacy account object that was used in `StateService`. As data +gets migrated off of that account object the response from `getAccounts`, which returns a record +where the key will be a user's ID and the value being the legacy account object. + +::: + +### Example PRs + +- [`EnvironmentService`](https://github.com/bitwarden/clients/pull/7621) +- [Org Keys](https://github.com/bitwarden/clients/pull/7521) + +## Testing + +Testing business logic with data and observables can sometimes be cumbersome. To help make that a +little easier there are a suite of helpful "fakes" that can be used instead of traditional "mocks". +Now instead of calling `mock()` into your service you can instead use +`new FakeStateProvider()`. + +`FakeStateProvider` exposes the specific provider's fakes as properties on itself. Each of those +specific providers gives a method `getFake` that allows you to get the fake version of state that +you can control and `expect`. + +## Advanced usage + +### `update` + +The update method has options defined as follows: + +```typescript +{ActiveUser|SingleUser|Global}State { + // ... rest of type left out for brevity + update(updateState: (state: T, dependency: TCombine) => T, options?: StateUpdateOptions); +} + +type StateUpdateOptions = { + shouldUpdate?: (state: T, dependency: TCombine) => boolean; + combineLatestWith?: Observable; + msTimeout?: number +} +``` + +The `shouldUpdate` option can be useful to help avoid an unnecessary update, and therefore avoid an +unnecessary emission of `state$`. You might want to use this to avoid setting state to `null` when +it is already `null`. The `shouldUpdate` method gives you in its first parameter the value of state +before any change has been made to it and the dependency you have, optionally, provided through +`combineLatestWith`. To avoid setting `null` to your state when it's already `null` you could call +`update` like below: + +```typescript +await myUserState.update(() => null, { shouldUpdate: (state) => state != null }); +``` + +The `combineLatestWith` option can be useful when updates to your state depend on the data from +another stream of data. In +[this example](https://github.com/bitwarden/clients/blob/2eebf890b5b1cfbf5cb7d1395ed921897d0417fd/libs/common/src/auth/services/account.service.ts#L88-L107) +you can see how we don't want to set a user ID to the active account ID unless that user ID exists +in our known accounts list. This can be preferred over the more manual implementation like such: + +```typescript +const accounts = await firstValueFrom(this.accounts$); +if (accounts?.[userId] == null) { + throw new Error(); +} +await this.activeAccountIdState.update(() => userId); +``` + +The use of the `combineLatestWith` option is preferred because it fixes a couple subtle issues. +First, the use of `firstValueFrom` with no `timeout`. Behind the scenes we enforce that the +observable given to `combineLatestWith` will emit a value in a timely manner, in this case a +`1000ms` timeout but that number is configurable through the `msTimeout` option. The second issue it +fixes is that we don't guarantee that your `updateState` function is called the instant that the +`update` method is called. We do however promise that it will be called before the returned promise +resolves or rejects. This may be because we have a lock on the current storage key. No such locking +mechanism exists today but it may be implemented in the future. As such, it is safer to use +`combineLatestWith` because the data is more likely to retrieved closer to when it needs to be +evaluated. + +## FAQ + +### Do I need to have my own in-memory cache? + +If you previously had a memory cache that exactly represented the data you stored on disk (not +decrypted for example), then you likely don't need that anymore. All the `*State` classes maintain +an in memory cache of the last known value in state for as long as someone is subscribed to the +data. The cache is cleared after 1000ms of no one subscribing to the state though. If you know you +have sporadic subscribers and a high cost of going to disk you may increase that time using the +`cleanupDelayMs` on `KeyDefinitionOptions`. + +### I store my data as a Record / Map but expose it as an array -- what should I do? + +Give `KeyDefinition` generic the record shape you want, or even use the static `record` helper +method. Then to convert that to an array that you expose just do a simple +`.pipe(map(data => this.transform(data)))` to convert that to the array you want to expose. + +### Why KeyDefinitionLike + +`KeyDefinitionLike` exists to help you create a frozen-in-time version of your `KeyDefinition`. This +is helpful in state migrations so that you don't have to import something from the greater +application which is something that should rarely happen. + +### When does my deserializer run? + +The `deserialier` that you provide in the `KeyDefinitionOptions` is used whenever your state is +retrieved from a storage service that stores its data as JSON. All disk storage services serialize +data into JSON but memory storage differs in this area across platforms. That's why it's imperative +to include a high quality JSON deserializer even if you think your object will only be stored in +memory. This can mean you might be able to drop the `*Data` class pattern for your code. Since the +`*Data` class generally represented the JSON safe version of your state which we now do +automatically through the `Jsonify` given to your in your `deserializer` method. + +### How do `StateService` storage options map to `StateDefinition`s? + +When moving state from `StateService` to the state provider pattern, you'll be asked to create a +`StateDefinition` for your state. This should be informed by the storage location that was being +used in the `StateService`. You can use the cross-reference below to help you decide how to map +between the two. + +| `StateService` Option | Desired Storage Location | Desired Web Storage Location | `StateDefinition` Equivalent | +| ------------------------------- | ------------------------ | ---------------------------- | ------------------------------------------------------------- | +| `defaultOnDiskOptions()` | Disk | Session | `new StateDefinition("state", "disk")` | +| `defaultOnDiskLocalOptions()` | Disk | Local | `new StateDefinition("state", "disk", { web: "disk-local" })` | +| `defaultOnDiskMemoryOptions()` | Disk | Session | `new StateDefinition("state", "disk")` | +| `defaultInMemoryOptions()` | Memory | Memory | `new StateDefinition("state", "memory")` | +| `defaultSecureStorageOptions()` | Disk | N/A | No migration path currently | + +#### Clarifying `defaultOnDiskMemoryOptions()` + +Despite its name, `defaultOnDiskMemoryOptions()` results in the web client storing the state in +session storage, _not_ in memory. As such, the equivalent `StateDefinition` storage location is +`"disk"`; since `"disk"` maps to session storage on the web client there is no reason to specify +`{ web: "memory" }` as a client-specific storage location if your previous state service options +used `defaultOnDiskMemoryOptions()`. + +However, we do have cases in which the `StateService` is extended in a particular client and +different storage options are defined there for a given element of state. For example, +`defaultOnDiskMemoryOptions()` is defined on the base `StateService` but `defaultInMemoryOptions()` +is defined on the web implementation. To replicate this behavior with a `StateDefinition` you would +use `new StateDefinition("state", "disk", { web: "memory" })`. + +### Should I use `ActiveUserState`? + +Probably not, `ActiveUserState` is either currently in the process of or already completed the +removal of its `update` method. This will effectively make it readonly, but you should consider +maybe not even using it for reading either. `update` is actively bad, while reading is just not as +dynamic of a API design. + +Take the following example: + +```typescript +private folderState: ActiveUserState> + +renameFolder(folderId: string, newName: string) { + // Get state + const folders = await firstValueFrom(this.folderState.state$); + // Mutate state + folders[folderId].name = await encryptString(newName); + // Save state + await this.folderState.update(() => folders); +} +``` + +You can imagine a scenario where the active user changes between the read and the write. This would +be a big problem because now user A's folders was stored in state for user B. By taking a user id +and utilizing `SingleUserState` instead you can avoid this problem by passing ensuring both +operation happen for the same user. This is obviously an extreme example where the point between the +read and write is pretty minimal but there are places in our application where the time between is +much larger. Maybe information is read out and placed into a form for editing and then the form can +be submitted to be saved. + +The first reason for why you maybe shouldn't use `ActiveUserState` for reading is for API +flexibility. Even though you may not need an API to return the data of a non-active user right now, +you or someone else may want to. If you have a method that takes the `UserId` then it can be +consumed by someone passing in the active user or by passing a non-active user. You can now have a +single API that is useful in multiple scenarios. + +The other reason is so that you can more cleanly switch users to new data when multiple streams are +in play. Consider the following example: + +```typescript +const view$ = combineLatest([ + this.folderService.activeUserFolders$, + this.cipherService.activeUserCiphers$, +]).pipe(map(([folders, ciphers]) => buildView(folders, ciphers))); +``` + +Since both are tied to the active user, you will get one emission when first subscribed to and +during an account switch, you will likely get TWO other emissions. One for each, inner observable +reacting to the new user. This could mean you try to combine the folders and ciphers of two +accounts. This is ideally not a huge issue because the last emission will have the same users data +but it's not ideal, and easily avoidable. Instead you can write it like this: + +```typescript +const view$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + throw new Error("This view should only be viewable while there is an active user."); + } + + return combineLatest([ + this.folderService.userFolders$(account.id), + this.cipherService.userCiphers$(account.id), + ]); + }), + map(([folders, ciphers]) => buildView(folders, ciphers)), +); +``` + +You have to write a little more code but you do a few things that might force you to think about the +UX and rules around when this information should be viewed. With `ActiveUserState` it will simply +not emit while there is no active user. But with this, you can choose what to do when there isn't an +active user and you could simple add a `first()` to the `activeAccount$` pipe if you do NOT want to +support account switching. An account switch will also emit the `combineLatest` information a single +time and the info will be always for the same account. + +## Structure + +![State Diagram](State_Diagram.svg)