@imbingox/acex 0.4.0-beta.1 → 0.4.0-beta.10

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.
@@ -1,9 +1,18 @@
1
1
  import { createHmac } from "node:crypto";
2
2
  import BigNumber from "bignumber.js";
3
+ import {
4
+ type HttpClientMessages,
5
+ type HttpRetryPolicy,
6
+ httpRequest,
7
+ isTransportError,
8
+ } from "../../internal/http-client.ts";
3
9
  import { createManagedWebSocket } from "../../internal/managed-websocket.ts";
4
10
  import type {
5
11
  AccountCredentials,
6
12
  PositionSide,
13
+ RateLimiter,
14
+ RateLimitScope,
15
+ TimeProvider,
7
16
  VenueAccountCapabilities,
8
17
  VenueOrderCapabilities,
9
18
  } from "../../types/index.ts";
@@ -11,20 +20,24 @@ import type {
11
20
  CancelAllOrdersRequest,
12
21
  CancelOrderRequest,
13
22
  CreateOrderRequest,
23
+ FetchOrderRequest,
14
24
  PrivateStreamCallbacks,
15
25
  PrivateStreamOptions,
16
26
  PrivateUserDataAdapter,
17
27
  RawAccountBootstrap,
18
28
  RawAccountUpdate,
19
29
  RawBalanceUpdate,
30
+ RawOpenOrdersSnapshot,
20
31
  RawOrderUpdate,
21
32
  RawPositionUpdate,
22
33
  RawRiskUpdate,
23
34
  StreamHandle,
24
35
  } from "../types.ts";
36
+ import { parseBinanceRateLimitUsage } from "./rate-limit.ts";
25
37
 
26
38
  type TimerHandle = ReturnType<typeof setInterval>;
27
39
  type SignedRequestMethod = "GET" | "POST" | "DELETE";
40
+ type FetchLike = typeof fetch;
28
41
 
