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

Commit d6895dd

Browse files
committed
Implement MultiContextUpKey
1 parent 7782432 commit d6895dd

File tree

3 files changed

+157
-26
lines changed

3 files changed

+157
-26
lines changed

src/context-up-key.spec.ts

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AfterEvent, afterEventOf } from 'fun-events';
22
import { ContextKeyError } from './context-key-error';
33
import { ContextRegistry } from './context-registry';
4-
import { SingleContextUpKey } from './context-up-key';
4+
import { MultiContextUpKey, SingleContextUpKey } from './context-up-key';
55
import { ContextValues } from './context-values';
66

77
describe('ContextUpKey', () => {
@@ -93,15 +93,85 @@ describe('ContextUpKey', () => {
9393

9494
expect(readValue(values.get(keyWithDefaults))).toBe(defaultValue);
9595
});
96+
97+
function readValue<Value>(from: AfterEvent<[Value]>): Value {
98+
99+
let received: Value = undefined!;
100+
101+
from.once(value => received = value);
102+
103+
return received;
104+
}
96105
});
97106

98-
});
107+
describe('MultiContextUpKey', () => {
99108

100-
function readValue<Value>(from: AfterEvent<[Value]>): Value {
109+
let key: MultiContextUpKey<string>;
101110

102-
let received: Value = undefined!;
111+
beforeEach(() => {
112+
key = new MultiContextUpKey('values');
113+
});
114+
115+
it('is associated with empty array by default', () => {
116+
expect(readValue(values.get(key))).toHaveLength(0);
117+
});
118+
it('is associated with default value if there is no provider', () => {
103119

104-
from.once(value => received = value);
120+
const defaultValue = ['default'];
121+
const keyWithDefaults = new MultiContextUpKey('key', { byDefault: () => defaultValue });
105122

106-
return received;
107-
}
123+
expect(readValue(values.get(keyWithDefaults))).toEqual(defaultValue);
124+
});
125+
it('is associated with default value if providers did not return any values', () => {
126+
127+
const defaultValue = ['default'];
128+
const keyWithDefaults = new MultiContextUpKey('key', { byDefault: () => defaultValue });
129+
130+
registry.provide({ a: keyWithDefaults, is: null });
131+
registry.provide({ a: keyWithDefaults, is: undefined });
132+
133+
expect(readValue(values.get(keyWithDefaults))).toEqual(defaultValue);
134+
});
135+
it('is associated with provided values array', () => {
136+
registry.provide({ a: key, is: 'a' });
137+
registry.provide({ a: key, is: undefined });
138+
registry.provide({ a: key, is: 'c' });
139+
140+
expect(readValue(values.get(key))).toEqual(['a', 'c']);
141+
});
142+
it('is associated with value', () => {
143+
registry.provide({ a: key, is: 'value' });
144+
145+
expect(readValue(values.get(key))).toEqual(['value']);
146+
});
147+
it('is associated with fallback value if there is no value provided', () => {
148+
expect(readValue(values.get(key, { or: afterEventOf('fallback') }))).toEqual(['fallback']);
149+
});
150+
it('prefers fallback value over default one', () => {
151+
expect(readValue(
152+
values.get(
153+
new MultiContextUpKey<string>(
154+
key.name,
155+
{ byDefault: () => ['default', 'value'] }
156+
),
157+
{ or: afterEventOf('fallback', 'value') },
158+
)
159+
)).toEqual(['fallback', 'value']);
160+
});
161+
it('throws if fallback value is `null`', () => {
162+
expect(() => readValue(values.get(key, { or: null })!)).toThrowError(ContextKeyError);
163+
});
164+
it('throws if fallback value is `undefined`', () => {
165+
expect(() => readValue(values.get(key, { or: undefined })!)).toThrowError(ContextKeyError);
166+
});
167+
168+
function readValue<Src>(from: AfterEvent<Src[]>): Src[] {
169+
170+
let received: Src[] = undefined!;
171+
172+
from.once((...value) => received = value);
173+
174+
return received;
175+
}
176+
});
177+
});

src/context-up-key.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,80 @@ export class SingleContextUpKey<Value> extends ContextUpKey<AfterEvent<[Value]>,
181181
return backup; // Backup value found.
182182
}
183183

184-
// Backup value is absent. Construct a error response.
184+
// Backup value is absent. Construct an error response.
185185
return afterEventBy<[Value]>(() => {
186186
throw new ContextKeyError(this);
187187
});
188188
});
189189
}
190190

