@satianurag/hiero-mirror-client 0.1.1 → 0.2.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/dist/index.cjs +125 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +53 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +53 -4
- package/dist/index.mjs +125 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -14
package/dist/index.cjs
CHANGED
|
@@ -229,7 +229,11 @@ function createErrorFromResponse(statusCode, body, rawBody, headers) {
|
|
|
229
229
|
const retryAfterHeader = headers?.get("retry-after");
|
|
230
230
|
if (retryAfterHeader) {
|
|
231
231
|
const parsed = Number.parseInt(retryAfterHeader, 10);
|
|
232
|
-
if (!Number.isNaN(parsed)) retryAfter = parsed;
|
|
232
|
+
if (!Number.isNaN(parsed) && String(parsed) === retryAfterHeader.trim()) retryAfter = parsed;
|
|
233
|
+
else {
|
|
234
|
+
const dateMs = Date.parse(retryAfterHeader);
|
|
235
|
+
if (!Number.isNaN(dateMs)) retryAfter = Math.max(0, Math.ceil((dateMs - Date.now()) / 1e3));
|
|
236
|
+
}
|
|
233
237
|
}
|
|
234
238
|
return new HieroRateLimitError(message, {
|
|
235
239
|
retryAfter,
|
|
@@ -261,35 +265,49 @@ function createParseError(rawBody, statusCode, cause) {
|
|
|
261
265
|
//#endregion
|
|
262
266
|
//#region src/http/etag-cache.ts
|
|
263
267
|
/**
|
|
264
|
-
*
|
|
268
|
+
* LRU ETag cache with configurable max size and TTL.
|
|
265
269
|
*
|
|
266
|
-
*
|
|
270
|
+
* Uses `Map` insertion order for LRU tracking:
|
|
271
|
+
* accessing an entry deletes and re-inserts it so it moves to the end.
|
|
272
|
+
* Eviction removes the oldest (first) entry.
|
|
267
273
|
*/
|
|
268
274
|
var ETagCache = class {
|
|
269
275
|
store = /* @__PURE__ */ new Map();
|
|
276
|
+
maxSize;
|
|
277
|
+
ttlMs;
|
|
278
|
+
constructor(options) {
|
|
279
|
+
this.maxSize = options?.maxSize ?? 500;
|
|
280
|
+
this.ttlMs = options?.ttlMs ?? 3e5;
|
|
281
|
+
}
|
|
270
282
|
/**
|
|
271
283
|
* Look up a cached ETag for the given URL.
|
|
272
|
-
*
|
|
273
|
-
* @returns The cached ETag string, or `undefined` if not cached.
|
|
284
|
+
* Returns `undefined` if not cached or expired.
|
|
274
285
|
*/
|
|
275
286
|
getETag(url) {
|
|
276
|
-
return this.
|
|
287
|
+
return this.getEntry(url)?.etag;
|
|
277
288
|
}
|
|
278
289
|
/**
|
|
279
290
|
* Look up the cached response body for the given URL.
|
|
280
|
-
*
|
|
281
|
-
* @returns The cached body, or `undefined` if not cached.
|
|
291
|
+
* Returns `undefined` if not cached or expired.
|
|
282
292
|
*/
|
|
283
293
|
getCachedBody(url) {
|
|
284
|
-
return this.
|
|
294
|
+
return this.getEntry(url)?.body;
|
|
285
295
|
}
|
|
286
296
|
/**
|
|
287
297
|
* Store or update a cache entry.
|
|
298
|
+
* If the cache is full, evicts the least-recently-used entry.
|
|
288
299
|
*/
|
|
289
300
|
set(url, etag, body) {
|
|
290
|
-
this.
|
|
301
|
+
const key = this.normalizeKey(url);
|
|
302
|
+
this.store.delete(key);
|
|
303
|
+
while (this.store.size >= this.maxSize) {
|
|
304
|
+
const oldest = this.store.keys().next().value;
|
|
305
|
+
if (oldest !== void 0) this.store.delete(oldest);
|
|
306
|
+
}
|
|
307
|
+
this.store.set(key, {
|
|
291
308
|
etag,
|
|
292
|
-
body
|
|
309
|
+
body,
|
|
310
|
+
createdAt: Date.now()
|
|
293
311
|
});
|
|
294
312
|
}
|
|
295
313
|
/**
|
|
@@ -311,6 +329,21 @@ var ETagCache = class {
|
|
|
311
329
|
return this.store.size;
|
|
312
330
|
}
|
|
313
331
|
/**
|
|
332
|
+
* Internal: get entry if not expired, refreshing LRU position.
|
|
333
|
+
*/
|
|
334
|
+
getEntry(url) {
|
|
335
|
+
const key = this.normalizeKey(url);
|
|
336
|
+
const entry = this.store.get(key);
|
|
337
|
+
if (!entry) return void 0;
|
|
338
|
+
if (Date.now() - entry.createdAt > this.ttlMs) {
|
|
339
|
+
this.store.delete(key);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.store.delete(key);
|
|
343
|
+
this.store.set(key, entry);
|
|
344
|
+
return entry;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
314
347
|
* Normalize a URL for use as a cache key.
|
|
315
348
|
*
|
|
316
349
|
* EC43: Removes trailing slashes.
|
|
@@ -448,20 +481,20 @@ var RateLimiter = class {
|
|
|
448
481
|
}
|
|
449
482
|
const deficit = 1 - this.tokens;
|
|
450
483
|
const waitMs = Math.ceil(deficit / this.refillRate);
|
|
484
|
+
if (signal?.aborted) throw signal.reason;
|
|
451
485
|
await new Promise((resolve, reject) => {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
486
|
+
const onAbort = () => {
|
|
487
|
+
clearTimeout(timer);
|
|
488
|
+
reject(signal?.reason);
|
|
489
|
+
};
|
|
456
490
|
const timer = setTimeout(() => {
|
|
491
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
457
492
|
this.refill();
|
|
458
493
|
this.tokens -= 1;
|
|
459
494
|
resolve();
|
|
460
495
|
}, waitMs);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
reject(signal.reason);
|
|
464
|
-
}, { once: true });
|
|
496
|
+
if (!signal) return;
|
|
497
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
465
498
|
});
|
|
466
499
|
}
|
|
467
500
|
/**
|
|
@@ -502,7 +535,7 @@ function isRetryableStatus(statusCode) {
|
|
|
502
535
|
*/
|
|
503
536
|
function isRetryableError(error) {
|
|
504
537
|
if (error instanceof TypeError) return true;
|
|
505
|
-
if (error instanceof DOMException && error.name === "AbortError") return false;
|
|
538
|
+
if (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) return false;
|
|
506
539
|
return false;
|
|
507
540
|
}
|
|
508
541
|
/**
|
|
@@ -536,11 +569,15 @@ function sleep(ms, signal) {
|
|
|
536
569
|
reject(signal.reason);
|
|
537
570
|
return;
|
|
538
571
|
}
|
|
539
|
-
const timer = setTimeout(
|
|
540
|
-
|
|
572
|
+
const timer = setTimeout(() => {
|
|
573
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
574
|
+
resolve();
|
|
575
|
+
}, ms);
|
|
576
|
+
const onAbort = () => {
|
|
541
577
|
clearTimeout(timer);
|
|
542
|
-
reject(signal
|
|
543
|
-
}
|
|
578
|
+
reject(signal?.reason);
|
|
579
|
+
};
|
|
580
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
544
581
|
});
|
|
545
582
|
}
|
|
546
583
|
//#endregion
|
|
@@ -610,13 +647,14 @@ function buildUrl(baseUrl, path, params) {
|
|
|
610
647
|
* Core HTTP client for the Hiero Mirror Node SDK.
|
|
611
648
|
*
|
|
612
649
|
* Wraps `fetch` with:
|
|
613
|
-
* - Timeout via `
|
|
650
|
+
* - Timeout via `AbortSignal.timeout()` + `AbortSignal.any()`
|
|
614
651
|
* - Safe JSON parsing (int64 precision)
|
|
615
652
|
* - Error factory integration
|
|
616
653
|
* - Rate limiting
|
|
617
654
|
* - Request deduplication
|
|
618
655
|
* - Retry with exponential backoff
|
|
619
|
-
* - ETag/conditional request support
|
|
656
|
+
* - ETag/conditional request support with LRU eviction
|
|
657
|
+
* - Request interceptors (beforeRequest / afterResponse)
|
|
620
658
|
*
|
|
621
659
|
* @internal
|
|
622
660
|
*/
|
|
@@ -629,6 +667,8 @@ var HttpClient = class {
|
|
|
629
667
|
etagCache;
|
|
630
668
|
logger;
|
|
631
669
|
fetchFn;
|
|
670
|
+
beforeRequestHooks;
|
|
671
|
+
afterResponseHooks;
|
|
632
672
|
constructor(options) {
|
|
633
673
|
this.baseUrl = options.baseUrl;
|
|
634
674
|
this.timeout = options.timeout ?? 3e4;
|
|
@@ -640,6 +680,16 @@ var HttpClient = class {
|
|
|
640
680
|
this.etagCache = new ETagCache();
|
|
641
681
|
this.logger = options.logger ?? {};
|
|
642
682
|
this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
683
|
+
this.beforeRequestHooks = options.beforeRequest ?? [];
|
|
684
|
+
this.afterResponseHooks = options.afterResponse ?? [];
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Clears internal caches and resets state.
|
|
688
|
+
* Call this when you are done with the client to free resources.
|
|
689
|
+
*/
|
|
690
|
+
destroy() {
|
|
691
|
+
this.etagCache.clear();
|
|
692
|
+
this.inflight.clear();
|
|
643
693
|
}
|
|
644
694
|
/**
|
|
645
695
|
* Performs a GET request with in-flight deduplication.
|
|
@@ -691,28 +741,40 @@ var HttpClient = class {
|
|
|
691
741
|
for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) try {
|
|
692
742
|
await this.rateLimiter.acquire(options?.signal);
|
|
693
743
|
const timeoutMs = options?.timeout ?? this.timeout;
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
744
|
+
const signals = [AbortSignal.timeout(timeoutMs)];
|
|
745
|
+
if (options?.signal) signals.push(options.signal);
|
|
746
|
+
const composedSignal = AbortSignal.any(signals);
|
|
697
747
|
try {
|
|
698
748
|
const headers = {
|
|
699
749
|
Accept: "application/json",
|
|
700
750
|
"Accept-Encoding": "gzip",
|
|
751
|
+
"User-Agent": `hiero-mirror-client/${VERSION}`,
|
|
701
752
|
...options?.headers
|
|
702
753
|
};
|
|
703
|
-
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
754
|
+
if (body !== void 0 && !(body instanceof Uint8Array)) headers["Content-Type"] = "application/json";
|
|
755
|
+
for (const hook of this.beforeRequestHooks) await hook({
|
|
756
|
+
method,
|
|
757
|
+
url,
|
|
758
|
+
headers
|
|
759
|
+
});
|
|
704
760
|
this.logger.debug?.(`[HTTP] ${method} ${url}`, { attempt });
|
|
761
|
+
const serializedBody = body instanceof Uint8Array ? body : body !== void 0 ? JSON.stringify(body) : void 0;
|
|
705
762
|
const response = await this.fetchFn(url, {
|
|
706
763
|
method,
|
|
707
764
|
headers,
|
|
708
|
-
body:
|
|
709
|
-
signal:
|
|
765
|
+
body: serializedBody,
|
|
766
|
+
signal: composedSignal
|
|
767
|
+
});
|
|
768
|
+
for (const hook of this.afterResponseHooks) await hook({
|
|
769
|
+
method,
|
|
770
|
+
url,
|
|
771
|
+
status: response.status,
|
|
772
|
+
headers: response.headers
|
|
710
773
|
});
|
|
711
|
-
clearTimeout(timeoutId);
|
|
712
774
|
return await this.handleResponse(response, url, attempt);
|
|
713
775
|
} catch (error) {
|
|
714
|
-
|
|
715
|
-
if (error instanceof DOMException && error.name === "AbortError"
|
|
776
|
+
if (error instanceof DOMException && error.name === "TimeoutError") throw new HieroTimeoutError(timeoutMs);
|
|
777
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
716
778
|
throw error;
|
|
717
779
|
}
|
|
718
780
|
} catch (error) {
|
|
@@ -1721,8 +1783,8 @@ var NetworkResource = class {
|
|
|
1721
1783
|
constructor(client) {
|
|
1722
1784
|
this.client = client;
|
|
1723
1785
|
}
|
|
1724
|
-
async getExchangeRate() {
|
|
1725
|
-
return mapExchangeRateSet((await this.client.get("/api/v1/network/exchangerate")).data);
|
|
1786
|
+
async getExchangeRate(params) {
|
|
1787
|
+
return mapExchangeRateSet((await this.client.get("/api/v1/network/exchangerate", params)).data);
|
|
1726
1788
|
}
|
|
1727
1789
|
async getFees(params) {
|
|
1728
1790
|
return mapFeeSchedule((await this.client.get("/api/v1/network/fees", params)).data);
|
|
@@ -2024,12 +2086,18 @@ var TopicStream = class TopicStream {
|
|
|
2024
2086
|
}
|
|
2025
2087
|
}
|
|
2026
2088
|
sleep(ms) {
|
|
2089
|
+
const signal = this.options.signal;
|
|
2027
2090
|
return new Promise((resolve) => {
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2091
|
+
const onDone = () => {
|
|
2092
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
2093
|
+
resolve();
|
|
2094
|
+
};
|
|
2095
|
+
const onAbort = () => {
|
|
2030
2096
|
clearTimeout(timer);
|
|
2031
2097
|
resolve();
|
|
2032
|
-
}
|
|
2098
|
+
};
|
|
2099
|
+
const timer = setTimeout(onDone, ms);
|
|
2100
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2033
2101
|
});
|
|
2034
2102
|
}
|
|
2035
2103
|
};
|
|
@@ -2161,7 +2229,9 @@ var MirrorNodeClient = class {
|
|
|
2161
2229
|
retry: { maxRetries: options.maxRetries ?? 2 },
|
|
2162
2230
|
rateLimitRps: options.rateLimitRps ?? 50,
|
|
2163
2231
|
logger: options.logger,
|
|
2164
|
-
fetch: options.fetch
|
|
2232
|
+
fetch: options.fetch,
|
|
2233
|
+
beforeRequest: options.beforeRequest,
|
|
2234
|
+
afterResponse: options.afterResponse
|
|
2165
2235
|
});
|
|
2166
2236
|
this.accounts = new AccountsResource(this.httpClient);
|
|
2167
2237
|
this.balances = new BalancesResource(this.httpClient);
|
|
@@ -2173,6 +2243,15 @@ var MirrorNodeClient = class {
|
|
|
2173
2243
|
this.topics = new TopicsResource(this.httpClient);
|
|
2174
2244
|
this.transactions = new TransactionsResource(this.httpClient);
|
|
2175
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Releases all internal resources (caches, in-flight maps).
|
|
2248
|
+
*
|
|
2249
|
+
* Call this when the client is no longer needed to prevent memory leaks
|
|
2250
|
+
* in long-running applications (servers, workers).
|
|
2251
|
+
*/
|
|
2252
|
+
destroy() {
|
|
2253
|
+
this.httpClient.destroy();
|
|
2254
|
+
}
|
|
2176
2255
|
};
|
|
2177
2256
|
//#endregion
|
|
2178
2257
|
//#region src/errors/HieroCapabilityError.ts
|
|
@@ -2212,7 +2291,11 @@ var HieroCapabilityError = class extends HieroError {
|
|
|
2212
2291
|
*
|
|
2213
2292
|
* @packageDocumentation
|
|
2214
2293
|
*/
|
|
2215
|
-
|
|
2294
|
+
/**
|
|
2295
|
+
* SDK version, injected at build time from package.json.
|
|
2296
|
+
* Falls back to 'development' for unbundled/test usage.
|
|
2297
|
+
*/
|
|
2298
|
+
const VERSION = "0.2.0";
|
|
2216
2299
|
//#endregion
|
|
2217
2300
|
exports.HieroCapabilityError = HieroCapabilityError;
|
|
2218
2301
|
exports.HieroError = HieroError;
|