29
42
  interface BinancePapiBalance {
30
43
  asset?: string;
@@ -144,7 +157,32 @@ type BinancePrivateMessage =
144
157
  const BINANCE_PAPI_REST_BASE_URL = "https://papi.binance.com";
145
158
  const BINANCE_PAPI_WS_BASE_URL = "wss://fstream.binance.com/pm/ws";
146
159
  const DEFAULT_RECV_WINDOW = 5_000;
160
+ const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
147
161
  const USDM_QUOTE_ASSETS = ["FDUSD", "USDC", "BUSD", "USDT"];
162
+ const SAFE_READ_RETRY_POLICY: HttpRetryPolicy = {
163
+ idempotent: true,
164
+ maxAttempts: 3,
165
+ };
166
+ const NO_RETRY_POLICY: HttpRetryPolicy = {
167
+ idempotent: false,
168
+ maxAttempts: 1,
169
+ };
170
+ const LISTEN_KEY_KEEPALIVE_RETRY_POLICY: HttpRetryPolicy = {
171
+ idempotent: true,
172
+ maxAttempts: 3,
173
+ };
174
+ const BINANCE_ORDER_NOT_FOUND_CODES = new Set(["-2011", "-2013"]);
175
+ function getBinancePapiHttpMessages(timeoutMs: number): HttpClientMessages {
176
+ return {
177
+ http: ({ status, statusText, url, rawBody }) =>
178
+ `Binance PAPI request failed: ${status} ${statusText ?? ""} ${url}${
179
+ rawBody ? ` ${rawBody}` : ""
180
+ }`,
181
+ timeout: () => `Binance PAPI fetch timeout after ${timeoutMs}ms`,
182
+ aborted: () => "Binance PAPI fetch aborted",
183
+ parse: ({ url }) => `Binance PAPI response parse failed: ${url}`,
184
+ };
185
+ }
148
186
 
149
187
  function requirePrivateCredentials(credentials: AccountCredentials): {
150
188
  apiKey: string;
@@ -174,6 +212,14 @@ function getNumberOption(
174
212
  : undefined;
175
213
  }
176
214
 
215
+ function getStringOption(
216
+ options: Record<string, unknown> | undefined,
217
+ key: string,
218
+ ): string | undefined {
219
+ const value = options?.[key];
220
+ return typeof value === "string" && value.length > 0 ? value : undefined;
221
+ }
222
+
177
223
  function signQuery(query: string, secret: string): string {
178
224
  return createHmac("sha256", secret).update(query).digest("hex");
179
225
  }
@@ -393,6 +439,27 @@ function mapAccountRefresh(
393
439
  };
394
440
  }
395
441
 
442
+ function mapAccountBootstrap(
443
+ balances: BinancePapiBalance[],
444
+ account: BinancePapiAccount,
445
+ positions: BinancePapiUmPosition[],
446
+ receivedAt: number,
447
+ ): RawAccountBootstrap {
448
+ return {
449
+ balances: balances.flatMap((balance) => {
450
+ const mapped = mapBalance(balance, receivedAt);
451
+ return mapped ? [mapped] : [];
452
+ }),
453
+ positions: positions.flatMap((position) => {
454
+ const mapped = mapUmPosition(position, receivedAt);
455
+ return mapped ? [mapped] : [];
456
+ }),
457
+ risk: mapAccountRisk(account, receivedAt, positions),
458
+ exchangeTs: account.updateTime,
459
+ receivedAt,
460
+ };
461
+ }
462
+
396
463
  function mapOpenOrder(
397
464
  input: BinancePapiOpenOrder,
398
465
  receivedAt: number,
@@ -521,19 +588,26 @@ function mapOrderUpdate(
521
588
  };
522
589
  }
523
590
 
524
- async function readJson<T>(response: Response, url: string): Promise<T> {
525
- const text = await response.text();
526
- if (!response.ok) {
527
- throw new Error(
528
- `Binance PAPI request failed: ${response.status} ${response.statusText} ${url} ${text}`,
529
- );
591
+ function isBinanceOrderNotFound(error: unknown): boolean {
592
+ if (!isTransportError(error) || error.kind !== "http") {
593
+ return false;
530
594
  }
531
595
 
532
- if (!text) {
533
- return {} as T;
596
+ if (error.status !== 400 && error.status !== 404) {
597
+ return false;
534
598
  }
535
599
 
536
- return JSON.parse(text) as T;
600
+ const rawBody = error.rawBody;
601
+ if (!rawBody) {
602
+ return false;
603
+ }
604
+
605
+ try {
606
+ const parsed = JSON.parse(rawBody) as { code?: unknown };
607
+ return BINANCE_ORDER_NOT_FOUND_CODES.has(`${parsed.code}`);
608
+ } catch {
609
+ return false;
610
+ }
537
611
  }
538
612
 
539
613
  export class BinancePrivateAdapter implements PrivateUserDataAdapter {
@@ -569,6 +643,15 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
569
643
  clientOrderId: true,
570
644
  };
571
645
 
646
+ constructor(
647
+ private readonly options: {
648
+ readonly fetchFn?: FetchLike;
649
+ readonly httpTimeoutMs?: number;
650
+ readonly signingClock?: TimeProvider;
651
+ readonly rateLimiter?: RateLimiter;
652
+ } = {},
653
+ ) {}
654
+
572
655
  async bootstrapAccount(
573
656
  credentials: AccountCredentials,
574
657
  accountOptions?: Record<string, unknown>,
@@ -580,34 +663,35 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
580
663
  "/papi/v1/balance",
581
664
  credentials,
582
665
  accountOptions,
666
+ undefined,
667
+ SAFE_READ_RETRY_POLICY,
583
668
  ),
584
669
  this.signedRequest<BinancePapiAccount>(
585
670
  "GET",
586
671
  "/papi/v1/account",
587
672
  credentials,
588
673
  accountOptions,
674
+ undefined,
675
+ SAFE_READ_RETRY_POLICY,
589
676
  ),
590
677
  this.signedRequest<BinancePapiUmPosition[]>(
591
678
  "GET",
592
679
  "/papi/v1/um/positionRisk",
593
680
  credentials,
594
681
  accountOptions,
682
+ undefined,
683
+ SAFE_READ_RETRY_POLICY,
595
684
  ),
596
685
  ]);
597
686
 
598
- return {
599
- balances: balances.flatMap((balance) => {
600
- const mapped = mapBalance(balance, receivedAt);
601
- return mapped ? [mapped] : [];
602
- }),
603
- positions: positions.flatMap((position) => {
604
- const mapped = mapUmPosition(position, receivedAt);
605
- return mapped ? [mapped] : [];
606
- }),
607
- risk: mapAccountRisk(account, receivedAt, positions),
608
- exchangeTs: account.updateTime,
609
- receivedAt,
610
- };
687
+ return mapAccountBootstrap(balances, account, positions, receivedAt);
688
+ }
689
+
690
+ async reconcileAccount(
691
+ credentials: AccountCredentials,
692
+ accountOptions?: Record<string, unknown>,
693
+ ): Promise<RawAccountBootstrap> {
694
+ return this.bootstrapAccount(credentials, accountOptions);
611
695
  }
612
696
 
613
697
  async refreshAccount(
@@ -621,12 +705,16 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
621
705
  "/papi/v1/account",
622
706
  credentials,
623
707
  accountOptions,
708
+ undefined,
709
+ SAFE_READ_RETRY_POLICY,
624
710
  ),
625
711
  this.signedRequest<BinancePapiUmPosition[]>(
626
712
  "GET",
627
713
  "/papi/v1/um/positionRisk",
628
714
  credentials,
629
715
  accountOptions,
716
+ undefined,
717
+ SAFE_READ_RETRY_POLICY,
630
718
  ),
631
719
  ]);
