@srvquery/core 0.0.1-next.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/README.md +130 -0
- package/dist/bin/buffer-cursor.d.ts +44 -0
- package/dist/bin/buffer-cursor.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +361 -0
- package/dist/net/dns.d.ts +13 -0
- package/dist/net/dns.d.ts.map +1 -0
- package/dist/net/errors.d.ts +45 -0
- package/dist/net/errors.d.ts.map +1 -0
- package/dist/net/http-client.d.ts +45 -0
- package/dist/net/http-client.d.ts.map +1 -0
- package/dist/net/udp-socket.d.ts +66 -0
- package/dist/net/udp-socket.d.ts.map +1 -0
- package/dist/util/retry.d.ts +28 -0
- package/dist/util/retry.d.ts.map +1 -0
- package/dist/util/timing.d.ts +5 -0
- package/dist/util/timing.d.ts.map +1 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @srvquery/core
|
|
2
|
+
|
|
3
|
+
 
|
|
4
|
+
|
|
5
|
+
Shared binary parsing and UDP query primitives for `srvquery` packages.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @srvquery/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
### BufferCursor
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { BufferCursor } from "@srvquery/core";
|
|
19
|
+
|
|
20
|
+
const cursor = new BufferCursor(
|
|
21
|
+
Buffer.from([
|
|
22
|
+
0x2a, // UInt8
|
|
23
|
+
0x34,
|
|
24
|
+
0x12, // UInt16LE
|
|
25
|
+
0xff,
|
|
26
|
+
0xff,
|
|
27
|
+
0xff,
|
|
28
|
+
0xff, // Int32LE
|
|
29
|
+
0x78,
|
|
30
|
+
0x56,
|
|
31
|
+
0x34,
|
|
32
|
+
0x12, // UInt32LE
|
|
33
|
+
0x00,
|
|
34
|
+
0x00,
|
|
35
|
+
0xc0,
|
|
36
|
+
0x3f, // FloatLE
|
|
37
|
+
0x01,
|
|
38
|
+
0x00,
|
|
39
|
+
0x00,
|
|
40
|
+
0x00,
|
|
41
|
+
0x00,
|
|
42
|
+
0x00,
|
|
43
|
+
0x00,
|
|
44
|
+
0x00, // BigUInt64LE
|
|
45
|
+
0x68,
|
|
46
|
+
0x69,
|
|
47
|
+
0x00, // C string
|
|
48
|
+
0xaa,
|
|
49
|
+
0xbb, // bytes
|
|
50
|
+
0xcc, // skipped byte
|
|
51
|
+
0xdd,
|
|
52
|
+
0xee, // remaining bytes
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
console.log(cursor.offset); // 0
|
|
57
|
+
console.log(cursor.readUInt8()); // 42
|
|
58
|
+
console.log(cursor.readUInt16LE()); // 4660
|
|
59
|
+
console.log(cursor.readInt32LE()); // -1
|
|
60
|
+
console.log(cursor.readUInt32LE()); // 305419896
|
|
61
|
+
console.log(cursor.readFloatLE()); // 1.5
|
|
62
|
+
console.log(cursor.readBigUInt64LE()); // 1n
|
|
63
|
+
console.log(cursor.readCString()); // "hi"
|
|
64
|
+
console.log(cursor.readBytes(2)); // <Buffer aa bb>
|
|
65
|
+
|
|
66
|
+
cursor.skip(1);
|
|
67
|
+
|
|
68
|
+
console.log(cursor.remaining); // 2
|
|
69
|
+
console.log(cursor.readRemaining()); // <Buffer dd ee>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### createUdpSocket
|
|
73
|
+
|
|
74
|
+
Raw UDP sockets do not retry unless `retry` is provided. `using` invokes `Symbol.dispose` when the
|
|
75
|
+
socket leaves scope:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { backoffStrategy, createUdpSocket } from "@srvquery/core";
|
|
79
|
+
|
|
80
|
+
using socket = createUdpSocket(
|
|
81
|
+
{
|
|
82
|
+
host: "127.0.0.1",
|
|
83
|
+
port: 27015,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
timeout: 2_000,
|
|
87
|
+
type: "udp4",
|
|
88
|
+
retry: {
|
|
89
|
+
retries: 3,
|
|
90
|
+
strategy: backoffStrategy,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const packets = await socket.send(
|
|
96
|
+
{ payload: Buffer.from("status\0") },
|
|
97
|
+
{
|
|
98
|
+
accept: (packet) => packet.length > 0,
|
|
99
|
+
end: (accepted) => accepted.length === 1,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
console.log(packets[0]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Call `close` when the lifetime cannot be expressed with `using`:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const socket = createUdpSocket({ host: "127.0.0.1", port: 27015 });
|
|
110
|
+
|
|
111
|
+
socket.close();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### withRetry
|
|
115
|
+
|
|
116
|
+
Use the retry helper independently when an asynchronous operation needs the same retry behavior:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { backoffStrategy, withRetry } from "@srvquery/core";
|
|
120
|
+
|
|
121
|
+
const result = await withRetry(query, {
|
|
122
|
+
retries: 3,
|
|
123
|
+
strategy: backoffStrategy,
|
|
124
|
+
fatal: (error) => error instanceof TypeError,
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The exported `RetryStrategy` type has the signature `(attempt: number) => number`. Its return value
|
|
129
|
+
is the delay in milliseconds before the next attempt. The included `backoffStrategy` uses
|
|
130
|
+
exponential delays of 100 ms, 200 ms, 400 ms, and so on.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Reads typed values sequentially from a buffer while tracking the current offset. */
|
|
2
|
+
export declare class BufferCursor {
|
|
3
|
+
#private;
|
|
4
|
+
readonly buffer: Buffer;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a cursor positioned at the start of a buffer.
|
|
7
|
+
* @param buffer Buffer to read from.
|
|
8
|
+
*/
|
|
9
|
+
constructor(buffer: Buffer);
|
|
10
|
+
/** Zero-based position of the next byte to read. */
|
|
11
|
+
get offset(): number;
|
|
12
|
+
/** Number of unread bytes after the current offset. */
|
|
13
|
+
get remaining(): number;
|
|
14
|
+
/** Reads an unsigned 8-bit integer and advances the cursor by one byte. */
|
|
15
|
+
readUInt8(): number;
|
|
16
|
+
/** Reads a little-endian unsigned 16-bit integer and advances the cursor by two bytes. */
|
|
17
|
+
readUInt16LE(): number;
|
|
18
|
+
/** Reads a little-endian signed 32-bit integer and advances the cursor by four bytes. */
|
|
19
|
+
readInt32LE(): number;
|
|
20
|
+
/** Reads a little-endian unsigned 32-bit integer and advances the cursor by four bytes. */
|
|
21
|
+
readUInt32LE(): number;
|
|
22
|
+
/** Reads a little-endian 32-bit float and advances the cursor by four bytes. */
|
|
23
|
+
readFloatLE(): number;
|
|
24
|
+
/** Reads a little-endian unsigned 64-bit integer and advances the cursor by eight bytes. */
|
|
25
|
+
readBigUInt64LE(): bigint;
|
|
26
|
+
/**
|
|
27
|
+
* Reads a null-terminated UTF-8 string.
|
|
28
|
+
* Advances to the byte after the terminator, or to the end when no terminator is present.
|
|
29
|
+
*/
|
|
30
|
+
readCString(): string;
|
|
31
|
+
/**
|
|
32
|
+
* Reads a buffer view of the requested length and advances the cursor.
|
|
33
|
+
* @throws {RangeError} When `length` is invalid or exceeds the remaining bytes.
|
|
34
|
+
*/
|
|
35
|
+
readBytes(length: number): Buffer;
|
|
36
|
+
/**
|
|
37
|
+
* Advances the cursor without reading bytes.
|
|
38
|
+
* @throws {RangeError} When `length` is invalid or exceeds the remaining bytes.
|
|
39
|
+
*/
|
|
40
|
+
skip(length: number): void;
|
|
41
|
+
/** Reads all unread bytes and advances the cursor to the end of the buffer. */
|
|
42
|
+
readRemaining(): Buffer;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=buffer-cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffer-cursor.d.ts","sourceRoot":"","sources":["../../src/bin/buffer-cursor.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,qBAAa,YAAY;;IAOX,QAAQ,CAAC,MAAM,EAAE,MAAM;IAJnC;;;OAGG;IACH,YAAqB,MAAM,EAAE,MAAM,EAAI;IAEvC,oDAAoD;IACpD,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,uDAAuD;IACvD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,2EAA2E;IAC3E,SAAS,IAAI,MAAM,CAIlB;IAED,0FAA0F;IAC1F,YAAY,IAAI,MAAM,CAIrB;IAED,yFAAyF;IACzF,WAAW,IAAI,MAAM,CAIpB;IAED,2FAA2F;IAC3F,YAAY,IAAI,MAAM,CAIrB;IAED,gFAAgF;IAChF,WAAW,IAAI,MAAM,CAIpB;IAED,4FAA4F;IAC5F,eAAe,IAAI,MAAM,CAIxB;IAED;;;OAGG;IACH,WAAW,IAAI,MAAM,CASpB;IAED;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOhC;IAED;;;OAGG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAKzB;IAED,+EAA+E;IAC/E,aAAa,IAAI,MAAM,CAItB;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { BufferCursor } from "./bin/buffer-cursor";
|
|
2
|
+
export { type CreateUdpSocketOptions, type CreateUdpSocketParams, type UdpSocketSendOptions, type UdpSocketSendParams, type UdpSocket, createUdpSocket, } from "./net/udp-socket";
|
|
3
|
+
export { type CreateHttpClientOptions, type CreateHttpClientParams, type HttpClient, createHttpClient, } from "./net/http-client";
|
|
4
|
+
export { QueryDnsResolutionError, QueryTransportError, QueryTimeoutError } from "./net/errors";
|
|
5
|
+
export { resolveIpv4 } from "./net/dns";
|
|
6
|
+
export { type RetryOptions, type RetryStrategy, backoffStrategy, defaultRetryOptions, withRetry, } from "./util/retry";
|
|
7
|
+
export { defaultTimeout } from "./util/timing";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAInD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,eAAe,EACf,mBAAmB,EACnB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import dgram from "node:dgram";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import dns from "node:dns";
|
|
5
|
+
//#region src/bin/buffer-cursor.ts
|
|
6
|
+
/** Reads typed values sequentially from a buffer while tracking the current offset. */
|
|
7
|
+
var BufferCursor = class {
|
|
8
|
+
#offset = 0;
|
|
9
|
+
/**
|
|
10
|
+
* Creates a cursor positioned at the start of a buffer.
|
|
11
|
+
* @param buffer Buffer to read from.
|
|
12
|
+
*/
|
|
13
|
+
constructor(buffer) {
|
|
14
|
+
this.buffer = buffer;
|
|
15
|
+
}
|
|
16
|
+
/** Zero-based position of the next byte to read. */
|
|
17
|
+
get offset() {
|
|
18
|
+
return this.#offset;
|
|
19
|
+
}
|
|
20
|
+
/** Number of unread bytes after the current offset. */
|
|
21
|
+
get remaining() {
|
|
22
|
+
return this.buffer.length - this.#offset;
|
|
23
|
+
}
|
|
24
|
+
/** Reads an unsigned 8-bit integer and advances the cursor by one byte. */
|
|
25
|
+
readUInt8() {
|
|
26
|
+
const value = this.buffer.readUInt8(this.#offset);
|
|
27
|
+
this.#offset += 1;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
/** Reads a little-endian unsigned 16-bit integer and advances the cursor by two bytes. */
|
|
31
|
+
readUInt16LE() {
|
|
32
|
+
const value = this.buffer.readUInt16LE(this.#offset);
|
|
33
|
+
this.#offset += 2;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/** Reads a little-endian signed 32-bit integer and advances the cursor by four bytes. */
|
|
37
|
+
readInt32LE() {
|
|
38
|
+
const value = this.buffer.readInt32LE(this.#offset);
|
|
39
|
+
this.#offset += 4;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
/** Reads a little-endian unsigned 32-bit integer and advances the cursor by four bytes. */
|
|
43
|
+
readUInt32LE() {
|
|
44
|
+
const value = this.buffer.readUInt32LE(this.#offset);
|
|
45
|
+
this.#offset += 4;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
/** Reads a little-endian 32-bit float and advances the cursor by four bytes. */
|
|
49
|
+
readFloatLE() {
|
|
50
|
+
const value = this.buffer.readFloatLE(this.#offset);
|
|
51
|
+
this.#offset += 4;
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** Reads a little-endian unsigned 64-bit integer and advances the cursor by eight bytes. */
|
|
55
|
+
readBigUInt64LE() {
|
|
56
|
+
const value = this.buffer.readBigUInt64LE(this.#offset);
|
|
57
|
+
this.#offset += 8;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads a null-terminated UTF-8 string.
|
|
62
|
+
* Advances to the byte after the terminator, or to the end when no terminator is present.
|
|
63
|
+
*/
|
|
64
|
+
readCString() {
|
|
65
|
+
const terminator = this.buffer.indexOf(0, this.#offset);
|
|
66
|
+
const value = this.buffer.toString("utf8", this.#offset, terminator === -1 ? this.buffer.length : terminator);
|
|
67
|
+
this.#offset = terminator === -1 ? this.buffer.length : terminator + 1;
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reads a buffer view of the requested length and advances the cursor.
|
|
72
|
+
* @throws {RangeError} When `length` is invalid or exceeds the remaining bytes.
|
|
73
|
+
*/
|
|
74
|
+
readBytes(length) {
|
|
75
|
+
if (!Number.isInteger(length) || length < 0 || length > this.remaining) throw new RangeError("Read length is outside the buffer bounds");
|
|
76
|
+
const value = this.buffer.subarray(this.#offset, this.#offset + length);
|
|
77
|
+
this.#offset += length;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Advances the cursor without reading bytes.
|
|
82
|
+
* @throws {RangeError} When `length` is invalid or exceeds the remaining bytes.
|
|
83
|
+
*/
|
|
84
|
+
skip(length) {
|
|
85
|
+
if (!Number.isInteger(length) || length < 0 || length > this.remaining) throw new RangeError("Skip length is outside the buffer bounds");
|
|
86
|
+
this.#offset += length;
|
|
87
|
+
}
|
|
88
|
+
/** Reads all unread bytes and advances the cursor to the end of the buffer. */
|
|
89
|
+
readRemaining() {
|
|
90
|
+
const value = this.buffer.subarray(this.#offset);
|
|
91
|
+
this.#offset = this.buffer.length;
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/net/errors.ts
|
|
97
|
+
/**
|
|
98
|
+
* Error thrown when a query does not receive any response within the
|
|
99
|
+
* configured timeout window, after all retries have been exhausted.
|
|
100
|
+
*/
|
|
101
|
+
var QueryTimeoutError = class extends Error {
|
|
102
|
+
constructor({ host, port, attempts }) {
|
|
103
|
+
super(`Query to ${host}:${port} timed out after ${attempts} attempt${attempts === 1 ? "" : "s"}`);
|
|
104
|
+
this.name = this.constructor.name;
|
|
105
|
+
this.host = host;
|
|
106
|
+
this.port = port;
|
|
107
|
+
this.attempts = attempts;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Error thrown for transport-level failures unrelated to timeout: low-level socket failures
|
|
112
|
+
* (e.g. `ECONNREFUSED`, `EHOSTUNREACH`), non-2xx HTTP responses, and malformed response bodies.
|
|
113
|
+
*/
|
|
114
|
+
var QueryTransportError = class extends Error {
|
|
115
|
+
constructor({ message, cause }) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = this.constructor.name;
|
|
118
|
+
this.cause = cause;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** Error thrown when a hostname cannot be resolved to an IPv4 address. */
|
|
122
|
+
var QueryDnsResolutionError = class extends Error {
|
|
123
|
+
constructor({ host, cause }) {
|
|
124
|
+
super(`Failed to resolve host "${host}" to an IPv4 address`);
|
|
125
|
+
this.name = this.constructor.name;
|
|
126
|
+
this.host = host;
|
|
127
|
+
this.cause = cause;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/util/timing.ts
|
|
132
|
+
/** Resolves after the requested number of milliseconds. */
|
|
133
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
+
/** Default time in milliseconds to wait for a response to a single request attempt. */
|
|
135
|
+
const defaultTimeout = 2e3;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/util/retry.ts
|
|
138
|
+
/** Exponential retry strategy producing delays of 100 ms, 200 ms, 400 ms, and so on. */
|
|
139
|
+
const backoffStrategy = (attempt) => 100 * 2 ** (attempt - 1);
|
|
140
|
+
/** Default retry policy: three attempts with exponential backoff. */
|
|
141
|
+
const defaultRetryOptions = Object.freeze({
|
|
142
|
+
retries: 3,
|
|
143
|
+
strategy: backoffStrategy
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* Calls `fn` up to `options.retries` times, retrying on rejection unless
|
|
147
|
+
* `fatal` says otherwise. Throws the last error when every attempt fails.
|
|
148
|
+
* @param fn Asynchronous operation to execute.
|
|
149
|
+
* @param options Attempt count, delay strategy, and fatal-error predicate.
|
|
150
|
+
* @returns The first successful result.
|
|
151
|
+
* @throws The fatal error or the final error after all attempts fail.
|
|
152
|
+
*/
|
|
153
|
+
async function withRetry(fn, { retries, fatal, strategy }) {
|
|
154
|
+
if (!Number.isInteger(retries) || retries < 1) throw new RangeError("Retry option 'retries' must be a positive integer");
|
|
155
|
+
let lastError;
|
|
156
|
+
for (let attempt = 1; attempt <= retries; attempt++) try {
|
|
157
|
+
return await fn();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
lastError = err;
|
|
160
|
+
if (fatal?.(err)) throw err;
|
|
161
|
+
if (attempt < retries && strategy) await delay(strategy(attempt));
|
|
162
|
+
}
|
|
163
|
+
throw lastError;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/net/udp-socket.ts
|
|
167
|
+
/**
|
|
168
|
+
* Creates a reusable UDP query socket for one destination.
|
|
169
|
+
*
|
|
170
|
+
* Each `send` call collects accepted packets until its completion callback succeeds. When retry
|
|
171
|
+
* options are configured, a timed-out or failed send is repeated on the same socket.
|
|
172
|
+
*/
|
|
173
|
+
const createUdpSocket = ({ host, port }, { timeout = defaultTimeout, type = "udp4", retry } = {}) => {
|
|
174
|
+
const socket = dgram.createSocket({ type });
|
|
175
|
+
const sendAttempt = ({ payload }, { accept = () => true, end = () => true } = {}) => {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
let settled = false;
|
|
178
|
+
const packets = [];
|
|
179
|
+
let timer;
|
|
180
|
+
const settle = (fn) => {
|
|
181
|
+
if (settled) return;
|
|
182
|
+
settled = true;
|
|
183
|
+
cleanup();
|
|
184
|
+
fn();
|
|
185
|
+
};
|
|
186
|
+
const resetTimer = () => {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
timer = setTimeout(() => {
|
|
189
|
+
settle(() => reject(new QueryTimeoutError({
|
|
190
|
+
host,
|
|
191
|
+
port,
|
|
192
|
+
attempts: 1
|
|
193
|
+
})));
|
|
194
|
+
}, timeout);
|
|
195
|
+
};
|
|
196
|
+
const onMessage = (msg) => {
|
|
197
|
+
if (settled || !accept(msg)) return;
|
|
198
|
+
packets.push(msg);
|
|
199
|
+
if (end(packets)) settle(() => resolve(packets));
|
|
200
|
+
else resetTimer();
|
|
201
|
+
};
|
|
202
|
+
const onError = (err) => {
|
|
203
|
+
settle(() => reject(new QueryTransportError({
|
|
204
|
+
message: `Socket error querying ${host}:${port}`,
|
|
205
|
+
cause: err
|
|
206
|
+
})));
|
|
207
|
+
};
|
|
208
|
+
const cleanup = () => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
socket.off("message", onMessage);
|
|
211
|
+
socket.off("error", onError);
|
|
212
|
+
};
|
|
213
|
+
socket.on("message", onMessage);
|
|
214
|
+
socket.on("error", onError);
|
|
215
|
+
resetTimer();
|
|
216
|
+
socket.send(payload, port, host, (err) => {
|
|
217
|
+
if (err) settle(() => reject(new QueryTransportError({
|
|
218
|
+
message: `Failed to send to ${host}:${port}`,
|
|
219
|
+
cause: err
|
|
220
|
+
})));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const send = async (params, options = {}) => {
|
|
225
|
+
if (!retry) return sendAttempt(params, options);
|
|
226
|
+
try {
|
|
227
|
+
return await withRetry(() => sendAttempt(params, options), retry);
|
|
228
|
+
} catch (lastError) {
|
|
229
|
+
if (lastError instanceof QueryTimeoutError) throw new QueryTimeoutError({
|
|
230
|
+
host,
|
|
231
|
+
port,
|
|
232
|
+
attempts: retry.retries
|
|
233
|
+
});
|
|
234
|
+
throw lastError;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
send,
|
|
239
|
+
ctx: {
|
|
240
|
+
host,
|
|
241
|
+
port
|
|
242
|
+
},
|
|
243
|
+
close: () => {
|
|
244
|
+
socket.close();
|
|
245
|
+
},
|
|
246
|
+
[Symbol.dispose]: () => {
|
|
247
|
+
socket.close();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/net/http-client.ts
|
|
253
|
+
const transports = {
|
|
254
|
+
http,
|
|
255
|
+
https
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Creates a reusable HTTP query client for one destination.
|
|
259
|
+
*
|
|
260
|
+
* Intended for query protocols that expose their state over plain JSON HTTP endpoints. Each `getJson` call is subject to the configured timeout and, when provided, retried according to `options.retry`.
|
|
261
|
+
*/
|
|
262
|
+
const createHttpClient = ({ host, port }, { protocol = "http", timeout = defaultTimeout, retry } = {}) => {
|
|
263
|
+
const { request } = transports[protocol];
|
|
264
|
+
const attempt = (path) => new Promise((resolve, reject) => {
|
|
265
|
+
const req = request({
|
|
266
|
+
host,
|
|
267
|
+
port,
|
|
268
|
+
path,
|
|
269
|
+
method: "GET"
|
|
270
|
+
}, (res) => {
|
|
271
|
+
const chunks = [];
|
|
272
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
273
|
+
res.on("error", (err) => {
|
|
274
|
+
reject(new QueryTransportError({
|
|
275
|
+
message: `HTTP response error from ${protocol}://${host}:${port}${path}`,
|
|
276
|
+
cause: err
|
|
277
|
+
}));
|
|
278
|
+
});
|
|
279
|
+
res.on("end", () => {
|
|
280
|
+
const { statusCode = 0 } = res;
|
|
281
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
282
|
+
reject(new QueryTransportError({
|
|
283
|
+
message: `Received HTTP ${statusCode} from ${protocol}://${host}:${port}${path}`,
|
|
284
|
+
cause: void 0
|
|
285
|
+
}));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
290
|
+
} catch (err) {
|
|
291
|
+
reject(new QueryTransportError({
|
|
292
|
+
message: `Received a non-JSON response from ${protocol}://${host}:${port}${path}`,
|
|
293
|
+
cause: err
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
req.setTimeout(timeout, () => {
|
|
299
|
+
req.destroy(new QueryTimeoutError({
|
|
300
|
+
host,
|
|
301
|
+
port,
|
|
302
|
+
attempts: 1
|
|
303
|
+
}));
|
|
304
|
+
});
|
|
305
|
+
req.on("error", (err) => {
|
|
306
|
+
if (err instanceof QueryTimeoutError) {
|
|
307
|
+
reject(err);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
reject(new QueryTransportError({
|
|
311
|
+
message: `Failed to query ${protocol}://${host}:${port}${path}`,
|
|
312
|
+
cause: err
|
|
313
|
+
}));
|
|
314
|
+
});
|
|
315
|
+
req.end();
|
|
316
|
+
});
|
|
317
|
+
const getJson = async (path) => {
|
|
318
|
+
if (!retry) return attempt(path);
|
|
319
|
+
try {
|
|
320
|
+
return await withRetry(() => attempt(path), retry);
|
|
321
|
+
} catch (lastError) {
|
|
322
|
+
if (lastError instanceof QueryTimeoutError) throw new QueryTimeoutError({
|
|
323
|
+
host,
|
|
324
|
+
port,
|
|
325
|
+
attempts: retry.retries
|
|
326
|
+
});
|
|
327
|
+
throw lastError;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
return {
|
|
331
|
+
getJson,
|
|
332
|
+
ctx: {
|
|
333
|
+
host,
|
|
334
|
+
port
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/net/dns.ts
|
|
340
|
+
/**
|
|
341
|
+
* Resolves a hostname or IP address to its IPv4 address.
|
|
342
|
+
*
|
|
343
|
+
* Useful for protocols that must embed a numeric IPv4 address in a request
|
|
344
|
+
* packet (rather than a hostname), since query protocols typically operate
|
|
345
|
+
* directly over raw sockets without a resolver of their own.
|
|
346
|
+
*
|
|
347
|
+
* @param host The hostname or IP address to resolve.
|
|
348
|
+
* @returns A promise that resolves to the IPv4 address.
|
|
349
|
+
* @throws {QueryDnsResolutionError} If the host cannot be resolved to an IPv4 address.
|
|
350
|
+
*/
|
|
351
|
+
const resolveIpv4 = (host) => new Promise((resolve, reject) => {
|
|
352
|
+
dns.lookup(host, { family: 4 }, (err, address) => {
|
|
353
|
+
if (err) reject(new QueryDnsResolutionError({
|
|
354
|
+
host,
|
|
355
|
+
cause: err
|
|
356
|
+
}));
|
|
357
|
+
else resolve(address);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
//#endregion
|
|
361
|
+
export { BufferCursor, QueryDnsResolutionError, QueryTimeoutError, QueryTransportError, backoffStrategy, createHttpClient, createUdpSocket, defaultRetryOptions, defaultTimeout, resolveIpv4, withRetry };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a hostname or IP address to its IPv4 address.
|
|
3
|
+
*
|
|
4
|
+
* Useful for protocols that must embed a numeric IPv4 address in a request
|
|
5
|
+
* packet (rather than a hostname), since query protocols typically operate
|
|
6
|
+
* directly over raw sockets without a resolver of their own.
|
|
7
|
+
*
|
|
8
|
+
* @param host The hostname or IP address to resolve.
|
|
9
|
+
* @returns A promise that resolves to the IPv4 address.
|
|
10
|
+
* @throws {QueryDnsResolutionError} If the host cannot be resolved to an IPv4 address.
|
|
11
|
+
*/
|
|
12
|
+
export declare const resolveIpv4: (host: string) => Promise<string>;
|
|
13
|
+
//# sourceMappingURL=dns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.d.ts","sourceRoot":"","sources":["../../src/net/dns.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAMrD,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type QueryTimeoutErrorCtor = {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
attempts: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when a query does not receive any response within the
|
|
8
|
+
* configured timeout window, after all retries have been exhausted.
|
|
9
|
+
*/
|
|
10
|
+
export declare class QueryTimeoutError extends Error {
|
|
11
|
+
/** Host targeted by the query. */
|
|
12
|
+
readonly host: string;
|
|
13
|
+
/** Port targeted by the query. */
|
|
14
|
+
readonly port: number;
|
|
15
|
+
/** Total number of attempts made before the timeout was reported. */
|
|
16
|
+
readonly attempts: number;
|
|
17
|
+
constructor({ host, port, attempts }: QueryTimeoutErrorCtor);
|
|
18
|
+
}
|
|
19
|
+
type QueryTransportErrorCtor = {
|
|
20
|
+
message: string;
|
|
21
|
+
cause: unknown;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Error thrown for transport-level failures unrelated to timeout: low-level socket failures
|
|
25
|
+
* (e.g. `ECONNREFUSED`, `EHOSTUNREACH`), non-2xx HTTP responses, and malformed response bodies.
|
|
26
|
+
*/
|
|
27
|
+
export declare class QueryTransportError extends Error {
|
|
28
|
+
/** Original error that caused the query to fail, if any. */
|
|
29
|
+
readonly cause: unknown;
|
|
30
|
+
constructor({ message, cause }: QueryTransportErrorCtor);
|
|
31
|
+
}
|
|
32
|
+
type QueryDnsResolutionErrorCtor = {
|
|
33
|
+
host: string;
|
|
34
|
+
cause: unknown;
|
|
35
|
+
};
|
|
36
|
+
/** Error thrown when a hostname cannot be resolved to an IPv4 address. */
|
|
37
|
+
export declare class QueryDnsResolutionError extends Error {
|
|
38
|
+
/** Hostname that failed to resolve. */
|
|
39
|
+
readonly host: string;
|
|
40
|
+
/** Original DNS lookup error. */
|
|
41
|
+
readonly cause: unknown;
|
|
42
|
+
constructor({ host, cause }: QueryDnsResolutionErrorCtor);
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/net/errors.ts"],"names":[],"mappings":"AAAA,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,kCAAkC;IAClC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,kCAAkC;IAClC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,SAAgB,QAAQ,EAAE,MAAM,CAAC;IAEjC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,qBAAqB,EAQ1D;CACF;AAED,KAAK,uBAAuB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,4DAA4D;IAC5D,SAAgB,KAAK,EAAE,OAAO,CAAC;IAE/B,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,uBAAuB,EAItD;CACF;AAED,KAAK,2BAA2B,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,0EAA0E;AAC1E,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,uCAAuC;IACvC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,iCAAiC;IACjC,SAAgB,KAAK,EAAE,OAAO,CAAC;IAE/B,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,2BAA2B,EAKvD;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RetryOptions } from "../util/retry";
|
|
2
|
+
/** Destination addressed by an HTTP query client. */
|
|
3
|
+
export type CreateHttpClientParams = {
|
|
4
|
+
/**
|
|
5
|
+
* Target host.
|
|
6
|
+
*/
|
|
7
|
+
host: string;
|
|
8
|
+
/**
|
|
9
|
+
* Target port.
|
|
10
|
+
*/
|
|
11
|
+
port: number;
|
|
12
|
+
};
|
|
13
|
+
/** Transport and retry settings for an HTTP query client. */
|
|
14
|
+
export type CreateHttpClientOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* URL scheme used to reach the server.
|
|
17
|
+
* @default "http"
|
|
18
|
+
*/
|
|
19
|
+
protocol?: "http" | "https";
|
|
20
|
+
/**
|
|
21
|
+
* Time in milliseconds to wait for a response to a single request attempt.
|
|
22
|
+
* @default 2000
|
|
23
|
+
*/
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** Retry behavior for each call to `getJson`. Retries are disabled when omitted. */
|
|
26
|
+
retry?: RetryOptions;
|
|
27
|
+
};
|
|
28
|
+
/** HTTP transport bound to a single query destination. */
|
|
29
|
+
export interface HttpClient {
|
|
30
|
+
/**
|
|
31
|
+
* Requests `path` and resolves with its response body parsed as JSON.
|
|
32
|
+
* @param path Path (including leading `/`) to request, relative to the client's base URL.
|
|
33
|
+
* @returns The parsed JSON response body.
|
|
34
|
+
*/
|
|
35
|
+
getJson<T = unknown>(path: string): Promise<T>;
|
|
36
|
+
/** Destination associated with this client. */
|
|
37
|
+
readonly ctx: Readonly<CreateHttpClientParams>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates a reusable HTTP query client for one destination.
|
|
41
|
+
*
|
|
42
|
+
* Intended for query protocols that expose their state over plain JSON HTTP endpoints. Each `getJson` call is subject to the configured timeout and, when provided, retried according to `options.retry`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const createHttpClient: ({ host, port }: CreateHttpClientParams, { protocol, timeout, retry }?: CreateHttpClientOptions) => HttpClient;
|
|
45
|
+
//# sourceMappingURL=http-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../../src/net/http-client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAa,MAAM,eAAe,CAAC;AAGxD,qDAAqD;AACrD,MAAM,MAAM,sBAAsB,GAAG;IACnC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/C,+CAA+C;IAC/C,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;CAChD;AAID;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,mBACX,sBAAsB,iCACkB,uBAAuB,KAC9E,UAmFF,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { RetryOptions } from "../util/retry";
|
|
2
|
+
/** Destination addressed by a UDP query socket. */
|
|
3
|
+
export type CreateUdpSocketParams = {
|
|
4
|
+
/**
|
|
5
|
+
* Target host.
|
|
6
|
+
*/
|
|
7
|
+
host: string;
|
|
8
|
+
/**
|
|
9
|
+
* Target port.
|
|
10
|
+
*/
|
|
11
|
+
port: number;
|
|
12
|
+
};
|
|
13
|
+
/** Transport and retry settings for a UDP query socket. */
|
|
14
|
+
export type CreateUdpSocketOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* Type of socket (udp4 or udp6).
|
|
17
|
+
* @default "udp4"
|
|
18
|
+
*/
|
|
19
|
+
type?: "udp4" | "udp6";
|
|
20
|
+
/**
|
|
21
|
+
* Time in milliseconds to wait for a response to a single send attempt.
|
|
22
|
+
* @default 2000
|
|
23
|
+
*/
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** Retry behavior for each call to `send`. Retries are disabled when omitted. */
|
|
26
|
+
retry?: RetryOptions;
|
|
27
|
+
};
|
|
28
|
+
/** Payload sent by a UDP query. */
|
|
29
|
+
export type UdpSocketSendParams = {
|
|
30
|
+
/** Payload to send over the UDP socket. */
|
|
31
|
+
payload: Buffer;
|
|
32
|
+
};
|
|
33
|
+
/** Packet filtering and completion callbacks for a UDP query. */
|
|
34
|
+
export type UdpSocketSendOptions = {
|
|
35
|
+
/**
|
|
36
|
+
* Called to determine if a received packet should be accepted.
|
|
37
|
+
* @param packet The received packet.
|
|
38
|
+
* @returns `true` if the packet should be accepted, `false` otherwise.
|
|
39
|
+
*/
|
|
40
|
+
accept?: (packet: Buffer) => boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Called after each accepted packet.
|
|
43
|
+
* Return true once enough packets have arrived.
|
|
44
|
+
* @default `() => true` (always end after the first accepted packet)
|
|
45
|
+
*/
|
|
46
|
+
end?: (packets: Buffer[]) => boolean;
|
|
47
|
+
};
|
|
48
|
+
/** UDP transport bound to a single query destination. */
|
|
49
|
+
export interface UdpSocket {
|
|
50
|
+
/** Sends a payload and resolves with the accepted response packets. */
|
|
51
|
+
send(params: UdpSocketSendParams, options?: UdpSocketSendOptions): Promise<Buffer[]>;
|
|
52
|
+
/** Destination associated with this socket. */
|
|
53
|
+
readonly ctx: Readonly<CreateUdpSocketParams>;
|
|
54
|
+
/** Closes the underlying UDP socket. */
|
|
55
|
+
close(): void;
|
|
56
|
+
/** Closes the underlying UDP socket when leaving a `using` scope. */
|
|
57
|
+
[Symbol.dispose](): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a reusable UDP query socket for one destination.
|
|
61
|
+
*
|
|
62
|
+
* Each `send` call collects accepted packets until its completion callback succeeds. When retry
|
|
63
|
+
* options are configured, a timed-out or failed send is repeated on the same socket.
|
|
64
|
+
*/
|
|
65
|
+
export declare const createUdpSocket: ({ host, port }: CreateUdpSocketParams, { timeout, type, retry }?: CreateUdpSocketOptions) => UdpSocket;
|
|
66
|
+
//# sourceMappingURL=udp-socket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"udp-socket.d.ts","sourceRoot":"","sources":["../../src/net/udp-socket.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAa,MAAM,eAAe,CAAC;AAGxD,mDAAmD;AACnD,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,mCAAmC;AACnC,MAAM,MAAM,mBAAmB,GAAG;IAChC,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,iEAAiE;AACjE,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC;;;;OAIG;IACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;CACtC,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,IAAI,CAAC,MAAM,EAAE,mBAAmB,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrF,+CAA+C;IAC/C,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,qBAAqB,CAAC,CAAC;IAC9C,wCAAwC;IACxC,KAAK,IAAI,IAAI,CAAC;IACd,qEAAqE;IACrE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,mBACV,qBAAqB,6BACe,sBAAsB,KACzE,SA0GF,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Calculates the delay before retrying after a failed, one-based attempt. */
|
|
2
|
+
export type RetryStrategy = (attempt: number) => number;
|
|
3
|
+
/** Exponential retry strategy producing delays of 100 ms, 200 ms, 400 ms, and so on. */
|
|
4
|
+
export declare const backoffStrategy: RetryStrategy;
|
|
5
|
+
/** Controls how an asynchronous operation is retried after failure. */
|
|
6
|
+
export interface RetryOptions {
|
|
7
|
+
/** Total number of attempts, including the first (non-retry) call. */
|
|
8
|
+
retries: number;
|
|
9
|
+
/** ms delay before the next attempt, given the attempt number just completed (1-based). */
|
|
10
|
+
strategy?: RetryStrategy;
|
|
11
|
+
/** Return true if this error should stop retrying and be thrown immediately. */
|
|
12
|
+
fatal?: (err: unknown) => boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Default retry policy: three attempts with exponential backoff. */
|
|
15
|
+
export declare const defaultRetryOptions: Readonly<{
|
|
16
|
+
retries: 3;
|
|
17
|
+
strategy: RetryStrategy;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Calls `fn` up to `options.retries` times, retrying on rejection unless
|
|
21
|
+
* `fatal` says otherwise. Throws the last error when every attempt fails.
|
|
22
|
+
* @param fn Asynchronous operation to execute.
|
|
23
|
+
* @param options Attempt count, delay strategy, and fatal-error predicate.
|
|
24
|
+
* @returns The first successful result.
|
|
25
|
+
* @throws The fatal error or the final error after all attempts fail.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withRetry<T>(fn: () => Promise<T>, { retries, fatal, strategy }: RetryOptions): Promise<T>;
|
|
28
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/util/retry.ts"],"names":[],"mappings":"AAEA,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAExD,wFAAwF;AACxF,eAAO,MAAM,eAAe,EAAE,aAAqD,CAAC;AAEpF,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,gFAAgF;IAChF,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CACnC;AAED,qEAAqE;AACrE,eAAO,MAAM,mBAAmB;;;EAGP,CAAC;AAE1B;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,YAAY,GACzC,OAAO,CAAC,CAAC,CAAC,CAmBZ"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Resolves after the requested number of milliseconds. */
|
|
2
|
+
export declare const delay: (ms: number) => Promise<unknown>;
|
|
3
|
+
/** Default time in milliseconds to wait for a response to a single request attempt. */
|
|
4
|
+
export declare const defaultTimeout = 2000;
|
|
5
|
+
//# sourceMappingURL=timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../src/util/timing.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,eAAO,MAAM,KAAK,OAAQ,MAAM,qBAAsD,CAAC;AAEvF,uFAAuF;AACvF,eAAO,MAAM,cAAc,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@srvquery/core",
|
|
3
|
+
"version": "0.0.1-next.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@internal/typescript-config": "0.0.0",
|
|
16
|
+
"@types/node": "^26.4.1",
|
|
17
|
+
"rolldown": "^1.2.7",
|
|
18
|
+
"typescript": "^7.0.2",
|
|
19
|
+
"vitest": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "rolldown -c rolldown.config.ts && tsc -p tsconfig.json",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
25
|
+
}
|
|
26
|
+
}
|