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.
@@ -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":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,MAAM,UAAU,aAAa,CAAC,OAAY;IACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AAC9B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAY;IACvC,aAAa,CAAC,OAAO,CAAC,CAAA;IACtB,aAAa,CAAC,IAAI,CAAC,CAAA;AACpB,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,GAAW,EAAE,aAAqB;IAC/E,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAErB,IAAI,WAAW,GAAG,GAAG,GAAG,EAAE,CAAA;IAE1B,OAAO,WAAW,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;QAC3C,WAAW,GAAG,IAAI,WAAW,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,WAAW,CAAA;AACnB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,MAAM,GAAG,CAAC;IACpD,MAAM,UAAU,GAAG,EAAE,IAAI,MAAM,CAAA;IAE/B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC,GAAG,UAAU,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,MAAc;IACnC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAA;IAEzB,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACpC,MAAM,YAAY,GAAG,GAAG,EAAE;YACzB,IAAI,KAAK,CAAC,WAAW,GAAG,MAAM,EAAE,CAAC;gBAChC,4BAA4B;gBAC5B,UAAU,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;YAC5B,CAAC;iBAAM,CAAC;gBACP,OAAO,EAAE,CAAA;YACV,CAAC;QACF,CAAC,CAAA;QAED,4BAA4B;QAC5B,UAAU,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,WAAmB,EAAE,OAAO,GAAG,mBAAmB;IAChF,IAAI,CAAC;QACJ,4CAA4C;QAC5C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QAEhC,OAAO,GAAG,CAAA;IACX,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,4CAA4C;QAC5C,0CAA0C;QAC1C,8DAA8D;QAC9D,yDAAyD;QACzD,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;YAEzC,OAAO,GAAG,CAAA;QACX,CAAC;QAAC,OAAO,aAAkB,EAAE,CAAC;YAC7B,qEAAqE;YACrE,MAAM,IAAI,KAAK,CAAC,wBAAwB,WAAW,qBAAqB,OAAO,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9G,CAAC;IACF,CAAC;AACF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW;IACxC,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAA;QACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAEjC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,OAAO,GAAG,CAAA;QACX,CAAC;QAED,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAA;IACV,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,YAAY,CAAC,OAAO,CAAC,CAAA;IACtB,CAAC,CAAC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC/C,iFAAiF;IACjF,kFAAkF;IAClF,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9D,OAAO,KAAK,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACJ,wCAAwC;QACxC,oEAAoE;QACpE,uDAAuD;QACvD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;QAE/B,wDAAwD;QACxD,iGAAiG;QACjG,6EAA6E;QAC7E,0DAA0D;QAC1D,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC7C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9C,OAAO,KAAK,CAAC;QACd,CAAC;QAED,8DAA8D;QAC9D,yEAAyE;QACzE,+EAA+E;QAC/E,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YACnB,OAAO,KAAK,CAAC;QACd,CAAC;QAED,oBAAoB;QACpB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,gEAAgE;QAChE,sFAAsF;QACtF,OAAO,KAAK,CAAC;IACd,CAAC;AACF,CAAC"}
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
+ }