@oirnoir/util 0.0.0-stage → 0.0.60

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 oirnoir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # @oirnoir/util
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Basic utility functions for primarily private use but published as open source.
4
+
5
+ This project is developed and maintained exclusively at [a private Forgejo instance](https://git.oirnoir.dev/OIRNOIR/util).
6
+
7
+ ## Supported runtimes
8
+
9
+ Node.js 26+ (via npm) and Deno (via JSR or npm).
10
+
11
+ The package is compiled to JavaScript for Node.js consumers; raw TypeScript is served directly to Deno consumers.
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
- "name": "@oirnoir/util",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@oirnoir/util",
3
+ "version": "0.0.60",
4
+ "license": "MIT",
5
+ "description": "Utility methods for Node.js and Deno",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=26"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "deno": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "scripts": {
24
+ "build": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --platform=node --external:timestring && tsc -p tsconfig.json",
25
+ "typecheck": "tsc --noEmit -p tsconfig.json",
26
+ "lint": "biome check . --write && biome lint",
27
+ "test": "node --test test/*.ts",
28
+ "publish:npm": "pnpm publish",
29
+ "publish:jsr": "deno publish --token $JSR_TOKEN"
30
+ },
31
+ "dependencies": {
32
+ "timestring": "^7.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@biomejs/biome": "2.6.2",
36
+ "@types/node": "26.6.6",
37
+ "@types/timestring": "^7.0.0",
38
+ "esbuild": "0.25.12",
39
+ "typescript": "5.9.3"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "packageManager": "pnpm@12.16.1"
45
+ }
package/src/Proxy.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The response object for the request. Includes the
3
+ * response headers from the proxy service.
4
+ */
5
+ export interface ProxyResponse extends Response {
6
+ proxyHeaders: Headers;
7
+ }
8
+
9
+ /**
10
+ * Proxy requests through a specific proprietary proxy.
11
+ */
12
+ export class FetchProxy {
13
+ proxyEndpoint: string;
14
+
15
+ constructor(proxyEndpoint: string) {
16
+ this.proxyEndpoint = proxyEndpoint;
17
+ if (this.proxyEndpoint.length == 0)
18
+ throw new Error("Proxy endpoint must not be a blank string");
19
+ }
20
+
21
+ async fetch(
22
+ endpoint: string | URL,
23
+ options: RequestInit = {}
24
+ ): Promise<ProxyResponse> {
25
+ const configuredOptions: RequestInit = {
26
+ method: options.method ?? "GET",
27
+ body: options.body ?? undefined,
28
+ mode: options.mode ?? undefined,
29
+ keepalive: options.keepalive ?? undefined,
30
+ signal: options.signal ?? undefined,
31
+ credentials: options.credentials ?? undefined,
32
+ redirect: "manual",
33
+ headers: {
34
+ endpoint: typeof endpoint == "string" ? endpoint : endpoint.toString(),
35
+ /* cspell: disable-next-line */
36
+ "oproxy-options": JSON.stringify({
37
+ method: options.method ?? "GET",
38
+ mode: options.mode ?? undefined,
39
+ credentials: options.credentials ?? undefined,
40
+ redirect: options.redirect ?? undefined,
41
+ referrer: options.referrer ?? undefined,
42
+ referrerPolicy: options.referrerPolicy ?? undefined,
43
+ integrity: options.integrity ?? undefined,
44
+ keepalive: options.keepalive ?? undefined
45
+ }),
46
+ headers: options.headers == null ? "{}" : JSON.stringify(options.headers)
47
+ }
48
+ };
49
+ const res = await fetch(this.proxyEndpoint, configuredOptions);
50
+ const receivedHeaders = res.headers.has("incomingHeaders")
51
+ ? JSON.parse(res.headers?.get("incomingHeaders") ?? "")
52
+ : null;
53
+ const receivedStatus = res.headers.has("receivedStatus")
54
+ ? Number(res.headers.get("receivedStatus"))
55
+ : res.status;
56
+ Object.defineProperty(res, "status", { get: () => receivedStatus });
57
+ const proxyHeaders = res.headers;
58
+ Object.defineProperty(res, "proxyHeaders", { get: () => proxyHeaders });
59
+ const resWithHeaders: ProxyResponse = res as ProxyResponse;
60
+ if (receivedHeaders != null) {
61
+ Object.defineProperty(resWithHeaders, "headers", {
62
+ get: () => new Headers(receivedHeaders)
63
+ });
64
+ }
65
+ return resWithHeaders;
66
+ }
67
+ }
package/src/Type.ts ADDED
@@ -0,0 +1,137 @@
1
+ /// Copyright (c) 2019 dirigeants, MIT License
2
+
3
+ /**
4
+ * The class for deep checking Types
5
+ */
6
+ export class Type {
7
+ /**
8
+ * The value to generate a deep Type of
9
+ */ // biome-ignore lint/suspicious/noExplicitAny: This can truly be any
10
+ public value: any;
11
+
12
+ /**
13
+ * The shallow type of this
14
+ */
15
+ public is: string;
16
+
17
+ /**
18
+ * The parent of this type
19
+ */
20
+ private parent?: Type;
21
+
22
+ /**
23
+ * The child keys of this Type
24
+ */
25
+ private childKeys: Map<string, Type>;
26
+
27
+ /**
28
+ * The child values of this Type
29
+ */
30
+ private childValues: Map<string, Type>;
31
+
32
+ /**
33
+ * @param value - The value to generate a deep Type of
34
+ * @param parent - The parent value used in recursion
35
+ */ // biome-ignore lint/suspicious/noExplicitAny: This can truly be any
36
+ constructor(value: any, parent?: Type) {
37
+ this.value = value;
38
+ this.is = Type.resolve(value);
39
+ this.parent = parent;
40
+ this.childKeys = new Map();
41
+ this.childValues = new Map();
42
+ }
43
+
44
+ /**
45
+ * The type string for the children of this Type
46
+ */
47
+ get childTypes(): string {
48
+ if (!this.childValues.size) return "";
49
+ return `<${
50
+ (this.childKeys.size ? `${Type.list(this.childKeys)}, ` : "") +
51
+ Type.list(this.childValues)
52
+ }>`;
53
+ }
54
+
55
+ /**
56
+ * The full type string generated.
57
+ */
58
+ toString(): string {
59
+ this.check();
60
+ return this.is + this.childTypes;
61
+ }
62
+
63
+ /**
64
+ * The subtype to create based on this.value's sub value.
65
+ */ // biome-ignore lint/suspicious/noExplicitAny: This can truly be any
66
+ public addValue(value: any) {
67
+ const child = new Type(value, this);
68
+ this.childValues.set(child.is, child);
69
+ }
70
+
71
+ /**
72
+ * The subtype to create based on this.value's entries.
73
+ */ // biome-ignore lint/suspicious/noExplicitAny: This can truly be any
74
+ private addEntry([key, value]: [string, any]) {
75
+ const child = new Type(key, this);
76
+ this.childKeys.set(child.is, child);
77
+ this.addValue(value);
78
+ }
79
+
80
+ /**
81
+ * Walks the linked list backwards, for checking circulars.
82
+ */
83
+ private *parents() {
84
+ let current: Type | undefined = this;
85
+ // biome-ignore lint/suspicious/noAssignInExpressions: <Not sure why, this is how the original was written>
86
+ while ((current = current.parent)) yield current;
87
+ }
88
+
89
+ /**
90
+ * Get the deep type name that defines the input.
91
+ */
92
+ check() {
93
+ if (Object.isFrozen(this)) return;
94
+ if (typeof this.value === "object" && this.isCircular())
95
+ this.is = `[Circular:${this.is}]`;
96
+ else if (this.value instanceof Map)
97
+ for (const entry of this.value) this.addEntry(entry);
98
+ else if (Array.isArray(this.value) || this.value instanceof Set)
99
+ for (const value of this.value) this.addValue(value);
100
+ else if (this.is === "Object") this.is = "any";
101
+ Object.freeze(this);
102
+ }
103
+
104
+ /**
105
+ * Checks if the value of this Type is a circular reference to any parent.
106
+ */
107
+ isCircular(): boolean {
108
+ for (const parent of this.parents())
109
+ if (parent.value === this.value) return true;
110
+ return false;
111
+ }
112
+
113
+ /**
114
+ * Resolves the type name that defines the input.
115
+ */
116
+ // biome-ignore lint/suspicious/noExplicitAny: This can truly be any
117
+ static resolve(value: any): string {
118
+ const type = typeof value;
119
+ switch (type) {
120
+ case "object":
121
+ return value === null ? "null" : value.constructor?.name || "any";
122
+ case "function":
123
+ return `${value.constructor.name}(${value.length}-arity)`;
124
+ case "undefined":
125
+ return "void";
126
+ default:
127
+ return type;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Joins the list of child types.
133
+ */
134
+ static list(values: Map<string, Type>): string {
135
+ return values.has("any") ? "any" : [...values.values()].sort().join(" | ");
136
+ }
137
+ }
package/src/index.ts ADDED
@@ -0,0 +1,369 @@
1
+ import {
2
+ type ExecException,
3
+ type ExecOptions,
4
+ exec,
5
+ spawn
6
+ } from "node:child_process";
7
+ import { setTimeout } from "node:timers";
8
+ /* cspell: disable-next-line */
9
+ import timeString from "timestring";
10
+
11
+ export * from "./Proxy.ts";
12
+ export * from "./Type.ts";
13
+
14
+ /**
15
+ * Adds commas to a number, for pretty human reading
16
+ */
17
+ export function commaNumber(num: number): string {
18
+ // The previous implementation behaved correctly but this one is way cleaner
19
+ // https://stackoverflow.com/questions/2901102/how-can-i-format-a-number-with-commas-as-thousands-separators
20
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
21
+ }
22
+
23
+ /**
24
+ * Represents a parsed user-inputted time.
25
+ * Returned by parseUserInputtedTime.
26
+ */
27
+ export class ParsedTime {
28
+ public readonly intervalMs: number;
29
+ public readonly timestamp?: number;
30
+
31
+ constructor(data: { intervalMs: number; timestamp?: number }) {
32
+ this.intervalMs = data.intervalMs;
33
+ this.timestamp = data.timestamp;
34
+ }
35
+
36
+ get intervalSeconds(): number {
37
+ return Math.trunc(this.intervalMs / 1000);
38
+ }
39
+
40
+ get string(): string {
41
+ return msToHuman(this.intervalMs);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Parse user-inputted time. Timestamps can be allowed, optionally.
47
+ * relativeTime is used to calculate intervals when timestamps are
48
+ * inputted.
49
+ */
50
+ export function parseUserInputtedTime(
51
+ rawTimeInterval: string,
52
+ allowTimestamps = false,
53
+ relativeTime: number = Date.now()
54
+ ): ParsedTime {
55
+ const digits = rawTimeInterval.match(/\d/g);
56
+ if (
57
+ (rawTimeInterval.endsWith("t") ||
58
+ (digits != null &&
59
+ digits.length > 0 &&
60
+ digits.join("") == rawTimeInterval)) &&
61
+ allowTimestamps
62
+ ) {
63
+ let timestampString = rawTimeInterval.match(/\d/g)?.join("");
64
+ if (
65
+ timestampString == null ||
66
+ !isNumeric(timestampString, { allowDecimal: true, allowNegative: true })
67
+ )
68
+ timestampString = String(relativeTime);
69
+ const timestamp =
70
+ timestampString.length == String(relativeTime).length ||
71
+ timestampString.length == String(relativeTime).length + 1 ||
72
+ timestampString.length == String(relativeTime).length - 1
73
+ ? Number(timestampString)
74
+ : timestampString.length == String(relativeTime).length - 3 ||
75
+ timestampString.length == String(relativeTime).length - 2 ||
76
+ timestampString.length == String(relativeTime).length - 4
77
+ ? Number(timestampString) * 1000
78
+ : Number(timestampString);
79
+ const timeInterval = timestamp - relativeTime;
80
+ return new ParsedTime({ intervalMs: timeInterval, timestamp });
81
+ }
82
+ let timeInterval: number;
83
+ try {
84
+ timeInterval = timeString(rawTimeInterval, "ms");
85
+ } catch {
86
+ return { intervalSeconds: 0, intervalMs: 0, string: "" };
87
+ }
88
+ return new ParsedTime({ intervalMs: timeInterval });
89
+ }
90
+
91
+ /**
92
+ * Convert a total quantity of milliseconds into modulated ms, seconds, minutes, hours, days, weeks, years
93
+ */
94
+ function explodeMs(ms: number) {
95
+ return {
96
+ milliseconds: Math.trunc(ms % 1000),
97
+ seconds: Math.trunc((ms / 1000) % 60),
98
+ minutes: Math.trunc((ms / 60000) % 60),
99
+ hours: Math.trunc((ms / 3600000) % 24),
100
+ days: Math.trunc((ms / 86400000) % 7),
101
+ weeks: Math.trunc((ms / 604800000) % 52),
102
+ years: Math.trunc(ms / 31449600000)
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Converts a quantity of ms to human-readable format
108
+ */
109
+ export function msToHuman(ms: number, includeMilliseconds = false): string {
110
+ const { milliseconds, seconds, minutes, hours, days, weeks, years } =
111
+ explodeMs(ms);
112
+ const durationComponents: string[] = [];
113
+ if (years > 0) {
114
+ durationComponents.push(`${years} year${years == 1 ? "" : "s"}`);
115
+ }
116
+ if (weeks > 0) {
117
+ durationComponents.push(`${weeks} week${weeks == 1 ? "" : "s"}`);
118
+ }
119
+ if (days > 0) {
120
+ durationComponents.push(`${days} day${days == 1 ? "" : "s"}`);
121
+ }
122
+ if (hours > 0) {
123
+ durationComponents.push(`${hours} hour${hours == 1 ? "" : "s"}`);
124
+ }
125
+ if (minutes > 0) {
126
+ durationComponents.push(`${minutes} minute${minutes == 1 ? "" : "s"}`);
127
+ }
128
+ if (seconds > 0) {
129
+ durationComponents.push(`${seconds} second${seconds == 1 ? "" : "s"}`);
130
+ }
131
+ if (milliseconds > 0 && includeMilliseconds) {
132
+ durationComponents.push(
133
+ `${milliseconds} millisecond${milliseconds == 1 ? "" : "s"}`
134
+ );
135
+ }
136
+ if (durationComponents.length == 0)
137
+ durationComponents.push(
138
+ includeMilliseconds ? "no time" : "less than a second"
139
+ );
140
+ const durationWords =
141
+ durationComponents.length >= 3
142
+ ? `${durationComponents.slice(0, -1).join(", ")}, and ${durationComponents.at(-1)}`
143
+ : durationComponents.join(" and ");
144
+ return durationWords;
145
+ }
146
+
147
+ /**
148
+ * Converts a quantity of ms to a shorter human-readable format
149
+ */
150
+ export function msToShort(ms: number, includeMilliseconds = false): string {
151
+ const { milliseconds, seconds, minutes, hours, days, weeks, years } =
152
+ explodeMs(ms);
153
+ let twoDigitMin = String(minutes);
154
+ while (twoDigitMin.length < 2) {
155
+ twoDigitMin = `0${twoDigitMin}`;
156
+ }
157
+ let twoDigitSec = String(seconds);
158
+ while (twoDigitSec.length < 2) {
159
+ twoDigitSec = `0${twoDigitSec}`;
160
+ }
161
+ let threeDigitMs = String(milliseconds);
162
+ while (threeDigitMs.length < 3) {
163
+ threeDigitMs = `0${threeDigitMs}`;
164
+ }
165
+ const durationComponents: string[] = [];
166
+ let topFound = false;
167
+ if (years > 0 || topFound) {
168
+ topFound = true;
169
+ durationComponents.push(`${years}y`);
170
+ }
171
+ if (weeks > 0 || topFound) {
172
+ topFound = true;
173
+ durationComponents.push(`${weeks}w`);
174
+ }
175
+ if (days > 0 || topFound) {
176
+ topFound = true;
177
+ durationComponents.push(`${days}d `);
178
+ }
179
+ if (hours > 0 || topFound) {
180
+ topFound = true;
181
+ durationComponents.push(`${hours}:`);
182
+ }
183
+ if (minutes > 0 || topFound) {
184
+ durationComponents.push(`${topFound ? twoDigitMin : minutes}:${twoDigitSec}`);
185
+ if (includeMilliseconds) durationComponents.push(`.${threeDigitMs}`);
186
+ } else {
187
+ durationComponents.push(`${seconds}`);
188
+ if (includeMilliseconds) durationComponents.push(`.${threeDigitMs}`);
189
+ durationComponents.push("s");
190
+ }
191
+ return durationComponents.join("");
192
+ }
193
+
194
+ /**
195
+ * Generate a random string of a specific length. Uses Math.random().
196
+ */
197
+ export function randomString(
198
+ length: number,
199
+ alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
200
+ ): string {
201
+ let result = "";
202
+ let counter = 0;
203
+ while (counter < length) {
204
+ result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
205
+ counter += 1;
206
+ }
207
+ return result;
208
+ }
209
+
210
+ /**
211
+ * Check if a string is entirely numeric.
212
+ */
213
+ export function isNumeric(
214
+ str: string | undefined | null,
215
+ options: { allowNegative: boolean; allowDecimal: boolean }
216
+ ): boolean {
217
+ if (str == null) return false;
218
+ let regex: RegExp;
219
+ if (options.allowNegative) {
220
+ if (options.allowDecimal) {
221
+ regex = /^-?\d+(\.\d+)?$/;
222
+ } else {
223
+ regex = /^-?\d+$/;
224
+ }
225
+ } else {
226
+ if (options.allowDecimal) {
227
+ regex = /^\d+(\.\d+)?$/;
228
+ } else {
229
+ regex = /^\d+$/;
230
+ }
231
+ }
232
+ return regex.test(str);
233
+ }
234
+
235
+ /**
236
+ * Returns a promise that resolves after a given number of milliseconds.
237
+ */
238
+ export function sleep(ms: number): Promise<void> {
239
+ return new Promise((resolve) => {
240
+ setTimeout(resolve, ms);
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Asynchronously execute a command-line command.
246
+ */
247
+ export function execAsync(
248
+ command: string,
249
+ settings?: ExecOptions
250
+ ): Promise<{
251
+ error: ExecException | null;
252
+ stdout: string;
253
+ stderr: string;
254
+ }> {
255
+ return new Promise((resolve) => {
256
+ exec(command, settings, (error, stdout, stderr) => {
257
+ resolve({ error, stdout: stdout as string, stderr: stderr as string });
258
+ });
259
+ });
260
+ }
261
+
262
+ /**
263
+ * A lightweight curl wrapper. Input arguments
264
+ * for curl, and a web API Response object is returned.
265
+ */
266
+ export async function curl(...curlArgs: string[]): Promise<Response> {
267
+ const child = spawn("curl", ["-isSL", ...curlArgs]);
268
+
269
+ const stdoutChunks: Buffer[] = [];
270
+ const stderrChunks: Buffer[] = [];
271
+
272
+ child.stdout.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
273
+ child.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
274
+
275
+ const code = await new Promise<number>((resolve, reject) => {
276
+ child.on("close", (c) => resolve(c ?? -1));
277
+ child.on("error", reject);
278
+ });
279
+
280
+ const stdoutText = new TextDecoder().decode(Buffer.concat(stdoutChunks));
281
+ const stderrText = new TextDecoder().decode(Buffer.concat(stderrChunks));
282
+
283
+ if (code !== 0) {
284
+ console.error("Curl stderr:");
285
+ console.error(stderrText);
286
+ throw new Error(`Curl exited with code ${code}`);
287
+ }
288
+
289
+ return parseRawHttpResponse(stdoutText);
290
+ }
291
+
292
+ /**
293
+ * Parses a raw HTTP response into a fetch Response
294
+ */
295
+ export function parseRawHttpResponse(raw: string): Response {
296
+ const [headerPart, ...bodyParts] = raw.split(/\r?\n\r?\n/);
297
+ const body = bodyParts.join("\n\n");
298
+
299
+ const lines = headerPart.split(/\r?\n/);
300
+ const statusLine = lines.shift();
301
+ if (statusLine === undefined)
302
+ throw new Error("Curl HTTP Response Syntax Error: No status line!");
303
+
304
+ const [, statusCode, ...statusTextParts] = statusLine.split(" ");
305
+ const status = Number(statusCode);
306
+ const statusText = statusTextParts.join(" ");
307
+
308
+ const headers = new Headers();
309
+ for (const line of lines) {
310
+ const idx = line.indexOf(":");
311
+ if (idx !== -1) {
312
+ headers.append(line.slice(0, idx).trim(), line.slice(idx + 1).trim());
313
+ }
314
+ }
315
+
316
+ const isNullBodyStatus =
317
+ status === 101 || status === 204 || status === 205 || status === 304;
318
+
319
+ return new Response(isNullBodyStatus ? null : body, {
320
+ status,
321
+ statusText,
322
+ headers
323
+ });
324
+ }
325
+
326
+ class PrivateTimeoutIndicator {}
327
+
328
+ /**
329
+ * Await a promise, but reject if the promise takes too long.
330
+ */
331
+ export async function withTimeout<T>(
332
+ promise: Promise<T>,
333
+ timeout: number
334
+ ): Promise<T> {
335
+ const promises = [
336
+ promise,
337
+ sleep(timeout).then(() => new PrivateTimeoutIndicator())
338
+ ];
339
+ const result = await Promise.race(promises);
340
+ if (result instanceof PrivateTimeoutIndicator) {
341
+ throw new Error("Timeout reached when awaiting promise resolution!");
342
+ }
343
+ return result;
344
+ }
345
+
346
+ /**
347
+ * Similar to `Array#splice()` but for two Uint8Arrays.
348
+ * Replaces `deleteCount` bytes of `array` starting from `start`
349
+ * with the contents of `toInsert`. If `toInsert` is not given, just deletes `deleteCount`.
350
+ */
351
+ export function spliceUint8Array(
352
+ array: Uint8Array,
353
+ start: number,
354
+ deleteCount: number,
355
+ toInsert?: Uint8Array
356
+ ): Uint8Array<ArrayBuffer> {
357
+ const s = Math.max(0, Math.min(Math.trunc(start) || 0, array.length));
358
+ const dc = Math.max(
359
+ 0,
360
+ Math.min(Math.trunc(deleteCount) || 0, array.length - s)
361
+ );
362
+ const output = new Uint8Array(array.length - dc + (toInsert?.length ?? 0));
363
+ const before = array.slice(0, s);
364
+ const after = array.slice(s + dc, array.length);
365
+ output.set(before);
366
+ if (toInsert != undefined) output.set(toInsert, s);
367
+ output.set(after, s + (toInsert?.length ?? 0));
368
+ return output;
369
+ }