init
This commit is contained in:
commit
16ba9b5400
11
.gh-sync.json
Normal file
11
.gh-sync.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"debug": false,
|
||||||
|
"matching": [
|
||||||
|
"*.json",
|
||||||
|
"*.md",
|
||||||
|
"*.yaml",
|
||||||
|
"*.csv",
|
||||||
|
"*.xls",
|
||||||
|
"*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
|
credentials.json
|
||||||
|
gcreds.json
|
||||||
|
token.json
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
jspm_packages
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
datasets
|
||||||
|
gcreds.json
|
||||||
|
xcredentials.json
|
||||||
|
tests
|
||||||
|
src
|
||||||
14
config.json
Normal file
14
config.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"products_path": "../../products/products",
|
||||||
|
"fragments_path": "../../products/bazar/fragments",
|
||||||
|
"vendor_name": "Plastic Hub",
|
||||||
|
"vendor_website": "www.precious-plastic.org",
|
||||||
|
"vendor_products_external": "https://github.com/plastic-hub/products/tree/master/products/",
|
||||||
|
"vendor_instagram": "https://www.instagram.com/plastichubcat",
|
||||||
|
"vendor_youtube": "https://www.youtube.com/channel/UCuWDxJtV2pf5BefHEy09Cew/featured?view_as=subscriber",
|
||||||
|
"vendor_github": "https://github.com/plastic-hub",
|
||||||
|
"vendor_contact_email": "mailto://cgoflyn@gmail.com",
|
||||||
|
"vendor_whatsapp": "tel://0034666894789",
|
||||||
|
"vendor_facebook" : "https://precious-plastic.org/library/machines/",
|
||||||
|
"vendor_blog" : "https://precious-plastic.org/"
|
||||||
|
}
|
||||||
112
package.json
Normal file
112
package.json
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"name": "@plastichub/cli",
|
||||||
|
"version": "1.0.10",
|
||||||
|
"description": "",
|
||||||
|
"main": "./build/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"help": "node build/main.js --help",
|
||||||
|
"build": "tsc -p .",
|
||||||
|
"dev": "tsc -w -p ."
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/plastic-hub/lang.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"bin": {
|
||||||
|
"ph-cli": "build/main.js"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/plastic-hub/lang/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/plastic-hub/lang#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@plastichub/fs": "^0.13.21",
|
||||||
|
"@plastichub/osr-sync": "0.0.22",
|
||||||
|
"@types/bluebird": "^3.5.29",
|
||||||
|
"add": "^2.0.6",
|
||||||
|
"apify": "^0.17.0",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"bluebird": "^3.7.2",
|
||||||
|
"chalk": "^2.4.1",
|
||||||
|
"cheerio": "^1.0.0-rc.3",
|
||||||
|
"chokidar": "^3.3.1",
|
||||||
|
"discourser": "^1.0.0",
|
||||||
|
"download": "^8.0.0",
|
||||||
|
"duration-timestamp": "^2.3.0",
|
||||||
|
"errlop": "^2.1.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"fast-glob": "^3.1.1",
|
||||||
|
"filenamify": "^4.1.0",
|
||||||
|
"googleapis": "^39.2.0",
|
||||||
|
"inline-css": "^2.6.2",
|
||||||
|
"isomorphic-unfetch": "^3.0.0",
|
||||||
|
"js-beautify": "^1.11.0",
|
||||||
|
"jsome": "^2.5.0",
|
||||||
|
"json-to-pretty-yaml": "^1.2.2",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
|
"markdown-table": "^2.0.0",
|
||||||
|
"moment": "^2.26.0",
|
||||||
|
"monocle-ts": "^1.2.0",
|
||||||
|
"native-promise-pool": "^3.0.0",
|
||||||
|
"ora": "^2.1.0",
|
||||||
|
"partial.lenses": "^13.13.2",
|
||||||
|
"pretty": "^2.0.0",
|
||||||
|
"puppeteer": "^5.2.1",
|
||||||
|
"ramda": "^0.25.0",
|
||||||
|
"readline": "^1.3.0",
|
||||||
|
"regexp.escape": "^1.1.0",
|
||||||
|
"sanitize-filename": "^1.6.3",
|
||||||
|
"showdown": "^1.9.1",
|
||||||
|
"simple-git": "^2.6.0",
|
||||||
|
"slash": "^3.0.0",
|
||||||
|
"slugify": "^1.4.6",
|
||||||
|
"source-map-support": "^0.5.16",
|
||||||
|
"ts-node-dev": "^1.0.0-pre.44",
|
||||||
|
"tslint": "^5.10.0",
|
||||||
|
"turndown": "^7.0.0",
|
||||||
|
"typescript": "^3.7.4",
|
||||||
|
"which": "^2.0.2",
|
||||||
|
"yargonaut": "^1.1.4",
|
||||||
|
"yargs": "^15.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@bevry/links": "^1.1.1",
|
||||||
|
"@bevry/update-contributors": "^1.0.1",
|
||||||
|
"@types/node-fetch": "^2.5.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||||
|
"@typescript-eslint/parser": "^2.30.0",
|
||||||
|
"@types/chalk": "^2.2.0",
|
||||||
|
"@types/cheerio": "^0.22.15",
|
||||||
|
"@types/chokidar": "^2.1.3",
|
||||||
|
"@types/download": "^6.2.4",
|
||||||
|
"@types/inline-css": "0.0.32",
|
||||||
|
"@types/js-beautify": "^1.8.2",
|
||||||
|
"@types/lodash": "^4.14.110",
|
||||||
|
"@types/moment": "^2.13.0",
|
||||||
|
"@types/node": "^13.1.1",
|
||||||
|
"@types/ora": "^1.3.4",
|
||||||
|
"@types/pretty": "^2.0.0",
|
||||||
|
"@types/puppeteer": "^1.5.0",
|
||||||
|
"@types/ramda": "^0.25.51",
|
||||||
|
"@types/showdown": "^1.9.3",
|
||||||
|
"@types/which": "^1.3.2",
|
||||||
|
"@types/yargs": "^13.0.4",
|
||||||
|
"@xblox/core": "^0.0.19",
|
||||||
|
"assert-helpers": "^6.1.0",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-config-bevry": "^3.3.0",
|
||||||
|
"eslint-config-prettier": "^6.11.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.3",
|
||||||
|
"jsdom": "^16.2.2",
|
||||||
|
"kava": "^4.4.0",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"projectz": "^1.19.1",
|
||||||
|
"surge": "^0.21.3",
|
||||||
|
"typedoc": "^0.17.6",
|
||||||
|
"valid-directory": "^1.6.0",
|
||||||
|
"valid-module": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/_cli.ts
Normal file
13
src/_cli.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// tweaks and handlers
|
||||||
|
export const defaults = () => {
|
||||||
|
// default command
|
||||||
|
const DefaultCommand = 'summary';
|
||||||
|
if (process.argv.length === 2) {
|
||||||
|
process.argv.push(DefaultCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently no default handler, display only :
|
||||||
|
process.on('unhandledRejection', (reason: string) => {
|
||||||
|
console.error('Unhandled rejection, reason: ', reason);
|
||||||
|
});
|
||||||
|
};
|
||||||
9346
src/api.ts
Normal file
9346
src/api.ts
Normal file
File diff suppressed because it is too large
Load Diff
55
src/argv.ts
Normal file
55
src/argv.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import * as CLI from 'yargs';
|
||||||
|
import {
|
||||||
|
warn, error,
|
||||||
|
default_path,
|
||||||
|
Options, OutputFormat, OutputTarget, inspect
|
||||||
|
} from './';
|
||||||
|
|
||||||
|
|
||||||
|
const LIGHT = 'http://google.co.uk';
|
||||||
|
const HEAVY = 'http://0.0.0.0:5555/app/xcf?debug=true&xblox=debug&xgrid=debug&davinci=debug&userDirectory=/PMaster/x4mm/user;'
|
||||||
|
|
||||||
|
// default options for all commands
|
||||||
|
export const defaultOptions = (yargs: CLI.Argv) => {
|
||||||
|
return yargs.option('url', {
|
||||||
|
default: LIGHT,
|
||||||
|
describe: 'The URL to analyze'
|
||||||
|
}).option('format', {
|
||||||
|
default: 'text',
|
||||||
|
describe: 'Normal human readable text or JSON [text|json]'
|
||||||
|
}).option('target', {
|
||||||
|
default: 'console',
|
||||||
|
describe: 'Output target [console|file]'
|
||||||
|
}).option('path', {
|
||||||
|
default: '',
|
||||||
|
describe: 'The target location on the local filesystem for --target=file'
|
||||||
|
}).option('debug', {
|
||||||
|
default: 'false',
|
||||||
|
describe: 'Enable internal debug message'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanitizes faulty user argv options for all commands.
|
||||||
|
export const sanitize = (argv: CLI.Arguments): Options => {
|
||||||
|
const args = argv as Options;
|
||||||
|
args.cwd = args.cwd || process.cwd();
|
||||||
|
if (!args.url) {
|
||||||
|
// internal user error, should never happen!
|
||||||
|
error('Invalid url, abort');
|
||||||
|
return process.exit();
|
||||||
|
}
|
||||||
|
// path given but target is not file, correct to file
|
||||||
|
if (args.path && args.target !== OutputTarget.FILE) {
|
||||||
|
args.target = OutputTarget.FILE;
|
||||||
|
}
|
||||||
|
// target is file but no path given, correct to default file
|
||||||
|
if (args.target === OutputTarget.FILE && !args.path) {
|
||||||
|
args.path = default_path(args.cwd, args.url);
|
||||||
|
}
|
||||||
|
// format string not valid
|
||||||
|
if (!(argv.format as string in OutputFormat)) {
|
||||||
|
warn(`Unknown output format ${argv.format}! Default to ${OutputFormat.text}`);
|
||||||
|
args.format = OutputFormat.text;
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
};
|
||||||
2
src/constants.ts
Normal file
2
src/constants.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const GIT_CHANGELOG_MESSAGE_PREFIX = 'ChangeLog:';
|
||||||
|
export const GIT_REPO = 'https://github.com/plastic-hub/products'
|
||||||
19
src/format.ts
Normal file
19
src/format.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Options, OutputFormat } from "./types";
|
||||||
|
import { error } from "./log";
|
||||||
|
|
||||||
|
export const render = (result: any, options: Options) => {
|
||||||
|
switch (options.format) {
|
||||||
|
case OutputFormat.text: {
|
||||||
|
//@TODO: human readable format
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
case OutputFormat.json: {
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
//private, should never happen since options had to be sanitized
|
||||||
|
error('format::render Invalid value in options.format');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/formatter.ts
Normal file
18
src/formatter.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const sizeToString = (bytes: number, si: boolean = true) => {
|
||||||
|
var units;
|
||||||
|
var u;
|
||||||
|
var b = bytes;
|
||||||
|
var thresh = si ? 1000 : 1024;
|
||||||
|
if (Math.abs(b) < thresh) {
|
||||||
|
return b + ' B';
|
||||||
|
}
|
||||||
|
units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB'];
|
||||||
|
u = -1;
|
||||||
|
do {
|
||||||
|
b /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (Math.abs(b) >= thresh && u < units.length - 1);
|
||||||
|
return b.toFixed(1) + ' ' + units[u];
|
||||||
|
};
|
||||||
52
src/git_push.sh
Normal file
52
src/git_push.sh
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||||
|
#
|
||||||
|
# Usage example: /bin/sh ./git_push.sh wing328 swagger-petstore-perl "minor update"
|
||||||
|
|
||||||
|
git_user_id=$1
|
||||||
|
git_repo_id=$2
|
||||||
|
release_note=$3
|
||||||
|
|
||||||
|
if [ "$git_user_id" = "" ]; then
|
||||||
|
git_user_id=""
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_repo_id" = "" ]; then
|
||||||
|
git_repo_id=""
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$release_note" = "" ]; then
|
||||||
|
release_note=""
|
||||||
|
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize the local directory as a Git repository
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Adds the files in the local repository and stages them for commit.
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||||
|
git commit -m "$release_note"
|
||||||
|
|
||||||
|
# Sets the new remote
|
||||||
|
git_remote=`git remote`
|
||||||
|
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||||
|
|
||||||
|
if [ "$GIT_TOKEN" = "" ]; then
|
||||||
|
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||||
|
git remote add origin https://github.com/${git_user_id}/${git_repo_id}.git
|
||||||
|
else
|
||||||
|
git remote add origin https://${git_user_id}:${GIT_TOKEN}@github.com/${git_user_id}/${git_repo_id}.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||||
|
echo "Git pushing to https://github.com/${git_user_id}/${git_repo_id}.git"
|
||||||
|
git push origin master 2>&1 | grep -v 'To https'
|
||||||
|
|
||||||
6
src/index.ts
Normal file
6
src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './paths';
|
||||||
|
export * from './format';
|
||||||
|
export * from './log';
|
||||||
|
export * from './types';
|
||||||
|
export * from './formatter';
|
||||||
|
export * from './main';
|
||||||
15
src/lib/common/array.ts
Normal file
15
src/lib/common/array.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Return last element
|
||||||
|
* @private
|
||||||
|
* @param {(Array<any>)} array
|
||||||
|
* @returns object | undefined
|
||||||
|
*/
|
||||||
|
export const lastOf = (array: Array<any>) => array[array.length - 1];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first element
|
||||||
|
* @private
|
||||||
|
* @param {(Array<any>)} array
|
||||||
|
* @returns object | undefined
|
||||||
|
*/
|
||||||
|
export const firstOf = (array: Array<any>) => array[0];
|
||||||
55
src/lib/common/enums.ts
Normal file
55
src/lib/common/enums.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
export const EVENTS = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum COMMANDS {
|
||||||
|
RUN_FILE = 'Run_File',
|
||||||
|
RUN_CLASS = 'Run_Class',
|
||||||
|
RUN_APP_SERVER_CLASS = 'Run_App_Server_Class',
|
||||||
|
RUN_APP_SERVER_CLASS_METHOD = 'Run_App_Server_Class_Method',
|
||||||
|
RUN_APP_SERVER_COMPONENT_METHOD = 'Run_App_Server_Component_Method',
|
||||||
|
CANCEL_APP_SERVER_COMPONENT_METHOD = 'Cancel_App_Server_Component_Method',
|
||||||
|
ANSWER_APP_SERVER_COMPONENT_METHOD_INTERRUPT = 'Answer_App_Server_Component_Method_Interrupt'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LOGGING_SIGNAL {
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum LOGGING_FLAGS {
|
||||||
|
/**
|
||||||
|
* No logging
|
||||||
|
* @constant
|
||||||
|
* @type int
|
||||||
|
*/
|
||||||
|
NONE = 0x00000000,
|
||||||
|
/**
|
||||||
|
* Log in the IDE's global console
|
||||||
|
* @constant
|
||||||
|
* @type int
|
||||||
|
*/
|
||||||
|
GLOBAL_CONSOLE = 0x00000001,
|
||||||
|
/**
|
||||||
|
* Log in the IDE's status bar
|
||||||
|
* @constant
|
||||||
|
* @type int
|
||||||
|
*/
|
||||||
|
STATUS_BAR = 0x00000002,
|
||||||
|
/**
|
||||||
|
* Create notification popup in the IDE
|
||||||
|
* @constant
|
||||||
|
* @type int
|
||||||
|
*/
|
||||||
|
POPUP = 0x00000004,
|
||||||
|
/**
|
||||||
|
* Log to file
|
||||||
|
* @constant
|
||||||
|
* @type int
|
||||||
|
*/
|
||||||
|
FILE = 0x00000008,
|
||||||
|
/**
|
||||||
|
* Log into the IDE's dev tool's console
|
||||||
|
* @constant
|
||||||
|
* @type int
|
||||||
|
*/
|
||||||
|
DEV_CONSOLE = 0x00000010
|
||||||
|
};
|
||||||
12
src/lib/common/platform.ts
Normal file
12
src/lib/common/platform.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { platform, arch } from 'os';
|
||||||
|
|
||||||
|
export const os = () => {
|
||||||
|
if (platform() === 'win32') {
|
||||||
|
return 'windows';
|
||||||
|
} else if (platform() === 'darwin') {
|
||||||
|
return 'osx';
|
||||||
|
} else if (arch() === 'arm') {
|
||||||
|
return 'arm';
|
||||||
|
}
|
||||||
|
return 'linux';
|
||||||
|
}
|
||||||
247
src/lib/common/primitives.ts
Normal file
247
src/lib/common/primitives.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _typeof = {
|
||||||
|
number: 'number',
|
||||||
|
string: 'string',
|
||||||
|
undefined: 'undefined',
|
||||||
|
object: 'object',
|
||||||
|
function: 'function'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is a JavaScript Array or not.
|
||||||
|
*/
|
||||||
|
export function isArray(array: any): array is any[] {
|
||||||
|
if (Array.isArray) {
|
||||||
|
return Array.isArray(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array && typeof (array.length) === _typeof.number && array.constructor === Array) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is a JavaScript String or not.
|
||||||
|
*/
|
||||||
|
export function isString(str: any): str is string {
|
||||||
|
if (typeof (str) === _typeof.string || str instanceof String) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is a JavaScript Array and each element in the array is a string.
|
||||||
|
*/
|
||||||
|
export function isStringArray(value: any): value is string[] {
|
||||||
|
return isArray(value) && (<any[]>value).every(elem => isString(elem));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns whether the provided parameter is of type `object` but **not**
|
||||||
|
* `null`, an `array`, a `regexp`, nor a `date`.
|
||||||
|
*/
|
||||||
|
export function isObject(obj: any): boolean {
|
||||||
|
// The method can't do a type cast since there are type (like strings) which
|
||||||
|
// are subclasses of any put not positvely matched by the function. Hence type
|
||||||
|
// narrowing results in wrong results.
|
||||||
|
return typeof obj === _typeof.object
|
||||||
|
&& obj !== null
|
||||||
|
&& !Array.isArray(obj)
|
||||||
|
&& !(obj instanceof RegExp)
|
||||||
|
&& !(obj instanceof Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In **contrast** to just checking `typeof` this will return `false` for `NaN`.
|
||||||
|
* @returns whether the provided parameter is a JavaScript Number or not.
|
||||||
|
*/
|
||||||
|
export function isNumber(obj: any): obj is number {
|
||||||
|
if ((typeof (obj) === _typeof.number || obj instanceof Number) && !isNaN(obj)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is a JavaScript Boolean or not.
|
||||||
|
*/
|
||||||
|
export function isBoolean(obj: any): obj is boolean {
|
||||||
|
return obj === true || obj === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is undefined.
|
||||||
|
*/
|
||||||
|
export function isUndefined(obj: any): boolean {
|
||||||
|
return typeof (obj) === _typeof.undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is undefined or null.
|
||||||
|
*/
|
||||||
|
export function isUndefinedOrNull(obj: any): boolean {
|
||||||
|
return isUndefined(obj) || obj === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is an empty JavaScript Object or not.
|
||||||
|
*/
|
||||||
|
export function isEmptyObject(obj: any): obj is any {
|
||||||
|
if (!isObject(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in obj) {
|
||||||
|
if (hasOwnProperty.call(obj, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameter is a JavaScript Function or not.
|
||||||
|
*/
|
||||||
|
export function isFunction(obj: any): obj is Function {
|
||||||
|
return typeof obj === _typeof.function;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the provided parameters is are JavaScript Function or not.
|
||||||
|
*/
|
||||||
|
export function areFunctions(...objects: any[]): boolean {
|
||||||
|
return objects && objects.length > 0 && objects.every(isFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypeConstraint = string | Function;
|
||||||
|
|
||||||
|
export function validateConstraints(args: any[], constraints: TypeConstraint[]): void {
|
||||||
|
const len = Math.min(args.length, constraints.length);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
validateConstraint(args[i], constraints[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateConstraint(arg: any, constraint: TypeConstraint): void {
|
||||||
|
|
||||||
|
if (isString(constraint)) {
|
||||||
|
if (typeof arg !== constraint) {
|
||||||
|
throw new Error(`argument does not match constraint: typeof ${constraint}`);
|
||||||
|
}
|
||||||
|
} else if (isFunction(constraint)) {
|
||||||
|
if (arg instanceof constraint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (arg && arg.constructor === constraint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (constraint.length === 1 && constraint.call(undefined, arg) === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new object of the provided class and will call the constructor with
|
||||||
|
* any additional argument supplied.
|
||||||
|
*/
|
||||||
|
export function create(ctor: Function, ...args: any[]): any {
|
||||||
|
let obj = Object.create(ctor.prototype);
|
||||||
|
ctor.apply(obj, args);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFunction0<T> {
|
||||||
|
(): T;
|
||||||
|
}
|
||||||
|
export interface IFunction1<A1, T> {
|
||||||
|
(a1: A1): T;
|
||||||
|
}
|
||||||
|
export interface IFunction2<A1, A2, T> {
|
||||||
|
(a1: A1, a2: A2): T;
|
||||||
|
}
|
||||||
|
export interface IFunction3<A1, A2, A3, T> {
|
||||||
|
(a1: A1, a2: A2, a3: A3): T;
|
||||||
|
}
|
||||||
|
export interface IFunction4<A1, A2, A3, A4, T> {
|
||||||
|
(a1: A1, a2: A2, a3: A3, a4: A4): T;
|
||||||
|
}
|
||||||
|
export interface IFunction5<A1, A2, A3, A4, A5, T> {
|
||||||
|
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5): T;
|
||||||
|
}
|
||||||
|
export interface IFunction6<A1, A2, A3, A4, A5, A6, T> {
|
||||||
|
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6): T;
|
||||||
|
}
|
||||||
|
export interface IFunction7<A1, A2, A3, A4, A5, A6, A7, T> {
|
||||||
|
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7): T;
|
||||||
|
}
|
||||||
|
export interface IFunction8<A1, A2, A3, A4, A5, A6, A7, A8, T> {
|
||||||
|
(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAction0 extends IFunction0<void> { }
|
||||||
|
export interface IAction1<A1> extends IFunction1<A1, void> { }
|
||||||
|
export interface IAction2<A1, A2> extends IFunction2<A1, A2, void> { }
|
||||||
|
export interface IAction3<A1, A2, A3> extends IFunction3<A1, A2, A3, void> { }
|
||||||
|
export interface IAction4<A1, A2, A3, A4> extends IFunction4<A1, A2, A3, A4, void> { }
|
||||||
|
export interface IAction5<A1, A2, A3, A4, A5> extends IFunction5<A1, A2, A3, A4, A5, void> { }
|
||||||
|
export interface IAction6<A1, A2, A3, A4, A5, A6> extends IFunction6<A1, A2, A3, A4, A5, A6, void> { }
|
||||||
|
export interface IAction7<A1, A2, A3, A4, A5, A6, A7> extends IFunction7<A1, A2, A3, A4, A5, A6, A7, void> { }
|
||||||
|
export interface IAction8<A1, A2, A3, A4, A5, A6, A7, A8> extends IFunction8<A1, A2, A3, A4, A5, A6, A7, A8, void> { }
|
||||||
|
|
||||||
|
export type NumberCallback = (index: number) => void;
|
||||||
|
|
||||||
|
export function count(to: number, callback: NumberCallback): void;
|
||||||
|
export function count(from: number, to: number, callback: NumberCallback): void;
|
||||||
|
export function count(fromOrTo: number, toOrCallback?: NumberCallback | number, callback?: NumberCallback): any {
|
||||||
|
var from: number, to: number;
|
||||||
|
|
||||||
|
if (isNumber(toOrCallback)) {
|
||||||
|
from = fromOrTo;
|
||||||
|
to = <number>toOrCallback;
|
||||||
|
} else {
|
||||||
|
from = 0;
|
||||||
|
to = fromOrTo;
|
||||||
|
callback = <NumberCallback>toOrCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
var op = from <= to ? (i: number) => i + 1 : (i: number) => i - 1;
|
||||||
|
var cmp = from <= to ? (a: number, b: number) => a < b : (a: number, b: number) => a > b;
|
||||||
|
|
||||||
|
for (var i = from; cmp(i, to); i = op(i)) {
|
||||||
|
callback(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countToArray(to: number): number[];
|
||||||
|
export function countToArray(from: number, to: number): number[];
|
||||||
|
export function countToArray(fromOrTo: number, to?: number): number[] {
|
||||||
|
var result: number[] = [];
|
||||||
|
var fn = (i: number) => result.push(i);
|
||||||
|
|
||||||
|
if (isUndefined(to)) {
|
||||||
|
count(fromOrTo, fn);
|
||||||
|
} else {
|
||||||
|
count(fromOrTo, to, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
487
src/lib/common/strings.ts
Normal file
487
src/lib/common/strings.ts
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
import { isArray, isObject, } from './primitives';
|
||||||
|
import { Hash } from './types';
|
||||||
|
import { isString } from 'lodash';
|
||||||
|
|
||||||
|
const escapeRegExpPattern = /[[\]{}()|\/\\^$.*+?]/g;
|
||||||
|
const escapeXmlPattern = /[&<]/g;
|
||||||
|
const escapeXmlForPattern = /[&<>'"]/g;
|
||||||
|
const escapeXmlMap: Hash<string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
'\'': '''
|
||||||
|
};
|
||||||
|
export const DefaultDelimiter = {
|
||||||
|
begin: '<%',
|
||||||
|
end: '%>'
|
||||||
|
};
|
||||||
|
export const hasFlag = (field, enumValue) => {
|
||||||
|
//noinspection JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage
|
||||||
|
// tslint:disable-next-line:no-bitwise
|
||||||
|
return ((1 << enumValue) & field) ? true : false;
|
||||||
|
};
|
||||||
|
export const hasFlagHex = (field, enumValue) => {
|
||||||
|
//noinspection JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage,JSBitwiseOperatorUsage
|
||||||
|
// tslint:disable-next-line:no-bitwise
|
||||||
|
return enumValue & field ? true : false;
|
||||||
|
};
|
||||||
|
export const disableFlag = (enumValue, field) => {
|
||||||
|
enumValue &= ~(1 << field);
|
||||||
|
return enumValue;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The minimum location of high surrogates
|
||||||
|
*/
|
||||||
|
export const HIGH_SURROGATE_MIN = 0xD800;
|
||||||
|
/**
|
||||||
|
* The maximum location of high surrogates
|
||||||
|
*/
|
||||||
|
export const HIGH_SURROGATE_MAX = 0xDBFF;
|
||||||
|
/**
|
||||||
|
* The minimum location of low surrogates
|
||||||
|
*/
|
||||||
|
export const LOW_SURROGATE_MIN = 0xDC00;
|
||||||
|
/**
|
||||||
|
* The maximum location of low surrogates
|
||||||
|
*/
|
||||||
|
export const LOW_SURROGATE_MAX = 0xDFFF;
|
||||||
|
|
||||||
|
const BASE64_KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
|
|
||||||
|
export const capitalize = (word) => {
|
||||||
|
return word.substring(0, 1).toUpperCase() + word.substring(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getJson = (inData, validOnly, ommit) => {
|
||||||
|
try {
|
||||||
|
return isString(inData) ? JSON.parse(inData) : validOnly === true ? null : inData;
|
||||||
|
} catch (e) {
|
||||||
|
ommit !== false && console.error('error parsing json data ' + inData + ' error = ' + e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes a string so that it can safely be passed to the RegExp constructor.
|
||||||
|
* @param text The string to be escaped
|
||||||
|
* @return The escaped string
|
||||||
|
*/
|
||||||
|
export function escapeRegExpEx(text: string): string {
|
||||||
|
return !text ? text : text.replace(escapeRegExpPattern, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a string to protect against tag injection.
|
||||||
|
* @param xml The string to be escaped
|
||||||
|
* @param forAttribute Whether to also escape ', ", and > in addition to < and &
|
||||||
|
* @return The escaped string
|
||||||
|
*/
|
||||||
|
export function escapeXml(xml: string, forAttribute: boolean = true): string {
|
||||||
|
if (!xml) {
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = forAttribute ? escapeXmlForPattern : escapeXmlPattern;
|
||||||
|
|
||||||
|
return xml.replace(pattern, function (character: string): string {
|
||||||
|
return escapeXmlMap[character];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUUID(): string {
|
||||||
|
const S4 = function () {
|
||||||
|
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||||
|
};
|
||||||
|
return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeRegExp(str: string): string {
|
||||||
|
const special = ['[', ']', '(', ')', '{', '}', '*', '+', '.', '|', '||'];
|
||||||
|
for (let n = 0; n < special.length; n++) {
|
||||||
|
str = str.replace(special[n], '\\' + special[n]);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function findOcurrences(expression: string, delimiters: IDelimiter): Array<string> {
|
||||||
|
// tslint:disable-next-line:no-object-literal-type-assertion
|
||||||
|
const d = {
|
||||||
|
begin: escapeRegExp(delimiters.begin),
|
||||||
|
end: escapeRegExp(delimiters.end)
|
||||||
|
} as IDelimiter;
|
||||||
|
return expression.match(new RegExp(d.begin + '([^' + d.end + ']*)' + d.end, 'g'));
|
||||||
|
};
|
||||||
|
|
||||||
|
export function multipleReplace(str: string, hash: any): string {
|
||||||
|
// to array
|
||||||
|
const a = [];
|
||||||
|
// tslint:disable-next-line:forin
|
||||||
|
for (let key in hash) {
|
||||||
|
a[a.length] = key;
|
||||||
|
}
|
||||||
|
return str.replace(new RegExp(a.join('\\b|\\b'), 'g'), function (m) {
|
||||||
|
return hash[m] || hash['\\' + m];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function replaceAll(find: string, replace: string, str: string): string {
|
||||||
|
return str ? str.split(find).join(replace) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IDelimiter {
|
||||||
|
begin: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replace(str: string, needle: any | null, what: string | any, delimiters: IDelimiter): string {
|
||||||
|
if (!str) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (what && isObject(what) || isArray(what)) {
|
||||||
|
what = what as any;
|
||||||
|
if (!delimiters) {
|
||||||
|
// fast case
|
||||||
|
return multipleReplace(str, what);
|
||||||
|
}
|
||||||
|
const occurrence = findOcurrences(str, delimiters);
|
||||||
|
if (!occurrence) {
|
||||||
|
return str;
|
||||||
|
} else {
|
||||||
|
for (let i = 0, j = occurrence.length; i < j; i++) {
|
||||||
|
const el = occurrence[i];
|
||||||
|
// strip off delimiters
|
||||||
|
let _variableName = replaceAll(delimiters.begin, '', el);
|
||||||
|
_variableName = replaceAll(delimiters.end, '', _variableName);
|
||||||
|
str = replaceAll(el, (what[_variableName]), str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
// fast case
|
||||||
|
return replaceAll(needle, what as string, str);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const substitute = (template, map ) => {
|
||||||
|
const transform = (k) => k || '';
|
||||||
|
return template.replace(/\$\{([^\s\:\}]+)(?:\:([^\s\:\}]+))?\}/g,
|
||||||
|
(match, key, format) => transform(map[key]).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
function decodeUtf8EncodedCodePoint(codePoint: number, validationRange: number[] = [0, Infinity], checkSurrogate?: boolean): string {
|
||||||
|
if (codePoint < validationRange[0] || codePoint > validationRange[1]) {
|
||||||
|
throw Error('Invalid continuation byte');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkSurrogate && codePoint >= HIGH_SURROGATE_MIN && codePoint <= LOW_SURROGATE_MAX) {
|
||||||
|
throw Error('Surrogate is not a scalar value');
|
||||||
|
}
|
||||||
|
|
||||||
|
let encoded = '';
|
||||||
|
|
||||||
|
if (codePoint > 0xFFFF) {
|
||||||
|
codePoint -= 0x010000;
|
||||||
|
encoded += String.fromCharCode(codePoint >>> 0x10 & 0x03FF | HIGH_SURROGATE_MIN);
|
||||||
|
codePoint = LOW_SURROGATE_MIN | codePoint & 0x03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded += String.fromCharCode(codePoint);
|
||||||
|
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUtf8EncodedCodePoint(codePoint: number): void {
|
||||||
|
if ((codePoint & 0xC0) !== 0x80) {
|
||||||
|
throw Error('Invalid continuation byte');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ByteBuffer = Uint16Array | Uint8Array | Buffer | number[];
|
||||||
|
|
||||||
|
export interface Codec {
|
||||||
|
encode(data: string): number[];
|
||||||
|
decode(data: ByteBuffer): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides facilities for encoding a string into an ASCII-encoded byte buffer and
|
||||||
|
* decoding an ASCII-encoded byte buffer into a string.
|
||||||
|
*/
|
||||||
|
export const ascii: Codec = {
|
||||||
|
/**
|
||||||
|
* Encodes a string into an ASCII-encoded byte buffer.
|
||||||
|
*
|
||||||
|
* @param data The text string to encode
|
||||||
|
*/
|
||||||
|
encode(data: string): number[] {
|
||||||
|
if (data == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0, length = data.length; i < length; i++) {
|
||||||
|
buffer[i] = data.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Decodes an ASCII-encoded byte buffer into a string.
|
||||||
|
*
|
||||||
|
* @param data The byte buffer to decode
|
||||||
|
*/
|
||||||
|
decode(data: ByteBuffer): string {
|
||||||
|
if (data == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = '';
|
||||||
|
|
||||||
|
for (let i = 0, length = data.length; i < length; i++) {
|
||||||
|
decoded += String.fromCharCode(data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides facilities for encoding a string into a Base64-encoded byte buffer and
|
||||||
|
* decoding a Base64-encoded byte buffer into a string.
|
||||||
|
*/
|
||||||
|
export const base64: Codec = {
|
||||||
|
/**
|
||||||
|
* Encodes a Base64-encoded string into a Base64 byte buffer.
|
||||||
|
*
|
||||||
|
* @param data The Base64-encoded string to encode
|
||||||
|
*/
|
||||||
|
encode(data: string): number[] {
|
||||||
|
if (data == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: number[] = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let length = data.length;
|
||||||
|
|
||||||
|
while (data[--length] === '=') { }
|
||||||
|
while (i < length) {
|
||||||
|
let encoded = BASE64_KEYSTR.indexOf(data[i++]) << 18;
|
||||||
|
if (i <= length) {
|
||||||
|
encoded |= BASE64_KEYSTR.indexOf(data[i++]) << 12;
|
||||||
|
}
|
||||||
|
if (i <= length) {
|
||||||
|
encoded |= BASE64_KEYSTR.indexOf(data[i++]) << 6;
|
||||||
|
}
|
||||||
|
if (i <= length) {
|
||||||
|
encoded |= BASE64_KEYSTR.indexOf(data[i++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push((encoded >>> 16) & 0xff);
|
||||||
|
buffer.push((encoded >>> 8) & 0xff);
|
||||||
|
buffer.push(encoded & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (buffer[buffer.length - 1] === 0) {
|
||||||
|
buffer.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Decodes a Base64-encoded byte buffer into a Base64-encoded string.
|
||||||
|
*
|
||||||
|
* @param data The byte buffer to decode
|
||||||
|
*/
|
||||||
|
decode(data: ByteBuffer): string {
|
||||||
|
if (data == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = '';
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (let length = data.length - (data.length % 3); i < length;) {
|
||||||
|
let encoded = data[i++] << 16 | data[i++] << 8 | data[i++];
|
||||||
|
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3F);
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3F);
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 6) & 0x3F);
|
||||||
|
decoded += BASE64_KEYSTR.charAt(encoded & 0x3F);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length % 3 === 1) {
|
||||||
|
let encoded = data[i++] << 16;
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3f);
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3f);
|
||||||
|
decoded += '==';
|
||||||
|
} else if (data.length % 3 === 2) {
|
||||||
|
let encoded = data[i++] << 16 | data[i++] << 8;
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 18) & 0x3f);
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 12) & 0x3f);
|
||||||
|
decoded += BASE64_KEYSTR.charAt((encoded >>> 6) & 0x3f);
|
||||||
|
decoded += '=';
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides facilities for encoding a string into a hex-encoded byte buffer and
|
||||||
|
* decoding a hex-encoded byte buffer into a string.
|
||||||
|
*/
|
||||||
|
export const hex: Codec = {
|
||||||
|
/**
|
||||||
|
* Encodes a string into a hex-encoded byte buffer.
|
||||||
|
*
|
||||||
|
* @param data The hex-encoded string to encode
|
||||||
|
*/
|
||||||
|
encode(data: string): number[] {
|
||||||
|
if (data == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0, length = data.length; i < length; i += 2) {
|
||||||
|
let encodedChar = parseInt(data.substr(i, 2), 16);
|
||||||
|
|
||||||
|
buffer.push(encodedChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Decodes a hex-encoded byte buffer into a hex-encoded string.
|
||||||
|
*
|
||||||
|
* @param data The byte buffer to decode
|
||||||
|
*/
|
||||||
|
decode(data: ByteBuffer): string {
|
||||||
|
if (data == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = '';
|
||||||
|
|
||||||
|
for (let i = 0, length = data.length; i < length; i++) {
|
||||||
|
decoded += data[i].toString(16).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides facilities for encoding a string into a UTF-8-encoded byte buffer and
|
||||||
|
* decoding a UTF-8-encoded byte buffer into a string.
|
||||||
|
* Inspired by the work of: https://github.com/mathiasbynens/utf8.js
|
||||||
|
*/
|
||||||
|
export const utf8: Codec = {
|
||||||
|
/**
|
||||||
|
* Encodes a string into a UTF-8-encoded byte buffer.
|
||||||
|
*
|
||||||
|
* @param data The text string to encode
|
||||||
|
*/
|
||||||
|
encode(data: string): number[] {
|
||||||
|
if (data == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0, length = data.length; i < length; i++) {
|
||||||
|
let encodedChar = data.charCodeAt(i);
|
||||||
|
/**
|
||||||
|
* Surrogates
|
||||||
|
* http://en.wikipedia.org/wiki/Universal_Character_Set_characters
|
||||||
|
*/
|
||||||
|
if (encodedChar >= HIGH_SURROGATE_MIN && encodedChar <= HIGH_SURROGATE_MAX) {
|
||||||
|
let lowSurrogate = data.charCodeAt(i + 1);
|
||||||
|
if (lowSurrogate >= LOW_SURROGATE_MIN && lowSurrogate <= LOW_SURROGATE_MAX) {
|
||||||
|
encodedChar = 0x010000 + (encodedChar - HIGH_SURROGATE_MIN) * 0x0400 + (lowSurrogate - LOW_SURROGATE_MIN);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encodedChar < 0x80) {
|
||||||
|
buffer.push(encodedChar);
|
||||||
|
} else {
|
||||||
|
if (encodedChar < 0x800) {
|
||||||
|
buffer.push(((encodedChar >> 0x06) & 0x1F) | 0xC0);
|
||||||
|
} else if (encodedChar < 0x010000) {
|
||||||
|
if (encodedChar >= HIGH_SURROGATE_MIN && encodedChar <= LOW_SURROGATE_MAX) {
|
||||||
|
throw Error('Surrogate is not a scalar value');
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(((encodedChar >> 0x0C) & 0x0F) | 0xE0);
|
||||||
|
buffer.push(((encodedChar >> 0x06) & 0x3F) | 0x80);
|
||||||
|
} else if (encodedChar < 0x200000) {
|
||||||
|
buffer.push(((encodedChar >> 0x12) & 0x07) | 0xF0);
|
||||||
|
buffer.push(((encodedChar >> 0x0C) & 0x3F) | 0x80);
|
||||||
|
buffer.push(((encodedChar >> 0x06) & 0x3F) | 0x80);
|
||||||
|
}
|
||||||
|
buffer.push((encodedChar & 0x3F) | 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Decodes a UTF-8-encoded byte buffer into a string.
|
||||||
|
*
|
||||||
|
* @param data The byte buffer to decode
|
||||||
|
*/
|
||||||
|
decode(data: ByteBuffer): string {
|
||||||
|
if (data == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = '';
|
||||||
|
|
||||||
|
for (let i = 0, length = data.length; i < length; i++) {
|
||||||
|
let byte1 = data[i] & 0xFF;
|
||||||
|
|
||||||
|
if ((byte1 & 0x80) === 0) {
|
||||||
|
decoded += decodeUtf8EncodedCodePoint(byte1);
|
||||||
|
} else if ((byte1 & 0xE0) === 0xC0) {
|
||||||
|
let byte2 = data[++i] & 0xFF;
|
||||||
|
validateUtf8EncodedCodePoint(byte2);
|
||||||
|
byte2 = byte2 & 0x3F;
|
||||||
|
let encodedByte = ((byte1 & 0x1F) << 0x06) | byte2;
|
||||||
|
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x80, Infinity]);
|
||||||
|
} else if ((byte1 & 0xF0) === 0xE0) {
|
||||||
|
let byte2 = data[++i] & 0xFF;
|
||||||
|
validateUtf8EncodedCodePoint(byte2);
|
||||||
|
byte2 = byte2 & 0x3F;
|
||||||
|
|
||||||
|
let byte3 = data[++i] & 0xFF;
|
||||||
|
validateUtf8EncodedCodePoint(byte3);
|
||||||
|
byte3 = byte3 & 0x3F;
|
||||||
|
|
||||||
|
let encodedByte = ((byte1 & 0x1F) << 0x0C) | (byte2 << 0x06) | byte3;
|
||||||
|
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x0800, Infinity], true);
|
||||||
|
} else if ((byte1 & 0xF8) === 0xF0) {
|
||||||
|
let byte2 = data[++i] & 0xFF;
|
||||||
|
validateUtf8EncodedCodePoint(byte2);
|
||||||
|
byte2 = byte2 & 0x3F;
|
||||||
|
|
||||||
|
let byte3 = data[++i] & 0xFF;
|
||||||
|
validateUtf8EncodedCodePoint(byte3);
|
||||||
|
byte3 = byte3 & 0x3F;
|
||||||
|
|
||||||
|
let byte4 = data[++i] & 0xFF;
|
||||||
|
validateUtf8EncodedCodePoint(byte4);
|
||||||
|
byte4 = byte4 & 0x3F;
|
||||||
|
|
||||||
|
let encodedByte = ((byte1 & 0x1F) << 0x0C) | (byte2 << 0x0C) | (byte3 << 0x06) | byte4;
|
||||||
|
decoded += decodeUtf8EncodedCodePoint(encodedByte, [0x010000, 0x10FFFF]);
|
||||||
|
} else {
|
||||||
|
validateUtf8EncodedCodePoint(byte1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
};
|
||||||
35
src/lib/common/types.ts
Normal file
35
src/lib/common/types.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
export * from './enums';
|
||||||
|
|
||||||
|
// tslint:disable-next-line:interface-name
|
||||||
|
export interface Hash<T> {
|
||||||
|
[id: string]: T;
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line:interface-name
|
||||||
|
export interface List<T> {
|
||||||
|
[index: number]: T;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Interface of the simple literal object with any string keys.
|
||||||
|
*/
|
||||||
|
export interface IObjectLiteral {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Represents some Type of the Object.
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:ban-types
|
||||||
|
export type ObjectType<T> = { new(): T } | (Function);
|
||||||
|
/**
|
||||||
|
* Same as Partial<T> but goes deeper and makes Partial<T> all its properties and sub-properties.
|
||||||
|
*/
|
||||||
|
export type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IDelimitter {
|
||||||
|
begin: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JSONPathExpression = string;
|
||||||
67
src/lib/content/front.ts
Normal file
67
src/lib/content/front.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { capitalize } from "../common/strings";
|
||||||
|
|
||||||
|
export const howto_header = (title, category, image, description: string = "", tagline: string = "", config: string = "") => {
|
||||||
|
return `---
|
||||||
|
image: ${image}
|
||||||
|
category: "${category}"
|
||||||
|
title: "${title}"
|
||||||
|
tagline: ${tagline || '""' }
|
||||||
|
description: ${description || `"Precious Plastic - Howto : ${category} :: ${title} "` }
|
||||||
|
${config}
|
||||||
|
---\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gallery_image = (path, title= "", alt ="") =>{
|
||||||
|
return `
|
||||||
|
- url: "${path}"
|
||||||
|
image_path: "${path}"
|
||||||
|
alt: "${alt}"
|
||||||
|
title: "${title}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const drawing_image = (path, pdf, title= "", alt ="") =>{
|
||||||
|
return `
|
||||||
|
- url: "${pdf}"
|
||||||
|
image_path: "${path}"
|
||||||
|
alt: "${alt}"
|
||||||
|
title: "${title}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const machine_header = (title, category, image, slug, rel, description: string = "", tagline: string = "", config: string = "") => {
|
||||||
|
return `---
|
||||||
|
image: ${image}
|
||||||
|
category: "${category}"
|
||||||
|
title: "${title}"
|
||||||
|
product_rel: "/${rel}"
|
||||||
|
tagline: ${tagline || '""'}
|
||||||
|
description: ${description || `"Precious Plastic - Machine : ${capitalize(category)} :: ${title}"` }
|
||||||
|
${config}
|
||||||
|
---\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const product_header = (title, category, image, slug, rel, description: string = "", tagline: string = "", config: string = "") => {
|
||||||
|
return `---
|
||||||
|
image: ${image}
|
||||||
|
category: "${category}"
|
||||||
|
title: "${title}"
|
||||||
|
product_rel: "/${rel}"
|
||||||
|
tagline: ${tagline || '""'}
|
||||||
|
description: ${description || `"Precious Plastic - Machine : ${title} by PlasticHub"` }
|
||||||
|
${config}
|
||||||
|
---\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projects_header = (title, category, image, slug, rel, description: string = "", tagline: string = "", config: string = "") => {
|
||||||
|
return `---
|
||||||
|
image: ${image}
|
||||||
|
category: "${category}"
|
||||||
|
title: "${title}"
|
||||||
|
product_rel: "/${rel}"
|
||||||
|
tagline: ${tagline || '""'}
|
||||||
|
description: ${description || `"Precious Plastic - ${capitalize(category)} :: ${title}"` }
|
||||||
|
${config}
|
||||||
|
sidebar:
|
||||||
|
nav: "projects"
|
||||||
|
---\n`;
|
||||||
|
}
|
||||||
26
src/lib/content/html.ts
Normal file
26
src/lib/content/html.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GIT_REPO } from '../../constants';
|
||||||
|
import { html_beautify } from 'js-beautify';
|
||||||
|
export const img = (file, label, id = '') => {
|
||||||
|
return `<div class="thumb">
|
||||||
|
<a href="${file}" _target="_blank" >
|
||||||
|
<img id="${id}" src="${file}" width="100%" />
|
||||||
|
</a>
|
||||||
|
<span class="thumb-label">${label}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changelog_entry = (e) => {
|
||||||
|
return `<div class="change_log_entry">
|
||||||
|
<span><pre>${e.date} </pre></span><span><a href="${GIT_REPO}/commit/${e.hash}">${e.msg}</a></span>
|
||||||
|
<ul>
|
||||||
|
${e.files.map((f) => {
|
||||||
|
return `<li><a href="${GIT_REPO}/blob/master/${f.path}">${f.path}</a></li>`
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changelog = (log: any[]) => {
|
||||||
|
return html_beautify(log.map(changelog_entry).join('<br/>'));
|
||||||
|
}
|
||||||
77
src/lib/content/md.ts
Normal file
77
src/lib/content/md.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import * as debug from '../..';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { isArray, isString } from 'util';
|
||||||
|
|
||||||
|
import { files, read, csvToMarkdown, toHTML, md2html, exists } from '../../lib/';
|
||||||
|
import { html_beautify } from 'js-beautify';
|
||||||
|
import { sync as mkdir } from '@plastichub/fs/dir';
|
||||||
|
import { substitute } from '../common/strings';
|
||||||
|
|
||||||
|
const md_tables = require('markdown-table');
|
||||||
|
|
||||||
|
export const parse_config = (config, root) => {
|
||||||
|
if (Object.keys(config)) {
|
||||||
|
for (const key in config) {
|
||||||
|
let val = config[key];
|
||||||
|
if (isArray(val)) {
|
||||||
|
if (key !== 'authors') {
|
||||||
|
config[key] = md2html(md_tables(val));
|
||||||
|
}
|
||||||
|
} else if (isString(val)) {
|
||||||
|
if (val.endsWith('.csv')) {
|
||||||
|
const parsed = path.parse(root);
|
||||||
|
let csv = path.resolve(`${parsed.dir}/${parsed.base}/${val}`) as any;
|
||||||
|
debug.info("Parsing CSV " + csv);
|
||||||
|
if (exists(csv)) {
|
||||||
|
csv = read(csv) || "";
|
||||||
|
try {
|
||||||
|
csv = md2html(csvToMarkdown(csv));
|
||||||
|
config[key] = csv;
|
||||||
|
} catch (e) {
|
||||||
|
debug.error(`Error converting csv to md ${val}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug.error(`Can't find CSV file at ${csv}`, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const md_edit_wrap = (content, f, prefix = '', context = '') => {
|
||||||
|
return html_beautify(`<div prefix="${prefix}" file="${path.parse(f).base}" context="${context}" class="fragment">${content}</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const read_fragments = (src, config, prefix = '', context = '') => {
|
||||||
|
|
||||||
|
if (!exists(src)) {
|
||||||
|
debug.warn(`Create template folder ${src}`);
|
||||||
|
mkdir(src);
|
||||||
|
}
|
||||||
|
let fragments = files(src, '*.html');
|
||||||
|
fragments.map((f) => {
|
||||||
|
config[path.parse(f).name] = md_edit_wrap(toHTML(f, true), f, prefix, context);
|
||||||
|
});
|
||||||
|
|
||||||
|
fragments = files(src, '*.md');
|
||||||
|
fragments.map((f) => {
|
||||||
|
config[path.parse(f).name] = md_edit_wrap(toHTML(f, false), f, prefix, context);
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _resolve = (config) => {
|
||||||
|
for (const key in config) {
|
||||||
|
if (config[key] && typeof config[key] == 'string') {
|
||||||
|
const resolved = substitute(config[key], config);
|
||||||
|
config[key] = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
export const resolveConfig = (config) => {
|
||||||
|
config = _resolve(config);
|
||||||
|
config = _resolve(config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
76
src/lib/content/tables.ts
Normal file
76
src/lib/content/tables.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Converts CSV to Markdown Table
|
||||||
|
*
|
||||||
|
* @param {string} csvContent - The string content of the CSV
|
||||||
|
* @param {string} delimiter - The character(s) to use as the CSV column delimiter
|
||||||
|
* @param {boolean} hasHeader - Whether to use the first row of Data as headers
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function csvToMarkdown(csvContent: string, delimiter: string = ",", hasHeader: boolean = true): string {
|
||||||
|
if (delimiter != "\t") {
|
||||||
|
csvContent = csvContent.replace(/\t/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = csvContent.split(/\r?\n/);
|
||||||
|
|
||||||
|
const tabularData: string[][] = [];
|
||||||
|
const maxRowLen: number[] = [];
|
||||||
|
|
||||||
|
columns.forEach((e, i) => {
|
||||||
|
if (typeof tabularData[i] == "undefined") {
|
||||||
|
tabularData[i] = [];
|
||||||
|
}
|
||||||
|
const regex = new RegExp(delimiter + '(?![^"]*"\\B)');
|
||||||
|
const row = e.split(regex);
|
||||||
|
row.forEach((ee, ii) => {
|
||||||
|
if (typeof maxRowLen[ii] == "undefined") {
|
||||||
|
maxRowLen[ii] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape pipes and backslashes
|
||||||
|
ee = ee.replace(/(\||\\)/g, "\\$1");
|
||||||
|
|
||||||
|
maxRowLen[ii] = Math.max(maxRowLen[ii], ee.length);
|
||||||
|
tabularData[i][ii] = ee;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let headerOutput = "";
|
||||||
|
let seperatorOutput = "";
|
||||||
|
|
||||||
|
maxRowLen.forEach((len) => {
|
||||||
|
const sizer = Array(len + 1 + 2);
|
||||||
|
|
||||||
|
seperatorOutput += "|" + sizer.join("-");
|
||||||
|
headerOutput += "|" + sizer.join(" ");
|
||||||
|
});
|
||||||
|
|
||||||
|
headerOutput += "| \n";
|
||||||
|
seperatorOutput += "| \n";
|
||||||
|
|
||||||
|
if (hasHeader) {
|
||||||
|
headerOutput = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowOutput = "";
|
||||||
|
tabularData.forEach((col, i) => {
|
||||||
|
maxRowLen.forEach((len, y) => {
|
||||||
|
const row = typeof col[y] == "undefined" ? "" : col[y];
|
||||||
|
const spacing = Array((len - row.length) + 1).join(" ");
|
||||||
|
const out = `| ${row}${spacing} `;
|
||||||
|
if (hasHeader && i === 0) {
|
||||||
|
headerOutput += out;
|
||||||
|
} else {
|
||||||
|
rowOutput += out;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasHeader && i === 0) {
|
||||||
|
headerOutput += "| \n";
|
||||||
|
} else {
|
||||||
|
rowOutput += "| \n";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return `\n\n ${headerOutput}${seperatorOutput}${rowOutput} \n\n`;
|
||||||
|
}
|
||||||
16
src/lib/core/.editorconfig
Normal file
16
src/lib/core/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[{package.json,.travis.yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
21
src/lib/core/.gitattributes
vendored
Normal file
21
src/lib/core/.gitattributes
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Set the default behavior, in case users don't have core.autocrlf set
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Files that should always be normalized and converted to native line
|
||||||
|
# endings on checkout.
|
||||||
|
*.js text
|
||||||
|
*.json text
|
||||||
|
*.ts text
|
||||||
|
*.md text
|
||||||
|
*.yml text
|
||||||
|
LICENSE text
|
||||||
|
|
||||||
|
# Files that are truly binary and should not be modified
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.jar binary
|
||||||
|
*.zip binary
|
||||||
|
*.psd binary
|
||||||
|
*.enc binary
|
||||||
7
src/lib/core/.github/CONTRIBUTING.md
vendored
Normal file
7
src/lib/core/.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Thank You
|
||||||
|
|
||||||
|
We very much welcome contributions to Dojo 2.
|
||||||
|
|
||||||
|
Because we have so many repositories that are part of Dojo 2, we have located our [Contributing Guidelines](https://github.com/dojo/meta/blob/master/CONTRIBUTING.md) in our [Dojo 2 Meta Repository](https://github.com/dojo/meta#readme).
|
||||||
|
|
||||||
|
Look forward to working with you on Dojo 2!!!
|
||||||
28
src/lib/core/.github/ISSUE_TEMPLATE.md
vendored
Normal file
28
src/lib/core/.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!--
|
||||||
|
Thank you for contributing to Dojo 2.
|
||||||
|
|
||||||
|
Our issue tracker is for bugs for Dojo 2.
|
||||||
|
|
||||||
|
Please make sure you have read our Contributing Guidelines
|
||||||
|
available at: https://github.com/dojo/meta/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
|
For general questions and discussion, join us on Gitter.im at: https://gitter.im/dojo/dojo2
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Bug / Enhancement** <!-- delete as appropriate -->
|
||||||
|
|
||||||
|
<!-- Summary of enhancement or bug-->
|
||||||
|
|
||||||
|
Package Version: <!-- package version -->
|
||||||
|
|
||||||
|
**Code**
|
||||||
|
|
||||||
|
<!-- a self contained example of code that demonstrates the issue -->
|
||||||
|
|
||||||
|
**Expected behavior:**
|
||||||
|
|
||||||
|
<!-- What did you expect to happen -->
|
||||||
|
|
||||||
|
**Actual behavior:**
|
||||||
|
|
||||||
|
<!-- What was the actual behavior? -->
|
||||||
20
src/lib/core/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
20
src/lib/core/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
**Type:** bug / feature
|
||||||
|
|
||||||
|
The following has been addressed in the PR:
|
||||||
|
|
||||||
|
* [ ] There is a related issue
|
||||||
|
* [ ] All code has been formatted with [`prettier`](https://prettier.io/) as per the [readme code style guidelines](./../#code-style)
|
||||||
|
* [ ] Unit or Functional tests are included in the PR
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Our bots should ensure:
|
||||||
|
|
||||||
|
* [ ] All contributors have signed a CLA
|
||||||
|
* [ ] The PR passes CI testing
|
||||||
|
* [ ] Code coverage is maintained
|
||||||
|
* [ ] The PR has been reviewed and approved
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Resolves #???
|
||||||
18
src/lib/core/.gitignore
vendored
Normal file
18
src/lib/core/.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.sublimets
|
||||||
|
.tscache
|
||||||
|
.tsconfig*.json
|
||||||
|
*.js
|
||||||
|
*.js.map
|
||||||
|
!/*.js
|
||||||
|
dist
|
||||||
|
deploy_key
|
||||||
|
node_modules
|
||||||
|
/typings/**
|
||||||
|
bower_components
|
||||||
|
tests/typings/dist/
|
||||||
|
.baseDir.ts
|
||||||
|
html-report
|
||||||
|
coverage-final.lcov
|
||||||
|
coverage-unmapped.json
|
||||||
|
/_build
|
||||||
|
/_apidoc
|
||||||
17
src/lib/core/.npmignore
Normal file
17
src/lib/core/.npmignore
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/_build
|
||||||
|
/docs
|
||||||
|
/html-report
|
||||||
|
/resources
|
||||||
|
/support
|
||||||
|
/tests
|
||||||
|
/typings
|
||||||
|
.editorconfig
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
.npmignore
|
||||||
|
.travis.yml
|
||||||
|
.tscache
|
||||||
|
.vscode
|
||||||
|
Gruntfile.js
|
||||||
|
tsconfig.json
|
||||||
|
tslint.json
|
||||||
32
src/lib/core/.travis.yml
Normal file
32
src/lib/core/.travis.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
sudo: false
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- '6'
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
# Please get your own free key if you want to test yourself
|
||||||
|
- SAUCE_USERNAME: dojo2-ts-ci
|
||||||
|
- SAUCE_ACCESS_KEY: e92610e3-834e-4bec-a3b5-6f7b9d874601
|
||||||
|
- BROWSERSTACK_USERNAME: dylanschiemann2
|
||||||
|
- BROWSERSTACK_ACCESS_KEY: 4Q2g8YAc9qeZzB2hECnS
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- openssl
|
||||||
|
before_install:
|
||||||
|
- if [ ${TRAVIS_BRANCH-""} == "master" ] && [ -n ${encrypted_12c8071d2874_key-""} ]; then openssl aes-256-cbc -K
|
||||||
|
$encrypted_12c8071d2874_key -iv $encrypted_12c8071d2874_iv
|
||||||
|
-in deploy_key.enc -out deploy_key -d; fi
|
||||||
|
install:
|
||||||
|
- travis_retry npm install grunt-cli
|
||||||
|
- travis_retry npm install
|
||||||
|
script:
|
||||||
|
- grunt
|
||||||
|
- grunt intern:browserstack --test-reporter
|
||||||
|
- grunt uploadCoverage
|
||||||
|
- grunt dist
|
||||||
|
- if [ "$TRAVIS_BRANCH" = "master" ]; then grunt doc; fi
|
||||||
|
notifications:
|
||||||
|
slack:
|
||||||
|
secure: ZuM/LrujGyUs8r/KGYtxaqRHNVyAMhQi+nrbfhxxqrfXqfFoikMqwGSlY4728MvOEj3ptJhcqjxHTp+rnajXTHkEXJ6y7yfIGekd5523dGVooESIu6oFjmh0iIVxJWLhxQ+DwABRYX0FsPAIMZCCVuK/0EM6Pw00uPwiPjmeRkylV+lUkEoWkuKCTxuaBDgY4ITDGprDVGmtJRQp7Weov4IOlfGLVdoupVd1AmXq5OyvpW4fVtBKR+aph0jAqSVOMFIas77njKKoSPzxGtqf1110MQ1QJc7yTY6J6XFZEPpv15cRjo/LRZFx3xf/hwAufxnZR0L1bxvuVG9mfn4q8m1Wgq6JyoWfg+DC12KSfo9Y2MJCIi/mAh5NkiP6wzsryhnQVLDKlnN5P611DuxeImsxYXMoZMqoZ/eT2djdm2PkJCwGilCRM1aTU2dfqpwbdM4aD88FzCfICigNuaIXhzinDrrBQtIn6EoRkqw0T1lplMxeG+lt2SLpcPsZmtjNVrFqb8lNQPm1RWOuo9toCPYeKnIBZt3wzTuEoj27nTVarNLQK1S74IyP8kKvnjtEeUGD0/a4ow9FnUCnARNfSx62ksB5sJV5DV4zi5tzuowTZsvyUM6tpt1U3SiKEIDIteurdS1G7vCNArucvP5cxCVqYbrgaFdRmJMEwvrAqXc=
|
||||||
|
on_success: change
|
||||||
2
src/lib/core/CONTRIBUTING.md
Normal file
2
src/lib/core/CONTRIBUTING.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
The [Contributing Guidelines](https://github.com/dojo/meta/blob/master/CONTRIBUTING.md)
|
||||||
|
for all Dojo 2 packages can be found in the [Dojo 2 Meta Repository](https://github.com/dojo/meta#readme).
|
||||||
17
src/lib/core/Gruntfile.js
Normal file
17
src/lib/core/Gruntfile.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = function (grunt) {
|
||||||
|
require('grunt-dojo2').initConfig(grunt, {
|
||||||
|
dtsGenerator: {
|
||||||
|
options: {
|
||||||
|
main: 'dojo-core/main'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typedoc: {
|
||||||
|
options: {
|
||||||
|
ignoreCompilerErrors: true, // Remove this once compile errors are resolved
|
||||||
|
}
|
||||||
|
},
|
||||||
|
intern: {
|
||||||
|
version: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
54
src/lib/core/LICENSE
Normal file
54
src/lib/core/LICENSE
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
The "New" BSD License
|
||||||
|
*********************
|
||||||
|
|
||||||
|
Copyright (c) 2015 - 2017, [JS Foundation](https://js.foundation/)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the JS Foundation nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Code for string.codePointAt, string.fromCodePoint, and string.repeat
|
||||||
|
adapted from polyfills by Mathias Bynens, under the MIT license:
|
||||||
|
|
||||||
|
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
153
src/lib/core/README.md
Normal file
153
src/lib/core/README.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
## The `@dojo/core` repository has been deprecated and merged into [`@dojo/framework`](https://github.com/dojo/framework)
|
||||||
|
|
||||||
|
You can read more about this change on our [blog](https://dojo.io/blog/). We will continue providing patches for `core` and other Dojo 2 repositories, and a [CLI migration tool](https://github.com/dojo/cli-upgrade) is available to aid in migrating projects from v2 to v3.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
# Dojo 2 core
|
||||||
|
|
||||||
|
[](https://travis-ci.org/dojo/core)
|
||||||
|
[](https://codecov.io/github/dojo/core?branch=master)
|
||||||
|
[](https://badge.fury.io/js/%40dojo%2Fcore)
|
||||||
|
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fdojo%2Fcore?ref=badge_shield)
|
||||||
|
|
||||||
|
This package provides a set of language helpers, utility functions, and classes for writing TypeScript applications. It includes APIs for feature detection, asynchronous operations, basic event handling,
|
||||||
|
and making HTTP requests.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use `@dojo/core`, install the package along with its required peer dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @dojo/core
|
||||||
|
|
||||||
|
# peer dependencies
|
||||||
|
npm install @dojo/has
|
||||||
|
npm install @dojo/shim
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [Feature Detection](#feature-detection)
|
||||||
|
- [Language Utilities](#language-utilities)
|
||||||
|
- [lang](#lang)
|
||||||
|
- [load](#load)
|
||||||
|
- [string](#string)
|
||||||
|
- [UrlSearchParams](#urlsearchparams)
|
||||||
|
- [Event Handling](#event-handling)
|
||||||
|
- [HTTP Requests](#http-requests)
|
||||||
|
- [Promises and Asynchronous Operations](#promises-and-asynchronous-operations)
|
||||||
|
- [Promise](#promise)
|
||||||
|
- [Task](#task)
|
||||||
|
|
||||||
|
### Feature Detection
|
||||||
|
|
||||||
|
Using the latest Web technologies isn't always as straightforward as developers would like due to differing support across platforms. [`@dojo/core/has`](docs/has.md) provides a simple feature detection API that makes it easy to
|
||||||
|
detect which platforms support which features.
|
||||||
|
|
||||||
|
### Language Utilities
|
||||||
|
|
||||||
|
The core package provides modules offering language utilities. Some of these are heavily based
|
||||||
|
on methods in the ES2015 proposal; others are additional APIs for commonly-performed tasks.
|
||||||
|
|
||||||
|
#### lang
|
||||||
|
|
||||||
|
The [`@dojo/core/lang` module](docs/lang.md) contains various utility functions for tasks such as copying objects
|
||||||
|
and creating late-bound or partially applied functions.
|
||||||
|
|
||||||
|
### load
|
||||||
|
The [`@dojo/core/load` module](docs/load.md) can be used to dynamically load modules or other arbitrary resources via plugins.
|
||||||
|
|
||||||
|
#### string
|
||||||
|
|
||||||
|
The [`@dojo/core/stringExtras` module](docs/stringExtras.md) contains various string functions that are not available as part of the ES2015 String APIs.
|
||||||
|
|
||||||
|
#### UrlSearchParams
|
||||||
|
|
||||||
|
The [`@dojo/core/UrlSearchParams` class](docs/UrlSearchParams.md) can be used to parse and generate URL query strings.
|
||||||
|
|
||||||
|
#### Event handling
|
||||||
|
|
||||||
|
The [`@dojo/core/on` module](docs/on.md) contains methods to handle events across types of listeners. It also includes methods to handle different event use cases including only firing
|
||||||
|
once and pauseable events.
|
||||||
|
|
||||||
|
#### HTTP requests
|
||||||
|
|
||||||
|
The [`@dojo/core/request` module](docs/request.md) contains methods to simplify making HTTP requests. It can handle
|
||||||
|
making requests in both node and the browser through the same methods.
|
||||||
|
|
||||||
|
### Promises and Asynchronous Operations
|
||||||
|
|
||||||
|
#### Promise
|
||||||
|
|
||||||
|
The `@dojo/core/Promise` class is an implementation of the ES2015 Promise API that also includes static state inspection and a `finally` method for cleanup actions.
|
||||||
|
|
||||||
|
`@dojo/core/async` contains a number of classes and utility modules to simplify working with asynchronous operations.
|
||||||
|
|
||||||
|
#### Task
|
||||||
|
|
||||||
|
The `@dojo/core/async/Task` class is an extension of `@dojo/core/Promise` that provides cancelation support.
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
This repository uses [`prettier`](https://prettier.io/) for code styling rules and formatting. A pre-commit hook is installed automatically and configured to run `prettier` against all staged files as per the configuration in the project's `package.json`.
|
||||||
|
|
||||||
|
An additional npm script to run `prettier` (with write set to `true`) against all `src` and `test` project files is available by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run prettier
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
To start working with this package, clone the repository and run `npm install`.
|
||||||
|
|
||||||
|
In order to build the project run `grunt dev` or `grunt dist`.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Test cases MUST be written using [Intern](https://theintern.github.io) using the Object test interface and Assert assertion interface.
|
||||||
|
|
||||||
|
90% branch coverage MUST be provided for all code submitted to this repository, as reported by istanbul’s combined coverage results for all supported platforms.
|
||||||
|
|
||||||
|
To test locally in node run:
|
||||||
|
|
||||||
|
`grunt test`
|
||||||
|
|
||||||
|
To test against browsers with a local selenium server run:
|
||||||
|
|
||||||
|
`grunt test:local`
|
||||||
|
|
||||||
|
To test against BrowserStack or Sauce Labs run:
|
||||||
|
|
||||||
|
`grunt test:browserstack`
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
`grunt test:saucelabs`
|
||||||
|
|
||||||
|
## Licensing information
|
||||||
|
|
||||||
|
© 2004–2018 [JS Foundation](https://js.foundation/) & contributors. [New BSD](http://opensource.org/licenses/BSD-3-Clause) license.
|
||||||
|
|
||||||
|
Some string functions (`codePointAt`, `fromCodePoint`, and `repeat`) adopted from polyfills by Mathias Bynens,
|
||||||
|
under the [MIT](http://opensource.org/licenses/MIT) license.
|
||||||
|
|
||||||
|
See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fdojo%2Fcore?ref=badge_large)
|
||||||
|
|
||||||
|
<!-- doc-viewer-config
|
||||||
|
{
|
||||||
|
"api": "docs/api.json",
|
||||||
|
"pages": [
|
||||||
|
"docs/UrlSearchParams.md",
|
||||||
|
"docs/has.md",
|
||||||
|
"docs/lang.md",
|
||||||
|
"docs/load.md",
|
||||||
|
"docs/on.md",
|
||||||
|
"docs/request.md",
|
||||||
|
"docs/stringExtras.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
-->
|
||||||
11
src/lib/core/codecov.yml
Normal file
11
src/lib/core/codecov.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
comment:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- feature/*
|
||||||
|
coverage:
|
||||||
|
notify:
|
||||||
|
slack:
|
||||||
|
default:
|
||||||
|
url: "secret:Edc+HC1cJt1JJoV0jwebOEPNfk5DXBFBMMWgjsFRUJUaymGWE8gQ/D8oCHq42U7cXaWEtKDc3fa4hBtdtVyZTsae/MXdu1x4HNkBuLx2ZI3WKOerVZRaJ6bXkA9EtvSXa6gjVouiGGIrnjkb4DxRS72m4bqnFKMHSGkkzgComjk="
|
||||||
|
threshold: 2
|
||||||
|
attachments: "sunburst, diff"
|
||||||
BIN
src/lib/core/deploy_key.enc
Normal file
BIN
src/lib/core/deploy_key.enc
Normal file
Binary file not shown.
151
src/lib/core/docs/UrlSearchParams.md
Normal file
151
src/lib/core/docs/UrlSearchParams.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# UrlSearchParams
|
||||||
|
|
||||||
|
The `UrlSearchParams` object makes working with URL query parameters a little easier.
|
||||||
|
|
||||||
|
## Creating a Url Search Params Object
|
||||||
|
|
||||||
|
### With search params string
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
```
|
||||||
|
|
||||||
|
### With object of search params
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams({
|
||||||
|
a: 'b',
|
||||||
|
b: 'c',
|
||||||
|
c: 'a'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instance methods
|
||||||
|
|
||||||
|
### `append`
|
||||||
|
|
||||||
|
Adds the value to the values that are associated with the key.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
|
||||||
|
const key = 'a';
|
||||||
|
const value = 'e';
|
||||||
|
|
||||||
|
searchParams.append(key, value);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `delete`
|
||||||
|
|
||||||
|
Removes the key from the search params.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
|
||||||
|
const key = 'a';
|
||||||
|
|
||||||
|
searchParams.delete(key);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `get`
|
||||||
|
|
||||||
|
Retrieves the first value for the key provided.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=first&a=second');
|
||||||
|
|
||||||
|
const key = 'a';
|
||||||
|
|
||||||
|
const result = searchParams.get(key);
|
||||||
|
|
||||||
|
result === 'first'; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `getAll`
|
||||||
|
|
||||||
|
Retrieves all the values for the key provided.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=first&a=second');
|
||||||
|
|
||||||
|
const key = 'a';
|
||||||
|
|
||||||
|
const result = searchParams.getAll(key);
|
||||||
|
|
||||||
|
result[0] === 'first'; // true
|
||||||
|
result[1] === 'second'; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `has`
|
||||||
|
|
||||||
|
Returns true if the key exists within the search params and false if it is not.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
|
||||||
|
const key = 'd';
|
||||||
|
|
||||||
|
const result = searchParams.has(key);
|
||||||
|
|
||||||
|
result === false; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `keys`
|
||||||
|
|
||||||
|
Returns an array of the keys of the search params.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
|
||||||
|
const result = searchParams.keys();
|
||||||
|
|
||||||
|
result instanceof Array; // true
|
||||||
|
result[0] === 'a'; // true
|
||||||
|
result[1] === 'b'; // true
|
||||||
|
result[2] === 'c'; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `set`
|
||||||
|
|
||||||
|
Sets the value of a key (clears previous values).
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
|
||||||
|
const key = 'a';
|
||||||
|
const value = 'e';
|
||||||
|
|
||||||
|
searchParams.set(key, value);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `toString`
|
||||||
|
|
||||||
|
Return a string of the search params.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { UrlSearchParams } from '@dojo/core/UrlSearchParams';
|
||||||
|
|
||||||
|
const searchParams = new UrlSearchParams('a=b&b=c&c=a');
|
||||||
|
|
||||||
|
const result = searchParams.toString();
|
||||||
|
|
||||||
|
result === 'a=b&b=c&c=a'; // true
|
||||||
|
```
|
||||||
53322
src/lib/core/docs/api.json
Normal file
53322
src/lib/core/docs/api.json
Normal file
File diff suppressed because it is too large
Load Diff
40
src/lib/core/docs/has.md
Normal file
40
src/lib/core/docs/has.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# has
|
||||||
|
|
||||||
|
## Detecting Features
|
||||||
|
|
||||||
|
The default export of `dojo-core/has` is a function which accepts a single parameter: the name of the feature to test for.
|
||||||
|
If the feature is available, a truthy value is returned, otherwise a falsy value is returned:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import has from 'dojo-core/has';
|
||||||
|
|
||||||
|
if (has('dom-addeventlistener')) {
|
||||||
|
element.addEventListener('click', function () { /* ... */ });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Feature Detections
|
||||||
|
|
||||||
|
It's important to be able to add new feature tests that aren't provided out-of-the-box by `dojo-core/has`.
|
||||||
|
This can be done easily by using the `add` function exported by the `has` module. It accepts two parameters:
|
||||||
|
the name of the feature, and either an immediate value indicating its availability or a function that resolves to a
|
||||||
|
value.
|
||||||
|
|
||||||
|
When a function is passed, the feature will be lazily evaluated - i.e. the function is not executed until the feature is
|
||||||
|
actually requested. The return value is then cached for future calls for the same feature.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { add as hasAdd } from 'dojo-core/has';
|
||||||
|
hasAdd('dom-queryselector', 'querySelector' in document && 'querySelectorAll' in document);
|
||||||
|
|
||||||
|
// Lazily executed; useful if a polyfill is loaded after page load
|
||||||
|
hasAdd('typedarray', function () {
|
||||||
|
return 'ArrayBuffer' in window;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing the Feature Cache
|
||||||
|
|
||||||
|
`dojo-core/has` maintains object hashes containing keys that correspond to all features that have been both
|
||||||
|
registered _and_ requested. The value associated with each feature name key corresponds to that feature's availability
|
||||||
|
in the current environment. The object hash containing evaluated features is accessible via the `cache` export.
|
||||||
210
src/lib/core/docs/lang.md
Normal file
210
src/lib/core/docs/lang.md
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# lang
|
||||||
|
|
||||||
|
## Module Exports
|
||||||
|
|
||||||
|
### `assign`
|
||||||
|
|
||||||
|
Copies values of own properties from the source object(s) to the target object.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { assign } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
var target = {
|
||||||
|
foo: 'bar'
|
||||||
|
};
|
||||||
|
|
||||||
|
var source = {
|
||||||
|
bar: 'foo'
|
||||||
|
};
|
||||||
|
|
||||||
|
assign(target, source);
|
||||||
|
|
||||||
|
target.foo === 'bar'; // true
|
||||||
|
target.bar === 'foo'; // true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `create`
|
||||||
|
|
||||||
|
Creates a new object from the given prototype, and copies all enumerable own properties of one or more source objects to the newly created target object.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { create } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
var oldObj = {
|
||||||
|
foo: 'bar',
|
||||||
|
obj: {
|
||||||
|
bar: 'foo'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var newObj = create(oldObj, {
|
||||||
|
bar: 'foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
newObj.bar === 'foo'; // true
|
||||||
|
newObj.foo === 'bar'; // true
|
||||||
|
newObj.obj.bar === 'foo'; // true
|
||||||
|
|
||||||
|
oldObj.foo = 'foo';
|
||||||
|
oldObj.obj.bar = 'bar';
|
||||||
|
|
||||||
|
newObj.foo === 'bar'; // true
|
||||||
|
newObj.obj.bar === 'bar'; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `deepAssign`
|
||||||
|
|
||||||
|
Copies the values of all enumerable own properties of one or more source objects to the target object, recursively copying all nested objects and arrays as well.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { deepAssign } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
var oldObj = {
|
||||||
|
foo: 'bar',
|
||||||
|
obj: {
|
||||||
|
bar: 'foo'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var newObj = deepAssign(oldObj, {
|
||||||
|
bar: 'foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
newObj.bar === 'foo'; // true
|
||||||
|
newObj.foo === 'bar'; // true
|
||||||
|
newObj.obj.bar === 'foo'; // true
|
||||||
|
|
||||||
|
oldObj.foo = 'foo';
|
||||||
|
oldObj.obj.bar = 'bar';
|
||||||
|
|
||||||
|
newObj.foo === 'bar'; // true
|
||||||
|
newObj.obj.bar === 'bar'; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mixin`
|
||||||
|
|
||||||
|
Copies values of own and inherited properties from the source object(s) to the target object.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { mixin } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
foo: 'bar',
|
||||||
|
fooObj: {
|
||||||
|
bar: 'foo'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mixin({}, obj);
|
||||||
|
|
||||||
|
result.foo === 'bar'; // true
|
||||||
|
result.fooObj.bar === 'foo'; // true
|
||||||
|
|
||||||
|
obj.fooObj.bar = 'bar';
|
||||||
|
|
||||||
|
result.fooObj.bar === 'bar'; // true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `deepMixin`
|
||||||
|
|
||||||
|
Copies the values of all enumerable (own or inherited) properties of one or more source objects to the target object, recursively copying all nested objects and arrays as well.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { deepMixin } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
foo: 'bar',
|
||||||
|
fooObj: {
|
||||||
|
bar: 'foo'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = deepMixin({}, obj);
|
||||||
|
|
||||||
|
result.foo === 'bar'; // true
|
||||||
|
result.fooObj.bar === 'foo'; // true
|
||||||
|
|
||||||
|
obj.fooObj.bar = 'bar';
|
||||||
|
|
||||||
|
result.fooObj.bar === 'bar'; // false
|
||||||
|
result.fooObj.bar === 'foo'; // true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `duplicate`
|
||||||
|
|
||||||
|
Creates a new object using the provided source's prototype as the prototype for the new object, and then deep copies the provided source's values into the new target.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { duplicate } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
var oldObj = {
|
||||||
|
foo: 'bar'
|
||||||
|
};
|
||||||
|
|
||||||
|
var newObj = duplicate(oldObj);
|
||||||
|
|
||||||
|
oldObj.foo = 'foo';
|
||||||
|
|
||||||
|
oldObj.foo === 'foo';
|
||||||
|
newObj.foo === 'bar';
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `partial`
|
||||||
|
|
||||||
|
Returns a function which invokes the given function with the given arguments prepended to its argument list. Like `Function.prototype.bind`, but does not alter execution context.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { partial } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
var add = function (a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addFive = partial(add, 5);
|
||||||
|
|
||||||
|
var result = addFive(4);
|
||||||
|
|
||||||
|
result === 9;
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `isIdentical`
|
||||||
|
|
||||||
|
Determines whether two values are the same (including NaN).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { isIdentical } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
isIdentical(1, 1); // true
|
||||||
|
isIdentical(NaN, NaN); // true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `lateBind`
|
||||||
|
|
||||||
|
Creates a function that calls the current method on an object with given arguments.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { lateBind } from '@dojo/core/lang';
|
||||||
|
|
||||||
|
var person = {
|
||||||
|
speak: function (name) {
|
||||||
|
return 'hi, ' + name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var personSpeak = lateBind(person, 'speak', 'name');
|
||||||
|
|
||||||
|
personSpeak() === 'hi, name'; // true
|
||||||
|
|
||||||
|
person.speak = function (name) {
|
||||||
|
return 'bye, ' + name;
|
||||||
|
};
|
||||||
|
|
||||||
|
personSpeak() === 'bye, name';
|
||||||
|
|
||||||
|
```
|
||||||
117
src/lib/core/docs/load.md
Normal file
117
src/lib/core/docs/load.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# load
|
||||||
|
|
||||||
|
## Module Exports
|
||||||
|
|
||||||
|
### 'isPlugin'
|
||||||
|
|
||||||
|
Tests a value to determine whether is a plugin (an object with a `load` method).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { isPlugin } from '@dojo/core/load';
|
||||||
|
|
||||||
|
// true
|
||||||
|
isPlugin({
|
||||||
|
load() {}
|
||||||
|
normalize() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
isPlugin(1); // false
|
||||||
|
isPlugin([]); // false
|
||||||
|
isPlugin([]); // false
|
||||||
|
// false
|
||||||
|
isPlugin({
|
||||||
|
observer() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 'load'
|
||||||
|
|
||||||
|
Dynamically loads a module or other resource.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import load, { useDefault } from '@dojo/core/load';
|
||||||
|
|
||||||
|
// Load a single module
|
||||||
|
load('mymodule').then(([ myModule ]: [ any ]) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load multiple modules
|
||||||
|
load('namespace/first', 'namespace/second').then(([ first, second ]: [ any, any ]) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load modules with relative ids as relative to the current module
|
||||||
|
load(require, './first', './second').then(([ first, second ]: [ any, any ]) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Automatically map modules to their default export
|
||||||
|
load('namespace/first').then(useDefault).then(([ first ]: [ any ]) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load a custom resource via a plugin
|
||||||
|
load(require, 'plugin!./template.html').then(([ html ]: [ string ]) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Plugins
|
||||||
|
|
||||||
|
AMD-style plugins can be used with `load` by passing in module ids with the format `{pluginId}!{resourceId}`. First, the plugin will be loaded, and then the resource id (the string that follows the `!` in the mid passed to `core/load`) will be normalized and passed to the plugin's `load` method. If the plugin module does not actually have a `load` method, then the resource id is ignored, and the module is returned as-is. Plugins can also expose an optional `normalize` method that is passed the resource id and a resolver method (which will be either `require.toUrl`, `require.resolve`, or an identity function, depending on the environment). Note that if no `normalize` method is provided, then the provided resource id will be resolved using `require.toUrl` or `require.resolve`, depending on the environment. Again, if the plugin module has a default export, the `normalize` method MUST exist on that object.
|
||||||
|
|
||||||
|
Note: the plugins that can be used with `load` loosely follow the [amdjs plugin API](https://github.com/amdjs/amdjs-api/blob/master/LoaderPlugins.md), with the following exceptions:
|
||||||
|
|
||||||
|
1. The plugin's `load` method does not receive a contextual `require` or a configuration object.
|
||||||
|
2. Rather than execute a callback when the resource has loaded, the plugin's `load` method instead must return a promise that resolves to that resource.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Plugin that does not use the default export.
|
||||||
|
import { Load } from '@dojo/core/load';
|
||||||
|
|
||||||
|
export function normalize(resourceId: string, resolver: (id: string) => string): string {
|
||||||
|
return resolver(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load(resourceId: string, load: Load) {
|
||||||
|
// This plugin does nothing more than load the resource id with `core/load`.
|
||||||
|
return load(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// The same plugin, but using the default export
|
||||||
|
import { Load } from '@dojo/core/load';
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
normalize(resourceId: string, resolver: (id: string) => string): string {
|
||||||
|
return resolver(resourceId);
|
||||||
|
},
|
||||||
|
|
||||||
|
load(resourceId: string, load: Load) {
|
||||||
|
// This plugin does nothing more than load the resource id with `core/load`.
|
||||||
|
return load(resourceId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default plugin;
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import load from '@dojo/core/load';
|
||||||
|
|
||||||
|
// 1. The module with the id `plugin` is loaded.
|
||||||
|
// 2. If `plugin` is not actually a plugin, then `plugin` itself is returned.
|
||||||
|
// 3. If the plugin has a normalize method, then "some/resource/id" is passed to it,
|
||||||
|
// and the return value is used as the resource id.
|
||||||
|
// 4. The resource id is passed to the plugin's `load` method.
|
||||||
|
// 5. The loaded resource is used to resolve the `load` promise.
|
||||||
|
load('plugin!some/resource/id').then(([ resource ]: [ any ]) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
65
src/lib/core/docs/on.md
Normal file
65
src/lib/core/docs/on.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# On
|
||||||
|
|
||||||
|
`dojo-core/on` provides event handling support with methods to attach and emit events.
|
||||||
|
|
||||||
|
## `emit`
|
||||||
|
|
||||||
|
Dispatch event to target.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { emit } from '@dojo/core/on';
|
||||||
|
|
||||||
|
var button = document.getElementById('button');
|
||||||
|
var DOMEventObject = {
|
||||||
|
type: 'click',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
emit(button, DOMEventObject);
|
||||||
|
|
||||||
|
```
|
||||||
|
## `on`
|
||||||
|
|
||||||
|
Adds event listener to target.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { on } from '@dojo/core/on';
|
||||||
|
|
||||||
|
var button = document.getElementById('button');
|
||||||
|
|
||||||
|
on(button, 'click', function (event) {
|
||||||
|
console.log(event.target.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
## `once`
|
||||||
|
|
||||||
|
Attach an event that can only be called once to a target.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { once } from '@dojo/core/on';
|
||||||
|
|
||||||
|
var button = document.getElementById('button');
|
||||||
|
once(button, 'click', function (event) {
|
||||||
|
console.log(event.target.id);
|
||||||
|
console.log('this event has been removed')
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
## `pausable`
|
||||||
|
|
||||||
|
Attach an event that can be paused to a target.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { pausable } from '@dojo/core/on';
|
||||||
|
|
||||||
|
var button = document.getElementById('button');
|
||||||
|
var buttonClickHandle = pausable(button, 'click', function (event) {
|
||||||
|
console.log(event.target.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonClickHandle.pause(); // when paused the event will not fire
|
||||||
|
buttonClickHandle.resume(); // after resuming the event will begin to fire again if triggered
|
||||||
|
|
||||||
|
```
|
||||||
75
src/lib/core/docs/request.md
Normal file
75
src/lib/core/docs/request.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# request
|
||||||
|
|
||||||
|
This modules provides 4 methods (get, post, delete, and put) to simplify sending http requests. Each of these methods returns a promise that resolves with the response.
|
||||||
|
|
||||||
|
* request
|
||||||
|
* get
|
||||||
|
* post
|
||||||
|
* delete
|
||||||
|
* put
|
||||||
|
|
||||||
|
## Making Requests
|
||||||
|
|
||||||
|
Making requests is similar to using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
||||||
|
|
||||||
|
A GET request,
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const json = await request('http://www.example.com').then(response => response.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
A POST request,
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await request.post('http://www.example.com', { body: JSON.stringify(myValues)}).then(response => response.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observables
|
||||||
|
|
||||||
|
Several observables are available to provide deeper insight into the state of a request.
|
||||||
|
|
||||||
|
### Monitoring Upload Progress
|
||||||
|
|
||||||
|
Upload progress can be monitored with the `upload` observable on the `Request` object. Since upload events automatically cause a preflight request, they can be disabled by setting `includeUploadProgress: false`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const req = request.post('http://www.example.com/', {
|
||||||
|
body: someLargeString
|
||||||
|
});
|
||||||
|
|
||||||
|
req.upload.subscribe(totalUploadedBytes => {
|
||||||
|
// do something with uploaded bytes
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that while the Node.js provider will emit a single upload event when it is done uploading, it cannot emit more granular upload events with `string` or `Buffer` body types. To receive more frequent upload events, you can use the `bodyStream` option to provide a `Readable` with the body content. Upload events will be emitted as the data is read from the stream.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
request.post('http://www.example.com/', {
|
||||||
|
bodyStream: fs.createReadStream('some-large-file')
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring Download Progress
|
||||||
|
|
||||||
|
You can monitor download progress by subscribing to the `download` observable on the `Response` object.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
request("http://www.example/some-large-file").then(response => {
|
||||||
|
response.download.subscribe(totalBytesDownloaded => {
|
||||||
|
// do something with totalBytesDownloaded
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Receiving Raw Data
|
||||||
|
|
||||||
|
You can receive the raw data from a response with the `data` observable. Depending on the provider, the value might be a `string`, or a `Buffer`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
request("http://www.example/some-large-file").then(response => {
|
||||||
|
response.data.subscribe(chunk => {
|
||||||
|
// do something with chunk
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
30
src/lib/core/docs/stringExtras.md
Normal file
30
src/lib/core/docs/stringExtras.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# stringExtras
|
||||||
|
|
||||||
|
## `escapeRegExp`
|
||||||
|
|
||||||
|
Escapes a string to safely be included in regular expressions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { escapeRegExp } from '@dojo/core/string';
|
||||||
|
|
||||||
|
const str = 'cat file.js | grep -c';
|
||||||
|
|
||||||
|
const result = escapeRegExp(str);
|
||||||
|
|
||||||
|
result === 'cat file\\.js \\| grep -c'; // true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## `escapeXml`
|
||||||
|
|
||||||
|
Escapes XML (or HTML) content in a string.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { escapeXml } from '@dojo/core/string';
|
||||||
|
|
||||||
|
const badCode = "<script>alert('hi')</script>";
|
||||||
|
|
||||||
|
const sanitized = escapeXml(badCode);
|
||||||
|
|
||||||
|
sanitized === '<script>alert('hi')</script>'; // true
|
||||||
|
```
|
||||||
91
src/lib/core/intern.json
Normal file
91
src/lib/core/intern.json
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"capabilities+": {
|
||||||
|
"project": "Dojo 2",
|
||||||
|
"name": "@dojo/core",
|
||||||
|
"fixSessionCapabilities": false,
|
||||||
|
"browserstack.debug": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"environments": [
|
||||||
|
{ "browserName": "node" }
|
||||||
|
],
|
||||||
|
|
||||||
|
"suites": [
|
||||||
|
"./_build/tests/unit/all.js"
|
||||||
|
],
|
||||||
|
|
||||||
|
"functionalSuites": [
|
||||||
|
"./_build/tests/functional/all.js"
|
||||||
|
],
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
"./_build/tests/plugins/echo-service.js"
|
||||||
|
],
|
||||||
|
|
||||||
|
"browser": {
|
||||||
|
"loader": "./node_modules/grunt-dojo2/lib/intern/internLoader.js",
|
||||||
|
"suites+": [
|
||||||
|
"./_build/tests/unit/all-browser.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"node": {
|
||||||
|
"suites+": [
|
||||||
|
"./_build/tests/unit/all-node.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"coverage": [
|
||||||
|
"./_build/src/**/*.js"
|
||||||
|
],
|
||||||
|
|
||||||
|
"configs": {
|
||||||
|
"browserstack": {
|
||||||
|
"tunnel": "browserstack",
|
||||||
|
|
||||||
|
"environments+": [
|
||||||
|
{ "browserName": "internet explorer", "version": "11", "os": "WINDOWS", "os_version": [ "8.1", "10" ] },
|
||||||
|
{ "browserName": "edge" },
|
||||||
|
{ "browserName": "firefox", "platform": "WINDOWS" },
|
||||||
|
{ "browserName": "chrome", "platform": "WINDOWS" },
|
||||||
|
{ "browserName": "safari", "version": "10.1", "platform": "MAC" }
|
||||||
|
],
|
||||||
|
|
||||||
|
"maxConcurrency": 5
|
||||||
|
},
|
||||||
|
|
||||||
|
"node-loader": {
|
||||||
|
"node": {
|
||||||
|
"loader": "./node_modules/grunt-dojo2/lib/intern/internLoader.js",
|
||||||
|
"suites+": [
|
||||||
|
"./_build/tests/unit/all-node-loader.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"local": {
|
||||||
|
"tunnel": "selenium",
|
||||||
|
"tunnelOptions": {
|
||||||
|
"hostname": "localhost",
|
||||||
|
"port": 4444
|
||||||
|
},
|
||||||
|
|
||||||
|
"environments+": [
|
||||||
|
{ "browserName": "chrome" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"saucelabs": {
|
||||||
|
"tunnel": "saucelabs",
|
||||||
|
"tunnelOptions": {},
|
||||||
|
|
||||||
|
"defaultTimeout": 10000,
|
||||||
|
"environments+": [
|
||||||
|
{ "browserName": "internet explorer", "version": [ "11.0" ], "platform": "Windows 7" },
|
||||||
|
{ "browserName": "firefox", "version": "43", "platform": "Windows 10" },
|
||||||
|
{ "browserName": "chrome", "platform": "Windows 10" }
|
||||||
|
],
|
||||||
|
"maxConcurrency": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/lib/core/package.json
Normal file
78
src/lib/core/package.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "@dojo/core",
|
||||||
|
"version": "2.0.1-pre",
|
||||||
|
"description": "Basic utilites for common TypeScript development",
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=3.0.0"
|
||||||
|
},
|
||||||
|
"homepage": "https://dojo.io",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/dojo/core/issues"
|
||||||
|
},
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"private": true,
|
||||||
|
"main": "main.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dojo/core.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublish": "grunt peerDepInstall",
|
||||||
|
"precommit": "lint-staged",
|
||||||
|
"prettier": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
||||||
|
"test": "grunt test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "~1.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dojo/has": "^2.0.0",
|
||||||
|
"@dojo/shim": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dojo/loader": "^2.0.0",
|
||||||
|
"@theintern/leadfoot": "~2.0.2",
|
||||||
|
"@types/benchmark": "~1.0.0",
|
||||||
|
"@types/chai": "~4.0.0",
|
||||||
|
"@types/express": "~4.0.39",
|
||||||
|
"@types/glob": "~5.0.0",
|
||||||
|
"@types/grunt": "~0.4.0",
|
||||||
|
"@types/multer": "~1.3.3",
|
||||||
|
"@types/node": "~9.6.5",
|
||||||
|
"@types/sinon": "~1.16.0",
|
||||||
|
"benchmark": "^1.0.0",
|
||||||
|
"express": "~4.15.3",
|
||||||
|
"grunt": "^1.0.1",
|
||||||
|
"grunt-contrib-clean": ">=1.0.0",
|
||||||
|
"grunt-contrib-copy": ">=1.0.0",
|
||||||
|
"grunt-dojo2": "latest",
|
||||||
|
"grunt-postcss": "^0.8.0",
|
||||||
|
"grunt-text-replace": ">=0.4.0",
|
||||||
|
"grunt-ts": ">=5.0.0",
|
||||||
|
"grunt-tslint": "5.0.1",
|
||||||
|
"grunt-typings": "^0.1.5",
|
||||||
|
"husky": "0.14.3",
|
||||||
|
"intern": "~4.1.0",
|
||||||
|
"lint-staged": "6.0.0",
|
||||||
|
"multer": "~1.3.0",
|
||||||
|
"prettier": "1.9.2",
|
||||||
|
"remap-istanbul": ">=0.6.3",
|
||||||
|
"sinon": "~1.17.6",
|
||||||
|
"tslint": "5.8.0",
|
||||||
|
"typescript": "~2.6.1"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"parser": "typescript",
|
||||||
|
"printWidth": 120,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/lib/core/resources/BrowserStackLogo.png
Normal file
BIN
src/lib/core/resources/BrowserStackLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
350
src/lib/core/src/DateObject.ts
Normal file
350
src/lib/core/src/DateObject.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import { Hash } from './interfaces';
|
||||||
|
|
||||||
|
export interface KwArgs {
|
||||||
|
dayOfMonth?: number;
|
||||||
|
hours?: number;
|
||||||
|
milliseconds?: number;
|
||||||
|
minutes?: number;
|
||||||
|
month: number;
|
||||||
|
seconds?: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationKwArgs {
|
||||||
|
days?: number;
|
||||||
|
hours?: number;
|
||||||
|
milliseconds?: number;
|
||||||
|
minutes?: number;
|
||||||
|
months?: number;
|
||||||
|
seconds?: number;
|
||||||
|
years?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties of a complete date
|
||||||
|
*/
|
||||||
|
export interface DateProperties {
|
||||||
|
dayOfMonth: number;
|
||||||
|
readonly dayOfWeek: number;
|
||||||
|
readonly daysInMonth: number;
|
||||||
|
hours: number;
|
||||||
|
readonly isLeapYear: boolean;
|
||||||
|
milliseconds: number;
|
||||||
|
minutes: number;
|
||||||
|
month: number;
|
||||||
|
seconds: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = [NaN, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
|
|
||||||
|
const isLeapYear = (function() {
|
||||||
|
const date = new Date();
|
||||||
|
function isLeapYear(year: number): boolean {
|
||||||
|
date.setFullYear(year, 1, 29);
|
||||||
|
return date.getDate() === 29;
|
||||||
|
}
|
||||||
|
return isLeapYear;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const operationOrder = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'];
|
||||||
|
const operationHash: Hash<string> = Object.create(null, {
|
||||||
|
days: { value: 'Date' },
|
||||||
|
hours: { value: 'UTCHours' },
|
||||||
|
milliseconds: { value: 'UTCMilliseconds' },
|
||||||
|
minutes: { value: 'UTCMinutes' },
|
||||||
|
months: { value: 'Month' },
|
||||||
|
seconds: { value: 'UTCSeconds' },
|
||||||
|
years: { value: 'FullYear' }
|
||||||
|
});
|
||||||
|
|
||||||
|
export class DateObject implements DateProperties {
|
||||||
|
static parse(str: string): DateObject {
|
||||||
|
return new DateObject(Date.parse(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
static now(): DateObject {
|
||||||
|
return new DateObject(Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _date: Date;
|
||||||
|
readonly utc: DateProperties;
|
||||||
|
|
||||||
|
constructor(value: number);
|
||||||
|
constructor(value: string);
|
||||||
|
constructor(value: Date);
|
||||||
|
constructor(value: KwArgs);
|
||||||
|
constructor();
|
||||||
|
constructor(value?: any) {
|
||||||
|
let _date: Date;
|
||||||
|
if (!arguments.length) {
|
||||||
|
_date = new Date();
|
||||||
|
} else if (value instanceof Date) {
|
||||||
|
_date = new Date(+value);
|
||||||
|
} else if (typeof value === 'number' || typeof value === 'string') {
|
||||||
|
_date = new Date(<any>value);
|
||||||
|
} else {
|
||||||
|
_date = new Date(
|
||||||
|
value.year,
|
||||||
|
value.month - 1,
|
||||||
|
value.dayOfMonth || 1,
|
||||||
|
value.hours || 0,
|
||||||
|
value.minutes || 0,
|
||||||
|
value.seconds || 0,
|
||||||
|
value.milliseconds || 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._date = _date;
|
||||||
|
const self = this;
|
||||||
|
this.utc = {
|
||||||
|
get isLeapYear(this: DateObject): boolean {
|
||||||
|
return isLeapYear(this.year);
|
||||||
|
},
|
||||||
|
get daysInMonth(this: DateObject): number {
|
||||||
|
const month = this.month;
|
||||||
|
|
||||||
|
if (month === 2 && this.isLeapYear) {
|
||||||
|
return 29;
|
||||||
|
}
|
||||||
|
return days[month];
|
||||||
|
},
|
||||||
|
|
||||||
|
get year(): number {
|
||||||
|
return self._date.getUTCFullYear();
|
||||||
|
},
|
||||||
|
set year(year: number) {
|
||||||
|
self._date.setUTCFullYear(year);
|
||||||
|
},
|
||||||
|
|
||||||
|
get month(): number {
|
||||||
|
return self._date.getUTCMonth() + 1;
|
||||||
|
},
|
||||||
|
set month(month: number) {
|
||||||
|
self._date.setUTCMonth(month - 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
get dayOfMonth(): number {
|
||||||
|
return self._date.getUTCDate();
|
||||||
|
},
|
||||||
|
set dayOfMonth(day: number) {
|
||||||
|
self._date.setUTCDate(day);
|
||||||
|
},
|
||||||
|
|
||||||
|
get hours(): number {
|
||||||
|
return self._date.getUTCHours();
|
||||||
|
},
|
||||||
|
set hours(hours: number) {
|
||||||
|
self._date.setUTCHours(hours);
|
||||||
|
},
|
||||||
|
|
||||||
|
get minutes(): number {
|
||||||
|
return self._date.getUTCMinutes();
|
||||||
|
},
|
||||||
|
set minutes(minutes: number) {
|
||||||
|
self._date.setUTCMinutes(minutes);
|
||||||
|
},
|
||||||
|
|
||||||
|
get seconds(): number {
|
||||||
|
return self._date.getUTCSeconds();
|
||||||
|
},
|
||||||
|
set seconds(seconds: number) {
|
||||||
|
self._date.setUTCSeconds(seconds);
|
||||||
|
},
|
||||||
|
|
||||||
|
get milliseconds(): number {
|
||||||
|
return self._date.getUTCMilliseconds();
|
||||||
|
},
|
||||||
|
set milliseconds(milliseconds: number) {
|
||||||
|
self._date.setUTCMilliseconds(milliseconds);
|
||||||
|
},
|
||||||
|
|
||||||
|
get dayOfWeek(): number {
|
||||||
|
return self._date.getUTCDay();
|
||||||
|
},
|
||||||
|
|
||||||
|
toString: function(): string {
|
||||||
|
return self._date.toUTCString();
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLeapYear(): boolean {
|
||||||
|
return isLeapYear(this.year);
|
||||||
|
}
|
||||||
|
|
||||||
|
get daysInMonth(): number {
|
||||||
|
const month = this.month;
|
||||||
|
|
||||||
|
if (month === 2 && this.isLeapYear) {
|
||||||
|
return 29;
|
||||||
|
}
|
||||||
|
return days[month];
|
||||||
|
}
|
||||||
|
|
||||||
|
get year(): number {
|
||||||
|
return this._date.getFullYear();
|
||||||
|
}
|
||||||
|
set year(year: number) {
|
||||||
|
const dayOfMonth = this.dayOfMonth;
|
||||||
|
|
||||||
|
this._date.setFullYear(year);
|
||||||
|
|
||||||
|
if (this.dayOfMonth < dayOfMonth) {
|
||||||
|
this.dayOfMonth = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get month(): number {
|
||||||
|
return this._date.getMonth() + 1;
|
||||||
|
}
|
||||||
|
set month(month: number) {
|
||||||
|
const dayOfMonth = this.dayOfMonth;
|
||||||
|
|
||||||
|
this._date.setMonth(month - 1);
|
||||||
|
|
||||||
|
if (this.dayOfMonth < dayOfMonth) {
|
||||||
|
this.dayOfMonth = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get dayOfMonth(): number {
|
||||||
|
return this._date.getDate();
|
||||||
|
}
|
||||||
|
set dayOfMonth(day: number) {
|
||||||
|
this._date.setDate(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
get hours(): number {
|
||||||
|
return this._date.getHours();
|
||||||
|
}
|
||||||
|
set hours(hours: number) {
|
||||||
|
this._date.setHours(hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
get minutes(): number {
|
||||||
|
return this._date.getMinutes();
|
||||||
|
}
|
||||||
|
set minutes(minutes: number) {
|
||||||
|
this._date.setMinutes(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
get seconds(): number {
|
||||||
|
return this._date.getSeconds();
|
||||||
|
}
|
||||||
|
set seconds(seconds: number) {
|
||||||
|
this._date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
get milliseconds(): number {
|
||||||
|
return this._date.getMilliseconds();
|
||||||
|
}
|
||||||
|
set milliseconds(milliseconds: number) {
|
||||||
|
this._date.setMilliseconds(milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
get time(): number {
|
||||||
|
return this._date.getTime();
|
||||||
|
}
|
||||||
|
set time(time: number) {
|
||||||
|
this._date.setTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dayOfWeek(): number {
|
||||||
|
return this._date.getDay();
|
||||||
|
}
|
||||||
|
get timezoneOffset(): number {
|
||||||
|
return this._date.getTimezoneOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: number): DateObject;
|
||||||
|
add(value: OperationKwArgs): DateObject;
|
||||||
|
add(value: any): DateObject {
|
||||||
|
const result = new DateObject(this.time);
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
result.time += value;
|
||||||
|
} else {
|
||||||
|
// Properties have to be added in a particular order to properly handle
|
||||||
|
// date overshoots in month and year calculations
|
||||||
|
operationOrder.forEach((property: string): void => {
|
||||||
|
if (!(property in value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateMethod = operationHash[property];
|
||||||
|
(<any>result._date)[`set${dateMethod}`]((<any>this._date)[`get${dateMethod}`]() + value[property]);
|
||||||
|
|
||||||
|
if ((property === 'years' || property === 'months') && result.dayOfMonth < this.dayOfMonth) {
|
||||||
|
// Set the day of the month to 0 to move the date to the first day of the previous
|
||||||
|
// month to fix overshoots when adding a month and the date is the 31st or adding
|
||||||
|
// a year and the date is the 29th
|
||||||
|
result.dayOfMonth = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
compare(value: DateObject): number {
|
||||||
|
const result = this.time - value.time;
|
||||||
|
if (result > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (result < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
compareDate(value: KwArgs): number {
|
||||||
|
const left = new DateObject(this);
|
||||||
|
const right = new DateObject(value);
|
||||||
|
|
||||||
|
left._date.setHours(0, 0, 0, 0);
|
||||||
|
right._date.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return left.compare(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareTime(value: KwArgs): number {
|
||||||
|
const left = new DateObject(this);
|
||||||
|
const right = new DateObject(value);
|
||||||
|
|
||||||
|
left._date.setFullYear(0, 0, 0);
|
||||||
|
right._date.setFullYear(0, 0, 0);
|
||||||
|
|
||||||
|
return left.compare(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this._date.toString();
|
||||||
|
}
|
||||||
|
toDateString(): string {
|
||||||
|
return this._date.toDateString();
|
||||||
|
}
|
||||||
|
toTimeString(): string {
|
||||||
|
return this._date.toTimeString();
|
||||||
|
}
|
||||||
|
toLocaleString(): string {
|
||||||
|
return this._date.toLocaleString();
|
||||||
|
}
|
||||||
|
toLocaleDateString(): string {
|
||||||
|
return this._date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
toLocaleTimeString(): string {
|
||||||
|
return this._date.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
toISOString(): string {
|
||||||
|
return this._date.toISOString();
|
||||||
|
}
|
||||||
|
toJSON(key?: any): string {
|
||||||
|
return this._date.toJSON(key);
|
||||||
|
}
|
||||||
|
valueOf(): number {
|
||||||
|
return this._date.valueOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateObject;
|
||||||
67
src/lib/core/src/Destroyable.ts
Normal file
67
src/lib/core/src/Destroyable.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Handle } from './interfaces';
|
||||||
|
import { createCompositeHandle } from './lang';
|
||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No operation function to replace own once instance is destoryed
|
||||||
|
*/
|
||||||
|
function noop(): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No op function used to replace own, once instance has been destoryed
|
||||||
|
*/
|
||||||
|
function destroyed(): never {
|
||||||
|
throw new Error('Call made to destroyed method');
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Destroyable {
|
||||||
|
/**
|
||||||
|
* register handles for the instance
|
||||||
|
*/
|
||||||
|
private handles: Handle[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.handles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register handles for the instance that will be destroyed when `this.destroy` is called
|
||||||
|
*
|
||||||
|
* @param {Handle} handle The handle to add for the instance
|
||||||
|
* @returns {Handle} a handle for the handle, removes the handle for the instance and calls destroy
|
||||||
|
*/
|
||||||
|
own(handles: Handle | Handle[]): Handle {
|
||||||
|
const handle = Array.isArray(handles) ? createCompositeHandle(...handles) : handles;
|
||||||
|
const { handles: _handles } = this;
|
||||||
|
_handles.push(handle);
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
_handles.splice(_handles.indexOf(handle));
|
||||||
|
handle.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destrpys all handers registered for the instance
|
||||||
|
*
|
||||||
|
* @returns {Promise<any} a promise that resolves once all handles have been destroyed
|
||||||
|
*/
|
||||||
|
destroy(): Promise<any> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.handles.forEach((handle) => {
|
||||||
|
handle && handle.destroy && handle.destroy();
|
||||||
|
});
|
||||||
|
this.destroy = noop;
|
||||||
|
this.own = destroyed;
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Destroyable;
|
||||||
132
src/lib/core/src/Evented.ts
Normal file
132
src/lib/core/src/Evented.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import Map from '@dojo/shim/Map';
|
||||||
|
import { Handle, EventType, EventObject } from './interfaces';
|
||||||
|
import { Destroyable } from './Destroyable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of computed regular expressions, keyed by string
|
||||||
|
*/
|
||||||
|
const regexMap = new Map<string, RegExp>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines is the event type glob has been matched
|
||||||
|
*
|
||||||
|
* @returns boolean that indicates if the glob is matched
|
||||||
|
*/
|
||||||
|
export function isGlobMatch(globString: string | symbol, targetString: string | symbol): boolean {
|
||||||
|
if (typeof targetString === 'string' && typeof globString === 'string' && globString.indexOf('*') !== -1) {
|
||||||
|
let regex: RegExp;
|
||||||
|
if (regexMap.has(globString)) {
|
||||||
|
regex = regexMap.get(globString)!;
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(`^${globString.replace(/\*/g, '.*')}$`);
|
||||||
|
regexMap.set(globString, regex);
|
||||||
|
}
|
||||||
|
return regex.test(targetString);
|
||||||
|
} else {
|
||||||
|
return globString === targetString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventedCallback<T = EventType, E extends EventObject<T> = EventObject<T>> = {
|
||||||
|
/**
|
||||||
|
* A callback that takes an `event` argument
|
||||||
|
*
|
||||||
|
* @param event The event object
|
||||||
|
*/
|
||||||
|
|
||||||
|
(event: E): boolean | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CustomEventTypes<T extends EventObject<any> = EventObject<any>> {
|
||||||
|
[index: string]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type which is either a targeted event listener or an array of listeners
|
||||||
|
* @template T The type of target for the events
|
||||||
|
* @template E The event type for the events
|
||||||
|
*/
|
||||||
|
export type EventedCallbackOrArray<T = EventType, E extends EventObject<T> = EventObject<T>> =
|
||||||
|
| EventedCallback<T, E>
|
||||||
|
| EventedCallback<T, E>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Class
|
||||||
|
*/
|
||||||
|
export class Evented<
|
||||||
|
M extends CustomEventTypes = {},
|
||||||
|
T = EventType,
|
||||||
|
O extends EventObject<T> = EventObject<T>
|
||||||
|
> extends Destroyable {
|
||||||
|
// The following member is purely so TypeScript remembers the type of `M` when extending so
|
||||||
|
// that the utilities in `on.ts` will work https://github.com/Microsoft/TypeScript/issues/20348
|
||||||
|
// tslint:disable-next-line
|
||||||
|
protected __typeMap__?: M;
|
||||||
|
/**
|
||||||
|
* map of listeners keyed by event type
|
||||||
|
*/
|
||||||
|
protected listenersMap: Map<T | keyof M, EventedCallback<T, O>[]> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the event object for the specified type
|
||||||
|
*
|
||||||
|
* @param event the event to emit
|
||||||
|
*/
|
||||||
|
emit<K extends keyof M>(event: M[K]): void;
|
||||||
|
emit(event: O): void;
|
||||||
|
emit(event: any): void {
|
||||||
|
this.listenersMap.forEach((methods, type) => {
|
||||||
|
if (isGlobMatch(type as any, event.type)) {
|
||||||
|
[...methods].forEach((method) => {
|
||||||
|
method.call(this, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catch all handler for various call signatures. The signatures are defined in
|
||||||
|
* `BaseEventedEvents`. You can add your own event type -> handler types by extending
|
||||||
|
* `BaseEventedEvents`. See example for details.
|
||||||
|
*
|
||||||
|
* @param args
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* interface WidgetBaseEvents extends BaseEventedEvents {
|
||||||
|
* (type: 'properties:changed', handler: PropertiesChangedHandler): Handle;
|
||||||
|
* }
|
||||||
|
* class WidgetBase extends Evented {
|
||||||
|
* on: WidgetBaseEvents;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
on<K extends keyof M>(type: K, listener: EventedCallbackOrArray<K, M[K]>): Handle;
|
||||||
|
on(type: T, listener: EventedCallbackOrArray<T, O>): Handle;
|
||||||
|
on(type: any, listener: EventedCallbackOrArray<any, any>): Handle {
|
||||||
|
if (Array.isArray(listener)) {
|
||||||
|
const handles = listener.map((listener) => this._addListener(type, listener));
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
handles.forEach((handle) => handle.destroy());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this._addListener(type, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addListener(type: T | keyof M, listener: EventedCallback<T, O>) {
|
||||||
|
const listeners = this.listenersMap.get(type) || [];
|
||||||
|
listeners.push(listener);
|
||||||
|
this.listenersMap.set(type, listeners);
|
||||||
|
return {
|
||||||
|
destroy: () => {
|
||||||
|
const listeners = this.listenersMap.get(type) || [];
|
||||||
|
listeners.splice(listeners.indexOf(listener), 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Evented;
|
||||||
180
src/lib/core/src/IdentityRegistry.ts
Normal file
180
src/lib/core/src/IdentityRegistry.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import Map from '@dojo/shim/Map';
|
||||||
|
import WeakMap from '@dojo/shim/WeakMap';
|
||||||
|
import { Iterable, IterableIterator } from '@dojo/shim/iterator';
|
||||||
|
import { Handle } from './interfaces';
|
||||||
|
import '@dojo/shim/Symbol';
|
||||||
|
import List from './List';
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
interface Entry<V> {
|
||||||
|
readonly handle: Handle;
|
||||||
|
readonly value: V;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State<V extends object> {
|
||||||
|
readonly entryMap: Map<Identity, Entry<V>>;
|
||||||
|
readonly idMap: WeakMap<V, Identity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateStateMap = new WeakMap<IdentityRegistry<any>, State<any>>();
|
||||||
|
|
||||||
|
function getState<V extends object>(instance: IdentityRegistry<V>): State<V> {
|
||||||
|
return privateStateMap.get(instance)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry identities can be strings or symbols. Note that the empty string is allowed.
|
||||||
|
*/
|
||||||
|
export type Identity = string | symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry of values, mapped by identities.
|
||||||
|
*/
|
||||||
|
export class IdentityRegistry<V extends object> implements Iterable<[Identity, V]> {
|
||||||
|
constructor() {
|
||||||
|
privateStateMap.set(this, {
|
||||||
|
entryMap: new Map<Identity, Entry<V>>(),
|
||||||
|
idMap: new WeakMap<V, Identity>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a value by its identifier.
|
||||||
|
*
|
||||||
|
* Throws if no value has been registered for the given identifier.
|
||||||
|
*
|
||||||
|
* @param id The identifier
|
||||||
|
* @return The value
|
||||||
|
*/
|
||||||
|
get(id: Identity): V {
|
||||||
|
const entry = getState(this).entryMap.get(id);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`Could not find a value for identity '${id.toString()}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the value has been registered.
|
||||||
|
* @param value The value
|
||||||
|
* @return `true` if the value has been registered, `false` otherwise
|
||||||
|
*/
|
||||||
|
contains(value: V): boolean {
|
||||||
|
return getState(this).idMap.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove from the registry the value for a given identifier.
|
||||||
|
* @param id The identifier
|
||||||
|
* @return `true` if the value was removed, `false` otherwise
|
||||||
|
*/
|
||||||
|
delete(id: Identity): boolean {
|
||||||
|
const entry = getState(this).entryMap.get(id);
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.handle.destroy();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a value has been registered for the given identifier.
|
||||||
|
* @param id The identifier
|
||||||
|
* @return `true` if a value has been registered, `false` otherwise
|
||||||
|
*/
|
||||||
|
has(id: Identity): boolean {
|
||||||
|
return getState(this).entryMap.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the identifier for which the given value has been registered.
|
||||||
|
*
|
||||||
|
* Throws if the value hasn't been registered.
|
||||||
|
*
|
||||||
|
* @param value The value
|
||||||
|
* @return The identifier otherwise
|
||||||
|
*/
|
||||||
|
identify(value: V): Identity | undefined {
|
||||||
|
if (!this.contains(value)) {
|
||||||
|
throw new Error('Could not identify non-registered value');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getState(this).idMap.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new value with a new identity.
|
||||||
|
*
|
||||||
|
* Throws if a different value has already been registered for the given identity,
|
||||||
|
* or if the value has already been registered with a different identity.
|
||||||
|
*
|
||||||
|
* @param id The identifier
|
||||||
|
* @param value The value
|
||||||
|
* @return A handle for deregistering the value. Note that when called repeatedly with
|
||||||
|
* the same identifier and value combination, the same handle is returned
|
||||||
|
*/
|
||||||
|
register(id: Identity, value: V): Handle {
|
||||||
|
const entryMap = getState<V>(this).entryMap;
|
||||||
|
const existingEntry = entryMap.get(id);
|
||||||
|
if (existingEntry && existingEntry.value !== value) {
|
||||||
|
const str = id.toString();
|
||||||
|
throw new Error(`A value has already been registered for the given identity (${str})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingId = this.contains(value) ? this.identify(value) : null;
|
||||||
|
if (existingId && existingId !== id) {
|
||||||
|
const str = existingId.toString();
|
||||||
|
throw new Error(`The value has already been registered with a different identity (${str})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding the same value with the same id is a noop, return the original handle.
|
||||||
|
if (existingEntry && existingId) {
|
||||||
|
return existingEntry.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = {
|
||||||
|
destroy: () => {
|
||||||
|
handle.destroy = noop;
|
||||||
|
getState(this).entryMap.delete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
entryMap.set(id, { handle, value });
|
||||||
|
getState(this).idMap.set(value, id);
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[Identity, V]> {
|
||||||
|
const values = new List<[Identity, V]>();
|
||||||
|
|
||||||
|
getState(this).entryMap.forEach((value: Entry<V>, key: Identity) => {
|
||||||
|
values.add([key, value.value]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return values.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<V> {
|
||||||
|
const values = new List<V>();
|
||||||
|
|
||||||
|
getState(this).entryMap.forEach((value: Entry<V>, key: Identity) => {
|
||||||
|
values.add(value.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return values.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
ids(): IterableIterator<Identity> {
|
||||||
|
return getState(this).entryMap.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): IterableIterator<[Identity, V]> {
|
||||||
|
return this.entries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IdentityRegistry;
|
||||||
106
src/lib/core/src/List.ts
Normal file
106
src/lib/core/src/List.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { isArrayLike, isIterable, Iterable, IterableIterator, ShimIterator } from '@dojo/shim/iterator';
|
||||||
|
import WeakMap from '@dojo/shim/WeakMap';
|
||||||
|
|
||||||
|
const listItems: WeakMap<List<any>, any[]> = new WeakMap<List<any>, any[]>();
|
||||||
|
|
||||||
|
function getListItems<T>(list: List<T>): T[] {
|
||||||
|
return (listItems.get(list) || []) as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class List<T> {
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return getListItems(this).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source?: Iterable<T> | ArrayLike<T>) {
|
||||||
|
listItems.set(this, []);
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
if (isArrayLike(source)) {
|
||||||
|
for (let i = 0; i < source.length; i++) {
|
||||||
|
this.add(source[i]);
|
||||||
|
}
|
||||||
|
} else if (isIterable(source)) {
|
||||||
|
for (const item of source) {
|
||||||
|
this.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: T): this {
|
||||||
|
getListItems(this).push(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
listItems.set(this, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(idx: number): boolean {
|
||||||
|
if (idx < this.size) {
|
||||||
|
getListItems(this).splice(idx, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[number, T]> {
|
||||||
|
return new ShimIterator<[number, T]>(getListItems(this).map<[number, T]>((value, index) => [index, value]));
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(fn: (value: T, idx: number, list: this) => void, thisArg?: any): void {
|
||||||
|
getListItems(this).forEach(fn.bind(thisArg ? thisArg : this));
|
||||||
|
}
|
||||||
|
|
||||||
|
has(idx: number): boolean {
|
||||||
|
return this.size > idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
includes(value: T): boolean {
|
||||||
|
return getListItems(this).indexOf(value) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexOf(value: T): number {
|
||||||
|
return getListItems(this).indexOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
join(separator: string = ','): string {
|
||||||
|
return getListItems(this).join(separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<number> {
|
||||||
|
return new ShimIterator<number>(getListItems(this).map<number>((_, index) => index));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndexOf(value: T): number {
|
||||||
|
return getListItems(this).lastIndexOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(value: T): void {
|
||||||
|
this.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pop(): T | undefined {
|
||||||
|
return getListItems(this).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
splice(start: number, deleteCount?: number, ...newItems: T[]): T[] {
|
||||||
|
return getListItems(this).splice(
|
||||||
|
start,
|
||||||
|
deleteCount === undefined ? this.size - start : deleteCount,
|
||||||
|
...newItems
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<T> {
|
||||||
|
return new ShimIterator<T>(getListItems(this).map<T>((value) => value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default List;
|
||||||
89
src/lib/core/src/MatchRegistry.ts
Normal file
89
src/lib/core/src/MatchRegistry.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { Handle } from './interfaces';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entry in a MatchRegistry. Each Entry contains a test to determine whether the Entry is applicable, and a value for
|
||||||
|
* the entry.
|
||||||
|
*/
|
||||||
|
interface Entry<T> {
|
||||||
|
readonly test: Test | null;
|
||||||
|
readonly value: T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry of values tagged with matchers.
|
||||||
|
*/
|
||||||
|
export class MatchRegistry<T> {
|
||||||
|
protected _defaultValue: T | undefined;
|
||||||
|
private readonly _entries: Entry<T>[] | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new MatchRegistry, optionally containing a given default value.
|
||||||
|
*/
|
||||||
|
constructor(defaultValue?: T) {
|
||||||
|
this._defaultValue = defaultValue;
|
||||||
|
this._entries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first entry in this registry that matches the given arguments. If no entry matches and the registry
|
||||||
|
* was created with a default value, that value will be returned. Otherwise, an exception is thrown.
|
||||||
|
*
|
||||||
|
* @param ...args Arguments that will be used to select a matching value.
|
||||||
|
* @returns the matching value, or a default value if one exists.
|
||||||
|
*/
|
||||||
|
match(...args: any[]): T {
|
||||||
|
const entries = this._entries ? this._entries.slice(0) : [];
|
||||||
|
let entry: Entry<T>;
|
||||||
|
|
||||||
|
for (let i = 0; (entry = entries[i]); ++i) {
|
||||||
|
if (entry.value && entry.test && entry.test.apply(null, args)) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._defaultValue !== undefined) {
|
||||||
|
return this._defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No match found');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a test + value pair with this registry.
|
||||||
|
*
|
||||||
|
* @param test The test that will be used to determine if the registered value matches a set of arguments.
|
||||||
|
* @param value A value being registered.
|
||||||
|
* @param first If true, the newly registered test and value will be the first entry in the registry.
|
||||||
|
*/
|
||||||
|
register(test: Test | null, value: T | null, first?: boolean): Handle {
|
||||||
|
let entries = this._entries;
|
||||||
|
let entry: Entry<T> | null = {
|
||||||
|
test: test,
|
||||||
|
value: value
|
||||||
|
};
|
||||||
|
|
||||||
|
(<any>entries)[first ? 'unshift' : 'push'](entry);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy: function(this: Handle) {
|
||||||
|
this.destroy = function(): void {};
|
||||||
|
let i = 0;
|
||||||
|
if (entries && entry) {
|
||||||
|
while ((i = entries.indexOf(entry, i)) > -1) {
|
||||||
|
entries.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test = value = entries = entry = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interface that a test function must implement.
|
||||||
|
*/
|
||||||
|
export interface Test {
|
||||||
|
(...args: any[]): boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MatchRegistry;
|
||||||
237
src/lib/core/src/MultiMap.ts
Normal file
237
src/lib/core/src/MultiMap.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { from as arrayFrom } from '@dojo/shim/array';
|
||||||
|
import { isArrayLike, isIterable, Iterable, IterableIterator, ShimIterator } from '@dojo/shim/iterator';
|
||||||
|
import Map from '@dojo/shim/Map';
|
||||||
|
import '@dojo/shim/Symbol';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map implmentation that supports multiple keys for specific value.
|
||||||
|
*
|
||||||
|
* @param T Accepts the type of the value
|
||||||
|
*/
|
||||||
|
export class MultiMap<T> implements Map<any[], T> {
|
||||||
|
private _map: Map<any, any>;
|
||||||
|
private _key: symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* @param iterator an array or iterator of tuples to initialize the map with.
|
||||||
|
*/
|
||||||
|
constructor(iterable?: ArrayLike<[any[], T]> | Iterable<[any[], T]>) {
|
||||||
|
this._map = new Map<any, any>();
|
||||||
|
this._key = Symbol();
|
||||||
|
if (iterable) {
|
||||||
|
if (isArrayLike(iterable)) {
|
||||||
|
for (let i = 0; i < iterable.length; i++) {
|
||||||
|
const value = iterable[i];
|
||||||
|
this.set(value[0], value[1]);
|
||||||
|
}
|
||||||
|
} else if (isIterable(iterable)) {
|
||||||
|
for (const value of iterable) {
|
||||||
|
this.set(value[0], value[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value for the array of keys provided
|
||||||
|
*
|
||||||
|
* @param keys The array of keys to store the value against
|
||||||
|
* @param value the value of the map entry
|
||||||
|
*
|
||||||
|
* @return the multi map instance
|
||||||
|
*/
|
||||||
|
set(keys: any[], value: T): this {
|
||||||
|
let map = this._map;
|
||||||
|
let childMap;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (map.get(keys[i])) {
|
||||||
|
map = map.get(keys[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
childMap = new Map<any, any>();
|
||||||
|
map.set(keys[i], childMap);
|
||||||
|
map = childMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(this._key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value entry for the array of keys
|
||||||
|
*
|
||||||
|
* @param keys The array of keys to look up the value for
|
||||||
|
*
|
||||||
|
* @return The value if found otherwise `undefined`
|
||||||
|
*/
|
||||||
|
get(keys: any[]): T | undefined {
|
||||||
|
let map = this._map;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
map = map.get(keys[i]);
|
||||||
|
|
||||||
|
if (!map) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.get(this._key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating if the key exists in the map
|
||||||
|
*
|
||||||
|
* @return boolean true if the key exists otherwise false
|
||||||
|
*/
|
||||||
|
has(keys: any[]): boolean {
|
||||||
|
let map = this._map;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
map = map.get(keys[i]);
|
||||||
|
if (!map) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the map, based on the number of unique keys
|
||||||
|
*/
|
||||||
|
get size(): number {
|
||||||
|
return arrayFrom(this.keys()).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the entry for the key provided.
|
||||||
|
*
|
||||||
|
* @param keys the key of the entry to remove
|
||||||
|
* @return boolean trus if the entry was deleted, false if the entry was not found
|
||||||
|
*/
|
||||||
|
delete(keys: any[]): boolean {
|
||||||
|
let map = this._map;
|
||||||
|
const path = [this._map];
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
map = map.get(keys[i]);
|
||||||
|
path.push(map);
|
||||||
|
if (!map) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.delete(this._key);
|
||||||
|
|
||||||
|
for (let i = keys.length - 1; i >= 0; i--) {
|
||||||
|
map = path[i].get(keys[i]);
|
||||||
|
if (map.size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
path[i].delete(keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an iterator that yields each value in the map
|
||||||
|
*
|
||||||
|
* @return An iterator containing the instance's values.
|
||||||
|
*/
|
||||||
|
values(): IterableIterator<T> {
|
||||||
|
const values: T[] = [];
|
||||||
|
|
||||||
|
const getValues = (map: Map<any, any>) => {
|
||||||
|
map.forEach((value, key) => {
|
||||||
|
if (key === this._key) {
|
||||||
|
values.push(value);
|
||||||
|
} else {
|
||||||
|
getValues(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getValues(this._map);
|
||||||
|
return new ShimIterator<T>(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an iterator that yields each key array in the map
|
||||||
|
*
|
||||||
|
* @return An iterator containing the instance's keys.
|
||||||
|
*/
|
||||||
|
keys(): IterableIterator<any[]> {
|
||||||
|
const finalKeys: any[][] = [];
|
||||||
|
|
||||||
|
const getKeys = (map: Map<any, any>, keys: any[] = []) => {
|
||||||
|
map.forEach((value, key) => {
|
||||||
|
if (key === this._key) {
|
||||||
|
finalKeys.push(keys);
|
||||||
|
} else {
|
||||||
|
const nextKeys = [...keys, key];
|
||||||
|
getKeys(value, nextKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getKeys(this._map);
|
||||||
|
return new ShimIterator<any[]>(finalKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator that yields each key/value pair as an array.
|
||||||
|
*
|
||||||
|
* @return An iterator for each key/value pair in the instance.
|
||||||
|
*/
|
||||||
|
entries(): IterableIterator<[any[], T]> {
|
||||||
|
const finalEntries: [any[], T][] = [];
|
||||||
|
|
||||||
|
const getKeys = (map: Map<any, any>, keys: any[] = []) => {
|
||||||
|
map.forEach((value, key) => {
|
||||||
|
if (key === this._key) {
|
||||||
|
finalEntries.push([keys, value]);
|
||||||
|
} else {
|
||||||
|
const nextKeys = [...keys, key];
|
||||||
|
getKeys(value, nextKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getKeys(this._map);
|
||||||
|
return new ShimIterator<[any[], T]>(finalEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a given function for each map entry. The function
|
||||||
|
* is invoked with three arguments: the element value, the
|
||||||
|
* element key, and the associated Map instance.
|
||||||
|
*
|
||||||
|
* @param callback The function to execute for each map entry,
|
||||||
|
* @param context The value to use for `this` for each execution of the calback
|
||||||
|
*/
|
||||||
|
forEach(callback: (value: T, key: any[], mapInstance: MultiMap<T>) => any, context?: {}): void {
|
||||||
|
const entries = this.entries();
|
||||||
|
|
||||||
|
for (const value of entries) {
|
||||||
|
callback.call(context, value[1], value[0], this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all keys and their associated values.
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this._map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): IterableIterator<[any[], T]> {
|
||||||
|
return this.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.toStringTag] = 'MultiMap';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MultiMap;
|
||||||
171
src/lib/core/src/Observable.ts
Normal file
171
src/lib/core/src/Observable.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import ObservableShim, { ObservableObject, Subscribable, SubscriptionObserver } from '@dojo/shim/Observable';
|
||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
import { Iterable } from '@dojo/shim/iterator';
|
||||||
|
|
||||||
|
function isSubscribable(object: any): object is Subscribable<any> {
|
||||||
|
return object && object.subscribe !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Observable<T> extends ObservableShim<T> {
|
||||||
|
static of<T>(...items: T[]): Observable<T> {
|
||||||
|
return <Observable<T>>super.of(...items);
|
||||||
|
}
|
||||||
|
|
||||||
|
static from<T>(item: Iterable<T> | ArrayLike<T> | ObservableObject): Observable<T> {
|
||||||
|
return <Observable<T>>super.from(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static defer<T>(deferFunction: () => Subscribable<T>): Observable<T> {
|
||||||
|
return new Observable<T>((observer) => {
|
||||||
|
const trueObservable = deferFunction();
|
||||||
|
|
||||||
|
return trueObservable.subscribe({
|
||||||
|
next(value: T) {
|
||||||
|
return observer.next(value);
|
||||||
|
},
|
||||||
|
error(errorValue?: any) {
|
||||||
|
return observer.error(errorValue);
|
||||||
|
},
|
||||||
|
complete(completeValue?: any) {
|
||||||
|
observer.complete(completeValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toPromise(): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
this.subscribe({
|
||||||
|
next(value: T) {
|
||||||
|
resolve(value);
|
||||||
|
},
|
||||||
|
error(error: any) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
map<U>(mapFunction: (x: T) => U): Observable<U> {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if (typeof mapFunction !== 'function') {
|
||||||
|
throw new TypeError('Map parameter must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Observable<U>((observer: SubscriptionObserver<U>) => {
|
||||||
|
self.subscribe({
|
||||||
|
next(value: T) {
|
||||||
|
try {
|
||||||
|
const result: U = mapFunction(value);
|
||||||
|
return observer.next(result);
|
||||||
|
} catch (e) {
|
||||||
|
return observer.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error(errorValue?: any) {
|
||||||
|
return observer.error(errorValue);
|
||||||
|
},
|
||||||
|
complete(completeValue?: any) {
|
||||||
|
return observer.complete(completeValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(filterFunction: (x: T) => boolean): Observable<T> {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if (typeof filterFunction !== 'function') {
|
||||||
|
throw new TypeError('Filter argument must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Observable<T>((observer: SubscriptionObserver<T>) => {
|
||||||
|
self.subscribe({
|
||||||
|
next(value: T) {
|
||||||
|
try {
|
||||||
|
if (filterFunction(value)) {
|
||||||
|
return observer.next(value);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return observer.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error(errorValue?: any) {
|
||||||
|
return observer.error(errorValue);
|
||||||
|
},
|
||||||
|
complete(completeValue?: any) {
|
||||||
|
return observer.complete(completeValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): Observable<T[]> {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return new Observable<T[]>((observer) => {
|
||||||
|
const values: T[] = [];
|
||||||
|
|
||||||
|
self.subscribe({
|
||||||
|
next(value: T) {
|
||||||
|
values.push(value);
|
||||||
|
},
|
||||||
|
error(errorValue?: any) {
|
||||||
|
return observer.error(errorValue);
|
||||||
|
},
|
||||||
|
complete(completeValue?: any) {
|
||||||
|
observer.next(values);
|
||||||
|
observer.complete(completeValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeAll(concurrent: number): Observable<any> {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return new Observable<Observable<any>>((observer) => {
|
||||||
|
let active: any[] = [];
|
||||||
|
let queue: any[] = [];
|
||||||
|
|
||||||
|
function checkForComplete() {
|
||||||
|
if (active.length === 0 && queue.length === 0) {
|
||||||
|
observer.complete();
|
||||||
|
} else if (queue.length > 0 && active.length < concurrent) {
|
||||||
|
const item = queue.shift();
|
||||||
|
|
||||||
|
if (isSubscribable(item)) {
|
||||||
|
const itemIndex = active.length;
|
||||||
|
active.push(item);
|
||||||
|
|
||||||
|
item.subscribe({
|
||||||
|
next(value: any) {
|
||||||
|
observer.next(value);
|
||||||
|
},
|
||||||
|
complete() {
|
||||||
|
active.splice(itemIndex, 1);
|
||||||
|
checkForComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
observer.next(item);
|
||||||
|
checkForComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.subscribe({
|
||||||
|
next(value: T) {
|
||||||
|
queue.push(value);
|
||||||
|
},
|
||||||
|
complete() {
|
||||||
|
checkForComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for convienence, re-export some interfaces from shim
|
||||||
|
export { Observable, Subscribable, SubscriptionObserver as Observer };
|
||||||
71
src/lib/core/src/QueuingEvented.ts
Normal file
71
src/lib/core/src/QueuingEvented.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Handle, EventObject, EventType } from './interfaces';
|
||||||
|
import Map from '@dojo/shim/Map';
|
||||||
|
import Evented, { CustomEventTypes, isGlobMatch, EventedCallbackOrArray } from './Evented';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
class QueuingEvented<
|
||||||
|
M extends CustomEventTypes = {},
|
||||||
|
T = EventType,
|
||||||
|
O extends EventObject<T> = EventObject<T>
|
||||||
|
> extends Evented<M, T, O> {
|
||||||
|
private _queue: Map<string | symbol, EventObject[]> = new Map();
|
||||||
|
|
||||||
|
public maxEvents = 0;
|
||||||
|
|
||||||
|
emit<K extends keyof M>(event: M[K]): void;
|
||||||
|
emit(event: O): void;
|
||||||
|
emit(event: any): void {
|
||||||
|
super.emit(event);
|
||||||
|
|
||||||
|
let hasMatch = false;
|
||||||
|
|
||||||
|
this.listenersMap.forEach((method, type) => {
|
||||||
|
// Since `type` is generic, the compiler doesn't know what type it is and `isGlobMatch` requires `string | symbol`
|
||||||
|
if (isGlobMatch(type as any, event.type)) {
|
||||||
|
hasMatch = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasMatch) {
|
||||||
|
let queue = this._queue.get(event.type);
|
||||||
|
|
||||||
|
if (!queue) {
|
||||||
|
queue = [];
|
||||||
|
this._queue.set(event.type, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.push(event);
|
||||||
|
|
||||||
|
if (this.maxEvents > 0) {
|
||||||
|
while (queue.length > this.maxEvents) {
|
||||||
|
queue.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on<K extends keyof M>(type: K, listener: EventedCallbackOrArray<K, M[K]>): Handle;
|
||||||
|
on(type: T, listener: EventedCallbackOrArray<T, O>): Handle;
|
||||||
|
on(type: any, listener: EventedCallbackOrArray<any, any>): Handle {
|
||||||
|
let handle = super.on(type, listener);
|
||||||
|
|
||||||
|
this.listenersMap.forEach((method, listenerType) => {
|
||||||
|
this._queue.forEach((events, queuedType) => {
|
||||||
|
if (isGlobMatch(listenerType as any, queuedType)) {
|
||||||
|
events.forEach((event) => this.emit(event));
|
||||||
|
this._queue.delete(queuedType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueuingEvented;
|
||||||
115
src/lib/core/src/Scheduler.ts
Normal file
115
src/lib/core/src/Scheduler.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { Handle } from './interfaces';
|
||||||
|
import { QueueItem, queueTask } from './queue';
|
||||||
|
|
||||||
|
function getQueueHandle(item: QueueItem): Handle {
|
||||||
|
return {
|
||||||
|
destroy: function(this: Handle) {
|
||||||
|
this.destroy = function() {};
|
||||||
|
item.isActive = false;
|
||||||
|
item.callback = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KwArgs {
|
||||||
|
deferWhileProcessing?: boolean;
|
||||||
|
queueFunction?: (callback: (...args: any[]) => any) => Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Scheduler {
|
||||||
|
protected readonly _boundDispatch: () => void;
|
||||||
|
protected _deferred: QueueItem[] | null = null;
|
||||||
|
protected _isProcessing: boolean;
|
||||||
|
protected readonly _queue: QueueItem[];
|
||||||
|
protected _task: Handle | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any callbacks registered during should be added to the current batch (`false`)
|
||||||
|
* or deferred until the next batch (`true`, default).
|
||||||
|
*/
|
||||||
|
deferWhileProcessing: boolean | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows users to specify the function that should be used to schedule callbacks.
|
||||||
|
* If no function is provided, then `queueTask` will be used.
|
||||||
|
*/
|
||||||
|
queueFunction: (callback: (...args: any[]) => any) => Handle;
|
||||||
|
|
||||||
|
protected _defer(callback: (...args: any[]) => void): Handle {
|
||||||
|
const item: QueueItem = {
|
||||||
|
isActive: true,
|
||||||
|
callback: callback
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this._deferred) {
|
||||||
|
this._deferred = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._deferred.push(item);
|
||||||
|
|
||||||
|
return getQueueHandle(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _dispatch(): void {
|
||||||
|
this._isProcessing = true;
|
||||||
|
if (this._task) {
|
||||||
|
this._task.destroy();
|
||||||
|
this._task = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = this._queue;
|
||||||
|
let item: QueueItem | undefined;
|
||||||
|
|
||||||
|
while ((item = queue.shift())) {
|
||||||
|
if (item.isActive && item.callback) {
|
||||||
|
item.callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isProcessing = false;
|
||||||
|
|
||||||
|
let deferred: QueueItem[] | null = this._deferred;
|
||||||
|
if (deferred && deferred.length) {
|
||||||
|
this._deferred = null;
|
||||||
|
|
||||||
|
let item: QueueItem | undefined;
|
||||||
|
while ((item = deferred.shift())) {
|
||||||
|
this._schedule(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _schedule(item: QueueItem): void {
|
||||||
|
if (!this._task) {
|
||||||
|
this._task = this.queueFunction(this._boundDispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._queue.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(kwArgs?: KwArgs) {
|
||||||
|
this.deferWhileProcessing = kwArgs && 'deferWhileProcessing' in kwArgs ? kwArgs.deferWhileProcessing : true;
|
||||||
|
this.queueFunction = kwArgs && kwArgs.queueFunction ? kwArgs.queueFunction : queueTask;
|
||||||
|
|
||||||
|
this._boundDispatch = this._dispatch.bind(this);
|
||||||
|
this._isProcessing = false;
|
||||||
|
this._queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule(callback: (...args: any[]) => void): Handle {
|
||||||
|
if (this._isProcessing && this.deferWhileProcessing) {
|
||||||
|
return this._defer(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const item: QueueItem = {
|
||||||
|
isActive: true,
|
||||||
|
callback: callback
|
||||||
|
};
|
||||||
|
|
||||||
|
this._schedule(item);
|
||||||
|
|
||||||
|
return getQueueHandle(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Scheduler;
|
||||||
193
src/lib/core/src/UrlSearchParams.ts
Normal file
193
src/lib/core/src/UrlSearchParams.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { Hash } from './interfaces';
|
||||||
|
import { duplicate } from './lang';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object with string keys and string or string array values that describes a query string.
|
||||||
|
*/
|
||||||
|
export type ParamList = Hash<string | string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a query string, returning a ParamList object.
|
||||||
|
*/
|
||||||
|
function parseQueryString(input: string): ParamList {
|
||||||
|
const query: Hash<string[]> = {};
|
||||||
|
const splits = input.split('&');
|
||||||
|
|
||||||
|
for (let i = 0; i < splits.length; i++) {
|
||||||
|
const entry = splits[i];
|
||||||
|
const indexOfFirstEquals = entry.indexOf('=');
|
||||||
|
let key: string;
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
if (indexOfFirstEquals >= 0) {
|
||||||
|
key = entry.slice(0, indexOfFirstEquals);
|
||||||
|
value = entry.slice(indexOfFirstEquals + 1);
|
||||||
|
} else {
|
||||||
|
key = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = key ? decodeURIComponent(key) : '';
|
||||||
|
value = value ? decodeURIComponent(value) : '';
|
||||||
|
|
||||||
|
if (key in query) {
|
||||||
|
query[key].push(value);
|
||||||
|
} else {
|
||||||
|
query[key] = [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of URL query search parameters.
|
||||||
|
*/
|
||||||
|
export class UrlSearchParams {
|
||||||
|
/**
|
||||||
|
* Constructs a new UrlSearchParams from a query string, an object of parameters and values, or another
|
||||||
|
* UrlSearchParams.
|
||||||
|
*/
|
||||||
|
constructor(input?: string | ParamList | UrlSearchParams) {
|
||||||
|
let list: ParamList = {};
|
||||||
|
|
||||||
|
if (input instanceof UrlSearchParams) {
|
||||||
|
// Copy the incoming UrlSearchParam's internal list
|
||||||
|
list = <ParamList>duplicate(input._list);
|
||||||
|
} else if (typeof input === 'object') {
|
||||||
|
// Copy the incoming object, assuming its property values are either arrays or strings
|
||||||
|
list = {};
|
||||||
|
for (const key in input) {
|
||||||
|
const value = (<ParamList>input)[key];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
list[key] = value.length ? value.slice() : [''];
|
||||||
|
} else if (value == null) {
|
||||||
|
list[key] = [''];
|
||||||
|
} else {
|
||||||
|
list[key] = [<string>value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof input === 'string') {
|
||||||
|
// Parse the incoming string as a query string
|
||||||
|
list = parseQueryString(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._list = list as Hash<string[] | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps property keys to arrays of values. The value for any property that has been set will be an array containing
|
||||||
|
* at least one item. Properties that have been deleted will have a value of 'undefined'.
|
||||||
|
*/
|
||||||
|
protected readonly _list: Hash<string[] | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a new value to the set of values for a key.
|
||||||
|
* @param key The key to add a value for
|
||||||
|
* @param value The value to add
|
||||||
|
*/
|
||||||
|
append(key: string, value: string): void {
|
||||||
|
if (!this.has(key)) {
|
||||||
|
this.set(key, value);
|
||||||
|
} else {
|
||||||
|
const values = this._list[key];
|
||||||
|
if (values) {
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all values for a key.
|
||||||
|
* @param key The key whose values are to be removed
|
||||||
|
*/
|
||||||
|
delete(key: string): void {
|
||||||
|
// Set to undefined rather than deleting the key, for better consistency across browsers.
|
||||||
|
// If a deleted key is re-added, most browsers put it at the end of iteration order, but IE maintains
|
||||||
|
// its original position. This approach maintains the original position everywhere.
|
||||||
|
this._list[key] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first value associated with a key.
|
||||||
|
* @param key The key to return the first value for
|
||||||
|
* @return The first string value for the key
|
||||||
|
*/
|
||||||
|
get(key: string): string | undefined {
|
||||||
|
if (!this.has(key)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const value = this._list[key];
|
||||||
|
return value ? value[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the values associated with a key.
|
||||||
|
* @param key The key to return all values for
|
||||||
|
* @return An array of strings containing all values for the key
|
||||||
|
*/
|
||||||
|
getAll(key: string): string[] | undefined {
|
||||||
|
if (!this.has(key)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this._list[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a key has been set to any value, false otherwise.
|
||||||
|
* @param key The key to test for existence
|
||||||
|
* @return A boolean indicating if the key has been set
|
||||||
|
*/
|
||||||
|
has(key: string): boolean {
|
||||||
|
return Array.isArray(this._list[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of all keys which have been set.
|
||||||
|
* @return An array of strings containing all keys set in the UrlSearchParams instance
|
||||||
|
*/
|
||||||
|
keys(): string[] {
|
||||||
|
const keys: string[] = [];
|
||||||
|
|
||||||
|
for (const key in this._list) {
|
||||||
|
if (this.has(key)) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value associated with a key.
|
||||||
|
* @param key The key to set the value of
|
||||||
|
*/
|
||||||
|
set(key: string, value: string): void {
|
||||||
|
this._list[key] = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this object's data as an encoded query string.
|
||||||
|
* @return A string in application/x-www-form-urlencoded format containing all of the set keys/values
|
||||||
|
*/
|
||||||
|
toString(): string {
|
||||||
|
const query: string[] = [];
|
||||||
|
|
||||||
|
for (const key in this._list) {
|
||||||
|
if (!this.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = this._list[key];
|
||||||
|
if (values) {
|
||||||
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
query.push(encodedKey + (values[i] ? '=' + encodeURIComponent(values[i]) : ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.join('&');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UrlSearchParams;
|
||||||
531
src/lib/core/src/aspect.ts
Normal file
531
src/lib/core/src/aspect.ts
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
import { Handle } from './interfaces';
|
||||||
|
import WeakMap from '@dojo/shim/WeakMap';
|
||||||
|
import { createHandle } from './lang';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that provides the necessary APIs to be MapLike
|
||||||
|
*/
|
||||||
|
export interface MapLike<K, V> {
|
||||||
|
get(key: K): V;
|
||||||
|
set(key: K, value?: V): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal type guard that determines if an value is MapLike or not
|
||||||
|
*
|
||||||
|
* @param value The value to guard against
|
||||||
|
*/
|
||||||
|
function isMapLike(value: any): value is MapLike<any, any> {
|
||||||
|
return value && typeof value.get === 'function' && typeof value.set === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Indexable {
|
||||||
|
[method: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The types of objects or maps where advice can be applied
|
||||||
|
*/
|
||||||
|
export type Targetable = MapLike<string | symbol, any> | Indexable;
|
||||||
|
|
||||||
|
type AdviceType = 'before' | 'after' | 'around';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A meta data structure when applying advice
|
||||||
|
*/
|
||||||
|
interface Advised {
|
||||||
|
readonly id?: number;
|
||||||
|
advice?: Function;
|
||||||
|
previous?: Advised;
|
||||||
|
next?: Advised;
|
||||||
|
readonly receiveArguments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that dispatches advice which is decorated with additional
|
||||||
|
* meta data about the advice to apply
|
||||||
|
*/
|
||||||
|
interface Dispatcher {
|
||||||
|
[type: string]: Advised | undefined;
|
||||||
|
(): any;
|
||||||
|
target: any;
|
||||||
|
before?: Advised;
|
||||||
|
around?: Advised;
|
||||||
|
after?: Advised;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinPointDispatchAdvice<T> {
|
||||||
|
before?: JoinPointBeforeAdvice[];
|
||||||
|
after?: JoinPointAfterAdvice<T>[];
|
||||||
|
readonly joinPoint: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinPointAfterAdvice<T> {
|
||||||
|
/**
|
||||||
|
* Advice which is applied *after*, receiving the result and arguments from the join point.
|
||||||
|
*
|
||||||
|
* @param result The result from the function being advised
|
||||||
|
* @param args The arguments that were supplied to the advised function
|
||||||
|
* @returns The value returned from the advice is then the result of calling the method
|
||||||
|
*/
|
||||||
|
(result: T, ...args: any[]): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinPointAroundAdvice<T> {
|
||||||
|
/**
|
||||||
|
* Advice which is applied *around*. The advising function receives the original function and
|
||||||
|
* needs to return a new function which will then invoke the original function.
|
||||||
|
*
|
||||||
|
* @param origFn The original function
|
||||||
|
* @returns A new function which will invoke the original function.
|
||||||
|
*/
|
||||||
|
(origFn: GenericFunction<T>): (...args: any[]) => T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinPointBeforeAdvice {
|
||||||
|
/**
|
||||||
|
* Advice which is applied *before*, receiving the original arguments, if the advising
|
||||||
|
* function returns a value, it is passed further along taking the place of the original
|
||||||
|
* arguments.
|
||||||
|
*
|
||||||
|
* @param args The arguments the method was called with
|
||||||
|
*/
|
||||||
|
(...args: any[]): any[] | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenericFunction<T> {
|
||||||
|
(...args: any[]): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A weak map of dispatchers used to apply the advice
|
||||||
|
*/
|
||||||
|
const dispatchAdviceMap = new WeakMap<Function, JoinPointDispatchAdvice<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A UID for tracking advice ordering
|
||||||
|
*/
|
||||||
|
let nextId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function that advises a join point
|
||||||
|
*
|
||||||
|
* @param dispatcher The current advice dispatcher
|
||||||
|
* @param type The type of before or after advice to apply
|
||||||
|
* @param advice The advice to apply
|
||||||
|
* @param receiveArguments If true, the advice will receive the arguments passed to the join point
|
||||||
|
* @return The handle that will remove the advice
|
||||||
|
*/
|
||||||
|
function adviseObject(
|
||||||
|
dispatcher: Dispatcher | undefined,
|
||||||
|
type: AdviceType,
|
||||||
|
advice: Function | undefined,
|
||||||
|
receiveArguments?: boolean
|
||||||
|
): Handle {
|
||||||
|
let previous = dispatcher && dispatcher[type];
|
||||||
|
let advised: Advised | undefined = {
|
||||||
|
id: nextId++,
|
||||||
|
advice: advice,
|
||||||
|
receiveArguments: receiveArguments
|
||||||
|
};
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
if (type === 'after') {
|
||||||
|
// add the listener to the end of the list
|
||||||
|
// note that we had to change this loop a little bit to workaround a bizarre IE10 JIT bug
|
||||||
|
while (previous.next && (previous = previous.next)) {}
|
||||||
|
previous.next = advised;
|
||||||
|
advised.previous = previous;
|
||||||
|
} else {
|
||||||
|
// add to the beginning
|
||||||
|
if (dispatcher) {
|
||||||
|
dispatcher.before = advised;
|
||||||
|
}
|
||||||
|
advised.next = previous;
|
||||||
|
previous.previous = advised;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatcher && (dispatcher[type] = advised);
|
||||||
|
}
|
||||||
|
|
||||||
|
advice = previous = undefined;
|
||||||
|
|
||||||
|
return createHandle(function() {
|
||||||
|
let { previous = undefined, next = undefined } = advised || {};
|
||||||
|
|
||||||
|
if (dispatcher && !previous && !next) {
|
||||||
|
dispatcher[type] = undefined;
|
||||||
|
} else {
|
||||||
|
if (previous) {
|
||||||
|
previous.next = next;
|
||||||
|
} else {
|
||||||
|
dispatcher && (dispatcher[type] = next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next) {
|
||||||
|
next.previous = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (advised) {
|
||||||
|
delete advised.advice;
|
||||||
|
}
|
||||||
|
dispatcher = advised = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advise a join point (function) with supplied advice
|
||||||
|
*
|
||||||
|
* @param joinPoint The function to be advised
|
||||||
|
* @param type The type of advice to be applied
|
||||||
|
* @param advice The advice to apply
|
||||||
|
*/
|
||||||
|
function adviseJoinPoint<F extends GenericFunction<T>, T>(
|
||||||
|
this: any,
|
||||||
|
joinPoint: F,
|
||||||
|
type: AdviceType,
|
||||||
|
advice: JoinPointBeforeAdvice | JoinPointAfterAdvice<T> | JoinPointAroundAdvice<T>
|
||||||
|
): F {
|
||||||
|
let dispatcher: F;
|
||||||
|
if (type === 'around') {
|
||||||
|
dispatcher = getJoinPointDispatcher(advice.apply(this, [joinPoint]));
|
||||||
|
} else {
|
||||||
|
dispatcher = getJoinPointDispatcher(joinPoint);
|
||||||
|
// cannot have undefined in map due to code logic, using !
|
||||||
|
const adviceMap = dispatchAdviceMap.get(dispatcher)!;
|
||||||
|
if (type === 'before') {
|
||||||
|
(adviceMap.before || (adviceMap.before = [])).unshift(<JoinPointBeforeAdvice>advice);
|
||||||
|
} else {
|
||||||
|
(adviceMap.after || (adviceMap.after = [])).push(advice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal function that resolves or creates the dispatcher for a given join point
|
||||||
|
*
|
||||||
|
* @param target The target object or map
|
||||||
|
* @param methodName The name of the method that the dispatcher should be resolved for
|
||||||
|
* @return The dispatcher
|
||||||
|
*/
|
||||||
|
function getDispatcherObject(target: Targetable, methodName: string | symbol): Dispatcher {
|
||||||
|
const existing = isMapLike(target) ? target.get(methodName) : target && target[methodName];
|
||||||
|
let dispatcher: Dispatcher;
|
||||||
|
|
||||||
|
if (!existing || existing.target !== target) {
|
||||||
|
/* There is no existing dispatcher, therefore we will create one */
|
||||||
|
dispatcher = <Dispatcher>function(this: Dispatcher): any {
|
||||||
|
let executionId = nextId;
|
||||||
|
let args = arguments;
|
||||||
|
let results: any;
|
||||||
|
let before = dispatcher.before;
|
||||||
|
|
||||||
|
while (before) {
|
||||||
|
if (before.advice) {
|
||||||
|
args = before.advice.apply(this, args) || args;
|
||||||
|
}
|
||||||
|
before = before.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dispatcher.around && dispatcher.around.advice) {
|
||||||
|
results = dispatcher.around.advice(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
let after = dispatcher.after;
|
||||||
|
while (after && after.id !== undefined && after.id < executionId) {
|
||||||
|
if (after.advice) {
|
||||||
|
if (after.receiveArguments) {
|
||||||
|
let newResults = after.advice.apply(this, args);
|
||||||
|
results = newResults === undefined ? results : newResults;
|
||||||
|
} else {
|
||||||
|
results = after.advice.call(this, results, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
after = after.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMapLike(target)) {
|
||||||
|
target.set(methodName, dispatcher);
|
||||||
|
} else {
|
||||||
|
target && (target[methodName] = dispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
dispatcher.around = {
|
||||||
|
advice: function(target: any, args: any[]): any {
|
||||||
|
return existing.apply(target, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.target = target;
|
||||||
|
} else {
|
||||||
|
dispatcher = existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the dispatcher function for a given joinPoint (method/function)
|
||||||
|
*
|
||||||
|
* @param joinPoint The function that is to be advised
|
||||||
|
*/
|
||||||
|
function getJoinPointDispatcher<F extends GenericFunction<T>, T>(joinPoint: F): F {
|
||||||
|
function dispatcher(this: Function, ...args: any[]): T {
|
||||||
|
// cannot have undefined in map due to code logic, using !
|
||||||
|
const { before, after, joinPoint } = dispatchAdviceMap.get(dispatcher)!;
|
||||||
|
if (before) {
|
||||||
|
args = before.reduce((previousArgs, advice) => {
|
||||||
|
const currentArgs = advice.apply(this, previousArgs);
|
||||||
|
return currentArgs || previousArgs;
|
||||||
|
}, args);
|
||||||
|
}
|
||||||
|
let result = joinPoint.apply(this, args);
|
||||||
|
if (after) {
|
||||||
|
result = after.reduce((previousResult, advice) => {
|
||||||
|
return advice.apply(this, [previousResult].concat(args));
|
||||||
|
}, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We want to "clone" the advice that has been applied already, if this
|
||||||
|
* joinPoint is already advised */
|
||||||
|
if (dispatchAdviceMap.has(joinPoint)) {
|
||||||
|
// cannot have undefined in map due to code logic, using !
|
||||||
|
const adviceMap = dispatchAdviceMap.get(joinPoint)!;
|
||||||
|
let { before, after } = adviceMap;
|
||||||
|
if (before) {
|
||||||
|
before = before.slice(0);
|
||||||
|
}
|
||||||
|
if (after) {
|
||||||
|
after = after.slice(0);
|
||||||
|
}
|
||||||
|
dispatchAdviceMap.set(dispatcher, {
|
||||||
|
joinPoint: adviceMap.joinPoint,
|
||||||
|
before,
|
||||||
|
after
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
/* Otherwise, this is a new joinPoint, so we will create the advice map afresh */
|
||||||
|
dispatchAdviceMap.set(dispatcher, { joinPoint });
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatcher as F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply advice *after* the supplied joinPoint (function)
|
||||||
|
*
|
||||||
|
* @param joinPoint A function that should have advice applied to
|
||||||
|
* @param advice The after advice
|
||||||
|
*/
|
||||||
|
function afterJoinPoint<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAfterAdvice<T>): F {
|
||||||
|
return adviseJoinPoint(joinPoint, 'after', advice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches "after" advice to be executed after the original method.
|
||||||
|
* The advising function will receive the original method's return value and arguments object.
|
||||||
|
* The value it returns will be returned from the method when it is called (even if the return value is undefined).
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the original method's return value and arguments object
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
function afterObject(
|
||||||
|
target: Targetable,
|
||||||
|
methodName: string | symbol,
|
||||||
|
advice: (originalReturn: any, originalArgs: IArguments) => any
|
||||||
|
): Handle {
|
||||||
|
return adviseObject(getDispatcherObject(target, methodName), 'after', advice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches "after" advice to be executed after the original method.
|
||||||
|
* The advising function will receive the original method's return value and arguments object.
|
||||||
|
* The value it returns will be returned from the method when it is called (even if the return value is undefined).
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the original method's return value and arguments object
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
export function after(
|
||||||
|
target: Targetable,
|
||||||
|
methodName: string | symbol,
|
||||||
|
advice: (originalReturn: any, originalArgs: IArguments) => any
|
||||||
|
): Handle;
|
||||||
|
/**
|
||||||
|
* Apply advice *after* the supplied joinPoint (function)
|
||||||
|
*
|
||||||
|
* @param joinPoint A function that should have advice applied to
|
||||||
|
* @param advice The after advice
|
||||||
|
*/
|
||||||
|
export function after<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAfterAdvice<T>): F;
|
||||||
|
export function after<F extends GenericFunction<T>, T>(
|
||||||
|
joinPointOrTarget: F | Targetable,
|
||||||
|
methodNameOrAdvice: string | symbol | JoinPointAfterAdvice<T>,
|
||||||
|
objectAdvice?: (originalReturn: any, originalArgs: IArguments) => any
|
||||||
|
): Handle | F {
|
||||||
|
if (typeof joinPointOrTarget === 'function') {
|
||||||
|
return afterJoinPoint(joinPointOrTarget, <JoinPointAfterAdvice<T>>methodNameOrAdvice);
|
||||||
|
} else {
|
||||||
|
return afterObject(joinPointOrTarget, <string | symbol>methodNameOrAdvice, objectAdvice!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply advice *around* the supplied joinPoint (function)
|
||||||
|
*
|
||||||
|
* @param joinPoint A function that should have advice applied to
|
||||||
|
* @param advice The around advice
|
||||||
|
*/
|
||||||
|
export function aroundJoinPoint<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAroundAdvice<T>): F {
|
||||||
|
return adviseJoinPoint<F, T>(joinPoint, 'around', advice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches "around" advice around the original method.
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the original function
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
export function aroundObject(
|
||||||
|
target: Targetable,
|
||||||
|
methodName: string | symbol,
|
||||||
|
advice: ((previous: Function) => Function)
|
||||||
|
): Handle {
|
||||||
|
let dispatcher: Dispatcher | undefined = getDispatcherObject(target, methodName);
|
||||||
|
let previous = dispatcher.around;
|
||||||
|
let advised: Function | undefined;
|
||||||
|
if (advice) {
|
||||||
|
advised = advice(function(this: Dispatcher): any {
|
||||||
|
if (previous && previous.advice) {
|
||||||
|
return previous.advice(this, arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.around = {
|
||||||
|
advice: function(target: any, args: any[]): any {
|
||||||
|
return advised ? advised.apply(target, args) : previous && previous.advice && previous.advice(target, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return createHandle(function() {
|
||||||
|
advised = dispatcher = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches "around" advice around the original method.
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the original function
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
export function around(
|
||||||
|
target: Targetable,
|
||||||
|
methodName: string | symbol,
|
||||||
|
advice: ((previous: Function) => Function)
|
||||||
|
): Handle;
|
||||||
|
/**
|
||||||
|
* Apply advice *around* the supplied joinPoint (function)
|
||||||
|
*
|
||||||
|
* @param joinPoint A function that should have advice applied to
|
||||||
|
* @param advice The around advice
|
||||||
|
*/
|
||||||
|
export function around<F extends GenericFunction<T>, T>(joinPoint: F, advice: JoinPointAroundAdvice<T>): F;
|
||||||
|
export function around<F extends GenericFunction<T>, T>(
|
||||||
|
joinPointOrTarget: F | Targetable,
|
||||||
|
methodNameOrAdvice: string | symbol | JoinPointAroundAdvice<T>,
|
||||||
|
objectAdvice?: ((previous: Function) => Function)
|
||||||
|
): Handle | F {
|
||||||
|
if (typeof joinPointOrTarget === 'function') {
|
||||||
|
return aroundJoinPoint(joinPointOrTarget, <JoinPointAroundAdvice<T>>methodNameOrAdvice);
|
||||||
|
} else {
|
||||||
|
return aroundObject(joinPointOrTarget, <string | symbol>methodNameOrAdvice, objectAdvice!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply advice *before* the supplied joinPoint (function)
|
||||||
|
*
|
||||||
|
* @param joinPoint A function that should have advice applied to
|
||||||
|
* @param advice The before advice
|
||||||
|
*/
|
||||||
|
export function beforeJoinPoint<F extends GenericFunction<any>>(joinPoint: F, advice: JoinPointBeforeAdvice): F {
|
||||||
|
return adviseJoinPoint(joinPoint, 'before', advice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches "before" advice to be executed before the original method.
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the same arguments as the original, and may return new arguments
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
export function beforeObject(
|
||||||
|
target: Targetable,
|
||||||
|
methodName: string | symbol,
|
||||||
|
advice: (...originalArgs: any[]) => any[] | void
|
||||||
|
): Handle {
|
||||||
|
return adviseObject(getDispatcherObject(target, methodName), 'before', advice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches "before" advice to be executed before the original method.
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the same arguments as the original, and may return new arguments
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
export function before(
|
||||||
|
target: Targetable,
|
||||||
|
methodName: string | symbol,
|
||||||
|
advice: (...originalArgs: any[]) => any[] | void
|
||||||
|
): Handle;
|
||||||
|
/**
|
||||||
|
* Apply advice *before* the supplied joinPoint (function)
|
||||||
|
*
|
||||||
|
* @param joinPoint A function that should have advice applied to
|
||||||
|
* @param advice The before advice
|
||||||
|
*/
|
||||||
|
export function before<F extends GenericFunction<any>>(joinPoint: F, advice: JoinPointBeforeAdvice): F;
|
||||||
|
export function before<F extends GenericFunction<T>, T>(
|
||||||
|
joinPointOrTarget: F | Targetable,
|
||||||
|
methodNameOrAdvice: string | symbol | JoinPointBeforeAdvice,
|
||||||
|
objectAdvice?: ((...originalArgs: any[]) => any[] | void)
|
||||||
|
): Handle | F {
|
||||||
|
if (typeof joinPointOrTarget === 'function') {
|
||||||
|
return beforeJoinPoint(joinPointOrTarget, <JoinPointBeforeAdvice>methodNameOrAdvice);
|
||||||
|
} else {
|
||||||
|
return beforeObject(joinPointOrTarget, <string | symbol>methodNameOrAdvice, objectAdvice!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches advice to be executed after the original method.
|
||||||
|
* The advising function will receive the same arguments as the original method.
|
||||||
|
* The value it returns will be returned from the method when it is called *unless* its return value is undefined.
|
||||||
|
*
|
||||||
|
* @param target Object whose method will be aspected
|
||||||
|
* @param methodName Name of method to aspect
|
||||||
|
* @param advice Advising function which will receive the same arguments as the original method
|
||||||
|
* @return A handle which will remove the aspect when destroy is called
|
||||||
|
*/
|
||||||
|
export function on(target: Targetable, methodName: string | symbol, advice: (...originalArgs: any[]) => any): Handle {
|
||||||
|
return adviseObject(getDispatcherObject(target, methodName), 'after', advice, true);
|
||||||
|
}
|
||||||
249
src/lib/core/src/async/ExtensiblePromise.ts
Normal file
249
src/lib/core/src/async/ExtensiblePromise.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { Thenable } from '@dojo/shim/interfaces';
|
||||||
|
import { isArrayLike, isIterable, Iterable } from '@dojo/shim/iterator';
|
||||||
|
import Promise, { Executor } from '@dojo/shim/Promise';
|
||||||
|
import '@dojo/shim/Symbol';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a list of values, and if any are ExtensiblePromise objects, insert the wrapped Promise in its place,
|
||||||
|
* otherwise use the original object. We use this to help use the native Promise methods like `all` and `race`.
|
||||||
|
*
|
||||||
|
* @param iterable The list of objects to iterate over
|
||||||
|
* @returns {any[]} The list of objects, as an array, with ExtensiblePromises being replaced by Promises.
|
||||||
|
*/
|
||||||
|
export function unwrapPromises(iterable: Iterable<any> | any[]): any[] {
|
||||||
|
const unwrapped: any[] = [];
|
||||||
|
|
||||||
|
if (isArrayLike(iterable)) {
|
||||||
|
for (let i = 0; i < iterable.length; i++) {
|
||||||
|
const item = iterable[i];
|
||||||
|
unwrapped.push(item instanceof ExtensiblePromise ? item._promise : item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const item of iterable) {
|
||||||
|
unwrapped.push(item instanceof ExtensiblePromise ? item._promise : item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unwrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DictionaryOfPromises<T> = { [_: string]: T | Promise<T> | Thenable<T> };
|
||||||
|
export type ListOfPromises<T> = Iterable<T | Thenable<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extensible base to allow Promises to be extended in ES5. This class basically wraps a native Promise object,
|
||||||
|
* giving an API like a native promise.
|
||||||
|
*/
|
||||||
|
export class ExtensiblePromise<T> {
|
||||||
|
/**
|
||||||
|
* Return a rejected promise wrapped in an ExtensiblePromise
|
||||||
|
*
|
||||||
|
* @param reason The reason for the rejection
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static reject<T>(reason?: any): ExtensiblePromise<never>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a rejected promise wrapped in an ExtensiblePromise
|
||||||
|
*
|
||||||
|
* @param reason The reason for the rejection
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static reject<T>(reason?: any): ExtensiblePromise<T> {
|
||||||
|
return new this((resolve, reject) => reject(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a resolved promise wrapped in an ExtensiblePromise
|
||||||
|
*
|
||||||
|
* @param value The value to resolve the promise with
|
||||||
|
*
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static resolve<P extends ExtensiblePromise<void>>(): P;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a resolved promise wrapped in an ExtensiblePromise
|
||||||
|
*
|
||||||
|
* @param value The value to resolve the promise with
|
||||||
|
*
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static resolve<T, P extends ExtensiblePromise<T>>(value: T | PromiseLike<T>): P;
|
||||||
|
static resolve(value?: any | PromiseLike<any>): ExtensiblePromise<any> {
|
||||||
|
return new this((resolve, reject) => resolve(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: DictionaryOfPromises<T>): ExtensiblePromise<{ [key: string]: T }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: (T | Thenable<T>)[]): ExtensiblePromise<T[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: T | Thenable<T>): ExtensiblePromise<T[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: ListOfPromises<T>): ExtensiblePromise<T[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(
|
||||||
|
iterable: DictionaryOfPromises<T> | ListOfPromises<T>
|
||||||
|
): ExtensiblePromise<T[] | { [key: string]: T }> {
|
||||||
|
if (!isArrayLike(iterable) && !isIterable(iterable)) {
|
||||||
|
const promiseKeys = Object.keys(iterable);
|
||||||
|
|
||||||
|
return new this((resolve, reject) => {
|
||||||
|
Promise.all(promiseKeys.map((key) => iterable[key])).then((promiseResults: T[]) => {
|
||||||
|
const returnValue: { [key: string]: T } = {};
|
||||||
|
|
||||||
|
promiseResults.forEach((value: T, index: number) => {
|
||||||
|
returnValue[promiseKeys[index]] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(returnValue);
|
||||||
|
}, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new this((resolve, reject) => {
|
||||||
|
Promise.all(unwrapPromises(<Iterable<T>>iterable)).then(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when one of the passed in objects have resolved
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns {ExtensiblePromise}
|
||||||
|
*/
|
||||||
|
static race<T>(iterable: Iterable<T | PromiseLike<T>> | (T | PromiseLike<T>)[]): ExtensiblePromise<T> {
|
||||||
|
return new this((resolve, reject) => {
|
||||||
|
Promise.race(unwrapPromises(iterable)).then(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Promise}
|
||||||
|
* The wrapped promise
|
||||||
|
*/
|
||||||
|
readonly _promise: Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new extended Promise.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* @param executor
|
||||||
|
* The executor function is called immediately when the Promise is instantiated. It is responsible for
|
||||||
|
* starting the asynchronous operation when it is invoked.
|
||||||
|
*
|
||||||
|
* The executor must call either the passed `resolve` function when the asynchronous operation has completed
|
||||||
|
* successfully, or the `reject` function when the operation fails.
|
||||||
|
*/
|
||||||
|
constructor(executor: Executor<T>) {
|
||||||
|
this._promise = new Promise<T>(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a callback to be invoked when the wrapped Promise is rejected.
|
||||||
|
*
|
||||||
|
* @param {Function} onRejected A function to call to handle the error. The parameter to the function will be the caught error.
|
||||||
|
*
|
||||||
|
* @returns {ExtensiblePromise}
|
||||||
|
*/
|
||||||
|
catch<TResult = never>(
|
||||||
|
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
|
||||||
|
): ExtensiblePromise<T | TResult> {
|
||||||
|
return this.then(undefined, onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a callback to be invoked when the wrapped Promise resolves or is rejected.
|
||||||
|
*
|
||||||
|
* @param {Function} onFulfilled A function to call to handle the resolution. The paramter to the function will be the resolved value, if any.
|
||||||
|
* @param {Function} onRejected A function to call to handle the error. The parameter to the function will be the caught error.
|
||||||
|
*
|
||||||
|
* @returns {ExtensiblePromise}
|
||||||
|
*/
|
||||||
|
then<TResult1 = T, TResult2 = never>(
|
||||||
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||||
|
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2> | void) | undefined | null
|
||||||
|
): ExtensiblePromise<TResult1 | TResult2> {
|
||||||
|
const executor: Executor<TResult1> = (resolve, reject) => {
|
||||||
|
function handler(rejected: boolean, valueOrError: T | TResult1 | Error) {
|
||||||
|
const callback: ((value: any) => any) | null | undefined = rejected ? onRejected : onFulfilled;
|
||||||
|
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
try {
|
||||||
|
resolve(callback(valueOrError));
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
} else if (rejected) {
|
||||||
|
reject(valueOrError);
|
||||||
|
} else {
|
||||||
|
resolve(valueOrError as TResult1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._promise.then(handler.bind(null, false), handler.bind(null, true));
|
||||||
|
};
|
||||||
|
|
||||||
|
return new (this.constructor as typeof ExtensiblePromise)(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly [Symbol.toStringTag]: 'Promise';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtensiblePromise;
|
||||||
384
src/lib/core/src/async/Task.ts
Normal file
384
src/lib/core/src/async/Task.ts
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
import { Thenable } from '@dojo/shim/interfaces';
|
||||||
|
import { isArrayLike, isIterable, Iterable } from '@dojo/shim/iterator';
|
||||||
|
import { Executor } from '@dojo/shim/Promise';
|
||||||
|
import ExtensiblePromise, { DictionaryOfPromises, ListOfPromises, unwrapPromises } from './ExtensiblePromise';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe the internal state of a task.
|
||||||
|
*/
|
||||||
|
export const enum State {
|
||||||
|
Fulfilled = 0,
|
||||||
|
Pending = 1,
|
||||||
|
Rejected = 2,
|
||||||
|
Canceled = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type guard that determines if `value` is a `Task`
|
||||||
|
* @param value The value to guard
|
||||||
|
*/
|
||||||
|
export function isTask<T>(value: any): value is Task<T> {
|
||||||
|
return Boolean(value && typeof value.cancel === 'function' && Array.isArray(value.children) && isThenable(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a given value has a `then` method.
|
||||||
|
* @param {any} value The value to check if is Thenable
|
||||||
|
* @returns {is Thenable<T>} A type guard if the value is thenable
|
||||||
|
*/
|
||||||
|
export function isThenable<T>(value: any): value is Thenable<T> {
|
||||||
|
return value && typeof value.then === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task is an extension of Promise that supports cancellation and the Task#finally method.
|
||||||
|
*/
|
||||||
|
export class Task<T> extends ExtensiblePromise<T> {
|
||||||
|
/**
|
||||||
|
* Return a Task that resolves when one of the passed in objects have resolved
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns {Task}
|
||||||
|
*/
|
||||||
|
static race<T>(iterable: Iterable<T | Thenable<T>> | (T | Thenable<T>)[]): Task<T> {
|
||||||
|
return new this((resolve, reject) => {
|
||||||
|
Promise.race(unwrapPromises(iterable)).then(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a rejected promise wrapped in a Task
|
||||||
|
*
|
||||||
|
* @param reason The reason for the rejection
|
||||||
|
* @returns A task
|
||||||
|
*/
|
||||||
|
static reject<T>(reason?: Error): Task<T> {
|
||||||
|
return new this((resolve, reject) => reject(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a resolved task.
|
||||||
|
*
|
||||||
|
* @param value The value to resolve with
|
||||||
|
*
|
||||||
|
* @return A task
|
||||||
|
*/
|
||||||
|
public static resolve(): Task<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a resolved task.
|
||||||
|
*
|
||||||
|
* @param value The value to resolve with
|
||||||
|
*
|
||||||
|
* @return A task
|
||||||
|
*/
|
||||||
|
public static resolve<T>(value: T | Thenable<T>): Task<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a resolved task.
|
||||||
|
*
|
||||||
|
* @param value The value to resolve with
|
||||||
|
*
|
||||||
|
* @return A task
|
||||||
|
*/
|
||||||
|
public static resolve<T>(value?: any): Task<T> {
|
||||||
|
return new this<T>((resolve, reject) => resolve(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: DictionaryOfPromises<T>): Task<{ [key: string]: T }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: (T | Thenable<T>)[]): Task<T[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: T | Thenable<T>): Task<T[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: ListOfPromises<T>): Task<T[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value
|
||||||
|
* pair, the returned promise's argument is a key/value pair of the original keys with their resolved values.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results));
|
||||||
|
* // { one: 1, two: 2 }
|
||||||
|
*
|
||||||
|
* @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects
|
||||||
|
* @returns An extensible promise
|
||||||
|
*/
|
||||||
|
static all<T>(iterable: DictionaryOfPromises<T> | ListOfPromises<T>): Task<any> {
|
||||||
|
return new Task(
|
||||||
|
(resolve, reject) => {
|
||||||
|
super.all(iterable).then(resolve, reject);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (isArrayLike(iterable)) {
|
||||||
|
for (let i = 0; i < iterable.length; i++) {
|
||||||
|
const promiseLike = iterable[i];
|
||||||
|
|
||||||
|
if (isTask(promiseLike)) {
|
||||||
|
promiseLike.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isIterable(iterable)) {
|
||||||
|
for (const promiseLike of iterable) {
|
||||||
|
if (isTask(promiseLike)) {
|
||||||
|
promiseLike.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.keys(iterable).forEach((key: any) => {
|
||||||
|
const promiseLike = iterable[key];
|
||||||
|
|
||||||
|
if (isTask(promiseLike)) {
|
||||||
|
promiseLike.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cancelation handler that will be called if this task is canceled.
|
||||||
|
*/
|
||||||
|
private canceler: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Children of this Task (i.e., Tasks that were created from this Task with `then` or `catch`).
|
||||||
|
*/
|
||||||
|
private readonly children: Task<any>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The finally callback for this Task (if it was created by a call to `finally`).
|
||||||
|
*/
|
||||||
|
private _finally: undefined | (() => void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the task
|
||||||
|
*/
|
||||||
|
protected _state: State;
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* Create a new task. Executor is run immediately. The canceler will be called when the task is canceled.
|
||||||
|
*
|
||||||
|
* @param executor Method that initiates some task
|
||||||
|
* @param canceler Method to call when the task is canceled
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor(executor: Executor<T>, canceler?: () => void) {
|
||||||
|
// we have to initialize these to avoid a compiler error of using them before they are initialized
|
||||||
|
let superResolve: (value?: T | Thenable<T> | undefined) => void = () => {};
|
||||||
|
let superReject: (reason?: any) => void = () => {};
|
||||||
|
|
||||||
|
super((resolve, reject) => {
|
||||||
|
superResolve = resolve;
|
||||||
|
superReject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._state = State.Pending;
|
||||||
|
|
||||||
|
this.children = [];
|
||||||
|
this.canceler = () => {
|
||||||
|
if (canceler) {
|
||||||
|
canceler();
|
||||||
|
}
|
||||||
|
this._cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't let the Task resolve if it's been canceled
|
||||||
|
try {
|
||||||
|
executor(
|
||||||
|
(value) => {
|
||||||
|
if (this._state === State.Canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._state = State.Fulfilled;
|
||||||
|
superResolve(value);
|
||||||
|
},
|
||||||
|
(reason) => {
|
||||||
|
if (this._state === State.Canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._state = State.Rejected;
|
||||||
|
superReject(reason);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (reason) {
|
||||||
|
this._state = State.Rejected;
|
||||||
|
superReject(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagates cancellation down through a Task tree. The Task's state is immediately set to canceled. If a Thenable
|
||||||
|
* finally task was passed in, it is resolved before calling this Task's finally callback; otherwise, this Task's
|
||||||
|
* finally callback is immediately executed. `_cancel` is called for each child Task, passing in the value returned
|
||||||
|
* by this Task's finally callback or a Promise chain that will eventually resolve to that value.
|
||||||
|
*/
|
||||||
|
private _cancel(finallyTask?: void | Thenable<any>): void {
|
||||||
|
this._state = State.Canceled;
|
||||||
|
|
||||||
|
const runFinally = () => {
|
||||||
|
try {
|
||||||
|
return this._finally && this._finally();
|
||||||
|
} catch (error) {
|
||||||
|
// Any errors in a `finally` callback are completely ignored during cancelation
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._finally) {
|
||||||
|
if (isThenable(finallyTask)) {
|
||||||
|
finallyTask = (<Thenable<any>>finallyTask).then(runFinally, runFinally);
|
||||||
|
} else {
|
||||||
|
finallyTask = runFinally();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.children.forEach(function(child) {
|
||||||
|
child._cancel(finallyTask);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately cancels this task if it has not already resolved. This Task and any descendants are synchronously set
|
||||||
|
* to the Canceled state and any `finally` added downstream from the canceled Task are invoked.
|
||||||
|
*/
|
||||||
|
cancel(): void {
|
||||||
|
if (this._state === State.Pending) {
|
||||||
|
this.canceler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catch<TResult = never>(
|
||||||
|
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined
|
||||||
|
): Task<T | TResult> {
|
||||||
|
return this.then(undefined, onRejected) as Task<T | TResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows for cleanup actions to be performed after resolution of a Promise.
|
||||||
|
*/
|
||||||
|
finally(callback: () => void): Task<T> {
|
||||||
|
// if this task is already canceled, call the task
|
||||||
|
if (this._state === State.Canceled) {
|
||||||
|
callback();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = this.then<any>(
|
||||||
|
(value) => Task.resolve(callback()).then(() => value),
|
||||||
|
(reason) =>
|
||||||
|
Task.resolve(callback()).then(() => {
|
||||||
|
throw reason;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep a reference to the callback; it will be called if the Task is canceled
|
||||||
|
task._finally = callback;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a callback to be invoked when the Task resolves or is rejected.
|
||||||
|
*
|
||||||
|
* @param onFulfilled A function to call to handle the resolution. The paramter to the function will be the resolved value, if any.
|
||||||
|
* @param onRejected A function to call to handle the error. The parameter to the function will be the caught error.
|
||||||
|
*
|
||||||
|
* @returns A task
|
||||||
|
*/
|
||||||
|
then<TResult1 = T, TResult2 = never>(
|
||||||
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||||
|
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
||||||
|
): Task<TResult1 | TResult2> {
|
||||||
|
// FIXME
|
||||||
|
// tslint:disable-next-line:no-var-keyword
|
||||||
|
var task = super.then(
|
||||||
|
// Don't call the onFulfilled or onRejected handlers if this Task is canceled
|
||||||
|
function(value) {
|
||||||
|
if (task._state === State.Canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onFulfilled) {
|
||||||
|
return onFulfilled(value);
|
||||||
|
}
|
||||||
|
return <any>value;
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
if (task._state === State.Canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onRejected) {
|
||||||
|
return onRejected(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
) as Task<TResult1 | TResult2>;
|
||||||
|
|
||||||
|
task.canceler = () => {
|
||||||
|
// If task's parent (this) hasn't been resolved, cancel it; downward propagation will start at the first
|
||||||
|
// unresolved parent
|
||||||
|
if (this._state === State.Pending) {
|
||||||
|
this.cancel();
|
||||||
|
} else {
|
||||||
|
// If task's parent has been resolved, propagate cancelation to the task's descendants
|
||||||
|
task._cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep track of child Tasks for propogating cancelation back down the chain
|
||||||
|
this.children.push(task);
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Task;
|
||||||
295
src/lib/core/src/async/iteration.ts
Normal file
295
src/lib/core/src/async/iteration.ts
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import * as array from '@dojo/shim/array';
|
||||||
|
import { isArrayLike, Iterable } from '@dojo/shim/iterator';
|
||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
import { Thenable } from '@dojo/shim/interfaces';
|
||||||
|
|
||||||
|
function isThenable<T>(value: any): value is Thenable<T> {
|
||||||
|
return value && typeof value.then === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValuesAndResults<T, U> = { values: T[] | undefined; results: U[] | undefined };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes all items and then applies the callback to each item and eventually returns an object containing the
|
||||||
|
* processed values and callback results
|
||||||
|
* @param items a list of synchronous/asynchronous values to process
|
||||||
|
* @param callback a callback that maps values to synchronous/asynchronous results
|
||||||
|
* @return a list of objects holding the synchronous values and synchronous results.
|
||||||
|
*/
|
||||||
|
function processValuesAndCallback<T, U>(
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Thenable<T>)[],
|
||||||
|
callback: Mapper<T, U>
|
||||||
|
): Promise<ValuesAndResults<T, U>> {
|
||||||
|
return Promise.all(items).then(function(results) {
|
||||||
|
const pass: (U | Promise<U>)[] = Array.prototype.map.call(results, callback);
|
||||||
|
return Promise.all(pass).then(function(pass) {
|
||||||
|
return { values: results, results: pass };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the index of the next value in a sparse array-like object
|
||||||
|
* @param list the sparse array-like object
|
||||||
|
* @param offset the starting offset
|
||||||
|
* @return the offset of the next index with a value; or -1 if not found
|
||||||
|
*/
|
||||||
|
function findNextValueIndex<T>(list: ArrayLike<T>, offset: number = -1): number {
|
||||||
|
offset++;
|
||||||
|
for (let length = list.length; offset < length; offset++) {
|
||||||
|
if (offset in list) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLastValueIndex(list: ArrayLike<any>, offset?: number): number {
|
||||||
|
offset = (offset === undefined ? list.length : offset) - 1;
|
||||||
|
for (; offset >= 0; offset--) {
|
||||||
|
if (offset in list) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generalReduce<T, U>(
|
||||||
|
findNextIndex: (list: ArrayLike<any>, offset?: number) => number,
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Reducer<T, U>,
|
||||||
|
initialValue?: U
|
||||||
|
): Promise<U> {
|
||||||
|
const hasInitialValue = arguments.length > 3;
|
||||||
|
return Promise.all(items).then(function(results) {
|
||||||
|
return new Promise<U>(function(resolve, reject) {
|
||||||
|
// As iterators do not have indices like `ArrayLike` objects, the results array
|
||||||
|
// is used to determine the next value.
|
||||||
|
const list = isArrayLike(items) ? items : results;
|
||||||
|
let i: number;
|
||||||
|
function next(currentValue: U | undefined): void {
|
||||||
|
i = findNextIndex(list, i);
|
||||||
|
if (i >= 0) {
|
||||||
|
if (results) {
|
||||||
|
if (currentValue) {
|
||||||
|
const result = callback(currentValue, results[i], i, results);
|
||||||
|
|
||||||
|
if (isThenable(result)) {
|
||||||
|
result.then(next, reject);
|
||||||
|
} else {
|
||||||
|
next(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(currentValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: U | undefined;
|
||||||
|
if (hasInitialValue) {
|
||||||
|
value = initialValue;
|
||||||
|
} else {
|
||||||
|
i = findNextIndex(list);
|
||||||
|
|
||||||
|
if (i < 0) {
|
||||||
|
throw new Error('reduce array with no initial value');
|
||||||
|
}
|
||||||
|
if (results) {
|
||||||
|
value = <any>results[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAndHaltOnCondition<T>(
|
||||||
|
condition: boolean,
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Filterer<T>
|
||||||
|
): Promise<boolean> {
|
||||||
|
return Promise.all(items).then(function(results) {
|
||||||
|
return new Promise<boolean>(function(resolve) {
|
||||||
|
let result: boolean | Thenable<boolean>;
|
||||||
|
let pendingCount = 0;
|
||||||
|
if (results) {
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
result = callback(results[i], i, results);
|
||||||
|
if (result === condition) {
|
||||||
|
return resolve(result);
|
||||||
|
} else if (isThenable(result)) {
|
||||||
|
pendingCount++;
|
||||||
|
result.then(function(result) {
|
||||||
|
if (result === condition) {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
pendingCount--;
|
||||||
|
if (pendingCount === 0) {
|
||||||
|
resolve(!condition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pendingCount === 0) {
|
||||||
|
resolve(!condition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether all elements in the array pass the provided callback
|
||||||
|
* @param items a collection of synchronous/asynchronous values
|
||||||
|
* @param callback a synchronous/asynchronous test
|
||||||
|
* @return eventually returns true if all values pass; otherwise false
|
||||||
|
*/
|
||||||
|
export function every<T>(
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Filterer<T>
|
||||||
|
): Promise<boolean> {
|
||||||
|
return testAndHaltOnCondition(false, items, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of elements which pass the provided callback
|
||||||
|
* @param items a collection of synchronous/asynchronous values
|
||||||
|
* @param callback a synchronous/asynchronous test
|
||||||
|
* @return eventually returns a new array with only values that have passed
|
||||||
|
*/
|
||||||
|
export function filter<T>(items: Iterable<T | Promise<T>> | (T | Promise<T>)[], callback: Filterer<T>): Promise<T[]> {
|
||||||
|
return processValuesAndCallback(items, callback).then(function(result) {
|
||||||
|
let arr: T[] = [];
|
||||||
|
if (result && result.results && result.values) {
|
||||||
|
for (let i = 0; i < result.results.length; i++) {
|
||||||
|
result.results[i] && arr.push(result.values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first value matching a filter function
|
||||||
|
* @param items a collection of synchronous/asynchronous values
|
||||||
|
* @param callback a synchronous/asynchronous test
|
||||||
|
* @return a promise eventually containing the item or undefined if a match is not found
|
||||||
|
*/
|
||||||
|
export function find<T>(
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Filterer<T>
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
const list = isArrayLike(items) ? items : array.from(items);
|
||||||
|
return findIndex(list, callback).then<T | undefined>(function(i) {
|
||||||
|
return i !== undefined && i >= 0 ? list[i] : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first index with a value matching the filter function
|
||||||
|
* @param items a collection of synchronous/asynchronous values
|
||||||
|
* @param callback a synchronous/asynchronous test
|
||||||
|
* @return a promise eventually containing the index of the matching item or -1 if a match is not found
|
||||||
|
*/
|
||||||
|
export function findIndex<T>(
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Thenable<T>)[],
|
||||||
|
callback: Filterer<T>
|
||||||
|
): Promise<number> {
|
||||||
|
// TODO we can improve this by returning immediately
|
||||||
|
return processValuesAndCallback(items, callback).then(function(result: ValuesAndResults<T, boolean>) {
|
||||||
|
if (result && result.results) {
|
||||||
|
for (let i = 0; i < result.results.length; i++) {
|
||||||
|
if (result.results[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform a list of items using a mapper function
|
||||||
|
* @param items a collection of synchronous/asynchronous values
|
||||||
|
* @param callback a synchronous/asynchronous transform function
|
||||||
|
* @return a promise eventually containing a collection of each transformed value
|
||||||
|
*/
|
||||||
|
export function map<T, U>(
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Mapper<T, U>
|
||||||
|
): Promise<U[] | null | undefined> {
|
||||||
|
return processValuesAndCallback(items, callback).then(function(result) {
|
||||||
|
return result ? result.results : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reduce a list of items down to a single value
|
||||||
|
* @param items a collection of synchronous/asynchronous values
|
||||||
|
* @param callback a synchronous/asynchronous reducer function
|
||||||
|
* @param [initialValue] the first value to pass to the callback
|
||||||
|
* @return a promise eventually containing a value that is the result of the reduction
|
||||||
|
*/
|
||||||
|
export function reduce<T, U>(
|
||||||
|
this: any,
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Reducer<T, U>,
|
||||||
|
initialValue?: U
|
||||||
|
): Promise<U> {
|
||||||
|
const args: any[] = <any[]>array.from(arguments);
|
||||||
|
args.unshift(findNextValueIndex);
|
||||||
|
return generalReduce.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reduceRight<T, U>(
|
||||||
|
this: any,
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
callback: Reducer<T, U>,
|
||||||
|
initialValue?: U
|
||||||
|
): Promise<U> {
|
||||||
|
const args: any[] = <any[]>array.from(arguments);
|
||||||
|
args.unshift(findLastValueIndex);
|
||||||
|
return generalReduce.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function series<T, U>(
|
||||||
|
items: Iterable<T | Promise<T>> | (T | Promise<T>)[],
|
||||||
|
operation: Mapper<T, U>
|
||||||
|
): Promise<U[]> {
|
||||||
|
return generalReduce(
|
||||||
|
findNextValueIndex,
|
||||||
|
items,
|
||||||
|
function(previousValue, currentValue: T, index: number, array: T[]) {
|
||||||
|
const result = operation(currentValue, index, array);
|
||||||
|
|
||||||
|
if (isThenable(result)) {
|
||||||
|
return result.then(function(value) {
|
||||||
|
previousValue.push(value);
|
||||||
|
return previousValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
previousValue.push(result);
|
||||||
|
return previousValue;
|
||||||
|
},
|
||||||
|
[] as U[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function some<T>(
|
||||||
|
items: Iterable<T | Promise<T>> | Array<T | Promise<T>>,
|
||||||
|
callback: Filterer<T>
|
||||||
|
): Promise<boolean> {
|
||||||
|
return testAndHaltOnCondition(true, items, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filterer<T> extends Mapper<T, boolean> {}
|
||||||
|
|
||||||
|
export interface Mapper<T, U> {
|
||||||
|
(value: T, index: number, array: T[]): U | Thenable<U>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Reducer<T, U> {
|
||||||
|
(previousValue: U, currentValue: T, index: number, array: T[]): U | Thenable<U>;
|
||||||
|
}
|
||||||
63
src/lib/core/src/async/timing.ts
Normal file
63
src/lib/core/src/async/timing.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import Promise from './ExtensiblePromise';
|
||||||
|
import { Thenable } from '@dojo/shim/interfaces';
|
||||||
|
|
||||||
|
export type IdentityValue<T> = T | (() => T | Thenable<T>);
|
||||||
|
export interface Identity<T> {
|
||||||
|
(value?: IdentityValue<T>): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for delaying a Promise chain for a specific number of milliseconds.
|
||||||
|
*
|
||||||
|
* @param milliseconds the number of milliseconds to delay
|
||||||
|
* @return {function (value: T | (() => T | Thenable<T>)): Promise<T>} a function producing a promise that eventually returns the value or executes the value function passed to it; usable with Thenable.then()
|
||||||
|
*/
|
||||||
|
export function delay<T>(milliseconds: number): Identity<T> {
|
||||||
|
return function(value?: IdentityValue<T>): Promise<T> {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
setTimeout(function() {
|
||||||
|
resolve(typeof value === 'function' ? value() : value);
|
||||||
|
}, milliseconds);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a promise chain if a result hasn't been found before the timeout
|
||||||
|
*
|
||||||
|
* @param milliseconds after this number of milliseconds a rejection will be returned
|
||||||
|
* @param reason The reason for the rejection
|
||||||
|
* @return {function(T): Promise<T>} a function that produces a promise that is rejected or resolved based on your timeout
|
||||||
|
*/
|
||||||
|
export function timeout<T>(milliseconds: number, reason: Error): Identity<T> {
|
||||||
|
const start = Date.now();
|
||||||
|
return function(value?: IdentityValue<T>): Promise<T> {
|
||||||
|
if (Date.now() - milliseconds > start) {
|
||||||
|
return Promise.reject<T>(reason);
|
||||||
|
}
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
return Promise.resolve(value());
|
||||||
|
}
|
||||||
|
return Promise.resolve(value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Promise that will reject itself automatically after a time.
|
||||||
|
* Useful for combining with other promises in Promise.race.
|
||||||
|
*/
|
||||||
|
export class DelayedRejection extends Promise<any> {
|
||||||
|
/**
|
||||||
|
* @param milliseconds the number of milliseconds to wait before triggering a rejection
|
||||||
|
* @param reason the reason for the rejection
|
||||||
|
*/
|
||||||
|
constructor(milliseconds: number, reason?: Error) {
|
||||||
|
super(() => {});
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(reason);
|
||||||
|
}, milliseconds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/lib/core/src/base64.ts
Normal file
42
src/lib/core/src/base64.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import global from '@dojo/shim/global';
|
||||||
|
import has, { add as hasAdd } from '@dojo/has/has';
|
||||||
|
|
||||||
|
hasAdd('btoa', 'btoa' in global, true);
|
||||||
|
hasAdd('atob', 'atob' in global, true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a string encoded in base64 and decode it
|
||||||
|
* @param encodedString The base64 encoded string
|
||||||
|
*/
|
||||||
|
export const decode: (encodedString: string) => string = has('atob')
|
||||||
|
? function(encodedString: string) {
|
||||||
|
/* this allows for utf8 characters to be decoded properly */
|
||||||
|
return decodeURIComponent(
|
||||||
|
Array.prototype.map
|
||||||
|
.call(
|
||||||
|
atob(encodedString),
|
||||||
|
(char: string) => '%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: function(encodedString: string): string {
|
||||||
|
return new Buffer(encodedString.toString(), 'base64').toString('utf8');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a string and encode it to base64
|
||||||
|
* @param rawString The string to encode
|
||||||
|
*/
|
||||||
|
export const encode: (rawString: string) => string = has('btoa')
|
||||||
|
? function(decodedString: string) {
|
||||||
|
/* this allows for utf8 characters to be encoded properly */
|
||||||
|
return btoa(
|
||||||
|
encodeURIComponent(decodedString).replace(/%([0-9A-F]{2})/g, (match, code: string) =>
|
||||||
|
String.fromCharCode(Number('0x' + code))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: function(rawString: string): string {
|
||||||
|
return new Buffer(rawString.toString(), 'utf8').toString('base64');
|
||||||
|
};
|
||||||
754
src/lib/core/src/compare.ts
Normal file
754
src/lib/core/src/compare.ts
Normal file
@ -0,0 +1,754 @@
|
|||||||
|
import { assign } from '@dojo/shim/object';
|
||||||
|
import { keys } from '@dojo/shim/object';
|
||||||
|
import Set from '@dojo/shim/Set';
|
||||||
|
|
||||||
|
/* Assigning to local variables to improve minification and readability */
|
||||||
|
|
||||||
|
const objectCreate = Object.create;
|
||||||
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
const defineProperty = Object.defineProperty;
|
||||||
|
const isArray = Array.isArray;
|
||||||
|
const isFrozen = Object.isFrozen;
|
||||||
|
const isSealed = Object.isSealed;
|
||||||
|
|
||||||
|
export type IgnorePropertyFunction = (name: string, a: any, b: any) => boolean;
|
||||||
|
|
||||||
|
export interface DiffOptions {
|
||||||
|
/**
|
||||||
|
* Allow functions to be values. Values will be considered equal if the `typeof` both values are `function`.
|
||||||
|
* When adding or updating the property, the value of the property of `a` will be used in the record, which
|
||||||
|
* will be a reference to the function.
|
||||||
|
*/
|
||||||
|
allowFunctionValues?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of strings or regular expressions which flag certain properties to be ignored. Alternatively
|
||||||
|
* a function, which returns `true` to have the property ignored or `false` to diff the property.
|
||||||
|
*/
|
||||||
|
ignoreProperties?: (string | RegExp)[] | IgnorePropertyFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of strings or regular expressions which flag certain values to be ignored. For flagged properties,
|
||||||
|
* if the property is present in both `a` and `b` the value will be ignored. If adding the property,
|
||||||
|
* whatever the value of the property of `a` will be used, which could be a reference.
|
||||||
|
*/
|
||||||
|
ignorePropertyValues?: (string | RegExp)[] | IgnorePropertyFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a generic constructor function
|
||||||
|
*/
|
||||||
|
export interface Constructor {
|
||||||
|
new (...args: any[]): object;
|
||||||
|
prototype: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A partial property descriptor that provides the property descriptor flags supported by the
|
||||||
|
* complex property construction of `patch()`
|
||||||
|
*
|
||||||
|
* All properties are value properties, with the value being supplied by the `ConstructRecord`
|
||||||
|
*/
|
||||||
|
export interface ConstructDescriptor {
|
||||||
|
/**
|
||||||
|
* Is the property configurable?
|
||||||
|
*/
|
||||||
|
configurable?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the property enumerable?
|
||||||
|
*/
|
||||||
|
enumerable?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the property configurable?
|
||||||
|
*/
|
||||||
|
writable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record that describes a constructor function and arguments necessary to create an instance of
|
||||||
|
* an object
|
||||||
|
*/
|
||||||
|
export interface AnonymousConstructRecord {
|
||||||
|
/**
|
||||||
|
* Any arguments to pass to the constructor function
|
||||||
|
*/
|
||||||
|
args?: any[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The constructor function to use to create the instance
|
||||||
|
*/
|
||||||
|
Ctor: Constructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The partial descriptor that is used to set the value of the instance
|
||||||
|
*/
|
||||||
|
descriptor?: ConstructDescriptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any patches to properties that need to occur on the instance
|
||||||
|
*/
|
||||||
|
propertyRecords?: (ConstructRecord | PatchRecord)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConstructRecord extends AnonymousConstructRecord {
|
||||||
|
/**
|
||||||
|
* The name of the property on the Object
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record that describes the mutations necessary to a property of an object to make that property look
|
||||||
|
* like another
|
||||||
|
*/
|
||||||
|
export type PatchRecord =
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* The name of the property on the Object
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the patch
|
||||||
|
*/
|
||||||
|
type: 'delete';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* A property descriptor that describes the property in `name`
|
||||||
|
*/
|
||||||
|
descriptor: PropertyDescriptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the property on the Object
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the patch
|
||||||
|
*/
|
||||||
|
type: 'add' | 'update';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional patch records which describe the value of the property
|
||||||
|
*/
|
||||||
|
valueRecords?: (ConstructRecord | PatchRecord | SpliceRecord)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different types of patch records supported
|
||||||
|
*/
|
||||||
|
export type PatchTypes = 'add' | 'update' | 'delete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record that describes a splice operation to perform on an array to make the array look like another array
|
||||||
|
*/
|
||||||
|
export interface SpliceRecord {
|
||||||
|
/**
|
||||||
|
* Any items that are being added to the array
|
||||||
|
*/
|
||||||
|
add?: any[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of items in the array to delete
|
||||||
|
*/
|
||||||
|
deleteCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type, set to `splice`
|
||||||
|
*/
|
||||||
|
type: 'splice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of where to start the splice
|
||||||
|
*/
|
||||||
|
start: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record that describes how to instantiate a new object via a constructor function
|
||||||
|
* @param Ctor The constructor function
|
||||||
|
* @param args Any arguments to be passed to the constructor function
|
||||||
|
*/
|
||||||
|
/* tslint:disable:variable-name */
|
||||||
|
export function createConstructRecord(
|
||||||
|
Ctor: Constructor,
|
||||||
|
args?: any[],
|
||||||
|
descriptor?: ConstructDescriptor
|
||||||
|
): AnonymousConstructRecord {
|
||||||
|
const record: AnonymousConstructRecord = assign(objectCreate(null), { Ctor });
|
||||||
|
if (args) {
|
||||||
|
record.args = args;
|
||||||
|
}
|
||||||
|
if (descriptor) {
|
||||||
|
record.descriptor = descriptor;
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
/* tslint:enable:variable-name */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal function that returns a new patch record
|
||||||
|
*
|
||||||
|
* @param type The type of patch record
|
||||||
|
* @param name The property name the record refers to
|
||||||
|
* @param descriptor The property descriptor to be installed on the object
|
||||||
|
* @param valueRecords Any subsequenet patch recrds to be applied to the value of the descriptor
|
||||||
|
*/
|
||||||
|
function createPatchRecord(
|
||||||
|
type: PatchTypes,
|
||||||
|
name: string,
|
||||||
|
descriptor?: PropertyDescriptor,
|
||||||
|
valueRecords?: (ConstructRecord | PatchRecord | SpliceRecord)[]
|
||||||
|
): PatchRecord {
|
||||||
|
const patchRecord = assign(objectCreate(null), {
|
||||||
|
type,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (descriptor) {
|
||||||
|
patchRecord.descriptor = descriptor;
|
||||||
|
}
|
||||||
|
if (valueRecords) {
|
||||||
|
patchRecord.valueRecords = valueRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
return patchRecord as PatchRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal function that returns a new splice record
|
||||||
|
*
|
||||||
|
* @param start Where in the array to start the splice
|
||||||
|
* @param deleteCount The number of elements to delete from the array
|
||||||
|
* @param add Elements to be added to the target
|
||||||
|
*/
|
||||||
|
function createSpliceRecord(start: number, deleteCount: number, add?: any[]): SpliceRecord {
|
||||||
|
const spliceRecord: SpliceRecord = assign(objectCreate(null), {
|
||||||
|
type: 'splice',
|
||||||
|
start,
|
||||||
|
deleteCount
|
||||||
|
});
|
||||||
|
|
||||||
|
if (add && add.length) {
|
||||||
|
spliceRecord.add = add;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spliceRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that produces a value property descriptor, which assumes that properties are enumerable, writable and configurable
|
||||||
|
* unless specified
|
||||||
|
*
|
||||||
|
* @param value The value for the descriptor
|
||||||
|
* @param writable Defaults to `true` if not specified
|
||||||
|
* @param enumerable Defaults to `true` if not specified
|
||||||
|
* @param configurable Defaults to `true` if not specified
|
||||||
|
*/
|
||||||
|
function createValuePropertyDescriptor(
|
||||||
|
value: any,
|
||||||
|
writable: boolean = true,
|
||||||
|
enumerable: boolean = true,
|
||||||
|
configurable: boolean = true
|
||||||
|
): PropertyDescriptor {
|
||||||
|
return assign(objectCreate(null), {
|
||||||
|
value,
|
||||||
|
writable,
|
||||||
|
enumerable,
|
||||||
|
configurable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a constructor record or `undefined` when diffing a value
|
||||||
|
*/
|
||||||
|
export type CustomDiffFunction<T> = (
|
||||||
|
value: T,
|
||||||
|
nameOrIndex: string | number,
|
||||||
|
parent: object
|
||||||
|
) => AnonymousConstructRecord | void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class which is used when making a custom comparison of a non-plain object or array
|
||||||
|
*/
|
||||||
|
export class CustomDiff<T> {
|
||||||
|
private _differ: CustomDiffFunction<T>;
|
||||||
|
|
||||||
|
constructor(diff: CustomDiffFunction<T>) {
|
||||||
|
this._differ = diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the difference of the `value`
|
||||||
|
* @param value The value to diff
|
||||||
|
* @param nameOrIndex A `string` if comparing a property or a `number` if comparing an array element
|
||||||
|
* @param parent The outer parent that this value is part of
|
||||||
|
*/
|
||||||
|
diff(value: T, nameOrIndex: string | number, parent: object): ConstructRecord | void {
|
||||||
|
const record = this._differ(value, nameOrIndex, parent);
|
||||||
|
if (record && typeof nameOrIndex === 'string') {
|
||||||
|
return assign(record, { name: nameOrIndex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function that detects the differences between an array and another value and returns a set of splice records that
|
||||||
|
* describe the differences
|
||||||
|
*
|
||||||
|
* @param a The first array to compare to
|
||||||
|
* @param b The second value to compare to
|
||||||
|
* @param options An options bag that allows configuration of the behaviour of `diffArray()`
|
||||||
|
*/
|
||||||
|
function diffArray(a: any[], b: any, options: DiffOptions): SpliceRecord[] {
|
||||||
|
/* This function takes an overly simplistic approach to calculating splice records. There are many situations where
|
||||||
|
* in complicated array mutations, the splice records can be more optimised.
|
||||||
|
*
|
||||||
|
* TODO: Raise an issue for this when it is finally merged and put into core
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { allowFunctionValues = false } = options;
|
||||||
|
|
||||||
|
const arrayA = a;
|
||||||
|
const lengthA = arrayA.length;
|
||||||
|
const arrayB = isArray(b) ? b : [];
|
||||||
|
const lengthB = arrayB.length;
|
||||||
|
const patchRecords: SpliceRecord[] = [];
|
||||||
|
|
||||||
|
if (!lengthA && lengthB) {
|
||||||
|
/* empty array */
|
||||||
|
patchRecords.push(createSpliceRecord(0, lengthB));
|
||||||
|
return patchRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
let add: any[] = [];
|
||||||
|
let start = 0;
|
||||||
|
let deleteCount = 0;
|
||||||
|
let last = -1;
|
||||||
|
|
||||||
|
function flushSpliceRecord() {
|
||||||
|
if (deleteCount || add.length) {
|
||||||
|
patchRecords.push(
|
||||||
|
createSpliceRecord(start, start + deleteCount > lengthB ? lengthB - start : deleteCount, add)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDifference(index: number, adding: boolean, value?: any) {
|
||||||
|
if (index > last + 1) {
|
||||||
|
/* flush the splice */
|
||||||
|
flushSpliceRecord();
|
||||||
|
start = index;
|
||||||
|
deleteCount = 0;
|
||||||
|
if (add.length) {
|
||||||
|
add = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adding) {
|
||||||
|
add.push(value);
|
||||||
|
}
|
||||||
|
deleteCount++;
|
||||||
|
last = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayA.forEach((valueA, index) => {
|
||||||
|
const valueB = arrayB[index];
|
||||||
|
|
||||||
|
if (
|
||||||
|
index in arrayB &&
|
||||||
|
(valueA === valueB || (allowFunctionValues && typeof valueA === 'function' && typeof valueB === 'function'))
|
||||||
|
) {
|
||||||
|
return; /* not different */
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValueAArray = isArray(valueA);
|
||||||
|
const isValueAPlainObject = isPlainObject(valueA);
|
||||||
|
|
||||||
|
if (isValueAArray || isValueAPlainObject) {
|
||||||
|
const value = isValueAArray
|
||||||
|
? isArray(valueB) ? valueB : []
|
||||||
|
: isPlainObject(valueB) ? valueB : Object.create(null);
|
||||||
|
const valueRecords = diff(valueA, value, options);
|
||||||
|
if (valueRecords.length) {
|
||||||
|
/* only add if there are changes */
|
||||||
|
addDifference(index, true, diff(valueA, value, options));
|
||||||
|
}
|
||||||
|
} else if (isPrimitive(valueA)) {
|
||||||
|
addDifference(index, true, valueA);
|
||||||
|
} else if (allowFunctionValues && typeof valueA === 'function') {
|
||||||
|
addDifference(index, true, valueA);
|
||||||
|
} else {
|
||||||
|
throw new TypeError(
|
||||||
|
`Value of array element "${index}" from first argument is not a primative, plain Object, or Array.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lengthB > lengthA) {
|
||||||
|
for (let index = lengthA; index < lengthB; index++) {
|
||||||
|
addDifference(index, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* flush any deletes */
|
||||||
|
flushSpliceRecord();
|
||||||
|
|
||||||
|
return patchRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function that detects the differences between plain objects and returns a set of patch records that
|
||||||
|
* describe the differences
|
||||||
|
*
|
||||||
|
* @param a The first plain object to compare to
|
||||||
|
* @param b The second plain bject to compare to
|
||||||
|
* @param options An options bag that allows configuration of the behaviour of `diffPlainObject()`
|
||||||
|
*/
|
||||||
|
function diffPlainObject(a: any, b: any, options: DiffOptions): (ConstructRecord | PatchRecord)[] {
|
||||||
|
const { allowFunctionValues = false, ignorePropertyValues = [] } = options;
|
||||||
|
const patchRecords: (ConstructRecord | PatchRecord)[] = [];
|
||||||
|
const { comparableA, comparableB } = getComparableObjects(a, b, options);
|
||||||
|
|
||||||
|
/* look for keys in a that are different from b */
|
||||||
|
keys(comparableA).reduce((patchRecords, name) => {
|
||||||
|
const valueA = a[name];
|
||||||
|
const valueB = b[name];
|
||||||
|
const bHasOwnProperty = hasOwnProperty.call(comparableB, name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
bHasOwnProperty &&
|
||||||
|
(valueA === valueB || (allowFunctionValues && typeof valueA === 'function' && typeof valueB === 'function'))
|
||||||
|
) {
|
||||||
|
/* not different */
|
||||||
|
/* when `allowFunctionValues` is true, functions are simply considered to be equal by `typeof` */
|
||||||
|
return patchRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = bHasOwnProperty ? 'update' : 'add';
|
||||||
|
|
||||||
|
const isValueAArray = isArray(valueA);
|
||||||
|
const isValueAPlainObject = isPlainObject(valueA);
|
||||||
|
|
||||||
|
if (isValueAArray || isValueAPlainObject) {
|
||||||
|
/* non-primitive values we can diff */
|
||||||
|
/* this is a bit complicated, but essentially if valueA and valueB are both arrays or plain objects, then
|
||||||
|
* we can diff those two values, if not, then we need to use an empty array or an empty object and diff
|
||||||
|
* the valueA with that */
|
||||||
|
const value =
|
||||||
|
(isValueAArray && isArray(valueB)) || (isValueAPlainObject && isPlainObject(valueB))
|
||||||
|
? valueB
|
||||||
|
: isValueAArray ? [] : objectCreate(null);
|
||||||
|
const valueRecords = diff(valueA, value, options);
|
||||||
|
if (valueRecords.length) {
|
||||||
|
/* only add if there are changes */
|
||||||
|
patchRecords.push(
|
||||||
|
createPatchRecord(type, name, createValuePropertyDescriptor(value), diff(valueA, value, options))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (isCustomDiff(valueA) && !isCustomDiff(valueB)) {
|
||||||
|
/* complex diff left hand */
|
||||||
|
const result = valueA.diff(valueB, name, b);
|
||||||
|
if (result) {
|
||||||
|
patchRecords.push(result);
|
||||||
|
}
|
||||||
|
} else if (isCustomDiff(valueB)) {
|
||||||
|
/* complex diff right hand */
|
||||||
|
const result = valueB.diff(valueA, name, a);
|
||||||
|
if (result) {
|
||||||
|
patchRecords.push(result);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
isPrimitive(valueA) ||
|
||||||
|
(allowFunctionValues && typeof valueA === 'function') ||
|
||||||
|
isIgnoredPropertyValue(name, a, b, ignorePropertyValues)
|
||||||
|
) {
|
||||||
|
/* primitive values, functions values if allowed, or ignored property values can just be copied */
|
||||||
|
patchRecords.push(createPatchRecord(type, name, createValuePropertyDescriptor(valueA)));
|
||||||
|
} else {
|
||||||
|
throw new TypeError(
|
||||||
|
`Value of property named "${name}" from first argument is not a primative, plain Object, or Array.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return patchRecords;
|
||||||
|
}, patchRecords);
|
||||||
|
|
||||||
|
/* look for keys in b that are not in a */
|
||||||
|
keys(comparableB).reduce((patchRecords, name) => {
|
||||||
|
if (!hasOwnProperty.call(comparableA, name)) {
|
||||||
|
patchRecords.push(createPatchRecord('delete', name));
|
||||||
|
}
|
||||||
|
return patchRecords;
|
||||||
|
}, patchRecords);
|
||||||
|
|
||||||
|
return patchRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes two plain objects to be compared, as well as options customizing the behavior of the comparison, and returns
|
||||||
|
* two new objects that contain only those properties that should be compared. If a property is ignored
|
||||||
|
* it will not be included in either returned object. If a property's value should be ignored it will be excluded
|
||||||
|
* if it is present in both objects.
|
||||||
|
* @param a The first object to compare
|
||||||
|
* @param b The second object to compare
|
||||||
|
* @param options An options bag indicating which properties should be ignored or have their values ignored, if any.
|
||||||
|
*/
|
||||||
|
export function getComparableObjects(a: any, b: any, options: DiffOptions) {
|
||||||
|
const { ignoreProperties = [], ignorePropertyValues = [] } = options;
|
||||||
|
const ignore = new Set<string>();
|
||||||
|
const keep = new Set<string>();
|
||||||
|
|
||||||
|
const isIgnoredProperty = Array.isArray(ignoreProperties)
|
||||||
|
? (name: string) => {
|
||||||
|
return ignoreProperties.some(
|
||||||
|
(value) => (typeof value === 'string' ? name === value : value.test(name))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: (name: string) => ignoreProperties(name, a, b);
|
||||||
|
|
||||||
|
const comparableA = keys(a).reduce(
|
||||||
|
(obj, name) => {
|
||||||
|
if (
|
||||||
|
isIgnoredProperty(name) ||
|
||||||
|
(hasOwnProperty.call(b, name) && isIgnoredPropertyValue(name, a, b, ignorePropertyValues))
|
||||||
|
) {
|
||||||
|
ignore.add(name);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
keep.add(name);
|
||||||
|
obj[name] = a[name];
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: any }
|
||||||
|
);
|
||||||
|
|
||||||
|
const comparableB = keys(b).reduce(
|
||||||
|
(obj, name) => {
|
||||||
|
if (ignore.has(name) || (!keep.has(name) && isIgnoredProperty(name))) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[name] = b[name];
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: any }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { comparableA, comparableB, ignore };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is a `ConstructRecord`
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isConstructRecord(value: any): value is ConstructRecord {
|
||||||
|
return Boolean(value && typeof value === 'object' && value !== null && value.Ctor && value.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIgnoredPropertyValue(
|
||||||
|
name: string,
|
||||||
|
a: any,
|
||||||
|
b: any,
|
||||||
|
ignoredPropertyValues: (string | RegExp)[] | IgnorePropertyFunction
|
||||||
|
) {
|
||||||
|
return Array.isArray(ignoredPropertyValues)
|
||||||
|
? ignoredPropertyValues.some((value) => {
|
||||||
|
return typeof value === 'string' ? name === value : value.test(name);
|
||||||
|
})
|
||||||
|
: ignoredPropertyValues(name, a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is a `PatchRecord`
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isPatchRecord(value: any): value is PatchRecord {
|
||||||
|
return Boolean(value && value.type && value.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is an array of `PatchRecord`s
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isPatchRecordArray(value: any): value is PatchRecord[] {
|
||||||
|
return Boolean(isArray(value) && value.length && isPatchRecord(value[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is a plain object. A plain object is an object that has
|
||||||
|
* either no constructor (e.g. `Object.create(null)`) or has Object as its constructor.
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isPlainObject(value: any): value is Object {
|
||||||
|
return Boolean(
|
||||||
|
value && typeof value === 'object' && (value.constructor === Object || value.constructor === undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is a primitive (including `null`), as these values are
|
||||||
|
* fine to just copy.
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isPrimitive(value: any): value is string | number | boolean | undefined | null {
|
||||||
|
const typeofValue = typeof value;
|
||||||
|
return (
|
||||||
|
value === null ||
|
||||||
|
typeofValue === 'undefined' ||
|
||||||
|
typeofValue === 'string' ||
|
||||||
|
typeofValue === 'number' ||
|
||||||
|
typeofValue === 'boolean'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is a `CustomDiff`
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isCustomDiff<T>(value: any): value is CustomDiff<T> {
|
||||||
|
return typeof value === 'object' && value instanceof CustomDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is a `SpliceRecord`
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isSpliceRecord(value: any): value is SpliceRecord {
|
||||||
|
return value && value.type === 'splice' && 'start' in value && 'deleteCount' in value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that determines if the value is an array of `SpliceRecord`s
|
||||||
|
*
|
||||||
|
* @param value The value to check
|
||||||
|
*/
|
||||||
|
function isSpliceRecordArray(value: any): value is SpliceRecord[] {
|
||||||
|
return Boolean(isArray(value) && value.length && isSpliceRecord(value[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal function that patches a target with a `SpliceRecord`
|
||||||
|
*/
|
||||||
|
function patchSplice(target: any[], { add, deleteCount, start }: SpliceRecord): any {
|
||||||
|
if (add && add.length) {
|
||||||
|
const deletedItems = deleteCount ? target.slice(start, start + deleteCount) : [];
|
||||||
|
add = add.map((value, index) => resolveTargetValue(value, deletedItems[index]));
|
||||||
|
target.splice(start, deleteCount, ...add);
|
||||||
|
} else {
|
||||||
|
target.splice(start, deleteCount);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal function that patches a target with a `PatchRecord`
|
||||||
|
*/
|
||||||
|
function patchPatch(target: any, record: PatchRecord): any {
|
||||||
|
const { name } = record;
|
||||||
|
if (record.type === 'delete') {
|
||||||
|
delete target[name];
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
const { descriptor, valueRecords } = record;
|
||||||
|
if (valueRecords && valueRecords.length) {
|
||||||
|
descriptor.value = patch(descriptor.value, valueRecords);
|
||||||
|
}
|
||||||
|
defineProperty(target, name, descriptor);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConstructDescriptor = {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
function patchConstruct(target: any, record: ConstructRecord): any {
|
||||||
|
const { args, descriptor = defaultConstructDescriptor, Ctor, name, propertyRecords } = record;
|
||||||
|
const value = new Ctor(...(args || []));
|
||||||
|
if (propertyRecords) {
|
||||||
|
propertyRecords.forEach(
|
||||||
|
(record) => (isConstructRecord(record) ? patchConstruct(value, record) : patchPatch(value, record))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
defineProperty(target, name, assign({ value }, descriptor));
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal function that takes a value from array being patched and the target value from the same
|
||||||
|
* index and determines the value that should actually be patched into the target array
|
||||||
|
*/
|
||||||
|
function resolveTargetValue(patchValue: any, targetValue: any): any {
|
||||||
|
const patchIsSpliceRecordArray = isSpliceRecordArray(patchValue);
|
||||||
|
return patchIsSpliceRecordArray || isPatchRecordArray(patchValue)
|
||||||
|
? patch(
|
||||||
|
patchIsSpliceRecordArray
|
||||||
|
? isArray(targetValue) ? targetValue : []
|
||||||
|
: isPlainObject(targetValue) ? targetValue : objectCreate(null),
|
||||||
|
patchValue
|
||||||
|
)
|
||||||
|
: patchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two plain objects or arrays and return a set of records which describe the differences between the two
|
||||||
|
*
|
||||||
|
* The records describe what would need to be applied to the second argument to make it look like the first argument
|
||||||
|
*
|
||||||
|
* @param a The plain object or array to compare with
|
||||||
|
* @param b The plain object or array to compare to
|
||||||
|
* @param options An options bag that allows configuration of the behaviour of `diff()`
|
||||||
|
*/
|
||||||
|
export function diff(a: any, b: any, options: DiffOptions = {}): (ConstructRecord | PatchRecord | SpliceRecord)[] {
|
||||||
|
if (typeof a !== 'object' || typeof b !== 'object') {
|
||||||
|
throw new TypeError('Arguments are not of type object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(a)) {
|
||||||
|
return diffArray(a, b, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(b)) {
|
||||||
|
b = objectCreate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlainObject(a) || !isPlainObject(b)) {
|
||||||
|
throw new TypeError('Arguments are not plain Objects or Arrays.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffPlainObject(a, b, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a set of patch records to a target.
|
||||||
|
*
|
||||||
|
* @param target The plain object or array that the patch records should be applied to
|
||||||
|
* @param records A set of patch records to be applied to the target
|
||||||
|
*/
|
||||||
|
export function patch(target: any, records: (ConstructRecord | PatchRecord | SpliceRecord)[]): any {
|
||||||
|
if (!isArray(target) && !isPlainObject(target)) {
|
||||||
|
throw new TypeError('A target for a patch must be either an array or a plain object.');
|
||||||
|
}
|
||||||
|
if (isFrozen(target) || isSealed(target)) {
|
||||||
|
throw new TypeError('Cannot patch sealed or frozen objects.');
|
||||||
|
}
|
||||||
|
|
||||||
|
records.forEach((record) => {
|
||||||
|
target = isSpliceRecord(record)
|
||||||
|
? patchSplice(isArray(target) ? target : [], record) /* patch arrays */
|
||||||
|
: isConstructRecord(record)
|
||||||
|
? patchConstruct(target, record) /* patch complex object */
|
||||||
|
: patchPatch(isPlainObject(target) ? target : {}, record); /* patch plain object */
|
||||||
|
});
|
||||||
|
return target;
|
||||||
|
}
|
||||||
10
src/lib/core/src/global.ts
Normal file
10
src/lib/core/src/global.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import globalObject from '@dojo/shim/global';
|
||||||
|
import { deprecated } from './instrument';
|
||||||
|
|
||||||
|
deprecated({
|
||||||
|
message: 'has been replaced with @dojo/shim/global',
|
||||||
|
name: '@dojo/core/global',
|
||||||
|
url: 'https://github.com/dojo/core/issues/302'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default globalObject;
|
||||||
70
src/lib/core/src/has.ts
Normal file
70
src/lib/core/src/has.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import global from '@dojo/shim/global';
|
||||||
|
import has, { add } from '@dojo/shim/support/has';
|
||||||
|
|
||||||
|
export * from '@dojo/shim/support/has';
|
||||||
|
export default has;
|
||||||
|
|
||||||
|
add('object-assign', typeof global.Object.assign === 'function', true);
|
||||||
|
|
||||||
|
add('arraybuffer', typeof global.ArrayBuffer !== 'undefined', true);
|
||||||
|
add('formdata', typeof global.FormData !== 'undefined', true);
|
||||||
|
add('filereader', typeof global.FileReader !== 'undefined', true);
|
||||||
|
add('xhr', typeof global.XMLHttpRequest !== 'undefined', true);
|
||||||
|
add('xhr2', has('xhr') && 'responseType' in global.XMLHttpRequest.prototype, true);
|
||||||
|
add(
|
||||||
|
'blob',
|
||||||
|
function() {
|
||||||
|
if (!has('xhr2')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new global.XMLHttpRequest();
|
||||||
|
request.open('GET', global.location.protocol + '//www.google.com', true);
|
||||||
|
request.responseType = 'blob';
|
||||||
|
request.abort();
|
||||||
|
return request.responseType === 'blob';
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
add('node-buffer', 'Buffer' in global && typeof global.Buffer === 'function', true);
|
||||||
|
|
||||||
|
add('fetch', 'fetch' in global && typeof global.fetch === 'function', true);
|
||||||
|
|
||||||
|
add(
|
||||||
|
'web-worker-xhr-upload',
|
||||||
|
typeof global.Promise !== 'undefined' &&
|
||||||
|
new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
if (global.Worker !== undefined && global.URL && global.URL.createObjectURL) {
|
||||||
|
const blob = new Blob(
|
||||||
|
[
|
||||||
|
`(function () {
|
||||||
|
self.addEventListener('message', function () {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
try {
|
||||||
|
xhr.upload;
|
||||||
|
postMessage('true');
|
||||||
|
} catch (e) {
|
||||||
|
postMessage('false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})()`
|
||||||
|
],
|
||||||
|
{ type: 'application/javascript' }
|
||||||
|
);
|
||||||
|
const worker = new Worker(URL.createObjectURL(blob));
|
||||||
|
worker.addEventListener('message', ({ data: result }) => {
|
||||||
|
resolve(result === 'true');
|
||||||
|
});
|
||||||
|
worker.postMessage({});
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// IE11 on Winodws 8.1 encounters a security error.
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
100
src/lib/core/src/instrument.ts
Normal file
100
src/lib/core/src/instrument.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import has from './has';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default message to warn when no other is provided
|
||||||
|
*/
|
||||||
|
const DEFAULT_DEPRECATED_MESSAGE = 'This function will be removed in future versions.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set, globalWarn will be used instead of `console.warn`
|
||||||
|
*/
|
||||||
|
let globalWarn: ((message?: any, ...optionalParams: any[]) => void) | undefined;
|
||||||
|
|
||||||
|
export interface DeprecatedOptions {
|
||||||
|
/**
|
||||||
|
* The message to use when warning
|
||||||
|
*/
|
||||||
|
message?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the method or function to use
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An alternative function to log the warning to
|
||||||
|
*/
|
||||||
|
warn?: (...args: any[]) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference an URL for more information when warning
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that will console warn that a function has been deprecated
|
||||||
|
*
|
||||||
|
* @param options Provide options which change the display of the message
|
||||||
|
*/
|
||||||
|
export function deprecated({ message, name, warn, url }: DeprecatedOptions = {}): void {
|
||||||
|
/* istanbul ignore else: testing with debug off is difficult */
|
||||||
|
if (has('debug')) {
|
||||||
|
message = message || DEFAULT_DEPRECATED_MESSAGE;
|
||||||
|
let warning = `DEPRECATED: ${name ? name + ': ' : ''}${message}`;
|
||||||
|
if (url) {
|
||||||
|
warning += `\n\n See ${url} for more details.\n\n`;
|
||||||
|
}
|
||||||
|
if (warn) {
|
||||||
|
warn(warning);
|
||||||
|
} else if (globalWarn) {
|
||||||
|
globalWarn(warning);
|
||||||
|
} else {
|
||||||
|
console.warn(warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that generates before advice that can be used to warn when an API has been deprecated
|
||||||
|
*
|
||||||
|
* @param options Provide options which change the display of the message
|
||||||
|
*/
|
||||||
|
export function deprecatedAdvice(options?: DeprecatedOptions): (...args: any[]) => any[] {
|
||||||
|
return function(...args: any[]): any[] {
|
||||||
|
deprecated(options);
|
||||||
|
return args;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method decorator that will console warn when a method if invoked that is deprecated
|
||||||
|
*
|
||||||
|
* @param options Provide options which change the display of the message
|
||||||
|
*/
|
||||||
|
export function deprecatedDecorator(options?: DeprecatedOptions): MethodDecorator {
|
||||||
|
return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||||
|
if (has('debug')) {
|
||||||
|
const { value: originalFn } = descriptor;
|
||||||
|
options = options || {};
|
||||||
|
propertyKey = String(propertyKey);
|
||||||
|
/* IE 10/11 don't have the name property on functions */
|
||||||
|
options.name = target.constructor.name ? `${target.constructor.name}#${propertyKey}` : propertyKey;
|
||||||
|
descriptor.value = function(...args: any[]) {
|
||||||
|
deprecated(options);
|
||||||
|
return originalFn.apply(target, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that will set the warn function that will be used instead of `console.warn` when
|
||||||
|
* logging warning messages
|
||||||
|
*
|
||||||
|
* @param warn The function (or `undefined`) to use instead of `console.warn`
|
||||||
|
*/
|
||||||
|
export function setWarn(warn?: ((message?: any, ...optionalParams: any[]) => void)): void {
|
||||||
|
globalWarn = warn;
|
||||||
|
}
|
||||||
287
src/lib/core/src/interfaces.ts
Normal file
287
src/lib/core/src/interfaces.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
export type EventType = string | symbol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base event object, which provides a `type` property
|
||||||
|
*/
|
||||||
|
export interface EventObject<T = EventType> {
|
||||||
|
/**
|
||||||
|
* The type of the event
|
||||||
|
*/
|
||||||
|
readonly type: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventErrorObject<T = EventType> extends EventObject<T> {
|
||||||
|
/**
|
||||||
|
* The error that is the subject of this event
|
||||||
|
*/
|
||||||
|
readonly error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for an object which provides a cancelable event API. By calling the
|
||||||
|
* `.preventDefault()` method on the object, the event should be cancelled and not
|
||||||
|
* proceed any further
|
||||||
|
*/
|
||||||
|
export interface EventCancelableObject<T = EventType> extends EventObject<T> {
|
||||||
|
/**
|
||||||
|
* Can the event be canceled?
|
||||||
|
*/
|
||||||
|
readonly cancelable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was the event canceled?
|
||||||
|
*/
|
||||||
|
readonly defaultPrevented: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the event
|
||||||
|
*/
|
||||||
|
preventDefault(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used through the toolkit as a consistent API to manage how callers can "cleanup"
|
||||||
|
* when doing a function.
|
||||||
|
*/
|
||||||
|
export interface Handle {
|
||||||
|
/**
|
||||||
|
* Perform the destruction/cleanup logic associated with this handle
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A general interface that can be used to renference a general index map of values of a particular type
|
||||||
|
*/
|
||||||
|
export interface Hash<T> {
|
||||||
|
[id: string]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base map of styles where each key is the name of the style attribute and the value is a string
|
||||||
|
* which represents the style
|
||||||
|
*/
|
||||||
|
export interface StylesMap {
|
||||||
|
[style: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interfaces to the `@dojo/loader` AMD loader
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AmdConfig {
|
||||||
|
/**
|
||||||
|
* The base URL that the loader will use to resolve modules
|
||||||
|
*/
|
||||||
|
baseUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of module identifiers and their replacement meta data
|
||||||
|
*/
|
||||||
|
map?: AmdModuleMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of packages that the loader should use when resolving a module ID
|
||||||
|
*/
|
||||||
|
packages?: AmdPackage[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of paths to use when resolving modules names
|
||||||
|
*/
|
||||||
|
paths?: { [path: string]: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of packages that the loader should use when resolving a module ID
|
||||||
|
*/
|
||||||
|
pkgs?: { [path: string]: AmdPackage };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdDefine {
|
||||||
|
/**
|
||||||
|
* Define a module
|
||||||
|
*
|
||||||
|
* @param moduleId the MID to use for the module
|
||||||
|
* @param dependencies an array of MIDs this module depends upon
|
||||||
|
* @param factory the factory function that will return the module
|
||||||
|
*/
|
||||||
|
(moduleId: string, dependencies: string[], factory: AmdFactory): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a module
|
||||||
|
*
|
||||||
|
* @param dependencies an array of MIDs this module depends upon
|
||||||
|
* @param factory the factory function that will return the module
|
||||||
|
*/
|
||||||
|
(dependencies: string[], factory: AmdFactory): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a module
|
||||||
|
*
|
||||||
|
* @param factory the factory function that will return the module
|
||||||
|
*/
|
||||||
|
(factory: AmdFactory): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a module
|
||||||
|
*
|
||||||
|
* @param value the value for the module
|
||||||
|
*/
|
||||||
|
(value: any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta data about this particular AMD loader
|
||||||
|
*/
|
||||||
|
amd: { [prop: string]: string | number | boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdFactory {
|
||||||
|
/**
|
||||||
|
* The module factory
|
||||||
|
*
|
||||||
|
* @param modules The arguments that represent the resolved versions of the module dependencies
|
||||||
|
*/
|
||||||
|
(...modules: any[]): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdHas {
|
||||||
|
/**
|
||||||
|
* Determine if a feature is present
|
||||||
|
*
|
||||||
|
* @param name the feature name to check
|
||||||
|
*/
|
||||||
|
(name: string): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a feature test
|
||||||
|
*
|
||||||
|
* @param name The name of the feature to register
|
||||||
|
* @param value The test for the feature
|
||||||
|
* @param now If `true` the test will be executed immediatly, if not, it will be lazily executed
|
||||||
|
* @param force If `true` the test value will be overwritten if already registered
|
||||||
|
*/
|
||||||
|
add(
|
||||||
|
name: string,
|
||||||
|
value: (global: Window, document?: HTMLDocument, element?: HTMLDivElement) => any,
|
||||||
|
now?: boolean,
|
||||||
|
force?: boolean
|
||||||
|
): void;
|
||||||
|
add(name: string, value: any, now?: boolean, force?: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdModuleMap extends AmdModuleMapItem {
|
||||||
|
[sourceMid: string]: AmdModuleMapReplacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdModuleMapItem {
|
||||||
|
[mid: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdModuleMapReplacement extends AmdModuleMapItem {
|
||||||
|
[findMid: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeRequire {
|
||||||
|
(moduleId: string): any;
|
||||||
|
resolve(moduleId: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdPackage {
|
||||||
|
/**
|
||||||
|
* The path to the root of the package
|
||||||
|
*/
|
||||||
|
location?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main module of the package (defaults to `main.js`)
|
||||||
|
*/
|
||||||
|
main?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The package name
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdRequire {
|
||||||
|
/**
|
||||||
|
* Resolve a list of module dependencies and pass them to the callback
|
||||||
|
*
|
||||||
|
* @param dependencies The array of MIDs to resolve
|
||||||
|
* @param callback The function to invoke with the resolved dependencies
|
||||||
|
*/
|
||||||
|
(dependencies: string[], callback: AmdRequireCallback): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and return a single module (compatability with CommonJS `require`)
|
||||||
|
*
|
||||||
|
* @param moduleId The module ID to resolve and return
|
||||||
|
*/
|
||||||
|
<ModuleType>(moduleId: string): ModuleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If running in the node environment, a reference to the original NodeJS `require`
|
||||||
|
*/
|
||||||
|
nodeRequire?: NodeRequire;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a relative MID and return an absolute MID
|
||||||
|
*
|
||||||
|
* @param moduleId The relative module ID to resolve
|
||||||
|
*/
|
||||||
|
toAbsMid(moduleId: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a path and resolve the full URL for the path
|
||||||
|
*
|
||||||
|
* @param path The path to resolve and return as a URL
|
||||||
|
*/
|
||||||
|
toUrl(path: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdRequireCallback {
|
||||||
|
/**
|
||||||
|
* The `require` callback
|
||||||
|
*
|
||||||
|
* @param modules The arguments that represent the resolved versions of dependencies
|
||||||
|
*/
|
||||||
|
(...modules: any[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AmdRootRequire extends AmdRequire {
|
||||||
|
/**
|
||||||
|
* The minimalist `has` API integrated with the `@dojo/loader`
|
||||||
|
*/
|
||||||
|
has: AmdHas;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event listener
|
||||||
|
*
|
||||||
|
* @param type The event type to listen for
|
||||||
|
* @param listener The listener to call when the event is emitted
|
||||||
|
*/
|
||||||
|
on(type: AmdRequireOnSignalType, listener: any): { remove: () => void };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the loader
|
||||||
|
*
|
||||||
|
* @param config The configuration to apply to the loader
|
||||||
|
*/
|
||||||
|
config(config: AmdConfig): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return internal values of loader for debug purposes
|
||||||
|
*
|
||||||
|
* @param name The name of the internal label
|
||||||
|
*/
|
||||||
|
inspect?(name: string): any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undefine a module, based on absolute MID that should be removed from the loader cache
|
||||||
|
*/
|
||||||
|
undef(moduleId: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signal type for the `require.on` API
|
||||||
|
*/
|
||||||
|
export type AmdRequireOnSignalType = 'error';
|
||||||
390
src/lib/core/src/lang.ts
Normal file
390
src/lib/core/src/lang.ts
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
import { Handle } from './interfaces';
|
||||||
|
import { assign } from '@dojo/shim/object';
|
||||||
|
|
||||||
|
export { assign } from '@dojo/shim/object';
|
||||||
|
|
||||||
|
const slice = Array.prototype.slice;
|
||||||
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard that ensures that the value can be coerced to Object
|
||||||
|
* to weed out host objects that do not derive from Object.
|
||||||
|
* This function is used to check if we want to deep copy an object or not.
|
||||||
|
* Note: In ES6 it is possible to modify an object's Symbol.toStringTag property, which will
|
||||||
|
* change the value returned by `toString`. This is a rare edge case that is difficult to handle,
|
||||||
|
* so it is not handled here.
|
||||||
|
* @param value The value to check
|
||||||
|
* @return If the value is coercible into an Object
|
||||||
|
*/
|
||||||
|
function shouldDeepCopyObject(value: any): value is Object {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Object]';
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyArray<T>(array: T[], inherited: boolean): T[] {
|
||||||
|
return array.map(function(item: T): T {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
return <any>copyArray(<any>item, inherited);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !shouldDeepCopyObject(item)
|
||||||
|
? item
|
||||||
|
: _mixin({
|
||||||
|
deep: true,
|
||||||
|
inherited: inherited,
|
||||||
|
sources: <Array<T>>[item],
|
||||||
|
target: <T>{}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MixinArgs<T extends {}, U extends {}> {
|
||||||
|
deep: boolean;
|
||||||
|
inherited: boolean;
|
||||||
|
sources: (U | null | undefined)[];
|
||||||
|
target: T;
|
||||||
|
copied?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _mixin<T extends {}, U extends {}>(kwArgs: MixinArgs<T, U>): T & U {
|
||||||
|
const deep = kwArgs.deep;
|
||||||
|
const inherited = kwArgs.inherited;
|
||||||
|
const target: any = kwArgs.target;
|
||||||
|
const copied = kwArgs.copied || [];
|
||||||
|
const copiedClone = [...copied];
|
||||||
|
|
||||||
|
for (let i = 0; i < kwArgs.sources.length; i++) {
|
||||||
|
const source = kwArgs.sources[i];
|
||||||
|
|
||||||
|
if (source === null || source === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (let key in source) {
|
||||||
|
if (inherited || hasOwnProperty.call(source, key)) {
|
||||||
|
let value: any = source[key];
|
||||||
|
|
||||||
|
if (copiedClone.indexOf(value) !== -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deep) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value = copyArray(value, inherited);
|
||||||
|
} else if (shouldDeepCopyObject(value)) {
|
||||||
|
const targetValue: any = target[key] || {};
|
||||||
|
copied.push(source);
|
||||||
|
value = _mixin({
|
||||||
|
deep: true,
|
||||||
|
inherited: inherited,
|
||||||
|
sources: [value],
|
||||||
|
target: targetValue,
|
||||||
|
copied
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <T & U>target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new object from the given prototype, and copies all enumerable own properties of one or more
|
||||||
|
* source objects to the newly created target object.
|
||||||
|
*
|
||||||
|
* @param prototype The prototype to create a new object from
|
||||||
|
* @param mixins Any number of objects whose enumerable own properties will be copied to the created object
|
||||||
|
* @return The new object
|
||||||
|
*/
|
||||||
|
export function create<
|
||||||
|
T extends {},
|
||||||
|
U extends {},
|
||||||
|
V extends {},
|
||||||
|
W extends {},
|
||||||
|
X extends {},
|
||||||
|
Y extends {},
|
||||||
|
Z extends {}
|
||||||
|
>(prototype: T, mixin1: U, mixin2: V, mixin3: W, mixin4: X, mixin5: Y, mixin6: Z): T & U & V & W & X & Y & Z;
|
||||||
|
export function create<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
|
||||||
|
prototype: T,
|
||||||
|
mixin1: U,
|
||||||
|
mixin2: V,
|
||||||
|
mixin3: W,
|
||||||
|
mixin4: X,
|
||||||
|
mixin5: Y
|
||||||
|
): T & U & V & W & X & Y;
|
||||||
|
export function create<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
|
||||||
|
prototype: T,
|
||||||
|
mixin1: U,
|
||||||
|
mixin2: V,
|
||||||
|
mixin3: W,
|
||||||
|
mixin4: X
|
||||||
|
): T & U & V & W & X;
|
||||||
|
export function create<T extends {}, U extends {}, V extends {}, W extends {}>(
|
||||||
|
prototype: T,
|
||||||
|
mixin1: U,
|
||||||
|
mixin2: V,
|
||||||
|
mixin3: W
|
||||||
|
): T & U & V & W;
|
||||||
|
export function create<T extends {}, U extends {}, V extends {}>(prototype: T, mixin1: U, mixin2: V): T & U & V;
|
||||||
|
export function create<T extends {}, U extends {}>(prototype: T, mixin: U): T & U;
|
||||||
|
export function create<T extends {}>(prototype: T): T;
|
||||||
|
export function create(prototype: any, ...mixins: any[]): any {
|
||||||
|
if (!mixins.length) {
|
||||||
|
throw new RangeError('lang.create requires at least one mixin object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = mixins.slice();
|
||||||
|
args.unshift(Object.create(prototype));
|
||||||
|
|
||||||
|
return assign.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the values of all enumerable own properties of one or more source objects to the target object,
|
||||||
|
* recursively copying all nested objects and arrays as well.
|
||||||
|
*
|
||||||
|
* @param target The target object to receive values from source objects
|
||||||
|
* @param sources Any number of objects whose enumerable own properties will be copied to the target object
|
||||||
|
* @return The modified target object
|
||||||
|
*/
|
||||||
|
export function deepAssign<
|
||||||
|
T extends {},
|
||||||
|
U extends {},
|
||||||
|
V extends {},
|
||||||
|
W extends {},
|
||||||
|
X extends {},
|
||||||
|
Y extends {},
|
||||||
|
Z extends {}
|
||||||
|
>(target: T, source1: U, source2: V, source3: W, source4: X, source5: Y, source6: Z): T & U & V & W & X & Y & Z;
|
||||||
|
export function deepAssign<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X,
|
||||||
|
source5: Y
|
||||||
|
): T & U & V & W & X & Y;
|
||||||
|
export function deepAssign<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X
|
||||||
|
): T & U & V & W & X;
|
||||||
|
export function deepAssign<T extends {}, U extends {}, V extends {}, W extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W
|
||||||
|
): T & U & V & W;
|
||||||
|
export function deepAssign<T extends {}, U extends {}, V extends {}>(target: T, source1: U, source2: V): T & U & V;
|
||||||
|
export function deepAssign<T extends {}, U extends {}>(target: T, source: U): T & U;
|
||||||
|
export function deepAssign(target: any, ...sources: any[]): any {
|
||||||
|
return _mixin({
|
||||||
|
deep: true,
|
||||||
|
inherited: false,
|
||||||
|
sources: sources,
|
||||||
|
target: target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the values of all enumerable (own or inherited) properties of one or more source objects to the
|
||||||
|
* target object, recursively copying all nested objects and arrays as well.
|
||||||
|
*
|
||||||
|
* @param target The target object to receive values from source objects
|
||||||
|
* @param sources Any number of objects whose enumerable properties will be copied to the target object
|
||||||
|
* @return The modified target object
|
||||||
|
*/
|
||||||
|
export function deepMixin<
|
||||||
|
T extends {},
|
||||||
|
U extends {},
|
||||||
|
V extends {},
|
||||||
|
W extends {},
|
||||||
|
X extends {},
|
||||||
|
Y extends {},
|
||||||
|
Z extends {}
|
||||||
|
>(target: T, source1: U, source2: V, source3: W, source4: X, source5: Y, source6: Z): T & U & V & W & X & Y & Z;
|
||||||
|
export function deepMixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X,
|
||||||
|
source5: Y
|
||||||
|
): T & U & V & W & X & Y;
|
||||||
|
export function deepMixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X
|
||||||
|
): T & U & V & W & X;
|
||||||
|
export function deepMixin<T extends {}, U extends {}, V extends {}, W extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W
|
||||||
|
): T & U & V & W;
|
||||||
|
export function deepMixin<T extends {}, U extends {}, V extends {}>(target: T, source1: U, source2: V): T & U & V;
|
||||||
|
export function deepMixin<T extends {}, U extends {}>(target: T, source: U): T & U;
|
||||||
|
export function deepMixin(target: any, ...sources: any[]): any {
|
||||||
|
return _mixin({
|
||||||
|
deep: true,
|
||||||
|
inherited: true,
|
||||||
|
sources: sources,
|
||||||
|
target: target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new object using the provided source's prototype as the prototype for the new object, and then
|
||||||
|
* deep copies the provided source's values into the new target.
|
||||||
|
*
|
||||||
|
* @param source The object to duplicate
|
||||||
|
* @return The new object
|
||||||
|
*/
|
||||||
|
export function duplicate<T extends {}>(source: T): T {
|
||||||
|
const target = Object.create(Object.getPrototypeOf(source));
|
||||||
|
|
||||||
|
return deepMixin(target, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether two values are the same value.
|
||||||
|
*
|
||||||
|
* @param a First value to compare
|
||||||
|
* @param b Second value to compare
|
||||||
|
* @return true if the values are the same; false otherwise
|
||||||
|
*/
|
||||||
|
export function isIdentical(a: any, b: any): boolean {
|
||||||
|
return (
|
||||||
|
a === b ||
|
||||||
|
/* both values are NaN */
|
||||||
|
(a !== a && b !== b)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that binds a method to the specified object at runtime. This is similar to
|
||||||
|
* `Function.prototype.bind`, but instead of a function it takes the name of a method on an object.
|
||||||
|
* As a result, the function returned by `lateBind` will always call the function currently assigned to
|
||||||
|
* the specified property on the object as of the moment the function it returns is called.
|
||||||
|
*
|
||||||
|
* @param instance The context object
|
||||||
|
* @param method The name of the method on the context object to bind to itself
|
||||||
|
* @param suppliedArgs An optional array of values to prepend to the `instance[method]` arguments list
|
||||||
|
* @return The bound function
|
||||||
|
*/
|
||||||
|
export function lateBind(instance: {}, method: string, ...suppliedArgs: any[]): (...args: any[]) => any {
|
||||||
|
return suppliedArgs.length
|
||||||
|
? function() {
|
||||||
|
const args: any[] = arguments.length ? suppliedArgs.concat(slice.call(arguments)) : suppliedArgs;
|
||||||
|
|
||||||
|
// TS7017
|
||||||
|
return (<any>instance)[method].apply(instance, args);
|
||||||
|
}
|
||||||
|
: function() {
|
||||||
|
// TS7017
|
||||||
|
return (<any>instance)[method].apply(instance, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the values of all enumerable (own or inherited) properties of one or more source objects to the
|
||||||
|
* target object.
|
||||||
|
*
|
||||||
|
* @return The modified target object
|
||||||
|
*/
|
||||||
|
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}, Z extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X,
|
||||||
|
source5: Y,
|
||||||
|
source6: Z
|
||||||
|
): T & U & V & W & X & Y & Z;
|
||||||
|
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}, Y extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X,
|
||||||
|
source5: Y
|
||||||
|
): T & U & V & W & X & Y;
|
||||||
|
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}, X extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W,
|
||||||
|
source4: X
|
||||||
|
): T & U & V & W & X;
|
||||||
|
export function mixin<T extends {}, U extends {}, V extends {}, W extends {}>(
|
||||||
|
target: T,
|
||||||
|
source1: U,
|
||||||
|
source2: V,
|
||||||
|
source3: W
|
||||||
|
): T & U & V & W;
|
||||||
|
export function mixin<T extends {}, U extends {}, V extends {}>(target: T, source1: U, source2: V): T & U & V;
|
||||||
|
export function mixin<T extends {}, U extends {}>(target: T, source: U): T & U;
|
||||||
|
export function mixin(target: any, ...sources: any[]): any {
|
||||||
|
return _mixin({
|
||||||
|
deep: false,
|
||||||
|
inherited: true,
|
||||||
|
sources: sources,
|
||||||
|
target: target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function which invokes the given function with the given arguments prepended to its argument list.
|
||||||
|
* Like `Function.prototype.bind`, but does not alter execution context.
|
||||||
|
*
|
||||||
|
* @param targetFunction The function that needs to be bound
|
||||||
|
* @param suppliedArgs An optional array of arguments to prepend to the `targetFunction` arguments list
|
||||||
|
* @return The bound function
|
||||||
|
*/
|
||||||
|
export function partial(targetFunction: (...args: any[]) => any, ...suppliedArgs: any[]): (...args: any[]) => any {
|
||||||
|
return function(this: any) {
|
||||||
|
const args: any[] = arguments.length ? suppliedArgs.concat(slice.call(arguments)) : suppliedArgs;
|
||||||
|
|
||||||
|
return targetFunction.apply(this, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object with a destroy method that, when called, calls the passed-in destructor.
|
||||||
|
* This is intended to provide a unified interface for creating "remove" / "destroy" handlers for
|
||||||
|
* event listeners, timers, etc.
|
||||||
|
*
|
||||||
|
* @param destructor A function that will be called when the handle's `destroy` method is invoked
|
||||||
|
* @return The handle object
|
||||||
|
*/
|
||||||
|
export function createHandle(destructor: () => void): Handle {
|
||||||
|
let called = false;
|
||||||
|
return {
|
||||||
|
destroy: function(this: Handle) {
|
||||||
|
if (!called) {
|
||||||
|
called = true;
|
||||||
|
destructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single handle that can be used to destroy multiple handles simultaneously.
|
||||||
|
*
|
||||||
|
* @param handles An array of handles with `destroy` methods
|
||||||
|
* @return The handle object
|
||||||
|
*/
|
||||||
|
export function createCompositeHandle(...handles: Handle[]): Handle {
|
||||||
|
return createHandle(function() {
|
||||||
|
for (let i = 0; i < handles.length; i++) {
|
||||||
|
handles[i].destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
110
src/lib/core/src/load.ts
Normal file
110
src/lib/core/src/load.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
import { AmdRequire, AmdDefine, NodeRequire } from './interfaces';
|
||||||
|
import { isPlugin, useDefault } from './load/util';
|
||||||
|
|
||||||
|
export type Require = AmdRequire | NodeRequire;
|
||||||
|
|
||||||
|
export interface Load {
|
||||||
|
(require: Require, ...moduleIds: string[]): Promise<any[]>;
|
||||||
|
(...moduleIds: string[]): Promise<any[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const require: Require;
|
||||||
|
|
||||||
|
declare const define: AmdDefine;
|
||||||
|
|
||||||
|
export function isAmdRequire(object: any): object is AmdRequire {
|
||||||
|
return typeof object.toUrl === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNodeRequire(object: any): object is NodeRequire {
|
||||||
|
return typeof object.resolve === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
const load: Load = (function(): Load {
|
||||||
|
const resolver = isAmdRequire(require)
|
||||||
|
? require.toUrl
|
||||||
|
: isNodeRequire(require) ? require.resolve : (resourceId: string) => resourceId;
|
||||||
|
|
||||||
|
function pluginLoad(moduleIds: string[], load: Load, loader: (modulesIds: string[]) => Promise<any>) {
|
||||||
|
const pluginResourceIds: string[] = [];
|
||||||
|
moduleIds = moduleIds.map((id: string, i: number) => {
|
||||||
|
const parts = id.split('!');
|
||||||
|
pluginResourceIds[i] = parts[1];
|
||||||
|
return parts[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
return loader(moduleIds).then((modules: any[]) => {
|
||||||
|
pluginResourceIds.forEach((resourceId: string, i: number) => {
|
||||||
|
if (typeof resourceId === 'string') {
|
||||||
|
const module = modules[i];
|
||||||
|
const defaultExport = module['default'] || module;
|
||||||
|
|
||||||
|
if (isPlugin(defaultExport)) {
|
||||||
|
resourceId =
|
||||||
|
typeof defaultExport.normalize === 'function'
|
||||||
|
? defaultExport.normalize(resourceId, resolver)
|
||||||
|
: resolver(resourceId);
|
||||||
|
|
||||||
|
modules[i] = defaultExport.load(resourceId, load);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(modules);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module === 'object' && typeof module.exports === 'object') {
|
||||||
|
return function load(contextualRequire: any, ...moduleIds: string[]): Promise<any[]> {
|
||||||
|
if (typeof contextualRequire === 'string') {
|
||||||
|
moduleIds.unshift(contextualRequire);
|
||||||
|
contextualRequire = require;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginLoad(moduleIds, load, (moduleIds: string[]) => {
|
||||||
|
try {
|
||||||
|
return Promise.resolve(
|
||||||
|
moduleIds.map(function(moduleId): any {
|
||||||
|
return contextualRequire(moduleId.split('!')[0]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else if (typeof define === 'function' && define.amd) {
|
||||||
|
return function load(contextualRequire: any, ...moduleIds: string[]): Promise<any[]> {
|
||||||
|
if (typeof contextualRequire === 'string') {
|
||||||
|
moduleIds.unshift(contextualRequire);
|
||||||
|
contextualRequire = require;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginLoad(moduleIds, load, (moduleIds: string[]) => {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
let errorHandle: { remove: () => void };
|
||||||
|
|
||||||
|
if (typeof contextualRequire.on === 'function') {
|
||||||
|
errorHandle = contextualRequire.on('error', (error: Error) => {
|
||||||
|
errorHandle.remove();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
contextualRequire(moduleIds, function(...modules: any[]) {
|
||||||
|
errorHandle && errorHandle.remove();
|
||||||
|
resolve(modules);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return function() {
|
||||||
|
return Promise.reject(new Error('Unknown loader'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
export default load;
|
||||||
|
|
||||||
|
export { isPlugin, useDefault };
|
||||||
59
src/lib/core/src/load/util.ts
Normal file
59
src/lib/core/src/load/util.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { isArrayLike, isIterable } from '@dojo/shim/iterator';
|
||||||
|
import { Load } from '../load';
|
||||||
|
|
||||||
|
export interface LoadPlugin<T> {
|
||||||
|
/**
|
||||||
|
* An optional method that normmalizes a resource id.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The raw resource id.
|
||||||
|
*
|
||||||
|
* @param resolver
|
||||||
|
* A method that can resolve an id to an absolute path. Depending on the environment, this will
|
||||||
|
* usually be either `require.toUrl` or `require.resolve`.
|
||||||
|
*/
|
||||||
|
normalize?: (resourceId: string, resolver: (resourceId: string) => string) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that loads the specified resource.
|
||||||
|
*
|
||||||
|
* @param resourceId
|
||||||
|
* The id of the resource to load.
|
||||||
|
*
|
||||||
|
* @param load
|
||||||
|
* The `load` method that was used to load and execute the plugin.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A promise that resolves to the loaded resource.
|
||||||
|
*/
|
||||||
|
load(resourceId: string, load: Load): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlugin(value: any): value is LoadPlugin<any> {
|
||||||
|
return Boolean(value) && typeof value.load === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDefault(modules: any[]): any[];
|
||||||
|
export function useDefault(module: any): any;
|
||||||
|
export function useDefault(modules: any | any[]): any[] | any {
|
||||||
|
if (isArrayLike(modules)) {
|
||||||
|
let processedModules: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < modules.length; i++) {
|
||||||
|
const module = modules[i];
|
||||||
|
processedModules.push(module.__esModule && module.default ? module.default : module);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedModules;
|
||||||
|
} else if (isIterable(modules)) {
|
||||||
|
let processedModules: any[] = [];
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
processedModules.push(module.__esModule && module.default ? module.default : module);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedModules;
|
||||||
|
} else {
|
||||||
|
return modules.__esModule && modules.default ? modules.default : modules;
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/lib/core/src/load/webpack.ts
Normal file
146
src/lib/core/src/load/webpack.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
import { isPlugin, useDefault } from './util';
|
||||||
|
|
||||||
|
interface ModuleIdMap {
|
||||||
|
[path: string]: { id: number; lazy: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BundleLoaderCallback<T> {
|
||||||
|
(value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebpackRequire<T> {
|
||||||
|
(id: number): T | BundleLoaderCallback<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A global map (set by the build) of resolved module paths to webpack-specific module data.
|
||||||
|
*/
|
||||||
|
declare const __modules__: ModuleIdMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The webpack-specific require function, set globally by webpack.
|
||||||
|
*/
|
||||||
|
declare const __webpack_require__: WebpackRequire<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Resolves an absolute path from an absolute base path and relative module ID.
|
||||||
|
*
|
||||||
|
* @param base
|
||||||
|
* The absolute base path.
|
||||||
|
*
|
||||||
|
* @param mid
|
||||||
|
* The relative module ID
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The resolved absolute module path.
|
||||||
|
*/
|
||||||
|
function resolveRelative(base: string, mid: string): string {
|
||||||
|
const isRelative = mid.match(/^\.+\//);
|
||||||
|
let result = base;
|
||||||
|
|
||||||
|
if (isRelative) {
|
||||||
|
if (mid.match(/^\.\//)) {
|
||||||
|
mid = mid.replace(/\.\//, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const up = mid.match(/\.\.\//g);
|
||||||
|
if (up) {
|
||||||
|
const chunks = base.split('/');
|
||||||
|
|
||||||
|
if (up.length > chunks.length) {
|
||||||
|
throw new Error('Path cannot go beyond root directory.');
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.splice(chunks.length - up.length);
|
||||||
|
result = chunks.join('/');
|
||||||
|
mid = mid.replace(/\.\.\//g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
mid = result + '/' + mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Returns the parent directory for the specified module ID.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* A function that returns the context module ID.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The parent directory of the path returned by the context function.
|
||||||
|
*/
|
||||||
|
function getBasePath(context: () => string): string {
|
||||||
|
return context()
|
||||||
|
.split('/')
|
||||||
|
.slice(0, -1)
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A webpack-specific function that replaces `@dojo/core/load` in its builds. In order for a module to be loaded,
|
||||||
|
* it must first be included in a webpack chunk, whether that chunk is included in the main build, or lazy-loaded.
|
||||||
|
* Note that this module is not intended for direct use, but rather is intended for use by a webpack plugin
|
||||||
|
* that sets the module ID map used to translate resolved module paths to webpack module IDs.
|
||||||
|
*
|
||||||
|
* @param contextRequire
|
||||||
|
* An optional function that returns the base path to use when resolving relative module IDs.
|
||||||
|
*
|
||||||
|
* @param ...mids
|
||||||
|
* One or more IDs for modules to load.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A promise to the loaded module values.
|
||||||
|
*/
|
||||||
|
export default function load(contextRequire: () => string, ...mids: string[]): Promise<any[]>;
|
||||||
|
export default function load(...mids: string[]): Promise<any[]>;
|
||||||
|
export default function load(...args: any[]): Promise<any[]> {
|
||||||
|
const req = __webpack_require__;
|
||||||
|
const context =
|
||||||
|
typeof args[0] === 'function'
|
||||||
|
? args[0]
|
||||||
|
: function() {
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const modules = __modules__ || {};
|
||||||
|
const base = getBasePath(context);
|
||||||
|
|
||||||
|
const results = args
|
||||||
|
.filter((mid: string | Function) => typeof mid === 'string')
|
||||||
|
.map((mid: string) => resolveRelative(base, mid))
|
||||||
|
.map((mid: string) => {
|
||||||
|
let [moduleId, pluginResourceId] = mid.split('!');
|
||||||
|
const moduleMeta = modules[mid] || modules[moduleId];
|
||||||
|
|
||||||
|
if (!moduleMeta) {
|
||||||
|
return Promise.reject(new Error(`Missing module: ${mid}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleMeta.lazy) {
|
||||||
|
return new Promise((resolve) => req(moduleMeta.id)(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = req(moduleMeta.id);
|
||||||
|
const defaultExport = module['default'] || module;
|
||||||
|
|
||||||
|
if (isPlugin(defaultExport)) {
|
||||||
|
pluginResourceId =
|
||||||
|
typeof defaultExport.normalize === 'function'
|
||||||
|
? defaultExport.normalize(pluginResourceId, (mid: string) => resolveRelative(base, mid))
|
||||||
|
: resolveRelative(base, pluginResourceId);
|
||||||
|
|
||||||
|
return Promise.resolve(defaultExport.load(pluginResourceId, <any>load));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(module);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isPlugin, useDefault };
|
||||||
47
src/lib/core/src/main.ts
Normal file
47
src/lib/core/src/main.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import * as aspect from './aspect';
|
||||||
|
import DateObject from './DateObject';
|
||||||
|
import Evented from './Evented';
|
||||||
|
import IdentityRegistry from './IdentityRegistry';
|
||||||
|
import * as lang from './lang';
|
||||||
|
import * as base64 from './base64';
|
||||||
|
import load from './load';
|
||||||
|
import MatchRegistry from './MatchRegistry';
|
||||||
|
import on, { emit } from './on';
|
||||||
|
import * as queue from './queue';
|
||||||
|
import request from './request';
|
||||||
|
import Scheduler from './Scheduler';
|
||||||
|
import * as stringExtras from './stringExtras';
|
||||||
|
import * as text from './text';
|
||||||
|
import UrlSearchParams from './UrlSearchParams';
|
||||||
|
import * as util from './util';
|
||||||
|
|
||||||
|
import * as iteration from './async/iteration';
|
||||||
|
import Task from './async/Task';
|
||||||
|
import * as timing from './async/timing';
|
||||||
|
|
||||||
|
const async = {
|
||||||
|
iteration,
|
||||||
|
Task,
|
||||||
|
timing
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
aspect,
|
||||||
|
async,
|
||||||
|
base64,
|
||||||
|
DateObject,
|
||||||
|
emit,
|
||||||
|
Evented,
|
||||||
|
IdentityRegistry,
|
||||||
|
lang,
|
||||||
|
load,
|
||||||
|
MatchRegistry,
|
||||||
|
on,
|
||||||
|
queue,
|
||||||
|
request,
|
||||||
|
Scheduler,
|
||||||
|
stringExtras,
|
||||||
|
text,
|
||||||
|
UrlSearchParams,
|
||||||
|
util
|
||||||
|
};
|
||||||
230
src/lib/core/src/on.ts
Normal file
230
src/lib/core/src/on.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { Handle, EventObject } from './interfaces';
|
||||||
|
import { createHandle, createCompositeHandle } from './lang';
|
||||||
|
import Evented, { CustomEventTypes } from './Evented';
|
||||||
|
|
||||||
|
export interface EventCallback<O = EventObject<string>> {
|
||||||
|
(event: O): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventEmitter {
|
||||||
|
on(event: string, listener: EventCallback): EventEmitter;
|
||||||
|
removeListener(event: string, listener: EventCallback): EventEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DOMEventObject extends EventObject {
|
||||||
|
bubbles: boolean;
|
||||||
|
cancelable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a normalized mechanism for dispatching events for event emitters, Evented objects, or DOM nodes.
|
||||||
|
* @param target The target to emit the event from
|
||||||
|
* @param event The event object to emit
|
||||||
|
* @return Boolean indicating if preventDefault was called on the event object (only relevant for DOM events;
|
||||||
|
* always false for other event emitters)
|
||||||
|
*/
|
||||||
|
export function emit<
|
||||||
|
M extends CustomEventTypes,
|
||||||
|
T,
|
||||||
|
O extends EventObject<T> = EventObject<T>,
|
||||||
|
K extends keyof M = keyof M
|
||||||
|
>(target: Evented<M, T, O>, event: M[K]): boolean;
|
||||||
|
export function emit<T, O extends EventObject<T> = EventObject<T>>(target: Evented<any, T, O>, event: O): boolean;
|
||||||
|
export function emit<O extends EventObject<string> = EventObject<string>>(
|
||||||
|
target: EventTarget | EventEmitter,
|
||||||
|
event: O
|
||||||
|
): boolean;
|
||||||
|
export function emit(target: any, event: EventObject<any>): boolean {
|
||||||
|
if (
|
||||||
|
target.dispatchEvent /* includes window and document */ &&
|
||||||
|
((target.ownerDocument && target.ownerDocument.createEvent) /* matches nodes */ ||
|
||||||
|
(target.document && target.document.createEvent) /* matches window */ ||
|
||||||
|
target.createEvent) /* matches document */
|
||||||
|
) {
|
||||||
|
const nativeEvent = (target.ownerDocument || target.document || target).createEvent('HTMLEvents');
|
||||||
|
nativeEvent.initEvent(
|
||||||
|
event.type,
|
||||||
|
Boolean((<DOMEventObject>event).bubbles),
|
||||||
|
Boolean((<DOMEventObject>event).cancelable)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let key in event) {
|
||||||
|
if (!(key in nativeEvent)) {
|
||||||
|
nativeEvent[key] = (<any>event)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target.dispatchEvent(nativeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.emit) {
|
||||||
|
if (target.removeListener) {
|
||||||
|
// Node.js EventEmitter
|
||||||
|
target.emit(event.type, event);
|
||||||
|
return false;
|
||||||
|
} else if (target.on) {
|
||||||
|
// Dojo Evented or similar
|
||||||
|
target.emit(event);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Target must be an event emitter');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a normalized mechanism for listening to events from event emitters, Evented objects, or DOM nodes.
|
||||||
|
* @param target Target to listen for event on
|
||||||
|
* @param type Event event type(s) to listen for; may a string or an array of strings
|
||||||
|
* @param listener Callback to handle the event when it fires
|
||||||
|
* @param capture Whether the listener should be registered in the capture phase (DOM events only)
|
||||||
|
* @return A handle which will remove the listener when destroy is called
|
||||||
|
*/
|
||||||
|
export default function on<
|
||||||
|
M extends CustomEventTypes,
|
||||||
|
T,
|
||||||
|
K extends keyof M = keyof M,
|
||||||
|
O extends EventObject<T> = EventObject<T>
|
||||||
|
>(target: Evented<M, T, O>, type: K | K[], listener: EventCallback<M[K]>): Handle;
|
||||||
|
export default function on<T, O extends EventObject<T> = EventObject<T>>(
|
||||||
|
target: Evented<any, T, O>,
|
||||||
|
type: T | T[],
|
||||||
|
listener: EventCallback<O>
|
||||||
|
): Handle;
|
||||||
|
export default function on(target: EventEmitter, type: string | string[], listener: EventCallback): Handle;
|
||||||
|
export default function on(
|
||||||
|
target: EventTarget,
|
||||||
|
type: string | string[],
|
||||||
|
listener: EventCallback,
|
||||||
|
capture?: boolean
|
||||||
|
): Handle;
|
||||||
|
export default function on(target: any, type: any, listener: any, capture?: boolean): Handle {
|
||||||
|
if (Array.isArray(type)) {
|
||||||
|
let handles: Handle[] = type.map(function(type: string): Handle {
|
||||||
|
return on(target, type, listener, capture);
|
||||||
|
});
|
||||||
|
|
||||||
|
return createCompositeHandle(...handles);
|
||||||
|
}
|
||||||
|
|
||||||
|
const callback = function(this: any) {
|
||||||
|
listener.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM EventTarget
|
||||||
|
if (target.addEventListener && target.removeEventListener) {
|
||||||
|
target.addEventListener(type, callback, capture);
|
||||||
|
return createHandle(function() {
|
||||||
|
target.removeEventListener(type, callback, capture);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.on) {
|
||||||
|
// EventEmitter
|
||||||
|
if (target.removeListener) {
|
||||||
|
target.on(type, callback);
|
||||||
|
return createHandle(function() {
|
||||||
|
target.removeListener(type, callback);
|
||||||
|
});
|
||||||
|
} else if (target.emit) {
|
||||||
|
// Evented
|
||||||
|
return target.on(type, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('Unknown event emitter object');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a mechanism for listening to the next occurrence of an event from event
|
||||||
|
* emitters, Evented objects, or DOM nodes.
|
||||||
|
* @param target Target to listen for event on
|
||||||
|
* @param type Event event type(s) to listen for; may be a string or an array of strings
|
||||||
|
* @param listener Callback to handle the event when it fires
|
||||||
|
* @param capture Whether the listener should be registered in the capture phase (DOM events only)
|
||||||
|
* @return A handle which will remove the listener when destroy is called
|
||||||
|
*/
|
||||||
|
export function once<
|
||||||
|
M extends CustomEventTypes,
|
||||||
|
T,
|
||||||
|
K extends keyof M = keyof M,
|
||||||
|
O extends EventObject<T> = EventObject<T>
|
||||||
|
>(target: Evented<M, T, O>, type: K | K[], listener: EventCallback<M[K]>): Handle;
|
||||||
|
export function once<T, O extends EventObject<T> = EventObject<T>>(
|
||||||
|
target: Evented<any, T, O>,
|
||||||
|
type: T | T[],
|
||||||
|
listener: EventCallback<O>
|
||||||
|
): Handle;
|
||||||
|
export function once(target: EventTarget, type: string | string[], listener: EventCallback, capture?: boolean): Handle;
|
||||||
|
export function once(target: EventEmitter, type: string | string[], listener: EventCallback): Handle;
|
||||||
|
export function once(target: any, type: any, listener: any, capture?: boolean): Handle {
|
||||||
|
// FIXME
|
||||||
|
// tslint:disable-next-line:no-var-keyword
|
||||||
|
var handle = on(
|
||||||
|
target,
|
||||||
|
type,
|
||||||
|
function(this: any) {
|
||||||
|
handle.destroy();
|
||||||
|
return listener.apply(this, arguments);
|
||||||
|
},
|
||||||
|
capture
|
||||||
|
);
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PausableHandle extends Handle {
|
||||||
|
pause(): void;
|
||||||
|
resume(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a mechanism for creating pausable listeners for events from event emitters, Evented objects, or DOM nodes.
|
||||||
|
* @param target Target to listen for event on
|
||||||
|
* @param type Event event type(s) to listen for; may a string or an array of strings
|
||||||
|
* @param listener Callback to handle the event when it fires
|
||||||
|
* @param capture Whether the listener should be registered in the capture phase (DOM events only)
|
||||||
|
* @return A handle with additional pause and resume methods; the listener will never fire when paused
|
||||||
|
*/
|
||||||
|
export function pausable<
|
||||||
|
M extends CustomEventTypes,
|
||||||
|
T,
|
||||||
|
K extends keyof M = keyof M,
|
||||||
|
O extends EventObject<T> = EventObject<T>
|
||||||
|
>(target: Evented<M, T, O>, type: K | K[], listener: EventCallback<M[K]>): PausableHandle;
|
||||||
|
export function pausable<T, O extends EventObject<T> = EventObject<T>>(
|
||||||
|
target: Evented<any, T, O>,
|
||||||
|
type: T | T[],
|
||||||
|
listener: EventCallback<O>
|
||||||
|
): PausableHandle;
|
||||||
|
export function pausable(
|
||||||
|
target: EventTarget,
|
||||||
|
type: string | string[],
|
||||||
|
listener: EventCallback,
|
||||||
|
capture?: boolean
|
||||||
|
): PausableHandle;
|
||||||
|
export function pausable(target: EventEmitter, type: string | string[], listener: EventCallback): PausableHandle;
|
||||||
|
export function pausable(target: any, type: any, listener: any, capture?: boolean): PausableHandle {
|
||||||
|
let paused: boolean;
|
||||||
|
|
||||||
|
const handle = <PausableHandle>on(
|
||||||
|
target,
|
||||||
|
type,
|
||||||
|
function(this: any) {
|
||||||
|
if (!paused) {
|
||||||
|
return listener.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
capture
|
||||||
|
);
|
||||||
|
|
||||||
|
handle.pause = function() {
|
||||||
|
paused = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.resume = function() {
|
||||||
|
paused = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
1
src/lib/core/src/queue.ts
Normal file
1
src/lib/core/src/queue.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@dojo/shim/support/queue';
|
||||||
66
src/lib/core/src/request.ts
Normal file
66
src/lib/core/src/request.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import has from '@dojo/has/has';
|
||||||
|
import Task from './async/Task';
|
||||||
|
import { RequestOptions, Response, Provider, UploadObservableTask } from './request/interfaces';
|
||||||
|
import ProviderRegistry from './request/ProviderRegistry';
|
||||||
|
import xhr from './request/providers/xhr';
|
||||||
|
|
||||||
|
export const providerRegistry = new ProviderRegistry();
|
||||||
|
|
||||||
|
const request: {
|
||||||
|
(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): UploadObservableTask<Response>;
|
||||||
|
put(url: string, options?: RequestOptions): UploadObservableTask<Response>;
|
||||||
|
|
||||||
|
setDefaultProvider(provider: Provider): void;
|
||||||
|
} = <any>function request(url: string, options: RequestOptions = {}): Task<Response> {
|
||||||
|
try {
|
||||||
|
return providerRegistry.match(url, options)(url, options);
|
||||||
|
} catch (error) {
|
||||||
|
return Task.reject<Response>(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
['DELETE', 'GET', 'HEAD', 'OPTIONS'].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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['POST', 'PUT'].forEach((method) => {
|
||||||
|
Object.defineProperty(request, method.toLowerCase(), {
|
||||||
|
value(url: string, options: RequestOptions = {}): UploadObservableTask<Response> {
|
||||||
|
options = Object.create(options);
|
||||||
|
options.method = method;
|
||||||
|
return <UploadObservableTask<Response>>request(url, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(request, 'setDefaultProvider', {
|
||||||
|
value(provider: Provider) {
|
||||||
|
providerRegistry.setDefaultProvider(provider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
providerRegistry.setDefaultProvider(xhr);
|
||||||
|
|
||||||
|
if (has('host-node')) {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
import('./request/providers/node').then((node) => {
|
||||||
|
providerRegistry.setDefaultProvider(node.default);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default request;
|
||||||
|
export * from './request/interfaces';
|
||||||
|
export { default as Headers } from './request/Headers';
|
||||||
|
export { default as TimeoutError } from './request/TimeoutError';
|
||||||
|
export { ResponseData } from './request/Response';
|
||||||
101
src/lib/core/src/request/Headers.ts
Normal file
101
src/lib/core/src/request/Headers.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Headers as HeadersInterface } from './interfaces';
|
||||||
|
import { IterableIterator, ShimIterator } from '@dojo/shim/iterator';
|
||||||
|
import Map from '@dojo/shim/Map';
|
||||||
|
|
||||||
|
function isHeadersLike(object: any): object is HeadersInterface {
|
||||||
|
return (
|
||||||
|
typeof object.append === 'function' &&
|
||||||
|
typeof object.entries === 'function' &&
|
||||||
|
typeof object[Symbol.iterator] === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Headers implements HeadersInterface {
|
||||||
|
protected map = new Map<string, string[]>();
|
||||||
|
|
||||||
|
constructor(headers?: { [key: string]: string } | HeadersInterface) {
|
||||||
|
if (headers) {
|
||||||
|
if (headers instanceof Headers) {
|
||||||
|
this.map = new Map(headers.map);
|
||||||
|
} else if (isHeadersLike(headers)) {
|
||||||
|
for (const [key, value] of headers) {
|
||||||
|
this.append(key, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let key in headers) {
|
||||||
|
this.set(key, headers[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
append(name: string, value: string) {
|
||||||
|
const values = this.map.get(name.toLowerCase());
|
||||||
|
|
||||||
|
if (values) {
|
||||||
|
values.push(value);
|
||||||
|
} else {
|
||||||
|
this.set(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(name: string) {
|
||||||
|
this.map.delete(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): IterableIterator<[string, string]> {
|
||||||
|
const entries: [string, string][] = [];
|
||||||
|
for (const [key, values] of this.map.entries()) {
|
||||||
|
values.forEach((value) => {
|
||||||
|
entries.push([key, value]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new ShimIterator(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string): string | null {
|
||||||
|
const values = this.map.get(name.toLowerCase());
|
||||||
|
|
||||||
|
if (values) {
|
||||||
|
return values[0];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(name: string): string[] {
|
||||||
|
const values = this.map.get(name.toLowerCase());
|
||||||
|
|
||||||
|
if (values) {
|
||||||
|
return values.slice(0);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has(name: string): boolean {
|
||||||
|
return this.map.has(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): IterableIterator<string> {
|
||||||
|
return this.map.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
set(name: string, value: string) {
|
||||||
|
this.map.set(name.toLowerCase(), [value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): IterableIterator<string> {
|
||||||
|
const values: string[] = [];
|
||||||
|
for (const value of this.map.values()) {
|
||||||
|
values.push(...value);
|
||||||
|
}
|
||||||
|
return new ShimIterator(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.entries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Headers;
|
||||||
27
src/lib/core/src/request/ProviderRegistry.ts
Normal file
27
src/lib/core/src/request/ProviderRegistry.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Provider, ProviderTest } from './interfaces';
|
||||||
|
import MatchRegistry, { Test } from '../MatchRegistry';
|
||||||
|
import { Handle } from '../interfaces';
|
||||||
|
|
||||||
|
export class ProviderRegistry extends MatchRegistry<Provider> {
|
||||||
|
setDefaultProvider(provider: Provider) {
|
||||||
|
this._defaultValue = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(test: string | RegExp | ProviderTest | null, value: Provider, first?: boolean): Handle {
|
||||||
|
let entryTest: Test | null;
|
||||||
|
|
||||||
|
if (typeof test === 'string') {
|
||||||
|
entryTest = (url, options) => test === url;
|
||||||
|
} else if (test instanceof RegExp) {
|
||||||
|
entryTest = (url, options) => {
|
||||||
|
return test ? test.test(url) : null;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
entryTest = test;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.register(entryTest, value, first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderRegistry;
|
||||||
75
src/lib/core/src/request/README.md
Normal file
75
src/lib/core/src/request/README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
## Features
|
||||||
|
|
||||||
|
This module allows the consumer to make HTTP requests using Node, XMLHttpRequest, or the Fetch API. The details of
|
||||||
|
these implementations are exposed through a common interface.
|
||||||
|
|
||||||
|
With this module you can,
|
||||||
|
|
||||||
|
* Easily make simple HTTP requests
|
||||||
|
* Convert response bodies to common formats like text, json, or html
|
||||||
|
* Access response headers of a request before the body is downloaded
|
||||||
|
* Monitor progress of a request
|
||||||
|
* Stream response data
|
||||||
|
|
||||||
|
## How do I use this package?
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
To make simple GET requests, you must register your provider (node, or XHR) then make the request. The overall
|
||||||
|
format of the API resembles the [Fetch Standard](https://fetch.spec.whatwg.org/).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import request from 'dojo-request';
|
||||||
|
import node from 'dojo-request/providers/node';
|
||||||
|
|
||||||
|
request.setDefaultProvider(node);
|
||||||
|
|
||||||
|
request.get('http://www.example.com').then(response => {
|
||||||
|
return response.text();
|
||||||
|
}).then(html => {
|
||||||
|
console.log(html);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Responses can be returned as an `ArrayBuffer`, `Blob`, XML document, JSON object, or text string.
|
||||||
|
|
||||||
|
You can also easily send request data,
|
||||||
|
|
||||||
|
```ts
|
||||||
|
request.post('http://www.example.com', {
|
||||||
|
body: 'request data here'
|
||||||
|
}).then(response => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Reading Response Headers
|
||||||
|
|
||||||
|
This approach allows for processing of response headers _before_ the response body is available.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
request.get('http://www.example.com').then(response => {
|
||||||
|
const expectedSize = Number(response.headers.get('content-length') || 0);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Events
|
||||||
|
|
||||||
|
`Response` objects also emit `start`, `end`, `progress`, and `data` events. You can use these events to monitor download progress
|
||||||
|
or stream a response directly to a file.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
request.get('http://www.example.com').then(response => {
|
||||||
|
response.on('progress', (event: ProgressEvent) => {
|
||||||
|
console.log(`Total downloaded: ${event.totalBytesDownloaded}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.blob();
|
||||||
|
}).then(blob => {
|
||||||
|
// do something with the data
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that there are some caveats when using these events. XHR cannot stream data (a final `data` event is sent at the end however).
|
||||||
69
src/lib/core/src/request/Response.ts
Normal file
69
src/lib/core/src/request/Response.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
import Task from '../async/Task';
|
||||||
|
import { Headers, Response as ResponseInterface, RequestOptions } from './interfaces';
|
||||||
|
import Observable from '../Observable';
|
||||||
|
|
||||||
|
export interface ResponseData {
|
||||||
|
task: Task<any>;
|
||||||
|
used: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Response implements ResponseInterface {
|
||||||
|
abstract readonly headers: Headers;
|
||||||
|
abstract readonly ok: boolean;
|
||||||
|
abstract readonly status: number;
|
||||||
|
abstract readonly statusText: string;
|
||||||
|
abstract readonly url: string;
|
||||||
|
abstract readonly bodyUsed: boolean;
|
||||||
|
abstract readonly requestOptions: RequestOptions;
|
||||||
|
|
||||||
|
abstract readonly download: Observable<number>;
|
||||||
|
abstract readonly data: Observable<any>;
|
||||||
|
|
||||||
|
json<T>(): Task<T> {
|
||||||
|
return <any>this.text().then(JSON.parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract arrayBuffer(): Task<ArrayBuffer>;
|
||||||
|
abstract blob(): Task<Blob>;
|
||||||
|
abstract formData(): Task<FormData>;
|
||||||
|
abstract text(): Task<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Response;
|
||||||
|
|
||||||
|
export function getFileReaderPromise<T>(reader: FileReader): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reader.onload = function() {
|
||||||
|
resolve(reader.result);
|
||||||
|
};
|
||||||
|
reader.onerror = function() {
|
||||||
|
reject(reader.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTextFromBlob(blob: Blob) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const promise = getFileReaderPromise<string>(reader);
|
||||||
|
reader.readAsText(blob);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getArrayBufferFromBlob(blob: Blob) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const promise = getFileReaderPromise<ArrayBuffer>(reader);
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTextFromArrayBuffer(buffer: ArrayBuffer) {
|
||||||
|
const view = new Uint8Array(buffer);
|
||||||
|
const chars: string[] = [];
|
||||||
|
|
||||||
|
view.forEach((charCode, index) => {
|
||||||
|
chars[index] = String.fromCharCode(charCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return chars.join('');
|
||||||
|
}
|
||||||
46
src/lib/core/src/request/SubscriptionPool.ts
Normal file
46
src/lib/core/src/request/SubscriptionPool.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { SubscriptionObserver } from '@dojo/shim/Observable';
|
||||||
|
|
||||||
|
export class SubscriptionPool<T> {
|
||||||
|
private _observers: SubscriptionObserver<T>[] = [];
|
||||||
|
private _queue: T[] = [];
|
||||||
|
private _queueMaxLength: number;
|
||||||
|
|
||||||
|
constructor(maxLength = 10) {
|
||||||
|
this._queueMaxLength = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(subscription: SubscriptionObserver<T>) {
|
||||||
|
this._observers.push(subscription);
|
||||||
|
|
||||||
|
while (this._queue.length > 0) {
|
||||||
|
this.next(this._queue.shift()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this._observers.splice(this._observers.indexOf(subscription), 1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
next(value: T) {
|
||||||
|
if (this._observers.length === 0) {
|
||||||
|
this._queue.push(value);
|
||||||
|
|
||||||
|
// when the queue is full, get rid of the first ones
|
||||||
|
while (this._queue.length > this._queueMaxLength) {
|
||||||
|
this._queue.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._observers.forEach((observer) => {
|
||||||
|
observer.next(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
this._observers.forEach((observer) => {
|
||||||
|
observer.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SubscriptionPool;
|
||||||
14
src/lib/core/src/request/TimeoutError.ts
Normal file
14
src/lib/core/src/request/TimeoutError.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export class TimeoutError implements Error {
|
||||||
|
readonly message: string;
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return 'TimeoutError';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(message?: string) {
|
||||||
|
message = message || 'The request timed out';
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeoutError;
|
||||||
84
src/lib/core/src/request/interfaces.d.ts
vendored
Normal file
84
src/lib/core/src/request/interfaces.d.ts
vendored
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { IterableIterator } from '@dojo/shim/iterator';
|
||||||
|
import Task from '../async/Task';
|
||||||
|
import UrlSearchParams, { ParamList } from '../UrlSearchParams';
|
||||||
|
import Observable from '../Observable';
|
||||||
|
|
||||||
|
export interface Body {
|
||||||
|
readonly bodyUsed: boolean;
|
||||||
|
|
||||||
|
arrayBuffer(): Task<ArrayBuffer>;
|
||||||
|
blob(): Task<Blob>;
|
||||||
|
formData(): Task<FormData>;
|
||||||
|
json<T>(): Task<T>;
|
||||||
|
text(): Task<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Headers {
|
||||||
|
append(name: string, value: string): void;
|
||||||
|
delete(name: string): void;
|
||||||
|
entries(): IterableIterator<[string, string]>;
|
||||||
|
get(name: string): string | null;
|
||||||
|
getAll(name: string): string[];
|
||||||
|
has(name: string): boolean;
|
||||||
|
keys(): IterableIterator<string>;
|
||||||
|
set(name: string, value: string): void;
|
||||||
|
values(): IterableIterator<string>;
|
||||||
|
[Symbol.iterator](): IterableIterator<[string, string]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadObservableTask<T> extends Task<T> {
|
||||||
|
upload: Observable<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Provider = (url: string, options?: RequestOptions) => UploadObservableTask<Response>;
|
||||||
|
|
||||||
|
export type ProviderTest = (url: string, options?: RequestOptions) => boolean | null;
|
||||||
|
|
||||||
|
export interface RequestOptions {
|
||||||
|
/**
|
||||||
|
* Enable cache busting (default false). Cache busting will make a new URL by appending a parameter to the
|
||||||
|
* requested URL
|
||||||
|
*/
|
||||||
|
cacheBust?: boolean;
|
||||||
|
credentials?: 'omit' | 'same-origin' | 'include';
|
||||||
|
/**
|
||||||
|
* Body to send along with the http request
|
||||||
|
*/
|
||||||
|
body?: Blob | BufferSource | FormData | UrlSearchParams | string;
|
||||||
|
/**
|
||||||
|
* Headers to send along with the http request
|
||||||
|
*/
|
||||||
|
headers?: Headers | { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* HTTP method
|
||||||
|
*/
|
||||||
|
method?: string;
|
||||||
|
/**
|
||||||
|
* Password for HTTP authentication
|
||||||
|
*/
|
||||||
|
password?: string;
|
||||||
|
/**
|
||||||
|
* Number of milliseconds before the request times out and is canceled
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
/**
|
||||||
|
* User for HTTP authentication
|
||||||
|
*/
|
||||||
|
user?: string;
|
||||||
|
/**
|
||||||
|
* Optional query parameter(s) for the URL. The requested url will have these query parameters appended.
|
||||||
|
*/
|
||||||
|
query?: string | ParamList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response extends Body {
|
||||||
|
readonly headers: Headers;
|
||||||
|
readonly ok: boolean;
|
||||||
|
readonly status: number;
|
||||||
|
readonly statusText: string;
|
||||||
|
readonly url: string;
|
||||||
|
readonly requestOptions: RequestOptions;
|
||||||
|
|
||||||
|
readonly download: Observable<number>;
|
||||||
|
readonly data: Observable<any>;
|
||||||
|
}
|
||||||
677
src/lib/core/src/request/providers/node.ts
Normal file
677
src/lib/core/src/request/providers/node.ts
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
import { Handle } from '../../interfaces';
|
||||||
|
import Set from '@dojo/shim/Set';
|
||||||
|
import WeakMap from '@dojo/shim/WeakMap';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
|
import * as urlUtil from 'url';
|
||||||
|
import * as zlib from 'zlib';
|
||||||
|
import Task from '../../async/Task';
|
||||||
|
import { deepAssign } from '../../lang';
|
||||||
|
import { queueTask } from '../../queue';
|
||||||
|
import { createTimer } from '../../util';
|
||||||
|
import Headers from '../Headers';
|
||||||
|
import { RequestOptions, UploadObservableTask } from '../interfaces';
|
||||||
|
import Response from '../Response';
|
||||||
|
import TimeoutError from '../TimeoutError';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import Observable from '../../Observable';
|
||||||
|
import SubscriptionPool from '../SubscriptionPool';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request options specific to a node request. For HTTPS options, see
|
||||||
|
* https://nodejs.org/api/tls.html#tls_tls_connect_options_callback for more details.
|
||||||
|
*/
|
||||||
|
export interface NodeRequestOptions extends RequestOptions {
|
||||||
|
/**
|
||||||
|
* User-agent header
|
||||||
|
*/
|
||||||
|
agent?: any;
|
||||||
|
/**
|
||||||
|
* If specified, the request body is read from the stream specified here, rather than from the `body` field.
|
||||||
|
*/
|
||||||
|
bodyStream?: Readable;
|
||||||
|
/**
|
||||||
|
* HTTPS optionally override the trusted CA certificates
|
||||||
|
*/
|
||||||
|
ca?: any;
|
||||||
|
/**
|
||||||
|
* HTTPS optional cert chains in PEM format. One cert chain should be provided per private key.
|
||||||
|
*/
|
||||||
|
cert?: string;
|
||||||
|
/**
|
||||||
|
* HTTPS optional cipher suite specification
|
||||||
|
*/
|
||||||
|
ciphers?: string;
|
||||||
|
dataEncoding?: string;
|
||||||
|
/**
|
||||||
|
* Whether or not to automatically follow redirects (default true)
|
||||||
|
*/
|
||||||
|
followRedirects?: boolean;
|
||||||
|
/**
|
||||||
|
* HTTPS optional private key in PEM format.
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
/**
|
||||||
|
* Local interface to bind for network connections.
|
||||||
|
*/
|
||||||
|
localAddress?: string;
|
||||||
|
/**
|
||||||
|
* HTTPS optional shared passphrase used for a single private key and/or a PFX.
|
||||||
|
*/
|
||||||
|
passphrase?: string;
|
||||||
|
/**
|
||||||
|
* HTTPS optional PFX or PKCS12 encoded private key and certificate chain.
|
||||||
|
*/
|
||||||
|
pfx?: any;
|
||||||
|
/**
|
||||||
|
* Optional proxy address. If specified, requests will be sent through this url.
|
||||||
|
*/
|
||||||
|
proxy?: string;
|
||||||
|
/**
|
||||||
|
* HTTPS If not false the server will reject any connection which is not authorized with the list of supplied CAs
|
||||||
|
*/
|
||||||
|
rejectUnauthorized?: boolean;
|
||||||
|
/**
|
||||||
|
* HTTPS optional SSL method to use, default is "SSLv23_method"
|
||||||
|
*/
|
||||||
|
secureProtocol?: string;
|
||||||
|
/**
|
||||||
|
* Unix Domain Socket (use one of host:port or socketPath)
|
||||||
|
*/
|
||||||
|
socketPath?: string;
|
||||||
|
/**
|
||||||
|
* Whether or not to add the gzip and deflate accept headers (default true)
|
||||||
|
*/
|
||||||
|
acceptCompression?: boolean;
|
||||||
|
/**
|
||||||
|
* A set of options to set on the HTTP request
|
||||||
|
*/
|
||||||
|
socketOptions?: {
|
||||||
|
/**
|
||||||
|
* Enable/disable keep-alive functionality, and optionally set the initial delay before the first keepalive probe is sent on an idle socket.
|
||||||
|
*/
|
||||||
|
keepAlive?: number;
|
||||||
|
/**
|
||||||
|
* Disables the Nagle algorithm. By default TCP connections use the Nagle algorithm, they buffer data before sending it off.
|
||||||
|
*/
|
||||||
|
noDelay?: boolean;
|
||||||
|
/**
|
||||||
|
* Number of milliseconds before the HTTP request times out
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Stream encoding on incoming HTTP response
|
||||||
|
*/
|
||||||
|
streamEncoding?: string;
|
||||||
|
/**
|
||||||
|
* Options to control redirect follow logic
|
||||||
|
*/
|
||||||
|
redirectOptions?: {
|
||||||
|
/**
|
||||||
|
* The limit to the number of redirects that will be followed (default 15). This is used to prevent infinite
|
||||||
|
* redirect loops.
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
count?: number;
|
||||||
|
/**
|
||||||
|
* Whether or not to keep the original HTTP method during 301 redirects (default false).
|
||||||
|
*/
|
||||||
|
keepOriginalMethod?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should be read from the package and not hard coded!
|
||||||
|
let version = '2.0.0-pre';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If not overridden, redirects will only be processed this many times before aborting (per request).
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
const DEFAULT_REDIRECT_LIMIT = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to be passed to node's request
|
||||||
|
*/
|
||||||
|
interface Options {
|
||||||
|
agent?: any;
|
||||||
|
auth?: string;
|
||||||
|
headers?: { [name: string]: string };
|
||||||
|
host?: string;
|
||||||
|
hostname?: string;
|
||||||
|
localAddress?: string;
|
||||||
|
method?: string;
|
||||||
|
path?: string;
|
||||||
|
port?: number;
|
||||||
|
socketPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTPS specific options for node
|
||||||
|
*/
|
||||||
|
interface HttpsOptions extends Options {
|
||||||
|
ca?: any;
|
||||||
|
cert?: string;
|
||||||
|
ciphers?: string;
|
||||||
|
key?: string;
|
||||||
|
passphrase?: string;
|
||||||
|
pfx?: any;
|
||||||
|
rejectUnauthorized?: boolean;
|
||||||
|
secureProtocol?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestData {
|
||||||
|
task: Task<http.IncomingMessage>;
|
||||||
|
buffer: any[];
|
||||||
|
data: Buffer;
|
||||||
|
size: number;
|
||||||
|
used: boolean;
|
||||||
|
nativeResponse: http.IncomingMessage;
|
||||||
|
requestOptions: NodeRequestOptions;
|
||||||
|
url: string;
|
||||||
|
downloadObservable: Observable<number>;
|
||||||
|
dataObservable: Observable<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataMap = new WeakMap<NodeResponse, RequestData>();
|
||||||
|
const discardedDuplicates = new Set<string>([
|
||||||
|
'age',
|
||||||
|
'authorization',
|
||||||
|
'content-length',
|
||||||
|
'content-type',
|
||||||
|
'etag',
|
||||||
|
'expires',
|
||||||
|
'from',
|
||||||
|
'host',
|
||||||
|
'if-modified-since',
|
||||||
|
'if-unmodified-since',
|
||||||
|
'last-modified',
|
||||||
|
'location',
|
||||||
|
'max-forwards',
|
||||||
|
'proxy-authorization',
|
||||||
|
'referer',
|
||||||
|
'retry-after',
|
||||||
|
'user-agent'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getDataTask(response: NodeResponse): Task<RequestData> {
|
||||||
|
const data = dataMap.get(response)!;
|
||||||
|
|
||||||
|
if (data.used) {
|
||||||
|
return Task.reject<any>(new TypeError('Body already read'));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.used = true;
|
||||||
|
|
||||||
|
return <Task<RequestData>>data.task.then((_) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a node native response object into something that resembles the fetch api
|
||||||
|
*/
|
||||||
|
export class NodeResponse extends Response {
|
||||||
|
readonly headers: Headers;
|
||||||
|
readonly ok: boolean;
|
||||||
|
readonly status: number;
|
||||||
|
readonly statusText: string;
|
||||||
|
|
||||||
|
downloadBody = true;
|
||||||
|
|
||||||
|
get bodyUsed(): boolean {
|
||||||
|
return dataMap.get(this)!.used;
|
||||||
|
}
|
||||||
|
|
||||||
|
get nativeResponse(): http.IncomingMessage {
|
||||||
|
return dataMap.get(this)!.nativeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
get requestOptions(): NodeRequestOptions {
|
||||||
|
return dataMap.get(this)!.requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return dataMap.get(this)!.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
get download(): Observable<number> {
|
||||||
|
return dataMap.get(this)!.downloadObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data(): Observable<any> {
|
||||||
|
return dataMap.get(this)!.dataObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(response: http.IncomingMessage) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const headers = (this.headers = new Headers());
|
||||||
|
for (let key in response.headers) {
|
||||||
|
const value = response.headers[key];
|
||||||
|
if (value) {
|
||||||
|
if (discardedDuplicates.has(key) && !Array.isArray(value)) {
|
||||||
|
headers.append(key, value);
|
||||||
|
}
|
||||||
|
(Array.isArray(value) ? value : value.split(/\s*,\s*/)).forEach((v) => {
|
||||||
|
headers.append(key, v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = response.statusCode || 0;
|
||||||
|
this.ok = this.status >= 200 && this.status < 300;
|
||||||
|
this.statusText = response.statusMessage || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayBuffer(): Task<ArrayBuffer> {
|
||||||
|
return <any>getDataTask(this).then((data) => {
|
||||||
|
if (data) {
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Buffer([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
blob(): Task<Blob> {
|
||||||
|
// Node doesn't support Blobs
|
||||||
|
return Task.reject<Blob>(new Error('Blob not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
formData(): Task<FormData> {
|
||||||
|
return Task.reject<FormData>(new Error('FormData not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
text(): Task<string> {
|
||||||
|
return <any>getDataTask(this).then((data) => {
|
||||||
|
return String(data ? data.data : '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect(
|
||||||
|
resolve: (p?: any) => void,
|
||||||
|
reject: (_?: Error) => void,
|
||||||
|
originalUrl: string,
|
||||||
|
redirectUrl: string | null,
|
||||||
|
options: NodeRequestOptions
|
||||||
|
): boolean {
|
||||||
|
if (!options.redirectOptions) {
|
||||||
|
options.redirectOptions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { limit: redirectLimit = DEFAULT_REDIRECT_LIMIT, count: redirectCount = 0 } = options.redirectOptions;
|
||||||
|
const { followRedirects = true } = options;
|
||||||
|
|
||||||
|
if (!followRedirects) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only check for undefined here because empty string redirects are now allowed
|
||||||
|
// (they'll resolve to the current url)
|
||||||
|
if (redirectUrl === undefined || redirectUrl === null) {
|
||||||
|
reject(new Error('asked to redirect but no location header was found'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectCount > redirectLimit) {
|
||||||
|
reject(new Error(`too many redirects, limit reached at ${redirectLimit}`));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.redirectOptions.count = redirectCount + 1;
|
||||||
|
|
||||||
|
// we wrap the url in a call to node's URL.resolve which will handle relative and partial
|
||||||
|
// redirects (like "/another-page" on the same domain).
|
||||||
|
resolve(node(urlUtil.resolve(originalUrl, redirectUrl), options));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuth(proxyAuth: string | undefined, options: NodeRequestOptions): string | undefined {
|
||||||
|
if (proxyAuth) {
|
||||||
|
return proxyAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.user || options.password) {
|
||||||
|
return `${options.user || ''}:${options.password || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function node(url: string, options: NodeRequestOptions = {}): UploadObservableTask<NodeResponse> {
|
||||||
|
const parsedUrl = urlUtil.parse(options.proxy || url);
|
||||||
|
|
||||||
|
const requestOptions: HttpsOptions = {
|
||||||
|
agent: options.agent,
|
||||||
|
auth: getAuth(parsedUrl.auth, options),
|
||||||
|
ca: options.ca,
|
||||||
|
cert: options.cert,
|
||||||
|
ciphers: options.ciphers,
|
||||||
|
host: parsedUrl.host,
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
key: options.key,
|
||||||
|
localAddress: options.localAddress,
|
||||||
|
method: options.method ? options.method.toUpperCase() : 'GET',
|
||||||
|
passphrase: options.passphrase,
|
||||||
|
path: parsedUrl.path,
|
||||||
|
pfx: options.pfx,
|
||||||
|
port: Number(parsedUrl.port),
|
||||||
|
rejectUnauthorized: options.rejectUnauthorized,
|
||||||
|
secureProtocol: options.secureProtocol,
|
||||||
|
socketPath: options.socketPath
|
||||||
|
};
|
||||||
|
|
||||||
|
requestOptions.headers = <{ [key: string]: string }>options.headers || {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Object.keys(requestOptions.headers)
|
||||||
|
.map((headerName) => headerName.toLowerCase())
|
||||||
|
.some((headerName) => headerName === 'user-agent')
|
||||||
|
) {
|
||||||
|
requestOptions.headers['user-agent'] = 'dojo/' + version + ' Node.js/' + process.version.replace(/^v/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.proxy) {
|
||||||
|
requestOptions.path = url;
|
||||||
|
if (parsedUrl.auth) {
|
||||||
|
requestOptions.headers['proxy-authorization'] = 'Basic ' + new Buffer(parsedUrl.auth).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedProxyUrl = urlUtil.parse(url);
|
||||||
|
if (parsedProxyUrl.host) {
|
||||||
|
requestOptions.headers['host'] = parsedProxyUrl.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedProxyUrl.auth) {
|
||||||
|
requestOptions.auth = parsedProxyUrl.auth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { acceptCompression = true } = options;
|
||||||
|
if (acceptCompression) {
|
||||||
|
requestOptions.headers['Accept-Encoding'] = 'gzip, deflate';
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = parsedUrl.protocol === 'https:' ? https.request(requestOptions) : http.request(requestOptions);
|
||||||
|
|
||||||
|
const uploadObserverPool = new SubscriptionPool<number>();
|
||||||
|
|
||||||
|
const requestTask = <UploadObservableTask<NodeResponse>>new Task<NodeResponse>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
let timeoutHandle: Handle;
|
||||||
|
let timeoutReject: Function = reject;
|
||||||
|
|
||||||
|
if (options.socketOptions) {
|
||||||
|
if (options.socketOptions.timeout) {
|
||||||
|
request.setTimeout(options.socketOptions.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('noDelay' in options.socketOptions) {
|
||||||
|
request.setNoDelay(options.socketOptions.noDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('keepAlive' in options.socketOptions) {
|
||||||
|
const initialDelay: number | undefined = options.socketOptions.keepAlive;
|
||||||
|
if (initialDelay !== undefined) {
|
||||||
|
request.setSocketKeepAlive(initialDelay >= 0, initialDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.once('response', (message: http.IncomingMessage) => {
|
||||||
|
const response = new NodeResponse(message);
|
||||||
|
|
||||||
|
// Redirection handling defaults to true in order to harmonise with the XHR provider, which will always
|
||||||
|
// follow redirects
|
||||||
|
if (response.status >= 300 && response.status < 400) {
|
||||||
|
const redirectOptions = options.redirectOptions || {};
|
||||||
|
const newOptions = deepAssign({}, options);
|
||||||
|
|
||||||
|
switch (response.status) {
|
||||||
|
case 300:
|
||||||
|
/**
|
||||||
|
* Note about 300 redirects. RFC 2616 doesn't specify what to do with them, it is up to the client to "pick
|
||||||
|
* the right one". We're picking like Chrome does, just don't pick any.
|
||||||
|
*/
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 301:
|
||||||
|
case 302:
|
||||||
|
/**
|
||||||
|
* RFC 2616 says,
|
||||||
|
*
|
||||||
|
* If the 301 status code is received in response to a request other
|
||||||
|
* than GET or HEAD, the user agent MUST NOT automatically redirect the
|
||||||
|
* request unless it can be confirmed by the user, since this might
|
||||||
|
* change the conditions under which the request was issued.
|
||||||
|
*
|
||||||
|
* Note: When automatically redirecting a POST request after
|
||||||
|
* receiving a 301 status code, some existing HTTP/1.0 user agents
|
||||||
|
* will erroneously change it into a GET request.
|
||||||
|
*
|
||||||
|
* We're going to be one of those erroneous agents, to prevent the request from failing..
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
requestOptions.method !== 'GET' &&
|
||||||
|
requestOptions.method !== 'HEAD' &&
|
||||||
|
!redirectOptions.keepOriginalMethod
|
||||||
|
) {
|
||||||
|
newOptions.method = 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirect(resolve, reject, url, response.headers.get('location'), newOptions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 303:
|
||||||
|
/**
|
||||||
|
* The response to the request can be found under a different URI and
|
||||||
|
* SHOULD be retrieved using a GET method on that resource.
|
||||||
|
*/
|
||||||
|
if (requestOptions.method !== 'GET') {
|
||||||
|
newOptions.method = 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirect(resolve, reject, url, response.headers.get('location'), newOptions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 304:
|
||||||
|
// do nothing so this can fall through and return the response as normal. Nothing more can
|
||||||
|
// be done for 304
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 305:
|
||||||
|
if (!response.headers.get('location')) {
|
||||||
|
reject(new Error('expected Location header to contain a proxy url'));
|
||||||
|
} else {
|
||||||
|
newOptions.proxy = response.headers.get('location') || '';
|
||||||
|
if (redirect(resolve, reject, url, '', newOptions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 307:
|
||||||
|
/**
|
||||||
|
* If the 307 status code is received in response to a request other
|
||||||
|
* than GET or HEAD, the user agent MUST NOT automatically redirect the
|
||||||
|
* request unless it can be confirmed by the user, since this might
|
||||||
|
* change the conditions under which the request was issued.
|
||||||
|
*/
|
||||||
|
if (redirect(resolve, reject, url, response.headers.get('location'), newOptions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
reject(new Error('unhandled redirect status ' + response.status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.streamEncoding && message.setEncoding(options.streamEncoding);
|
||||||
|
|
||||||
|
const downloadSubscriptionPool = new SubscriptionPool<number>();
|
||||||
|
const dataSubscriptionPool = new SubscriptionPool<any>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
[RFC 2616](https://tools.ietf.org/html/rfc2616#page-118) says that content-encoding can have multiple
|
||||||
|
values, so we split them here and put them in a list to process later.
|
||||||
|
*/
|
||||||
|
const contentEncodings = response.headers.getAll('content-encoding');
|
||||||
|
|
||||||
|
const task = new Task<http.IncomingMessage>(
|
||||||
|
(resolve, reject) => {
|
||||||
|
timeoutReject = reject;
|
||||||
|
|
||||||
|
// we queue this up for later to allow listeners to register themselves before we start receiving data
|
||||||
|
queueTask(() => {
|
||||||
|
/*
|
||||||
|
* Note that this is the raw data, if your input stream is zipped, and you are piecing
|
||||||
|
* together the downloaded data, you'll have to decompress it yourself
|
||||||
|
*/
|
||||||
|
message.on('data', (chunk: any) => {
|
||||||
|
dataSubscriptionPool.next(chunk);
|
||||||
|
|
||||||
|
if (response.downloadBody) {
|
||||||
|
data.buffer.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.size +=
|
||||||
|
typeof chunk === 'string'
|
||||||
|
? Buffer.byteLength(chunk, options.streamEncoding)
|
||||||
|
: chunk.length;
|
||||||
|
|
||||||
|
downloadSubscriptionPool.next(data.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
message.once('end', () => {
|
||||||
|
timeoutHandle && timeoutHandle.destroy();
|
||||||
|
|
||||||
|
let dataAsBuffer = options.streamEncoding
|
||||||
|
? new Buffer(data.buffer.join(''), 'utf8')
|
||||||
|
: Buffer.concat(data.buffer, data.size);
|
||||||
|
|
||||||
|
const handleEncoding = function() {
|
||||||
|
/*
|
||||||
|
Content encoding is ordered by the order in which they were applied to the
|
||||||
|
content, so do undo the encoding we have to start at the end and work backwards.
|
||||||
|
*/
|
||||||
|
if (contentEncodings.length) {
|
||||||
|
const encoding = contentEncodings.pop()!.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (encoding === '' || encoding === 'none' || encoding === 'identity') {
|
||||||
|
// do nothing, response stream is as-is
|
||||||
|
handleEncoding();
|
||||||
|
} else if (encoding === 'gzip') {
|
||||||
|
zlib.gunzip(dataAsBuffer, function(err: Error | null, result: Buffer) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataAsBuffer = result;
|
||||||
|
handleEncoding();
|
||||||
|
});
|
||||||
|
} else if (encoding === 'deflate') {
|
||||||
|
zlib.inflate(dataAsBuffer, function(err: Error | null, result: Buffer) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataAsBuffer = result;
|
||||||
|
handleEncoding();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error('Unsupported content encoding, ' + encoding));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.data = dataAsBuffer;
|
||||||
|
|
||||||
|
resolve(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEncoding();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
request.abort();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: RequestData = {
|
||||||
|
task,
|
||||||
|
buffer: [],
|
||||||
|
data: Buffer.alloc(0),
|
||||||
|
size: 0,
|
||||||
|
used: false,
|
||||||
|
url: url,
|
||||||
|
requestOptions: options,
|
||||||
|
nativeResponse: message,
|
||||||
|
downloadObservable: new Observable<number>((observer) => downloadSubscriptionPool.add(observer)),
|
||||||
|
dataObservable: new Observable<any>((observer) => dataSubscriptionPool.add(observer))
|
||||||
|
};
|
||||||
|
|
||||||
|
dataMap.set(response, data);
|
||||||
|
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.once('error', reject);
|
||||||
|
|
||||||
|
if (options.bodyStream) {
|
||||||
|
options.bodyStream.pipe(request);
|
||||||
|
let uploadedSize = 0;
|
||||||
|
|
||||||
|
options.bodyStream.on('data', (chunk: any) => {
|
||||||
|
uploadedSize += chunk.length;
|
||||||
|
uploadObserverPool.next(uploadedSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
options.bodyStream.on('end', () => {
|
||||||
|
uploadObserverPool.complete();
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
} else if (options.body) {
|
||||||
|
const body = options.body.toString();
|
||||||
|
|
||||||
|
request.on('response', () => {
|
||||||
|
uploadObserverPool.next(body.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.end(body);
|
||||||
|
} else {
|
||||||
|
request.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.timeout && options.timeout > 0 && options.timeout !== Infinity) {
|
||||||
|
timeoutHandle = createTimer(() => {
|
||||||
|
timeoutReject && timeoutReject(new TimeoutError('The request timed out'));
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
request.abort();
|
||||||
|
}
|
||||||
|
).catch(function(error: Error): any {
|
||||||
|
const parsedUrl = urlUtil.parse(url);
|
||||||
|
|
||||||
|
if (parsedUrl.auth) {
|
||||||
|
parsedUrl.auth = '(redacted)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedUrl = urlUtil.format(parsedUrl);
|
||||||
|
|
||||||
|
error.message = '[' + requestOptions.method + ' ' + sanitizedUrl + '] ' + error.message;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
requestTask.upload = new Observable<number>((observer) => uploadObserverPool.add(observer));
|
||||||
|
|
||||||
|
return requestTask;
|
||||||
|
}
|
||||||
333
src/lib/core/src/request/providers/xhr.ts
Normal file
333
src/lib/core/src/request/providers/xhr.ts
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import { Handle } from '../../interfaces';
|
||||||
|
import global from '@dojo/shim/global';
|
||||||
|
import WeakMap from '@dojo/shim/WeakMap';
|
||||||
|
import Task, { State } from '../../async/Task';
|
||||||
|
import has from '../../has';
|
||||||
|
import Observable from '../../Observable';
|
||||||
|
import { createTimer } from '../../util';
|
||||||
|
import Headers from '../Headers';
|
||||||
|
import { RequestOptions, UploadObservableTask } from '../interfaces';
|
||||||
|
import Response, { getArrayBufferFromBlob, getTextFromBlob } from '../Response';
|
||||||
|
import SubscriptionPool from '../SubscriptionPool';
|
||||||
|
import TimeoutError from '../TimeoutError';
|
||||||
|
import { generateRequestUrl } from '../util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request options specific to an XHR request
|
||||||
|
*/
|
||||||
|
export interface XhrRequestOptions extends RequestOptions {
|
||||||
|
/**
|
||||||
|
* Controls whether or not the request is synchronous (blocks the main thread) or asynchronous (default).
|
||||||
|
*/
|
||||||
|
blockMainThread?: boolean;
|
||||||
|
/**
|
||||||
|
* Controls whether or not the X-Requested-With header is added to the request (default true). Set to false to not
|
||||||
|
* include the header.
|
||||||
|
*/
|
||||||
|
includeRequestedWithHeader?: boolean;
|
||||||
|
/**
|
||||||
|
* Controls whether or not to subscribe to events on `XMLHttpRequest.upload`, if available. This causes all requests
|
||||||
|
* to be preflighted (https://xhr.spec.whatwg.org/#request)
|
||||||
|
*/
|
||||||
|
includeUploadProgress?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestData {
|
||||||
|
task: Task<XMLHttpRequest>;
|
||||||
|
used: boolean;
|
||||||
|
requestOptions: XhrRequestOptions;
|
||||||
|
nativeResponse: XMLHttpRequest;
|
||||||
|
url: string;
|
||||||
|
downloadObservable: Observable<number>;
|
||||||
|
dataObservable: Observable<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataMap = new WeakMap<XhrResponse, RequestData>();
|
||||||
|
|
||||||
|
function getDataTask(response: XhrResponse): Task<XMLHttpRequest> {
|
||||||
|
const data = dataMap.get(response)!;
|
||||||
|
|
||||||
|
if (data.used) {
|
||||||
|
return Task.reject<any>(new TypeError('Body already read'));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.used = true;
|
||||||
|
|
||||||
|
return data.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an XHR request in a response that mimics the fetch API
|
||||||
|
*/
|
||||||
|
export class XhrResponse extends Response {
|
||||||
|
readonly headers: Headers;
|
||||||
|
readonly ok: boolean;
|
||||||
|
readonly status: number;
|
||||||
|
readonly statusText: string;
|
||||||
|
|
||||||
|
get bodyUsed(): boolean {
|
||||||
|
return dataMap.get(this)!.used;
|
||||||
|
}
|
||||||
|
|
||||||
|
get nativeResponse(): XMLHttpRequest {
|
||||||
|
return dataMap.get(this)!.nativeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
get requestOptions(): XhrRequestOptions {
|
||||||
|
return dataMap.get(this)!.requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return dataMap.get(this)!.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
get download(): Observable<number> {
|
||||||
|
return dataMap.get(this)!.downloadObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data(): Observable<any> {
|
||||||
|
return dataMap.get(this)!.dataObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(request: XMLHttpRequest) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const headers = (this.headers = new Headers());
|
||||||
|
|
||||||
|
const responseHeaders = request.getAllResponseHeaders();
|
||||||
|
if (responseHeaders) {
|
||||||
|
const lines = responseHeaders.split(/\r\n/g);
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const match = lines[i].match(/^(.*?): (.*)$/);
|
||||||
|
if (match) {
|
||||||
|
headers.append(match[1], match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = request.status;
|
||||||
|
this.ok = this.status >= 200 && this.status < 300;
|
||||||
|
this.statusText = request.statusText || 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayBuffer(): Task<ArrayBuffer> {
|
||||||
|
return Task.reject<ArrayBuffer>(new Error('ArrayBuffer not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
blob(): Task<Blob> {
|
||||||
|
return Task.reject<Blob>(new Error('Blob not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
formData(): Task<FormData> {
|
||||||
|
return Task.reject<FormData>(new Error('FormData not supported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
text(): Task<string> {
|
||||||
|
return <any>getDataTask(this).then((request: XMLHttpRequest) => {
|
||||||
|
return String(request.responseText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
xml(): Task<Document> {
|
||||||
|
return <any>this.text().then((text: string) => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
return parser.parseFromString(text, this.headers.get('content-type') || 'text/html');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has('blob')) {
|
||||||
|
XhrResponse.prototype.blob = function(this: XhrResponse): Task<Blob> {
|
||||||
|
return <any>getDataTask(this).then((request: XMLHttpRequest) => request.response);
|
||||||
|
};
|
||||||
|
|
||||||
|
XhrResponse.prototype.text = function(this: XhrResponse): Task<string> {
|
||||||
|
return <any>this.blob().then(getTextFromBlob);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (has('arraybuffer')) {
|
||||||
|
XhrResponse.prototype.arrayBuffer = function(this: XhrResponse): Task<ArrayBuffer> {
|
||||||
|
return <any>this.blob().then(getArrayBufferFromBlob);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has('formdata')) {
|
||||||
|
XhrResponse.prototype.formData = function(this: XhrResponse): Task<FormData> {
|
||||||
|
return <any>this.text().then((text: string) => {
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
text
|
||||||
|
.trim()
|
||||||
|
.split('&')
|
||||||
|
.forEach((keyValues) => {
|
||||||
|
if (keyValues) {
|
||||||
|
const pairs = keyValues.split('=');
|
||||||
|
const name = (pairs.shift() || '').replace(/\+/, ' ');
|
||||||
|
const value = pairs.join('=').replace(/\+/, ' ');
|
||||||
|
|
||||||
|
data.append(decodeURIComponent(name), decodeURIComponent(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
function setOnError(request: XMLHttpRequest, reject: Function) {
|
||||||
|
request.addEventListener('error', function(event) {
|
||||||
|
reject(new TypeError(event.error || 'Network request failed'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function xhr(url: string, options: XhrRequestOptions = {}): UploadObservableTask<XhrResponse> {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
const requestUrl = generateRequestUrl(url, options);
|
||||||
|
|
||||||
|
options = Object.create(options);
|
||||||
|
|
||||||
|
if (!options.method) {
|
||||||
|
options.method = 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAborted = false;
|
||||||
|
|
||||||
|
function abort() {
|
||||||
|
isAborted = true;
|
||||||
|
if (request) {
|
||||||
|
request.abort();
|
||||||
|
request.onreadystatechange = noop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutHandle: Handle;
|
||||||
|
let timeoutReject: Function;
|
||||||
|
|
||||||
|
const task = <UploadObservableTask<XhrResponse>>new Task<XhrResponse>((resolve, reject) => {
|
||||||
|
timeoutReject = reject;
|
||||||
|
|
||||||
|
request.onreadystatechange = function() {
|
||||||
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.readyState === 2) {
|
||||||
|
const response = new XhrResponse(request);
|
||||||
|
|
||||||
|
const downloadSubscriptionPool = new SubscriptionPool<number>();
|
||||||
|
const dataSubscriptionPool = new SubscriptionPool<any>();
|
||||||
|
|
||||||
|
const task = new Task<XMLHttpRequest>((resolve, reject) => {
|
||||||
|
timeoutReject = reject;
|
||||||
|
|
||||||
|
request.onprogress = function(event: any) {
|
||||||
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSubscriptionPool.next(event.loaded);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onreadystatechange = function() {
|
||||||
|
if (isAborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.readyState === 4) {
|
||||||
|
request.onreadystatechange = noop;
|
||||||
|
timeoutHandle && timeoutHandle.destroy();
|
||||||
|
|
||||||
|
dataSubscriptionPool.next(request.response);
|
||||||
|
dataSubscriptionPool.complete();
|
||||||
|
|
||||||
|
resolve(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setOnError(request, reject);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
dataMap.set(response, {
|
||||||
|
task,
|
||||||
|
used: false,
|
||||||
|
nativeResponse: request,
|
||||||
|
requestOptions: options,
|
||||||
|
url: requestUrl,
|
||||||
|
downloadObservable: new Observable<number>((observer) => downloadSubscriptionPool.add(observer)),
|
||||||
|
dataObservable: new Observable<any>((observer) => dataSubscriptionPool.add(observer))
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setOnError(request, reject);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
request.open(options.method, requestUrl, !options.blockMainThread, options.user, options.password);
|
||||||
|
|
||||||
|
if (has('filereader') && has('blob')) {
|
||||||
|
request.responseType = 'blob';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.timeout && options.timeout > 0 && options.timeout !== Infinity) {
|
||||||
|
timeoutHandle = createTimer(() => {
|
||||||
|
// Reject first, since aborting will also fire onreadystatechange which would reject with a
|
||||||
|
// less specific error. (This is also why we set up our own timeout rather than using
|
||||||
|
// native timeout and ontimeout, because that aborts and fires onreadystatechange before ontimeout.)
|
||||||
|
timeoutReject && timeoutReject(new TimeoutError('The XMLHttpRequest request timed out'));
|
||||||
|
abort();
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasContentTypeHeader = false;
|
||||||
|
let hasRequestedWithHeader = false;
|
||||||
|
const { includeRequestedWithHeader = true, includeUploadProgress = true } = options;
|
||||||
|
|
||||||
|
if (options.headers) {
|
||||||
|
const requestHeaders = new Headers(options.headers);
|
||||||
|
|
||||||
|
hasRequestedWithHeader = requestHeaders.has('x-requested-with');
|
||||||
|
hasContentTypeHeader = requestHeaders.has('content-type');
|
||||||
|
|
||||||
|
for (const [key, value] of requestHeaders) {
|
||||||
|
request.setRequestHeader(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRequestedWithHeader && includeRequestedWithHeader) {
|
||||||
|
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContentTypeHeader && has('formdata') && options.body instanceof global.FormData) {
|
||||||
|
// Assume that most forms do not contain large binary files. If that is not the case,
|
||||||
|
// then "multipart/form-data" should be manually specified as the "Content-Type" header.
|
||||||
|
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
}
|
||||||
|
|
||||||
|
task.finally(() => {
|
||||||
|
if (task.state !== State.Fulfilled) {
|
||||||
|
request.onreadystatechange = noop;
|
||||||
|
timeoutHandle && timeoutHandle.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includeUploadProgress) {
|
||||||
|
const uploadObserverPool = new SubscriptionPool<number>();
|
||||||
|
task.upload = new Observable<number>((observer) => uploadObserverPool.add(observer));
|
||||||
|
|
||||||
|
if (has('host-browser') || has('web-worker-xhr-upload')) {
|
||||||
|
request.upload.addEventListener('progress', (event) => {
|
||||||
|
uploadObserverPool.next(event.loaded);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.send(options.body || null);
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
28
src/lib/core/src/request/util.ts
Normal file
28
src/lib/core/src/request/util.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { RequestOptions } from './interfaces';
|
||||||
|
import UrlSearchParams from '../UrlSearchParams';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a URL formatted with optional query string and cache-busting segments.
|
||||||
|
*
|
||||||
|
* @param url The base URL.
|
||||||
|
* @param options The RequestOptions used to generate the query string or cacheBust.
|
||||||
|
*/
|
||||||
|
export function generateRequestUrl(url: string, options: RequestOptions = {}): string {
|
||||||
|
let query = new UrlSearchParams(options.query).toString();
|
||||||
|
if (options.cacheBust) {
|
||||||
|
const bustString = String(Date.now());
|
||||||
|
query += query ? `&${bustString}` : bustString;
|
||||||
|
}
|
||||||
|
const separator = url.indexOf('?') > -1 ? '&' : '?';
|
||||||
|
return query ? `${url}${separator}${query}` : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStringFromFormData(formData: any): string {
|
||||||
|
const fields: string[] = [];
|
||||||
|
|
||||||
|
for (const key of formData.keys()) {
|
||||||
|
fields.push(encodeURIComponent(key) + '=' + encodeURIComponent(formData.get(key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields.join('&');
|
||||||
|
}
|
||||||
39
src/lib/core/src/stringExtras.ts
Normal file
39
src/lib/core/src/stringExtras.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Hash } from './interfaces';
|
||||||
|
|
||||||
|
const escapeRegExpPattern = /[[\]{}()|\/\\^$.*+?]/g;
|
||||||
|
const escapeXmlPattern = /[&<]/g;
|
||||||
|
const escapeXmlForPattern = /[&<>'"]/g;
|
||||||
|
const escapeXmlMap: Hash<string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes a string so that it can safely be passed to the RegExp constructor.
|
||||||
|
* @param text The string to be escaped
|
||||||
|
* @return The escaped string
|
||||||
|
*/
|
||||||
|
export function escapeRegExp(text: string): string {
|
||||||
|
return !text ? text : text.replace(escapeRegExpPattern, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a string to protect against tag injection.
|
||||||
|
* @param xml The string to be escaped
|
||||||
|
* @param forAttribute Whether to also escape ', ", and > in addition to < and &
|
||||||
|
* @return The escaped string
|
||||||
|
*/
|
||||||
|
export function escapeXml(xml: string, forAttribute: boolean = true): string {
|
||||||
|
if (!xml) {
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = forAttribute ? escapeXmlForPattern : escapeXmlPattern;
|
||||||
|
|
||||||
|
return xml.replace(pattern, function(character: string): string {
|
||||||
|
return escapeXmlMap[character];
|
||||||
|
});
|
||||||
|
}
|
||||||
116
src/lib/core/src/text.ts
Normal file
116
src/lib/core/src/text.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import Promise from '@dojo/shim/Promise';
|
||||||
|
import has from './has';
|
||||||
|
import request from './request';
|
||||||
|
import { NodeRequire, AmdRequire, AmdConfig } from './interfaces';
|
||||||
|
import { Require, isAmdRequire } from './load';
|
||||||
|
|
||||||
|
declare const require: Require;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Strips <?xml ...?> declarations so that external SVG and XML
|
||||||
|
* documents can be added to a document without worry. Also, if the string
|
||||||
|
* is an HTML document, only the part inside the body tag is returned.
|
||||||
|
*/
|
||||||
|
function strip(text: string): string {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replace(/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, '');
|
||||||
|
let matches = text.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im);
|
||||||
|
text = matches ? matches[1] : text;
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Host-specific method to retrieve text
|
||||||
|
*/
|
||||||
|
let getText: (url: string, callback: (value: string | null) => void) => void;
|
||||||
|
|
||||||
|
if (has('host-browser')) {
|
||||||
|
getText = function(url: string, callback: (value: string | null) => void): void {
|
||||||
|
request(url).then((response) => {
|
||||||
|
response.text().then((data) => {
|
||||||
|
callback(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else if (has('host-node')) {
|
||||||
|
let fs = isAmdRequire(require) && require.nodeRequire ? require.nodeRequire('fs') : (<NodeRequire>require)('fs');
|
||||||
|
getText = function(url: string, callback: (value: string) => void): void {
|
||||||
|
fs.readFile(url, { encoding: 'utf8' }, function(error: Error, data: string): void {
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
getText = function(): void {
|
||||||
|
throw new Error('dojo/text not supported on this platform');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cache of previously-loaded text resources
|
||||||
|
*/
|
||||||
|
let textCache: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cache of pending text resources
|
||||||
|
*/
|
||||||
|
let pending: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
export function get(url: string): Promise<string | null> {
|
||||||
|
let promise = new Promise<string | null>(function(resolve, reject) {
|
||||||
|
getText(url, function(text) {
|
||||||
|
resolve(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalize(id: string, toAbsMid: (moduleId: string) => string): string {
|
||||||
|
let parts = id.split('!');
|
||||||
|
let url = parts[0];
|
||||||
|
|
||||||
|
return (/^\./.test(url) ? toAbsMid(url) : url) + (parts[1] ? '!' + parts[1] : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load(id: string, require: AmdRequire, load: (value?: any) => void, config?: AmdConfig): void {
|
||||||
|
let parts = id.split('!');
|
||||||
|
let stripFlag = parts.length > 1;
|
||||||
|
let mid = parts[0];
|
||||||
|
let url = require.toUrl(mid);
|
||||||
|
let text: string | undefined;
|
||||||
|
|
||||||
|
function finish(text: string): void {
|
||||||
|
load(stripFlag ? strip(text) : text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mid in textCache) {
|
||||||
|
text = textCache[mid];
|
||||||
|
} else if (url in textCache) {
|
||||||
|
text = textCache[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
if (pending[url]) {
|
||||||
|
pending[url].push(finish);
|
||||||
|
} else {
|
||||||
|
let pendingList = (pending[url] = [finish]);
|
||||||
|
getText(url, function(value) {
|
||||||
|
textCache[mid] = textCache[url] = value;
|
||||||
|
for (let i = 0; i < pendingList.length; ) {
|
||||||
|
pendingList[i++](value || '');
|
||||||
|
}
|
||||||
|
delete pending[url];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finish(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/lib/core/src/util.ts
Normal file
122
src/lib/core/src/util.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { Handle } from './interfaces';
|
||||||
|
import { createHandle } from './lang';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a setTimeout call in a handle, allowing the timeout to be cleared by calling destroy.
|
||||||
|
*
|
||||||
|
* @param callback Callback to be called when the timeout elapses
|
||||||
|
* @param delay Number of milliseconds to wait before calling the callback
|
||||||
|
* @return Handle which can be destroyed to clear the timeout
|
||||||
|
*/
|
||||||
|
export function createTimer(callback: (...args: any[]) => void, delay?: number): Handle {
|
||||||
|
let timerId: number | null = setTimeout(callback, delay);
|
||||||
|
|
||||||
|
return createHandle(function() {
|
||||||
|
if (timerId) {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
timerId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a callback, returning a function which fires after no further calls are received over a set interval.
|
||||||
|
*
|
||||||
|
* @param callback Callback to wrap
|
||||||
|
* @param delay Number of milliseconds to wait after any invocations before calling the original callback
|
||||||
|
* @return Debounced function
|
||||||
|
*/
|
||||||
|
export function debounce<T extends (this: any, ...args: any[]) => void>(callback: T, delay: number): T {
|
||||||
|
// node.d.ts clobbers setTimeout/clearTimeout with versions that return/receive NodeJS.Timer,
|
||||||
|
// but browsers return/receive a number
|
||||||
|
let timer: Handle | null;
|
||||||
|
|
||||||
|
return <T>function() {
|
||||||
|
timer && timer.destroy();
|
||||||
|
|
||||||
|
let context = this;
|
||||||
|
let args: IArguments | null = arguments;
|
||||||
|
|
||||||
|
timer = guaranteeMinimumTimeout(function() {
|
||||||
|
callback.apply(context, args);
|
||||||
|
args = context = timer = null;
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a callback, returning a function which fires at most once per set interval.
|
||||||
|
*
|
||||||
|
* @param callback Callback to wrap
|
||||||
|
* @param delay Number of milliseconds to wait before allowing the original callback to be called again
|
||||||
|
* @return Throttled function
|
||||||
|
*/
|
||||||
|
export function throttle<T extends (this: any, ...args: any[]) => void>(callback: T, delay: number): T {
|
||||||
|
let ran: boolean | null;
|
||||||
|
|
||||||
|
return <T>function() {
|
||||||
|
if (ran) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ran = true;
|
||||||
|
|
||||||
|
callback.apply(this, arguments);
|
||||||
|
guaranteeMinimumTimeout(function() {
|
||||||
|
ran = null;
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like throttle, but calls the callback at the end of each interval rather than the beginning.
|
||||||
|
* Useful for e.g. resize or scroll events, when debounce would appear unresponsive.
|
||||||
|
*
|
||||||
|
* @param callback Callback to wrap
|
||||||
|
* @param delay Number of milliseconds to wait before calling the original callback and allowing it to be called again
|
||||||
|
* @return Throttled function
|
||||||
|
*/
|
||||||
|
export function throttleAfter<T extends (this: any, ...args: any[]) => void>(callback: T, delay: number): T {
|
||||||
|
let ran: boolean | null;
|
||||||
|
|
||||||
|
return <T>function() {
|
||||||
|
if (ran) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ran = true;
|
||||||
|
|
||||||
|
let context = this;
|
||||||
|
let args: IArguments | null = arguments;
|
||||||
|
|
||||||
|
guaranteeMinimumTimeout(function() {
|
||||||
|
callback.apply(context, args);
|
||||||
|
args = context = ran = null;
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function guaranteeMinimumTimeout(callback: (...args: any[]) => void, delay?: number): Handle {
|
||||||
|
const startTime = Date.now();
|
||||||
|
let timerId: number | null;
|
||||||
|
|
||||||
|
function timeoutHandler() {
|
||||||
|
const delta = Date.now() - startTime;
|
||||||
|
if (delay == null || delta >= delay) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
// Cast setTimeout return value to fix TypeScript parsing bug. Without it,
|
||||||
|
// it thinks we are using the Node version of setTimeout.
|
||||||
|
// Revisit this with the next TypeScript update.
|
||||||
|
// Set another timer for the mount of time that we came up short.
|
||||||
|
timerId = <any>setTimeout(timeoutHandler, delay - delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timerId = setTimeout(timeoutHandler, delay);
|
||||||
|
return createHandle(() => {
|
||||||
|
if (timerId != null) {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
timerId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
12
src/lib/core/src/uuid.ts
Normal file
12
src/lib/core/src/uuid.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Returns a v4 compliant UUID.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export default function uuid(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = (Math.random() * 16) | 0,
|
||||||
|
v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
2
src/lib/core/tests/benchmark/all.ts
Normal file
2
src/lib/core/tests/benchmark/all.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import './lang';
|
||||||
|
import './stringExtras';
|
||||||
269
src/lib/core/tests/benchmark/lang.ts
Normal file
269
src/lib/core/tests/benchmark/lang.ts
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import Benchmark = require('benchmark');
|
||||||
|
import lang = require('../../src/lang');
|
||||||
|
|
||||||
|
function onComplete(this: any) {
|
||||||
|
console.log(this.name + ': ' + this.hz + ' with a margin of error of ' + this.stats.moe);
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleSource = { a: 1, b: 'Lorem ipsum', c: 4 };
|
||||||
|
const simpleSourceWithArray = {
|
||||||
|
a: 1,
|
||||||
|
b: 'Dolor sit amet.',
|
||||||
|
c: [1, 2, 3],
|
||||||
|
d: 5
|
||||||
|
};
|
||||||
|
const sourceFromConstructor = (function() {
|
||||||
|
function Answers(this: any, kwArgs: { [key: string]: any }) {
|
||||||
|
Object.keys(kwArgs).forEach(function(this: any, key: string): void {
|
||||||
|
(<any>this)[key] = kwArgs[key];
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new (<any>Answers)({
|
||||||
|
universe: 42
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
const sourceWithInherited = Object.create(
|
||||||
|
Object.create(null, {
|
||||||
|
x: {
|
||||||
|
value: '1234567890'.split('')
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
enumerable: true,
|
||||||
|
value: /\s/
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
a: {
|
||||||
|
value: 1,
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
value: 'Lorem ipsum',
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true
|
||||||
|
},
|
||||||
|
c: {
|
||||||
|
value: [],
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true
|
||||||
|
},
|
||||||
|
d: {
|
||||||
|
value: 4,
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let benchmarks: Benchmark[] = [];
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.assign (single source)',
|
||||||
|
function() {
|
||||||
|
lang.assign(Object.create(null), simpleSource);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.assign (multiple sources)',
|
||||||
|
function() {
|
||||||
|
lang.assign(
|
||||||
|
Object.create(null),
|
||||||
|
simpleSource,
|
||||||
|
simpleSourceWithArray,
|
||||||
|
sourceWithInherited,
|
||||||
|
sourceFromConstructor
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.deepAssign (single source)',
|
||||||
|
function() {
|
||||||
|
lang.deepAssign(Object.create(null), simpleSource);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.deepAssign (multiple sources)',
|
||||||
|
function() {
|
||||||
|
lang.deepAssign(
|
||||||
|
Object.create(null),
|
||||||
|
simpleSource,
|
||||||
|
simpleSourceWithArray,
|
||||||
|
sourceWithInherited,
|
||||||
|
sourceFromConstructor
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.mixin (single source)',
|
||||||
|
function() {
|
||||||
|
lang.mixin(Object.create(null), simpleSource);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.mixin (multiple sources)',
|
||||||
|
function() {
|
||||||
|
lang.mixin(
|
||||||
|
Object.create(null),
|
||||||
|
simpleSource,
|
||||||
|
simpleSourceWithArray,
|
||||||
|
sourceWithInherited,
|
||||||
|
sourceFromConstructor
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.deepMixin (single source)',
|
||||||
|
function() {
|
||||||
|
lang.deepMixin(Object.create(null), simpleSource);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.deepMixin (multiple sources)',
|
||||||
|
function() {
|
||||||
|
lang.deepMixin(
|
||||||
|
Object.create(null),
|
||||||
|
simpleSource,
|
||||||
|
simpleSourceWithArray,
|
||||||
|
sourceWithInherited,
|
||||||
|
sourceFromConstructor
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.create',
|
||||||
|
function() {
|
||||||
|
lang.create(simpleSource, simpleSource, simpleSource, simpleSource);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.isIdentical',
|
||||||
|
(function() {
|
||||||
|
let a = Number('asdfx{}');
|
||||||
|
let b = Number('xkcd');
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
lang.isIdentical(a, b);
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.lateBind',
|
||||||
|
(function() {
|
||||||
|
let object: any = {
|
||||||
|
method: function() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
lang.lateBind(object, 'method');
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.lateBind (partial application)',
|
||||||
|
(function() {
|
||||||
|
let object: any = {
|
||||||
|
method: function() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
lang.lateBind(object, 'method', 1, 2, 3);
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.push(
|
||||||
|
new Benchmark(
|
||||||
|
'lang.partial',
|
||||||
|
(function() {
|
||||||
|
function f() {}
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
lang.partial(f, 1, 2, 3);
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
onComplete: onComplete
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
benchmarks.forEach(function(benchmark: Benchmark): void {
|
||||||
|
benchmark.run();
|
||||||
|
});
|
||||||
11
src/lib/core/tests/benchmark/stringExtras.ts
Normal file
11
src/lib/core/tests/benchmark/stringExtras.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Benchmark = require('benchmark');
|
||||||
|
|
||||||
|
let benchmarks: any[] = [];
|
||||||
|
|
||||||
|
benchmarks.forEach(function(benchmark: any): void {
|
||||||
|
new Benchmark(benchmark.name, benchmark.fn, {
|
||||||
|
onComplete: function(this: any) {
|
||||||
|
console.log(this.name + ': ' + this.hz + ' with a margin of error of ' + this.stats.moe);
|
||||||
|
}
|
||||||
|
}).run();
|
||||||
|
});
|
||||||
1
src/lib/core/tests/functional/all.ts
Normal file
1
src/lib/core/tests/functional/all.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import './text/textPlugin';
|
||||||
30
src/lib/core/tests/functional/text/textPlugin.html
Normal file
30
src/lib/core/tests/functional/text/textPlugin.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Text Plugin Test</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="../../../../node_modules/@dojo/shim/util/amd.js"></script>
|
||||||
|
<script src="../../../../node_modules/@dojo/loader/loader.js"></script>
|
||||||
|
<script>
|
||||||
|
require.config(shimAmdDependencies({
|
||||||
|
baseUrl: '../../../../',
|
||||||
|
packages: [
|
||||||
|
{ name: 'src', location: '_build/src' },
|
||||||
|
{ name: 'tests', location: '_build/tests' }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
require(['@dojo/shim/main'], function () {
|
||||||
|
require([
|
||||||
|
'src/text!./_build/tests/support/data/correctText.txt'
|
||||||
|
], function (result) {
|
||||||
|
window.loaderTestResults = {
|
||||||
|
text: result
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
src/lib/core/tests/functional/text/textPlugin.ts
Normal file
44
src/lib/core/tests/functional/text/textPlugin.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const { registerSuite } = intern.getInterface('object');
|
||||||
|
const { assert } = intern.getPlugin('chai');
|
||||||
|
import Test from 'intern/lib/Test';
|
||||||
|
import pollUntil from '@theintern/leadfoot/helpers/pollUntil';
|
||||||
|
|
||||||
|
async function executeTest(test: Test, htmlTestPath: string, timeout = 10000) {
|
||||||
|
try {
|
||||||
|
return await test.remote.get(htmlTestPath).then(
|
||||||
|
pollUntil<{ text: string }>(
|
||||||
|
function() {
|
||||||
|
return (<any>window).loaderTestResults || null;
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
timeout
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('loaderTestResult was not set.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = 'abc';
|
||||||
|
|
||||||
|
registerSuite('text plugin', {
|
||||||
|
async 'correct text'() {
|
||||||
|
const results = await executeTest(this, `${__dirname}/textPlugin.html`);
|
||||||
|
assert.strictEqual(results.text, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'strips XML'(this: any) {
|
||||||
|
const results = await executeTest(this, `${__dirname}/textPluginXML.html`);
|
||||||
|
assert.strictEqual(results.text, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'strips HTML'(this: any) {
|
||||||
|
const results = await executeTest(this, `${__dirname}/textPluginHTML.html`);
|
||||||
|
assert.strictEqual(results.text, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
async 'strips empty file'(this: any) {
|
||||||
|
const results = await executeTest(this, `${__dirname}/textPluginEmpty.html`);
|
||||||
|
assert.strictEqual(results.text, '');
|
||||||
|
}
|
||||||
|
});
|
||||||
30
src/lib/core/tests/functional/text/textPluginEmpty.html
Normal file
30
src/lib/core/tests/functional/text/textPluginEmpty.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Text Plugin Test</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="../../../../node_modules/@dojo/shim/util/amd.js"></script>
|
||||||
|
<script src="../../../../node_modules/@dojo/loader/loader.js"></script>
|
||||||
|
<script>
|
||||||
|
require.config(shimAmdDependencies({
|
||||||
|
baseUrl: '../../../../',
|
||||||
|
packages: [
|
||||||
|
{ name: 'src', location: '_build/src' },
|
||||||
|
{ name: 'tests', location: '_build/tests' }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
require(['@dojo/shim/main'], function () {
|
||||||
|
require([
|
||||||
|
'src/text!./_build/tests/support/data/stripEmpty.xml!strip'
|
||||||
|
], function (result) {
|
||||||
|
window.loaderTestResults = {
|
||||||
|
text: result
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
src/lib/core/tests/functional/text/textPluginHTML.html
Normal file
30
src/lib/core/tests/functional/text/textPluginHTML.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Text Plugin Test</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="../../../../node_modules/@dojo/shim/util/amd.js"></script>
|
||||||
|
<script src="../../../../node_modules/@dojo/loader/loader.js"></script>
|
||||||
|
<script>
|
||||||
|
require.config(shimAmdDependencies({
|
||||||
|
baseUrl: '../../../../',
|
||||||
|
packages: [
|
||||||
|
{ name: 'src', location: '_build/src' },
|
||||||
|
{ name: 'tests', location: '_build/tests' }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
require(['@dojo/shim/main'], function () {
|
||||||
|
require([
|
||||||
|
'src/text!./_build/tests/support/data/strip.html!strip'
|
||||||
|
], function (result) {
|
||||||
|
window.loaderTestResults = {
|
||||||
|
text: result
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user