### Bootstrap sagun Application Source: https://github.com/iiiristram/sagun/blob/master/README.md Example of setting up the sagun library in a React application using Redux and Redux Saga. It includes initializing services, configuring the store, and rendering the root component. ```tsx // bootstrap.tsx import { applyMiddleware, createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import createSagaMiddleware from 'redux-saga'; import React from 'react'; import ReactDOM from 'react-dom'; import { ComponentLifecycleService, OperationService, asyncOperationsReducer, Root, useOperation, } from '@iiiristram/sagun'; import { call } from 'typed-redux-saga'; import App from './your-app-path.js'; const sagaMiddleware = createSagaMiddleware(); const store = applyMiddleware(sagaMiddleware)(createStore)( combineReducers({ asyncOperations: asyncOperationsReducer, }) ); // set up destination for storage useOperation.setPath(state => state.asyncOperations); // two basic services which provide library workflow const operationService = new OperationService(); const componentLifecycleService = new ComponentLifecycleService(operationService); sagaMiddleware.run(function* () { yield* call(operationService.run); yield* call(componentLifecycleService.run); }); ReactDOM.render( , window.document.getElementById('app') ); ``` -------------------------------- ### Full Service Definition with Decorators Source: https://github.com/iiiristram/sagun/blob/master/README.md Presents a comprehensive example of a Sagun service, including optional constructor parameters, lifecycle methods (`run`, `destroy`), and methods decorated for framework integration (`@daemon`, `@operation`). ```typescript class MyService extends Service { // OPTIONAL private _someOtherService: MyOtherService // REQUIRED toString() { return 'MyService' } // OPTIONAL constructor( @inject(OperationService) operationService: OperationService, @inject(MyOtherService) someOtherService: MyOtherService ) { super(operationService) this._someOtherService = someOtherService; } // OPTIONAL *run(...args: MyArgs) { yield call([this, super.run]); yield call(this._someOtherService.run) const result: MyRes = ...; return result; } // OPTIONAL *destroy(...args: MyArgs) { yield call([this, super.run]); yield call(this._someOtherService.destroy) } @daemon() // make method reachable by redux action @operation // write result to redux state *foo(a: Type1, b: Type2) { // your custom logic } } ``` -------------------------------- ### Customize Service Initialization and Cleanup Source: https://github.com/iiiristram/sagun/blob/master/README.md Explains how to override the `run` and `destroy` methods in a service to customize its initialization and cleanup logic. This allows for complex setup and teardown procedures. ```typescript class MyService extends Service { toString() { return 'MyService' } *run(...args: MyArgs) { // IMPORTANT yield call([this, super.run]); const result: MyRes = ...; return result; } *destroy(...args: MyArgs) { // IMPORTANT yield call([this, super.destroy]); ... } ... } ``` -------------------------------- ### Register and Create Services in React Context Source: https://github.com/iiiristram/sagun/blob/master/README.md Provides an example of using the dependency injection container within a React component. It shows how to register custom dependencies and create service instances. ```typescript // somewhere in react components ... const di = useDI(); // register custom dependency by key di.registerDependency(KEY, DATA); // create instance of Dependency resolving all its dependencies const service1 = context.createService(Service1); const custom = context.createService(CustomClass); // register Dependency instancies so they could be resolved as dependencies for other services context.registerService(service1); context.registerService(custom); // create service with resolved dependencies after their registration const service2 = context.createService(Service2); ... ``` -------------------------------- ### Client-Side Hydration Setup with Sagun Source: https://github.com/iiiristram/sagun/blob/master/README.md Initializes the client-side application by hydrating the server-rendered HTML. It re-creates the Redux store, sets up Sagun services with the SSR context, and attaches the application to the DOM, ensuring state consistency between server and client. ```typescript // client.ts import { Root, OperationService, ComponentLifecycleService } from '@iiiristram/sagun'; import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import { Provider } from 'react-redux'; import { call } from 'redux-saga/effects'; import ReactDOM from 'react-dom'; // Assuming asyncOperationsReducer and App are defined elsewhere // import asyncOperationsReducer from './reducers'; // import App from './App'; // Placeholder for actual reducer and App component const asyncOperationsReducer = (state = {}, action) => state; const App = () => null; // Ensure global variables are defined or handle their absence declare global { interface Window { __STATE_FROM_SERVER__: any; __SSR_CONTEXT__: any; } } const sagaMiddleware = createSagaMiddleware(); const store = applyMiddleware(sagaMiddleware)(createStore)( asyncOperationsReducer, window.__STATE_FROM_SERVER__ ); const operationService = new OperationService({ hash: window.__SSR_CONTEXT__ }); const componentLifecycleService = new ComponentLifecycleService(operationService); sagaMiddleware.run(function* () { yield* call(operationService.run); yield* call(componentLifecycleService.run); }); useOperation.setPath(state => state); ReactDOM.hydrate( , window.document.getElementById('app'), ); ``` -------------------------------- ### Server-Side Rendering Setup with Sagun Source: https://github.com/iiiristram/sagun/blob/master/README.md Configures the server to render React components asynchronously using Sagun. It initializes services, sets up the Redux store with sagas, and renders the application to a string, injecting initial state and SSR context into the HTML response. ```typescript // server.ts import { Root, OperationService, ComponentLifecycleService } from '@iiiristram/sagun'; import { renderToStringAsync } from '@iiiristram/sagun/server'; import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import { Provider } from 'react-redux'; import { call } from 'redux-saga/effects'; // Assuming asyncOperationsReducer and App are defined elsewhere // import asyncOperationsReducer from './reducers'; // import App from './App'; // Placeholder for actual reducer and App component const asyncOperationsReducer = (state = {}, action) => state; const App = () => null; useOperation.setPath(state => state); const render = async (req, res) => { const sagaMiddleware = createSagaMiddleware(); const store = applyMiddleware(sagaMiddleware)(createStore)( asyncOperationsReducer ); // provide "hash" option const operationService = new OperationService({ hash: {} }); const componentLifecycleService = new ComponentLifecycleService(operationService); const task = sagaMiddleware.run(function* () { yield* call(operationService.run); yield* call(componentLifecycleService.run); }); // this will incrementally render application, // awaiting till all Suspense components resolved const html = await renderToStringAsync( ); // cleanup sagas task.cancel(); await task.toPromise(); res.write(`
${html}
`.trim()); res.end(); }; ``` -------------------------------- ### React Client Hydration Setup with Sagun Source: https://github.com/iiiristram/sagun/blob/master/README.md Configures the client-side hydration for a React application using Sagun. It initializes the Redux store, saga middleware, and operation services with state and context from the server, then hydrates the existing DOM. ```javascript // client.ts const sagaMiddleware = createSagaMiddleware(); const store = applyMiddleware(sagaMiddleware)(createStore)( asyncOperationsReducer, window.__STATE_FROM_SERVER__ ); const operationService = new OperationService({ hash: window.__SSR_CONTEXT__ }); const componentLifecycleService = new ComponentLifecycleService(operationService); sagaMiddleware.run(function* () { yield* call(operationService.run); yield* call(componentLifecycleService.run); }); useOperation.setPath(state => state); ReactDOM.hydrateRoot( window.document.getElementById('app'), ); ``` -------------------------------- ### React SSR Server Setup with Sagun Source: https://github.com/iiiristram/sagun/blob/master/README.md Configures the server-side rendering pipeline for a React application using Sagun. It sets up Redux store, saga middleware, and uses react-dom/server to stream the rendered HTML, including initial state and context for client-side hydration. ```typescript // server.ts import { createDeferred, Root, OperationService, ComponentLifecycleService } from '@iiiristram/sagun'; import { renderToPipeableStream } from 'react-dom/server'; import { PassThrough } from "stream"; useOperation.setPath(state => state); const render = async (req, res) => { const sagaMiddleware = createSagaMiddleware(); const store = applyMiddleware(sagaMiddleware)(createStore)( asyncOperationsReducer ); // provide "hash" option const operationService = new OperationService({ hash: {} }); const componentLifecycleService = new ComponentLifecycleService(operationService); const task = sagaMiddleware.run(function* () { yield* call(operationService.run); yield* call(componentLifecycleService.run); }); // this will incrementally render application, // awaiting till all Suspense components resolved let html = ''; const defer = createDeferred(); renderToPipeableStream( , { onAllReady() { const s = new PassThrough(); stream.pipe(s); s.on('data', chunk => { html += chunk; }); s.on('end', () => { defer.resolve(); }); }, onError(err) { console.error(err); }, } ); await defer.promise; // cleanup sagas task.cancel(); await task.toPromise(); res.write(`
${html}
`.trim()); res.end(); }); ``` -------------------------------- ### Provide Redux Actions for Service Methods with @daemon Source: https://github.com/iiiristram/sagun/blob/master/README.md Details how to use the `@daemon` decorator to expose service methods as Redux actions, allowing them to be triggered from the UI. The example shows decorating a service method and then calling it via the `actions` object obtained from `useServiceConsumer` in a React component. ```typescript import { Service, daemon } from '@iiiristram/sagun'; class MyService extends Service { toString() { return 'MyService'; } @daemon() // Can optionally take arguments for configuration *foo(a: number, b: number) { console.log('Invoked with', a, b); } } ``` ```tsx // MyComponent.tsx import { useServiceConsumer } from '@iiiristram/sagun'; function MyComponent() { const { actions } = useServiceConsumer(MyService); return ; } ``` -------------------------------- ### Define a Sagun Service Source: https://github.com/iiiristram/sagun/blob/master/README.md Demonstrates how to create a basic service by extending the `Service` class from the Sagun framework. Services are the core components managed by the dependency injection system. ```typescript import {Service} from '@iiiristram/sagun'; class Service1 extends Service { toString() { return 'Service1' } ... } ``` -------------------------------- ### Define and Use Sagun Services Source: https://github.com/iiiristram/sagun/blob/master/README.md Demonstrates the fundamental structure of Sagun services, which are classes inheriting from a base `Service`. It covers defining custom services, their lifecycle methods like `run` and `destroy`, and how to instantiate, register, and initialize them within a React application using `useDI`, `createService`, `registerService`, and `useService`. ```typescript class Service { constructor(operationService: OperationService): Service; *run(...args: TRunArgs): TRes; // inits service and sets status to "ready" *destroy(...args: TRunArgs): void; // cleanup service and sets status to "unavailable" getStatus(): 'unavailable' | 'ready'; getUUID(): string; // uniq service id } ``` ```typescript import { Service } from '@iiiristram/sagun'; class MyService extends Service { // each service has to override "toString" with custom string. // it is used for actions and operations generation. // it should be defined as class method NOT AS PROPERTY. toString() { return 'MyService'; } *foo(a: Type1, b: Type2) { // your custom logic } } ``` ```tsx import { useDI, useService, Operation } from '@iiiristram/sagun'; function HomePage() { const context = useDI(); // create instance of service resolving all its dependencies const service = context.createService(MyService); // register service in order it could be resolved as dependency for other services context.registerService(service); // init service const { operationId } = useService(service); return ( // await service initialization {() => } ); } ``` -------------------------------- ### Root Component Usage Source: https://github.com/iiiristram/sagun/blob/master/README.md Demonstrates the integration of the Root component, which is essential for providing necessary contexts to the application. It requires wrapping the main application component and passing service instances. ```tsx import { ComponentLifecycleService, OperationService, Root, } from '@iiiristram/sagun'; // Assume operationService and componentLifecycleService are initialized elsewhere const operationService = new OperationService(); const componentLifecycleService = new ComponentLifecycleService(operationService); ReactDOM.render( , window.document.getElementById('app') ); ``` -------------------------------- ### Inject Dependencies into Service Constructor Source: https://github.com/iiiristram/sagun/blob/master/README.md Demonstrates how to use the `@inject` decorator to declare and inject dependencies into a service's constructor. This allows services to automatically receive their required components. ```typescript import {Service, inject} from '@iiiristram/sagun'; class Service2 extends Service { private _service1: Service1 private _customClass: CustomClass private _data: Data toString() { return 'Service2' } constructor( // default dependency for all services @inject(OperationService) operationService: OperationService, @inject(Service1) service1: Service1, @inject(CustomClass) customClass: CustomClass, @inject(KEY) data: Data ) { super(operationService) this._service1 = service1 this._customClass = customClass this._data = data } ... } ``` -------------------------------- ### TypeScript Compiler Options Source: https://github.com/iiiristram/sagun/blob/master/README.md Configuration for enabling experimental decorators in TypeScript projects using sagun. ```json { "compilerOptions": { "experimentalDecorators": true } } ``` -------------------------------- ### Define a Custom Dependency Source: https://github.com/iiiristram/sagun/blob/master/README.md Shows how to create custom dependencies by extending the `Dependency` class. These can be registered and injected into services, providing reusable components or data. ```typescript import {Dependency} from '@iiiristram/sagun'; class CustomClass extends Dependency { toString() { return 'CustomClass' } ... } ``` -------------------------------- ### useSaga and Operation Component Source: https://github.com/iiiristram/sagun/blob/master/README.md Illustrates the usage of the useSaga hook for managing asynchronous operations and the Operation component for subscribing to and rendering results from these operations. It shows how to define an operation within onLoad. ```tsx import {useSaga, Operation} from '@iiiristram/sagun'; function MyComponent() { const {operationId} = useSaga({ onLoad: function* () { // do something } }); return ( // await service initialization {() => } ) } ``` -------------------------------- ### Define Custom Dependency Key and Data Source: https://github.com/iiiristram/sagun/blob/master/README.md Illustrates the pattern for defining a unique key for a custom dependency and its associated data. This key is used for registration and injection. ```typescript import {DependencyKey} from '@iiiristram/sagun'; export type Data = {...} export const KEY = '...' as DependencyKey export const DATA: Data = {...} ``` -------------------------------- ### useServiceConsumer for Service Method Invocation Source: https://github.com/iiiristram/sagun/blob/master/README.md Retrieves a service by its constructor and creates Redux actions to invoke methods decorated with `@daemon`. Actions are bound to the store, eliminating the need for explicit `dispatch` calls. ```ts import { Service, daemon } from '@iiiristram/sagun'; class MyService extends Service { toString() { return 'MyService'; } @daemon() *foo(a: number, b: number) { console.log('Invoked with', a, b); } } ``` ```tsx import { useServiceConsumer } from '@iiiristram/sagun'; function MyComponent() { const { actions } = useServiceConsumer(MyService); return ; } ``` -------------------------------- ### useDI Hook and IDIContext API Source: https://github.com/iiiristram/sagun/blob/master/README.md Defines the IDIContext interface, which provides methods for registering and resolving dependencies within the Sagun framework. This context is primarily accessed via the useDI hook. ```APIDOC IDIContext: registerDependency(key: DependencyKey, dependency: D): void - Registers a custom dependency by a unique key. getDependency(key: DependencyKey): D - Retrieves a custom dependency by its key. registerService(service: Dependency): void - Registers a dependency instance. createService(Ctr: Ctr): T - Creates a dependency instance, resolving all its sub-dependencies. - Throws an error if sub-dependencies were not registered prior. getService(Ctr: Ctr): T - Retrieves a dependency instance if it was registered. - Throws an error if the dependency was not registered. createServiceActions>(service: T, bind?: Store): ActionAPI - Creates actions for service methods marked with the @daemon decorator. - Optionally binds these actions to a provided store. ``` -------------------------------- ### useService Hook as Shortcut Source: https://github.com/iiiristram/sagun/blob/master/README.md A shortcut for useSaga, simplifying the binding of service methods (run, dispose) to the component lifecycle. It takes a service instance and arguments, returning an operationId for tracking. ```tsx const { operationId } = useService(service, [...args]); ``` ```tsx const { operationId } = useSaga( { onLoad: service.run, onDispose: service.dispose, }, [...args] ); ``` -------------------------------- ### useOperation Hook for Suspense-Compatible Operations Source: https://github.com/iiiristram/sagun/blob/master/README.md Creates a subscription to an operation in the Redux store, compatible with React.Suspense. It allows falling back to a loader while an operation executes and requires setting a path in the store for operation lookup. ```ts import { Service, operation } from '@iiiristram/sagun'; class MyService extends Service { toString() { return 'MyService'; } @operation *foo() { return 'Hello'; } } ``` ```tsx import { useServiceConsumer, useOperation, getId } from '@iiiristram/sagun'; function MyComponent() { const { service } = useServiceConsumer(MyService); const operation = useOperation({ operationId: getId(service.foo), suspense: true, // turn on Suspense compatibility }); return
{operation?.result} World
; } ``` ```tsx import { Suspense } from 'react'; function Parent() { return ( ); } ``` ```ts // bootstrap.ts useOperation.setPath(state => ...) // i.e. state => state.asyncOperations ``` -------------------------------- ### SSR with Sagas and @operation Decorator Source: https://github.com/iiiristram/sagun/blob/master/README.md Explains how to enable Server-Side Rendering (SSR) for sagas using the @operation decorator with the 'ssr: true' option. It details collecting pure data on the server and rendering components that await operation results via Suspense and the Operation component. ```typescript // MyService.ts class MyService extends Service { @operation({ // Enable ssr for operation, so its result will be collected. // Operations marked this way won't be executed on client at first time, // so don't put here any logic with application state, like forms, // such logic probably has to be also executed on the client. // You should collect pure data here. ssr: true }) *fetchSomething() { // ... } } // MyComponent.tsx function MyComponent() { const {operationId} = useSaga({ onLoad: myService.fetchSomething, }) return ( // subscribe to saga that contains the operation via Operation or useOperation, // if no subscription, render won't await this saga {({result}) => } ) } // App.tsx function App() { return ( // ensure there is Suspense that will handle your operation ) } ``` -------------------------------- ### AsyncOperation and OperationId Types Source: https://github.com/iiiristram/sagun/blob/master/README.md Defines the core data structure `AsyncOperation` for managing asynchronous states and the custom `OperationId` type for uniquely identifying operations within the application state. ```APIDOC AsyncOperation Represents the state of an asynchronous operation. Properties: id: OperationId - Unique identifier for the operation. isLoading?: boolean - True if the operation is currently in progress. isError?: boolean - True if the operation finished with an error. isBlocked?: boolean - True if the operation should not be executed. error?: TErr - The error object if the operation failed. args?: TArgs - The arguments the operation was executed with. result?: TRes - The successful result of the operation. meta?: TMeta - Any additional metadata associated with the operation. OperationId A custom type that extends `string` to provide type-safe identification of operations. At runtime, it is a string, but it enforces type checking at compile time. Example: const id = 'MY_ID' as OperationId; const str: string = id; // OK, type information is lost at runtime // operation.id = str; // TYPE ERROR: id must be of type OperationId ``` -------------------------------- ### Sagun @daemon Decorator Source: https://github.com/iiiristram/sagun/blob/master/README.md The @daemon decorator provides metadata for service methods, allowing them to be invoked by Redux actions after `service.run` is called. It controls execution modes like synchronous, latest, every, and scheduling. ```ts import {Service, daemon, DaemonMode} from '@iiiristram/sagun'; class MyService extends Service { toString() { return 'MyService' } // by default method won't be called until previous call finished (DaemonMode.Sync). // i.e. block new page load until previous page loaded @daemon() *method_1(a: number, b: number) { ... } // cancel previous call and starts new (like redux-saga takeLatest) // i.e. multiple clicks to "Search" button @daemon(DaemonMode.Last) *method_2(a: number, b: number) { ... } // call method every time, no order guarantied (like redux-saga takeEvery) // i.e. send some analytics @daemon(DaemonMode.Every) *method_3(a: number, b: number) { ... } // has no corresponding action, after service run, method will be called every N ms, provided by second argument // i.e. make some polling @daemon(DaemonMode.Schedule, ms) *method_4() { ... } @daemon( // DaemonMode.Sync / DaemonMode.Last / DaemonMode.Every mode, // provide action to trigger instead of auto-generated action, same type as redux-saga "take" effect accepts action ) *method_5(a: number, b: number) { ... } } ``` -------------------------------- ### Sagun @inject Decorator Source: https://github.com/iiiristram/sagun/blob/master/README.md The @inject decorator is applied to service constructor arguments to enable dependency resolution. It allows services to declare their dependencies, such as other services. ```ts // MyService.ts import {Service, inject} from '@iiiristram/sagun'; class MyService extends Service { ... constructor( // default dependency for all services @inject(OperationService) operationService: OperationService, @inject(MyOtherService) myOtherService: MyOtherService ) { super(operationService) ... } ... } ``` -------------------------------- ### useSaga Hook for Component Lifecycle Source: https://github.com/iiiristram/sagun/blob/master/README.md Binds saga execution to the component lifecycle, executing on render for Suspense compatibility. It's used for application logic like form initialization or aggregating service methods. Handles cancellation of long-running tasks and ensures proper disposal. ```tsx function MyComponent(props) { const {a, b} = props; // operationId to subscribe to onLoad results const {operationId} = useSaga({ // "id" required in case component placed inside Suspense boundary, // cause in some cases React drop component state, // and it's impossible to generate stable implicit id. // See https://github.com/facebook/react/issues/24669. // // Id has to be uniq for each component instance (i.e. use `item_${id}` for list items). id: "operation-id", // executes on render and args changed onLoad: function*(arg_a, arg_b) { console.log('I am rendered') yield call(service1.foo, arg_a) yield call(service2.bazz, arg_b) }, // executes before next onLoad and on unmount onDispose: function*(arg_a, arg_b) { console.log('I was changed') } // arguments for sagas, so sagas re-executed on any argument change }, [a, b]) ... } ``` -------------------------------- ### Save Service Method Results with @operation Source: https://github.com/iiiristram/sagun/blob/master/README.md Explains how to use the `@operation` decorator to automatically save the return value of a service method to a Redux store. It also shows how to consume these results in a React component using `useServiceConsumer` and `useOperation` by referencing the operation ID derived from the service method. ```typescript // MyService.ts import { Service, operation } from '@iiiristram/sagun'; class MyService extends Service { toString() { return 'MyService'; } @operation *foo() { return 'Hello'; } } ``` ```tsx // MyComponent.tsx import { useServiceConsumer, useOperation, getId } from '@iiiristram/sagun'; function MyComponent() { // resolve service instance, that was registered somewhere in parent components const { service } = useServiceConsumer(MyService); const operation = useOperation({ operationId: getId(service.foo), // get operation id from service method }); return
{operation?.result} World
; } ``` -------------------------------- ### Sagun @operation Decorator Source: https://github.com/iiiristram/sagun/blob/master/README.md The @operation decorator creates an operation in the Redux store for a wrapped service method. It supports auto-generated IDs, custom IDs, function-based IDs, and configuration options like update strategies and SSR enablement. ```ts import {Service, operation, OperationId} from '@iiiristram/sagun'; export const MY_CUSTOM_ID = 'MY_CUSTOM_ID' as OperationId class MyService extends Service { toString() { return 'MyService' } // create an operation with auto-generated id, // which can be retrieved by util "getId" @operation *method_1() { ... } // create an operation with provided id, i.e. it's possible to assign same operation for different methods @operation(MY_CUSTOM_ID) *method_2() { return 1; } // create an operation id depending on arguments provided for method @operation((...args) => args.join('_') as OperationId) *method_3(...args) { return 1; } @operation({ // optional, could be constant or function id, // optional, function that allows to change operation values, but it should not change operation generics // (ie if operation result was a number it should be a number after change) updateStrategy: function*(operation) { const changedOperation = ... // change operation somehow return changedOperation }, ssr: true, // enable execution on server }) *method_4(...args) { return 1; } } // Update strategy example import { Service, operation, OperationId } from '@iiiristram/sagun'; class MyServiceWithUpdate extends Service { toString() { return 'MyServiceWithUpdate'; } @operation({ updateStrategy: function* mergeStrategy(next) { const prev = yield select(state => state.asyncOperations.get(next.id)); return { ...next, result: prev?.result && next.result ? [...prev.result, ...next.result] : next.result || prev?.result, }; }, }) *loadList(pageNumber: number) { const items: Array = yield call(fetch, { pageNumber }); return items; } } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.