General sensenet independent client side utilities
# Yarn
yarn add @sensenet/client-utils
# NPM
npm install @sensenet/client-utils
You can implement disposable resources and use them with a using() or usingAsync() syntax. Example:
class Resource implements Disposable {
dispose() {
// cleanup logics
}
}
using(new Resource(), resource => {
// do something with the resource
})
usingAsync(new Resource(), async resource => {
// do something with the resource, allows awaiting promises
})
You can track value changes using with this simple Observable implementation.
Example:
const observableValue = new ObservableValue<number>(0)
const observer = observableValue.subscribe(newValue => {
console.log('Value changed:', newValue)
})
// To update the value
observableValue.setValue(Math.random())
// if you want to dispose a single observer
observer.dispose()
// if you want to dispose the whole observableValue with all of its observers:
observableValue.dispose()
The class contains small helper methods for path transformation and manipulation.
Retrier is a utility that can keep trying an operation until it succeeds, times out or reach a specified retry limit.
const funcToRetry: () => Promise<boolean> = async () => {
const hasSucceeded = false
// ...
// custom logic
// ...
return hasSucceeded
}
const retrierSuccess = await Retrier.create(funcToRetry)
.setup({
Retries: 3,
RetryIntervalMs: 1,
timeoutMs: 1000,
})
.run()
Trace is an utility that can be used to track method calls, method returns and errors
const methodTracer: Disposable = Trace.method({
object: myObjectInstance, // You can define an object constructor for static methods as well
method: myObjectInstance.method, // The method to be tracked
methodName: 'method', // Unique identifier for the method
isAsync: true, // if you set to async, method finished will be *await*-ed
onCalled: traceData => {
console.log('Method called:', traceData)
},
onFinished: traceData => {
console.log('Method call finished:', traceData)
},
onError: traceData => {
console.log('Method throwed an error:', traceData)
},
})
// if you want to stop receiving events
methodTracer.dispose()
You can start using the Logging service with an injector in the following way
import { ConsoleLogger, Injector } from '@sensenet/client-utils'
const myInjector = new Injector().useLogging(ConsoleLogger, Logger1, Logger2 /** ...your Logger implementations */)
You can retrieve the Logger instance with
const myLogger = myInjector.logger
You can log a simple event with
myLogger.addEntry({
level: LogLevel.Verbose,
message: 'My log message',
scope: 'logger/test',
data: {
foo: 1,
bar: 42,
},
})
or
myLogger.verbose({
message: 'My log message',
scope: '@sensenet/client-utils/test',
data: {
foo: 1,
bar: 42,
},
})
The two snippets do the same - they will add a log entry to each registered logger.
At the most of the cases, you use a logger in a service with a specific scope. You can create and use a scoped logger in the following way
const scopedLogger = myLogger.withScope('logger/test')
scopedLogger.verbose({ message: 'FooBarBaz' })
You can implement your own logging logic in the similar way as this custom log collector
import { AbstractLogger, Injectable, LeveledLogEntry } from '@sensenet/client-utils'
@Injectable({ lifetime: 'singleton' })
export class MyCustomLogCollector extends AbstractLogger {
private readonly entries: Array<LeveledLogEntry<any>> = []
public getEntries() {
return [...this.entries]
}
public async addEntry<T>(entry: LeveledLogEntry<T>): Promise<void> {
this.entries.push(entry)
}
constructor() {
super()
}
}
Injectors act as containers, they are responsible for creating / retrieving service instances based on the provided Injectable metadata. You can create an injector with simply instantiating the class
const myInjector = new Injector()
You can organize your injector(s) in trees by creating child injectors. You can use the children and services with scoped lifetime for contextual services.
const childInjector = myInjector.createChild({ owner: 'myCustomContext' })
You can create an injectable service from a plain class when decorating with the @Injectable()
decorator.
@Injectable({
/** Injectable options */
})
export class MySercive {
/** ...service implementation... */
constructor(s1: OtherInjectableService, s2: AnotherInjectableService) {}
}
The constructor parameters (s1: OtherInjectableService
and s2: AnotherInjectableService
) should be also decorated and will be resolved recursively.
You can define a specific Lifetime for Injectable services on the decorator
@Injectable({
lifetime: 'transient',
})
export class MySercive {
/** ...service implementation... */
}
The lifetime can be
- transient - A new instance will be created each time when you get an instance
- scoped - A new instance will be created if it doesn't exist on the current scope. Can be useful for injectable services that can be used for contextual data.
- singleton - A new instance will be created only if it doesn't exists on the root injector. It will act as a singleton in other cases.
Injectables can only depend on services with longer lifetime, e.g. a transient can depend on a singleton, but inversing it will throw an error
You can retrieve a service by calling
const service = myInjector.getInstance(MySercive)
There are cases that you have to set a service instance explicitly. You can do that in the following way
class MyService {
constructor(public readonly foo: string)
}
myInjector.setExplicitInstance(new MyService('bar'))