@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 ADDED
@@ -0,0 +1,130 @@
1
+ # @srvquery/core
2
+
3
+ ![npm Version](https://shieldcn.dev/npm/@srvquery/core.svg?variant=secondary) ![npm Weekly Downloads](https://shieldcn.dev/npm/@srvquery/core/downloads.svg)
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"}
@@ -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
+ }