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

Commit bfa8bd4

Browse files
authored
Merge pull request #4 from surol/updatable-context-values
Updatable context values
2 parents facd46c + d6895dd commit bfa8bd4

13 files changed

+728
-192
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"dependencies": {
2222
"a-iterable": "^2.4.2",
2323
"call-thru": "^2.5.2",
24+
"fun-events": "^5.0.3",
2425
"tslib": "^1.10.0"
2526
},
2627
"devDependencies": {

src/context-key-error.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ContextKey } from './context-key';
2+
3+
/**
4+
* An error indicating the absence of context value with the given key.
5+
*/
6+
export class ContextKeyError extends Error {
7+
8+
/**
9+
* A missing value key.
10+
*/
11+
readonly key: ContextKey<any, any, any>;
12+
13+
/**
14+
* Constructs an invalid context key error.
15+
*
16+
* @param key Missing value key.
17+
* @param message Arbitrary error message.
18+
*/
19+
constructor(key: ContextKey<any, any, any>, message: string = `There is no value with key ${key}`) {
20+
super(message);
21+
this.key = key;
22+
}
23+
24+
}

src/context-key.ts

Lines changed: 2 additions & 2 deletions
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.
@@ -124,7 +124,7 @@ export abstract class ContextSeedKey<Src, Seed> extends ContextKey<Seed, Src, Se
124124
*
125125
* @param key A key of context value having its sources associated with this key.
126126
*/
127-
protected constructor(key: ContextKey<any, Src>) {
127+
constructor(key: ContextKey<any, Src>) {
128128
super(`${key.name}:seed`);
129129
}
130130

src/context-registry.spec.ts

Lines changed: 1 addition & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AIterable } from 'a-iterable';
22
import { ContextRegistry } from './context-registry';
33
import { ContextValues } from './context-values';
4-
import { MultiContextKey, SingleContextKey } from './simple-context-key';
4+
import { SingleContextKey } from './simple-context-key';
55
import Mock = jest.Mock;
66

77
describe('ContextRegistry', () => {
@@ -49,182 +49,6 @@ describe('ContextRegistry', () => {
4949
});
5050
});
5151

