Development Workflow Guide
This guide provides practical patterns for implementing features in the Requests & Offers project, following our established 7-layer Effect-TS architecture.
Constitution Compliance
All development activities must comply with the Requests and Offers Constitution. This workflow implements the 7 Core Principles:
- I. Evidence-Based Development: All patterns reference established implementations
- II. 7-Layer Effect-TS Architecture: This workflow enforces the standardized architecture
- III. Type Safety First: Effect-TS patterns with schema validation boundaries
- IV. Comprehensive Testing: TDD approach with Sweettest and Vitest testing
- V. Peer-to-Peer First: Holochain DHT functionality prioritized throughout
- VI. Standardized Patterns: Service Types domain template replication
- VII. Community-Driven Design: Simplified MVP approach for hAppenings.community
Overview
Our development workflow follows a standardized pattern across all domains, ensuring consistency and maintainability. Every feature implementation follows the same 7-layer structure with established patterns.
Development Cycle
1. Domain Analysis & Planning
Before implementing any feature, understand the domain structure:
# Explore existing domains for patterns
ls ui/src/lib/services/zomes/ # Service layer implementations
ls ui/src/lib/stores/ # Store layer implementations
ls ui/src/lib/composables/domain/ # Composable implementations
ls ui/src/lib/components/ # Component implementations
Use Service Types as Template: The service-types domain is 100% complete and serves as the architectural template for all new implementations.
2. Layer Implementation Order
Always implement layers in this order to maintain dependencies:
- Zome Layer (Backend) → 2. Service Layer → 3. Store Layer → 4. Composable Layer → 5. Component Layer → 6. Error Handling → 7. Testing
3. Backend Development (Zomes)
Coordinator Zome Implementation
# Create new zome structure
cd dnas/requests_and_offers/zomes/coordinator/
mkdir my_domain && cd my_domain
Pattern: Follow the coordinator/integrity separation:
#![allow(unused)] fn main() { // coordinator/my_domain/src/lib.rs use hdk::prelude::*; use my_domain_integrity::*; #[hdk_extern] pub fn create_my_entity(input: CreateMyEntityInput) -> ExternResult<Record> { // Business logic implementation } #[hdk_extern] pub fn get_my_entities() -> ExternResult<Vec<Record>> { // Query implementation } }
Testing Pattern
# Create and run zome tests
bun test:my-domain
Test all CRUD operations and business logic before moving to frontend.
4. Service Layer Implementation
Create Effect-native services with dependency injection:
// ui/src/lib/services/zomes/myDomain.service.ts
import { Context, Effect, pipe } from "effect";
import { HolochainClientService } from "../HolochainClientService.svelte";
export interface MyDomainService {
readonly createMyEntity: (
input: CreateMyEntityInput,
) => Effect.Effect<UIMyEntity, MyDomainError>;
readonly getAllMyEntities: () => Effect.Effect<UIMyEntity[], MyDomainError>;
readonly updateMyEntity: (
hash: ActionHash,
input: UpdateMyEntityInput,
) => Effect.Effect<UIMyEntity, MyDomainError>;
readonly deleteMyEntity: (
hash: ActionHash,
) => Effect.Effect<void, MyDomainError>;
}
export const MyDomainService =
Context.GenericTag<MyDomainService>("MyDomainService");
export const makeMyDomainService = Effect.gen(function* () {
const client = yield* HolochainClientService;
const createMyEntity = (input: CreateMyEntityInput) =>
Effect.gen(function* () {
const record = yield* client.callZome({
zome_name: "my_domain",
fn_name: "create_my_entity",
payload: input,
});
return createUIMyEntity(record);
}).pipe(
Effect.mapError((error) =>
MyDomainError.fromError(error, MY_DOMAIN_CONTEXTS.CREATE_MY_ENTITY),
),
Effect.withSpan("MyDomainService.createMyEntity"),
);
return { createMyEntity, getAllMyEntities, updateMyEntity, deleteMyEntity };
});
Key Patterns:
- Use
Effect.genfor complex operations with dependencies - Use
.pipefor error handling and tracing - Apply consistent error transformation with domain contexts
- Include telemetry with
Effect.withSpan
5. Store Layer Implementation
Create factory functions combining Svelte 5 Runes with Effect-TS:
// ui/src/lib/stores/myDomain.store.svelte.ts
import { Effect, Context } from "effect";
import { MyDomainService } from "$lib/services";
export const createMyDomainStore = () => {
// Reactive state with Svelte 5 Runes
let entities = $state<UIMyEntity[]>([]);
let isLoading = $state(false);
let error = $state<string | null>(null);
// Cache management
const cache = createModuleCache<ActionHash, UIMyEntity>(
"myDomain",
5 * 60 * 1000,
);
// 1. Entity Creation Helper
const createUIEntity = (record: Record): UIMyEntity | null => {
try {
const decoded = decode(record.entry);
return {
hash: record.signed_action.hashed.hash,
...decoded,
createdAt: new Date(
record.signed_action.hashed.content.timestamp / 1000,
),
};
} catch (error) {
console.error("Failed to create UI entity:", error);
return null;
}
};
// 2. Record Mapping Helper
const mapRecordsToUIEntities = (records: Record[]): UIMyEntity[] => {
return records
.map(createUIEntity)
.filter((entity): entity is UIMyEntity => entity !== null);
};
// 3. Cache Sync Helper
const syncCacheWithEntities = () => {
entities.forEach((entity) => cache.set(entity.hash, entity));
};
// 4. Event Emission Helpers
const eventEmitters = createEventEmitters<UIMyEntity>("myDomain");
// 5. Data Fetching Helper
const fetchEntities = Effect.gen(function* () {
const myDomainService = yield* MyDomainService;
isLoading = true;
error = null;
const result = yield* myDomainService.getAllMyEntities();
entities = mapRecordsToUIEntities(result);
syncCacheWithEntities();
isLoading = false;
return entities;
}).pipe(
Effect.catchAll((err) =>
Effect.sync(() => {
error = err.message;
isLoading = false;
return [];
}),
),
);
// 6. Loading State Helper
const withLoadingState = <T>(operation: Effect.Effect<T, any>) =>
Effect.gen(function* () {
isLoading = true;
error = null;
const result = yield* operation;
isLoading = false;
return result;
}).pipe(
Effect.catchAll((err) =>
Effect.sync(() => {
error = err.message;
isLoading = false;
throw err;
}),
),
);
// 7. Record Creation Helper
const handleNewRecord = (newEntity: UIMyEntity) => {
entities = [...entities, newEntity];
cache.set(newEntity.hash, newEntity);
eventEmitters.entityCreated(newEntity);
};
// 8. Status Transition Helper (if applicable)
const updateEntityStatus = (hash: ActionHash, newStatus: EntityStatus) => {
const index = entities.findIndex((e) => e.hash === hash);
if (index !== -1) {
entities[index] = { ...entities[index], status: newStatus };
cache.set(hash, entities[index]);
eventEmitters.entityUpdated(entities[index]);
}
};
// 9. Collection Processor
const processMultipleCollections = (response: ComplexResponse) => {
if (response.myEntities) {
entities = mapRecordsToUIEntities(response.myEntities);
}
// Handle related collections...
};
return {
// Reactive state
entities: () => entities,
isLoading: () => isLoading,
error: () => error,
// Actions
fetchEntities,
createEntity: (input: CreateMyEntityInput) =>
withLoadingState(
Effect.gen(function* () {
const myDomainService = yield* MyDomainService;
const newEntity = yield* myDomainService.createMyEntity(input);
handleNewRecord(newEntity);
return newEntity;
}),
),
// Helpers (exposed for composables)
createUIEntity,
mapRecordsToUIEntities,
eventEmitters,
};
};
Implementation Notes:
- Always implement all 9 standardized helper functions
- Use Svelte 5 Runes for reactive state
- Integrate Effect-TS for async operations
- Include proper error handling and loading states
- Implement cache management with TTL
6. Composable Layer Implementation
Create composables that abstract business logic from components:
// ui/src/lib/composables/domain/my-domain/useMyDomainManagement.svelte.ts
import { Effect } from "effect";
import { useErrorBoundary } from "$lib/composables";
import { createMyDomainStore } from "$lib/stores";
import { MY_DOMAIN_CONTEXTS } from "$lib/errors";
export function useMyDomainManagement() {
// Create domain store
const store = createMyDomainStore();
// Error boundaries for different operations
const loadingErrorBoundary = useErrorBoundary({
context: MY_DOMAIN_CONTEXTS.FETCH_MY_ENTITIES,
enableLogging: true,
enableFallback: true,
maxRetries: 2,
});
const createErrorBoundary = useErrorBoundary({
context: MY_DOMAIN_CONTEXTS.CREATE_MY_ENTITY,
enableLogging: true,
maxRetries: 1,
});
// Enhanced operations with error handling
const loadEntities = async () => {
await loadingErrorBoundary.execute(store.fetchEntities, []);
};
const createEntity = async (input: CreateMyEntityInput) => {
await createErrorBoundary.execute(store.createEntity(input));
};
// Reactive state that components can use
let state = $state({
entities: store.entities,
isLoading: store.isLoading,
error: store.error,
loadingError: () => loadingErrorBoundary.state.error,
createError: () => createErrorBoundary.state.error,
});
return {
// State
state,
// Actions
loadEntities,
createEntity,
// Error boundaries
loadingErrorBoundary,
createErrorBoundary,
// Store methods (if needed directly)
store,
};
}
7. Component Layer Implementation
Create Svelte components that use composables for business logic:
<!-- ui/src/lib/components/my-domain/MyDomainGrid.svelte -->
<script>
import { useMyDomainManagement } from '$lib/composables';
import ErrorDisplay from '$lib/components/shared/ErrorDisplay.svelte';
const {
state,
loadEntities,
createEntity,
loadingErrorBoundary,
createErrorBoundary
} = useMyDomainManagement();
// Load entities on mount
$effect(() => {
loadEntities();
});
</script>
<!-- Error Displays -->
{#if state.loadingError()}
<ErrorDisplay
error={state.loadingError()}
context="Loading entities"
variant="inline"
showRetry={true}
onretry={() => loadEntities()}
ondismiss={() => loadingErrorBoundary.clearError()}
/>
{/if}
<!-- Loading State -->
{#if state.isLoading()}
<div>Loading entities...</div>
{/if}
<!-- Entity Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each state.entities() as entity (entity.hash)}
<MyDomainCard {entity} />
{/each}
</div>
8. Error Handling Implementation
Create domain-specific error handling:
// ui/src/lib/errors/my-domain.errors.ts
import { Data } from "effect";
export class MyDomainError extends Data.TaggedError("MyDomainError")<{
readonly message: string;
readonly cause?: unknown;
readonly context?: string;
readonly entityId?: string;
readonly operation?: string;
}> {
static fromError(
error: unknown,
context: string,
entityId?: string,
operation?: string,
): MyDomainError {
const message = error instanceof Error ? error.message : String(error);
return new MyDomainError({
message,
cause: error,
context,
entityId,
operation,
});
}
}
// Add to error-contexts.ts
export const MY_DOMAIN_CONTEXTS = {
CREATE_MY_ENTITY: "Failed to create entity",
GET_MY_ENTITY: "Failed to get entity",
UPDATE_MY_ENTITY: "Failed to update entity",
DELETE_MY_ENTITY: "Failed to delete entity",
FETCH_MY_ENTITIES: "Failed to fetch entities",
} as const;
9. Testing Implementation
Create comprehensive tests for each layer:
# Backend tests (Sweettest, requires Nix)
nix develop --command cargo test --manifest-path tests/sweettest/Cargo.toml
# Frontend unit tests
cd ui && bun test:unit -- my-domain
# Integration tests
cd ui && bun test:integration -- my-domain
Test Structure:
// ui/tests/unit/stores/myDomain.store.test.ts
import { describe, it, expect } from "vitest";
import { createMyDomainStore } from "$lib/stores";
describe("MyDomain Store", () => {
it("should create UI entities correctly", () => {
const store = createMyDomainStore();
const mockRecord = createMockRecord();
const entity = store.createUIEntity(mockRecord);
expect(entity).toBeDefined();
expect(entity.hash).toBe(mockRecord.signed_action.hashed.hash);
});
it("should handle all 9 helper functions", () => {
const store = createMyDomainStore();
// Test each helper function
expect(typeof store.createUIEntity).toBe("function");
expect(typeof store.mapRecordsToUIEntities).toBe("function");
// ... test all 9 helpers
});
});
Best Practices
Effect-TS Patterns
When to Use Effect.gen vs .pipe
// Use Effect.gen for:
// 1. Injecting dependencies
const serviceOperation = Effect.gen(function* () {
const service = yield* MyDomainService;
const result = yield* service.getAllEntities();
return result;
});
// 2. Sequential operations with error handling
const complexOperation = Effect.gen(function* () {
const step1 = yield* operation1();
const step2 = yield* operation2(step1);
return yield* operation3(step2);
});
// Use .pipe for:
// 1. Error handling
const withErrorHandling = operation.pipe(
Effect.mapError((error) => MyDomainError.fromError(error, context)),
Effect.catchAll(fallbackOperation),
);
// 2. Transformations
const transformed = operation.pipe(
Effect.map((result) => transformResult(result)),
Effect.withSpan("operation-name"),
);
Store Patterns
Always Implement the 9 Helper Functions
- createUIEntity: Convert Holochain Record to UI entity
- mapRecordsToUIEntities: Map array of Records with null safety
- createCacheSyncHelper: Sync cache with state arrays
- createEventEmitters: Standardized event broadcasting
- createEntityFetcher: Higher-order fetching with loading state
- withLoadingState: Wrap operations with loading/error patterns
- createRecordCreationHelper: Process new records and update cache
- createStatusTransitionHelper: Handle status changes atomically
- processMultipleRecordCollections: Handle complex responses
Component Patterns
Use Composables for Business Logic
<script>
// ✅ Good - Use composable for business logic
const { state, actions, errorBoundaries } = useMyDomainManagement();
// ❌ Avoid - Direct store usage in components
// const store = createMyDomainStore();
</script>
Consistent Error Display
<!-- Always include error displays for user feedback -->
{#if errorBoundary.state.error}
<ErrorDisplay
error={errorBoundary.state.error}
context="Operation description"
showRetry={true}
onretry={() => retryOperation()}
ondismiss={() => errorBoundary.clearError()}
/>
{/if}
Common Patterns
Domain Implementation Checklist
- Zome implemented with coordinator/integrity pattern
- Service layer with Effect-TS and dependency injection
- Store layer with all 9 helper functions
- Composable layer abstracting business logic
- Component layer using composables
- Error handling with domain-specific errors and contexts
- Tests covering all layers (backend + frontend)
- Documentation updated with new domain
Performance Considerations
- Cache Management: Implement TTL-based caching for frequently accessed data
- Event Bus: Use event emitters for cross-domain communication
- Loading States: Always provide loading feedback for async operations
- Error Recovery: Implement retry strategies and fallback mechanisms
Maintenance Guidelines
- Follow Service Types: Use as reference implementation for all patterns
- Consistent Naming: Follow established naming conventions across domains
- Documentation: Update guides when adding new patterns
- Testing: Maintain test coverage for all new functionality
Next Steps
- Study Complete Examples: Examine service-types, requests, and offers domains
- Practice Implementation: Try implementing a simple domain following this workflow
- Read Architecture Guides: Architectural Patterns and Effect-TS Primer
- Join Community: Connect with other developers on Discord
This workflow ensures consistency, maintainability, and scalability across all feature implementations in the project.