-
Notifications
You must be signed in to change notification settings - Fork 58
Updating request library to mimic fetch API #261
Changes from all commits
90a389e
f494bb0
728d09e
d183378
dbca8b4
78cfb1b
f3845a3
7827d27
b5faa1a
25bc77f
1b17667
d1c5fec
724b046
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import WeakMap from '@dojo/shim/WeakMap'; | ||
import Evented from './Evented'; | ||
import { EventObject, Handle } from './interfaces'; | ||
|
||
interface QueuingEventedData { | ||
queue: { [eventType: string]: EventObject[] }; | ||
subscribed: { [eventType: string]: boolean }; | ||
} | ||
|
||
const queingEventedSubscribers = new WeakMap<QueuingEvented, QueuingEventedData>(); | ||
|
||
/** | ||
* An implementation of the Evented class that queues up events when no listeners are | ||
* listening. When a listener is subscribed, the queue will be published to the listener. | ||
* When the queue is full, the oldest events will be discarded to make room for the newest ones. | ||
* | ||
* @property maxEvents The number of events to queue before old events are discarded. If zero (default), an unlimited number of events is queued. | ||
*/ | ||
export default class QueuingEvented extends Evented { | ||
maxEvents: number = 0; | ||
|
||
constructor() { | ||
super(); | ||
|
||
queingEventedSubscribers.set(this, { | ||
queue: {}, | ||
subscribed: {} | ||
}); | ||
} | ||
|
||
emit<T extends EventObject>(data: T): void { | ||
const queueData = queingEventedSubscribers.get(this); | ||
|
||
if (!queueData.subscribed[ data.type ]) { | ||
let eventQueue = queueData.queue[ data.type ]; | ||
|
||
if (eventQueue === undefined) { | ||
eventQueue = []; | ||
queueData.queue[ data.type ] = eventQueue; | ||
} | ||
|
||
eventQueue.push(data); | ||
|
||
if (this.maxEvents > 0 && eventQueue.length > this.maxEvents) { | ||
eventQueue.shift(); | ||
} | ||
|
||
return; | ||
} | ||
|
||
super.emit(data); | ||
} | ||
|
||
on(type: string, listener: (event: EventObject) => void): Handle { | ||
const result = super.on(type, listener); | ||
|
||
const queueData = queingEventedSubscribers.get(this); | ||
|
||
queueData.subscribed[ type ] = true; | ||
|
||
const eventQueue = queueData.queue[ type ]; | ||
|
||
if (eventQueue !== undefined) { | ||
// dispatch queued events | ||
eventQueue.forEach((eventObject) => { | ||
this.emit(eventObject); | ||
}); | ||
|
||
queueData.queue[ type ] = []; | ||
} | ||
|
||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,207 +1,49 @@ | ||
import Promise from '@dojo/shim/Promise'; | ||
import Task from './async/Task'; | ||
import { Handle } from '@dojo/interfaces/core'; | ||
import has from './has'; | ||
import MatchRegistry, { Test } from './MatchRegistry'; | ||
import load from './load'; | ||
import { ParamList } from './UrlSearchParams'; | ||
import ProviderRegistry from './request/ProviderRegistry'; | ||
import { RequestOptions, Response, Provider } from './request/interfaces'; | ||
import xhr from './request/providers/xhr'; | ||
|
||
declare var require: any; | ||
|
||
export class FilterRegistry extends MatchRegistry<RequestFilter> { | ||
register(test: string | RegExp | RequestFilterTest | null, value: RequestFilter, first?: boolean): Handle { | ||
let entryTest: Test; | ||
const inTest = test; | ||
|
||
if (typeof inTest === 'string') { | ||
entryTest = (response, url, options) => { | ||
return inTest === url; | ||
}; | ||
} | ||
else if (inTest instanceof RegExp) { | ||
entryTest = (response, url, options) => { | ||
return inTest.test(url); | ||
}; | ||
} | ||
else { | ||
entryTest = <RequestFilterTest> inTest; | ||
} | ||
export const providerRegistry = new ProviderRegistry(); | ||
|
||
return super.register(entryTest, value, first); | ||
const request: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason why we don't also support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a good reason that I know of 😄 You can still still make a custom request with whatever method you want, but adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, given we actually normalize, e.g. send There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Original reason was likely based on what was done in Dojo 1, fwiw. :) |
||
(url: string, options?: RequestOptions): Task<Response>; | ||
delete(url: string, options?: RequestOptions): Task<Response>; | ||
get(url: string, options?: RequestOptions): Task<Response>; | ||
head(url: string, options?: RequestOptions): Task<Response>; | ||
options(url: string, options?: RequestOptions): Task<Response>; | ||
post(url: string, options?: RequestOptions): Task<Response>; | ||
put(url: string, options?: RequestOptions): Task<Response>; | ||
|
||
setDefaultProvider(provider: Provider): void; | ||
} = <any> function request(url: string, options: RequestOptions = {}): Task<Response> { | ||
try { | ||
return providerRegistry.match(url, options)(url, options); | ||
} | ||
} | ||
|
||
let defaultProvider = './request/xhr'; | ||
if (has('host-node')) { | ||
defaultProvider = './request/node'; | ||
} | ||
|
||
export class ProviderRegistry extends MatchRegistry<RequestProvider> { | ||
private _providerPromise: Promise<RequestProvider>; | ||
|
||
constructor() { | ||
super(); | ||
|
||
const deferRequest = (url: string, options?: RequestOptions): ResponsePromise<any> => { | ||
let canceled = false; | ||
let actualResponse: ResponsePromise<any>; | ||
return new Task<Response<any>>((resolve, reject) => { | ||
this._providerPromise.then(function (provider) { | ||
if (canceled) { | ||
return; | ||
} | ||
if (provider) { | ||
actualResponse = provider(url, options); | ||
actualResponse.then(resolve, reject); | ||
} | ||
}); | ||
}, function () { | ||
if (!canceled) { | ||
canceled = true; | ||
} | ||
if (actualResponse) { | ||
actualResponse.cancel(); | ||
} | ||
}); | ||
}; | ||
|
||
// The first request to hit the default value will kick off the import of the default | ||
// provider. While that import is in-flight, subsequent requests will queue up while | ||
// waiting for the provider to be fulfilled. | ||
this._defaultValue = (url: string, options?: RequestOptions): ResponsePromise<any> => { | ||
this._providerPromise = load(require, defaultProvider).then(([ providerModule ]: [ { default: RequestProvider } ]): RequestProvider => { | ||
this._defaultValue = providerModule.default; | ||
return providerModule.default; | ||
}); | ||
this._defaultValue = deferRequest; | ||
return deferRequest(url, options); | ||
}; | ||
catch (error) { | ||
return Task.reject<Response>(error); | ||
} | ||
}; | ||
|
||
register(test: string | RegExp | RequestProviderTest | null, value: RequestProvider, first?: boolean): Handle { | ||
let entryTest: Test; | ||
|
||
if (typeof test === 'string') { | ||
entryTest = (url, options) => { | ||
return test === url; | ||
}; | ||
} | ||
else if (test instanceof RegExp) { | ||
entryTest = (url, options) => { | ||
return test ? (<RegExp> test).test(url) : null; | ||
}; | ||
} | ||
else { | ||
entryTest = <RequestProviderTest> test; | ||
[ 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT' ].forEach(method => { | ||
Object.defineProperty(request, method.toLowerCase(), { | ||
value(url: string, options: RequestOptions = {}): Task<Response> { | ||
options = Object.create(options); | ||
options.method = method; | ||
return request(url, options); | ||
} | ||
}); | ||
}); | ||
|
||
return super.register(entryTest, value, first); | ||
Object.defineProperty(request, 'setDefaultProvider', { | ||
value(provider: Provider) { | ||
providerRegistry.setDefaultProvider(provider); | ||
} | ||
} | ||
|
||
/** | ||
* Request filters, which filter or modify responses. The default filter simply passes a response through unchanged. | ||
*/ | ||
export const filterRegistry = new FilterRegistry(function (response: Response<any>): Response<any> { | ||
return response; | ||
}); | ||
|
||
/** | ||
* Request providers, which fulfill requests. | ||
*/ | ||
export const providerRegistry = new ProviderRegistry(); | ||
|
||
export interface RequestError<T> extends Error { | ||
response: Response<T>; | ||
} | ||
|
||
export interface RequestFilter { | ||
<T>(response: Response<T>, url: string, options?: RequestOptions): T; | ||
} | ||
|
||
export interface RequestFilterTest extends Test { | ||
<T>(response: Response<any>, url: string, options?: RequestOptions): boolean | null; | ||
} | ||
|
||
export interface RequestOptions { | ||
auth?: string; | ||
cacheBust?: any; | ||
data?: any; | ||
headers?: { [name: string]: string; }; | ||
method?: string; | ||
password?: string; | ||
query?: string | ParamList; | ||
responseType?: string; | ||
timeout?: number; | ||
user?: string; | ||
} | ||
|
||
export interface RequestProvider { | ||
<T>(url: string, options?: RequestOptions): ResponsePromise<T>; | ||
} | ||
|
||
export interface RequestProviderTest extends Test { | ||
(url: string, options?: RequestOptions): boolean | null; | ||
} | ||
|
||
export interface Response<T> { | ||
data: T | null; | ||
nativeResponse?: any; | ||
requestOptions: RequestOptions; | ||
statusCode: number | null | undefined; | ||
statusText?: string | null; | ||
url: string; | ||
|
||
getHeader(name: string): null | string; | ||
} | ||
|
||
/** | ||
* The task returned by a request, which will resolve to a Response | ||
*/ | ||
export interface ResponsePromise<T> extends Task<Response<T>> {} | ||
|
||
/** | ||
* Make a request, returning a Promise that will resolve or reject when the request completes. | ||
*/ | ||
const request: { | ||
<T>(url: string, options?: RequestOptions): ResponsePromise<T>; | ||
delete<T>(url: string, options?: RequestOptions): ResponsePromise<T>; | ||
get<T>(url: string, options?: RequestOptions): ResponsePromise<T>; | ||
post<T>(url: string, options?: RequestOptions): ResponsePromise<T>; | ||
put<T>(url: string, options?: RequestOptions): ResponsePromise<T>; | ||
} = <any> function request<T>(url: string, options: RequestOptions = {}): ResponsePromise<T> { | ||
const promise = providerRegistry.match(url, options)(url, options) | ||
.then(function (response: Response<T>) { | ||
return Task.resolve(filterRegistry.match(response, url, options)(response, url, options)) | ||
.then(function (filterResponse: any) { | ||
response.data = filterResponse.data; | ||
return response; | ||
}); | ||
}); | ||
|
||
return promise; | ||
}; | ||
|
||
[ 'DELETE', 'GET', 'POST', 'PUT' ].forEach(function (method) { | ||
(<any> request)[method.toLowerCase()] = function <T>(url: string, options: RequestOptions = {}): ResponsePromise<T> { | ||
options = Object.create(options); | ||
options.method = method; | ||
return request(url, options); | ||
}; | ||
}); | ||
providerRegistry.setDefaultProvider(xhr); | ||
|
||
export default request; | ||
|
||
/** | ||
* Add a filter that automatically parses incoming JSON responses. | ||
*/ | ||
filterRegistry.register( | ||
function (response: Response<any>, url: string, options: RequestOptions): boolean { | ||
return Boolean(typeof response.data && options && options.responseType === 'json'); | ||
}, | ||
function (response: Response<any>, url: string, options: RequestOptions): Object { | ||
return { | ||
data: JSON.parse(String(response.data)) | ||
}; | ||
} | ||
); | ||
export * from './request/interfaces'; | ||
export { default as Headers } from './request/Headers'; | ||
export { default as TimeoutError } from './request/TimeoutError'; | ||
export { default as Response, ResponseData } from './request/Response'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add some documentation to this class about what it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added!