@cexyio/cexy 0.1.0-dev.4 → 0.1.0-dev.5

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 CHANGED
@@ -75,8 +75,12 @@ if (res.stopped !== "done" || res.failed.length) console.warn("left over:", res.
75
75
 
76
76
  `untilDone` repeats while `has_more` is true or a failure is `INVALID_STATE` / `SERVICE_UNAVAILABLE`.
77
77
  After a call without progress it waits 1, 2, 4, 8, then 15 s, and it stops after `maxRounds` calls
78
- (default 20) or before a wait would pass `timeBudgetMs` (default 120 000). The server allows 30 cancel-all
79
- calls per minute per account; a 429 is retried after its Retry-After, which counts against the budget.
78
+ (default 20) or before a wait would pass `timeBudgetMs` (default 120 000). Every round is exactly one
79
+ request (the loop owns the retries, so it never sends more than `maxRounds` requests): a 429 round waits its
80
+ Retry-After, which counts against the budget; another retryable error (5xx, network) takes the next backoff
81
+ step; a wait that would pass the budget ends the loop with `stopped: "time_budget"` and `last_error_code`.
82
+ A non-retryable error (e.g. a key without the trade scope) throws `CancelAllInterruptedError` with the error
83
+ and the partial result. The server allows 30 cancel-all calls per minute per account.
80
84
 
81
85
  Give both `apiKey` and `apiSecret`, or neither: passing only one throws at construction.
82
86
 
@@ -145,10 +149,13 @@ try {
145
149
 
146
150
  - Timeout per attempt: `timeoutMs` (default 10 s). Retries: `maxRetries` (default 3), exponential backoff with full jitter.
147
151
  - Retried: network errors, timeouts and responses with `retryable: true`.
148
- - 429 waits at least `Retry-After` / `details.retry_after_seconds`.
152
+ - 429 waits at least `Retry-After` / `details.retry_after_seconds`. Server wait hints are untrusted: unusable
153
+ values are ignored, and a hint longer than 120 s (`MAX_SERVER_WAIT_MS`) is never waited: the call fails at
154
+ once with `RateLimitError`, whose `retryAfterMs` still has the server's value. The client-side rate limiter
155
+ never blocks longer than 120 s because of a server hint.
149
156
  - GETs retry freely.
150
- - **Orders:** safety rests on `client_order_id`, not on `Idempotency-Key` (the server does not honour
151
- that header on `POST /trading/orders`, order cancels or cancel-all). `placeOrder` always sends a
157
+ - **Orders:** safety rests on `client_order_id`. The server does not honour `Idempotency-Key` on
158
+ `POST /trading/orders`, order cancels or cancel-all, so the SDK does not send it there. `placeOrder` always sends a
152
159
  `client_order_id` (a UUID if you do not set one); it is unique per account and a repeat is refused
153
160
  before any funds move. After an ambiguous failure (network error, timeout, 5xx) the SDK first looks the
154
161
  order up by that id. If the order exists it is returned with `recovered: true`; only if it does not
@@ -157,9 +164,9 @@ try {
157
164
  - **Cancels:** `cancelOrder` retries network errors; if a *retry* gets `INVALID_STATE`, the first attempt
158
165
  already cancelled the order, so the SDK fetches and returns it. `cancelAll` is naturally repeatable and
159
166
  is retried the same way (a retry reports only what it cancelled).
160
- - **Pool join/exit** send an auto-generated `Idempotency-Key`, reused on every retry; the server honours
161
- it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key still in flight) is
162
- retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
167
+ - **Pool join/exit** are the only requests that send an `Idempotency-Key` (auto-generated, reused on every
168
+ retry); the server honours it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key
169
+ still in flight) is retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
163
170
  - `onRetry` lets you log retries.
164
171
 
165
172
  Every method takes a last `RequestOptions` argument: `{ signal, timeoutMs, maxRetries, idempotencyKey }`.
package/dist/index.cjs CHANGED
@@ -31,6 +31,16 @@ var OrderStateUnknownError = class extends CexyError {
31
31
  this.clientOrderId = clientOrderId;
32
32
  }
33
33
  };
