Networking
The Hosanna UI Framework provides a robust networking layer that enables cross-platform HTTP communication. This documentation covers the key components and usage patterns for making network requests in your Hosanna applications.
Live Example
Ui And Interaction Networking
@view('UiAndInteractionNetworking')
export class UiAndInteractionNetworkingView extends BaseExampleScreenView<UiAndInteractionNetworkingState> {
@state loading = '';
@state result = '';
protected getViews(): ViewStruct<ViewState>[] {
// --- edit below ---
return [
VGroup([
Button({ text: 'Fetch Posts' })
.isInitialFocus(true)
.onClick(() => this.fetchExample()),
Label({ text: `State: ${this.loading}` }),
Label({ text: this.result, width: 1000, wrap: true }),
]).itemSpacing(16)
.translation([50, 50])
]
// --- edit above ---
}
private fetchExample() {
this.loading = 'loading';
this.result = '';
const url = 'https://jsonplaceholder.typicode.com/posts/1';
return this.dispatch<IHsFetchResponse>(HttpMethod.Get, { url })
.then((response) => {
this.loading = 'loaded';
this.result = JSON.stringify(response.json);
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch((reason: any) => {
this.loading = 'error';
this.result = String(reason);
});
}
}Trigger a request and observe how the view updates from the async response.
HsFetch
The core of Hosanna's networking capabilities is the HsFetch class, which provides a platform-agnostic implementation of the Fetch API. It works across Roku, Apple TV, Android TV, Samsung TV, iOS, Android, and Web through the active platform adapter.
Basic Usage
const hosannaFetch = new HsFetch();
const response = await hosannaFetch.fetch(url, options);
Supported HTTP Methods
The framework supports all standard HTTP methods through the HttpMethod enum:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
HTTP Commands
Hosanna provides a high-level abstraction for HTTP requests through the HttpCommands class. This integrates with Hosanna's async command system for better state management and testing capabilities.
Making HTTP Requests
In a Hosanna view, you can make HTTP requests using the dispatch method:
// GET request
this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/data'
})
.then((response) => {
// Handle response
})
.catch((error) => {
// Handle error
});
// POST request with body
this.dispatch<IHsFetchResponse>(Http.Post, {
url: 'https://api.example.com/data',
options: {
body: JSON.stringify({ key: 'value' }),
headers: {
'Content-Type': 'application/json'
}
}
});
Post-Processing Responses
You can automatically process HTTP responses using the postProcessFunction option. This function is invoked automatically after a successful HTTP response, allowing you to update UI state, show notifications, or cache data without promise chains.
Use postProcessFunction when you want automatic post-processing without promise chains. It's especially useful for updating UI state immediately after successful responses.
On platforms that use background tasks (like Roku), HsFetchOptions.postProcessFunction and transformFunction are typed as AsyncFunctionPointer. You must pass exported functions (no inline callbacks or .bind(this)) so Hosanna can serialize the function pointer and invoke it safely from a Roku Task or other background thread.
Basic Usage
Define a post-processing function in a separate module and export it, then pass it to your HTTP command. The function always receives a single IHsFetchResponse parameter:
// apiHandlers.ts
export function handleApiResponse(response: IHsFetchResponse) {
console.info('API call completed:', response);
}
// In your view class method
import { handleApiResponse } from './apiHandlers';
// Use with HTTP command
this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/users',
headers: { 'Authorization': 'Bearer token' },
// Exported function reference; receives IHsFetchResponse only
postProcessFunction: handleApiResponse
});
Inline Functions Will Be Rejected
Inline arrow functions are convenient, but they are not supported where AsyncFunctionPointer is used (such as HsFetchOptions.postProcessFunction and transformFunction). The linter will report a diagnostic if you try this pattern:
// Incorrect: this looks convenient but will be rejected
this.dispatch<IHsFetchResponse>(Http.Post, {
url: 'https://api.example.com/submit',
body: formData,
postProcessFunction: (result: IHsFetchResponse) => {
if (result.ok) {
this.showSuccessMessage();
} else {
this.showErrorMessage(result.statusText);
}
}
});
Export the function (or a factory that returns a closure) at module scope and pass that exported function to postProcessFunction or transformFunction. Hosanna's linter flags inline or .bind(this) callbacks in these slots so they can be safely executed in a Roku Task or other background thread.
With Promise Chains
Traditional promise handling still works and can be combined with postProcessFunction:
export function cacheResponse(response: IHsFetchResponse) {
console.info('Automatic post-processing', response.status);
}
this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/data',
postProcessFunction: cacheResponse
})
.then((response: IHsFetchResponse) => {
// Traditional promise handling still works
console.info('Response:', response);
});
- postProcessFunction: Optional function that receives
IHsFetchResponseas its first argument - Automatic invocation: Called automatically after successful HTTP responses
- Error isolation: Post-processing errors are logged but don't fail the HTTP request
- Works with mocks: Post-processing also works with mock responses in development
Use Cases
AsyncFunctionPointer cannot be inline or anonymous functions — they must be exported functions defined at module scope.
Update UI state after data fetch:
// Correct: export the function first
export function updateUserProfile(response: IHsFetchResponse) {
// Update your app's state using services or stores that are safe to access
console.info('Profile loaded', response.json);
}
this.dispatch<IHsFetchResponse>(Http.Get, {
url: '/api/user/profile',
postProcessFunction: updateUserProfile // Receives IHsFetchResponse only
});
Show notifications:
// Correct: export the function first
export function handleSubmitResponse(response: IHsFetchResponse) {
if (response.ok) {
console.info('Success!');
}
}
this.dispatch<IHsFetchResponse>(Http.Post, {
url: '/api/submit',
body: formData,
postProcessFunction: handleSubmitResponse
});
Cache responses:
// Correct: export the function (no instance context needed)
export function cacheResponse(response: IHsFetchResponse) {
localStorage.setItem('cachedData', JSON.stringify(response.json));
}
this.dispatch<IHsFetchResponse>(Http.Get, {
url: '/api/data',
postProcessFunction: cacheResponse // Use exported function directly
});
- Must be exported functions:
AsyncFunctionPointercannot be inline or anonymous functions — they must be exported functions defined at module scope. - Parameter limit: Functions support up to 5 parameters maximum
- Module scope: Functions must be accessible from module scope (not nested functions, closures, or inline arrow functions)
- Serialization: Functions are serialized as string pointers, so closures and
thiscontext are not captured directly - Resolution: Functions must be resolved using
hs_resolveAsyncFunctionPointer()before calling (handled automatically by the framework)
Passing function pointers across tasks
You can pass a function pointer to be executed in a background task using AsyncFunctionPointer. Functions are automatically serialized as string pointers and resolved when invoked, allowing safe execution across async boundaries.
import { processRows } from './transformFunctions';
this.dispatch<IHsFetchResponse>(Http.Get, {
url: 'https://api.example.com/data',
transformFunction: processRows // Function reference, not object
});
The function is serialized as an AsyncFunctionPointer (string pointer) when dispatched. The runtime automatically resolves it using hs_resolveAsyncFunctionPointer() before execution in the background task.
Type signature:
// hosanna-api
declare type AsyncFunctionPointer = (...args: any[]) => any;
declare function hs_resolveAsyncFunctionPointer(ptr: AsyncFunctionPointer): Function;
- Must be exported functions:
AsyncFunctionPointercannot be inline or anonymous functions — they must be exported functions defined at module scope. - Functions support up to 5 parameters maximum
- Functions must be accessible from module scope (not nested functions, closures, or inline arrow functions)
- Functions are serialized as string pointers, so closures and
thiscontext are not captured directly
Features
1. Cancellation Support
Requests can be cancelled using the HsCancellationToken:
const cancellationToken = new HsCancellationToken();
hosannaFetch.fetch(url, options, cancellationToken);
// Cancel the request when needed
cancellationToken.cancel();
2. Mock Responses
For testing and development purposes, Hosanna supports mock responses through the developer utilities:
interface IDeveloperUtils {
getMockResponseForHttpRequest(method: HttpMethod, url: string): IHsFetchResponse | undefined;
}
3. Response Handling
The IHsFetchResponse interface provides a standardized way to handle responses:
interface IHsFetchResponse {
ok: boolean;
json: any;
text: string;
status: number;
statusText: string;
headers: Record<string, string>;
ipAddress: string;
}
- ok:
trueif status is 200-299,falseotherwise - json: Parsed JSON if Content-Type is
application/json - text: Raw text response if not JSON
- status: HTTP status code
- statusText: HTTP status text
- headers: Response headers as key-value pairs
- ipAddress: Target IP address of the request
Best Practices
-
Error Handling: Always implement proper error handling for network requests:
this.dispatch<IHsFetchResponse>(Http.Get, { url })
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json;
})
.catch((error) => {
console.error('Network request failed:', error);
}); -
Request Cancellation: Use cancellation tokens for long-running requests that might need to be cancelled.
-
Headers: Set appropriate headers for your requests, especially for content types and authentication:
const options = {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token'
}
}; -
Response Type Handling: Use TypeScript types to ensure type safety when handling responses:
interface MyDataType {
id: number;
name: string;
}
this.dispatch<IHsFetchResponse>(Http.Get, { url })
.then((response) => response.json as MyDataType)
.then((data) => {
// TypeScript knows the shape of 'data'
}); -
Post-Processing Functions: Use
postProcessFunctionfor automatic response handling when you want cleaner, more declarative code. Use promise chains when you need complex error handling or multiple async operations:export function updateUiFromResponse(response: IHsFetchResponse) {
console.info('Received data', response.json);
}
// Use postProcessFunction for simple, automatic processing
this.dispatch<IHsFetchResponse>(Http.Get, {
url: '/api/data',
postProcessFunction: updateUiFromResponse
});
// Use promise chains for complex logic
this.dispatch<IHsFetchResponse>(Http.Get, { url })
.then((response) => {
if (!response.ok) throw new Error('Failed');
return processData(response.json);
})
.then((data) => this.updateUI(data))
.catch((error) => this.handleError(error));
Post-processing function errors are logged but don't fail the HTTP request. This ensures your network operations succeed even if post-processing encounters issues.
Platform-Specific Considerations
The networking layer automatically handles platform-specific implementations:
- Web: Uses the native Fetch API
- Roku: Uses
roUrlTransferunder the hood - Apple TV, Android TV, iOS, and Android: Use native platform networking through the platform target
- Samsung TV: Uses the web-based TV target networking path
Example Implementation
Here's a complete example of implementing a data fetching view:
@view('DataFetchingView')
export class DataFetchingView extends BaseView<ViewState> {
private loadData() {
const url = 'https://api.example.com/data';
return this.dispatch<IHsFetchResponse>(Http.Get, {
url,
options: {
headers: {
'Content-Type': 'application/json'
}
}
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json;
})
.catch((error) => {
console.error('Failed to fetch data:', error);
// Handle error in UI
});
}
}