632
720
 
@@ -637,18 +725,61 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
637
725
  credentials: AccountCredentials,
638
726
  accountOptions?: Record<string, unknown>,
639
727
  ): Promise<RawOrderUpdate[]> {
728
+ const snapshot = await this.fetchOpenOrders(credentials, accountOptions);
729
+ return snapshot.orders;
730
+ }
731
+
732
+ async fetchOpenOrders(
733
+ credentials: AccountCredentials,
734
+ accountOptions?: Record<string, unknown>,
735
+ ): Promise<RawOpenOrdersSnapshot> {
640
736
  const receivedAt = Date.now();
641
737
  const orders = await this.signedRequest<BinancePapiOpenOrder[]>(
642
738
  "GET",
643
739
  "/papi/v1/um/openOrders",
644
740
  credentials,
645
741
  accountOptions,
742
+ undefined,
743
+ SAFE_READ_RETRY_POLICY,
646
744
  );
647
745
 
648
- return orders.flatMap((order) => {
649
- const mapped = mapOpenOrder(order, receivedAt);
650
- return mapped ? [mapped] : [];
651
- });
746
+ return {
747
+ orders: orders.flatMap((order) => {
748
+ const mapped = mapOpenOrder(order, receivedAt);
749
+ return mapped ? [mapped] : [];
750
+ }),
751
+ snapshotReceivedAt: receivedAt,
752
+ };
753
+ }
754
+
755
+ async fetchOrder(
756
+ credentials: AccountCredentials,
757
+ request: FetchOrderRequest,
758
+ accountOptions?: Record<string, unknown>,
759
+ ): Promise<RawOrderUpdate | undefined> {
760
+ const receivedAt = Date.now();
761
+ try {
762
+ const response = await this.signedRequest<BinancePapiOpenOrder>(
763
+ "GET",
764
+ "/papi/v1/um/order",
765
+ credentials,
766
+ accountOptions,
767
+ {
768
+ symbol: encodeUmSymbol(request.symbol),
769
+ orderId: request.orderId,
770
+ origClientOrderId: request.clientOrderId,
771
+ },
772
+ SAFE_READ_RETRY_POLICY,
773
+ );
774
+
775
+ return mapOpenOrder(response, receivedAt);
776
+ } catch (error) {
777
+ if (isBinanceOrderNotFound(error)) {
778
+ return undefined;
779
+ }
780
+
781
+ throw error;
782
+ }
652
783
  }
653
784
 
654
785
  async createOrder(
@@ -681,6 +812,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
681
812
  : `${request.reduceOnly}`,
682
813
  positionSide: encodePositionSide(request.positionSide),
683
814
  },
815
+ NO_RETRY_POLICY,
684
816
  );
685
817
 
686
818
  const mapped = mapOpenOrder(response, receivedAt);
@@ -709,6 +841,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
709
841
  orderId: request.orderId,
710
842
  origClientOrderId: request.clientOrderId,
711
843
  },
