Skip to main content

Services

Services are the primary containers for business logic in Sagun. They encapsulate related functionality and manage their own lifecycle.

Service Hierarchy

Dependency (base class)
└── Service (daemons, lifecycle)
└── CustomService (business logic)

Creating a Service

import { Service, operation, daemon } from '@iiiristram/sagun';
import { call } from 'typed-redux-saga';

class ProductService extends Service {
// REQUIRED: unique identifier
toString() {
return 'ProductService';
}

@operation
@daemon()
*fetchProducts(category: string) {
const products = yield* call(api.getProducts, category);
return products;
}
}

Service Lifecycle

Initialization

Services are initialized using the useService hook:

function ProductPage() {
const di = useDI();
const service = di.createService(ProductService);
di.registerService(service);

// Calls service.run() and service.destroy() on component unmount
const { operationId } = useService(service);

return (
<Operation operationId={operationId}>
{() => <ProductList />}
</Operation>
);
}

Custom Run and Destroy

Override run and destroy for custom lifecycle logic:

class ProductService extends Service<[string], Product[]> {
private category: string = '';

toString() { return 'ProductService'; }

*run(category: string) {
// IMPORTANT: call super.run() first
yield* call([this, super.run]);

this.category = category;

// Custom initialization
yield* call(this.fetchProducts, category);

return this.getProducts();
}

*destroy() {
// IMPORTANT: call super.destroy()
yield* call([this, super.destroy]);

// Custom cleanup
this.category = '';
}

@operation
*fetchProducts(category: string) {
return yield* call(api.getProducts, category);
}
}

Service Status

service.getStatus(); // 'unavailable' | 'ready'
service.getUUID(); // Unique instance ID

Daemon Modes

The @daemon decorator makes methods callable via Redux actions:

import { daemon, DaemonMode } from '@iiiristram/sagun';

class SearchService extends Service {
toString() { return 'SearchService'; }

// DaemonMode.Sync (default) — wait for previous call to complete
@daemon()
*loadPage(page: number) { /* ... */ }

// DaemonMode.Last — cancel previous call, run latest (takeLatest)
@daemon(DaemonMode.Last)
*search(query: string) { /* ... */ }

// DaemonMode.Every — run all calls in parallel (takeEvery)
@daemon(DaemonMode.Every)
*trackEvent(event: string) { /* ... */ }

// DaemonMode.Schedule — periodic execution
@daemon(DaemonMode.Schedule, 30000) // Every 30 seconds
*pollUpdates() { /* ... */ }
}

Calling Service Methods

From Components (via actions)

function SearchForm() {
const { actions } = useServiceConsumer(SearchService);

return (
<input onChange={(e) => actions.search(e.target.value)} />
);
}

From Other Sagas

class OrderService extends Service {
@operation
*createOrder(items: Item[]) {
const order = yield* call(api.createOrder, items);

// Direct call to another service method
yield* call(this._analytics.trackEvent, 'order_created');

return order;
}
}

Best Practices

1. Single Responsibility

Each service should handle one domain:

// ✅ Good
class UserService extends Service { /* user operations */ }
class AuthService extends Service { /* auth operations */ }

// ❌ Bad
class UserAndAuthService extends Service { /* mixed */ }

2. Use Dependency Injection

class OrderService extends Service {
constructor(
@inject(OperationService) os: OperationService,
@inject(UserService) private userService: UserService,
@inject(CartService) private cartService: CartService,
) {
super(os);
}
}

3. Keep Methods Focused

class ProductService extends Service {
// ✅ Focused methods
@operation
*fetchProduct(id: string) { /* ... */ }

@operation
*fetchProductReviews(id: string) { /* ... */ }

// ❌ Avoid god methods
*fetchProductWithReviewsAndRelatedAndCart() { /* ... */ }
}