52-
describe('Single value', () => {
53-
it('provides value', () => {
54-
55-
const value = 'test value';
56-
57-
mockProvider.mockReturnValue(value);
58-
59-
expect(values.get(key)).toBe(value);
60-
});
61-
it('throws if there is neither default nor fallback value', () => {
62-
expect(() => values.get(new SingleContextKey(key.name))).toThrowError();
63-
expect(() => values.get(new SingleContextKey(key.name), {})).toThrowError();
64-
});
65-
it('provides fallback value is there is no provider', () => {
66-
expect(values.get(new SingleContextKey<string>(key.name), { or: 'fallback' })).toBe('fallback');
67-
});
68-
it('provides default value if provider did not provide any value', () => {
69-
70-
const defaultValue = 'default';
71-
const keyWithDefaults = new SingleContextKey(key.name, { byDefault: () => defaultValue });
72-
73-
registry.provide({ a: keyWithDefaults, is: null });
74-
75-
expect(values.get(keyWithDefaults)).toBe(defaultValue);
76-
});
77-
it('provides default value if there is no provider', () => {
78-
expect(values.get(new SingleContextKey<string>(key.name, { byDefault: () => 'default' }))).toBe('default');
79-
});
80-
it('prefers fallback value over default one', () => {
81-
expect(values.get(new SingleContextKey<string>(key.name, { byDefault: () => 'default' }), { or: 'fallback' }))
82-
.toBe('fallback');
83-
});
84-
it('prefers default value if fallback one is absent', () => {
85-
expect(values.get(new SingleContextKey<string>(key.name, { byDefault: () => 'default' }), {}))
86-
.toBe('default');
87-
});
88-
it('prefers `null` fallback value over key one', () => {
89-
expect(values.get(new SingleContextKey<string>(key.name, { byDefault: () => 'default' }), { or: null }))
90-
.toBeNull();
91-
});
92-
it('prefers `undefined` fallback value over key one', () => {
93-
expect(values.get(new SingleContextKey<string>(key.name, { byDefault: () => 'default' }), { or: undefined }))
94-
.toBeUndefined();
95-
});
96-
it('caches the value', () => {
97-
98-
const value = 'value';
99-
100-
mockProvider.mockReturnValue(value);
101-
102-
expect(values.get(key)).toBe(value);
103-
expect(values.get(key)).toBe(value);
104-
105-
expect(mockProvider).toHaveBeenCalledTimes(1);
106-
});
107-
it('caches default value', () => {
108-
109-
const value = 'default value';
110-
const defaultProviderSpy = jest.fn(() => value);
111-
const keyWithDefault = new SingleContextKey('key-with-default', { byDefault: defaultProviderSpy });
112-
113-
expect(values.get(keyWithDefault)).toBe(value);
114-
expect(values.get(keyWithDefault)).toBe(value);
115-
expect(defaultProviderSpy).toHaveBeenCalledTimes(1);
116-
});
117-
it('does not cache fallback value', () => {
118-
119-
const value1 = 'value1';
120-
const value2 = 'value2';
121-
122-
expect(values.get(key, { or: value1 })).toBe(value1);
123-
expect(values.get(key, { or: value2 })).toBe(value2);
124-
});
125-
it('rebuilds the value in another context', () => {
126-
127-
const value1 = 'value1';
128-
const value2 = 'value2';
129-
130-
mockProvider.mockReturnValue(value1);
131-
expect(values.get(key)).toBe(value1);
132-
133-
const values2 = registry.newValues();
134-
135-
mockProvider.mockReturnValue(value2);
136-
expect(values.get(key)).toBe(value1);
137-
expect(values2.get(key)).toBe(value2);
138-
139-
expect(mockProvider).toHaveBeenCalledTimes(2);
140-
});
141-
});
142-
143-
describe('Multi-value', () => {
144-
145-
let multiKey: MultiContextKey<string>;
146-
147-
beforeEach(() => {
148-
multiKey = new MultiContextKey('values');
149-
});
150-
151-
it('is associated with empty array by default', () => {
152-
expect(values.get(multiKey)).toEqual([]);
153-
});
154-
it('is associated with empty array if providers did not return any values', () => {
155-
registry.provide({ a: multiKey, is: null });
156-
registry.provide({ a: multiKey, is: undefined });
157-
158-
expect(values.get(multiKey)).toEqual([]);
159-
});
160-
it('is associated with default value if there is no provider', () => {
161-
162-
const defaultValue = ['default'];
163-
const keyWithDefaults = new MultiContextKey('key', { byDefault: () => defaultValue });
164-
165-
expect(values.get(keyWithDefaults)).toEqual(defaultValue);
166-
});
167-
it('is associated with default value if providers did not return any values', () => {
168-
169-
const defaultValue = ['default'];
170-
const keyWithDefaults = new MultiContextKey('key', { byDefault: () => defaultValue });
171-
172-
registry.provide({ a: keyWithDefaults, is: null });
173-
registry.provide({ a: keyWithDefaults, is: undefined });
174-
175-
expect(values.get(keyWithDefaults)).toEqual(defaultValue);
176-
});
177-
it('is associated with provided values array', () => {
178-
registry.provide({ a: multiKey, is: 'a' });
179-
registry.provide({ a: multiKey, is: undefined });
180-
registry.provide({ a: multiKey, is: 'c' });
181-
182-
expect(values.get(multiKey)).toEqual(['a', 'c']);
183-
});
184-
it('is associated with value', () => {
185-
registry.provide({ a: multiKey, is: 'value' });
186-
187-
expect(values.get(multiKey)).toEqual(['value']);
188-
});
189-
it('throws if there is no default value', () => {
190-
expect(() => values.get(new MultiContextKey(multiKey.name, { byDefault: () => null }))).toThrowError();
191-
});
192-
it('is associated with empty array by default', () => {
193-
expect(values.get(new MultiContextKey(multiKey.name))).toEqual([]);
194-
});
195-
it('is associated with default value is there is no value', () => {
196-
expect(values.get(new MultiContextKey<string>(multiKey.name), { or: ['default'] }))
197-
.toEqual(['default']);
198-
});
199-
it('is associated with key default value is there is no value', () => {
200-
expect(values.get(new MultiContextKey<string>(multiKey.name, { byDefault: () => ['default'] })))
201-
.toEqual(['default']);
202-
});
203-
it('prefers fallback value over default one', () => {
204-
expect(values.get(
205-
new MultiContextKey<string>(
206-
multiKey.name,
207-
{ byDefault: () => ['key', 'default'] }
208-
),
209-
{ or: ['explicit', 'default'] }
210-
)).toEqual(['explicit', 'default']);
211-
});
212-
it('prefers `null` fallback value over default one', () => {
213-
expect(values.get(
214-
new MultiContextKey<string>(multiKey.name, { byDefault: () => ['key', 'default'] }),
215-
{ or: null }
216-
)).toBeNull();
217-
});
218-
it('prefers `undefined` fallback value over default one', () => {
219-
expect(values.get(new MultiContextKey<string>(
220-
multiKey.name,
221-
{ byDefault: () => ['key', 'default'] }
222-
),
223-
{ or: undefined }
224-
)).toBeUndefined();
225-
});
226-
});
227-
22852
describe('Providers combination', () => {
22953

23054
let provider2Spy: Mock;

src/context-registry.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ContextRef, ContextRequest } from './context-ref';
77
import { ContextSeeder, ContextSeeds } from './context-seeder';
88
import { contextValueSpec, ContextValueSpec } from './context-value-spec';
99
import { ContextValues } from './context-values';
10+
import { ContextKeyError } from './context-key-error';
1011

1112
type SeedFactory<Ctx extends ContextValues, Seed> = (this: void, context: Ctx) => Seed;
1213

@@ -53,13 +54,15 @@ export class ContextRegistry<Ctx extends ContextValues = ContextValues> {
5354
* @typeparam Src Source value type.
5455
* @typeparam Seed Value seed type.
5556
* @param spec Context value specifier.
57+
*
58+
* @returns A function that removes the given context value specifier when called.
5659
*/
57-
provide<Deps extends any[], Src, Seed>(spec: ContextValueSpec<Ctx, any, Deps, Src, Seed>): void {
60+
provide<Deps extends any[], Src, Seed>(spec: ContextValueSpec<Ctx, any, Deps, Src, Seed>): () => void {
5861

5962
const { a: { [ContextKey__symbol]: { seedKey } }, by } = contextValueSpec(spec);
6063
const [seeder] = this._seeding<Src, Seed>(seedKey);
6164

62-
seeder.provide(by);
65+
return seeder.provide(by);
6366
}
6467

6568
/**
@@ -182,7 +185,7 @@ export class ContextRegistry<Ctx extends ContextValues = ContextValues> {
182185
const defaultValue = defaultProvider();
183186

184187
if (defaultValue == null) {
185-
throw new Error(`There is no value with key ${key}`);
188+
throw new ContextKeyError(key);
186189
}
187190

188191
return defaultValue;

src/context-seeder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ export interface ContextSeeder<Ctx extends ContextValues, Src, Seed> {
2323
* Provides context value.
2424
*
2525
* @param provider Context value provider.
26+
*
27+
* @returns A function that removes the given context value `provider` when called.
2628
*/
27-
provide(provider: ContextValueProvider<Ctx, Src>): void;
29+
provide(provider: ContextValueProvider<Ctx, Src>): () => void;
2830

2931
/**
3032
* Creates context value seed for target `context`.

0 commit comments

Comments
 (0)