Gathering detailed insights and metrics for @iiiristram/sagun
Gathering detailed insights and metrics for @iiiristram/sagun
Gathering detailed insights and metrics for @iiiristram/sagun
Gathering detailed insights and metrics for @iiiristram/sagun
npm install @iiiristram/sagun
Typescript
Module System
Node Version
NPM Version
58.1
Supply Chain
70.8
Quality
85.9
Maintenance
100
Vulnerability
99.3
License
TypeScript (97.95%)
JavaScript (1.96%)
HTML (0.09%)
Total Downloads
3,968
Last Day
9
Last Week
38
Last Month
582
Last Year
1,450
14 Stars
54 Commits
2 Forks
2 Watchers
2 Branches
2 Contributors
Updated on May 20, 2025
Minified
Minified + Gzipped
Latest Version
2.0.2
Package Id
@iiiristram/sagun@2.0.2
Unpacked Size
147.96 kB
Size
37.05 kB
File Count
78
NPM Version
10.7.0
Node Version
18.20.3
Published on
May 20, 2025
Cumulative downloads
Total Downloads
Last Day
0%
9
Compared to previous day
Last Week
-71.9%
38
Compared to previous week
Last Month
219.8%
582
Compared to previous month
Last Year
135.8%
1,450
Compared to previous year
7
31
Strongly-typed service-based isomorphic architecture on top of redux-saga
Currently compatible only with typescript codebase with following options enabled
1{ 2 "compilerOptions": { 3 "experimentalDecorators": true 4 } 5}
peer dependencies:
npm i --save react react-dom redux react-redux redux-saga immutable
lib install
npm i --save @iiiristram/sagun
recommended to install
npm i --save typed-redux-saga
- provide strongly-typed effects for redux-saga
1// bootstrap.tsx 2import { applyMiddleware, createStore, combineReducers } from 'redux'; 3import { Provider } from 'react-redux'; 4import createSagaMiddleware from 'redux-saga'; 5import React from 'react'; 6import ReactDOM from 'react-dom'; 7import { 8 ComponentLifecycleService, 9 OperationService, 10 asyncOperationsReducer, 11 Root, 12 useOperation, 13} from '@iiiristram/sagun'; 14import { call } from 'typed-redux-saga'; 15import App from './your-app-path.js'; 16 17const sagaMiddleware = createSagaMiddleware(); 18const store = applyMiddleware(sagaMiddleware)(createStore)( 19 combineReducers({ 20 asyncOperations: asyncOperationsReducer, 21 }) 22); 23 24// set up destination for storage 25useOperation.setPath(state => state.asyncOperations); 26 27// two basic services which provide library workflow 28const operationService = new OperationService(); 29const componentLifecycleService = new ComponentLifecycleService(operationService); 30 31sagaMiddleware.run(function* () { 32 yield* call(operationService.run); 33 yield* call(componentLifecycleService.run); 34}); 35 36ReactDOM.render( 37 <Root operationService={operationService} componentLifecycleService={componentLifecycleService}> 38 <Provider store={store}> 39 <App /> 40 </Provider> 41 </Root>, 42 window.document.getElementById('app') 43);
The core data structure which represents some part of your application state is AsyncOperation
.
1type AsyncOperation<TRes = unknown, TArgs = unknown[], TMeta = unknown, TErr = Error> = { 2 id: OperationId<TRes, TArgs, TMeta, TErr>; // uniq id 3 isLoading?: boolean; // is operation in process 4 isError?: boolean; // was operation finished with error 5 isBlocked?: boolean; // should operation be executed 6 error?: TErr; // error if any 7 args?: TArgs; // arguments operation was executed with 8 result?: TRes; // result of operation if it was finished 9 meta?: TMeta; // any additional data 10};
So the whole state of the application is represented by dictionary of such operations accessible by their id.
ID of operation is described by custom type OperationId<TRes, TArgs, TMeta, TErr>
that extends string
with operation description, so it could be retrieved by type system by ID only. It is just a string at runtime.
1const id = 'MY_ID' as OperationId<MyRes>; 2const str: string = id; // OK by all info lost 3... 4operation.id = str; // TYPE ERROR id has to be of OperationId type
Services are primary containers for your business logic, they are represented by classes, which are inherited from base Service
class.
1class Service<TRunArgs extends any[] = [], TRes = void> { 2 constructor(operationService: OperationService): Service; 3 *run(...args: TRunArgs): TRes; // inits service and sets status to "ready" 4 *destroy(...args: TRunArgs): void; // cleanup service and sets status to "unavailable" 5 getStatus(): 'unavailable' | 'ready'; 6 getUUID(): string; // uniq service id 7}
So custom service could be defined like this
1import { Service } from '@iiiristram/sagun'; 2 3class MyService extends Service { 4 // each service has to override "toString" with custom string. 5 // it is used for actions and operations generation. 6 // it should be defined as class method NOT AS PROPERTY. 7 toString() { 8 return 'MyService'; 9 } 10 11 *foo(a: Type1, b: Type2) { 12 // your custom logic 13 } 14}
To make service initialized you should invoke useService
in the root of components subtree, where this service required, for example in a page root component
1import { useDI, useService, Operation } from '@iiiristram/sagun'; 2 3function HomePage() { 4 const context = useDI(); 5 // create instance of service resolving all its dependencies 6 const service = context.createService(MyService); 7 // register service in order it could be resolved as dependency for other services 8 context.registerService(service); 9 // init service 10 const { operationId } = useService(service); 11 12 return ( 13 // await service initialization 14 <Operation operationId={operationId}>{() => <Content />}</Operation> 15 ); 16}
In order to save method result to redux store, method has to be marked with @operation
decorator
1// MyService.ts 2import { Service, operation } from '@iiiristram/sagun'; 3 4class MyService extends Service { 5 toString() { 6 return 'MyService'; 7 } 8 9 @operation 10 *foo() { 11 return 'Hello'; 12 } 13}
1// MyComponent.tsx 2import { useServiceConsumer, useOperation, getId } from '@iiiristram/sagun'; 3 4function MyComponent() { 5 // resolve service instance, that was registered somewhere in parent components 6 const { service } = useServiceConsumer(MyService); 7 const operation = useOperation({ 8 operationId: getId(service.foo), // get operation id from service method 9 }); 10 return <div>{operation?.result} World</div>; 11}
In order to be able to trigger method from UI by redux action, this method has to be marked with @daemon
decorator
1import { Service, daemon } from '@iiiristram/sagun'; 2 3class MyService extends Service { 4 toString() { 5 return 'MyService'; 6 } 7 8 @daemon() 9 *foo(a: number, b: number) { 10 console.log('Invoked with', a, b); 11 } 12}
1// MyComponent.tsx 2import { useServiceConsumer } from '@iiiristram/sagun'; 3 4function MyComponent() { 5 const { actions } = useServiceConsumer(MyService); 6 return <button onClick={() => actions.foo(1, 2)}>Click me</button>; 7}
It is possible to declare service dependencies via constructor arguments.
Each dependency should be ether an instance of some class, that extends Dependency
class, or associated with a string uniq constant (dependency key).
Service
class has been already inherited from Dependency
.
Service with custom dependencies should mark them with @inject
decorator.
1// Service1.ts 2import {Service} from '@iiiristram/sagun'; 3 4class Service1 extends Service { 5 toString() { 6 return 'Service1' 7 } 8 9 ... 10}
1// CustomClass.ts 2import {Dependency} from '@iiiristram/sagun'; 3 4class CustomClass extends Dependency { 5 toString() { 6 return 'CustomClass' 7 } 8 9 ... 10}
1// customDependency.ts 2import {DependencyKey} from '@iiiristram/sagun'; 3 4export type Data = {...} 5export const KEY = '...' as DependencyKey<Data> 6export const DATA: Data = {...}
1// somewhere in react components
2...
3const di = useDI();
4
5// register custom dependency by key
6di.registerDependency(KEY, DATA);
7
8// create instance of Dependency resolving all its dependencies
9const service1 = context.createService(Service1);
10const custom = context.createService(CustomClass);
11
12// register Dependency instancies so they could be resolved as dependencies for other services
13context.registerService(service1);
14context.registerService(custom);
15
16// create service with resolved dependencies after their registration
17const service2 = context.createService(Service2);
18...
1// Service2.ts 2import {Service, inject} from '@iiiristram/sagun'; 3 4class Service2 extends Service { 5 private _service1: Service1 6 private _customClass: CustomClass 7 private _data: Data 8 9 toString() { 10 return 'Service2' 11 } 12 13 constructor( 14 // default dependency for all services 15 @inject(OperationService) operationService: OperationService, 16 @inject(Service1) service1: Service1, 17 @inject(CustomClass) customClass: CustomClass, 18 @inject(KEY) data: Data 19 ) { 20 super(operationService) 21 this._service1 = service1 22 this._customClass = customClass 23 this._data = data 24 } 25 ... 26}
It's possible to customize service initialization and cleanup by overriding run
and destroy
methods
1class MyService extends Service<MyArgs, MyRes> { 2 toString() { 3 return 'MyService' 4 } 5 6 *run(...args: MyArgs) { 7 // IMPORTANT 8 yield call([this, super.run]); 9 const result: MyRes = ...; 10 return result; 11 } 12 13 *destroy(...args: MyArgs) { 14 // IMPORTANT 15 yield call([this, super.destroy]); 16 ... 17 } 18 ... 19}
1class MyService extends Service<MyArgs, MyRes> { 2 // OPTIONAL 3 private _someOtherService: MyOtherService 4 5 // REQUIRED 6 toString() { 7 return 'MyService' 8 } 9 10 // OPTIONAL 11 constructor( 12 @inject(OperationService) operationService: OperationService, 13 @inject(MyOtherService) someOtherService: MyOtherService 14 ) { 15 super(operationService) 16 this._someOtherService = someOtherService; 17 } 18 19 // OPTIONAL 20 *run(...args: MyArgs) { 21 yield call([this, super.run]); 22 yield call(this._someOtherService.run) 23 const result: MyRes = ...; 24 return result; 25 } 26 27 // OPTIONAL 28 *destroy(...args: MyArgs) { 29 yield call([this, super.run]); 30 yield call(this._someOtherService.destroy) 31 } 32 33 @daemon() // make method reachable by redux action 34 @operation // write result to redux state 35 *foo(a: Type1, b: Type2) { 36 // your custom logic 37 } 38}
This decorator create on operation in redux store for a wrapped service method.
1// MyService.ts 2import {Service, operation, OperationId} from '@iiiristram/sagun'; 3 4export const MY_CUSTOM_ID = 'MY_CUSTOM_ID' as OperationId<number> 5 6class MyService extends Service { 7 toString() { 8 return 'MyService' 9 } 10 11 // create an operation with auto-generated id, 12 // which can be retrieved by util "getId" 13 @operation 14 *method_1() { 15 ... 16 } 17 18 // create an operation with provided id, 19 // i.e. it's possible to assign same operation for different methods 20 @operation(MY_CUSTOM_ID) 21 *method_2() { 22 return 1; 23 } 24 25 // create an operation id depending on arguments provided for method 26 @operation((...args) => args.join('_') as OperationId<number>) 27 *method_3(...args) { 28 return 1; 29 } 30 31 @operation({ 32 // optional, could be constant or function 33 id, 34 // optional, function that allows to change operation values, 35 // but it should not change operation generics 36 // (ie if operation result was a number it should be a number after change) 37 updateStrategy: function*(operation) { 38 const changedOperation = ... // change operation somehow 39 return changedOperation 40 }, 41 ssr: true, // enable execution on server 42 }) 43 *method_4(...args) { 44 return 1; 45 } 46}
Update strategy example
1// MyService.ts 2import { Service, operation, OperationId } from '@iiiristram/sagun'; 3 4class MyService extends Service { 5 toString() { 6 return 'MyService'; 7 } 8 9 @operation({ 10 updateStrategy: function* mergeStrategy(next) { 11 const prev = yield select(state => state.asyncOperations.get(next.id)); 12 return { 13 ...next, 14 result: prev?.result && next.result ? [...prev.result, ...next.result] : next.result || prev?.result, 15 }; 16 }, 17 }) 18 *loadList(pageNumber: number) { 19 const items: Array<Entity> = yield call(fetch, { pageNumber }); 20 return items; 21 } 22}
This decorator provide some meta-data for method, so it could be invoked by redux action after service.run
called.
Decorator doesn't affect cases when method directly called from another saga, all logic applied only for redux actions.
1import {Service, daemon, DaemonMode} from '@iiiristram/sagun'; 2 3class MyService extends Service { 4 toString() { 5 return 'MyService' 6 } 7 8 // by default method won't be called until previous call finished (DaemonMode.Sync). 9 // i.e. block new page load until previous page loaded 10 @daemon() 11 *method_1(a: number, b: number) { 12 ... 13 } 14 15 // cancel previous call and starts new (like redux-saga takeLatest) 16 // i.e. multiple clicks to "Search" button 17 @daemon(DaemonMode.Last) 18 *method_2(a: number, b: number) { 19 ... 20 } 21 22 // call method every time, no order guarantied (like redux-saga takeEvery) 23 // i.e. send some analytics 24 @daemon(DaemonMode.Every) 25 *method_3(a: number, b: number) { 26 ... 27 } 28 29 // has no corresponding action, 30 // after service run, method will be called every N ms, provided by second argument 31 // i.e. make some polling 32 @daemon(DaemonMode.Schedule, ms) 33 *method_4() { 34 ... 35 } 36 37 @daemon( 38 // DaemonMode.Sync / DaemonMode.Last / DaemonMode.Every 39 mode, 40 // provide action to trigger instead of auto-generated action, 41 // same type as redux-saga "take" effect accepts 42 action 43 ) 44 *method_5(a: number, b: number) { 45 ... 46 } 47}
This decorator has to be applied to arguments of service's constructor in order service dependencies could be resolved.
1// MyService.ts 2import {Service, inject} from '@iiiristram/sagun'; 3 4class MyService extends Service { 5 ... 6 constructor( 7 // default dependency for all services 8 @inject(OperationService) operationService: OperationService, 9 @inject(MyOtherService) myOtherService: MyOtherService 10 ) { 11 super(operationService) 12 ... 13 } 14 ... 15}
Binds saga execution to component lifecycle. Executes in a useMemo
-like way.
Hook executes on render, not on reconciliation complete (required for Suspense compatibility).
Should be used to execute some application logic like form initialization, or to aggregate multiple methods of services
1function MyComponent(props) {
2 const {a, b} = props;
3 // operationId to subscribe to onLoad results
4 const {operationId} = useSaga({
5 // "id" required in case component placed inside Suspense boundary,
6 // cause in some cases React drop component state,
7 // and it's impossible to generate stable implicit id.
8 // See https://github.com/facebook/react/issues/24669.
9 //
10 // Id has to be uniq for each component instance (i.e. use `item_${id}` for list items).
11 id: "operation-id",
12 // executes on render and args changed
13 onLoad: function*(arg_a, arg_b) {
14 console.log('I am rendered')
15 yield call(service1.foo, arg_a)
16 yield call(service2.bazz, arg_b)
17 },
18 // executes before next onLoad and on unmount
19 onDispose: function*(arg_a, arg_b) {
20 console.log('I was changed')
21 }
22 // arguments for sagas, so sagas re-executed on any argument change
23 }, [a, b])
24
25 ...
26}
27
If changes happened in the middle of long running onLoad
, this saga will be canceled (break on nearest yield) and onDispose
will be called.
It is guaranteed that onDispose
will be fully executed before next onLoad
, so if changes happened multiple times during long running onDispose
, onLoad
will be called only once with latest arguments. onLoad
is wrapped into operation, so you are able to subscribe to its execution using operationId
, provided by the hook.
Same as useSaga
but id
parameter is optional, hook made for back-compatibility and easier migration to next major.
1const { operationId } = useService(service, [...args]);
This is shortcut for
1const { operationId } = useSaga(
2 {
3 onLoad: service.run,
4 onDispose: service.dispose,
5 },
6 [...args]
7);
This hook retrieves service by its constructor, and create corresponding redux actions to invoke methods, marked by @daemon
decorator. Actions are bond to store, so no dispatch
necessary.
1import { Service, daemon } from '@iiiristram/sagun'; 2 3class MyService extends Service { 4 toString() { 5 return 'MyService'; 6 } 7 8 @daemon() 9 *foo(a: number, b: number) { 10 console.log('Invoked with', a, b); 11 } 12}
1// MyComponent.tsx 2import { useServiceConsumer } from '@iiiristram/sagun'; 3 4function MyComponent() { 5 const { actions } = useServiceConsumer(MyService); 6 return <button onClick={() => actions.foo(1, 2)}>Click me</button>; 7}
This hook creates a subscription to operation in the redux store. It is compatible with React.Suspense
, so it's possible to fallback to some loader while operation is executing.
1// MyService.ts 2import { Service, operation } from '@iiiristram/sagun'; 3 4class MyService extends Service { 5 toString() { 6 return 'MyService'; 7 } 8 9 @operation 10 *foo() { 11 return 'Hello'; 12 } 13}
1// MyComponent.tsx 2import { useServiceConsumer, useOperation, getId } from '@iiiristram/sagun'; 3 4function MyComponent() { 5 const { service } = useServiceConsumer(MyService); 6 const operation = useOperation({ 7 operationId: getId(service.foo), 8 suspense: true, // turn on Suspense compatibility 9 }); 10 11 return <div>{operation?.result} World</div>; 12}
1// Parent.tsx 2function Parent() { 3 return ( 4 <Suspense fallback=""> 5 <MyComponent /> 6 </Suspense> 7 ); 8}
Before using the hook your should provide path in store, where to look for operation.
1// bootstrap.ts 2useOperation.setPath(state => ...) // i.e. state => state.asyncOperations
This hook return a context which is primally used to register and resolve dependencies for your services. Context API looks like
1type IDIContext = { 2 // register custom dependency by key 3 registerDependency<D>(key: DependencyKey<D>, dependency: D): void; 4 // get custom dependency by key 5 getDependency<D>(key: DependencyKey<D>): D; 6 // register dependency instance 7 registerService: (service: Dependency) => void; 8 // create dependency instance resolving all sub-dependencies, 9 // in case they were registered before, throw an error otherwise 10 createService: <T extends Dependency>(Ctr: Ctr<T>) => T; 11 // retrieve dependency instance if it was registered, 12 // throw an error otherwise 13 getService: <T extends Dependency>(Ctr: Ctr<T>) => T; 14 // create actions for service methods marked by @daemon, 15 // bind them to store if any provided 16 createServiceActions: <T extends BaseService<any, any>>(service: T, bind?: Store<any, AnyAction>) => ActionAPI<T>; 17};
This component provides all necessary contexts. You have to wrap your application with it.
1import { 2 ComponentLifecycleService, 3 OperationService, 4 Root, 5} from '@iiiristram/sagun'; 6 7... 8 9const operationService = new OperationService(); 10const componentLifecycleService = new ComponentLifecycleService(operationService); 11 12ReactDOM.render( 13 <Root 14 operationService={operationService} 15 componentLifecycleService={componentLifecycleService} 16 > 17 <App /> 18 </Root>, 19 window.document.getElementById('app') 20);
This component encapsulates useOperation
1import {useSaga, Operation} from '@iiiristram/sagun'; 2 3function MyComponent() { 4 const {operationId} = useSaga({ 5 onLoad: function* () { 6 // do something 7 } 8 ); 9 10 return ( 11 // await service initialization 12 <Operation operationId={operationId}> 13 {() => <Content/>} 14 </Operation> 15 ) 16}
Provides IoC container, you shouldn't use this context directly, there is hook useDI
for this purpose.
Provides boolean flag, if false
no sagas will be executed on server in a children subtrees.
In order to make your sagas work with SSR you should do the following
1// MyService.ts 2class MyService extends Service { 3 @operation({ 4 // Enable ssr for operation, so it's result will be collected. 5 // Operations marked this way won't be executed on client at first time, 6 // so don't put here any logic with application state, like forms, 7 // such logic probably has to be also executed on the client. 8 // You should collect pure data here. 9 ssr: true 10 }) 11 *fetchSomething() { 12 // 13 } 14} 15 16// MyComponent.tsx 17function MyComponent() { 18 const {operationId} = useSaga({ 19 onLoad: myService.fetchSomething, 20 }) 21 22 return ( 23 // subscribe to saga that contains the operation via Operation or useOperation, 24 // if no subscription, render won't await this saga 25 <Operation 26 // getId(myService.fetchSomething) also could be used 27 operationId={operationId} 28 > 29 {({result}) => <Content result={result}/>} 30 </Operation> 31 ) 32} 33 34// App.tsx 35function App() { 36 return ( 37 // ensure there is Suspense that will handle your operation 38 <Suspense fallback=""> 39 <MyComponent/> 40 </Suspense> 41 ) 42}
1// server.ts 2import { Root, OperationService, ComponentLifecycleService } from '@iiiristram/sagun'; 3import { renderToStringAsync } from '@iiiristram/sagun/server'; 4 5useOperation.setPath(state => state); 6const render = async (req, res) => { 7 const sagaMiddleware = createSagaMiddleware(); 8 const store = applyMiddleware(sagaMiddleware)(createStore)( 9 asyncOperationsReducer 10 ); 11 12 // provide "hash" option 13 const operationService = new OperationService({ hash: {} }); 14 const componentLifecycleService = new ComponentLifecycleService(operationService); 15 16 const task = sagaMiddleware.run(function* () { 17 yield* call(operationService.run); 18 yield* call(componentLifecycleService.run); 19 }); 20 21 // this will incrementally render application, 22 // awaiting till all Suspense components resolved 23 const html = await renderToStringAsync( 24 <Root operationService={operationService} componentLifecycleService={componentLifecycleService}> 25 <Provider store={store}> 26 <App /> 27 </Provider> 28 </Root> 29 ); 30 31 // cleanup sagas 32 task.cancel(); 33 await task.toPromise(); 34 35 res.write(` 36 <html> 37 <body> 38 <script id="state"> 39 window.__STATE_FROM_SERVER__ = ${JSON.stringify(store.getState())}; 40 </script> 41 <script id="hash"> 42 window.__SSR_CONTEXT__ = ${JSON.stringify(operationService.getHash())}; 43 </script> 44 <div id="app">${html}</div> 45 </body> 46 </html> 47 `.trim()); 48 res.end(); 49}); 50 51// client.ts 52const sagaMiddleware = createSagaMiddleware(); 53const store = applyMiddleware(sagaMiddleware)(createStore)( 54 asyncOperationsReducer, 55 window.__STATE_FROM_SERVER__ 56); 57 58const operationService = new OperationService({ hash: window.__SSR_CONTEXT__ }); 59const componentLifecycleService = new ComponentLifecycleService(operationService); 60 61sagaMiddleware.run(function* () { 62 yield* call(operationService.run); 63 yield* call(componentLifecycleService.run); 64}); 65 66useOperation.setPath(state => state); 67 68ReactDOM.hydrate( 69 <Root operationService={operationService} componentLifecycleService={componentLifecycleService}> 70 <Provider store={store}> 71 <App /> 72 </Provider> 73 </Root>, 74 window.document.getElementById('app'), 75);
1// server.ts 2import { createDeferred, Root, OperationService, ComponentLifecycleService } from '@iiiristram/sagun'; 3import { renderToPipeableStream } from 'react-dom/server'; 4import { PassThrough } from "stream"; 5 6useOperation.setPath(state => state); 7const render = async (req, res) => { 8 const sagaMiddleware = createSagaMiddleware(); 9 const store = applyMiddleware(sagaMiddleware)(createStore)( 10 asyncOperationsReducer 11 ); 12 13 // provide "hash" option 14 const operationService = new OperationService({ hash: {} }); 15 const componentLifecycleService = new ComponentLifecycleService(operationService); 16 17 const task = sagaMiddleware.run(function* () { 18 yield* call(operationService.run); 19 yield* call(componentLifecycleService.run); 20 }); 21 22 // this will incrementally render application, 23 // awaiting till all Suspense components resolved 24 let html = ''; 25 const defer = createDeferred(); 26 renderToPipeableStream( 27 <Root operationService={operationService} componentLifecycleService={componentLifecycleService}> 28 <Provider store={store}> 29 <App /> 30 </Provider> 31 </Root>, 32 { 33 onAllReady() { 34 const s = new PassThrough(); 35 stream.pipe(s); 36 37 s.on('data', chunk => { 38 html += chunk; 39 }); 40 41 s.on('end', () => { 42 defer.resolve(); 43 }); 44 }, 45 onError(err) { 46 console.error(err); 47 }, 48 } 49 ); 50 51 await defer.promise; 52 53 // cleanup sagas 54 task.cancel(); 55 await task.toPromise(); 56 57 res.write(` 58 <html> 59 <body> 60 <script id="state"> 61 window.__STATE_FROM_SERVER__ = ${JSON.stringify(store.getState())}; 62 </script> 63 <script id="hash"> 64 window.__SSR_CONTEXT__ = ${JSON.stringify(operationService.getHash())}; 65 </script> 66 <div id="app">${html}</div> 67 </body> 68 </html> 69 `.trim()); 70 res.end(); 71}); 72 73// client.ts 74const sagaMiddleware = createSagaMiddleware(); 75const store = applyMiddleware(sagaMiddleware)(createStore)( 76 asyncOperationsReducer, 77 window.__STATE_FROM_SERVER__ 78); 79 80const operationService = new OperationService({ hash: window.__SSR_CONTEXT__ }); 81const componentLifecycleService = new ComponentLifecycleService(operationService); 82 83sagaMiddleware.run(function* () { 84 yield* call(operationService.run); 85 yield* call(componentLifecycleService.run); 86}); 87 88useOperation.setPath(state => state); 89 90ReactDOM.hydrateRoot( 91 window.document.getElementById('app'), 92 <Root operationService={operationService} componentLifecycleService={service}> 93 <Provider store={store}> 94 <App /> 95 </Provider> 96 </Root> 97);
No vulnerabilities found.
Reason
26 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
0 existing vulnerabilities detected
Reason
Found 0/30 approved changesets -- score normalized to 0
Reason
detected GitHub workflow tokens with excessive permissions
Details
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
security policy file not detected
Details
Reason
license file not detected
Details
Reason
project is not fuzzed
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Score
Last Scanned on 2025-06-30
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More