README
IoC Context Values Provider
This library allows to construct an IoC context, other components can request values from.
An IoC context is an object with get()
method implemented. This method returns a context value by its key.
Accessing Context Values
A context should implement a ContextValues
interface. This interface declares a get()
method accepting a
ContextRequest
identifying the requested value (e.g. a ContextKey
instance), and a non-mandatory options.
The following code returns a string value associated with key
, or throws an exception if the value not found.
import { SingleContextKey } from 'context-values';
const key = new SingleContextKey<string>('my-key');
myContext.get(key)
Fallback Value
Normally, if the value associated with the given key can not be found, an exception is thrown. To avoid this, a fallback value can be provided. It will be returned if the value not found.
import { SingleContextKey } from 'context-values';
const key = new SingleContextKey<string>('my-key');
myContext.get(key, { or: 'empty' });
Context Value Request
The get()
method accepts not only a ContextKey
instance, but arbitrary ContextRequest
. The latter is just an
object with key
property containing a ContextKey
instance to find.
This can be handy e.g. when requesting an instance of some known type:
import { ContextKey, SingleContextKey } from 'context-values';
class MyService {
// MyService class (not instance) implements a `ContextRequest`
static readonly key: ContextKey<MyService> = new SingleContextKey('my-service');
}
myContext.get(MyService); // No need to specify `MyService.key` here
myContext.get(MyService.key); // The same as above.
Providing Context Values
Context values can be provided using ContextRegistry
.
Then the values can be requested from ContextValues
instance constructed by the newValues()
method of the registry.
import { ContextRegistry, SingleContextKey } from 'context-values';
const key1 = new SingleContextKey<string>('key1');
const key2 = new SingleContextKey<number>('key2');
const registry = new ContextRegistry();
registry.provide({ a: key1, is: 'string' });
registry.provide({ a: key2, by: ctx => ctx.get(key1).length })
const context = registry.newValues();
context.get(key1); // 'string'
context.get(key2); // 6
Context Value Target
The provide()
method accepts not only a ContextKey
instance, but arbitrary ContextTarget
. The latter is just an
object with [ContextKey__symbol]
property containing a ContextKey
to provide.
This can be handy e.g. when providing an instance of some known type:
import { ContextKey, ContextKey__symbol, ContextRegistry, SingleContextKey } from 'context-values';
class MyService {
// MyService class (not instance) implements a `ContextRequest`
static readonly [ContextKey__symbol]: ContextKey<MyService> = new SingleContextKey('my-service');
}
const registry = new ContextRegistry();
registry.provide({ a: MyService, is: new MyService() });
const context = registry.newValues();
context.get(MyService); // No need to specify `MyService.key` here
Context Value Specifier
The provide()
method of ContextRegistry
accepts a context value specifier as its only parameter.
This specifier defines a value (or, more precisely, the value sources). It may specify the value in a different ways:
registry.provide({ a: key, is: value })
- provides the value explicitly.registry.provide({ a: key, by: ctx => calculateValue(ctx) })
- evaluates the value in most generic way.ctx
here is the target context.registry.provide({ a: key, by: (a, b) => calculateValue(a, b), with: [keyA, keyB] })
- evaluates the value based on other context values with keyskeyA
andkeyB
.registry.provide({ a: key, as: MyService })
- constructs the value asnew MyService(ctx)
, wherectx
is the target context. Thea
property may be omitted ifMyService
has a static[ContextKey__symbol]
property. See Context Value Target.registry.provide({ a: key, as: MyService, with: [keyA, keyB] })
- constructs the value asnew MyService(a, b)
, wherea
andb
are context values with keyskeyA
andkeyB
respectively. Thea
property may be omitted ifMyService
has a static[ContextKey__symbol]
property. See Context Value Target.registry.provide({ a: key, via: otherKey })
- makes the value available underotherKey
available underkey
. I.e. aliases it.
Context Value Key
Context value keys identify context values.
They extend a ContextKey
abstract class. The following implementations are available:
SingleContextKey
that allows associate a single value with it, andMultiContextKey
that allows to associate multiple values with it.
import { ContextRegistry, SingleContextKey, MultiContextKey } from 'context-values';
const key1 = new SingleContextKey<string>('key1');
const key2 = new MultiContextKey<number>('key2');
const registry = new ContextRegistry();
registry.provide({ a: key1, is: 'value1' });
registry.provide({ a: key1, is: 'value2' });
registry.provide({ a: key2, is: 1 });
registry.provide({ a: key2, is: 2 });
const context = registry.newValues();
context.get(key1); // 'value2' - SingleContextKey uses the latest value provided
context.get(key2); // [1, 2] - MultiContextKey returns all provided values as an array
Default Value
Context value key may declare a default value. It will be evaluated and returned when the value is not found and no fallback value specified in the request.
The default value is evaluated by the function accepting a ContextValues
instance as its only argument.
import { ContextRegistry, SingleContextKey, MultiContextKey } from 'context-values';
const key1 = new SingleContextKey<string>('key1');
const key2 = new SingleContextKey<number>('key2', { byDefault: ctx => ctx.get('key1').length });
const key3 = new MultiContextKey<number>('key3');
const registry = new ContextRegistry();
registry.provide({ a: key1, is: 'value' });
const context = registry.newValues();
context.get(key1); // 'value'
context.get(key2); // 6 - evaluated, as it is not provided
context.get(key2, { or: null }); // null - fallback value always takes precedence
context.get(key3); // [] - MultiContextKey uses it as a default value, unless explicitly specified
registry.provide({ a: key2, value: 999 });
context.get(key2); // 999 - provided explicitly
Custom Context Key
It is possible to implement a custom ContextKey
.
For that extend e.g. a SimpleContextKey
that implements the boilerplate. The only method left to implement then is a
grow()
one.
The grow()
method takes a single ContextValueOpts
parameter containing the value construction options and returns
a context value constructed out of the provided value sources.
Value Sources and Seeds
Instead of the value itself, the registry allows to provide its sources. Those are combined into value seed. That is passed to ContextKey.grow() method to construct the value (or grow it from the seed).
There could be multiple sources per single value. And they could be of different type.
For example, the seed of SimpleValueKey
and MultiValueKey
is an AIterable instance. The latter is enhanced
Iterable
with Array-like API, including map()
, flatMap()
, forEach()
, and other methods.
import { AIterable } from 'a-iterable';
import {
ContextRegistry,
ContextValueOpts,
ContextValues,
SimpleContextKey,
} from 'context-values';
class ConcatContextKey<Src> extends SimpleContextKey<string, Src> {
constructor(name: string) {
super(name);
}
grow<Ctx extends ContextValues>(
opts: ContextValueOpts<Ctx, string, Src, AIterable<Src>>,
): string | null | undefined {
const result = opts.seed.reduce((p, s) => p != null ? `${p}, ${s}` : `${s}`, null);
if (result != null) {
return result;
}
// No sources provided. Returning empty string, unless a fallback value provided.
return opts.byDefault(() => '');
}
}
const key1 = new ConcatContextKey<number>('my-numbers');
const key2 = new ConcatContextKey<string>('my-string');
const registry = new ContextRegistry();
registry.provide({ a: key1, is: 1 });
registry.provide({ a: key1, is: 2 });
registry.provide({ a: key1, is: 3 });
const context = registry.newValues();
context.get(key1); // '1, 2, 3' - concatenated value
context.get(key2); // '' - empty string by default
context.get(key2, { or: undefined }); // undefined - fallback value
A context value for particular key is constructed at most once. Thus, the grow()
method is called at most once per
key.
A context value specifier is consulted at most once per key. And only when the grow()
method requested the source value. So, for example, if multiple sources specified for the same SingleContextKey
, only
the last one will be constructed and used as a context value. The rest of them won't be constructed at all.
Updatable Context Values
A SimpleContextKey
, and thus SingleContextKey
and MultiContextKey
extending it, imply that once the associated
context value constructed, it no longer changes. I.e. event though more sources provided for the same key in
ContextRegistry
, they won't affect the already constructed value.
However, it is possible to update context values. For that a ContextUpKey
abstract context value key implementation
may be used, or SingleContextUpKey
and MultiContextUpKey
implementations.
They provide an AfterEvent keeper of value. The receivers registered in this keeper would receive the actual value
each time it changes. E.g. when new value source is provided in ContextRegistry
.
This functionality is implemented in context-value/updatable
sub-module and depends on fun-events
.
import { ContextRegistry } from 'context-values';
import { SingleContextUpKey } from 'context-values/updatable';
const key = new SingleContextUpKey<string>('updatable-value');
const registry = new ContextRegistry();
registry.provide({ a: key, is: 'initial' });
const values = registry.newValues();
values.get(key)(value => console.log(value)); // Log: 'initial'
registry.provide({ a: key, is: 'updated' }); // Log: 'updated'