Skip to content

Commit b8a89fe

Browse files
committed
Add a region provider abstraction
1 parent 45815d0 commit b8a89fe

File tree

12 files changed

+3234
-0
lines changed

12 files changed

+3234
-0
lines changed

packages/region-provider/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {chain} from "../lib/chain";
2+
import {fromString} from "../lib/fromString";
3+
4+
describe('chain', () => {
5+
it('should distill many credential providers into one', async () => {
6+
const provider = chain(
7+
fromString('foo'),
8+
fromString('bar'),
9+
);
10+
11+
expect(typeof await provider()).toBe('string');
12+
});
13+
14+
it('should return the resolved value of the first successful promise', async () => {
15+
const provider = chain(
16+
() => Promise.reject(new Error('Move along')),
17+
() => Promise.reject(new Error('Nothing to see here')),
18+
fromString('foo')
19+
);
20+
21+
expect(await provider()).toBe('foo');
22+
});
23+
24+
it('should not invoke subsequent providers one resolves', async () => {
25+
const providers = [
26+
jest.fn(() => Promise.reject(new Error('Move along'))),
27+
jest.fn(() => Promise.resolve('foo')),
28+
jest.fn(() => fail('This provider should not be invoked'))
29+
];
30+
31+
expect(await chain(...providers)()).toBe('foo');
32+
expect(providers[0].mock.calls.length).toBe(1);
33+
expect(providers[1].mock.calls.length).toBe(1);
34+
expect(providers[2].mock.calls.length).toBe(0);
35+
});
36+
37+
it('should reject chains with no links', async () => {
38+
await expect(chain()()).rejects.toMatchObject({
39+
message: 'No providers in chain'
40+
});
41+
});
42+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {fromString} from '../lib/fromString';
2+
3+
describe('fromString', () => {
4+
it('should convert a string into a provider', async () => {
5+
const region = 'region';
6+
const provider = fromString(region);
7+
8+
await expect(provider()).resolves.toBe(region);
9+
});
10+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {memoize} from "../lib/memoize";
2+
3+
describe('memoize', () => {
4+
it('should cache the resolved provider', async () => {
5+
const provider = jest.fn(() => Promise.resolve('foo'));
6+
const memoized = memoize(provider);
7+
8+
expect(await memoized()).toEqual('foo');
9+
expect(provider.mock.calls.length).toBe(1);
10+
expect(await memoized()).toEqual('foo');
11+
expect(provider.mock.calls.length).toBe(1);
12+
});
13+
14+
it('should always return the same promise', () => {
15+
const provider = jest.fn(() => Promise.resolve('foo'));
16+
const memoized = memoize(provider);
17+
const result = memoized();
18+
19+
expect(memoized()).toBe(result);
20+
});
21+
});

packages/region-provider/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './lib/chain';
2+
export * from './lib/fromString';
3+
export * from './lib/memoize';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Provider<T> {
2+
(): Promise<T>;
3+
}

packages/region-provider/lib/chain.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Provider} from "@aws/types";
2+
3+
/**
4+
* Compose a single credential provider function from multiple credential
5+
* providers. The first provider in the argument list will always be invoked;
6+
* subsequent providers in the list will be invoked in the order in which the
7+
* were received if the preceding provider did not successfully resolve.
8+
*
9+
* If no providers were received or no provider resolves successfully, the
10+
* returned promise will be rejected.
11+
*/
12+
export function chain<T>(
13+
...providers: Array<Provider<T>>
14+
): Provider<T> {
15+
return () => {
16+
providers = providers.slice(0);
17+
let provider = providers.shift();
18+
if (provider === undefined) {
19+
return Promise.reject(new Error(
20+
'No providers in chain'
21+
));
22+
}
23+
24+
let promise = provider();
25+
while (provider = providers.shift()) {
26+
promise = promise.catch(provider);
27+
}
28+
29+
return promise;
30+
}
31+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {Provider} from '@aws/types';
2+
3+
export function fromString(region: string): Provider<string> {
4+
const promisified = Promise.resolve(region);
5+
return () => promisified;
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Provider} from "@aws/types";
2+
3+
/**
4+
* Decorates a provider function with static memoization. The underlying
5+
* provider will be invoked once, and all invocations of the returned provider
6+
* will return the same promise object.
7+
*/
8+
export function memoize<T>(provider: Provider<T>): Provider<T> {
9+
let result = provider();
10+
return () => result;
11+
}

packages/region-provider/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@aws/region-provider",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "AWS region provider core. Provides an abstraction for sourcing a region from one or more asynchronous sources",
6+
"main": "index.js",
7+
"scripts": {
8+
"prepublishOnly": "tsc",
9+
"pretest": "tsc",
10+
"test": "jest"
11+
},
12+
"keywords": [
13+
"aws",
14+
"credentials"
15+
],
16+
"author": "[email protected]",
17+
"license": "Apache-2.0",
18+
"dependencies": {
19+
"@aws/types": "^0.0.1"
20+
},
21+
"devDependencies": {
22+
"@types/jest": "^20.0.2",
23+
"jest": "^20.0.4",
24+
"typescript": "^2.3"
25+
}
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"target": "es5",
5+
"declaration": true,
6+
"strict": true,
7+
"sourceMap": true,
8+
"lib": [
9+
"es5",
10+
"es2015.promise"
11+
]
12+
}
13+
}

0 commit comments

Comments
 (0)