@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 +15 -8
- package/dist/index.cjs +131 -63
- package/dist/index.d.cts +52 -23
- package/dist/index.d.ts +52 -23
- package/dist/index.js +130 -64
- package/package.json +1 -1
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).
|
|
79
|
-
|
|
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
|
|
151
|
-
|
|
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
|
|
161
|
-
it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 (
|
|
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:
|
|
324
|
-
*
|
|
325
|
-
* honours it
|
|
326
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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 }
|
|
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,
|
|
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
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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 (
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
139
|
-
* The server honours it
|
|
140
|
-
* restarts safe
|
|
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:
|
|
188
|
-
*
|
|
189
|
-
* honours it
|
|
190
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
139
|
-
* The server honours it
|
|
140
|
-
* restarts safe
|
|
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:
|
|
188
|
-
*
|
|
189
|
-
* honours it
|
|
190
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
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.
|
|
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.
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 (
|
|
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:
|
|
322
|
-
*
|
|
323
|
-
* honours it
|
|
324
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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 }
|
|
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,
|
|
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
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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 (
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
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.
|
|
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 };
|