844
+ NO_RETRY_POLICY,
712
845
  );
713
846
 
714
847
  const mapped = mapOpenOrder(response, receivedAt);
@@ -735,6 +868,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
735
868
  {
736
869
  symbol: encodeUmSymbol(request.symbol),
737
870
  },
871
+ NO_RETRY_POLICY,
738
872
  );
739
873
 
740
874
  return responses.flatMap((response) => {
@@ -747,7 +881,7 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
747
881
  credentials: AccountCredentials,
748
882
  callbacks: PrivateStreamCallbacks,
749
883
  options: PrivateStreamOptions,
750
- _accountOptions?: Record<string, unknown>,
884
+ accountOptions?: Record<string, unknown>,
751
885
  ): StreamHandle {
752
886
  let closed = false;
753
887
  let listenKey: string | undefined;
@@ -769,17 +903,19 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
769
903
 
770
904
  const key = listenKey;
771
905
  listenKey = undefined;
772
- void this.closeUserDataStream(credentials, key).catch((error) => {
773
- callbacks.onError(
774
- error instanceof Error
775
- ? error
776
- : new Error("Failed to close Binance PAPI listenKey"),
777
- );
778
- });
906
+ void this.closeUserDataStream(credentials, key, accountOptions).catch(
907
+ (error) => {
908
+ callbacks.onError(
909
+ error instanceof Error
910
+ ? error
911
+ : new Error("Failed to close Binance PAPI listenKey"),
912
+ );
913
+ },
914
+ );
779
915
  };
780
916
 
781
917
  const ready = (async () => {
782
- listenKey = await this.startUserDataStream(credentials);
918
+ listenKey = await this.startUserDataStream(credentials, accountOptions);
783
919
  if (closed) {
784
920
  closeListenKey();
785
921
  return;
@@ -790,15 +926,17 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
790
926
  return;
791
927
  }
792
928
 
793
- void this.keepAliveUserDataStream(credentials, listenKey).catch(
794
- (error) => {
795
- callbacks.onError(
796
- error instanceof Error
797
- ? error
798
- : new Error("Failed to keep Binance PAPI listenKey alive"),
799
- );
800
- },
801
- );
929
+ void this.keepAliveUserDataStream(
930
+ credentials,
931
+ listenKey,
932
+ accountOptions,
933
+ ).catch((error) => {
934
+ callbacks.onError(
935
+ error instanceof Error
936
+ ? error
937
+ : new Error("Failed to keep Binance PAPI listenKey alive"),
938
+ );
939
+ });
802
940
  }, options.listenKeyKeepAliveMs);
803
941
 
804
942
  websocket = createManagedWebSocket<BinancePrivateMessage>({
@@ -863,8 +1001,12 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
863
1001
  credentials: AccountCredentials,
864
1002
  accountOptions?: Record<string, unknown>,
865
1003
  queryParams?: Record<string, string | undefined>,
1004
+ retryPolicy?: HttpRetryPolicy,
866
1005
  ): Promise<T> {
867
1006
  const { apiKey, secret } = requirePrivateCredentials(credentials);
1007
+ const scope = this.rateLimitScope(method, path, accountOptions);
1008
+ await this.options.rateLimiter?.beforeRequest({ scope });
1009
+
868
1010
  const params = new URLSearchParams();
869
1011
  for (const [key, value] of Object.entries(queryParams ?? {})) {
870
1012
  if (value !== undefined) {
@@ -873,7 +1015,11 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
873
1015
  }
874
1016
  params.set(
875
1017
  "timestamp",
876
- `${getNumberOption(accountOptions, "timestamp") ?? Date.now()}`,
1018
+ `${
1019
+ getNumberOption(accountOptions, "timestamp") ??
1020
+ this.options.signingClock?.now() ??
1021
+ Date.now()
1022
+ }`,
877
1023
  );
878
1024
  params.set(
879
1025
  "recvWindow",
@@ -882,22 +1028,59 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
882
1028
  params.set("signature", signQuery(params.toString(), secret));
883
1029
 
884
1030
  const url = `${BINANCE_PAPI_REST_BASE_URL}${path}?${params.toString()}`;
885
- const response = await fetch(url, {
886
- method,
887
- headers: {
888
- "X-MBX-APIKEY": apiKey,
889
- },
890
- });
1031
+ const timeoutMs = this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
1032
+ try {
1033
+ const response = await httpRequest<T>({
1034
+ fetchFn: this.options.fetchFn,
1035
+ url,
1036
+ method,
1037
+ headers: {
1038
+ "X-MBX-APIKEY": apiKey,
1039
+ },
1040
+ timeoutMs,
1041
+ parseAs: "json",
1042
+ emptyBody: "empty_object",
1043
+ retryPolicy: retryPolicy ?? NO_RETRY_POLICY,
1044
+ messages: getBinancePapiHttpMessages(timeoutMs),
1045
+ });
891
1046
 
892
- return readJson<T>(response, url);
1047
+ await this.options.rateLimiter?.afterResponse(
1048
+ { scope },
1049
+ {
1050
+ status: response.status,
1051
+ headers: response.headers,
1052
+ usage: parseBinanceRateLimitUsage(response.headers),
1053
+ },
1054
+ );
1055
+
1056
+ return response.body;
1057
+ } catch (error) {
1058
+ if (isTransportError(error)) {
1059
+ await this.options.rateLimiter?.onTransportError(
1060
+ { scope },
1061
+ {
1062
+ status: error.status,
1063
+ headers: error.headers,
1064
+ retryAfterMs: error.retryAfterMs,
1065
+ usage: parseBinanceRateLimitUsage(error.headers),
1066
+ },
1067
+ );
1068
+ }
1069
+
1070
+ throw error;
1071
+ }
893
1072
  }
894
1073
 
895
1074
  private async startUserDataStream(
896
1075
  credentials: AccountCredentials,
1076
+ accountOptions?: Record<string, unknown>,
897
1077
  ): Promise<string> {
898
1078
  const response = await this.userStreamRequest<BinanceListenKeyResponse>(
899
1079
  "POST",
900
1080
  credentials,
1081
+ undefined,
1082
+ NO_RETRY_POLICY,
1083
+ accountOptions,
901
1084
  );
902
1085
  if (!response.listenKey) {
903
1086
  throw new Error("Binance PAPI did not return a listenKey");
@@ -909,22 +1092,28 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
909
1092
  private async keepAliveUserDataStream(
910
1093
  credentials: AccountCredentials,
911
1094
  listenKey: string,
1095
+ accountOptions?: Record<string, unknown>,
912
1096
  ): Promise<void> {
913
1097
  await this.userStreamRequest<Record<string, never>>(
914
1098
  "PUT",
915
1099
  credentials,
916
1100
  listenKey,
1101
+ LISTEN_KEY_KEEPALIVE_RETRY_POLICY,
1102
+ accountOptions,
917
1103
  );
918
1104
  }
919
1105
 
920
1106
  private async closeUserDataStream(
921
1107
  credentials: AccountCredentials,
922
1108
  listenKey: string,
1109
+ accountOptions?: Record<string, unknown>,
923
1110
  ): Promise<void> {
924
1111
  await this.userStreamRequest<Record<string, never>>(
925
1112
  "DELETE",
926
1113
  credentials,
927
1114
  listenKey,
1115
+ NO_RETRY_POLICY,
1116
+ accountOptions,
928
1117
  );
929
1118
  }
930
1119
 
@@ -932,8 +1121,17 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
932
1121
  method: "POST" | "PUT" | "DELETE",
933
1122
  credentials: AccountCredentials,
934
1123
  listenKey?: string,
1124
+ retryPolicy: HttpRetryPolicy = NO_RETRY_POLICY,
1125
+ accountOptions?: Record<string, unknown>,
935
1126
  ): Promise<T> {
936
1127
  const { apiKey } = requirePrivateCredentials(credentials);
1128
+ const scope = this.rateLimitScope(
1129
+ method,
1130
+ "/papi/v1/listenKey",
1131
+ accountOptions,
1132
+ );
1133
+ await this.options.rateLimiter?.beforeRequest({ scope });
1134
+
937
1135
  const params = new URLSearchParams();
938
1136
  if (listenKey) {
939
1137
  params.set("listenKey", listenKey);
@@ -943,13 +1141,58 @@ export class BinancePrivateAdapter implements PrivateUserDataAdapter {
943
1141
  const url = `${BINANCE_PAPI_REST_BASE_URL}/papi/v1/listenKey${
944
1142
  query ? `?${query}` : ""
945
1143
  }`;
946
- const response = await fetch(url, {
947
- method,
948
- headers: {
949
- "X-MBX-APIKEY": apiKey,
950
- },
951
- });
1144
+ const timeoutMs = this.options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
1145
+ try {
1146
+ const response = await httpRequest<T>({
1147
+ fetchFn: this.options.fetchFn,
1148
+ url,
1149
+ method,
1150
+ headers: {
1151
+ "X-MBX-APIKEY": apiKey,
1152
+ },
1153
+ timeoutMs,
1154
+ parseAs: "json",
1155
+ emptyBody: "empty_object",
1156
+ retryPolicy,
1157
+ messages: getBinancePapiHttpMessages(timeoutMs),
1158
+ });
1159
+
1160
+ await this.options.rateLimiter?.afterResponse(
1161
+ { scope },
1162
+ {
1163
+ status: response.status,
1164
+ headers: response.headers,
1165
+ usage: parseBinanceRateLimitUsage(response.headers),
1166
+ },
1167
+ );
1168
+
1169
+ return response.body;
1170
+ } catch (error) {
1171
+ if (isTransportError(error)) {
1172
+ await this.options.rateLimiter?.onTransportError(
1173
+ { scope },
1174
+ {
1175
+ status: error.status,
1176
+ headers: error.headers,
1177
+ retryAfterMs: error.retryAfterMs,
1178
+ usage: parseBinanceRateLimitUsage(error.headers),
1179
+ },
1180
+ );
1181
+ }
952
1182
 
953
- return readJson<T>(response, url);
1183
+ throw error;
1184
+ }
1185
+ }
1186
+
1187
+ private rateLimitScope(
1188
+ method: string,
1189
+ path: string,
1190
+ accountOptions?: Record<string, unknown>,
1191
+ ): RateLimitScope {
1192
+ return {
1193
+ venue: "binance",
1194
+ accountId: getStringOption(accountOptions, "accountId"),
1195
+ endpointKey: `${method} ${path}`,
1196
+ };
954
1197
  }
955
1198
  }
@@ -0,0 +1,47 @@
1
+ import type { RateLimitUsage } from "../../types/index.ts";
2
+
3
+ const USED_WEIGHT_PREFIX = "x-mbx-used-weight-";
4
+ const ORDER_COUNT_PREFIX = "x-mbx-order-count-";
5
+
6
+ export function parseBinanceRateLimitUsage(
7
+ headers: Headers,
8
+ ): RateLimitUsage | undefined {
9
+ const weight: Record<string, number> = {};
10
+ const orderCount: Record<string, number> = {};
11
+
12
+ for (const name of headers.keys()) {
13
+ const normalizedName = name.toLowerCase();
14
+ if (normalizedName.startsWith(USED_WEIGHT_PREFIX)) {
15
+ const interval = normalizedName.slice(USED_WEIGHT_PREFIX.length);
16
+ const value = parseHeaderNumber(headers.get(name));
17
+ if (interval && value !== undefined) {
18
+ weight[interval] = value;
19
+ }
20
+ continue;
21
+ }
22
+
23
+ if (normalizedName.startsWith(ORDER_COUNT_PREFIX)) {
24
+ const interval = normalizedName.slice(ORDER_COUNT_PREFIX.length);
25
+ const value = parseHeaderNumber(headers.get(name));
26
+ if (interval && value !== undefined) {
27
+ orderCount[interval] = value;
28
+ }
29
+ }
30
+ }
31
+
32
+ return Object.keys(weight).length > 0 || Object.keys(orderCount).length > 0
33
+ ? {
34
+ weight: Object.keys(weight).length > 0 ? weight : undefined,
35
+ orderCount: Object.keys(orderCount).length > 0 ? orderCount : undefined,
36
+ }
37
+ : undefined;
38
+ }
39
+
40
+ function parseHeaderNumber(value: string | null): number | undefined {
41
+ if (value === null) {
42
+ return undefined;
43
+ }
44
+
45
+ const parsed = Number(value);
46
+ return Number.isFinite(parsed) ? parsed : undefined;
47
+ }