State Persistence
Two slices are persisted to localStorage through a shared meta-reducer factory:
auth and marketplaces. Everything else is re-fetched on load.
The meta-reducer factory
src/app/core/reducers/persistent-store.meta-reducer-factory.ts
export const storageMetaReducer = <S, A extends Action = Action>(
saveKeys: string[],
localStorageKey: string,
storageService: PersistentStateManager,
) => {
let onInit = true;
return (reducer: ActionReducer<S, A>) => (state: S, action: A): S => {
const nextState = reducer(state, action);
if (onInit) { (1)
onInit = false;
const savedState = storageService.getSavedState(localStorageKey);
return merge(nextState, savedState); (2)
}
const stateToSave = pick(nextState, saveKeys); (3)
storageService.setSavedState(stateToSave, localStorageKey);
return nextState;
};
};
| 1 | The very first action through the reducer is treated as hydration. |
| 2 | lodash.merge deep-merges the saved snapshot over the freshly computed
state, so persisted values win. |
| 3 | On every subsequent action, only the listed keys are written back. |
PersistentStateManager
src/app/core/pyb/services/persistent-state-manager.service.ts
@Injectable({ providedIn: 'root' })
export class PersistentStateManager {
private storageKeys = {};
setSavedState(state: any, localStorageKey: string): void {
this.storageKeys[localStorageKey] = true;
localStorage.setItem(localStorageKey, JSON.stringify(state));
}
getSavedState(localStorageKey: string): any {
return JSON.parse(localStorage.getItem(localStorageKey));
}
clear() {
for (const key in this.storageKeys) {
if (typeof key === 'string') { localStorage.removeItem(key); }
}
this.storageKeys = {};
}
}
clear() only removes keys this instance has written. It is called from
AuthEffects.logout$, which is followed by an explicit
localStorage.removeItem(authTokenKey) and a full page reload — belt and braces.
Persisted slices
auth
Configured in AuthModule:
{ provide: AUTH_LOCAL_STORAGE_KEY, useValue: '__auth_storage__' },
{ provide: AUTH_STORAGE_KEYS, useValue: ['auth'] },
{
provide: AUTH_CONFIG_TOKEN,
deps: [AUTH_STORAGE_KEYS, AUTH_LOCAL_STORAGE_KEY, PersistentStateManager],
useFactory: getAuthPersistentStateFactory,
},
Storage key |
|
Saved keys |
|
Not saved |
|
This is what makes AuthGuard able to read isLoggedIn from the store on a cold
load: the slice is rehydrated before the router evaluates guards.
marketplaces
Configured in MarketplacesFeatureModule:
Storage key |
|
Saved keys |
|
Not saved |
Loading and error flags |
The marketplace catalog is large, effectively static reference data, and needed early to render the marketplace pages — caching it removes a blocking request from every page load.
The session token
The token is not part of the persisted slice. It is written directly by
AuthEffects.login$ and register$ to localStorage[environment.authTokenKey],
and read by AuthService.getAccessToken() and AuthEffects.init$.
Other localStorage keys
| Key | Owner / purpose |
|---|---|
|
The session token. |
|
Persisted |
|
Persisted |
|
Replay guard for DocuSign returns. See DocuSign Contract Flows. |
|
Plaid Link token, read and removed by |
Caveats
|
Hydration merges, it does not replace
|
|
The
onInit flag is per-factory-call
|
|
Every action writes to
localStorageThe save branch runs on every dispatched action, serializing the picked keys each time. It is acceptable at the current slice sizes, but persisting a large or rapidly changing slice would put JSON serialization on the hot path. |
|
Persisted state is never trusted for authorization
On boot, |