Set of ES6 decorators to improve your apps
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. The primary benefit of the Decorator pattern is that you can take a rather vanilla object and wrap it in more advanced behaviors. Learn more
npm install @s-ui/decorators
Name | Description | Link |
---|---|---|
@inlineError |
Wrap a function to handle the errors for you. | Take me there |
@AsyncInlineError() |
Wrap an async function to handle errors for you and return a tuple [error, result]. | Take me there |
@streamify() |
Creates a stream of calls to any method of a class. | Take me there |
@cache() |
Creates a Memory or LRU cache. | Take me there |
@tracer() |
Sends a performance timing metric to the configured reporter. | Take me there |
@Deprecated() |
Dispatch a warning message on browser cli and enables you the possibility of monitor those logs. | Take me there |
Wrapper any function and handle the errors for you.
If the function is a sync function:
- When is execute return [null, resp]
- When throw an exception return [err, null]
If the function return a promise: (You should use the @AsyncInlineError()
)
- When is resolved return [null, resp]
- When is rejected return [err, null]
- When throw an exception return [err, null]
Warning
If you use the @inlineError on async functions, you should migrate it and use the new @AsyncInlineError() instead. You will see a console warning on your browser and an ESlint warning in your linting health.
import {inlineError} from '@s-ui/decorators'
class Buzz {
@inlineError
method() {
return Promise.reject(new Error('KO'))
}
}
const buzz = new Buzz()
const [err, resp] = buzz.method()
console.log(typeof err) // ==> Error
Wrap an async function to handle errors for you and return a tuple [error, result]. This decorator is a function to enable the possibility to improve in the future with new features and keep retrocompatibility.
- When the promise function is resolved return [null, resp]
- When the promise function is rejected return [err, null]
- When throw an exception return [err, null]
import {AsyncInlineError} from '@s-ui/decorators'
class Buzz {
@AsyncInlineError()
method() {
return Promise.reject(new Error('KO'))
}
}
const buzz = new Buzz()
const [err, resp] = buzz.method()
console.log(typeof err) // ==> Error
Creates a stream of calls to any method of a class. Dependency of RxJS
import {streamify} from '@s-ui/decorators'
@streamify('greeting', 'greetingAsync')
class Person {
greeting(name) {
return `Hi ${name}`
}
greetingAsync(name) {
return new Promise(resolve => setTimeout(resolve, 100, `Hi ${name}`))
}
}
const person = new Person()
person.$.greeting.subscribe(({params, result}) => {
console.log(`method was called with ${params} and response was "${result}"`) // => method was called with ['Carlos'] and response was "Hi Carlos"
})
person.$.greetingAsync.subscribe(({params, result}) => {
console.log(`method was called with ${params} and response was "${result}"`) // => method was called with ['Carlos'] and response was "Hi Carlos"
})
person.greeting('Carlos')
person.greetingAsync('Carlos')
There are two types of cache handlers (Memory LRU and Redis LRU):
Creates a cache of calls to any method of a class, only when the response is not an error.
import {cache} from '@s-ui/decorators'
class Dummy {
@cache()
syncRndNumber(num) {
return Math.random()
}
}
const dummy = new Dummy()
const firstCall = dummy.syncRndNumber()
const secondCall = dummy.syncRndNumber()
// => firstCall === secondCall
Dump cache to console if setting to truthy 'dumpCache' key in localStorage:
localStorage.__dumpCache__ = true
By default the TTL for the keys in the cache is 500ms, but it can be changed with the ttl
option.
import {cache} from '@s-ui/decorators'
class Dummy {
@cache({ttl: 2000})
syncRndNumber(num) {
return Math.random()
}
}
For this method the cache is of 2 seconds.
It is possible to set TTL using a string with the format ttl: 'XXX [second|seconds|minute|minutes|hour|hours]'
,
thus, avoiding writing very large integers.
It creates a cache of the decorated method response of a class, only when the response is not an error. You must decorate methods that return a promise and its resolved value is a plain javascript object, a JSON, or a simple type (number, string...).
If you are using Redis cache decorator in a sui-domain extended project, you should decorate UseCase
classes execute
methods which are the ones returning plain JSON objects.
Note: Redis cache only works in server side.
import {UseCase} from '@s-ui/domain'
import {inlineError, cache} from '@s-ui/decorators'
export class GetSeoTagsSearchUseCase extends UseCase {
@cache({
server: true,
ttl: '1 minute',
redis: {host: 'localhost', port: 6379}
})
@inlineError
async execute({adSearchParamsAggregate}) {
const [seoTagsError, seoTagsResponse] = await this._service.execute({
adSearchParamsAggregate
})
if (seoTagsError) {
return Promise.reject(seoTagsError)
}
return seoTagsResponse?.toJSON()
}
}
To have Redis cache fully working, previously a Redis server should be up and running, you must set server
flag to true
and provide desired redis
server connection config: @cache({server: true, redis: {host: YOUR_REDIS_HOST, port: YOUR_REDIS_PORT_NUMBER}})
, if one of these params is not provided redis cache won't be activated.
To do real requests against your Redis server you must set USE_REDIS_IN_SUI_DECORATORS_CACHE
variable to true
(process.env.USE_REDIS_IN_SUI_DECORATORS_CACHE
in SSR).
You can add it in your web-app config-[dev|pre|pro]
file as USE_REDIS_IN_SUI_DECORATORS_CACHE: true
, or as you wish.
In case you want to pass tests against a real redis server you should set this variable, otherwise tests are running against a mocked redis.
This decorator will look for a USE_VERSION_NAMESPACE_FOR_REDIS_SUI_DECORATORS_CACHE
variable in the host, global.USE_VERSION_NAMESPACE_FOR_REDIS_SUI_DECORATORS_CACHE
in SSR. This variable will add a version tag namespace in the cache key stored in Redis, it would be helpful to avoid not cleaned cache entries for different web-app deployed versions. If it's not decalred cache entry will be stored without version namespace in the key.
Common for both LRU and Redis:
-
ttl: Time to life for each cache register (default:
500ms
) -
server: If the cache will be used in a NodeJS env. Be careful that could break your server. You should set it to true if you are adding redis config and want to activate redis cache. (default:
false
) -
algorithm: Which algorithm will be used to discard register in the cache when will be full. For now, only
lru
available. (default:lru
) -
size: Maximum number of registers in the cache, when they exceed this number they will be erased (default:
100
) -
cacheKeyString: String param containing cache key(it must be unique). It is useful to define a fixed cache key(constructor name + function name, e.g.
cacheKeyString: GetAdListSearchUseCase#execute
) and avoid problems with code minification. By default the following cache key will be created for${target.constructor.name}::${fnName}
(default:undefined
)
Only for Redis:
- redis: desired redis server connection config
@cache({server: true, redis: {host: YOUR_REDIS_HOST, port: YOUR_REDIS_PORT_NUMBER}})
. (default:undefined
, ifredis={} -> {host: '127.0.0.1', port: 6379}
) Rememberserver
flag must be true andprocess.env.USE_REDIS_IN_SUI_DECORATORS_CACHE
must be setted to true to connect to the provided redis server.
In some cases we might want to disable the cache
for certain environment or testing purposes. In that case, we should expose a variable into the global scope as:
// For client side
window.__SUI_CACHE_DISABLED__ = true
// Server side
global.__SUI_CACHE_DISABLED__ = true
Sends a performance timing metric to the configured reporter.
import {tracer} from '@s-ui/decorators'
class SomeUseCase {
@tracer({metric: 'METRIC_1'})
execute({input}) {
return ...
}
}
This decorator will look for a __SUI_DECORATOR_TRACER_REPORTER__
variable in the host (window.__SUI_DECORATOR_TRACER_REPORTER__
in browser/`global.SUI_DECORATOR_TRACER_REPORTER in SSR).
If no reporter defined is found it will use the default ConsoleReporter
which will output the messages in console.
Also, the tracer provides a `DataDogReporter which implements the Reporter Interface. This reporter needs a client to be passed to the reporter constructor. In this case, we are using hot-shots, which is a StatsD compatible client.
Note: be sure to define this in a server-only executed file.
import {DataDogReporter} from '@s-ui/decorators/lib/decorators/tracer'
import StatsD from 'hot-shots'
global.__SUI_DECORATOR_TRACER_REPORTER__ = new DataDogReporter({
client: new StatsD({
errorHandler: error => {
console.log('Socket errors caught here: ', error)
},
globalTags: {
env: process.env.NODE_ENV,
node_ssr: 'milanuncios',
origin: 'server'
}
}),
siteName: 'ma'
})
The provided DataDogReporter
accepts a siteName
parameter that will be appended to the metric name:
frontend.${siteName}.tracer.datadog.reporter
, so we could look for our metric in datadog as frontend.ma.tracer.datadog.reporter
.
After having the reporter configured, you need to add the @tracer
in the useCases / methods you want to be
measured. The tracer uses the Performance API.
import {UseCase} from '@s-ui/domain'
import {inlineError, tracer} from '@s-ui/decorators'
export class GetAdSearchParamsFromURLSearchUseCase extends UseCase {
...
@tracer()
@inlineError
async execute({path}) {
The decorator accepts an optional metric
parameter that will be sent to the reporter.
import {UseCase} from '@s-ui/domain'
import {inlineError, tracer} from '@s-ui/decorators'
export class GetAdSearchParamsFromURLSearchUseCase extends UseCase {
...
@tracer({metric: 'get_search_params'})
@inlineError
async execute({path}) {
The @tracer
decorator works fine with the @inlineError
decorator, but it should be placed first:
(...)
@tracer({metric: 'metric_1'})
@inlineError
async execute({path}) {
...
Used in a Class or Method, it will help you to mark code as deprecated and follow logs and add monitoring to check if someone is using the code before to be removed. It can be usefull for refactors.
To enable the possibility of monitor those logs, you should add a reporter to the global scope. This reporter will be called each time the method with the decorator is called. See the example below to see how to do it.
This decorator needs 2 parameters:
message
: A message to be shown in the console when the method is called.key
: Used to identify the log when you add monitoring.
Note
We suggest you to put some unique Keys and explicit messages to make your life easier on detect which methos is being used and avoid duplicated warning messages on your CLI.
import {Deprecated} from '@s-ui/decorators'
// Using the decorator in a method
class Buzz {
@Deprecated({key: 'method', message: 'method is deprecated, use newMethod instead'})
method() {
return Promise.reject(new Error('KO'))
}
}
// Using the decorator in a class
@Deprecated({key: 'class', message: 'Buzz class is deprecated, use X class instead'})
class Buzz {
method() {
return Promise.reject(new Error('KO'))
}
}
const deprecatedLogsMiddleware = ({key, message}) => {
console.warn(`Deprecated ==> key: ${key} - message: ${message}`)
// Here you send this data to your monitoring tool
}
global.__SUI_DECORATOR_DEPRECATED_REPORTER__ = deprecatedLogsMiddleware
window.__SUI_DECORATOR_DEPRECATED_REPORTER__ = deprecatedLogsMiddleware