The doc is out of date, soon to be updated
actualization in the document meant the result of a function excution, assume the function execution is expensive/or unexpected (for example, we expect limited amount of instances)
factory is the function that used to provide the value
const scope = createScope()
const valueExecutor = provide(() => /* Some value */)
const value = await scope.resolve(valueExecutor)
Factory can be a sync or async function. scope#resolve
is always async ops
Within same the same scope, the value is cached (singleton)
const scope = createScope()
const seed = provide(async () => /* Read from config */)
const hashFnExecutor = map(seed, (seed) => () => /* Do hashing */)
const hashFn = await scope.resolve(hashFnExecutor)
Given this code
map(
dependencies, // static dependency reference
(dependencies) => { // actualized static dependencies
// preparation code, reusable part, cache etc
return (inputParams: any[]): FinalValue => { // Runtime dependencies
/* Implementation */
}
}
)
Example
const scope = createScope()
const seed = provide(async () => /* Read from config */)
const hashFnExecutor = map(seed, (seed) => (value: string) => /* Do hashing */)
const hashFn = await scope.resolve(hashFnExecutor)
// hashFn('value') <-- hashed value
Use combine to group multiple dependencies
const stringValue = provide(() => '1')
const intValue = provide(() => 2)
const combined: Executor<{ stringValue: string, intValue: number }> = combine({ stringValue, intValue })
const use = map(combined, ({ intValue, stringValue}) => { /**/ })
// ^^ int
// ^^ string
// shortcut, works most of the time, soemtime typescript can't resolve it
const use = map({ stringValue, intValue }, ({ intValue, stringValue}) => { /**/ })
const stringValue1 = provide(() => '1')
const stringValue2 = provide(() => '2')
const stringValues: Executor<string[]> = group(stringValue1, stringValue2 )
Use the special scoper
to access to the current scope.
const constantSeed = provide(() => 1)
const randomSeed = provide(() => Math.random())
const seed = map(
combine({
scoper,
constantSeed: value(constantSeed), // wrap inside value so it won't be resolved
randomSeed: value(randomSeed), // wrap inside value so it won't be resolved
}),
async ({
scoper,
constantSeed,
randomSeed
}) => {
if (condition) return await scoper.resolve(constantSeed)
return await scoper.resolve(randomSeed)
}
)
Can also use flat
to resolve Executor<Executor<V>>
// In that case
const seed = flat(map(
combine({
constantSeed: value(constantSeed), // wrap inside value so it won't be resolved
randomSeed: value(randomSeed), // wrap inside value so it won't be resolved
}),
async ({
constantSeed,
randomSeed
}) => condition ? constantSeed : randomSeed
))
Main purpose of submodule is to make the code
- side-effect free (app'll be faster to start)
- testing made easy
- testing should be fast, and easy, mock free and can run in parallel
Scope is the key in this testing technique
There are certain common testing techniques
For example
function tomorrow() {
/** implementation */
}
This function is likely rely on new Date()
to implement. This is a global object and by mocking the global object, the test will not be able to run in parallel and depending on test framework mocking to be able to test.
Rewrite the code into
const newDateFn = value(() => new Date())
const tomorrowFn = map(newDateFn, (newDateFn) => {
/** Implementation **/
})
The implementation is mostly the same, now how to test?
// use ResolveValue to force value of dependency
const scope = createScope()
scope.resolveValue(newDateFn, value(() => /* mock date*/))
const r = await scope.resolve(torrowFn)
r() // <-- day after the mock date