Skip to content
This repository was archived by the owner on Sep 17, 2023. It is now read-only.

Commit 4da8938

Browse files
committed
Introduce ContextUpKey
An updatable context value key.
1 parent e25c19b commit 4da8938

File tree

5 files changed

+257
-2
lines changed

5 files changed

+257
-2
lines changed

src/context-key.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export interface ContextValueOpts<Ctx extends ContextValues, Value, Src, Seed> {
102102
/**
103103
* Handles missing context value.
104104
*
105-
* It can be called to prefer a fallback value over default one specified by the value key.
105+
* It can be called to prefer a fallback value over the default one specified in the value key.
106106
*
107107
* @param defaultProvider Default value provider. It is called unless a fallback value is specified.
108108
* If it returns a non-null/non-undefined value, then the returned value will be associated with the context key.

src/context-registry.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('ContextRegistry', () => {
6363
expect(() => values.get(new SingleContextKey(key.name))).toThrowError(ContextKeyError);
6464
expect(() => values.get(new SingleContextKey(key.name), {})).toThrowError(ContextKeyError);
6565
});
66-
it('provides fallback value is there is no provider', () => {
66+
it('provides fallback value if there is no provider', () => {
6767
expect(values.get(new SingleContextKey<string>(key.name), { or: 'fallback' })).toBe('fallback');
6868
});
6969
it('provides default value if provider did not provide any value', () => {

src/context-up-key.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { AfterEvent, afterEventOf } from 'fun-events';
2+
import { ContextKeyError } from './context-key-error';
3+
import { ContextRegistry } from './context-registry';
4+
import { SingleContextUpKey } from './context-up-key';
5+
import { ContextValues } from './context-values';
6+
7+
describe('ContextUpKey', () => {
8+
9+
let registry: ContextRegistry<ContextValues>;
10+
let values: ContextValues;
11+
12+
beforeEach(() => {
13+
registry = new ContextRegistry();
14+
values = registry.newValues();
15+
});
16+
17+
describe('SingleContextUpKey', () => {
18+
19+
let key: SingleContextUpKey<string>;
20+
21+
beforeEach(() => {
22+
key = new SingleContextUpKey('test-key');
23+
});
24+
25+
it('provides a value specified verbatim', () => {
26+
27+
const value = 'test value';
28+
29+
registry.provide({ a: key, is: value });
30+
expect(readValue(values.get(key))).toBe(value);
31+
});
32+
it('provides a value specified by event keeper', () => {
33+
34+
const value = 'test value';
35+
36+
registry.provide({ a: key, is: afterEventOf(value) });
37+
expect(readValue(values.get(key))).toBe(value);
38+
});
39+
it('throws if there is neither default nor fallback value', () => {
40+
expect(() => readValue(values.get(key))).toThrowError(ContextKeyError);
41+
expect(() => readValue(values.get(key, {})!)).toThrowError(ContextKeyError);
42+
});
43+
it('throws if fallback value is `null`', () => {
44+
expect(() => readValue(values.get(key, { or: null })!)).toThrowError(ContextKeyError);
45+
});
46+
it('throws if fallback value is `undefined`', () => {
47+
expect(() => readValue(values.get(key, { or: undefined })!)).toThrowError(ContextKeyError);
48+
});
49+
it('provides fallback value if there is no provider', () => {
50+
expect(readValue(values.get(key, { or: afterEventOf('fallback') }))).toBe('fallback');
51+
});
52+
it('provides default value if provider did not provide any value', () => {
53+
54+
const defaultValue = 'default';
55+
const keyWithDefaults = new SingleContextUpKey<string>(key.name, { byDefault: () => defaultValue });
56+
57+
registry.provide({ a: keyWithDefaults, is: null });
58+
59+
expect(readValue(values.get(keyWithDefaults))).toBe(defaultValue);
60+
});
61+
});
62+
63+
});
64+
65+
function readValue<Value>(from: AfterEvent<[Value]>): Value {
66+
67+
let received: Value = undefined!;
68+
69+
from.once(value => received = value);
70+
71+
return received;
72+
}

src/context-up-key.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { flatMapIt, mapIt, overArray } from 'a-iterable';
2+
import { asis, NextArgs, nextArgs, noop } from 'call-thru';
3+
import {
4+
AfterEvent,
5+
afterEventBy,
6+
afterEventFrom,
7+
afterEventFromEach,
8+
afterEventOf,
9+
EventKeeper,
10+
isEventKeeper,
11+
trackValue,
12+
ValueTracker,
13+
} from 'fun-events';
14+
import { ContextKey, ContextSeedKey, ContextValueOpts } from './context-key';
15+
import { ContextSeeder } from './context-seeder';
16+
import { ContextValueProvider } from './context-value-spec';
17+
import { ContextValues } from './context-values';
18+
import { ContextKeyError } from './context-key-error';
19+
20+
class ContextUpSeeder<Ctx extends ContextValues, Src>
21+
implements ContextSeeder<Ctx, Src | EventKeeper<Src[]>, AfterEvent<Src[]>> {
22+
23+
private readonly _providers: ValueTracker<ContextValueProvider<Ctx, Src | EventKeeper<Src[]>>[]> = trackValue([]);
24+
25+
provide(provider: ContextValueProvider<Ctx, Src | EventKeeper<Src[]>>): void {
26+
this._providers.it = [...this._providers.it, provider];
27+
}
28+
29+
seed(context: Ctx, initial: AfterEvent<Src[]> = afterEventOf<Src[]>()): AfterEvent<Src[]> {
30+
return this.combine(initial, upSrcKeepers(context, this._providers));
31+
}
32+
33+
isEmpty(): boolean {
34+
return false;
35+
}
36+
37+
combine(first: AfterEvent<Src[]>, second: AfterEvent<Src[]>): AfterEvent<Src[]> {
38+
return afterEventFromEach(
39+
first,
40+
second,
41+
).keep.thru(
42+
(...sources) => flatUpSources(sources),
43+
);
44+
}
45+
46+
}
47+
48+
function upSrcKeepers<Ctx extends ContextValues, Src>(
49+
context: Ctx,
50+
providersTracker: ValueTracker<ContextValueProvider<Ctx, Src | EventKeeper<Src[]>>[]>,
51+
): AfterEvent<Src[]> {
52+
return providersTracker.read.keep.dig(
53+
providers => !providers.length ? afterEventOf() : afterEventFromEach(
54+
...mapIt(
55+
mapIt(
56+
overArray(providers),
57+
prov => prov(context),
58+
),
59+
toUpSrcKeeper,
60+
),
61+
).keep.thru(
62+
(...sources) => flatUpSources(sources),
63+
));
64+
}
65+
66+
function toUpSrcKeeper<Src>(src: null | undefined | Src | EventKeeper<Src[]>): AfterEvent<Src[]> {
67+
return src == null ? afterEventOf() : isUpSrcKeeper(src) ? afterEventFrom(src) : afterEventOf(src);
68+
}
69+
70+
function isUpSrcKeeper<Src>(src: Src | EventKeeper<Src[]>): src is EventKeeper<Src[]> {
71+
return (typeof src === 'object' || typeof src === 'function') && isEventKeeper(src as (object | Function));
72+
}
73+
74+
function flatUpSources<Src, NextReturn>(sources: Src[][]): NextArgs<Src[], NextReturn> {
75+
return nextArgs<Src[], NextReturn>(
76+
...flatMapIt(overArray(sources), asis)
77+
);
78+
}
79+
80+
class ContextSeedUpKey<Src> extends ContextSeedKey<Src | EventKeeper<Src[]>, AfterEvent<Src[]>> {
81+
82+
seeder<Ctx extends ContextValues>(): ContextSeeder<Ctx, Src | EventKeeper<Src[]>, AfterEvent<Src[]>> {
83+
return new ContextUpSeeder();
84+
}
85+
86+
}
87+
88+
/**
89+
* Abstract implementation of updatable context value key.
90+
*
91+
* Accepts single value sources and `EventKeeper`s of value source arrays.
92+
*
93+
* Collects value sources into `AfterEvent` registrar of source values array receivers.
94+
*
95+
* @typeparam Value Context value type.
96+
* @typeparam Src Source value type.
97+
*/
98+
export abstract class ContextUpKey<Value, Src> extends ContextKey<Value, Src | EventKeeper<Src[]>, AfterEvent<Src[]>> {
99+
100+
readonly seedKey: ContextSeedKey<Src | EventKeeper<Src[]>, AfterEvent<Src[]>>;
101+
102+
/**
103+
* Constructs simple context value key.
104+
*
105+
* @param name Human-readable key name.
106+
* @param seedKey Value seed key. A new one will be constructed when omitted.
107+
*/
108+
protected constructor(name: string, seedKey?: ContextSeedKey<Src | EventKeeper<Src[]>, AfterEvent<Src[]>>) {
109+
super(name);
110+
this.seedKey = seedKey || new ContextSeedUpKey(this);
111+
}
112+
113+
}
114+
115+
/**
116+
* Single updatable context value key.
117+
*
118+
* The associated value is an `AfterEvent` registrar of receivers of the last source value. It is always present,
119+
* but signals an [[ContextKeyError]] error on attempt to receive an absent value.
120+
*
121+
* It is an error to provide a `null` or `undefined` {@link ContextRequest.Opts.or fallback value} when requesting
122+
* an associated value. Use an `afterEventOf()` result as a fallback instead.
123+
*
124+
* @typeparam Value Context value type.
125+
*/
126+
export class SingleContextUpKey<Value> extends ContextUpKey<AfterEvent<[Value]>, Value> {
127+
128+
/**
129+
* A provider of context value used when there is no value associated with this key.
130+
*/
131+
readonly byDefault: ContextValueProvider<ContextValues, Value>;
132+
133+
/**
134+
* Constructs single updatable context value key.
135+
*
136+
* @param name Human-readable key name.
137+
* @param seedKey Value seed key. A new one will be constructed when omitted.
138+
* @param byDefault Optional default value provider. If unspecified or `undefined` the key has no default
139+
* value.
140+
*/
141+
constructor(
142+
name: string,
143+
{
144+
seedKey,
145+
byDefault = noop,
146+
}: {
147+
seedKey?: ContextSeedKey<Value | EventKeeper<Value[]>, AfterEvent<Value[]>>,
148+
byDefault?: ContextValueProvider<ContextValues, Value>,
149+
} = {}) {
150+
super(name, seedKey);
151+
this.byDefault = byDefault;
152+
}
153+
154+
grow<Ctx extends ContextValues>(
155+
opts: ContextValueOpts<Ctx, AfterEvent<[Value]>, EventKeeper<Value[]> | Value, AfterEvent<Value[]>>,
156+
): AfterEvent<[Value]> {
157+
return opts.seed.keep.dig((...sources) => {
158+
if (sources.length) {
159+
// Sources present. Take the last one.
160+
return afterEventOf(sources[sources.length - 1]);
161+
}
162+
163+
// Sources absent. Attempt to detect the backup value.
164+
const backup = opts.byDefault(() => {
165+
166+
const defaultValue = this.byDefault(opts.context);
167+
168+
return defaultValue && afterEventOf(defaultValue);
169+
});
170+
171+
if (backup != null) {
172+
return backup; // Backup value found.
173+
}
174+
175+
// Backup value is absent. Construct a error response.
176+
return afterEventBy<[Value]>(() => {
177+
throw new ContextKeyError(this);
178+
});
179+
});
180+
}
181+
182+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './context-key-error';
33
export * from './context-ref';
44
export * from './context-registry';
55
export * from './context-seeder';
6+
export * from './context-up-key';
67
export * from './context-value-spec';
78
export * from './context-values';
89
export * from './simple-context-key';

0 commit comments

Comments
 (0)