@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 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
- * Simple in-memory ETag cache.
268
+ * LRU ETag cache with configurable max size and TTL.
265
269
  *
266
- * Keys are normalized URLs (no trailing slashes).
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.store.get(this.normalizeKey(url))?.etag;
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.store.get(this.normalizeKey(url))?.body;
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.store.set(this.normalizeKey(url), {
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
- if (signal?.aborted) {
453
- reject(signal.reason);
454
- return;
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
- signal?.addEventListener("abort", () => {
462
- clearTimeout(timer);
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(resolve, ms);
540
- signal?.addEventListener("abort", () => {
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.reason);
543
- }, { once: true });
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 `AbortController`
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 (stubbed for Step 11)
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 controller = new AbortController();
695
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
696
- if (options?.signal) options.signal.addEventListener("abort", () => controller.abort(options.signal?.reason), { once: true });
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: body !== void 0 ? JSON.stringify(body) : void 0,
709
- signal: controller.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
- clearTimeout(timeoutId);
715
- if (error instanceof DOMException && error.name === "AbortError" && !options?.signal?.aborted) throw new HieroTimeoutError(timeoutMs);
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 timer = setTimeout(resolve, ms);
2029
- this.options.signal?.addEventListener("abort", () => {
2091
+ const onDone = () => {
2092
+ if (signal) signal.removeEventListener("abort", onAbort);
2093
+ resolve();
2094
+ };
2095
+ const onAbort = () => {
2030
2096
  clearTimeout(timer);
2031
2097
  resolve();
2032
- }, { once: true });
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
- const VERSION = "0.0.0";
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;