State Management Documentation
This document details the state management approach in the Requests and Offers application, which uses Svelte stores integrated with Effect TS for robust error handling and data flow management.
Store Architecture
The application uses a standardized store architecture located in ui/src/lib/stores/, with factory functions creating
Effect-integrated Svelte stores.
Store Organization
/stores/
├── serviceTypes.store.svelte.ts # Service types state
├── requests.store.svelte.ts # Requests state
├── offers.store.svelte.ts # Offers state
├── users.store.svelte.ts # User profiles state
├── organizations.store.svelte.ts # Organizations state
├── administration.store.svelte.ts # Admin functionality state
├── mediums_of_exchange.store.svelte.ts # Medium of exchange state
├── hrea.store.svelte.ts # hREA integration state
└── storeEvents.ts # Event bus definitions
The 9-Helper Function Pattern
Stores implement a standardized architecture using nine helper functions for consistent patterns:
-
createUIEntity(): Transforms Holochain records into UI-friendly entitiesconst createUIEntity = (record: Record): UIEntity => ({ id: encodeHashToBase64(record.actionHash), // Transform record entry data to UI model }); -
mapRecordsToUIEntities(): Handles bulk record mapping with error handlingconst mapRecordsToUIEntities = ( records: Record[], ): E.Effect<UIEntity[], DomainError> => E.gen(function* () { // Map and validate records to UI entities }); -
createCacheSyncHelper(): Synchronizes cache with application stateconst createCacheSyncHelper = () => { return { syncCacheToState: () => { /* Implementation */ }, invalidateCache: () => { /* Implementation */ }, }; }; -
createEventEmitters(): Standardized event emission patternsconst createEventEmitters = () => { return { emitEntityCreated: (entity: UIEntity) => { /* Implementation */ }, emitEntityUpdated: (entity: UIEntity) => { /* Implementation */ }, // Other event emitters }; }; -
createEntitiesFetcher(): Data fetching with state updatesconst createEntitiesFetcher = (service: DomainService, state: State) => { return { fetchAllEntities: () => { /* Implementation */ }, fetchEntityById: (id: string) => { /* Implementation */ }, }; }; -
withLoadingState(): Loading state management for consistent UXconst withLoadingState = async <T>( operation: () => Promise<T>, state: { loading: boolean }, ): Promise<T> => { state.loading = true; try { return await operation(); } finally { state.loading = false; } }; -
createRecordCreationHelper(): Record creation operation patternsconst createRecordCreationHelper = ( service: DomainService, state: State, ) => { return { createEntity: (input: EntityInput) => { /* Implementation */ }, // Other creation methods }; }; -
createStatusTransitionHelper(): Status transition managementconst createStatusTransitionHelper = ( service: DomainService, state: State, ) => { return { approveEntity: (id: string) => { /* Implementation */ }, rejectEntity: (id: string) => { /* Implementation */ }, // Other status transitions }; }; -
processMultipleRecordCollections(): Complex collection processingconst processMultipleRecordCollections = <T>( collections: Record[][], processor: (records: Record[]) => T[], ): T[] => { // Implementation for handling multiple record collections };
Store Factory Pattern
Stores are created using a factory pattern that returns store objects directly:
export const createDomainStore = () => {
// Reactive state with Svelte 5 Runes
let entities = $state<UIDomainEntity[]>([]);
let isLoading = $state(false);
let error = $state<string | null>(null);
// Cache management
const cache = createModuleCache<ActionHash, UIDomainEntity>(
"domain",
5 * 60 * 1000,
);
// Implement all 9 helper functions
const createUIEntity = (record: Record): UIDomainEntity | null => {
// Implementation
};
const mapRecordsToUIEntities = (records: Record[]): UIDomainEntity[] => {
// Implementation
};
// ... other helper functions
// Main operations using Effect-TS
const fetchEntities = Effect.gen(function* () {
const domainService = yield* DomainService;
const result = yield* domainService.getAllEntities();
entities = mapRecordsToUIEntities(result);
return entities;
});
return {
// Reactive state accessors
entities: () => entities,
isLoading: () => isLoading,
error: () => error,
// Operations
fetchEntities,
// ... other operations
// Helper functions
createUIEntity,
mapRecordsToUIEntities,
// ... other helpers
};
};
Lazy Initialization with Proxy Pattern
Stores use a proxy pattern for lazy initialization to prevent circular dependencies:
let _domainStore: DomainStore | undefined;
export const getDomainStore = (): DomainStore => {
if (!_domainStore) {
_domainStore = pipe(
createDomainStore(),
E.provide(DomainServiceLive),
E.provide(CacheServiceLive),
E.runSync,
);
}
return _domainStore;
};
export const domainStore = new Proxy({} as DomainStore, {
get: (target, prop) => {
return getDomainStore()[prop as keyof DomainStore];
},
});
Cross-Store Communication with Event Bus
Stores communicate using an event bus pattern defined in storeEvents.ts:
export const EntityCreatedEvent = EventTag<{
type: "entity_created";
payload: {
domain: "service_types" | "requests" | "offers" | "users" | "organizations";
entity: any;
};
}>();
export const EntityUpdatedEvent = EventTag<{
type: "entity_updated";
payload: {
domain: "service_types" | "requests" | "offers" | "users" | "organizations";
entity: any;
};
}>();
// Other events...
Stores can subscribe to events from other stores:
$effect(() => {
const subscription = pipe(
EventBus.subscribe(EntityCreatedEvent),
S.filter((event) => event.payload.domain === "service_types"),
S.tap((event) => {
// Handle the event in this store
}),
S.runDrain,
);
return () => subscription.interrupt();
});
Data Flow
The application follows a unidirectional data flow pattern:
- User Interaction: Component triggers an action via store method
- Store Processing: Store calls appropriate service method using Effect TS
- Service Execution: Service communicates with Holochain backend
- Data Return: Results flow back through service to store
- State Update: Store updates its state using Svelte 5 reactivity
- UI Update: Components reactively update based on store state changes
- Cross-Store Updates: Event bus notifies other stores of relevant changes
Implementation Status
| Store | Implementation Status | Notes |
|---|---|---|
| serviceTypes.store | ✅ Complete | Fully standardized - Reference implementation |
| requests.store | ✅ Complete | Fully standardized with 9-helper pattern |
| offers.store | ✅ Complete | Fully standardized with 9-helper pattern |
| users.store | ✅ Complete | Converted to Effect architecture with 9-helper pattern |
| organizations.store | ✅ Complete | Converted to Effect architecture with 9-helper pattern |
| administration.store | ✅ Complete | Converted to Effect architecture with 9-helper pattern |
Best Practices
- Always use Effect for async operations: Avoid mixing Promises and Effects
- Implement proper error handling: Use tagged errors with context
- Maintain store isolation: Use event bus for cross-store communication
- Follow the 9-helper pattern: For consistent implementation
- Add proper type definitions: For all store state and methods
- Document store interfaces: For developer reference
- Add comprehensive tests: For store functionality