34
+ var CancelAllInterruptedError = class extends CexyError {
35
+ error;
36
+ partial;
37
+ constructor(error, partial) {
38
+ const what = error instanceof CexyApiError ? `${error.code}: ${error.message}` : String(error);
39
+ super(`cancelAll stopped after ${partial.rounds} round(s): ${what}`, { cause: error });
40
+ this.error = error;
41
+ this.partial = partial;
42
+ }
43
+ };
34
44
  var CexyApiError = class extends CexyError {
35
45
  status;
36
46
  code;
@@ -136,20 +146,22 @@ function isKnownErrorCode(code) {
136
146
  return KNOWN_CODES.has(code);
137
147
  }
138
148
  var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
149
+ var MAX_SERVER_WAIT_MS = 12e4;
139
150
  function retryAfterMs(headers, details) {
140
151
  let best = null;
141
- const h = headers?.get("retry-after");
152
+ const h = headers?.get("retry-after")?.trim();
142
153
  if (h) {
143
- const secs = Number(h);
144
- if (Number.isFinite(secs)) best = Math.max(0, secs * 1e3);
145
- else {
154
+ if (/^\d+(\.\d+)?$/.test(h)) {
155
+ const ms = Number(h) * 1e3;
156
+ if (Number.isFinite(ms)) best = ms;
157
+ } else if (/[a-z]/i.test(h)) {
146
158
  const at = Date.parse(h);
147
- if (!Number.isNaN(at)) best = Math.max(0, at - Date.now());
159
+ if (Number.isFinite(at)) best = Math.max(0, at - Date.now());
148
160
  }
149
161
  }
150
162
  const d = details?.["retry_after_seconds"];
151
- const ds = typeof d === "number" ? d : typeof d === "string" ? Number(d) : NaN;
152
- if (Number.isFinite(ds)) best = Math.max(best ?? 0, ds * 1e3);
163
+ const ds = typeof d === "number" ? d : typeof d === "string" && /^\d+(\.\d+)?$/.test(d.trim()) ? Number(d) : NaN;
164
+ if (Number.isFinite(ds) && ds >= 0 && Number.isFinite(ds * 1e3)) best = Math.max(best ?? 0, ds * 1e3);
153
165
  return best;
154
166
  }
155
167
  function errorFromResponse(status, body, headers, redact = (t) => t) {
@@ -312,6 +324,7 @@ var OPERATIONS = {
312
324
  };
313
325
 
314
326
  // src/http.ts
327
+ var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
315
328
  var BACKOFF_BASE_MS = 500;
316
329
  var BACKOFF_MAX_MS = 1e4;
317
330
  var Transport = class {
@@ -320,16 +333,15 @@ var Transport = class {
320
333
  this.config = config;
321
334
  }
322
335
  /**
323
- * Sends a request with the standard retry policy: GETs retry on retryable errors and
324
- * network failures. Mutations carry an `Idempotency-Key` reused on every attempt; the server
325
- * honours it on pool join/exit, which makes their retries safe. The other mutations routed
326
- * here (cancel-all) are naturally repeatable. `placeOrder` and `cancelOrder` use
336
+ * Sends a request with the standard retry policy: retryable errors and network failures are
337
+ * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
338
+ * honours it there, which makes their retries safe); the other mutations routed here
339
+ * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
327
340
  * `attempt()` with their own policies.
328
341
  */
329
342
  async request(spec, opts = {}) {
330
343
  const info = OPERATIONS[spec.op];
331
- const isMutation = info.method !== "GET";
332
- const idempotencyKey = isMutation ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
344
+ const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
333
345
  const maxRetries = opts.maxRetries ?? this.config.maxRetries;
334
346
  for (let attempt = 0; ; attempt++) {
335
347
  try {
@@ -340,8 +352,13 @@ var Transport = class {
340
352
  }
341
353
  }
342
354
  }
343
- /** Waits before retry number `attempt + 1`, honouring server hints. */
355
+ /**
356
+ * Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
357
+ * `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
358
+ */
344
359
  async backoff(op2, info, attempt, err, idempotencyKey, signal) {
360
+ const hint = serverHintMs(err);
361
+ if (hint !== null && hint > MAX_SERVER_WAIT_MS) throw err;
345
362
  const delayMs = this.retryDelay(attempt, err);
346
363
  this.config.onRetry?.({
347
364
  operation: op2,
@@ -356,8 +373,8 @@ var Transport = class {
356
373
  }
357
374
  /** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
358
375
  retryDelay(attempt, err) {
359
- const hint = err instanceof RateLimitError ? err.retryAfterMs : err instanceof CexyApiError ? retryAfterMs(void 0, err.details) : null;
360
- if (hint !== null && hint > 0) return Math.ceil(hint + this.config.random() * 250);
376
+ const hint = serverHintMs(err);
377
+ if (hint !== null && hint > 0 && hint <= MAX_SERVER_WAIT_MS) return Math.ceil(hint + this.config.random() * 250);
361
378
  const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
362
379
  return Math.ceil(this.config.random() * cap);
363
380
  }
@@ -372,7 +389,7 @@ var Transport = class {
372
389
  body = JSON.stringify(spec.body);
373
390
  headers.set("Content-Type", "application/json");
374
391
  }
375
- if (info.method !== "GET" && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
392
+ if (IDEMPOTENT_OPS.has(spec.op) && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
376
393
  if (info.auth === "api_key") {
377
394
  const auth = this.config.authenticator;
378
395
  if (!auth) {
@@ -455,6 +472,11 @@ var Transport = class {
455
472
  return this.config.authenticator ? this.config.authenticator.redact(text) : text;
456
473
  }
457
474
  };
475
+ function serverHintMs(err) {
476
+ if (err instanceof RateLimitError) return err.retryAfterMs;
477
+ if (err instanceof CexyApiError) return retryAfterMs(void 0, err.details);
478
+ return null;
479
+ }
458
480
  function isRetryable(err) {
459
481
  if (err instanceof CexyConnectionError) return true;
460
482
  if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
@@ -486,6 +508,7 @@ function errMessage(err) {
486
508
  }
487
509
 
488
510
  // src/limiter.ts
511
+ var MAX_BLOCK_MS = 12e4;
489
512
  var RateLimiter = class {
490
513
  #rpm;
491
514
  #tokens;
@@ -519,14 +542,14 @@ var RateLimiter = class {
519
542
  return;
520
543
  }
521
544
  const msPerToken = 6e4 / this.#rpm;
522
- await this.#sleep(Math.ceil((1 - this.#tokens) * msPerToken), signal);
545
+ await this.#sleep(Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * msPerToken)), signal);
523
546
  }
524
547
  }
525
548
  /** Adapts to the server's rate-limit headers. Never raises the configured limit. */
526
549
  update(headers) {
527
550
  this.#refill();
528
551
  const limit = num(headers.get("x-ratelimit-limit"));
529
- if (limit !== null && limit > 0 && limit < this.#rpm) {
552
+ if (limit !== null && limit >= 1 && limit < this.#rpm) {
530
553
  this.#rpm = limit;
531
554
  this.#tokens = Math.min(this.#tokens, limit);
532
555
  }
@@ -538,10 +561,13 @@ var RateLimiter = class {
538
561
  else this.blockFor(6e4 / this.#rpm);
539
562
  }
540
563
  }
541
- /** Blocks all requests for `ms` (used for 429 Retry-After). */
564
+ /**
565
+ * Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
566
+ * hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
567
+ */
542
568
  blockFor(ms) {
543
- if (!(ms > 0)) return;
544
- this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + ms);
569
+ if (!(ms > 0) || !Number.isFinite(ms)) return;
570
+ this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + Math.min(ms, MAX_BLOCK_MS));
545
571
  }
546
572
  #refill() {
547
573
  const now = this.#now();
@@ -793,6 +819,12 @@ var WalletResource = class extends Resource {
793
819
  };
794
820
  var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
795
821
  var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
822
+ function errorCode(err) {
823
+ if (err instanceof CexyApiError) return err.code;
824
+ if (err instanceof CexyTimeoutError) return "TIMEOUT";
825
+ if (err instanceof CexyConnectionError) return "CONNECTION_ERROR";
826
+ return "ERROR";
827
+ }
796
828
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
797
829
  var TradingResource = class extends Resource {
798
830
  /** Open orders, optionally filtered by market/status. */
@@ -823,7 +855,7 @@ var TradingResource = class extends Resource {
823
855
  *
824
856
  * Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
825
857
  * per account and the server refuses a repeat before any funds move. The server does NOT
826
- * honour `Idempotency-Key` on orders (the header is sent but gives no protection). After an
858
+ * honour `Idempotency-Key` on orders, so none is sent. After an
827
859
  * ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
828
860
  * `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
829
861
  * exist does it send the order again, with the same `client_order_id`, so a late-arriving
@@ -838,12 +870,11 @@ var TradingResource = class extends Resource {
838
870
  assertAmountFields(order, ORDER_AMOUNT_FIELDS, "placeOrder");
839
871
  const clientOrderId = order.client_order_id ?? newId();
840
872
  const body = { ...order, client_order_id: clientOrderId };
841
- const idempotencyKey = opts.idempotencyKey ?? newId();
842
873
  const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
843
874
  const info = OPERATIONS.place_order;
844
875
  for (let attempt = 0; ; attempt++) {
845
876
  try {
846
- const raw = await this.t.attempt({ op: "place_order", body, idempotencyKey }, opts);
877
+ const raw = await this.t.attempt({ op: "place_order", body }, opts);
847
878
  const data = raw.data.data;
848
879
  return { ...data, client_order_id: clientOrderId, recovered: false };
849
880
  } catch (err) {
@@ -853,11 +884,11 @@ var TradingResource = class extends Resource {
853
884
  const existing = await this.#lookup(clientOrderId, err, opts);
854
885
  if (existing) return { order: existing, fills: [], client_order_id: clientOrderId, recovered: true };
855
886
  if (duplicateAfterRetry || attempt >= maxRetries) throw err;
856
- await this.t.backoff("place_order", info, attempt, err, idempotencyKey, opts.signal);
887
+ await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
857
888
  continue;
858
889
  }
859
890
  if (isRetryable(err) && attempt < maxRetries) {
860
- await this.t.backoff("place_order", info, attempt, err, idempotencyKey, opts.signal);
891
+ await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
861
892
  continue;
862
893
  }
863
894
  throw err;
@@ -881,10 +912,9 @@ var TradingResource = class extends Resource {
881
912
  async cancelOrder(orderId, opts = {}) {
882
913
  const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
883
914
  const info = OPERATIONS.cancel_order;
884
- const idempotencyKey = opts.idempotencyKey ?? newId();
885
915
  for (let attempt = 0; ; attempt++) {
886
916
  try {
887
- const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId }, idempotencyKey }, opts);
917
+ const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId } }, opts);
888
918
  return raw.data.data;
889
919
  } catch (err) {
890
920
  if (opts.signal?.aborted) throw err;
@@ -892,7 +922,7 @@ var TradingResource = class extends Resource {
892
922
  return this.order(orderId, { signal: opts.signal, timeoutMs: opts.timeoutMs });
893
923
  }
894
924
  if (isRetryable(err) && attempt < maxRetries) {
895
- await this.t.backoff("cancel_order", info, attempt, err, idempotencyKey, opts.signal);
925
+ await this.t.backoff("cancel_order", info, attempt, err, void 0, opts.signal);
896
926
  continue;
897
927
  }
898
928
  throw err;
@@ -906,7 +936,7 @@ var TradingResource = class extends Resource {
906
936
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
907
937
  else if (hasSymbol && symbol === null) body = {};
908
938
  else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
909
- const once = () => this.data({ op: "cancel_all", body }, opts);
939
+ const once = (o) => this.data({ op: "cancel_all", body }, o ?? opts);
910
940
  if (params.untilDone !== true) return once();
911
941
  const maxRounds = params.maxRounds ?? 20;
912
942
  const budgetMs = params.timeBudgetMs ?? 12e4;
@@ -915,53 +945,88 @@ var TradingResource = class extends Resource {
915
945
  const now = this.t.config.now ?? Date.now;
916
946
  const start = now();
917
947
  const state = /* @__PURE__ */ new Map();
948
+ const roundOpts = { ...opts, maxRetries: 0 };
918
949
  let rounds = 0;
919
950
  let idle = 0;
920
- let last;
951
+ let hasMore = false;
952
+ let lastErrorCode;
921
953
  let stopped;
954
+ const result = () => {
955
+ const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
956
+ return {
957
+ cancelled: pick("cancelled"),
958
+ already_closed: pick("already_closed"),
959
+ failed: pick("failed"),
960
+ failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
961
+ has_more: hasMore,
962
+ rounds,
963
+ stopped,
964
+ ...lastErrorCode !== void 0 ? { last_error_code: lastErrorCode } : {}
965
+ };
966
+ };
922
967
  for (; ; ) {
923
- last = await once();
924
- rounds++;
925
- const failures = last.failures ?? [];
926
- for (const id of last.cancelled ?? []) state.set(id, { list: "cancelled" });
927
- for (const id of last.already_closed ?? []) state.set(id, { list: "already_closed" });
928
- for (const id of last.failed ?? []) {
929
- const failure = failures.find((f) => f.order_id === id);
930
- state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
931
- }
932
- const progress = (last.cancelled?.length ?? 0) + (last.already_closed?.length ?? 0) > 0;
933
- if (!last.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
934
- stopped = "done";
935
- break;
968
+ let waitMs = 0;
969
+ let res;
970
+ try {
971
+ res = await once(roundOpts);
972
+ } catch (err) {
973
+ rounds++;
974
+ if (opts?.signal?.aborted) throw err;
975
+ if (!isRetryable(err)) {
976
+ stopped = "done";
977
+ throw new CancelAllInterruptedError(err, result());
978
+ }
979
+ lastErrorCode = errorCode(err);
980
+ if (rounds >= maxRounds) {
981
+ stopped = "max_rounds";
982
+ break;
983
+ }
984
+ const hint = err instanceof RateLimitError ? err.retryAfterMs : null;
985
+ if (hint !== null) {
986
+ waitMs = hint;
987
+ if (hint > MAX_SERVER_WAIT_MS) {
988
+ stopped = "time_budget";
989
+ break;
990
+ }
991
+ } else {
992
+ waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
993
+ }
936
994
  }
937
- if (rounds >= maxRounds) {
938
- stopped = "max_rounds";
939
- break;
995
+ if (res) {
996
+ rounds++;
997
+ lastErrorCode = void 0;
998
+ hasMore = res.has_more;
999
+ const failures = res.failures ?? [];
1000
+ for (const id of res.cancelled ?? []) state.set(id, { list: "cancelled" });
1001
+ for (const id of res.already_closed ?? []) state.set(id, { list: "already_closed" });
1002
+ for (const id of res.failed ?? []) {
1003
+ const failure = failures.find((f) => f.order_id === id);
1004
+ state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
1005
+ }
1006
+ const progress = (res.cancelled?.length ?? 0) + (res.already_closed?.length ?? 0) > 0;
1007
+ if (!res.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
1008
+ stopped = "done";
1009
+ break;
1010
+ }
1011
+ if (rounds >= maxRounds) {
1012
+ stopped = "max_rounds";
1013
+ break;
1014
+ }
1015
+ if (progress) idle = 0;
1016
+ else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
940
1017
  }
941
- let waitMs = 0;
942
- if (progress) idle = 0;
943
- else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
944
1018
  if (now() - start + waitMs >= budgetMs) {
945
1019
  stopped = "time_budget";
946
1020
  break;
947
1021
  }
948
1022
  if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
949
1023
  }
950
- const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
951
- return {
952
- cancelled: pick("cancelled"),
953
- already_closed: pick("already_closed"),
954
- failed: pick("failed"),
955
- failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
956
- has_more: last.has_more,
957
- rounds,
958
- stopped
959
- };
1024
+ return result();
960
1025
  }
961
1026
  };
962
1027
 
963
1028
  // src/version.ts
964
- var VERSION = "0.1.0-dev.4";
1029
+ var VERSION = "0.1.0-dev.5";
965
1030
  var USER_AGENT = `cexy-typescript/${VERSION}`;
966
1031
 
967
1032
  // src/ws/emitter.ts
@@ -1729,7 +1794,8 @@ var CexyClient = class {
1729
1794
  const sleep2 = options.sleep ?? sleep;
1730
1795
  const limiter = options.rateLimit === false ? null : new RateLimiter({
1731
1796
  requestsPerMinute: options.rateLimit?.requestsPerMinute ?? (authenticator ? DEFAULT_RPM_WITH_KEY : DEFAULT_RPM_ANONYMOUS),
1732
- sleep: sleep2
1797
+ sleep: sleep2,
1798
+ ...options.now ? { now: options.now } : {}
1733
1799
  });
1734
1800
  this.#baseUrl = baseUrl;
1735
1801
  this.#transport = new Transport({
@@ -1817,6 +1883,7 @@ exports.ApiKeyAuthenticator = ApiKeyAuthenticator;
1817
1883
  exports.AssetsResource = AssetsResource;
1818
1884
  exports.AuthenticationError = AuthenticationError;
1819
1885
  exports.CLIENT_ERROR_CODES = CLIENT_ERROR_CODES;
1886
+ exports.CancelAllInterruptedError = CancelAllInterruptedError;
1820
1887
  exports.CexyApiError = CexyApiError;
1821
1888
  exports.CexyClient = CexyClient;
1822
1889
  exports.CexyConfigError = CexyConfigError;
@@ -1837,6 +1904,7 @@ exports.InvalidAmountError = InvalidAmountError;
1837
1904
  exports.JurisdictionBlockedError = JurisdictionBlockedError;
1838
1905
  exports.KNOWN_EVENT_TYPES = KNOWN_EVENT_TYPES;
1839
1906
  exports.LiveOrderBook = LiveOrderBook;
1907
+ exports.MAX_SERVER_WAIT_MS = MAX_SERVER_WAIT_MS;
1840
1908
  exports.MarketsResource = MarketsResource;
1841
1909
  exports.NetworksResource = NetworksResource;
1842
1910
  exports.NotFoundError = NotFoundError;
package/dist/index.d.cts CHANGED
@@ -36,12 +36,6 @@ declare class ApiKeyAuthenticator implements Authenticator {
36
36
  toJSON(): Record<string, string>;
37
37
  }
38
38
 
39
- /**
40
- * Client-side token bucket. `CexyClient` defaults to 100 requests/minute without credentials
41
- * and 300/minute with an API key (the server allows about 120/min per IP and 600/min per
42
- * key; this keeps a margin). It adapts to `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
43
- * `X-RateLimit-Reset` when the server sends them, and to 429 `Retry-After`.
44
- */
45
39
  interface RateLimiterOptions {
46
40
  requestsPerMinute: number;
47
41
  /** @internal for tests */
@@ -62,7 +56,10 @@ declare class RateLimiter {
62
56
  acquire(signal?: AbortSignal): Promise<void>;
63
57
  /** Adapts to the server's rate-limit headers. Never raises the configured limit. */
64
58
  update(headers: Headers): void;
65
- /** Blocks all requests for `ms` (used for 429 Retry-After). */
59
+ /**
60
+ * Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
61
+ * hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
62
+ */
66
63
  blockFor(ms: number): void;
67
64
  }
68
65
 
@@ -135,10 +132,10 @@ interface RequestOptions {
135
132
  /** Overrides the client's `maxRetries`. */
136
133
  maxRetries?: number;
137
134
  /**
138
- * Mutations only: the `Idempotency-Key` to send. Generated automatically when absent.
139
- * The server honours it on pool join/exit; set it yourself to make a retry across process
140
- * restarts safe there. Orders and cancels do NOT honour it: their safety comes from
141
- * `client_order_id` (see `placeOrder`).
135
+ * Pool join/exit only: the `Idempotency-Key` to send (generated automatically when absent,
136
+ * and reused on every retry). The server honours it there; set it yourself to make a retry
137
+ * across process restarts safe. No other request sends the header: orders and cancels do
138
+ * not honour it, and their safety comes from `client_order_id` (see `placeOrder`).
142
139
  */
143
140
  idempotencyKey?: string;
144
141
  }
@@ -184,14 +181,17 @@ declare class Transport {
184
181
  readonly config: TransportConfig;
185
182
  constructor(config: TransportConfig);
186
183
  /**
187
- * Sends a request with the standard retry policy: GETs retry on retryable errors and
188
- * network failures. Mutations carry an `Idempotency-Key` reused on every attempt; the server
189
- * honours it on pool join/exit, which makes their retries safe. The other mutations routed
190
- * here (cancel-all) are naturally repeatable. `placeOrder` and `cancelOrder` use
184
+ * Sends a request with the standard retry policy: retryable errors and network failures are
185
+ * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
186
+ * honours it there, which makes their retries safe); the other mutations routed here
187
+ * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
191
188
  * `attempt()` with their own policies.
192
189
  */
193
190
  request(spec: CallSpec, opts?: RequestOptions): Promise<RawResponse>;
194
- /** Waits before retry number `attempt + 1`, honouring server hints. */
191
+ /**
192
+ * Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
193
+ * `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
194
+ */
195
195
  backoff(op: OperationId, info: OperationInfo, attempt: number, err: unknown, idempotencyKey: string | undefined, signal?: AbortSignal): Promise<void>;
196
196
  /** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
197
197
  retryDelay(attempt: number, err: unknown): number;
@@ -3238,6 +3238,11 @@ interface CancelAllUntilDoneResult extends CancelAllResult {
3238
3238
  rounds: number;
3239
3239
  /** `done`: nothing left to retry; otherwise the loop's limit that ended it. */
3240
3240
  stopped: CancelAllStopReason;
3241
+ /**
3242
+ * The error code of the last round when that round failed with a retryable error (e.g.
3243
+ * `RATE_LIMITED`, `SERVICE_UNAVAILABLE`, `CONNECTION_ERROR`); absent when it succeeded.
3244
+ */
3245
+ last_error_code?: string;
3241
3246
  }
3242
3247
  type Candle = S["CandleResponse"];
3243
3248
  type CandleInterval = S["CandleInterval"];
@@ -3427,7 +3432,7 @@ declare class TradingResource extends Resource {
3427
3432
  *
3428
3433
  * Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
3429
3434
  * per account and the server refuses a repeat before any funds move. The server does NOT
3430
- * honour `Idempotency-Key` on orders (the header is sent but gives no protection). After an
3435
+ * honour `Idempotency-Key` on orders, so none is sent. After an
3431
3436
  * ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
3432
3437
  * `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
3433
3438
  * exist does it send the order again, with the same `client_order_id`, so a late-arriving
@@ -3459,10 +3464,18 @@ declare class TradingResource extends Resource {
3459
3464
  * waits 1, 2, 4, 8 and then 15 s, starting over after any progress. It stops after
3460
3465
  * `maxRounds` calls or before a wait would pass `timeBudgetMs`, and returns the merged
3461
3466
  * result with `rounds` and `stopped`. Other failure codes are returned, never retried.
3467
+ * In this mode every round is exactly one HTTP request (the loop owns the retries, so it
3468
+ * never sends more than `maxRounds` requests): a 429 round waits the server's Retry-After
3469
+ * (without advancing the backoff), another retryable error (5xx, network) takes the next
3470
+ * backoff step, and a wait that would pass the budget ends the loop with
3471
+ * `stopped: "time_budget"` and `last_error_code`. A non-retryable error (e.g. a key without
3472
+ * the trade scope) throws `CancelAllInterruptedError`, which carries the error and the
3473
+ * partial result.
3462
3474
  *
3463
- * The server allows 30 cancel-all calls per minute per account (a 429 is retried after its
3464
- * Retry-After). The call is naturally repeatable and needs no Idempotency-Key, so it is
3465
- * retried after network errors; a retry reports only what that retry did.
3475
+ * The server allows 30 cancel-all calls per minute per account. A single call (without
3476
+ * `untilDone`) follows the normal retry policy: a 429 is retried after its Retry-After (up to
3477
+ * 120 s; a longer one fails at once) and network errors are retried, since the call is
3478
+ * naturally repeatable; a retry reports only what that retry did. No Idempotency-Key is sent.
3466
3479
  */
3467
3480
  cancelAll(params: CancelAllParams & {
3468
3481
  untilDone: true;
@@ -3512,6 +3525,16 @@ declare class OrderStateUnknownError extends CexyError {
3512
3525
  readonly clientOrderId: string;
3513
3526
  constructor(clientOrderId: string, cause: unknown);
3514
3527
  }
3528
+ /**
3529
+ * A `cancelAll({ ..., untilDone: true })` loop hit an error it does not retry (for example
3530
+ * `ForbiddenError` for a key without the trade scope). `error` is that error; `partial` is what
3531
+ * the earlier rounds did, in the same shape as the loop's normal result.
3532
+ */
3533
+ declare class CancelAllInterruptedError extends CexyError {
3534
+ readonly error: unknown;
3535
+ readonly partial: CancelAllUntilDoneResult;
3536
+ constructor(error: unknown, partial: CancelAllUntilDoneResult);
3537
+ }
3515
3538
  interface CexyApiErrorInit {
3516
3539
  status: number;
3517
3540
  code: ErrorCode;
@@ -3576,6 +3599,12 @@ declare const CLIENT_ERROR_CODES: {
3576
3599
  };
3577
3600
  /** True for codes listed in errors.yaml (server codes; see `CLIENT_ERROR_CODES` for the SDK's own). */
3578
3601
  declare function isKnownErrorCode(code: string): code is KnownErrorCode;
3602
+ /**
3603
+ * The longest wait the SDK accepts from a server hint (`Retry-After`, `retry_after_seconds`,
3604
+ * `X-RateLimit-Reset`): 120 s. A longer hint is never waited; the call fails at once instead
3605
+ * (see `Transport.backoff`), and the client-side rate limiter caps its block at this value.
3606
+ */
3607
+ declare const MAX_SERVER_WAIT_MS = 120000;
3579
3608
  /**
3580
3609
  * Builds the right error subclass for an HTTP error response.
3581
3610
  * - A known code maps by HTTP status (401 -> AuthenticationError, 403 -> ForbiddenError, ...).
@@ -4022,11 +4051,11 @@ declare function isAmount(value: unknown): value is Amount;
4022
4051
  declare function assertAmountFields(body: Record<string, unknown>, fields: readonly string[], context: string): void;
4023
4052
 
4024
4053
  /** SDK version, kept in sync with package.json (a test enforces this). */
4025
- declare const VERSION = "0.1.0-dev.4";
4054
+ declare const VERSION = "0.1.0-dev.5";
4026
4055
  /** Default User-Agent product token. */
4027
- declare const USER_AGENT = "cexy-typescript/0.1.0-dev.4";
4056
+ declare const USER_AGENT = "cexy-typescript/0.1.0-dev.5";
4028
4057
 
4029
4058
  /** True for loopback hosts, the only ones where plain-text transport may be allowed. */
4030
4059
  declare function isLocalHost(hostname: string): boolean;
4031
4060
 
4032
- export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, CLIENT_ERROR_CODES, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type CancelAllStopReason, type CancelAllUntilDoneResult, type CancelFailure, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
4061
+ export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, CLIENT_ERROR_CODES, CancelAllInterruptedError, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type CancelAllStopReason, type CancelAllUntilDoneResult, type CancelFailure, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, MAX_SERVER_WAIT_MS, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
package/dist/index.d.ts CHANGED
@@ -36,12 +36,6 @@ declare class ApiKeyAuthenticator implements Authenticator {
36
36
  toJSON(): Record<string, string>;
37
37
  }
38
38
 
39
- /**
40
- * Client-side token bucket. `CexyClient` defaults to 100 requests/minute without credentials
41
- * and 300/minute with an API key (the server allows about 120/min per IP and 600/min per
42
- * key; this keeps a margin). It adapts to `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
43
- * `X-RateLimit-Reset` when the server sends them, and to 429 `Retry-After`.
44
- */
45
39
  interface RateLimiterOptions {
46
40
  requestsPerMinute: number;
47
41
  /** @internal for tests */
@@ -62,7 +56,10 @@ declare class RateLimiter {
62
56
  acquire(signal?: AbortSignal): Promise<void>;
63
57
  /** Adapts to the server's rate-limit headers. Never raises the configured limit. */
64
58
  update(headers: Headers): void;
65
- /** Blocks all requests for `ms` (used for 429 Retry-After). */
59
+ /**
60
+ * Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
61
+ * hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
62
+ */
66
63
  blockFor(ms: number): void;
67
64
  }
68
65
 
@@ -135,10 +132,10 @@ interface RequestOptions {
135
132
  /** Overrides the client's `maxRetries`. */
136
133
  maxRetries?: number;
137
134
  /**
138
- * Mutations only: the `Idempotency-Key` to send. Generated automatically when absent.
139
- * The server honours it on pool join/exit; set it yourself to make a retry across process
140
- * restarts safe there. Orders and cancels do NOT honour it: their safety comes from
141
- * `client_order_id` (see `placeOrder`).
135
+ * Pool join/exit only: the `Idempotency-Key` to send (generated automatically when absent,
136
+ * and reused on every retry). The server honours it there; set it yourself to make a retry
137
+ * across process restarts safe. No other request sends the header: orders and cancels do
138
+ * not honour it, and their safety comes from `client_order_id` (see `placeOrder`).
142
139
  */
143
140
  idempotencyKey?: string;
144
141
  }
@@ -184,14 +181,17 @@ declare class Transport {
184
181
  readonly config: TransportConfig;
185
182
  constructor(config: TransportConfig);
186
183
  /**
187
- * Sends a request with the standard retry policy: GETs retry on retryable errors and
188
- * network failures. Mutations carry an `Idempotency-Key` reused on every attempt; the server
189
- * honours it on pool join/exit, which makes their retries safe. The other mutations routed
190
- * here (cancel-all) are naturally repeatable. `placeOrder` and `cancelOrder` use
184
+ * Sends a request with the standard retry policy: retryable errors and network failures are
185
+ * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
186
+ * honours it there, which makes their retries safe); the other mutations routed here
187
+ * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
191
188
  * `attempt()` with their own policies.
192
189
  */
193
190
  request(spec: CallSpec, opts?: RequestOptions): Promise<RawResponse>;
194
- /** Waits before retry number `attempt + 1`, honouring server hints. */
191
+ /**
192
+ * Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
193
+ * `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
194
+ */
195
195
  backoff(op: OperationId, info: OperationInfo, attempt: number, err: unknown, idempotencyKey: string | undefined, signal?: AbortSignal): Promise<void>;
196
196
  /** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
197
197
  retryDelay(attempt: number, err: unknown): number;
@@ -3238,6 +3238,11 @@ interface CancelAllUntilDoneResult extends CancelAllResult {
3238
3238
  rounds: number;
3239
3239
  /** `done`: nothing left to retry; otherwise the loop's limit that ended it. */
3240
3240
  stopped: CancelAllStopReason;
3241
+ /**
3242
+ * The error code of the last round when that round failed with a retryable error (e.g.
3243
+ * `RATE_LIMITED`, `SERVICE_UNAVAILABLE`, `CONNECTION_ERROR`); absent when it succeeded.
3244
+ */
3245
+ last_error_code?: string;
3241
3246
  }
3242
3247
  type Candle = S["CandleResponse"];
3243
3248
  type CandleInterval = S["CandleInterval"];
@@ -3427,7 +3432,7 @@ declare class TradingResource extends Resource {
3427
3432
  *
3428
3433
  * Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
3429
3434
  * per account and the server refuses a repeat before any funds move. The server does NOT
3430
- * honour `Idempotency-Key` on orders (the header is sent but gives no protection). After an
3435
+ * honour `Idempotency-Key` on orders, so none is sent. After an
3431
3436
  * ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
3432
3437
  * `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
3433
3438
  * exist does it send the order again, with the same `client_order_id`, so a late-arriving
@@ -3459,10 +3464,18 @@ declare class TradingResource extends Resource {
3459
3464
  * waits 1, 2, 4, 8 and then 15 s, starting over after any progress. It stops after
3460
3465
  * `maxRounds` calls or before a wait would pass `timeBudgetMs`, and returns the merged
3461
3466
  * result with `rounds` and `stopped`. Other failure codes are returned, never retried.
3467
+ * In this mode every round is exactly one HTTP request (the loop owns the retries, so it
3468
+ * never sends more than `maxRounds` requests): a 429 round waits the server's Retry-After
3469
+ * (without advancing the backoff), another retryable error (5xx, network) takes the next
3470
+ * backoff step, and a wait that would pass the budget ends the loop with
3471
+ * `stopped: "time_budget"` and `last_error_code`. A non-retryable error (e.g. a key without
3472
+ * the trade scope) throws `CancelAllInterruptedError`, which carries the error and the
3473
+ * partial result.
3462
3474
  *
3463
- * The server allows 30 cancel-all calls per minute per account (a 429 is retried after its
3464
- * Retry-After). The call is naturally repeatable and needs no Idempotency-Key, so it is
3465
- * retried after network errors; a retry reports only what that retry did.
3475
+ * The server allows 30 cancel-all calls per minute per account. A single call (without
3476
+ * `untilDone`) follows the normal retry policy: a 429 is retried after its Retry-After (up to
3477
+ * 120 s; a longer one fails at once) and network errors are retried, since the call is
3478
+ * naturally repeatable; a retry reports only what that retry did. No Idempotency-Key is sent.
3466
3479
  */
3467
3480
  cancelAll(params: CancelAllParams & {
3468
3481
  untilDone: true;
@@ -3512,6 +3525,16 @@ declare class OrderStateUnknownError extends CexyError {
3512
3525
  readonly clientOrderId: string;
3513
3526
  constructor(clientOrderId: string, cause: unknown);
3514
3527
  }
3528
+ /**
3529
+ * A `cancelAll({ ..., untilDone: true })` loop hit an error it does not retry (for example
3530
+ * `ForbiddenError` for a key without the trade scope). `error` is that error; `partial` is what
3531
+ * the earlier rounds did, in the same shape as the loop's normal result.
3532
+ */
3533
+ declare class CancelAllInterruptedError extends CexyError {
3534
+ readonly error: unknown;
3535
+ readonly partial: CancelAllUntilDoneResult;
3536
+ constructor(error: unknown, partial: CancelAllUntilDoneResult);
3537
+ }
3515
3538
  interface CexyApiErrorInit {
3516
3539
  status: number;
3517
3540
  code: ErrorCode;
@@ -3576,6 +3599,12 @@ declare const CLIENT_ERROR_CODES: {
3576
3599
  };
3577
3600
  /** True for codes listed in errors.yaml (server codes; see `CLIENT_ERROR_CODES` for the SDK's own). */
3578
3601
  declare function isKnownErrorCode(code: string): code is KnownErrorCode;
3602
+ /**
3603
+ * The longest wait the SDK accepts from a server hint (`Retry-After`, `retry_after_seconds`,
3604
+ * `X-RateLimit-Reset`): 120 s. A longer hint is never waited; the call fails at once instead
3605
+ * (see `Transport.backoff`), and the client-side rate limiter caps its block at this value.
3606
+ */
3607
+ declare const MAX_SERVER_WAIT_MS = 120000;
3579
3608
  /**
3580
3609
  * Builds the right error subclass for an HTTP error response.
3581
3610
  * - A known code maps by HTTP status (401 -> AuthenticationError, 403 -> ForbiddenError, ...).
@@ -4022,11 +4051,11 @@ declare function isAmount(value: unknown): value is Amount;
4022
4051
  declare function assertAmountFields(body: Record<string, unknown>, fields: readonly string[], context: string): void;
4023
4052
 
4024
4053
  /** SDK version, kept in sync with package.json (a test enforces this). */
4025
- declare const VERSION = "0.1.0-dev.4";
4054
+ declare const VERSION = "0.1.0-dev.5";
4026
4055
  /** Default User-Agent product token. */
4027
- declare const USER_AGENT = "cexy-typescript/0.1.0-dev.4";
4056
+ declare const USER_AGENT = "cexy-typescript/0.1.0-dev.5";
4028
4057
 
4029
4058
  /** True for loopback hosts, the only ones where plain-text transport may be allowed. */
4030
4059
  declare function isLocalHost(hostname: string): boolean;
4031
4060
 
4032
- export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, CLIENT_ERROR_CODES, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type CancelAllStopReason, type CancelAllUntilDoneResult, type CancelFailure, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
4061
+ export { AccountResource, type Amount, type ApiKey, ApiKeyAuthenticator, type ApiScope, type Asset, type AssetNetwork, AssetsResource, type AuthRequest, type AuthResult, type AuthenticatedFrame, AuthenticationError, type Authenticator, type Balance, type BalanceUpdatedEvent, type BookLevel, CLIENT_ERROR_CODES, CancelAllInterruptedError, type CancelAllParams, type CancelAllRequest, type CancelAllResult, type CancelAllStopReason, type CancelAllUntilDoneResult, type CancelFailure, type Candle, type CandleInterval, CexyApiError, type CexyApiErrorInit, CexyClient, type CexyClientOptions, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, type CexyWebSocketEvents, type CexyWebSocketOptions, type CloseInfo, ConflictError, type CursorParams, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, type Deposit, type DepositAddress, type DepositEvent, type DepositStatus, type ErrorBody, type ErrorCode, type ErrorFrame, type EventMap, type ExchangeConfig, type ExitPoolRequest, type ExitPoolResult, ExportsResource, type FeeSchedule, FeesResource, type FetchLike, type Fill, ForbiddenError, InvalidAmountError, type IterateOptions, type JoinPoolRequest, type JoinPoolResult, JurisdictionBlockedError, KNOWN_EVENT_TYPES, type KnownErrorCode, type LedgerEntry, type LedgerEntryKind, type LiquidityRole, type Listener, LiveOrderBook, type LiveOrderBookEvents, type LiveOrderBookOptions, MAX_SERVER_WAIT_MS, type MaintenanceState, type Market, type MarketStatus, type MarketStatusEvent, MarketsResource, type Network, NetworksResource, NotFoundError, type Notification, type NotificationKind, OPERATIONS, type OperationAuth, type OperationId, type OperationInfo, type OperationScope, type Order, type OrderBook, type OrderBookUpdateData, type OrderBookUpdateEvent, type OrderEvent, type OrderSide, OrderStateUnknownError, type OrderStatus, type OrderType, PRIVATE_CHANNELS, type Page, type PlaceOrderRequest, type PlaceOrderResponse, type PlaceOrderResult, type PongFrame, type Pool, type PoolStatus, PoolsResource, type PublicTrade, type QueryOf, RateLimitError, RateLimiter, type RateLimiterOptions, type RateLimiterState, type ReconnectOptions, type RequestOptions, type ResyncReason, type RetryInfo, SUPPORTED_PROTOCOL_VERSION, ServerError, type ServerTime, type SessionRevokedData, type SessionRevokedEvent, type SnapshotSource, type SortDirection, type SubAccount, type SubscribeResult, type SubscribedFrame, type TickerUpdateEvent, type TimeInForce, type TradeNewEvent, TradingResource, type TriggerDirection, TypedEmitter, USER_AGENT, UnprocessableError, type UnsubscribedFrame, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, type WebSocketConstructor, type WebSocketLike, type WelcomeFrame, type Withdrawal, type WithdrawalAddress, type WithdrawalStatus, type WithdrawalUpdatedEvent, type WsEvent, type WsLogger, assertAmountFields, type components, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, type operations, paginate, type paths };
package/dist/index.js CHANGED
@@ -29,6 +29,16 @@ var OrderStateUnknownError = class extends CexyError {
29
29
  this.clientOrderId = clientOrderId;
30
30
  }
31
31
  };
32
+ var CancelAllInterruptedError = class extends CexyError {
33
+ error;
34
+ partial;
35
+ constructor(error, partial) {
36
+ const what = error instanceof CexyApiError ? `${error.code}: ${error.message}` : String(error);
37
+ super(`cancelAll stopped after ${partial.rounds} round(s): ${what}`, { cause: error });
38
+ this.error = error;
39
+ this.partial = partial;
40
+ }
41
+ };
32
42
  var CexyApiError = class extends CexyError {
33
43
  status;
34
44
  code;
@@ -134,20 +144,22 @@ function isKnownErrorCode(code) {
134
144
  return KNOWN_CODES.has(code);
135
145
  }
136
146
  var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
147
+ var MAX_SERVER_WAIT_MS = 12e4;
137
148
  function retryAfterMs(headers, details) {
138
149
  let best = null;
139
- const h = headers?.get("retry-after");
150
+ const h = headers?.get("retry-after")?.trim();
140
151
  if (h) {
141
- const secs = Number(h);
142
- if (Number.isFinite(secs)) best = Math.max(0, secs * 1e3);
143
- else {
152
+ if (/^\d+(\.\d+)?$/.test(h)) {
153
+ const ms = Number(h) * 1e3;
154
+ if (Number.isFinite(ms)) best = ms;
155
+ } else if (/[a-z]/i.test(h)) {
144
156
  const at = Date.parse(h);
145
- if (!Number.isNaN(at)) best = Math.max(0, at - Date.now());
157
+ if (Number.isFinite(at)) best = Math.max(0, at - Date.now());
146
158
  }
147
159
  }
148
160
  const d = details?.["retry_after_seconds"];
149
- const ds = typeof d === "number" ? d : typeof d === "string" ? Number(d) : NaN;
150
- if (Number.isFinite(ds)) best = Math.max(best ?? 0, ds * 1e3);
161
+ const ds = typeof d === "number" ? d : typeof d === "string" && /^\d+(\.\d+)?$/.test(d.trim()) ? Number(d) : NaN;
162
+ if (Number.isFinite(ds) && ds >= 0 && Number.isFinite(ds * 1e3)) best = Math.max(best ?? 0, ds * 1e3);
151
163
  return best;
152
164
  }
153
165
  function errorFromResponse(status, body, headers, redact = (t) => t) {
@@ -310,6 +322,7 @@ var OPERATIONS = {
310
322
  };
311
323
 
312
324
  // src/http.ts
325
+ var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
313
326
  var BACKOFF_BASE_MS = 500;
314
327
  var BACKOFF_MAX_MS = 1e4;
315
328
  var Transport = class {
@@ -318,16 +331,15 @@ var Transport = class {
318
331
  this.config = config;
319
332
  }
320
333
  /**
321
- * Sends a request with the standard retry policy: GETs retry on retryable errors and
322
- * network failures. Mutations carry an `Idempotency-Key` reused on every attempt; the server
323
- * honours it on pool join/exit, which makes their retries safe. The other mutations routed
324
- * here (cancel-all) are naturally repeatable. `placeOrder` and `cancelOrder` use
334
+ * Sends a request with the standard retry policy: retryable errors and network failures are
335
+ * retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
336
+ * honours it there, which makes their retries safe); the other mutations routed here
337
+ * (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
325
338
  * `attempt()` with their own policies.
326
339
  */
327
340
  async request(spec, opts = {}) {
328
341
  const info = OPERATIONS[spec.op];
329
- const isMutation = info.method !== "GET";
330
- const idempotencyKey = isMutation ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
342
+ const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
331
343
  const maxRetries = opts.maxRetries ?? this.config.maxRetries;
332
344
  for (let attempt = 0; ; attempt++) {
333
345
  try {
@@ -338,8 +350,13 @@ var Transport = class {
338
350
  }
339
351
  }
340
352
  }
341
- /** Waits before retry number `attempt + 1`, honouring server hints. */
353
+ /**
354
+ * Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
355
+ * `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
356
+ */
342
357
  async backoff(op2, info, attempt, err, idempotencyKey, signal) {
358
+ const hint = serverHintMs(err);
359
+ if (hint !== null && hint > MAX_SERVER_WAIT_MS) throw err;
343
360
  const delayMs = this.retryDelay(attempt, err);
344
361
  this.config.onRetry?.({
345
362
  operation: op2,
@@ -354,8 +371,8 @@ var Transport = class {
354
371
  }
355
372
  /** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
356
373
  retryDelay(attempt, err) {
357
- const hint = err instanceof RateLimitError ? err.retryAfterMs : err instanceof CexyApiError ? retryAfterMs(void 0, err.details) : null;
358
- if (hint !== null && hint > 0) return Math.ceil(hint + this.config.random() * 250);
374
+ const hint = serverHintMs(err);
375
+ if (hint !== null && hint > 0 && hint <= MAX_SERVER_WAIT_MS) return Math.ceil(hint + this.config.random() * 250);
359
376
  const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
360
377
  return Math.ceil(this.config.random() * cap);
361
378
  }
@@ -370,7 +387,7 @@ var Transport = class {
370
387
  body = JSON.stringify(spec.body);
371
388
  headers.set("Content-Type", "application/json");
372
389
  }
373
- if (info.method !== "GET" && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
390
+ if (IDEMPOTENT_OPS.has(spec.op) && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
374
391
  if (info.auth === "api_key") {
375
392
  const auth = this.config.authenticator;
376
393
  if (!auth) {
@@ -453,6 +470,11 @@ var Transport = class {
453
470
  return this.config.authenticator ? this.config.authenticator.redact(text) : text;
454
471
  }
455
472
  };
473
+ function serverHintMs(err) {
474
+ if (err instanceof RateLimitError) return err.retryAfterMs;
475
+ if (err instanceof CexyApiError) return retryAfterMs(void 0, err.details);
476
+ return null;
477
+ }
456
478
  function isRetryable(err) {
457
479
  if (err instanceof CexyConnectionError) return true;
458
480
  if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
@@ -484,6 +506,7 @@ function errMessage(err) {
484
506
  }
485
507
 
486
508
  // src/limiter.ts
509
+ var MAX_BLOCK_MS = 12e4;
487
510
  var RateLimiter = class {
488
511
  #rpm;
489
512
  #tokens;
@@ -517,14 +540,14 @@ var RateLimiter = class {
517
540
  return;
518
541
  }
519
542
  const msPerToken = 6e4 / this.#rpm;
520
- await this.#sleep(Math.ceil((1 - this.#tokens) * msPerToken), signal);
543
+ await this.#sleep(Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * msPerToken)), signal);
521
544
  }
522
545
  }
523
546
  /** Adapts to the server's rate-limit headers. Never raises the configured limit. */
524
547
  update(headers) {
525
548
  this.#refill();
526
549
  const limit = num(headers.get("x-ratelimit-limit"));
527
- if (limit !== null && limit > 0 && limit < this.#rpm) {
550
+ if (limit !== null && limit >= 1 && limit < this.#rpm) {
528
551
  this.#rpm = limit;
529
552
  this.#tokens = Math.min(this.#tokens, limit);
530
553
  }
@@ -536,10 +559,13 @@ var RateLimiter = class {
536
559
  else this.blockFor(6e4 / this.#rpm);
537
560
  }
538
561
  }
539
- /** Blocks all requests for `ms` (used for 429 Retry-After). */
562
+ /**
563
+ * Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
564
+ * hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
565
+ */
540
566
  blockFor(ms) {
541
- if (!(ms > 0)) return;
542
- this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + ms);
567
+ if (!(ms > 0) || !Number.isFinite(ms)) return;
568
+ this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + Math.min(ms, MAX_BLOCK_MS));
543
569
  }
544
570
  #refill() {
545
571
  const now = this.#now();
@@ -791,6 +817,12 @@ var WalletResource = class extends Resource {
791
817
  };
792
818
  var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
793
819
  var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
820
+ function errorCode(err) {
821
+ if (err instanceof CexyApiError) return err.code;
822
+ if (err instanceof CexyTimeoutError) return "TIMEOUT";
823
+ if (err instanceof CexyConnectionError) return "CONNECTION_ERROR";
824
+ return "ERROR";
825
+ }
794
826
  var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
795
827
  var TradingResource = class extends Resource {
796
828
  /** Open orders, optionally filtered by market/status. */
@@ -821,7 +853,7 @@ var TradingResource = class extends Resource {
821
853
  *
822
854
  * Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
823
855
  * per account and the server refuses a repeat before any funds move. The server does NOT
824
- * honour `Idempotency-Key` on orders (the header is sent but gives no protection). After an
856
+ * honour `Idempotency-Key` on orders, so none is sent. After an
825
857
  * ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
826
858
  * `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
827
859
  * exist does it send the order again, with the same `client_order_id`, so a late-arriving
@@ -836,12 +868,11 @@ var TradingResource = class extends Resource {
836
868
  assertAmountFields(order, ORDER_AMOUNT_FIELDS, "placeOrder");
837
869
  const clientOrderId = order.client_order_id ?? newId();
838
870
  const body = { ...order, client_order_id: clientOrderId };
839
- const idempotencyKey = opts.idempotencyKey ?? newId();
840
871
  const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
841
872
  const info = OPERATIONS.place_order;
842
873
  for (let attempt = 0; ; attempt++) {
843
874
  try {
844
- const raw = await this.t.attempt({ op: "place_order", body, idempotencyKey }, opts);
875
+ const raw = await this.t.attempt({ op: "place_order", body }, opts);
845
876
  const data = raw.data.data;
846
877
  return { ...data, client_order_id: clientOrderId, recovered: false };
847
878
  } catch (err) {
@@ -851,11 +882,11 @@ var TradingResource = class extends Resource {
851
882
  const existing = await this.#lookup(clientOrderId, err, opts);
852
883
  if (existing) return { order: existing, fills: [], client_order_id: clientOrderId, recovered: true };
853
884
  if (duplicateAfterRetry || attempt >= maxRetries) throw err;
854
- await this.t.backoff("place_order", info, attempt, err, idempotencyKey, opts.signal);
885
+ await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
855
886
  continue;
856
887
  }
857
888
  if (isRetryable(err) && attempt < maxRetries) {
858
- await this.t.backoff("place_order", info, attempt, err, idempotencyKey, opts.signal);
889
+ await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
859
890
  continue;
860
891
  }
861
892
  throw err;
@@ -879,10 +910,9 @@ var TradingResource = class extends Resource {
879
910
  async cancelOrder(orderId, opts = {}) {
880
911
  const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
881
912
  const info = OPERATIONS.cancel_order;
882
- const idempotencyKey = opts.idempotencyKey ?? newId();
883
913
  for (let attempt = 0; ; attempt++) {
884
914
  try {
885
- const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId }, idempotencyKey }, opts);
915
+ const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId } }, opts);
886
916
  return raw.data.data;
887
917
  } catch (err) {
888
918
  if (opts.signal?.aborted) throw err;
@@ -890,7 +920,7 @@ var TradingResource = class extends Resource {
890
920
  return this.order(orderId, { signal: opts.signal, timeoutMs: opts.timeoutMs });
891
921
  }
892
922
  if (isRetryable(err) && attempt < maxRetries) {
893
- await this.t.backoff("cancel_order", info, attempt, err, idempotencyKey, opts.signal);
923
+ await this.t.backoff("cancel_order", info, attempt, err, void 0, opts.signal);
894
924
  continue;
895
925
  }
896
926
  throw err;
@@ -904,7 +934,7 @@ var TradingResource = class extends Resource {
904
934
  if (typeof symbol === "string" && symbol !== "") body = { symbol };
905
935
  else if (hasSymbol && symbol === null) body = {};
906
936
  else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
907
- const once = () => this.data({ op: "cancel_all", body }, opts);
937
+ const once = (o) => this.data({ op: "cancel_all", body }, o ?? opts);
908
938
  if (params.untilDone !== true) return once();
909
939
  const maxRounds = params.maxRounds ?? 20;
910
940
  const budgetMs = params.timeBudgetMs ?? 12e4;
@@ -913,53 +943,88 @@ var TradingResource = class extends Resource {
913
943
  const now = this.t.config.now ?? Date.now;
914
944
  const start = now();
915
945
  const state = /* @__PURE__ */ new Map();
946
+ const roundOpts = { ...opts, maxRetries: 0 };
916
947
  let rounds = 0;
917
948
  let idle = 0;
918
- let last;
949
+ let hasMore = false;
950
+ let lastErrorCode;
919
951
  let stopped;
952
+ const result = () => {
953
+ const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
954
+ return {
955
+ cancelled: pick("cancelled"),
956
+ already_closed: pick("already_closed"),
957
+ failed: pick("failed"),
958
+ failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
959
+ has_more: hasMore,
960
+ rounds,
961
+ stopped,
962
+ ...lastErrorCode !== void 0 ? { last_error_code: lastErrorCode } : {}
963
+ };
964
+ };
920
965
  for (; ; ) {
921
- last = await once();
922
- rounds++;
923
- const failures = last.failures ?? [];
924
- for (const id of last.cancelled ?? []) state.set(id, { list: "cancelled" });
925
- for (const id of last.already_closed ?? []) state.set(id, { list: "already_closed" });
926
- for (const id of last.failed ?? []) {
927
- const failure = failures.find((f) => f.order_id === id);
928
- state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
929
- }
930
- const progress = (last.cancelled?.length ?? 0) + (last.already_closed?.length ?? 0) > 0;
931
- if (!last.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
932
- stopped = "done";
933
- break;
966
+ let waitMs = 0;
967
+ let res;
968
+ try {
969
+ res = await once(roundOpts);
970
+ } catch (err) {
971
+ rounds++;
972
+ if (opts?.signal?.aborted) throw err;
973
+ if (!isRetryable(err)) {
974
+ stopped = "done";
975
+ throw new CancelAllInterruptedError(err, result());
976
+ }
977
+ lastErrorCode = errorCode(err);
978
+ if (rounds >= maxRounds) {
979
+ stopped = "max_rounds";
980
+ break;
981
+ }
982
+ const hint = err instanceof RateLimitError ? err.retryAfterMs : null;
983
+ if (hint !== null) {
984
+ waitMs = hint;
985
+ if (hint > MAX_SERVER_WAIT_MS) {
986
+ stopped = "time_budget";
987
+ break;
988
+ }
989
+ } else {
990
+ waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
991
+ }
934
992
  }
935
- if (rounds >= maxRounds) {
936
- stopped = "max_rounds";
937
- break;
993
+ if (res) {
994
+ rounds++;
995
+ lastErrorCode = void 0;
996
+ hasMore = res.has_more;
997
+ const failures = res.failures ?? [];
998
+ for (const id of res.cancelled ?? []) state.set(id, { list: "cancelled" });
999
+ for (const id of res.already_closed ?? []) state.set(id, { list: "already_closed" });
1000
+ for (const id of res.failed ?? []) {
1001
+ const failure = failures.find((f) => f.order_id === id);
1002
+ state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
1003
+ }
1004
+ const progress = (res.cancelled?.length ?? 0) + (res.already_closed?.length ?? 0) > 0;
1005
+ if (!res.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
1006
+ stopped = "done";
1007
+ break;
1008
+ }
1009
+ if (rounds >= maxRounds) {
1010
+ stopped = "max_rounds";
1011
+ break;
1012
+ }
1013
+ if (progress) idle = 0;
1014
+ else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
938
1015
  }
939
- let waitMs = 0;
940
- if (progress) idle = 0;
941
- else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
942
1016
  if (now() - start + waitMs >= budgetMs) {
943
1017
  stopped = "time_budget";
944
1018
  break;
945
1019
  }
946
1020
  if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
947
1021
  }
948
- const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
949
- return {
950
- cancelled: pick("cancelled"),
951
- already_closed: pick("already_closed"),
952
- failed: pick("failed"),
953
- failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
954
- has_more: last.has_more,
955
- rounds,
956
- stopped
957
- };
1022
+ return result();
958
1023
  }
959
1024
  };
960
1025
 
961
1026
  // src/version.ts
962
- var VERSION = "0.1.0-dev.4";
1027
+ var VERSION = "0.1.0-dev.5";
963
1028
  var USER_AGENT = `cexy-typescript/${VERSION}`;
964
1029
 
965
1030
  // src/ws/emitter.ts
@@ -1727,7 +1792,8 @@ var CexyClient = class {
1727
1792
  const sleep2 = options.sleep ?? sleep;
1728
1793
  const limiter = options.rateLimit === false ? null : new RateLimiter({
1729
1794
  requestsPerMinute: options.rateLimit?.requestsPerMinute ?? (authenticator ? DEFAULT_RPM_WITH_KEY : DEFAULT_RPM_ANONYMOUS),
1730
- sleep: sleep2
1795
+ sleep: sleep2,
1796
+ ...options.now ? { now: options.now } : {}
1731
1797
  });
1732
1798
  this.#baseUrl = baseUrl;
1733
1799
  this.#transport = new Transport({
@@ -1810,4 +1876,4 @@ function canSetUserAgent() {
1810
1876
  return !(typeof g.window !== "undefined" && typeof g.window.document !== "undefined");
1811
1877
  }
1812
1878
 
1813
- export { AccountResource, ApiKeyAuthenticator, AssetsResource, AuthenticationError, CLIENT_ERROR_CODES, CexyApiError, CexyClient, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, ConflictError, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, ExportsResource, FeesResource, ForbiddenError, InvalidAmountError, JurisdictionBlockedError, KNOWN_EVENT_TYPES, LiveOrderBook, MarketsResource, NetworksResource, NotFoundError, OPERATIONS, OrderStateUnknownError, PRIVATE_CHANNELS, PoolsResource, RateLimitError, RateLimiter, SUPPORTED_PROTOCOL_VERSION, ServerError, TradingResource, TypedEmitter, USER_AGENT, UnprocessableError, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, assertAmountFields, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, paginate };
1879
+ export { AccountResource, ApiKeyAuthenticator, AssetsResource, AuthenticationError, CLIENT_ERROR_CODES, CancelAllInterruptedError, CexyApiError, CexyClient, CexyConfigError, CexyConnectionError, CexyError, CexyTimeoutError, CexyWebSocket, CexyWebSocketError, ConflictError, DEFAULT_BASE_URL, DEFAULT_RPM_ANONYMOUS, DEFAULT_RPM_WITH_KEY, DEFAULT_WS_URL, ExportsResource, FeesResource, ForbiddenError, InvalidAmountError, JurisdictionBlockedError, KNOWN_EVENT_TYPES, LiveOrderBook, MAX_SERVER_WAIT_MS, MarketsResource, NetworksResource, NotFoundError, OPERATIONS, OrderStateUnknownError, PRIVATE_CHANNELS, PoolsResource, RateLimitError, RateLimiter, SUPPORTED_PROTOCOL_VERSION, ServerError, TradingResource, TypedEmitter, USER_AGENT, UnprocessableError, VERSION, ValidationError, WS_BOOK_DEPTH, WalletResource, assertAmountFields, errorFromResponse, isAmount, isKnownErrorCode, isLocalHost, isRetryable, paginate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cexyio/cexy",
3
- "version": "0.1.0-dev.4",
3
+ "version": "0.1.0-dev.5",
4
4
  "description": "Official TypeScript/JavaScript SDK for the CEXY.io REST and WebSocket API",
5
5
  "license": "MIT",
6
6
  "author": "CEXY.io",