diff --git a/samples/react/MusicStore/ReactApp/fx/domain-task.d.ts b/samples/react/MusicStore/ReactApp/fx/domain-task.d.ts deleted file mode 100644 index 902905a..0000000 --- a/samples/react/MusicStore/ReactApp/fx/domain-task.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// TODO: Move this on to definitelytyped, and take a dependency on whatwg-fetch -// so that the 'fetch' function can have the correct type args - -declare module 'domain-task' { - function addTask(task: PromiseLike): void; -} - -declare module 'domain-task/fetch' { - function fetch(url, options?): Promise; -} diff --git a/samples/react/MusicStore/tsd.json b/samples/react/MusicStore/tsd.json index f5347d6..cebcea0 100644 --- a/samples/react/MusicStore/tsd.json +++ b/samples/react/MusicStore/tsd.json @@ -37,6 +37,9 @@ }, "redux-thunk/redux-thunk.d.ts": { "commit": "e69fe60f2d6377ea4fae539493997b098f52cad1" + }, + "whatwg-fetch/whatwg-fetch.d.ts": { + "commit": "f4b1797c1201b6c575668f5d7ea12d9b1ab21846" } } } diff --git a/samples/react/MusicStore/typings/tsd.d.ts b/samples/react/MusicStore/typings/tsd.d.ts index ae00f74..eda8bf6 100644 --- a/samples/react/MusicStore/typings/tsd.d.ts +++ b/samples/react/MusicStore/typings/tsd.d.ts @@ -9,3 +9,4 @@ /// /// /// +/// diff --git a/samples/react/MusicStore/typings/whatwg-fetch/whatwg-fetch.d.ts b/samples/react/MusicStore/typings/whatwg-fetch/whatwg-fetch.d.ts new file mode 100644 index 0000000..64dd904 --- /dev/null +++ b/samples/react/MusicStore/typings/whatwg-fetch/whatwg-fetch.d.ts @@ -0,0 +1,85 @@ +// Type definitions for fetch API +// Project: https://github.com/github/fetch +// Definitions by: Ryan Graham +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +declare class Request extends Body { + constructor(input: string|Request, init?:RequestInit); + method: string; + url: string; + headers: Headers; + context: string|RequestContext; + referrer: string; + mode: string|RequestMode; + credentials: string|RequestCredentials; + cache: string|RequestCache; +} + +interface RequestInit { + method?: string; + headers?: HeaderInit|{ [index: string]: string }; + body?: BodyInit; + mode?: string|RequestMode; + credentials?: string|RequestCredentials; + cache?: string|RequestCache; +} + +declare enum RequestContext { + "audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch", + "font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import", + "internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script", + "serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker", + "xmlhttprequest", "xslt" +} +declare enum RequestMode { "same-origin", "no-cors", "cors" } +declare enum RequestCredentials { "omit", "same-origin", "include" } +declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" } + +declare class Headers { + append(name: string, value: string): void; + delete(name: string):void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; +} + +declare class Body { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + json(): Promise; + text(): Promise; +} +declare class Response extends Body { + constructor(body?: BodyInit, init?: ResponseInit); + error(): Response; + redirect(url: string, status: number): Response; + type: string|ResponseType; + url: string; + status: number; + ok: boolean; + statusText: string; + headers: Headers; + clone(): Response; +} + +declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } + +interface ResponseInit { + status: number; + statusText?: string; + headers?: HeaderInit; +} + +declare type HeaderInit = Headers|Array; +declare type BodyInit = Blob|FormData|string; +declare type RequestInfo = Request|string; + +interface Window { + fetch(url: string|Request, init?: RequestInit): Promise; +} + +declare var fetch: typeof window.fetch; diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/.gitignore b/src/Microsoft.AspNet.SpaServices/npm/domain-task/.gitignore new file mode 100644 index 0000000..a1df9a5 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/.gitignore @@ -0,0 +1,4 @@ +/typings/ +/node_modules/ +/*.js +/*.d.ts diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/.npmignore b/src/Microsoft.AspNet.SpaServices/npm/domain-task/.npmignore new file mode 100644 index 0000000..858cdc4 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/.npmignore @@ -0,0 +1,3 @@ +!/*.js +!/*.d.ts +/typings/ diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/LICENSE.txt b/src/Microsoft.AspNet.SpaServices/npm/domain-task/LICENSE.txt new file mode 100644 index 0000000..0bdc196 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/README.md b/src/Microsoft.AspNet.SpaServices/npm/domain-task/README.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/README.md @@ -0,0 +1 @@ +TODO diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/package.json b/src/Microsoft.AspNet.SpaServices/npm/domain-task/package.json new file mode 100644 index 0000000..c44f196 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/package.json @@ -0,0 +1,16 @@ +{ + "name": "domain-task", + "version": "1.0.0", + "description": "Tracks outstanding operations for a logical thread of execution", + "main": "main.js", + "scripts": { + "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"domain-task\"'", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Microsoft", + "license": "Apache-2.0", + "dependencies": { + "domain-context": "^0.5.1", + "isomorphic-fetch": "^2.2.1" + } +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/domain-context.d.ts b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/domain-context.d.ts new file mode 100644 index 0000000..1114f92 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/domain-context.d.ts @@ -0,0 +1,9 @@ +declare module 'domain' { + var active: Domain; +} + +declare module 'domain-context' { + function get(key: string): any; + function set(key: string, value: any): void; + function runInNewDomain(code: () => void): void; +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/fetch.ts b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/fetch.ts new file mode 100644 index 0000000..381c2f1 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/fetch.ts @@ -0,0 +1,62 @@ +import * as url from 'url'; +import * as domain from 'domain'; +import * as domainContext from 'domain-context'; +import { addTask } from './main'; +const isomorphicFetch = require('isomorphic-fetch'); +const isBrowser: boolean = (new Function('try { return this === window; } catch (e) { return false; }'))(); + +// Not using a symbol, because this may need to run in a version of Node.js that doesn't support them +const domainTaskStateKey = '__DOMAIN_TASK_INTERNAL_FETCH_BASEURL__DO_NOT_REFERENCE_THIS__'; +let noDomainBaseUrl: string; + +function issueRequest(baseUrl: string, req: string | Request, init?: RequestInit): Promise { + // Resolve relative URLs + if (baseUrl) { + if (req instanceof Request) { + const reqAsRequest = req as Request; + reqAsRequest.url = url.resolve(baseUrl, reqAsRequest.url); + } else { + req = url.resolve(baseUrl, req as string); + } + } else if (!isBrowser) { + // TODO: Consider only throwing if it's a relative URL, since absolute ones would work fine + throw new Error(` + When running outside the browser (e.g., in Node.js), you must specify a base URL + before invoking domain-task's 'fetch' wrapper. + Example: + import { baseUrl } from 'domain-task/fetch'; + baseUrl('http://example.com'); // Relative URLs will be resolved against this + `); + } + + // Currently, some part of ASP.NET (perhaps just Kestrel on Mac - unconfirmed) doesn't complete + // its responses if we send 'Connection: close', which is the default. So if no 'Connection' header + // has been specified explicitly, use 'Connection: keep-alive'. + init = init || {}; + init.headers = init.headers || {}; + if (!init.headers['Connection']) { + init.headers['Connection'] = 'keep-alive'; + } + + return isomorphicFetch(req, init); +} + +export function fetch(url: string | Request, init?: RequestInit): Promise { + const promise = issueRequest(baseUrl(), url, init); + addTask(promise); + return promise; +} + +export function baseUrl(url?: string): string { + if (url) { + if (domain.active) { + // There's an active domain (e.g., in Node.js), so associate the base URL with it + domainContext.set(domainTaskStateKey, url); + } else { + // There's no active domain (e.g., in browser), so there's just one shared base URL + noDomainBaseUrl = url; + } + } + + return domain.active ? domainContext.get(domainTaskStateKey) : noDomainBaseUrl; +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/isomorphic-fetch.d.ts b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/isomorphic-fetch.d.ts new file mode 100644 index 0000000..319aacb --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/isomorphic-fetch.d.ts @@ -0,0 +1,4 @@ +declare module 'isomorphic-fetch' { + var fetch: (url: string | Request, init?: RequestInit) => Promise; + export default fetch; +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/main.ts b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/main.ts new file mode 100644 index 0000000..f10f730 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/src/main.ts @@ -0,0 +1,59 @@ +import * as domain from 'domain'; +import * as domainContext from 'domain-context'; +const domainTasksStateKey = '__DOMAIN_TASKS'; + +export function addTask(task: PromiseLike) { + if (task && domain.active) { + const state = domainContext.get(domainTasksStateKey) as DomainTasksState; + if (state) { + state.numRemainingTasks++; + task.then(() => { + // The application may have other listeners chained to this promise *after* + // this listener. Since we don't want the combined task to complete until + // all the handlers for child tasks have finished, delay the following by + // one tick. + setTimeout(() => { + state.numRemainingTasks--; + if (state.numRemainingTasks === 0 && !state.hasIssuedSuccessCallback) { + state.hasIssuedSuccessCallback = true; + state.completionCallback(/* error */ null); + } + }, 0); + }, (error) => { + state.completionCallback(error); + }); + } + } +} + +export function run(codeToRun: () => T, completionCallback: (error: any) => void): T { + let synchronousResult: T; + domainContext.runInNewDomain(() => { + const state: DomainTasksState = { + numRemainingTasks: 0, + hasIssuedSuccessCallback: false, + completionCallback: domain.active.bind(completionCallback) + }; + + try { + domainContext.set(domainTasksStateKey, state); + synchronousResult = codeToRun(); + + // If no tasks were registered synchronously, then we're done already + if (state.numRemainingTasks === 0 && !state.hasIssuedSuccessCallback) { + state.hasIssuedSuccessCallback = true; + state.completionCallback(/* error */ null); + } + } catch(ex) { + state.completionCallback(ex); + } + }); + + return synchronousResult; +} + +interface DomainTasksState { + numRemainingTasks: number; + hasIssuedSuccessCallback: boolean; + completionCallback: (error: any) => void; +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/tsconfig.json b/src/Microsoft.AspNet.SpaServices/npm/domain-task/tsconfig.json new file mode 100644 index 0000000..de676e9 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "commonjs", + "target": "es5", + "declaration": true, + "outDir": "." + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/Microsoft.AspNet.SpaServices/npm/domain-task/tsd.json b/src/Microsoft.AspNet.SpaServices/npm/domain-task/tsd.json new file mode 100644 index 0000000..c9ddf99 --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/npm/domain-task/tsd.json @@ -0,0 +1,18 @@ +{ + "version": "v4", + "repo": "borisyankov/DefinitelyTyped", + "ref": "master", + "path": "typings", + "bundle": "typings/tsd.d.ts", + "installed": { + "node/node.d.ts": { + "commit": "3030a4be536b6530c06b80081f1333dc0de4d703" + }, + "es6-promise/es6-promise.d.ts": { + "commit": "3030a4be536b6530c06b80081f1333dc0de4d703" + }, + "whatwg-fetch/whatwg-fetch.d.ts": { + "commit": "3030a4be536b6530c06b80081f1333dc0de4d703" + } + } +}