easier-http-request 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +19 -0
- package/dist/exports/Exports.d.ts +32 -0
- package/dist/exports/Exports.d.ts.map +1 -0
- package/dist/exports/Exports.js +187 -0
- package/dist/exports/Exports.js.map +1 -0
- package/dist/http-request/SimpleHttpRequest.d.ts +2 -0
- package/dist/http-request/SimpleHttpRequest.d.ts.map +1 -0
- package/dist/http-request/SimpleHttpRequest.js +2 -0
- package/dist/http-request/SimpleHttpRequest.js.map +1 -0
- package/dist/tests/Test.d.ts +2 -0
- package/dist/tests/Test.d.ts.map +1 -0
- package/dist/tests/Test.js +2 -0
- package/dist/tests/Test.js.map +1 -0
- package/dist/utilities/ObjectUtilities.d.ts +5 -0
- package/dist/utilities/ObjectUtilities.d.ts.map +1 -0
- package/dist/utilities/ObjectUtilities.js +145 -0
- package/dist/utilities/ObjectUtilities.js.map +1 -0
- package/dist/utilities/Timer.d.ts +14 -0
- package/dist/utilities/Timer.d.ts.map +1 -0
- package/dist/utilities/Timer.js +61 -0
- package/dist/utilities/Timer.js.map +1 -0
- package/dist/utilities/Utilities.d.ts +10 -0
- package/dist/utilities/Utilities.d.ts.map +1 -0
- package/dist/utilities/Utilities.js +110 -0
- package/dist/utilities/Utilities.js.map +1 -0
- package/package.json +35 -0
- package/src/exports/Exports.ts +277 -0
- package/src/tests/Test.ts +0 -0
- package/src/utilities/ObjectUtilities.ts +193 -0
- package/src/utilities/Timer.ts +79 -0
- package/src/utilities/Utilities.ts +127 -0
- package/tsconfig.json +55 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Timer } from './Timer.js';
|
|
2
|
+
export function writeToStderr(message) {
|
|
3
|
+
process.stderr.write(message);
|
|
4
|
+
}
|
|
5
|
+
export function logToStderr(message) {
|
|
6
|
+
writeToStderr(message);
|
|
7
|
+
writeToStderr('\n');
|
|
8
|
+
}
|
|
9
|
+
export function formatIntegerWithLeadingZeros(num, minDigitCount) {
|
|
10
|
+
num = Math.floor(num);
|
|
11
|
+
let numAsString = `${num}`;
|
|
12
|
+
while (numAsString.length < minDigitCount) {
|
|
13
|
+
numAsString = `0${numAsString}`;
|
|
14
|
+
}
|
|
15
|
+
return numAsString;
|
|
16
|
+
}
|
|
17
|
+
export function roundToDigits(val, digits = 3) {
|
|
18
|
+
const multiplier = 10 ** digits;
|
|
19
|
+
return Math.round(val * multiplier) / multiplier;
|
|
20
|
+
}
|
|
21
|
+
export function sleep(timeMs) {
|
|
22
|
+
const timer = new Timer();
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const tickCallback = () => {
|
|
25
|
+
if (timer.elapsedTime < timeMs) {
|
|
26
|
+
//setImmediate(tickCallback)
|
|
27
|
+
setTimeout(tickCallback, 0);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//setImmediate(tickCallback)
|
|
34
|
+
setTimeout(tickCallback, 0);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function parseUrlOrPath(inputString, baseUrl = 'https://dummy.com') {
|
|
38
|
+
try {
|
|
39
|
+
// Attempt 1: Try parsing as an absolute URL
|
|
40
|
+
const url = new URL(inputString);
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// If it failed, it's likely a relative URL.
|
|
45
|
+
// Attempt 2: Try parsing with a base URL.
|
|
46
|
+
// The base URL can be a real one (e.g., your server's origin)
|
|
47
|
+
// or a dummy one if you only care about path/query/hash.
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(inputString, baseUrl);
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
catch (relativeError) {
|
|
53
|
+
// If it fails even with a base, it's truly malformed or unparseable.
|
|
54
|
+
throw new Error(`Failed to parse URL '${inputString}' even with base '${baseUrl}': ${relativeError.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function getDomainName(url) {
|
|
59
|
+
try {
|
|
60
|
+
const hostname = (new URL(url)).hostname;
|
|
61
|
+
const parts = hostname.split('.');
|
|
62
|
+
if (parts.length < 2) {
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
return parts.slice(parts.length - 2).join('.');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function yieldToEventLoop() {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
setImmediate(resolve);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function isValidHttpUrl(urlString) {
|
|
77
|
+
// 1. Basic type check and trim: Ensure the input is actually a non-empty string.
|
|
78
|
+
// While TypeScript handles type at compile time, this adds runtime robustness.
|
|
79
|
+
if (typeof urlString !== 'string' || urlString.trim() === '') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
// 2. Attempt to construct a URL object.
|
|
84
|
+
// The URL constructor throws a TypeError if the URL is not valid
|
|
85
|
+
// (e.g., malformed, relative without a base, etc.).
|
|
86
|
+
const url = new URL(urlString);
|
|
87
|
+
// 3. Additional robustness: Restrict allowed protocols.
|
|
88
|
+
// The URL constructor accepts many protocols (e.g., 'ftp:', 'file:', 'data:', 'javascript:').
|
|
89
|
+
// For typical web applications, we usually only want 'http:' or 'https:'.
|
|
90
|
+
// The `protocol` property includes the trailing colon.
|
|
91
|
+
const allowedProtocols = ['http:', 'https:'];
|
|
92
|
+
if (!allowedProtocols.includes(url.protocol)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// 4. Optional but often desired: Ensure a non-empty hostname.
|
|
96
|
+
// URLs like "http://" or "https://" are technically valid URL objects
|
|
97
|
+
// but have an empty hostname, which might not be desired for a "valid" URL.
|
|
98
|
+
if (!url.hostname) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// All checks passed
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// If the URL constructor throws an error, it's not a valid URL.
|
|
106
|
+
// console.error("URL validation error:", error); // Uncomment for debugging if needed
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=Utilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Utilities.js","sourceRoot":"","sources":["../../src/utilities/Utilities.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easier-http-request",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Easier HTTP(S) request method. Wraps the Fetch API with some additional features and conveniences.",
|
|
5
|
+
"author": "Rotem Dan",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": ["http", "http-request", "fetch-wrapper", "fetch", "fetch-api"],
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/rotemdan/easier-http-request"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/rotemdan/easier-http-request/issues"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"private": false,
|
|
19
|
+
"main": "./dist/exports/Exports.js",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"tsconfig.json",
|
|
25
|
+
"LICENSE.md",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node dist/tests/Test.js"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "~26.1.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { isPlainObject } from '../utilities/ObjectUtilities.js'
|
|
2
|
+
import { isValidHttpUrl } from '../utilities/Utilities.js'
|
|
3
|
+
|
|
4
|
+
export async function requestHttp(config: EasierHttpRequestConfig): Promise<Response> {
|
|
5
|
+
// Error if given a bad URL
|
|
6
|
+
if (!isValidHttpUrl(config.url)) {
|
|
7
|
+
throw new Error(`'${config.url}' is not a valid HTTP(S) URL.`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Resolve configuration
|
|
11
|
+
config = { ...defaultEasierHttpRequestConfig, ...config }
|
|
12
|
+
|
|
13
|
+
// Process URL and Params
|
|
14
|
+
const urlObject = new URL(config.url)
|
|
15
|
+
|
|
16
|
+
// Add parameters from parameter object
|
|
17
|
+
for (const [key, valueOrValues] of Object.entries(config.params!)) {
|
|
18
|
+
if (valueOrValues == null) {
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const values = Array.isArray(valueOrValues) ? valueOrValues : [valueOrValues]
|
|
23
|
+
|
|
24
|
+
for (const value of values) {
|
|
25
|
+
urlObject.searchParams.append(key, String(value))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Derive final URL as string
|
|
30
|
+
const urlString = urlObject.toString()
|
|
31
|
+
|
|
32
|
+
// Get method
|
|
33
|
+
const method = config.method!.toUpperCase()
|
|
34
|
+
|
|
35
|
+
// Build headers
|
|
36
|
+
const headers = new Headers()
|
|
37
|
+
|
|
38
|
+
for (const [key, valueOrValues] of Object.entries(config.headers!)) {
|
|
39
|
+
if (valueOrValues === undefined) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let values: string[]
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(valueOrValues)) {
|
|
46
|
+
values = valueOrValues
|
|
47
|
+
} else {
|
|
48
|
+
values = [valueOrValues]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Ensure repeating keys with different capitalizations don't concatenate with each other
|
|
52
|
+
headers.delete(key)
|
|
53
|
+
|
|
54
|
+
// Append header values
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
headers.append(key, String(value))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Process request body
|
|
61
|
+
let body: BodyInit | undefined = undefined
|
|
62
|
+
|
|
63
|
+
const bodyArg = config.body
|
|
64
|
+
|
|
65
|
+
if (bodyArg !== undefined) {
|
|
66
|
+
if (['GET', 'HEAD'].includes(method)) {
|
|
67
|
+
throw new Error(`Request method '${method}' doesn't accept a request body.`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isPlainObject(bodyArg)) {
|
|
71
|
+
body = JSON.stringify(bodyArg)
|
|
72
|
+
|
|
73
|
+
headers.set('Content-Type', 'application/json')
|
|
74
|
+
} else {
|
|
75
|
+
// TODO: consider ensuring bodyArg type is valid
|
|
76
|
+
body = bodyArg as any
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle timeout and user-defined abort signal
|
|
81
|
+
const abortController = new AbortController()
|
|
82
|
+
const abortSignal = abortController.signal
|
|
83
|
+
|
|
84
|
+
// Set timeout to abort when maximum time elapses before headers are sent by the server
|
|
85
|
+
const timeoutMilliseconds = Math.floor(config.timeout! * 1000)
|
|
86
|
+
|
|
87
|
+
const requestTimeoutHandle = setTimeout(() => {
|
|
88
|
+
abortController?.abort('timeout')
|
|
89
|
+
}, timeoutMilliseconds)
|
|
90
|
+
|
|
91
|
+
// Set user abort listener
|
|
92
|
+
const userAbortListener = () => abortController?.abort()
|
|
93
|
+
config.abortSignal?.addEventListener('abort', userAbortListener)
|
|
94
|
+
|
|
95
|
+
// Prepare fetch options (RequestInit)
|
|
96
|
+
const fetchInitOptions: RequestInit = {
|
|
97
|
+
// Pass arguments
|
|
98
|
+
method,
|
|
99
|
+
headers,
|
|
100
|
+
body,
|
|
101
|
+
signal: abortSignal,
|
|
102
|
+
|
|
103
|
+
// Forward other aruments to 'fetch', unmodified.
|
|
104
|
+
redirect: config.redirect,
|
|
105
|
+
integrity: config.integrity,
|
|
106
|
+
|
|
107
|
+
credentials: config.credentials,
|
|
108
|
+
mode: config.mode,
|
|
109
|
+
referrer: config.referrer,
|
|
110
|
+
referrerPolicy: config.referrerPolicy,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Execute fetch
|
|
114
|
+
let response: Response
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(urlString, fetchInitOptions)
|
|
118
|
+
|
|
119
|
+
// Check for HTTP error (non-2xx status)
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
let errorBody: unknown
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Attempt to parse body for error details, but don't fail if it's not JSON/text
|
|
125
|
+
const contentType = response.headers.get('content-type')
|
|
126
|
+
|
|
127
|
+
if (contentType?.includes('application/json')) {
|
|
128
|
+
errorBody = await response.json()
|
|
129
|
+
} else if (contentType?.includes('text/')) {
|
|
130
|
+
errorBody = await response.text()
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
// Ignore parsing errors for the error body itself
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const status = response.status
|
|
137
|
+
const statusText = response.statusText || 'Unknown Error'
|
|
138
|
+
const errorMessage = `Request failed with status code ${status}: ${statusText}`
|
|
139
|
+
|
|
140
|
+
const error = new EasierHttpRequestError(errorMessage, config, response, errorBody)
|
|
141
|
+
|
|
142
|
+
throw error
|
|
143
|
+
}
|
|
144
|
+
} catch (error: any) {
|
|
145
|
+
if (error instanceof EasierHttpRequestError || error instanceof EasierHttpRequestTimeoutError) {
|
|
146
|
+
// This is our custom HTTP error (non-2xx)
|
|
147
|
+
throw error
|
|
148
|
+
} else if (error.name === 'AbortError') {
|
|
149
|
+
if (error.reason === 'timeout') {
|
|
150
|
+
throw new EasierHttpRequestTimeoutError(`Request timed out after ${config.timeout!}ms.`)
|
|
151
|
+
} else {
|
|
152
|
+
throw error
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Other network errors
|
|
156
|
+
const networkError = new EasierHttpRequestError(`Network Error: ${error.message}`, config);
|
|
157
|
+
// Optionally attach the original error
|
|
158
|
+
(networkError as any).originalError = error
|
|
159
|
+
|
|
160
|
+
throw networkError
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
// Clear timeout
|
|
164
|
+
clearTimeout(requestTimeoutHandle)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return response
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Request configuration object type
|
|
171
|
+
export interface EasierHttpRequestConfig {
|
|
172
|
+
///////////////////////////////////////////////////////////////////////////
|
|
173
|
+
// Simplified parameters. Wraps over 'fetch'.
|
|
174
|
+
///////////////////////////////////////////////////////////////////////////
|
|
175
|
+
|
|
176
|
+
// The URL for the request.
|
|
177
|
+
url: string
|
|
178
|
+
|
|
179
|
+
// The HTTP method. Defaults to 'GET'.
|
|
180
|
+
method?: HttpRequestMethod
|
|
181
|
+
|
|
182
|
+
// Parameters to be appended to the URL as a query string.
|
|
183
|
+
params?: Record<string, unknown>
|
|
184
|
+
|
|
185
|
+
// Custom headers to include in the request.
|
|
186
|
+
headers?: Record<string, string | string[] | undefined>
|
|
187
|
+
|
|
188
|
+
// Request body
|
|
189
|
+
body?: unknown
|
|
190
|
+
|
|
191
|
+
// The maximum number of seconds to wait for HTTP headers to be received from the server
|
|
192
|
+
timeout?: number
|
|
193
|
+
|
|
194
|
+
// Abort signal
|
|
195
|
+
abortSignal?: AbortSignal
|
|
196
|
+
|
|
197
|
+
///////////////////////////////////////////////////////////////////////////
|
|
198
|
+
// Additional parameters. Forwarded directly to 'fetch', unmodified.
|
|
199
|
+
///////////////////////////////////////////////////////////////////////////
|
|
200
|
+
|
|
201
|
+
// How to handle redirections
|
|
202
|
+
redirect?: RequestRedirect
|
|
203
|
+
|
|
204
|
+
// Subresource integrity value of the request.
|
|
205
|
+
integrity?: string
|
|
206
|
+
|
|
207
|
+
// Credentials
|
|
208
|
+
credentials?: RequestCredentials
|
|
209
|
+
|
|
210
|
+
// CORS mode
|
|
211
|
+
mode?: RequestMode
|
|
212
|
+
|
|
213
|
+
// Referrer
|
|
214
|
+
referrer?: string
|
|
215
|
+
|
|
216
|
+
// Referrer policy
|
|
217
|
+
referrerPolicy?: ReferrerPolicy
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Default configuration
|
|
221
|
+
export const defaultEasierHttpRequestConfig: EasierHttpRequestConfig = {
|
|
222
|
+
url: '',
|
|
223
|
+
method: 'GET',
|
|
224
|
+
params: {},
|
|
225
|
+
headers: {},
|
|
226
|
+
body: undefined,
|
|
227
|
+
timeout: 60 * 2,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// HTTP methods
|
|
231
|
+
export type HttpRequestMethod =
|
|
232
|
+
'get' | 'GET' |
|
|
233
|
+
'delete' | 'DELETE' |
|
|
234
|
+
'head' | 'HEAD' |
|
|
235
|
+
'options' | 'OPTIONS' |
|
|
236
|
+
'post' | 'POST' |
|
|
237
|
+
'put' | 'PUT' |
|
|
238
|
+
'patch' | 'PATCH' |
|
|
239
|
+
'purge' | 'PURGE' |
|
|
240
|
+
'link' | 'LINK' |
|
|
241
|
+
'unlink' | 'UNLINK'
|
|
242
|
+
|
|
243
|
+
// Request error object
|
|
244
|
+
export class EasierHttpRequestError extends Error {
|
|
245
|
+
constructor(
|
|
246
|
+
public readonly message: string,
|
|
247
|
+
public readonly config: EasierHttpRequestConfig,
|
|
248
|
+
public readonly response?: Response,
|
|
249
|
+
public readonly errorBody?: unknown) {
|
|
250
|
+
super(message)
|
|
251
|
+
|
|
252
|
+
this.name = 'RequestError'
|
|
253
|
+
|
|
254
|
+
// Capture stack trace, excluding constructor call from it
|
|
255
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
256
|
+
Error.captureStackTrace(this, EasierHttpRequestError)
|
|
257
|
+
} else {
|
|
258
|
+
this.stack = (new Error(message)).stack
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get statusCode() {
|
|
263
|
+
return this.response?.status
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
get statusText() {
|
|
267
|
+
return this.response?.statusText
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class EasierHttpRequestTimeoutError extends Error {
|
|
272
|
+
constructor(public readonly message: string) {
|
|
273
|
+
super(message)
|
|
274
|
+
|
|
275
|
+
this.name = 'RequestTimeoutError'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export function extendDeep(base: any, extension: any): any {
|
|
2
|
+
const baseClone = deepClone(base)
|
|
3
|
+
|
|
4
|
+
if (isPlainObject(base) && extension === undefined) {
|
|
5
|
+
return baseClone
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const extensionClone = deepClone(extension)
|
|
9
|
+
if (!isPlainObject(base) || !isPlainObject(extension)) {
|
|
10
|
+
return extensionClone
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const propName in extensionClone) {
|
|
14
|
+
if (!extensionClone.hasOwnProperty(propName)) {
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
baseClone[propName] = extendDeep(baseClone[propName], extensionClone[propName])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return baseClone
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shallowClone<T>(val: T) {
|
|
25
|
+
return clone(val, false)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deepClone<T>(val: T) {
|
|
29
|
+
return clone(val, true)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
|
|
33
|
+
if (val === undefined || val === null || typeof val !== 'object') {
|
|
34
|
+
return val
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const obj = <any>val
|
|
38
|
+
const prototypeIdentifier = toString.call(obj)
|
|
39
|
+
|
|
40
|
+
switch (prototypeIdentifier) {
|
|
41
|
+
case '[object Array]': {
|
|
42
|
+
if (seenObjects.includes(obj)) {
|
|
43
|
+
throw new Error('deepClone: encountered a cyclic object')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
seenObjects.push(obj)
|
|
47
|
+
|
|
48
|
+
const clonedArray = new Array(obj.length)
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < obj.length; i++) {
|
|
51
|
+
if (deep) {
|
|
52
|
+
clonedArray[i] = clone(obj[i], true, seenObjects)
|
|
53
|
+
} else {
|
|
54
|
+
clonedArray[i] = obj[i]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
seenObjects.pop()
|
|
59
|
+
|
|
60
|
+
return clonedArray as any
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case '[object ArrayBuffer]': {
|
|
64
|
+
const clonedArray = new Uint8Array(obj.byteLength)
|
|
65
|
+
clonedArray.set(new Uint8Array(obj))
|
|
66
|
+
|
|
67
|
+
return clonedArray.buffer as any
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case '[object Int8Array]': {
|
|
71
|
+
const clonedArray = new Int8Array(obj.length)
|
|
72
|
+
clonedArray.set(obj)
|
|
73
|
+
|
|
74
|
+
return clonedArray as any
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case '[object Uint8Array]': {
|
|
78
|
+
const clonedArray = new Uint8Array(obj.length)
|
|
79
|
+
clonedArray.set(obj)
|
|
80
|
+
|
|
81
|
+
return clonedArray as any
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case '[object Uint8ClampedArray]': {
|
|
85
|
+
const clonedArray = new Uint8ClampedArray(obj.length)
|
|
86
|
+
clonedArray.set(obj)
|
|
87
|
+
|
|
88
|
+
return clonedArray as any
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case '[object Int16Array]': {
|
|
92
|
+
const clonedArray = new Int16Array(obj.length)
|
|
93
|
+
clonedArray.set(obj)
|
|
94
|
+
|
|
95
|
+
return clonedArray as any
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case '[object Uint16Array]': {
|
|
99
|
+
const clonedArray = new Uint16Array(obj.length)
|
|
100
|
+
clonedArray.set(obj)
|
|
101
|
+
|
|
102
|
+
return clonedArray as any
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case '[object Int32Array]': {
|
|
106
|
+
const clonedArray = new Int32Array(obj.length)
|
|
107
|
+
clonedArray.set(obj)
|
|
108
|
+
|
|
109
|
+
return clonedArray as any
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case '[object Uint32Array]': {
|
|
113
|
+
const clonedArray = new Uint32Array(obj.length)
|
|
114
|
+
clonedArray.set(obj)
|
|
115
|
+
|
|
116
|
+
return clonedArray as any
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case '[object Float32Array]': {
|
|
120
|
+
const clonedArray = new Float32Array(obj.length)
|
|
121
|
+
clonedArray.set(obj)
|
|
122
|
+
|
|
123
|
+
return clonedArray as any
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case '[object Float64Array]': {
|
|
127
|
+
const clonedArray = new Float64Array(obj.length)
|
|
128
|
+
clonedArray.set(obj)
|
|
129
|
+
|
|
130
|
+
return clonedArray as any
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case '[object BigInt64Array]': {
|
|
134
|
+
const clonedArray = new BigInt64Array(obj.length)
|
|
135
|
+
clonedArray.set(obj)
|
|
136
|
+
|
|
137
|
+
return clonedArray as any
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case '[object BigUint64Array]': {
|
|
141
|
+
const clonedArray = new BigUint64Array(obj.length)
|
|
142
|
+
clonedArray.set(obj)
|
|
143
|
+
|
|
144
|
+
return clonedArray as any
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case '[object Date]': {
|
|
148
|
+
return new Date(obj.valueOf()) as any
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case '[object RegExp]': {
|
|
152
|
+
return obj
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case '[object Function]': {
|
|
156
|
+
return obj
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case '[object Object]': {
|
|
160
|
+
if (seenObjects.includes(obj)) {
|
|
161
|
+
throw new Error('deepClone: encountered a cyclic object')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
seenObjects.push(obj)
|
|
165
|
+
|
|
166
|
+
const clonedObj: any = {}
|
|
167
|
+
|
|
168
|
+
for (const propName in obj) {
|
|
169
|
+
if (!obj.hasOwnProperty(propName)) {
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (deep) {
|
|
174
|
+
clonedObj[propName] = clone(obj[propName], true, seenObjects)
|
|
175
|
+
} else {
|
|
176
|
+
clonedObj[propName] = obj[propName]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
seenObjects.pop()
|
|
181
|
+
|
|
182
|
+
return clonedObj
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
default: {
|
|
186
|
+
throw new Error(`Cloning of type ${prototypeIdentifier} is not supported`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function isPlainObject(val: any) {
|
|
192
|
+
return val != null && typeof val === 'object' && toString.call(val) === '[object Object]'
|
|
193
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { logToStderr, roundToDigits } from './Utilities.js'
|
|
2
|
+
|
|
3
|
+
export class Timer {
|
|
4
|
+
startTime = 0
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.restart()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
restart() {
|
|
11
|
+
this.startTime = Timer.currentTime
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get elapsedTime(): number {
|
|
15
|
+
// Elapsed time (milliseconds)
|
|
16
|
+
return Timer.currentTime - this.startTime
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get elapsedTimeSeconds(): number {
|
|
20
|
+
// Elapsed time (seconds)
|
|
21
|
+
return this.elapsedTime / 1000
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getElapsedTimeAndRestart(): number {
|
|
25
|
+
const elapsedTime = this.elapsedTime
|
|
26
|
+
this.restart()
|
|
27
|
+
|
|
28
|
+
return elapsedTime
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logAndRestart(title: string, timePrecision = 3): number {
|
|
32
|
+
const elapsedTime = this.elapsedTime
|
|
33
|
+
|
|
34
|
+
//
|
|
35
|
+
const message = `${title}: ${roundToDigits(elapsedTime, timePrecision)}ms`
|
|
36
|
+
|
|
37
|
+
logToStderr(message)
|
|
38
|
+
//
|
|
39
|
+
|
|
40
|
+
this.restart()
|
|
41
|
+
|
|
42
|
+
return elapsedTime
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static get currentTime(): number {
|
|
46
|
+
if (!this.getTimestamp) {
|
|
47
|
+
this.createTimestampFunction()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return this.getTimestamp()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static get microsecondTimestamp(): number {
|
|
54
|
+
return Math.floor(Timer.currentTime * 1000)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static createTimestampFunction() {
|
|
58
|
+
if (typeof process === 'object' && typeof process.hrtime === 'function') {
|
|
59
|
+
let baseTimestamp = 0
|
|
60
|
+
|
|
61
|
+
this.getTimestamp = () => {
|
|
62
|
+
const nodeTimeNanoSeconds = process.hrtime.bigint()
|
|
63
|
+
const nodeTimeMilliseconds = Number(nodeTimeNanoSeconds) / 1_000_000
|
|
64
|
+
|
|
65
|
+
return baseTimestamp + nodeTimeMilliseconds
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
baseTimestamp = Date.now() - this.getTimestamp()
|
|
69
|
+
} else if (typeof performance === 'object' && performance.now) {
|
|
70
|
+
const baseTimestamp = Date.now() - performance.now()
|
|
71
|
+
|
|
72
|
+
this.getTimestamp = () => baseTimestamp + performance.now()
|
|
73
|
+
} else {
|
|
74
|
+
this.getTimestamp = () => Date.now()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static getTimestamp: () => number
|
|
79
|
+
}
|