191191
}
192+
193+
/**
194+
* Multiple updatable context values key.
195+
*
196+
* The associated value is an `AfterEvent` registrar of receivers of the source values array. It is always present,
197+
* even though the array can be empty.
198+
*
199+
* It is an error to provide a `null` or `undefined` {@link ContextRequest.Opts.or fallback value} when requesting
200+
* an associated value. Use an `afterEventOf()` result as a fallback instead.
201+
*
202+
* @typeparam Value Context value type.
203+
*/
204+
export class MultiContextUpKey<Src> extends ContextUpKey<AfterEvent<Src[]>, Src> {
205+
206+
/**
207+
* A provider of context value used when there is no value associated with this key.
208+
*/
209+
readonly byDefault: ContextValueProvider<ContextValues, readonly Src[]>;
210+
211+
/**
212+
* Constructs multiple updatable context value key.
213+
*
214+
* @param name Human-readable key name.
215+
* @param seedKey Value seed key. A new one will be constructed when omitted.
216+
* @param byDefault Optional default value provider. If unspecified or `undefined` the key has no default
217+
* value.
218+
*/
219+
constructor(
220+
name: string,
221+
{
222+
seedKey,
223+
byDefault = noop,
224+
}: {
225+
seedKey?: ContextSeedKey<Src | EventKeeper<Src[]>, AfterEvent<Src[]>>,
226+
byDefault?: ContextValueProvider<ContextValues, readonly Src[]>,
227+
} = {}) {
228+
super(name, seedKey);
229+
this.byDefault = byDefault;
230+
}
231+
232+
grow<Ctx extends ContextValues>(
233+
opts: ContextValueOpts<Ctx, AfterEvent<Src[]>, EventKeeper<Src[]> | Src, AfterEvent<Src[]>>,
234+
): AfterEvent<Src[]> {
235+
return opts.seed.keep.dig((...sources) => {
236+
if (sources.length) {
237+
// Sources present. Use them.
238+
return afterEventOf(...sources);
239+
}
240+
241+
// Sources absent. Attempt to detect the backup value.
242+
const backup = opts.byDefault(() => {
243+
244+
const defaultValue = this.byDefault(opts.context);
245+
246+
return defaultValue ? afterEventOf(...defaultValue) : afterEventOf();
247+
});
248+
249+
if (backup != null) {
250+
return backup; // Backup value found.
251+
}
252+
253+
// Backup value is absent. Construct an error response.
254+
return afterEventBy<Src[]>(() => {
255+
throw new ContextKeyError(this);
256+
});
257+
});
258+
}
259+
260+
}

src/simple-context-key.spec.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -194,44 +194,36 @@ describe('SimpleContextKey', () => {
194194

195195
expect(values.get(key)).toEqual(['value']);
196196
});
197+
it('is associated with fallback value if there is no value provided', () => {
198+
expect(values.get(key, { or: ['fallback'] })).toEqual(['fallback']);
199+
});
197200
it('throws if there is no default value', () => {
198201
expect(() => values.get(new MultiContextKey(key.name, { byDefault: () => null }))).toThrowError();
199202
});
200-
it('is associated with empty array by default', () => {
201-
expect(values.get(new MultiContextKey(key.name))).toEqual([]);
202-
});
203-
it('is associated with default value is there is no value', () => {
204-
expect(values.get(new MultiContextKey<string>(key.name), { or: ['default'] }))
205-
.toEqual(['default']);
206-
});
207-
it('is associated with key default value is there is no value', () => {
208-
expect(values.get(new MultiContextKey<string>(key.name, { byDefault: () => ['default'] })))
209-
.toEqual(['default']);
210-
});
211203
it('prefers fallback value over default one', () => {
212204
expect(
213205
values.get(
214206
new MultiContextKey<string>(
215207
key.name,
216-
{ byDefault: () => ['key', 'default'] }
208+
{ byDefault: () => ['default', 'value'] }
217209
),
218-
{ or: ['explicit', 'default'] }
210+
{ or: ['fallback', 'value'] },
219211
)
220-
).toEqual(['explicit', 'default']);
212+
).toEqual(['fallback', 'value']);
221213
});
222214
it('prefers `null` fallback value over default one', () => {
223215
expect(values.get(
224-
new MultiContextKey<string>(key.name, { byDefault: () => ['key', 'default'] }),
225-
{ or: null }
216+
new MultiContextKey<string>(key.name, { byDefault: () => ['default', 'value'] }),
217+
{ or: null },
226218
)).toBeNull();
227219
});
228220
it('prefers `undefined` fallback value over default one', () => {
229221
expect(
230222
values.get(new MultiContextKey<string>(
231223
key.name,
232-
{ byDefault: () => ['key', 'default'] }
224+
{ byDefault: () => ['default', 'value'] }
233225
),
234-
{ or: undefined }
226+
{ or: undefined },
235227
)
236228
).toBeUndefined();
237229
});

0 commit comments

Comments
 (0)